blob: 1805f96c2cf5bc8df2285e90ee72f3cb2a15cc72 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.pip.phone;
import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_CLOSE;
import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_FULL;
import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_NONE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.app.IActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Handler;
import android.os.RemoteException;
import android.util.Log;
import android.util.Size;
import android.view.IPinnedStackController;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
import com.android.internal.os.logging.MetricsLoggerWrapper;
import com.android.internal.policy.PipSnapAlgorithm;
import com.android.systemui.R;
import com.android.systemui.shared.system.InputConsumerController;
import com.android.systemui.statusbar.FlingAnimationUtils;
import java.io.PrintWriter;
/**
* Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding
* the PIP.
*/
public class PipTouchHandler {
private static final String TAG = "PipTouchHandler";
// Allow the PIP to be dragged to the edge of the screen to be minimized.
private static final boolean ENABLE_MINIMIZE = false;
// Allow the PIP to be flung from anywhere on the screen to the bottom to be dismissed.
private static final boolean ENABLE_FLING_DISMISS = false;
private static final int SHOW_DISMISS_AFFORDANCE_DELAY = 225;
// Allow dragging the PIP to a location to close it
private static final boolean ENABLE_DISMISS_DRAG_TO_EDGE = true;
private final Context mContext;
private final IActivityManager mActivityManager;
private final ViewConfiguration mViewConfig;
private final PipMenuListener mMenuListener = new PipMenuListener();
private IPinnedStackController mPinnedStackController;
private final PipMenuActivityController mMenuController;
private final PipDismissViewController mDismissViewController;
private final PipSnapAlgorithm mSnapAlgorithm;
private final AccessibilityManager mAccessibilityManager;
private boolean mShowPipMenuOnAnimationEnd = false;
// The current movement bounds
private Rect mMovementBounds = new Rect();
// The reference inset bounds, used to determine the dismiss fraction
private Rect mInsetBounds = new Rect();
// The reference bounds used to calculate the normal/expanded target bounds
private Rect mNormalBounds = new Rect();
private Rect mNormalMovementBounds = new Rect();
private Rect mExpandedBounds = new Rect();
private Rect mExpandedMovementBounds = new Rect();
private int mExpandedShortestEdgeSize;
// Used to workaround an issue where the WM rotation happens before we are notified, allowing
// us to send stale bounds
private int mDeferResizeToNormalBoundsUntilRotation = -1;
private int mDisplayRotation;
private Handler mHandler = new Handler();
private Runnable mShowDismissAffordance = new Runnable() {
@Override
public void run() {
if (ENABLE_DISMISS_DRAG_TO_EDGE) {
mDismissViewController.showDismissTarget();
}
}
};
private ValueAnimator.AnimatorUpdateListener mUpdateScrimListener =
new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
updateDismissFraction();
}
};
// Behaviour states
private int mMenuState = MENU_STATE_NONE;
private boolean mIsMinimized;
private boolean mIsImeShowing;
private int mImeHeight;
private int mImeOffset;
private boolean mIsShelfShowing;
private int mShelfHeight;
private float mSavedSnapFraction = -1f;
private boolean mSendingHoverAccessibilityEvents;
private boolean mMovementWithinMinimize;
private boolean mMovementWithinDismiss;
// Touch state
private final PipTouchState mTouchState;
private final FlingAnimationUtils mFlingAnimationUtils;
private final PipTouchGesture[] mGestures;
private final PipMotionHelper mMotionHelper;
// Temp vars
private final Rect mTmpBounds = new Rect();
/**
* A listener for the PIP menu activity.
*/
private class PipMenuListener implements PipMenuActivityController.Listener {
@Override
public void onPipMenuStateChanged(int menuState, boolean resize) {
setMenuState(menuState, resize);
}
@Override
public void onPipExpand() {
if (!mIsMinimized) {
mMotionHelper.expandPip();
}
}
@Override
public void onPipMinimize() {
setMinimizedStateInternal(true);
mMotionHelper.animateToClosestMinimizedState(mMovementBounds, null /* updateListener */);
}
@Override
public void onPipDismiss() {
MetricsLoggerWrapper.logPictureInPictureDismissByTap(mContext,
PipUtils.getTopPinnedActivity(mContext, mActivityManager));
mMotionHelper.dismissPip();
}
@Override
public void onPipShowMenu() {
mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
mMovementBounds, true /* allowMenuTimeout */, willResizeMenu());
}
}
public PipTouchHandler(Context context, IActivityManager activityManager,
PipMenuActivityController menuController,
InputConsumerController inputConsumerController) {
// Initialize the Pip input consumer
mContext = context;
mActivityManager = activityManager;
mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
mViewConfig = ViewConfiguration.get(context);
mMenuController = menuController;
mMenuController.addListener(mMenuListener);
mDismissViewController = new PipDismissViewController(context);
mSnapAlgorithm = new PipSnapAlgorithm(mContext);
mFlingAnimationUtils = new FlingAnimationUtils(context, 2.5f);
mGestures = new PipTouchGesture[] {
mDefaultMovementGesture
};
mMotionHelper = new PipMotionHelper(mContext, mActivityManager, mMenuController,
mSnapAlgorithm, mFlingAnimationUtils);
mTouchState = new PipTouchState(mViewConfig, mHandler,
() -> mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
mMovementBounds, true /* allowMenuTimeout */, willResizeMenu()));
Resources res = context.getResources();
mExpandedShortestEdgeSize = res.getDimensionPixelSize(
R.dimen.pip_expanded_shortest_edge_size);
mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
// Register the listener for input consumer touch events
inputConsumerController.setTouchListener(this::handleTouchEvent);
inputConsumerController.setRegistrationListener(this::onRegistrationChanged);
onRegistrationChanged(inputConsumerController.isRegistered());
}
public void setTouchEnabled(boolean enabled) {
mTouchState.setAllowTouches(enabled);
}
public void showPictureInPictureMenu() {
// Only show the menu if the user isn't currently interacting with the PiP
if (!mTouchState.isUserInteracting()) {
mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
mMovementBounds, false /* allowMenuTimeout */, willResizeMenu());
}
}
public void onActivityPinned() {
cleanUp();
mShowPipMenuOnAnimationEnd = true;
}
public void onActivityUnpinned(ComponentName topPipActivity) {
if (topPipActivity == null) {
// Clean up state after the last PiP activity is removed
cleanUp();
}
}
public void onPinnedStackAnimationEnded() {
// Always synchronize the motion helper bounds once PiP animations finish
mMotionHelper.synchronizePinnedStackBounds();
if (mShowPipMenuOnAnimationEnd) {
mMenuController.showMenu(MENU_STATE_CLOSE, mMotionHelper.getBounds(),
mMovementBounds, true /* allowMenuTimeout */, false /* willResizeMenu */);
mShowPipMenuOnAnimationEnd = false;
}
}
public void onConfigurationChanged() {
mMotionHelper.onConfigurationChanged();
mMotionHelper.synchronizePinnedStackBounds();
}
public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
mIsImeShowing = imeVisible;
mImeHeight = imeHeight;
}
public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) {
mIsShelfShowing = shelfVisible;
mShelfHeight = shelfHeight;
}
public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect animatingBounds,
boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation) {
final int bottomOffset = mIsImeShowing ? mImeHeight : 0;
// Re-calculate the expanded bounds
mNormalBounds = normalBounds;
Rect normalMovementBounds = new Rect();
mSnapAlgorithm.getMovementBounds(mNormalBounds, insetBounds, normalMovementBounds,
bottomOffset);
// Calculate the expanded size
float aspectRatio = (float) normalBounds.width() / normalBounds.height();
Point displaySize = new Point();
mContext.getDisplay().getRealSize(displaySize);
Size expandedSize = mSnapAlgorithm.getSizeForAspectRatio(aspectRatio,
mExpandedShortestEdgeSize, displaySize.x, displaySize.y);
mExpandedBounds.set(0, 0, expandedSize.getWidth(), expandedSize.getHeight());
Rect expandedMovementBounds = new Rect();
mSnapAlgorithm.getMovementBounds(mExpandedBounds, insetBounds, expandedMovementBounds,
bottomOffset);
// If this is from an IME or shelf adjustment, then we should move the PiP so that it is not
// occluded by the IME or shelf.
if (fromImeAdjustment || fromShelfAdjustment) {
if (mTouchState.isUserInteracting()) {
// Defer the update of the current movement bounds until after the user finishes
// touching the screen
} else {
final int adjustedOffset = Math.max(mIsImeShowing ? mImeHeight + mImeOffset : 0,
mIsShelfShowing ? mShelfHeight : 0);
Rect normalAdjustedBounds = new Rect();
mSnapAlgorithm.getMovementBounds(mNormalBounds, insetBounds, normalAdjustedBounds,
adjustedOffset);
Rect expandedAdjustedBounds = new Rect();
mSnapAlgorithm.getMovementBounds(mExpandedBounds, insetBounds,
expandedAdjustedBounds, adjustedOffset);
final Rect toAdjustedBounds = mMenuState == MENU_STATE_FULL
? expandedAdjustedBounds
: normalAdjustedBounds;
final Rect toMovementBounds = mMenuState == MENU_STATE_FULL
? expandedMovementBounds
: normalMovementBounds;
// If the PIP window needs to shift to right above shelf/IME and it's already above
// that, don't move the PIP window.
if (toAdjustedBounds.bottom < mMovementBounds.bottom
&& animatingBounds.top < toAdjustedBounds.bottom) {
return;
}
// If the PIP window needs to shift down due to dismissal of shelf/IME but it's way
// above the position as if shelf/IME shows, don't move the PIP window.
int movementBoundsAdjustment = toMovementBounds.bottom - mMovementBounds.bottom;
int offsetAdjustment = fromImeAdjustment ? mImeOffset : mShelfHeight;
if (toAdjustedBounds.bottom >= mMovementBounds.bottom
&& animatingBounds.top
< toAdjustedBounds.bottom - movementBoundsAdjustment - offsetAdjustment) {
return;
}
animateToOffset(animatingBounds, toAdjustedBounds);
}
}
// Update the movement bounds after doing the calculations based on the old movement bounds
// above
mNormalMovementBounds = normalMovementBounds;
mExpandedMovementBounds = expandedMovementBounds;
mDisplayRotation = displayRotation;
mInsetBounds.set(insetBounds);
updateMovementBounds(mMenuState);
// If we have a deferred resize, apply it now
if (mDeferResizeToNormalBoundsUntilRotation == displayRotation) {
mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction,
mNormalMovementBounds, mMovementBounds, mIsMinimized,
true /* immediate */);
mSavedSnapFraction = -1f;
mDeferResizeToNormalBoundsUntilRotation = -1;
}
}
private void animateToOffset(Rect animatingBounds, Rect toAdjustedBounds) {
final Rect bounds = new Rect(animatingBounds);
bounds.offset(0, toAdjustedBounds.bottom - bounds.top);
// In landscape mode, PIP window can go offset while launching IME. We want to align the
// the top of the PIP window with the top of the movement bounds in that case.
bounds.offset(0, Math.max(0, mMovementBounds.top - bounds.top));
mMotionHelper.animateToOffset(bounds);
}
private void onRegistrationChanged(boolean isRegistered) {
mAccessibilityManager.setPictureInPictureActionReplacingConnection(isRegistered
? new PipAccessibilityInteractionConnection(mMotionHelper,
this::onAccessibilityShowMenu, mHandler) : null);
if (!isRegistered && mTouchState.isUserInteracting()) {
// If the input consumer is unregistered while the user is interacting, then we may not
// get the final TOUCH_UP event, so clean up the dismiss target as well
cleanUpDismissTarget();
}
}
private void onAccessibilityShowMenu() {
mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
mMovementBounds, false /* allowMenuTimeout */, willResizeMenu());
}
private boolean handleTouchEvent(MotionEvent ev) {
// Skip touch handling until we are bound to the controller
if (mPinnedStackController == null) {
return true;
}
// Update the touch state
mTouchState.onTouchEvent(ev);
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
mMotionHelper.synchronizePinnedStackBounds();
for (PipTouchGesture gesture : mGestures) {
gesture.onDown(mTouchState);
}
break;
}
case MotionEvent.ACTION_MOVE: {
for (PipTouchGesture gesture : mGestures) {
if (gesture.onMove(mTouchState)) {
break;
}
}
break;
}
case MotionEvent.ACTION_UP: {
// Update the movement bounds again if the state has changed since the user started
// dragging (ie. when the IME shows)
updateMovementBounds(mMenuState);
for (PipTouchGesture gesture : mGestures) {
if (gesture.onUp(mTouchState)) {
break;
}
}
// Fall through to clean up
}
case MotionEvent.ACTION_CANCEL: {
mTouchState.reset();
break;
}
case MotionEvent.ACTION_HOVER_ENTER:
case MotionEvent.ACTION_HOVER_MOVE: {
if (mAccessibilityManager.isEnabled() && !mSendingHoverAccessibilityEvents) {
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
event.setImportantForAccessibility(true);
event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID);
event.setWindowId(
AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID);
mAccessibilityManager.sendAccessibilityEvent(event);
mSendingHoverAccessibilityEvents = true;
}
break;
}
case MotionEvent.ACTION_HOVER_EXIT: {
if (mAccessibilityManager.isEnabled() && mSendingHoverAccessibilityEvents) {
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
event.setImportantForAccessibility(true);
event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID);
event.setWindowId(
AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID);
mAccessibilityManager.sendAccessibilityEvent(event);
mSendingHoverAccessibilityEvents = false;
}
break;
}
}
return mMenuState == MENU_STATE_NONE;
}
/**
* Updates the appearance of the menu and scrim on top of the PiP while dismissing.
*/
private void updateDismissFraction() {
// Skip updating the dismiss fraction when the IME is showing. This is to work around an
// issue where starting the menu activity for the dismiss overlay will steal the window
// focus, which closes the IME.
if (mMenuController != null && !mIsImeShowing) {
Rect bounds = mMotionHelper.getBounds();
final float target = mInsetBounds.bottom;
float fraction = 0f;
if (bounds.bottom > target) {
final float distance = bounds.bottom - target;
fraction = Math.min(distance / bounds.height(), 1f);
}
if (Float.compare(fraction, 0f) != 0 || mMenuController.isMenuActivityVisible()) {
// Update if the fraction > 0, or if fraction == 0 and the menu was already visible
mMenuController.setDismissFraction(fraction);
}
}
}
/**
* Sets the controller to update the system of changes from user interaction.
*/
void setPinnedStackController(IPinnedStackController controller) {
mPinnedStackController = controller;
}
/**
* Sets the minimized state.
*/
private void setMinimizedStateInternal(boolean isMinimized) {
if (!ENABLE_MINIMIZE) {
return;
}
setMinimizedState(isMinimized, false /* fromController */);
}
/**
* Sets the minimized state.
*/
void setMinimizedState(boolean isMinimized, boolean fromController) {
if (!ENABLE_MINIMIZE) {
return;
}
if (mIsMinimized != isMinimized) {
MetricsLoggerWrapper.logPictureInPictureMinimize(mContext,
isMinimized, PipUtils.getTopPinnedActivity(mContext, mActivityManager));
}
mIsMinimized = isMinimized;
mSnapAlgorithm.setMinimized(isMinimized);
if (fromController) {
if (isMinimized) {
// Move the PiP to the new bounds immediately if minimized
mMotionHelper.movePip(mMotionHelper.getClosestMinimizedBounds(mNormalBounds,
mMovementBounds));
}
} else if (mPinnedStackController != null) {
try {
mPinnedStackController.setIsMinimized(isMinimized);
} catch (RemoteException e) {
Log.e(TAG, "Could not set minimized state", e);
}
}
}
/**
* Sets the menu visibility.
*/
private void setMenuState(int menuState, boolean resize) {
if (menuState == MENU_STATE_FULL) {
// Save the current snap fraction and if we do not drag or move the PiP, then
// we store back to this snap fraction. Otherwise, we'll reset the snap
// fraction and snap to the closest edge
Rect expandedBounds = new Rect(mExpandedBounds);
if (resize) {
mSavedSnapFraction = mMotionHelper.animateToExpandedState(expandedBounds,
mMovementBounds, mExpandedMovementBounds);
}
} else if (menuState == MENU_STATE_NONE) {
// Try and restore the PiP to the closest edge, using the saved snap fraction
// if possible
if (resize) {
if (mDeferResizeToNormalBoundsUntilRotation == -1) {
// This is a very special case: when the menu is expanded and visible,
// navigating to another activity can trigger auto-enter PiP, and if the
// revealed activity has a forced rotation set, then the controller will get
// updated with the new rotation of the display. However, at the same time,
// SystemUI will try to hide the menu by creating an animation to the normal
// bounds which are now stale. In such a case we defer the animation to the
// normal bounds until after the next onMovementBoundsChanged() call to get the
// bounds in the new orientation
try {
int displayRotation = mPinnedStackController.getDisplayRotation();
if (mDisplayRotation != displayRotation) {
mDeferResizeToNormalBoundsUntilRotation = displayRotation;
}
} catch (RemoteException e) {
Log.e(TAG, "Could not get display rotation from controller");
}
}
if (mDeferResizeToNormalBoundsUntilRotation == -1) {
Rect normalBounds = new Rect(mNormalBounds);
mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction,
mNormalMovementBounds, mMovementBounds, mIsMinimized,
false /* immediate */);
mSavedSnapFraction = -1f;
}
} else {
// If resizing is not allowed, then the PiP should be frozen until the transition
// ends as well
setTouchEnabled(false);
mSavedSnapFraction = -1f;
}
}
mMenuState = menuState;
updateMovementBounds(menuState);
if (menuState != MENU_STATE_CLOSE) {
MetricsLoggerWrapper.logPictureInPictureMenuVisible(mContext, menuState == MENU_STATE_FULL);
}
}
/**
* @return the motion helper.
*/
public PipMotionHelper getMotionHelper() {
return mMotionHelper;
}
/**
* Gesture controlling normal movement of the PIP.
*/
private PipTouchGesture mDefaultMovementGesture = new PipTouchGesture() {
// Whether the PiP was on the left side of the screen at the start of the gesture
private boolean mStartedOnLeft;
private final Point mStartPosition = new Point();
private final PointF mDelta = new PointF();
@Override
public void onDown(PipTouchState touchState) {
if (!touchState.isUserInteracting()) {
return;
}
Rect bounds = mMotionHelper.getBounds();
mDelta.set(0f, 0f);
mStartPosition.set(bounds.left, bounds.top);
mStartedOnLeft = bounds.left < mMovementBounds.centerX();
mMovementWithinMinimize = true;
mMovementWithinDismiss = touchState.getDownTouchPosition().y >= mMovementBounds.bottom;
// If the menu is still visible, and we aren't minimized, then just poke the menu
// so that it will timeout after the user stops touching it
if (mMenuState != MENU_STATE_NONE && !mIsMinimized) {
mMenuController.pokeMenu();
}
if (ENABLE_DISMISS_DRAG_TO_EDGE) {
mDismissViewController.createDismissTarget();
mHandler.postDelayed(mShowDismissAffordance, SHOW_DISMISS_AFFORDANCE_DELAY);
}
}
@Override
boolean onMove(PipTouchState touchState) {
if (!touchState.isUserInteracting()) {
return false;
}
if (touchState.startedDragging()) {
mSavedSnapFraction = -1f;
if (ENABLE_DISMISS_DRAG_TO_EDGE) {
mHandler.removeCallbacks(mShowDismissAffordance);
mDismissViewController.showDismissTarget();
}
}
if (touchState.isDragging()) {
// Move the pinned stack freely
final PointF lastDelta = touchState.getLastTouchDelta();
float lastX = mStartPosition.x + mDelta.x;
float lastY = mStartPosition.y + mDelta.y;
float left = lastX + lastDelta.x;
float top = lastY + lastDelta.y;
if (!touchState.allowDraggingOffscreen() || !ENABLE_MINIMIZE) {
left = Math.max(mMovementBounds.left, Math.min(mMovementBounds.right, left));
}
if (ENABLE_DISMISS_DRAG_TO_EDGE) {
// Allow pip to move past bottom bounds
top = Math.max(mMovementBounds.top, top);
} else {
top = Math.max(mMovementBounds.top, Math.min(mMovementBounds.bottom, top));
}
// Add to the cumulative delta after bounding the position
mDelta.x += left - lastX;
mDelta.y += top - lastY;
mTmpBounds.set(mMotionHelper.getBounds());
mTmpBounds.offsetTo((int) left, (int) top);
mMotionHelper.movePip(mTmpBounds);
if (ENABLE_DISMISS_DRAG_TO_EDGE) {
updateDismissFraction();
}
final PointF curPos = touchState.getLastTouchPosition();
if (mMovementWithinMinimize) {
// Track if movement remains near starting edge to identify swipes to minimize
mMovementWithinMinimize = mStartedOnLeft
? curPos.x <= mMovementBounds.left + mTmpBounds.width()
: curPos.x >= mMovementBounds.right;
}
if (mMovementWithinDismiss) {
// Track if movement remains near the bottom edge to identify swipe to dismiss
mMovementWithinDismiss = curPos.y >= mMovementBounds.bottom;
}
return true;
}
return false;
}
@Override
public boolean onUp(PipTouchState touchState) {
if (ENABLE_DISMISS_DRAG_TO_EDGE) {
// Clean up the dismiss target regardless of the touch state in case the touch
// enabled state changes while the user is interacting
cleanUpDismissTarget();
}
if (!touchState.isUserInteracting()) {
return false;
}
final PointF vel = touchState.getVelocity();
final boolean isHorizontal = Math.abs(vel.x) > Math.abs(vel.y);
final float velocity = PointF.length(vel.x, vel.y);
final boolean isFling = velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond();
final boolean isUpWithinDimiss = ENABLE_FLING_DISMISS
&& touchState.getLastTouchPosition().y >= mMovementBounds.bottom
&& mMotionHelper.isGestureToDismissArea(mMotionHelper.getBounds(), vel.x,
vel.y, isFling);
final boolean isFlingToBot = isFling && vel.y > 0 && !isHorizontal
&& (mMovementWithinDismiss || isUpWithinDimiss);
if (ENABLE_DISMISS_DRAG_TO_EDGE) {
// Check if the user dragged or flung the PiP offscreen to dismiss it
if (mMotionHelper.shouldDismissPip() || isFlingToBot) {
MetricsLoggerWrapper.logPictureInPictureDismissByDrag(mContext,
PipUtils.getTopPinnedActivity(mContext, mActivityManager));
mMotionHelper.animateDismiss(mMotionHelper.getBounds(), vel.x,
vel.y, mUpdateScrimListener);
return true;
}
}
if (touchState.isDragging()) {
final boolean isFlingToEdge = isFling && isHorizontal && mMovementWithinMinimize
&& (mStartedOnLeft ? vel.x < 0 : vel.x > 0);
if (ENABLE_MINIMIZE &&
!mIsMinimized && (mMotionHelper.shouldMinimizePip() || isFlingToEdge)) {
// Pip should be minimized
setMinimizedStateInternal(true);
if (mMenuState == MENU_STATE_FULL) {
// If the user dragged the expanded PiP to the edge, then hiding the menu
// will trigger the PiP to be scaled back to the normal size with the
// minimize offset adjusted
mMenuController.hideMenu();
} else {
mMotionHelper.animateToClosestMinimizedState(mMovementBounds,
mUpdateScrimListener);
}
return true;
}
if (mIsMinimized) {
// If we're dragging and it wasn't a minimize gesture then we shouldn't be
// minimized.
setMinimizedStateInternal(false);
}
AnimatorListenerAdapter postAnimationCallback = null;
if (mMenuState != MENU_STATE_NONE) {
// If the menu is still visible, and we aren't minimized, then just poke the
// menu so that it will timeout after the user stops touching it
mMenuController.showMenu(mMenuState, mMotionHelper.getBounds(),
mMovementBounds, true /* allowMenuTimeout */, willResizeMenu());
} else {
// If the menu is not visible, then we can still be showing the activity for the
// dismiss overlay, so just finish it after the animation completes
postAnimationCallback = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mMenuController.hideMenu();
}
};
}
if (isFling) {
mMotionHelper.flingToSnapTarget(velocity, vel.x, vel.y, mMovementBounds,
mUpdateScrimListener, postAnimationCallback,
mStartPosition);
} else {
mMotionHelper.animateToClosestSnapTarget(mMovementBounds, mUpdateScrimListener,
postAnimationCallback);
}
} else if (mIsMinimized) {
// This was a tap, so no longer minimized
mMotionHelper.animateToClosestSnapTarget(mMovementBounds, null /* updateListener */,
null /* animatorListener */);
setMinimizedStateInternal(false);
} else if (mMenuState != MENU_STATE_FULL) {
if (mTouchState.isDoubleTap()) {
// Expand to fullscreen if this is a double tap
mMotionHelper.expandPip();
} else if (!mTouchState.isWaitingForDoubleTap()) {
// User has stalled long enough for this not to be a drag or a double tap, just
// expand the menu
mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
mMovementBounds, true /* allowMenuTimeout */, willResizeMenu());
} else {
// Next touch event _may_ be the second tap for the double-tap, schedule a
// fallback runnable to trigger the menu if no touch event occurs before the
// next tap
mTouchState.scheduleDoubleTapTimeoutCallback();
}
} else {
mMenuController.hideMenu();
mMotionHelper.expandPip();
}
return true;
}
};
/**
* Updates the current movement bounds based on whether the menu is currently visible.
*/
private void updateMovementBounds(int menuState) {
boolean isMenuExpanded = menuState == MENU_STATE_FULL;
mMovementBounds = isMenuExpanded
? mExpandedMovementBounds
: mNormalMovementBounds;
try {
mPinnedStackController.setMinEdgeSize(isMenuExpanded ? mExpandedShortestEdgeSize : 0);
} catch (RemoteException e) {
Log.e(TAG, "Could not set minimized state", e);
}
}
/**
* Removes the dismiss target and cancels any pending callbacks to show it.
*/
private void cleanUpDismissTarget() {
mHandler.removeCallbacks(mShowDismissAffordance);
mDismissViewController.destroyDismissTarget();
}
/**
* Resets some states related to the touch handling.
*/
private void cleanUp() {
if (mIsMinimized) {
setMinimizedStateInternal(false);
}
cleanUpDismissTarget();
}
/**
* @return whether the menu will resize as a part of showing the full menu.
*/
private boolean willResizeMenu() {
return mExpandedBounds.width() != mNormalBounds.width() ||
mExpandedBounds.height() != mNormalBounds.height();
}
public void dump(PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);
pw.println(innerPrefix + "mMovementBounds=" + mMovementBounds);
pw.println(innerPrefix + "mNormalBounds=" + mNormalBounds);
pw.println(innerPrefix + "mNormalMovementBounds=" + mNormalMovementBounds);
pw.println(innerPrefix + "mExpandedBounds=" + mExpandedBounds);
pw.println(innerPrefix + "mExpandedMovementBounds=" + mExpandedMovementBounds);
pw.println(innerPrefix + "mMenuState=" + mMenuState);
pw.println(innerPrefix + "mIsMinimized=" + mIsMinimized);
pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing);
pw.println(innerPrefix + "mImeHeight=" + mImeHeight);
pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing);
pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight);
pw.println(innerPrefix + "mSavedSnapFraction=" + mSavedSnapFraction);
pw.println(innerPrefix + "mEnableDragToEdgeDismiss=" + ENABLE_DISMISS_DRAG_TO_EDGE);
pw.println(innerPrefix + "mEnableMinimize=" + ENABLE_MINIMIZE);
mSnapAlgorithm.dump(pw, innerPrefix);
mTouchState.dump(pw, innerPrefix);
mMotionHelper.dump(pw, innerPrefix);
}
}