PipTouchHandler.java revision 114aeea51677345daca89c392adaef84d4499bd9
1c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk/* 2f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz * Copyright (C) 2016 The Android Open Source Project 3c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * 4c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * Licensed under the Apache License, Version 2.0 (the "License"); 5c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * you may not use this file except in compliance with the License. 6c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * You may obtain a copy of the License at 7c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * 8f7a9eea8fe577f2f5edbbe6e73891a54351286c6Benjamin Franz * http://www.apache.org/licenses/LICENSE-2.0 9c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * 10c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * Unless required by applicable law or agreed to in writing, software 11c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * distributed under the License is distributed on an "AS IS" BASIS, 12c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * See the License for the specific language governing permissions and 14c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * limitations under the License. 15c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk */ 16c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk 17f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzpackage com.android.systemui.pip.phone; 18c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk 19808bdc569c7da3a587696d228c645db7bd82b2c5Lorenzo Colittiimport static android.app.ActivityManager.StackId.PINNED_STACK_ID; 20f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport static android.view.WindowManager.INPUT_CONSUMER_PIP; 21c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk 22c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport static com.android.systemui.Interpolators.FAST_OUT_LINEAR_IN; 23c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN; 24f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport static com.android.systemui.Interpolators.LINEAR_OUT_SLOW_IN; 25f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz 268e1c5db7598f647c9a4c4d0b46776ac142679538Rubin Xuimport android.animation.Animator; 27c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport android.animation.AnimatorListenerAdapter; 28f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.animation.ValueAnimator; 29f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.animation.ValueAnimator.AnimatorUpdateListener; 30c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport android.app.ActivityManager.StackInfo; 31f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.app.IActivityManager; 32c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport android.content.Context; 33f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.graphics.Point; 34f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.graphics.PointF; 35f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.graphics.Rect; 36f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.os.Looper; 37f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.os.RemoteException; 38f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.util.Log; 39c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport android.view.IPinnedStackController; 40c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport android.view.IWindowManager; 41f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.view.InputChannel; 42f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.view.InputEvent; 43c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport android.view.InputEventReceiver; 44f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.view.MotionEvent; 45c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport android.view.ViewConfiguration; 46f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz 47c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport com.android.internal.os.BackgroundThread; 48f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport com.android.internal.policy.PipMotionHelper; 496d90275bb14ddd11841be54b42c603e10cf93a7fJulia Reynoldsimport com.android.internal.policy.PipSnapAlgorithm; 50f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport com.android.systemui.statusbar.FlingAnimationUtils; 51c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport com.android.systemui.tuner.TunerService; 52c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk 53f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz/** 54c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding 55c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * the PIP. 56f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz */ 57f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzpublic class PipTouchHandler implements TunerService.Tunable { 58f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz private static final String TAG = "PipTouchHandler"; 59f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz private static final boolean DEBUG_ALLOW_OUT_OF_BOUNDS_STACK = false; 60f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz 61c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private static final String TUNER_KEY_DRAG_TO_DISMISS = "pip_drag_to_dismiss"; 62c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private static final String TUNER_KEY_ALLOW_MINIMIZE = "pip_allow_minimize"; 63c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk 64f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz private static final int SNAP_STACK_DURATION = 225; 65f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz private static final int DISMISS_STACK_DURATION = 375; 66f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz private static final int EXPAND_STACK_DURATION = 225; 67f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz private static final int MINIMIZE_STACK_MAX_DURATION = 200; 68f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz 69f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz // The fraction of the stack width that the user has to drag offscreen to minimize the PIP 70f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz private static final float MINIMIZE_OFFSCREEN_FRACTION = 0.15f; 71c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk 72c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private final Context mContext; 73f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz private final IActivityManager mActivityManager; 74c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private final IWindowManager mWindowManager; 75c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private final ViewConfiguration mViewConfig; 76c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private final PipMenuListener mMenuListener = new PipMenuListener(); 77c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private IPinnedStackController mPinnedStackController; 78c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk 79c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private PipInputEventReceiver mInputEventReceiver; 80c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private PipMenuActivityController mMenuController; 81c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private PipDismissViewController mDismissViewController; 82c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private final PipSnapAlgorithm mSnapAlgorithm; 83c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private PipMotionHelper mMotionHelper; 84c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk 85c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk // Allow dragging the PIP to a location to close it 86c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private boolean mEnableDragToDismiss = false; 87f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz // Allow the PIP to be "docked" slightly offscreen 88c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private boolean mEnableMinimizing = true; 89c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk 90c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private final Rect mStableInsets = new Rect(); 91c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private final Rect mPinnedStackBounds = new Rect(); 92c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private final Rect mBoundedPinnedStackBounds = new Rect(); 93c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private ValueAnimator mPinnedStackBoundsAnimator = null; 94c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private ValueAnimator.AnimatorUpdateListener mUpdatePinnedStackBoundsListener = 95c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk new AnimatorUpdateListener() { 96c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk @Override 97c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk public void onAnimationUpdate(ValueAnimator animation) { 98c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk mPinnedStackBounds.set((Rect) animation.getAnimatedValue()); 99c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk } 100c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk }; 101c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk 102c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk // Behaviour states 103c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private boolean mIsTappingThrough; 104c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private boolean mIsMinimized; 1052daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk 1062daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk // Touch state 107c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private final PipTouchState mTouchState; 108c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private final FlingAnimationUtils mFlingAnimationUtils; 1092daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk private final PipTouchGesture[] mGestures; 1102daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk 1112daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk // Temporary vars 1122daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk private final Rect mTmpBounds = new Rect(); 1132daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk 1142daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk /** 1152daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk * Input handler used for Pip windows. 116c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk */ 117c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk private final class PipInputEventReceiver extends InputEventReceiver { 118 119 public PipInputEventReceiver(InputChannel inputChannel, Looper looper) { 120 super(inputChannel, looper); 121 } 122 123 @Override 124 public void onInputEvent(InputEvent event) { 125 boolean handled = true; 126 try { 127 // To be implemented for input handling over Pip windows 128 if (event instanceof MotionEvent) { 129 MotionEvent ev = (MotionEvent) event; 130 handled = handleTouchEvent(ev); 131 } 132 } finally { 133 finishInputEvent(event, handled); 134 } 135 } 136 } 137 138 /** 139 * A listener for the PIP menu activity. 140 */ 141 private class PipMenuListener implements PipMenuActivityController.Listener { 142 @Override 143 public void onPipMenuVisibilityChanged(boolean visible) { 144 if (!visible) { 145 mIsTappingThrough = false; 146 registerInputConsumer(); 147 } else { 148 unregisterInputConsumer(); 149 } 150 } 151 152 @Override 153 public void onPipExpand() { 154 if (!mIsMinimized) { 155 expandPinnedStackToFullscreen(); 156 } 157 } 158 159 @Override 160 public void onPipMinimize() { 161 setMinimizedState(true); 162 animateToClosestMinimizedTarget(); 163 } 164 165 @Override 166 public void onPipDismiss() { 167 BackgroundThread.getHandler().post(PipTouchHandler.this::dismissPinnedStack); 168 } 169 } 170 171 public PipTouchHandler(Context context, PipMenuActivityController menuController, 172 IActivityManager activityManager, IWindowManager windowManager) { 173 174 // Initialize the Pip input consumer 175 mContext = context; 176 mActivityManager = activityManager; 177 mWindowManager = windowManager; 178 mViewConfig = ViewConfiguration.get(context); 179 mMenuController = menuController; 180 mMenuController.addListener(mMenuListener); 181 mDismissViewController = new PipDismissViewController(context); 182 mSnapAlgorithm = new PipSnapAlgorithm(mContext); 183 mTouchState = new PipTouchState(mViewConfig); 184 mFlingAnimationUtils = new FlingAnimationUtils(context, 2f); 185 mGestures = new PipTouchGesture[] { 186 mDragToDismissGesture, mTapThroughGesture, mMinimizeGesture, mDefaultMovementGesture 187 }; 188 mMotionHelper = new PipMotionHelper(BackgroundThread.getHandler()); 189 registerInputConsumer(); 190 setSnapToEdge(true); 191 192 // Register any tuner settings changes 193 TunerService.get(context).addTunable(this, TUNER_KEY_DRAG_TO_DISMISS, 194 TUNER_KEY_ALLOW_MINIMIZE); 195 } 196 197 @Override 198 public void onTuningChanged(String key, String newValue) { 199 if (newValue == null) { 200 // Reset back to default 201 mEnableDragToDismiss = false; 202 mEnableMinimizing = true; 203 setMinimizedState(false); 204 return; 205 } 206 switch (key) { 207 case TUNER_KEY_DRAG_TO_DISMISS: 208 mEnableDragToDismiss = Integer.parseInt(newValue) != 0; 209 break; 210 case TUNER_KEY_ALLOW_MINIMIZE: 211 mEnableMinimizing = Integer.parseInt(newValue) != 0; 212 break; 213 } 214 } 215 216 public void onActivityPinned() { 217 // Reset some states once we are pinned 218 if (mIsTappingThrough) { 219 mIsTappingThrough = false; 220 registerInputConsumer(); 221 } 222 if (mIsMinimized) { 223 setMinimizedState(false); 224 } 225 } 226 227 public void onConfigurationChanged() { 228 mSnapAlgorithm.onConfigurationChanged(); 229 updateBoundedPinnedStackBounds(false /* updatePinnedStackBounds */); 230 } 231 232 public void onMinimizedStateChanged(boolean isMinimized) { 233 mIsMinimized = isMinimized; 234 } 235 236 public void onSnapToEdgeStateChanged(boolean isSnapToEdge) { 237 mSnapAlgorithm.setSnapToEdge(isSnapToEdge); 238 } 239 240 private boolean handleTouchEvent(MotionEvent ev) { 241 // Skip touch handling until we are bound to the controller 242 if (mPinnedStackController == null) { 243 return true; 244 } 245 246 // Update the touch state 247 mTouchState.onTouchEvent(ev); 248 249 switch (ev.getAction()) { 250 case MotionEvent.ACTION_DOWN: { 251 // Cancel any existing animations on the pinned stack 252 if (mPinnedStackBoundsAnimator != null) { 253 mPinnedStackBoundsAnimator.cancel(); 254 } 255 256 updateBoundedPinnedStackBounds(true /* updatePinnedStackBounds */); 257 for (PipTouchGesture gesture : mGestures) { 258 gesture.onDown(mTouchState); 259 } 260 try { 261 mPinnedStackController.setInInteractiveMode(true); 262 } catch (RemoteException e) { 263 Log.e(TAG, "Could not set dragging state", e); 264 } 265 break; 266 } 267 case MotionEvent.ACTION_MOVE: { 268 for (PipTouchGesture gesture : mGestures) { 269 if (gesture.onMove(mTouchState)) { 270 break; 271 } 272 } 273 break; 274 } 275 case MotionEvent.ACTION_UP: { 276 // Update the movement bounds again if the state has changed since the user started 277 // dragging (ie. when the IME shows) 278 updateBoundedPinnedStackBounds(false /* updatePinnedStackBounds */); 279 280 for (PipTouchGesture gesture : mGestures) { 281 if (gesture.onUp(mTouchState)) { 282 break; 283 } 284 } 285 286 // Fall through to clean up 287 } 288 case MotionEvent.ACTION_CANCEL: { 289 try { 290 mPinnedStackController.setInInteractiveMode(false); 291 } catch (RemoteException e) { 292 Log.e(TAG, "Could not set dragging state", e); 293 } 294 break; 295 } 296 } 297 return !mIsTappingThrough; 298 } 299 300 /** 301 * @return whether the current touch state is a horizontal drag offscreen. 302 */ 303 private boolean isDraggingOffscreen(PipTouchState touchState) { 304 PointF lastDelta = touchState.getLastTouchDelta(); 305 PointF downDelta = touchState.getDownTouchDelta(); 306 float left = mPinnedStackBounds.left + lastDelta.x; 307 return !(mBoundedPinnedStackBounds.left <= left && left <= mBoundedPinnedStackBounds.right) 308 && Math.abs(downDelta.x) > Math.abs(downDelta.y); 309 } 310 311 /** 312 * Registers the input consumer. 313 */ 314 private void registerInputConsumer() { 315 if (mInputEventReceiver == null) { 316 final InputChannel inputChannel = new InputChannel(); 317 try { 318 mWindowManager.destroyInputConsumer(INPUT_CONSUMER_PIP); 319 mWindowManager.createInputConsumer(INPUT_CONSUMER_PIP, inputChannel); 320 } catch (RemoteException e) { 321 Log.e(TAG, "Failed to create PIP input consumer", e); 322 } 323 mInputEventReceiver = new PipInputEventReceiver(inputChannel, Looper.myLooper()); 324 } 325 } 326 327 /** 328 * Unregisters the input consumer. 329 */ 330 private void unregisterInputConsumer() { 331 if (mInputEventReceiver != null) { 332 try { 333 mWindowManager.destroyInputConsumer(INPUT_CONSUMER_PIP); 334 } catch (RemoteException e) { 335 Log.e(TAG, "Failed to destroy PIP input consumer", e); 336 } 337 mInputEventReceiver.dispose(); 338 mInputEventReceiver = null; 339 } 340 } 341 342 /** 343 * Sets the controller to update the system of changes from user interaction. 344 */ 345 void setPinnedStackController(IPinnedStackController controller) { 346 mPinnedStackController = controller; 347 } 348 349 /** 350 * Sets the snap-to-edge state and notifies the controller. 351 */ 352 private void setSnapToEdge(boolean snapToEdge) { 353 onSnapToEdgeStateChanged(snapToEdge); 354 355 if (mPinnedStackController != null) { 356 try { 357 mPinnedStackController.setSnapToEdge(snapToEdge); 358 } catch (RemoteException e) { 359 Log.e(TAG, "Could not set snap mode to edge", e); 360 } 361 } 362 } 363 364 /** 365 * Sets the minimized state and notifies the controller. 366 */ 367 private void setMinimizedState(boolean isMinimized) { 368 onMinimizedStateChanged(isMinimized); 369 370 if (mPinnedStackController != null) { 371 try { 372 mPinnedStackController.setIsMinimized(isMinimized); 373 } catch (RemoteException e) { 374 Log.e(TAG, "Could not set minimized state", e); 375 } 376 } 377 } 378 379 /** 380 * @return whether the given {@param pinnedStackBounds} indicates the PIP should be minimized. 381 */ 382 private boolean shouldMinimizedPinnedStack() { 383 Point displaySize = new Point(); 384 mContext.getDisplay().getRealSize(displaySize); 385 if (mPinnedStackBounds.left < 0) { 386 float offscreenFraction = (float) -mPinnedStackBounds.left / mPinnedStackBounds.width(); 387 return offscreenFraction >= MINIMIZE_OFFSCREEN_FRACTION; 388 } else if (mPinnedStackBounds.right > displaySize.x) { 389 float offscreenFraction = (float) (mPinnedStackBounds.right - displaySize.x) / 390 mPinnedStackBounds.width(); 391 return offscreenFraction >= MINIMIZE_OFFSCREEN_FRACTION; 392 } else { 393 return false; 394 } 395 } 396 397 /** 398 * Flings the minimized PIP to the closest minimized snap target. 399 */ 400 private void flingToMinimizedSnapTarget(float velocityY) { 401 // We currently only allow flinging the minimized stack up and down, so just lock the 402 // movement bounds to the current stack bounds horizontally 403 Rect movementBounds = new Rect(mPinnedStackBounds.left, mBoundedPinnedStackBounds.top, 404 mPinnedStackBounds.left, mBoundedPinnedStackBounds.bottom); 405 Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, mPinnedStackBounds, 406 0 /* velocityX */, velocityY); 407 if (!mPinnedStackBounds.equals(toBounds)) { 408 mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds, 409 toBounds, 0, FAST_OUT_SLOW_IN, mUpdatePinnedStackBoundsListener); 410 mFlingAnimationUtils.apply(mPinnedStackBoundsAnimator, 0, 411 distanceBetweenRectOffsets(mPinnedStackBounds, toBounds), 412 velocityY); 413 mPinnedStackBoundsAnimator.start(); 414 } 415 } 416 417 /** 418 * Animates the PIP to the minimized state, slightly offscreen. 419 */ 420 private void animateToClosestMinimizedTarget() { 421 Point displaySize = new Point(); 422 mContext.getDisplay().getRealSize(displaySize); 423 Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(mBoundedPinnedStackBounds, 424 mPinnedStackBounds); 425 mSnapAlgorithm.applyMinimizedOffset(toBounds, mBoundedPinnedStackBounds, displaySize, 426 mStableInsets); 427 mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds, 428 toBounds, MINIMIZE_STACK_MAX_DURATION, LINEAR_OUT_SLOW_IN, 429 mUpdatePinnedStackBoundsListener); 430 mPinnedStackBoundsAnimator.addListener(new AnimatorListenerAdapter() { 431 @Override 432 public void onAnimationEnd(Animator animation) { 433 mMenuController.hideMenu(); 434 } 435 }); 436 mPinnedStackBoundsAnimator.start(); 437 } 438 439 /** 440 * Flings the PIP to the closest snap target. 441 */ 442 private void flingToSnapTarget(float velocity, float velocityX, float velocityY) { 443 Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(mBoundedPinnedStackBounds, 444 mPinnedStackBounds, velocityX, velocityY); 445 if (!mPinnedStackBounds.equals(toBounds)) { 446 mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds, 447 toBounds, 0, FAST_OUT_SLOW_IN, mUpdatePinnedStackBoundsListener); 448 mFlingAnimationUtils.apply(mPinnedStackBoundsAnimator, 0, 449 distanceBetweenRectOffsets(mPinnedStackBounds, toBounds), 450 velocity); 451 mPinnedStackBoundsAnimator.start(); 452 } 453 } 454 455 /** 456 * Animates the PIP to the closest snap target. 457 */ 458 private void animateToClosestSnapTarget() { 459 Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(mBoundedPinnedStackBounds, 460 mPinnedStackBounds); 461 if (!mPinnedStackBounds.equals(toBounds)) { 462 mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds, 463 toBounds, SNAP_STACK_DURATION, FAST_OUT_SLOW_IN, mUpdatePinnedStackBoundsListener); 464 mPinnedStackBoundsAnimator.start(); 465 } 466 } 467 468 /** 469 * Animates the dismissal of the PIP over the dismiss target bounds. 470 */ 471 private void animateDismissPinnedStack(Rect dismissBounds) { 472 Rect toBounds = new Rect(dismissBounds.centerX(), 473 dismissBounds.centerY(), 474 dismissBounds.centerX() + 1, 475 dismissBounds.centerY() + 1); 476 mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds, 477 toBounds, DISMISS_STACK_DURATION, FAST_OUT_LINEAR_IN, mUpdatePinnedStackBoundsListener); 478 mPinnedStackBoundsAnimator.addListener(new AnimatorListenerAdapter() { 479 @Override 480 public void onAnimationEnd(Animator animation) { 481 BackgroundThread.getHandler().post(PipTouchHandler.this::dismissPinnedStack); 482 } 483 }); 484 mPinnedStackBoundsAnimator.start(); 485 } 486 487 /** 488 * Resizes the pinned stack back to fullscreen. 489 */ 490 private void expandPinnedStackToFullscreen() { 491 BackgroundThread.getHandler().post(() -> { 492 try { 493 mActivityManager.resizeStack(PINNED_STACK_ID, null /* bounds */, 494 true /* allowResizeInDockedMode */, true /* preserveWindows */, 495 true /* animate */, EXPAND_STACK_DURATION); 496 } catch (RemoteException e) { 497 Log.e(TAG, "Error showing PIP menu activity", e); 498 } 499 }); 500 } 501 502 /** 503 * Tries to the move the pinned stack to the given {@param bounds}. 504 */ 505 private void movePinnedStack(Rect bounds) { 506 if (!bounds.equals(mPinnedStackBounds)) { 507 mPinnedStackBounds.set(bounds); 508 mMotionHelper.resizeToBounds(mPinnedStackBounds); 509 } 510 } 511 512 /** 513 * Dismisses the pinned stack. 514 */ 515 private void dismissPinnedStack() { 516 try { 517 mActivityManager.removeStack(PINNED_STACK_ID); 518 } catch (RemoteException e) { 519 Log.e(TAG, "Failed to remove PIP", e); 520 } 521 } 522 523 /** 524 * Updates the movement bounds of the pinned stack. 525 */ 526 private void updateBoundedPinnedStackBounds(boolean updatePinnedStackBounds) { 527 try { 528 StackInfo info = mActivityManager.getStackInfo(PINNED_STACK_ID); 529 if (info != null) { 530 if (updatePinnedStackBounds) { 531 mPinnedStackBounds.set(info.bounds); 532 } 533 mWindowManager.getStableInsets(info.displayId, mStableInsets); 534 mBoundedPinnedStackBounds.set(mWindowManager.getPictureInPictureMovementBounds( 535 info.displayId)); 536 } 537 } catch (RemoteException e) { 538 Log.e(TAG, "Could not fetch PIP movement bounds.", e); 539 } 540 } 541 542 /** 543 * @return the distance between points {@param p1} and {@param p2}. 544 */ 545 private float distanceBetweenRectOffsets(Rect r1, Rect r2) { 546 return PointF.length(r1.left - r2.left, r1.top - r2.top); 547 } 548 549 /** 550 * Gesture controlling dragging over a target to dismiss the PIP. 551 */ 552 private PipTouchGesture mDragToDismissGesture = new PipTouchGesture() { 553 @Override 554 public void onDown(PipTouchState touchState) { 555 if (mEnableDragToDismiss) { 556 // TODO: Consider setting a timer such at after X time, we show the dismiss 557 // target if the user hasn't already dragged some distance 558 mDismissViewController.createDismissTarget(); 559 } 560 } 561 562 @Override 563 boolean onMove(PipTouchState touchState) { 564 if (mEnableDragToDismiss && touchState.startedDragging()) { 565 mDismissViewController.showDismissTarget(); 566 } 567 return false; 568 } 569 570 @Override 571 public boolean onUp(PipTouchState touchState) { 572 if (mEnableDragToDismiss) { 573 try { 574 if (touchState.isDragging()) { 575 Rect dismissBounds = mDismissViewController.getDismissBounds(); 576 PointF lastTouch = touchState.getLastTouchPosition(); 577 if (dismissBounds.contains((int) lastTouch.x, (int) lastTouch.y)) { 578 animateDismissPinnedStack(dismissBounds); 579 return true; 580 } 581 } 582 } finally { 583 mDismissViewController.destroyDismissTarget(); 584 } 585 } 586 return false; 587 } 588 }; 589 590 /**** Gestures ****/ 591 592 /** 593 * Gesture controlling dragging the PIP slightly offscreen to minimize it. 594 */ 595 private PipTouchGesture mMinimizeGesture = new PipTouchGesture() { 596 @Override 597 boolean onMove(PipTouchState touchState) { 598 if (mEnableMinimizing) { 599 boolean isDraggingOffscreen = isDraggingOffscreen(touchState); 600 if (touchState.startedDragging() && isDraggingOffscreen) { 601 // Reset the minimized state once we drag horizontally 602 setMinimizedState(false); 603 } 604 605 if (touchState.allowDraggingOffscreen() && isDraggingOffscreen) { 606 // Move the pinned stack, but ignore the vertical movement 607 float left = mPinnedStackBounds.left + touchState.getLastTouchDelta().x; 608 mTmpBounds.set(mPinnedStackBounds); 609 mTmpBounds.offsetTo((int) left, mPinnedStackBounds.top); 610 if (!mTmpBounds.equals(mPinnedStackBounds)) { 611 mPinnedStackBounds.set(mTmpBounds); 612 mMotionHelper.resizeToBounds(mPinnedStackBounds); 613 } 614 return true; 615 } else if (mIsMinimized && touchState.isDragging()) { 616 // Move the pinned stack, but ignore the horizontal movement 617 PointF lastDelta = touchState.getLastTouchDelta(); 618 float top = mPinnedStackBounds.top + lastDelta.y; 619 top = Math.max(mBoundedPinnedStackBounds.top, Math.min( 620 mBoundedPinnedStackBounds.bottom, top)); 621 mTmpBounds.set(mPinnedStackBounds); 622 mTmpBounds.offsetTo(mPinnedStackBounds.left, (int) top); 623 movePinnedStack(mTmpBounds); 624 return true; 625 } 626 } 627 return false; 628 } 629 630 @Override 631 public boolean onUp(PipTouchState touchState) { 632 if (mEnableMinimizing) { 633 if (touchState.isDragging()) { 634 if (isDraggingOffscreen(touchState)) { 635 if (shouldMinimizedPinnedStack()) { 636 setMinimizedState(true); 637 animateToClosestMinimizedTarget(); 638 return true; 639 } 640 } else if (mIsMinimized) { 641 PointF vel = touchState.getVelocity(); 642 if (vel.length() > mFlingAnimationUtils.getMinVelocityPxPerSecond()) { 643 flingToMinimizedSnapTarget(vel.y); 644 } else { 645 animateToClosestMinimizedTarget(); 646 } 647 return true; 648 } 649 } else if (mIsMinimized) { 650 setMinimizedState(false); 651 animateToClosestSnapTarget(); 652 return true; 653 } 654 } 655 return false; 656 } 657 }; 658 659 /** 660 * Gesture controlling tapping on the PIP to show an overlay. 661 */ 662 private PipTouchGesture mTapThroughGesture = new PipTouchGesture() { 663 @Override 664 boolean onMove(PipTouchState touchState) { 665 return false; 666 } 667 668 @Override 669 public boolean onUp(PipTouchState touchState) { 670 if (!touchState.isDragging() && !mIsMinimized && !mIsTappingThrough) { 671 mMenuController.showMenu(); 672 mIsTappingThrough = true; 673 return true; 674 } 675 return false; 676 } 677 }; 678 679 /** 680 * Gesture controlling normal movement of the PIP. 681 */ 682 private PipTouchGesture mDefaultMovementGesture = new PipTouchGesture() { 683 @Override 684 boolean onMove(PipTouchState touchState) { 685 if (touchState.startedDragging()) { 686 // For now, once the user has started a drag that the other gestures have not 687 // intercepted, disallow those gestures from intercepting again to drag offscreen 688 touchState.setDisallowDraggingOffscreen(); 689 } 690 691 if (touchState.isDragging()) { 692 // Move the pinned stack freely 693 PointF lastDelta = touchState.getLastTouchDelta(); 694 float left = mPinnedStackBounds.left + lastDelta.x; 695 float top = mPinnedStackBounds.top + lastDelta.y; 696 if (!DEBUG_ALLOW_OUT_OF_BOUNDS_STACK) { 697 left = Math.max(mBoundedPinnedStackBounds.left, Math.min( 698 mBoundedPinnedStackBounds.right, left)); 699 top = Math.max(mBoundedPinnedStackBounds.top, Math.min( 700 mBoundedPinnedStackBounds.bottom, top)); 701 } 702 mTmpBounds.set(mPinnedStackBounds); 703 mTmpBounds.offsetTo((int) left, (int) top); 704 movePinnedStack(mTmpBounds); 705 return true; 706 } 707 return false; 708 } 709 710 @Override 711 public boolean onUp(PipTouchState touchState) { 712 if (touchState.isDragging()) { 713 PointF vel = mTouchState.getVelocity(); 714 float velocity = PointF.length(vel.x, vel.y); 715 if (velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond()) { 716 flingToSnapTarget(velocity, vel.x, vel.y); 717 } else { 718 animateToClosestSnapTarget(); 719 } 720 } else { 721 expandPinnedStackToFullscreen(); 722 } 723 return true; 724 } 725 }; 726} 727