1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.pip; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 20 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; 21 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY; 22 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; 23 import static android.util.RotationUtils.deltaRotation; 24 import static android.util.RotationUtils.rotateBounds; 25 26 import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_PIP; 27 import static com.android.wm.shell.ShellTaskOrganizer.taskListenerTypeToString; 28 import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; 29 import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_BOUNDS; 30 import static com.android.wm.shell.pip.PipAnimationController.FRACTION_START; 31 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; 32 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; 33 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN; 34 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_NONE; 35 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; 36 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SAME; 37 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SNAP_AFTER_RESIZE; 38 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; 39 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE; 40 import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection; 41 import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; 42 import static com.android.wm.shell.pip.PipAnimationController.isRemovePipDirection; 43 44 import android.animation.Animator; 45 import android.animation.AnimatorListenerAdapter; 46 import android.animation.ValueAnimator; 47 import android.annotation.NonNull; 48 import android.annotation.Nullable; 49 import android.app.ActivityManager; 50 import android.app.ActivityTaskManager; 51 import android.app.PictureInPictureParams; 52 import android.app.TaskInfo; 53 import android.content.ComponentName; 54 import android.content.Context; 55 import android.content.pm.ActivityInfo; 56 import android.content.res.Configuration; 57 import android.graphics.Rect; 58 import android.os.RemoteException; 59 import android.os.SystemClock; 60 import android.util.Log; 61 import android.util.Rational; 62 import android.view.Display; 63 import android.view.Surface; 64 import android.view.SurfaceControl; 65 import android.window.TaskOrganizer; 66 import android.window.WindowContainerToken; 67 import android.window.WindowContainerTransaction; 68 69 import com.android.internal.annotations.VisibleForTesting; 70 import com.android.wm.shell.R; 71 import com.android.wm.shell.ShellTaskOrganizer; 72 import com.android.wm.shell.animation.Interpolators; 73 import com.android.wm.shell.common.DisplayController; 74 import com.android.wm.shell.common.ScreenshotUtils; 75 import com.android.wm.shell.common.ShellExecutor; 76 import com.android.wm.shell.common.SyncTransactionQueue; 77 import com.android.wm.shell.common.annotations.ShellMainThread; 78 import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; 79 import com.android.wm.shell.pip.phone.PipMotionHelper; 80 import com.android.wm.shell.transition.Transitions; 81 82 import java.io.PrintWriter; 83 import java.util.Objects; 84 import java.util.Optional; 85 import java.util.function.Consumer; 86 import java.util.function.IntConsumer; 87 88 /** 89 * Manages PiP tasks such as resize and offset. 90 * 91 * This class listens on {@link TaskOrganizer} callbacks for windowing mode change 92 * both to and from PiP and issues corresponding animation if applicable. 93 * Normally, we apply series of {@link SurfaceControl.Transaction} when the animator is running 94 * and files a final {@link WindowContainerTransaction} at the end of the transition. 95 * 96 * This class is also responsible for general resize/offset PiP operations within SysUI component, 97 * see also {@link PipMotionHelper}. 98 */ 99 public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, 100 DisplayController.OnDisplaysChangedListener { 101 private static final String TAG = PipTaskOrganizer.class.getSimpleName(); 102 private static final boolean DEBUG = false; 103 /** 104 * The alpha type is set for swiping to home. But the swiped task may not enter PiP. And if 105 * another task enters PiP by non-swipe ways, e.g. call API in foreground or switch to 3-button 106 * navigation, then the alpha type is unexpected. 107 */ 108 private static final int ONE_SHOT_ALPHA_ANIMATION_TIMEOUT_MS = 1000; 109 110 /** 111 * The fixed start delay in ms when fading out the content overlay from bounds animation. 112 * This is to overcome the flicker caused by configuration change when rotating from landscape 113 * to portrait PiP in button navigation mode. 114 */ 115 private static final int CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 500; 116 117 // Not a complete set of states but serves what we want right now. 118 private enum State { 119 UNDEFINED(0), 120 TASK_APPEARED(1), 121 ENTRY_SCHEDULED(2), 122 ENTERING_PIP(3), 123 ENTERED_PIP(4), 124 EXITING_PIP(5); 125 126 private final int mStateValue; 127 State(int value)128 State(int value) { 129 mStateValue = value; 130 } 131 isInPip()132 private boolean isInPip() { 133 return mStateValue >= TASK_APPEARED.mStateValue 134 && mStateValue != EXITING_PIP.mStateValue; 135 } 136 137 /** 138 * Resize request can be initiated in other component, ignore if we are no longer in PIP, 139 * still waiting for animation or we're exiting from it. 140 * 141 * @return {@code true} if the resize request should be blocked/ignored. 142 */ shouldBlockResizeRequest()143 private boolean shouldBlockResizeRequest() { 144 return mStateValue < ENTERING_PIP.mStateValue 145 || mStateValue == EXITING_PIP.mStateValue; 146 } 147 } 148 149 private final Context mContext; 150 private final SyncTransactionQueue mSyncTransactionQueue; 151 private final PipBoundsState mPipBoundsState; 152 private final PipBoundsAlgorithm mPipBoundsAlgorithm; 153 private final @NonNull PipMenuController mPipMenuController; 154 private final PipAnimationController mPipAnimationController; 155 private final PipTransitionController mPipTransitionController; 156 private final PipUiEventLogger mPipUiEventLoggerLogger; 157 private final int mEnterAnimationDuration; 158 private final int mExitAnimationDuration; 159 private final int mCrossFadeAnimationDuration; 160 private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; 161 private final Optional<LegacySplitScreenController> mSplitScreenOptional; 162 protected final ShellTaskOrganizer mTaskOrganizer; 163 protected final ShellExecutor mMainExecutor; 164 165 // These callbacks are called on the update thread 166 private final PipAnimationController.PipAnimationCallback mPipAnimationCallback = 167 new PipAnimationController.PipAnimationCallback() { 168 @Override 169 public void onPipAnimationStart(TaskInfo taskInfo, 170 PipAnimationController.PipTransitionAnimator animator) { 171 final int direction = animator.getTransitionDirection(); 172 if (direction == TRANSITION_DIRECTION_TO_PIP) { 173 // TODO (b//169221267): Add jank listener for transactions without buffer updates. 174 //InteractionJankMonitor.getInstance().begin( 175 // InteractionJankMonitor.CUJ_LAUNCHER_APP_CLOSE_TO_PIP, 2000); 176 } 177 sendOnPipTransitionStarted(direction); 178 } 179 180 @Override 181 public void onPipAnimationEnd(TaskInfo taskInfo, SurfaceControl.Transaction tx, 182 PipAnimationController.PipTransitionAnimator animator) { 183 final int direction = animator.getTransitionDirection(); 184 final int animationType = animator.getAnimationType(); 185 final Rect destinationBounds = animator.getDestinationBounds(); 186 if (isInPipDirection(direction) && animator.getContentOverlay() != null) { 187 fadeOutAndRemoveOverlay(animator.getContentOverlay(), 188 animator::clearContentOverlay, true /* withStartDelay*/); 189 } 190 if (mWaitForFixedRotation && animationType == ANIM_TYPE_BOUNDS 191 && direction == TRANSITION_DIRECTION_TO_PIP) { 192 // Notify the display to continue the deferred orientation change. 193 final WindowContainerTransaction wct = new WindowContainerTransaction(); 194 wct.scheduleFinishEnterPip(mToken, destinationBounds); 195 mTaskOrganizer.applyTransaction(wct); 196 // The final task bounds will be applied by onFixedRotationFinished so that all 197 // coordinates are in new rotation. 198 mSurfaceTransactionHelper.round(tx, mLeash, isInPip()); 199 mDeferredAnimEndTransaction = tx; 200 return; 201 } 202 final boolean isExitPipDirection = isOutPipDirection(direction) 203 || isRemovePipDirection(direction); 204 if (mState != State.EXITING_PIP || isExitPipDirection) { 205 // Finish resize as long as we're not exiting PIP, or, if we are, only if this is 206 // the end of an exit PIP animation. 207 // This is necessary in case there was a resize animation ongoing when exit PIP 208 // started, in which case the first resize will be skipped to let the exit 209 // operation handle the final resize out of PIP mode. See b/185306679. 210 finishResize(tx, destinationBounds, direction, animationType); 211 sendOnPipTransitionFinished(direction); 212 } 213 } 214 215 @Override 216 public void onPipAnimationCancel(TaskInfo taskInfo, 217 PipAnimationController.PipTransitionAnimator animator) { 218 final int direction = animator.getTransitionDirection(); 219 if (isInPipDirection(direction) && animator.getContentOverlay() != null) { 220 fadeOutAndRemoveOverlay(animator.getContentOverlay(), 221 animator::clearContentOverlay, true /* withStartDelay */); 222 } 223 sendOnPipTransitionCancelled(direction); 224 } 225 }; 226 227 private final PipAnimationController.PipTransactionHandler mPipTransactionHandler = 228 new PipAnimationController.PipTransactionHandler() { 229 @Override 230 public boolean handlePipTransaction(SurfaceControl leash, 231 SurfaceControl.Transaction tx, Rect destinationBounds) { 232 if (mPipMenuController.isMenuVisible()) { 233 mPipMenuController.movePipMenu(leash, tx, destinationBounds); 234 return true; 235 } 236 return false; 237 } 238 }; 239 240 private ActivityManager.RunningTaskInfo mTaskInfo; 241 // To handle the edge case that onTaskInfoChanged callback is received during the entering 242 // PiP transition, where we do not want to intercept the transition but still want to apply the 243 // changed RunningTaskInfo when it finishes. 244 private ActivityManager.RunningTaskInfo mDeferredTaskInfo; 245 private WindowContainerToken mToken; 246 private SurfaceControl mLeash; 247 private State mState = State.UNDEFINED; 248 private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS; 249 private long mLastOneShotAlphaAnimationTime; 250 private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory 251 mSurfaceControlTransactionFactory; 252 private PictureInPictureParams mPictureInPictureParams; 253 private IntConsumer mOnDisplayIdChangeCallback; 254 /** 255 * The end transaction of PiP animation for switching between PiP and fullscreen with 256 * orientation change. The transaction should be applied after the display is rotated. 257 */ 258 private SurfaceControl.Transaction mDeferredAnimEndTransaction; 259 /** Whether the existing PiP is hidden by alpha. */ 260 private boolean mHasFadeOut; 261 262 /** 263 * If set to {@code true}, the entering animation will be skipped and we will wait for 264 * {@link #onFixedRotationFinished(int)} callback to actually enter PiP. 265 */ 266 private boolean mWaitForFixedRotation; 267 268 /** 269 * The rotation that the display will apply after expanding PiP to fullscreen. This is only 270 * meaningful if {@link #mWaitForFixedRotation} is true. 271 */ 272 private @Surface.Rotation int mNextRotation; 273 274 private @Surface.Rotation int mCurrentRotation; 275 276 /** 277 * If set to {@code true}, no entering PiP transition would be kicked off and most likely 278 * it's due to the fact that Launcher is handling the transition directly when swiping 279 * auto PiP-able Activity to home. 280 * See also {@link #startSwipePipToHome(ComponentName, ActivityInfo, PictureInPictureParams)}. 281 */ 282 private boolean mInSwipePipToHomeTransition; 283 284 /** 285 * An optional overlay used to mask content changing between an app in/out of PiP, only set if 286 * {@link #mInSwipePipToHomeTransition} is true. 287 */ 288 private SurfaceControl mSwipePipToHomeOverlay; 289 PipTaskOrganizer(Context context, @NonNull SyncTransactionQueue syncTransactionQueue, @NonNull PipBoundsState pipBoundsState, @NonNull PipBoundsAlgorithm boundsHandler, @NonNull PipMenuController pipMenuController, @NonNull PipAnimationController pipAnimationController, @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, @NonNull PipTransitionController pipTransitionController, Optional<LegacySplitScreenController> splitScreenOptional, @NonNull DisplayController displayController, @NonNull PipUiEventLogger pipUiEventLogger, @NonNull ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor)290 public PipTaskOrganizer(Context context, 291 @NonNull SyncTransactionQueue syncTransactionQueue, 292 @NonNull PipBoundsState pipBoundsState, 293 @NonNull PipBoundsAlgorithm boundsHandler, 294 @NonNull PipMenuController pipMenuController, 295 @NonNull PipAnimationController pipAnimationController, 296 @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, 297 @NonNull PipTransitionController pipTransitionController, 298 Optional<LegacySplitScreenController> splitScreenOptional, 299 @NonNull DisplayController displayController, 300 @NonNull PipUiEventLogger pipUiEventLogger, 301 @NonNull ShellTaskOrganizer shellTaskOrganizer, 302 @ShellMainThread ShellExecutor mainExecutor) { 303 mContext = context; 304 mSyncTransactionQueue = syncTransactionQueue; 305 mPipBoundsState = pipBoundsState; 306 mPipBoundsAlgorithm = boundsHandler; 307 mPipMenuController = pipMenuController; 308 mPipTransitionController = pipTransitionController; 309 mEnterAnimationDuration = context.getResources() 310 .getInteger(R.integer.config_pipEnterAnimationDuration); 311 mExitAnimationDuration = context.getResources() 312 .getInteger(R.integer.config_pipExitAnimationDuration); 313 mCrossFadeAnimationDuration = context.getResources() 314 .getInteger(R.integer.config_pipCrossfadeAnimationDuration); 315 mSurfaceTransactionHelper = surfaceTransactionHelper; 316 mPipAnimationController = pipAnimationController; 317 mPipUiEventLoggerLogger = pipUiEventLogger; 318 mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; 319 mSplitScreenOptional = splitScreenOptional; 320 mTaskOrganizer = shellTaskOrganizer; 321 mMainExecutor = mainExecutor; 322 323 // TODO: Can be removed once wm components are created on the shell-main thread 324 mMainExecutor.execute(() -> { 325 mTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_PIP); 326 }); 327 displayController.addDisplayWindowListener(this); 328 } 329 getCurrentOrAnimatingBounds()330 public Rect getCurrentOrAnimatingBounds() { 331 PipAnimationController.PipTransitionAnimator animator = 332 mPipAnimationController.getCurrentAnimator(); 333 if (animator != null && animator.isRunning()) { 334 return new Rect(animator.getDestinationBounds()); 335 } 336 return mPipBoundsState.getBounds(); 337 } 338 isInPip()339 public boolean isInPip() { 340 return mState.isInPip(); 341 } 342 343 /** 344 * Returns whether the entry animation is waiting to be started. 345 */ isEntryScheduled()346 public boolean isEntryScheduled() { 347 return mState == State.ENTRY_SCHEDULED; 348 } 349 350 /** 351 * Registers a callback when a display change has been detected when we enter PiP. 352 */ registerOnDisplayIdChangeCallback(IntConsumer onDisplayIdChangeCallback)353 public void registerOnDisplayIdChangeCallback(IntConsumer onDisplayIdChangeCallback) { 354 mOnDisplayIdChangeCallback = onDisplayIdChangeCallback; 355 } 356 357 /** 358 * Sets the preferred animation type for one time. 359 * This is typically used to set the animation type to 360 * {@link PipAnimationController#ANIM_TYPE_ALPHA}. 361 */ setOneShotAnimationType(@ipAnimationController.AnimationType int animationType)362 public void setOneShotAnimationType(@PipAnimationController.AnimationType int animationType) { 363 mOneShotAnimationType = animationType; 364 if (animationType == ANIM_TYPE_ALPHA) { 365 mLastOneShotAlphaAnimationTime = SystemClock.uptimeMillis(); 366 } 367 } 368 369 /** 370 * Callback when Launcher starts swipe-pip-to-home operation. 371 * @return {@link Rect} for destination bounds. 372 */ startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, PictureInPictureParams pictureInPictureParams)373 public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, 374 PictureInPictureParams pictureInPictureParams) { 375 mInSwipePipToHomeTransition = true; 376 sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP); 377 setBoundsStateForEntry(componentName, pictureInPictureParams, activityInfo); 378 return mPipBoundsAlgorithm.getEntryDestinationBounds(); 379 } 380 381 /** 382 * Callback when launcher finishes swipe-pip-to-home operation. 383 * Expect {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)} afterwards. 384 */ stopSwipePipToHome(ComponentName componentName, Rect destinationBounds, SurfaceControl overlay)385 public void stopSwipePipToHome(ComponentName componentName, Rect destinationBounds, 386 SurfaceControl overlay) { 387 // do nothing if there is no startSwipePipToHome being called before 388 if (mInSwipePipToHomeTransition) { 389 mPipBoundsState.setBounds(destinationBounds); 390 mSwipePipToHomeOverlay = overlay; 391 } 392 } 393 getSurfaceControl()394 public SurfaceControl getSurfaceControl() { 395 return mLeash; 396 } 397 setBoundsStateForEntry(ComponentName componentName, PictureInPictureParams params, ActivityInfo activityInfo)398 private void setBoundsStateForEntry(ComponentName componentName, PictureInPictureParams params, 399 ActivityInfo activityInfo) { 400 mPipBoundsState.setBoundsStateForEntry(componentName, 401 mPipBoundsAlgorithm.getAspectRatioOrDefault(params), 402 mPipBoundsAlgorithm.getMinimalSize(activityInfo)); 403 } 404 405 /** 406 * Expands PiP to the previous bounds, this is done in two phases using 407 * {@link WindowContainerTransaction} 408 * - setActivityWindowingMode to either fullscreen or split-secondary at beginning of the 409 * transaction. without changing the windowing mode of the Task itself. This makes sure the 410 * activity render it's final configuration while the Task is still in PiP. 411 * - setWindowingMode to undefined at the end of transition 412 * @param animationDurationMs duration in millisecond for the exiting PiP transition 413 */ exitPip(int animationDurationMs)414 public void exitPip(int animationDurationMs) { 415 if (!mState.isInPip() || mState == State.EXITING_PIP || mToken == null) { 416 Log.wtf(TAG, "Not allowed to exitPip in current state" 417 + " mState=" + mState + " mToken=" + mToken); 418 return; 419 } 420 421 mPipUiEventLoggerLogger.log( 422 PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN); 423 final WindowContainerTransaction wct = new WindowContainerTransaction(); 424 final Rect destinationBounds = mPipBoundsState.getDisplayBounds(); 425 final int direction = syncWithSplitScreenBounds(destinationBounds) 426 ? TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN 427 : TRANSITION_DIRECTION_LEAVE_PIP; 428 final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); 429 mSurfaceTransactionHelper.scale(tx, mLeash, destinationBounds, mPipBoundsState.getBounds()); 430 tx.setWindowCrop(mLeash, destinationBounds.width(), destinationBounds.height()); 431 // We set to fullscreen here for now, but later it will be set to UNDEFINED for 432 // the proper windowing mode to take place. See #applyWindowingModeChangeOnExit. 433 wct.setActivityWindowingMode(mToken, 434 direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN 435 ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY 436 : WINDOWING_MODE_FULLSCREEN); 437 wct.setBounds(mToken, destinationBounds); 438 wct.setBoundsChangeTransaction(mToken, tx); 439 // Set the exiting state first so if there is fixed rotation later, the running animation 440 // won't be interrupted by alpha animation for existing PiP. 441 mState = State.EXITING_PIP; 442 mSyncTransactionQueue.queue(wct); 443 mSyncTransactionQueue.runInSync(t -> { 444 // Make sure to grab the latest source hint rect as it could have been 445 // updated right after applying the windowing mode change. 446 final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( 447 mPictureInPictureParams, destinationBounds); 448 final PipAnimationController.PipTransitionAnimator<?> animator = 449 animateResizePip(mPipBoundsState.getBounds(), destinationBounds, sourceHintRect, 450 direction, animationDurationMs, 0 /* startingAngle */); 451 if (animator != null) { 452 // Even though the animation was started above, re-apply the transaction for the 453 // first frame using the SurfaceControl.Transaction supplied by the 454 // SyncTransactionQueue. This is necessary because the initial surface transform 455 // may not be applied until the next frame if a different Transaction than the one 456 // supplied is used, resulting in 1 frame not being cropped to the source rect 457 // hint during expansion that causes a visible jank/flash. See b/184166183. 458 animator.applySurfaceControlTransaction(mLeash, t, FRACTION_START); 459 } 460 }); 461 } 462 applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction)463 private void applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction) { 464 // Reset the final windowing mode. 465 wct.setWindowingMode(mToken, getOutPipWindowingMode()); 466 // Simply reset the activity mode set prior to the animation running. 467 wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); 468 mSplitScreenOptional.ifPresent(splitScreen -> { 469 if (direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { 470 wct.reparent(mToken, splitScreen.getSecondaryRoot(), true /* onTop */); 471 } 472 }); 473 } 474 475 /** 476 * Removes PiP immediately. 477 */ removePip()478 public void removePip() { 479 if (!mState.isInPip() || mToken == null) { 480 Log.wtf(TAG, "Not allowed to removePip in current state" 481 + " mState=" + mState + " mToken=" + mToken); 482 return; 483 } 484 485 // removePipImmediately is expected when the following animation finishes. 486 ValueAnimator animator = mPipAnimationController 487 .getAnimator(mTaskInfo, mLeash, mPipBoundsState.getBounds(), 488 1f /* alphaStart */, 0f /* alphaEnd */) 489 .setTransitionDirection(TRANSITION_DIRECTION_REMOVE_STACK) 490 .setPipTransactionHandler(mPipTransactionHandler) 491 .setPipAnimationCallback(mPipAnimationCallback); 492 animator.setDuration(mExitAnimationDuration); 493 animator.setInterpolator(Interpolators.ALPHA_OUT); 494 animator.start(); 495 mState = State.EXITING_PIP; 496 } 497 removePipImmediately()498 private void removePipImmediately() { 499 try { 500 // Reset the task bounds first to ensure the activity configuration is reset as well 501 final WindowContainerTransaction wct = new WindowContainerTransaction(); 502 wct.setBounds(mToken, null); 503 mTaskOrganizer.applyTransaction(wct); 504 505 ActivityTaskManager.getService().removeRootTasksInWindowingModes( 506 new int[]{ WINDOWING_MODE_PINNED }); 507 } catch (RemoteException e) { 508 Log.e(TAG, "Failed to remove PiP", e); 509 } 510 } 511 512 @Override onTaskAppeared(ActivityManager.RunningTaskInfo info, SurfaceControl leash)513 public void onTaskAppeared(ActivityManager.RunningTaskInfo info, SurfaceControl leash) { 514 Objects.requireNonNull(info, "Requires RunningTaskInfo"); 515 mTaskInfo = info; 516 mToken = mTaskInfo.token; 517 mState = State.TASK_APPEARED; 518 mLeash = leash; 519 mPictureInPictureParams = mTaskInfo.pictureInPictureParams; 520 setBoundsStateForEntry(mTaskInfo.topActivity, mPictureInPictureParams, 521 mTaskInfo.topActivityInfo); 522 523 mPipUiEventLoggerLogger.setTaskInfo(mTaskInfo); 524 mPipUiEventLoggerLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER); 525 526 // If the displayId of the task is different than what PipBoundsHandler has, then update 527 // it. This is possible if we entered PiP on an external display. 528 if (info.displayId != mPipBoundsState.getDisplayId() 529 && mOnDisplayIdChangeCallback != null) { 530 mOnDisplayIdChangeCallback.accept(info.displayId); 531 } 532 533 if (mInSwipePipToHomeTransition) { 534 if (!mWaitForFixedRotation) { 535 onEndOfSwipePipToHomeTransition(); 536 } else { 537 Log.d(TAG, "Defer onTaskAppeared-SwipePipToHome until end of fixed rotation."); 538 } 539 return; 540 } 541 542 if (mOneShotAnimationType == ANIM_TYPE_ALPHA 543 && SystemClock.uptimeMillis() - mLastOneShotAlphaAnimationTime 544 > ONE_SHOT_ALPHA_ANIMATION_TIMEOUT_MS) { 545 Log.d(TAG, "Alpha animation is expired. Use bounds animation."); 546 mOneShotAnimationType = ANIM_TYPE_BOUNDS; 547 } 548 if (mWaitForFixedRotation) { 549 onTaskAppearedWithFixedRotation(); 550 return; 551 } 552 553 final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); 554 Objects.requireNonNull(destinationBounds, "Missing destination bounds"); 555 final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); 556 557 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 558 if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { 559 mPipMenuController.attach(mLeash); 560 } 561 return; 562 } 563 564 if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { 565 mPipMenuController.attach(mLeash); 566 final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( 567 info.pictureInPictureParams, currentBounds); 568 scheduleAnimateResizePip(currentBounds, destinationBounds, 0 /* startingAngle */, 569 sourceHintRect, TRANSITION_DIRECTION_TO_PIP, mEnterAnimationDuration, 570 null /* updateBoundsCallback */); 571 mState = State.ENTERING_PIP; 572 } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { 573 enterPipWithAlphaAnimation(destinationBounds, mEnterAnimationDuration); 574 mOneShotAnimationType = ANIM_TYPE_BOUNDS; 575 } else { 576 throw new RuntimeException("Unrecognized animation type: " + mOneShotAnimationType); 577 } 578 } 579 onTaskAppearedWithFixedRotation()580 private void onTaskAppearedWithFixedRotation() { 581 if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { 582 Log.d(TAG, "Defer entering PiP alpha animation, fixed rotation is ongoing"); 583 // If deferred, hide the surface till fixed rotation is completed. 584 final SurfaceControl.Transaction tx = 585 mSurfaceControlTransactionFactory.getTransaction(); 586 tx.setAlpha(mLeash, 0f); 587 tx.show(mLeash); 588 tx.apply(); 589 mOneShotAnimationType = ANIM_TYPE_BOUNDS; 590 return; 591 } 592 final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); 593 final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( 594 mPictureInPictureParams, currentBounds); 595 final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); 596 animateResizePip(currentBounds, destinationBounds, sourceHintRect, 597 TRANSITION_DIRECTION_TO_PIP, mEnterAnimationDuration, 0 /* startingAngle */); 598 mState = State.ENTERING_PIP; 599 } 600 601 /** 602 * Called when the display rotation handling is skipped (e.g. when rotation happens while in 603 * the middle of an entry transition). 604 */ onDisplayRotationSkipped()605 public void onDisplayRotationSkipped() { 606 if (isEntryScheduled()) { 607 // The PIP animation is scheduled to start with the previous orientation's bounds, 608 // re-calculate the entry bounds and restart the alpha animation. 609 final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); 610 enterPipWithAlphaAnimation(destinationBounds, mEnterAnimationDuration); 611 } 612 } 613 614 @VisibleForTesting enterPipWithAlphaAnimation(Rect destinationBounds, long durationMs)615 void enterPipWithAlphaAnimation(Rect destinationBounds, long durationMs) { 616 // If we are fading the PIP in, then we should move the pip to the final location as 617 // soon as possible, but set the alpha immediately since the transaction can take a 618 // while to process 619 final SurfaceControl.Transaction tx = 620 mSurfaceControlTransactionFactory.getTransaction(); 621 tx.setAlpha(mLeash, 0f); 622 tx.apply(); 623 mState = State.ENTRY_SCHEDULED; 624 applyEnterPipSyncTransaction(destinationBounds, () -> { 625 mPipAnimationController 626 .getAnimator(mTaskInfo, mLeash, destinationBounds, 0f, 1f) 627 .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) 628 .setPipAnimationCallback(mPipAnimationCallback) 629 .setPipTransactionHandler(mPipTransactionHandler) 630 .setDuration(durationMs) 631 .start(); 632 // mState is set right after the animation is kicked off to block any resize 633 // requests such as offsetPip that may have been called prior to the transition. 634 mState = State.ENTERING_PIP; 635 }, null /* boundsChangeTransaction */); 636 } 637 onEndOfSwipePipToHomeTransition()638 private void onEndOfSwipePipToHomeTransition() { 639 final Rect destinationBounds = mPipBoundsState.getBounds(); 640 final SurfaceControl swipeToHomeOverlay = mSwipePipToHomeOverlay; 641 final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); 642 mSurfaceTransactionHelper 643 .resetScale(tx, mLeash, destinationBounds) 644 .crop(tx, mLeash, destinationBounds) 645 .round(tx, mLeash, isInPip()); 646 // The animation is finished in the Launcher and here we directly apply the final touch. 647 applyEnterPipSyncTransaction(destinationBounds, () -> { 648 // Ensure menu's settled in its final bounds first. 649 finishResizeForMenu(destinationBounds); 650 sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); 651 652 // Remove the swipe to home overlay 653 if (swipeToHomeOverlay != null) { 654 fadeOutAndRemoveOverlay(swipeToHomeOverlay, 655 null /* callback */, false /* withStartDelay */); 656 } 657 }, tx); 658 mInSwipePipToHomeTransition = false; 659 mSwipePipToHomeOverlay = null; 660 } 661 applyEnterPipSyncTransaction(Rect destinationBounds, Runnable runnable, @Nullable SurfaceControl.Transaction boundsChangeTransaction)662 private void applyEnterPipSyncTransaction(Rect destinationBounds, Runnable runnable, 663 @Nullable SurfaceControl.Transaction boundsChangeTransaction) { 664 // PiP menu is attached late in the process here to avoid any artifacts on the leash 665 // caused by addShellRoot when in gesture navigation mode. 666 mPipMenuController.attach(mLeash); 667 final WindowContainerTransaction wct = new WindowContainerTransaction(); 668 wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); 669 wct.setBounds(mToken, destinationBounds); 670 if (boundsChangeTransaction != null) { 671 wct.setBoundsChangeTransaction(mToken, boundsChangeTransaction); 672 } 673 mSyncTransactionQueue.queue(wct); 674 if (runnable != null) { 675 mSyncTransactionQueue.runInSync(t -> runnable.run()); 676 } 677 } 678 sendOnPipTransitionStarted( @ipAnimationController.TransitionDirection int direction)679 private void sendOnPipTransitionStarted( 680 @PipAnimationController.TransitionDirection int direction) { 681 if (direction == TRANSITION_DIRECTION_TO_PIP) { 682 mState = State.ENTERING_PIP; 683 } 684 mPipTransitionController.sendOnPipTransitionStarted(direction); 685 } 686 687 @VisibleForTesting sendOnPipTransitionFinished( @ipAnimationController.TransitionDirection int direction)688 void sendOnPipTransitionFinished( 689 @PipAnimationController.TransitionDirection int direction) { 690 if (direction == TRANSITION_DIRECTION_TO_PIP) { 691 mState = State.ENTERED_PIP; 692 } 693 mPipTransitionController.sendOnPipTransitionFinished(direction); 694 // Apply the deferred RunningTaskInfo if applicable after all proper callbacks are sent. 695 if (direction == TRANSITION_DIRECTION_TO_PIP && mDeferredTaskInfo != null) { 696 onTaskInfoChanged(mDeferredTaskInfo); 697 mDeferredTaskInfo = null; 698 } 699 } 700 sendOnPipTransitionCancelled( @ipAnimationController.TransitionDirection int direction)701 private void sendOnPipTransitionCancelled( 702 @PipAnimationController.TransitionDirection int direction) { 703 mPipTransitionController.sendOnPipTransitionCancelled(direction); 704 } 705 706 /** 707 * Note that dismissing PiP is now originated from SystemUI, see {@link #exitPip(int)}. 708 * Meanwhile this callback is invoked whenever the task is removed. For instance: 709 * - as a result of removeRootTasksInWindowingModes from WM 710 * - activity itself is died 711 * Nevertheless, we simply update the internal state here as all the heavy lifting should 712 * have been done in WM. 713 */ 714 @Override onTaskVanished(ActivityManager.RunningTaskInfo info)715 public void onTaskVanished(ActivityManager.RunningTaskInfo info) { 716 if (mState == State.UNDEFINED) { 717 return; 718 } 719 final WindowContainerToken token = info.token; 720 Objects.requireNonNull(token, "Requires valid WindowContainerToken"); 721 if (token.asBinder() != mToken.asBinder()) { 722 Log.wtf(TAG, "Unrecognized token: " + token); 723 return; 724 } 725 clearWaitForFixedRotation(); 726 mInSwipePipToHomeTransition = false; 727 mPictureInPictureParams = null; 728 mState = State.UNDEFINED; 729 // Re-set the PIP bounds to none. 730 mPipBoundsState.setBounds(new Rect()); 731 mPipUiEventLoggerLogger.setTaskInfo(null); 732 mPipMenuController.detach(); 733 734 if (info.displayId != Display.DEFAULT_DISPLAY && mOnDisplayIdChangeCallback != null) { 735 mOnDisplayIdChangeCallback.accept(Display.DEFAULT_DISPLAY); 736 } 737 738 final PipAnimationController.PipTransitionAnimator<?> animator = 739 mPipAnimationController.getCurrentAnimator(); 740 if (animator != null) { 741 if (animator.getContentOverlay() != null) { 742 removeContentOverlay(animator.getContentOverlay(), animator::clearContentOverlay); 743 } 744 animator.removeAllUpdateListeners(); 745 animator.removeAllListeners(); 746 animator.cancel(); 747 } 748 } 749 750 @Override onTaskInfoChanged(ActivityManager.RunningTaskInfo info)751 public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) { 752 Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken"); 753 if (mState != State.ENTERED_PIP && mState != State.EXITING_PIP) { 754 Log.d(TAG, "Defer onTaskInfoChange in current state: " + mState); 755 // Defer applying PiP parameters if the task is entering PiP to avoid disturbing 756 // the animation. 757 mDeferredTaskInfo = info; 758 return; 759 } 760 mPipBoundsState.setLastPipComponentName(info.topActivity); 761 mPipBoundsState.setOverrideMinSize( 762 mPipBoundsAlgorithm.getMinimalSize(info.topActivityInfo)); 763 final PictureInPictureParams newParams = info.pictureInPictureParams; 764 if (newParams == null || !applyPictureInPictureParams(newParams)) { 765 Log.d(TAG, "Ignored onTaskInfoChanged with PiP param: " + newParams); 766 return; 767 } 768 // Aspect ratio changed, re-calculate bounds if valid. 769 final Rect destinationBounds = mPipBoundsAlgorithm.getAdjustedDestinationBounds( 770 mPipBoundsState.getBounds(), mPipBoundsState.getAspectRatio()); 771 Objects.requireNonNull(destinationBounds, "Missing destination bounds"); 772 scheduleAnimateResizePip(destinationBounds, mEnterAnimationDuration, 773 null /* updateBoundsCallback */); 774 } 775 776 @Override supportSizeCompatUI()777 public boolean supportSizeCompatUI() { 778 // PIP doesn't support size compat. 779 return false; 780 } 781 782 @Override onFixedRotationStarted(int displayId, int newRotation)783 public void onFixedRotationStarted(int displayId, int newRotation) { 784 mNextRotation = newRotation; 785 mWaitForFixedRotation = true; 786 787 if (mState.isInPip()) { 788 // Fade out the existing PiP to avoid jump cut during seamless rotation. 789 fadeExistingPip(false /* show */); 790 } 791 } 792 793 @Override onFixedRotationFinished(int displayId)794 public void onFixedRotationFinished(int displayId) { 795 if (!mWaitForFixedRotation) { 796 return; 797 } 798 if (mState == State.TASK_APPEARED) { 799 if (mInSwipePipToHomeTransition) { 800 onEndOfSwipePipToHomeTransition(); 801 } else { 802 // Schedule a regular animation to ensure all the callbacks are still being sent. 803 enterPipWithAlphaAnimation(mPipBoundsAlgorithm.getEntryDestinationBounds(), 804 mEnterAnimationDuration); 805 } 806 } else if (mState == State.ENTERED_PIP && mHasFadeOut) { 807 fadeExistingPip(true /* show */); 808 } else if (mState == State.ENTERING_PIP && mDeferredAnimEndTransaction != null) { 809 final PipAnimationController.PipTransitionAnimator<?> animator = 810 mPipAnimationController.getCurrentAnimator(); 811 final Rect destinationBounds = animator.getDestinationBounds(); 812 mPipBoundsState.setBounds(destinationBounds); 813 applyEnterPipSyncTransaction(destinationBounds, () -> { 814 finishResizeForMenu(destinationBounds); 815 sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); 816 }, mDeferredAnimEndTransaction); 817 } 818 clearWaitForFixedRotation(); 819 } 820 fadeExistingPip(boolean show)821 private void fadeExistingPip(boolean show) { 822 final float alphaStart = show ? 0 : 1; 823 final float alphaEnd = show ? 1 : 0; 824 mPipAnimationController 825 .getAnimator(mTaskInfo, mLeash, mPipBoundsState.getBounds(), alphaStart, alphaEnd) 826 .setTransitionDirection(TRANSITION_DIRECTION_SAME) 827 .setPipTransactionHandler(mPipTransactionHandler) 828 .setDuration(show ? mEnterAnimationDuration : mExitAnimationDuration) 829 .start(); 830 mHasFadeOut = !show; 831 } 832 clearWaitForFixedRotation()833 private void clearWaitForFixedRotation() { 834 mWaitForFixedRotation = false; 835 mDeferredAnimEndTransaction = null; 836 } 837 838 @Override onDisplayConfigurationChanged(int displayId, Configuration newConfig)839 public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { 840 mCurrentRotation = newConfig.windowConfiguration.getRotation(); 841 } 842 843 /** 844 * Called when display size or font size of settings changed 845 */ onDensityOrFontScaleChanged(Context context)846 public void onDensityOrFontScaleChanged(Context context) { 847 mSurfaceTransactionHelper.onDensityOrFontScaleChanged(context); 848 } 849 850 /** 851 * TODO(b/152809058): consolidate the display info handling logic in SysUI 852 * 853 * @param destinationBoundsOut the current destination bounds will be populated to this param 854 */ 855 @SuppressWarnings("unchecked") onMovementBoundsChanged(Rect destinationBoundsOut, boolean fromRotation, boolean fromImeAdjustment, boolean fromShelfAdjustment, WindowContainerTransaction wct)856 public void onMovementBoundsChanged(Rect destinationBoundsOut, boolean fromRotation, 857 boolean fromImeAdjustment, boolean fromShelfAdjustment, 858 WindowContainerTransaction wct) { 859 // note that this can be called when swipe-to-home or fixed-rotation is happening. 860 // Skip this entirely if that's the case. 861 final boolean waitForFixedRotationOnEnteringPip = mWaitForFixedRotation 862 && (mState != State.ENTERED_PIP); 863 if ((mInSwipePipToHomeTransition || waitForFixedRotationOnEnteringPip) && fromRotation) { 864 if (DEBUG) { 865 Log.d(TAG, "Skip onMovementBoundsChanged on rotation change" 866 + " mInSwipePipToHomeTransition=" + mInSwipePipToHomeTransition 867 + " mWaitForFixedRotation=" + mWaitForFixedRotation 868 + " mState=" + mState); 869 } 870 return; 871 } 872 final PipAnimationController.PipTransitionAnimator animator = 873 mPipAnimationController.getCurrentAnimator(); 874 if (animator == null || !animator.isRunning() 875 || animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) { 876 final boolean rotatingPip = mState.isInPip() && fromRotation; 877 if (rotatingPip && mWaitForFixedRotation && mHasFadeOut) { 878 // The position will be used by fade-in animation when the fixed rotation is done. 879 mPipBoundsState.setBounds(destinationBoundsOut); 880 } else if (rotatingPip) { 881 // Update bounds state to final destination first. It's important to do this 882 // before finishing & cancelling the transition animation so that the MotionHelper 883 // bounds are synchronized to the destination bounds when the animation ends. 884 mPipBoundsState.setBounds(destinationBoundsOut); 885 // If we are rotating while there is a current animation, immediately cancel the 886 // animation (remove the listeners so we don't trigger the normal finish resize 887 // call that should only happen on the update thread) 888 int direction = TRANSITION_DIRECTION_NONE; 889 if (animator != null) { 890 direction = animator.getTransitionDirection(); 891 animator.removeAllUpdateListeners(); 892 animator.removeAllListeners(); 893 animator.cancel(); 894 // Do notify the listeners that this was canceled 895 sendOnPipTransitionCancelled(direction); 896 sendOnPipTransitionFinished(direction); 897 } 898 899 // Create a reset surface transaction for the new bounds and update the window 900 // container transaction 901 final SurfaceControl.Transaction tx = createFinishResizeSurfaceTransaction( 902 destinationBoundsOut); 903 prepareFinishResizeTransaction(destinationBoundsOut, direction, tx, wct); 904 } else { 905 // There could be an animation on-going. If there is one on-going, last-reported 906 // bounds isn't yet updated. We'll use the animator's bounds instead. 907 if (animator != null && animator.isRunning()) { 908 if (!animator.getDestinationBounds().isEmpty()) { 909 destinationBoundsOut.set(animator.getDestinationBounds()); 910 } 911 } else { 912 if (!mPipBoundsState.getBounds().isEmpty()) { 913 destinationBoundsOut.set(mPipBoundsState.getBounds()); 914 } 915 } 916 } 917 return; 918 } 919 920 final Rect currentDestinationBounds = animator.getDestinationBounds(); 921 destinationBoundsOut.set(currentDestinationBounds); 922 if (!fromImeAdjustment && !fromShelfAdjustment 923 && mPipBoundsState.getDisplayBounds().contains(currentDestinationBounds)) { 924 // no need to update the destination bounds, bail early 925 return; 926 } 927 928 final Rect newDestinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); 929 if (newDestinationBounds.equals(currentDestinationBounds)) return; 930 if (animator.getAnimationType() == ANIM_TYPE_BOUNDS) { 931 if (mWaitForFixedRotation) { 932 // The new destination bounds are in next rotation (DisplayLayout has been rotated 933 // in computeRotatedBounds). The animation runs in previous rotation so the end 934 // bounds need to be transformed. 935 final Rect displayBounds = mPipBoundsState.getDisplayBounds(); 936 final Rect rotatedEndBounds = new Rect(newDestinationBounds); 937 rotateBounds(rotatedEndBounds, displayBounds, mNextRotation, mCurrentRotation); 938 animator.updateEndValue(rotatedEndBounds); 939 } else { 940 animator.updateEndValue(newDestinationBounds); 941 } 942 } 943 animator.setDestinationBounds(newDestinationBounds); 944 destinationBoundsOut.set(newDestinationBounds); 945 } 946 947 /** 948 * @return {@code true} if the aspect ratio is changed since no other parameters within 949 * {@link PictureInPictureParams} would affect the bounds. 950 */ applyPictureInPictureParams(@onNull PictureInPictureParams params)951 private boolean applyPictureInPictureParams(@NonNull PictureInPictureParams params) { 952 final Rational currentAspectRatio = 953 mPictureInPictureParams != null ? mPictureInPictureParams.getAspectRatioRational() 954 : null; 955 final boolean aspectRatioChanged = !Objects.equals(currentAspectRatio, 956 params.getAspectRatioRational()); 957 mPictureInPictureParams = params; 958 if (aspectRatioChanged) { 959 mPipBoundsState.setAspectRatio(params.getAspectRatio()); 960 } 961 return aspectRatioChanged; 962 } 963 964 /** 965 * Animates resizing of the pinned stack given the duration. 966 */ scheduleAnimateResizePip(Rect toBounds, int duration, Consumer<Rect> updateBoundsCallback)967 public void scheduleAnimateResizePip(Rect toBounds, int duration, 968 Consumer<Rect> updateBoundsCallback) { 969 scheduleAnimateResizePip(toBounds, duration, TRANSITION_DIRECTION_NONE, 970 updateBoundsCallback); 971 } 972 973 /** 974 * Animates resizing of the pinned stack given the duration. 975 */ scheduleAnimateResizePip(Rect toBounds, int duration, @PipAnimationController.TransitionDirection int direction, Consumer<Rect> updateBoundsCallback)976 public void scheduleAnimateResizePip(Rect toBounds, int duration, 977 @PipAnimationController.TransitionDirection int direction, 978 Consumer<Rect> updateBoundsCallback) { 979 if (mWaitForFixedRotation) { 980 Log.d(TAG, "skip scheduleAnimateResizePip, entering pip deferred"); 981 return; 982 } 983 scheduleAnimateResizePip(mPipBoundsState.getBounds(), toBounds, 0 /* startingAngle */, 984 null /* sourceHintRect */, direction, duration, updateBoundsCallback); 985 } 986 987 /** 988 * Animates resizing of the pinned stack given the duration and start bounds. 989 * This is used when the starting bounds is not the current PiP bounds. 990 */ scheduleAnimateResizePip(Rect fromBounds, Rect toBounds, int duration, float startingAngle, Consumer<Rect> updateBoundsCallback)991 public void scheduleAnimateResizePip(Rect fromBounds, Rect toBounds, int duration, 992 float startingAngle, Consumer<Rect> updateBoundsCallback) { 993 if (mWaitForFixedRotation) { 994 Log.d(TAG, "skip scheduleAnimateResizePip, entering pip deferred"); 995 return; 996 } 997 scheduleAnimateResizePip(fromBounds, toBounds, startingAngle, null /* sourceHintRect */, 998 TRANSITION_DIRECTION_SNAP_AFTER_RESIZE, duration, updateBoundsCallback); 999 } 1000 1001 /** 1002 * Animates resizing of the pinned stack given the duration and start bounds. 1003 * This always animates the angle to zero from the starting angle. 1004 */ scheduleAnimateResizePip( Rect currentBounds, Rect destinationBounds, float startingAngle, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, int durationMs, Consumer<Rect> updateBoundsCallback)1005 private @Nullable PipAnimationController.PipTransitionAnimator<?> scheduleAnimateResizePip( 1006 Rect currentBounds, Rect destinationBounds, float startingAngle, Rect sourceHintRect, 1007 @PipAnimationController.TransitionDirection int direction, int durationMs, 1008 Consumer<Rect> updateBoundsCallback) { 1009 if (!mState.isInPip()) { 1010 // TODO: tend to use shouldBlockResizeRequest here as well but need to consider 1011 // the fact that when in exitPip, scheduleAnimateResizePip is executed in the window 1012 // container transaction callback and we want to set the mState immediately. 1013 return null; 1014 } 1015 1016 final PipAnimationController.PipTransitionAnimator<?> animator = animateResizePip( 1017 currentBounds, destinationBounds, sourceHintRect, direction, durationMs, 1018 startingAngle); 1019 if (updateBoundsCallback != null) { 1020 updateBoundsCallback.accept(destinationBounds); 1021 } 1022 return animator; 1023 } 1024 1025 /** 1026 * Directly perform manipulation/resize on the leash. This will not perform any 1027 * {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called. 1028 */ scheduleResizePip(Rect toBounds, Consumer<Rect> updateBoundsCallback)1029 public void scheduleResizePip(Rect toBounds, Consumer<Rect> updateBoundsCallback) { 1030 // Could happen when exitPip 1031 if (mToken == null || mLeash == null) { 1032 Log.w(TAG, "Abort animation, invalid leash"); 1033 return; 1034 } 1035 mPipBoundsState.setBounds(toBounds); 1036 final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); 1037 mSurfaceTransactionHelper 1038 .crop(tx, mLeash, toBounds) 1039 .round(tx, mLeash, mState.isInPip()); 1040 if (mPipMenuController.isMenuVisible()) { 1041 mPipMenuController.resizePipMenu(mLeash, tx, toBounds); 1042 } else { 1043 tx.apply(); 1044 } 1045 if (updateBoundsCallback != null) { 1046 updateBoundsCallback.accept(toBounds); 1047 } 1048 } 1049 1050 /** 1051 * Directly perform manipulation/resize on the leash, along with rotation. This will not perform 1052 * any {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called. 1053 */ scheduleUserResizePip(Rect startBounds, Rect toBounds, Consumer<Rect> updateBoundsCallback)1054 public void scheduleUserResizePip(Rect startBounds, Rect toBounds, 1055 Consumer<Rect> updateBoundsCallback) { 1056 scheduleUserResizePip(startBounds, toBounds, 0 /* degrees */, updateBoundsCallback); 1057 } 1058 1059 /** 1060 * Directly perform a scaled matrix transformation on the leash. This will not perform any 1061 * {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called. 1062 */ scheduleUserResizePip(Rect startBounds, Rect toBounds, float degrees, Consumer<Rect> updateBoundsCallback)1063 public void scheduleUserResizePip(Rect startBounds, Rect toBounds, float degrees, 1064 Consumer<Rect> updateBoundsCallback) { 1065 // Could happen when exitPip 1066 if (mToken == null || mLeash == null) { 1067 Log.w(TAG, "Abort animation, invalid leash"); 1068 return; 1069 } 1070 1071 if (startBounds.isEmpty() || toBounds.isEmpty()) { 1072 Log.w(TAG, "Attempted to user resize PIP to or from empty bounds, aborting."); 1073 return; 1074 } 1075 1076 final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); 1077 mSurfaceTransactionHelper 1078 .scale(tx, mLeash, startBounds, toBounds, degrees) 1079 .round(tx, mLeash, startBounds, toBounds); 1080 if (mPipMenuController.isMenuVisible()) { 1081 mPipMenuController.movePipMenu(mLeash, tx, toBounds); 1082 } else { 1083 tx.apply(); 1084 } 1085 if (updateBoundsCallback != null) { 1086 updateBoundsCallback.accept(toBounds); 1087 } 1088 } 1089 1090 /** 1091 * Finish an intermediate resize operation. This is expected to be called after 1092 * {@link #scheduleResizePip}. 1093 */ scheduleFinishResizePip(Rect destinationBounds)1094 public void scheduleFinishResizePip(Rect destinationBounds) { 1095 scheduleFinishResizePip(destinationBounds, null /* updateBoundsCallback */); 1096 } 1097 1098 /** 1099 * Same as {@link #scheduleFinishResizePip} but with a callback. 1100 */ scheduleFinishResizePip(Rect destinationBounds, Consumer<Rect> updateBoundsCallback)1101 public void scheduleFinishResizePip(Rect destinationBounds, 1102 Consumer<Rect> updateBoundsCallback) { 1103 scheduleFinishResizePip(destinationBounds, TRANSITION_DIRECTION_NONE, updateBoundsCallback); 1104 } 1105 1106 /** 1107 * Finish an intermediate resize operation. This is expected to be called after 1108 * {@link #scheduleResizePip}. 1109 * 1110 * @param destinationBounds the final bounds of the PIP after resizing 1111 * @param direction the transition direction 1112 * @param updateBoundsCallback a callback to invoke after finishing the resize 1113 */ scheduleFinishResizePip(Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, Consumer<Rect> updateBoundsCallback)1114 public void scheduleFinishResizePip(Rect destinationBounds, 1115 @PipAnimationController.TransitionDirection int direction, 1116 Consumer<Rect> updateBoundsCallback) { 1117 if (mState.shouldBlockResizeRequest()) { 1118 return; 1119 } 1120 1121 finishResize(createFinishResizeSurfaceTransaction(destinationBounds), destinationBounds, 1122 direction, -1); 1123 if (updateBoundsCallback != null) { 1124 updateBoundsCallback.accept(destinationBounds); 1125 } 1126 } 1127 createFinishResizeSurfaceTransaction( Rect destinationBounds)1128 private SurfaceControl.Transaction createFinishResizeSurfaceTransaction( 1129 Rect destinationBounds) { 1130 final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); 1131 mSurfaceTransactionHelper 1132 .crop(tx, mLeash, destinationBounds) 1133 .resetScale(tx, mLeash, destinationBounds) 1134 .round(tx, mLeash, mState.isInPip()); 1135 return tx; 1136 } 1137 1138 /** 1139 * Offset the PiP window by a given offset on Y-axis, triggered also from screen rotation. 1140 */ scheduleOffsetPip(Rect originalBounds, int offset, int duration, Consumer<Rect> updateBoundsCallback)1141 public void scheduleOffsetPip(Rect originalBounds, int offset, int duration, 1142 Consumer<Rect> updateBoundsCallback) { 1143 if (mState.shouldBlockResizeRequest()) { 1144 return; 1145 } 1146 if (mWaitForFixedRotation) { 1147 Log.d(TAG, "skip scheduleOffsetPip, entering pip deferred"); 1148 return; 1149 } 1150 offsetPip(originalBounds, 0 /* xOffset */, offset, duration); 1151 Rect toBounds = new Rect(originalBounds); 1152 toBounds.offset(0, offset); 1153 if (updateBoundsCallback != null) { 1154 updateBoundsCallback.accept(toBounds); 1155 } 1156 } 1157 offsetPip(Rect originalBounds, int xOffset, int yOffset, int durationMs)1158 private void offsetPip(Rect originalBounds, int xOffset, int yOffset, int durationMs) { 1159 if (mTaskInfo == null) { 1160 Log.w(TAG, "mTaskInfo is not set"); 1161 return; 1162 } 1163 final Rect destinationBounds = new Rect(originalBounds); 1164 destinationBounds.offset(xOffset, yOffset); 1165 animateResizePip(originalBounds, destinationBounds, null /* sourceHintRect */, 1166 TRANSITION_DIRECTION_SAME, durationMs, 0); 1167 } 1168 finishResize(SurfaceControl.Transaction tx, Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, @PipAnimationController.AnimationType int type)1169 private void finishResize(SurfaceControl.Transaction tx, Rect destinationBounds, 1170 @PipAnimationController.TransitionDirection int direction, 1171 @PipAnimationController.AnimationType int type) { 1172 final Rect preResizeBounds = new Rect(mPipBoundsState.getBounds()); 1173 mPipBoundsState.setBounds(destinationBounds); 1174 if (direction == TRANSITION_DIRECTION_REMOVE_STACK) { 1175 removePipImmediately(); 1176 return; 1177 } else if (isInPipDirection(direction) && type == ANIM_TYPE_ALPHA) { 1178 // TODO: Synchronize this correctly in #applyEnterPipSyncTransaction 1179 finishResizeForMenu(destinationBounds); 1180 return; 1181 } 1182 1183 WindowContainerTransaction wct = new WindowContainerTransaction(); 1184 prepareFinishResizeTransaction(destinationBounds, direction, tx, wct); 1185 1186 // Only corner drag, pinch or expand/un-expand resizing may lead to animating the finish 1187 // resize operation. 1188 final boolean mayAnimateFinishResize = direction == TRANSITION_DIRECTION_USER_RESIZE 1189 || direction == TRANSITION_DIRECTION_SNAP_AFTER_RESIZE 1190 || direction == TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; 1191 // Animate with a cross-fade if enabled and seamless resize is disables by the app. 1192 final boolean animateCrossFadeResize = mayAnimateFinishResize 1193 && mPictureInPictureParams != null 1194 && !mPictureInPictureParams.isSeamlessResizeEnabled(); 1195 if (animateCrossFadeResize) { 1196 // Take a snapshot of the PIP task and show it. We'll fade it out after the wct 1197 // transaction is applied and the activity is laid out again. 1198 preResizeBounds.offsetTo(0, 0); 1199 final Rect snapshotDest = new Rect(0, 0, destinationBounds.width(), 1200 destinationBounds.height()); 1201 // Note: Put this at layer=MAX_VALUE-2 since the input consumer for PIP is placed at 1202 // MAX_VALUE-1 1203 final SurfaceControl snapshotSurface = ScreenshotUtils.takeScreenshot( 1204 mSurfaceControlTransactionFactory.getTransaction(), mLeash, preResizeBounds, 1205 Integer.MAX_VALUE - 2); 1206 if (snapshotSurface != null) { 1207 mSyncTransactionQueue.queue(wct); 1208 mSyncTransactionQueue.runInSync(t -> { 1209 // Scale the snapshot from its pre-resize bounds to the post-resize bounds. 1210 mSurfaceTransactionHelper.scale(t, snapshotSurface, preResizeBounds, 1211 snapshotDest); 1212 1213 // Start animation to fade out the snapshot. 1214 fadeOutAndRemoveOverlay(snapshotSurface, 1215 null /* callback */, false /* withStartDelay */); 1216 }); 1217 } else { 1218 applyFinishBoundsResize(wct, direction); 1219 } 1220 } else { 1221 applyFinishBoundsResize(wct, direction); 1222 } 1223 1224 finishResizeForMenu(destinationBounds); 1225 } 1226 1227 /** Moves the PiP menu to the destination bounds. */ finishResizeForMenu(Rect destinationBounds)1228 public void finishResizeForMenu(Rect destinationBounds) { 1229 if (!isInPip()) { 1230 return; 1231 } 1232 mPipMenuController.movePipMenu(null, null, destinationBounds); 1233 mPipMenuController.updateMenuBounds(destinationBounds); 1234 } 1235 prepareFinishResizeTransaction(Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, SurfaceControl.Transaction tx, WindowContainerTransaction wct)1236 private void prepareFinishResizeTransaction(Rect destinationBounds, 1237 @PipAnimationController.TransitionDirection int direction, 1238 SurfaceControl.Transaction tx, 1239 WindowContainerTransaction wct) { 1240 final Rect taskBounds; 1241 if (isInPipDirection(direction)) { 1242 // If we are animating from fullscreen using a bounds animation, then reset the 1243 // activity windowing mode set by WM, and set the task bounds to the final bounds 1244 taskBounds = destinationBounds; 1245 wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); 1246 } else if (isOutPipDirection(direction)) { 1247 // If we are animating to fullscreen or split screen, then we need to reset the 1248 // override bounds on the task to ensure that the task "matches" the parent's bounds. 1249 taskBounds = null; 1250 applyWindowingModeChangeOnExit(wct, direction); 1251 } else { 1252 // Just a resize in PIP 1253 taskBounds = destinationBounds; 1254 } 1255 mSurfaceTransactionHelper.round(tx, mLeash, isInPip()); 1256 1257 wct.setBounds(mToken, taskBounds); 1258 wct.setBoundsChangeTransaction(mToken, tx); 1259 } 1260 1261 /** 1262 * Applies the window container transaction to finish a bounds resize. 1263 * 1264 * Called by {@link #finishResize(SurfaceControl.Transaction, Rect, int, int)}} once it has 1265 * finished preparing the transaction. It allows subclasses to modify the transaction before 1266 * applying it. 1267 */ applyFinishBoundsResize(@onNull WindowContainerTransaction wct, @PipAnimationController.TransitionDirection int direction)1268 public void applyFinishBoundsResize(@NonNull WindowContainerTransaction wct, 1269 @PipAnimationController.TransitionDirection int direction) { 1270 mTaskOrganizer.applyTransaction(wct); 1271 } 1272 1273 /** 1274 * The windowing mode to restore to when resizing out of PIP direction. Defaults to undefined 1275 * and can be overridden to restore to an alternate windowing mode. 1276 */ getOutPipWindowingMode()1277 public int getOutPipWindowingMode() { 1278 // By default, simply reset the windowing mode to undefined. 1279 return WINDOWING_MODE_UNDEFINED; 1280 } 1281 animateResizePip( Rect currentBounds, Rect destinationBounds, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, int durationMs, float startingAngle)1282 private @Nullable PipAnimationController.PipTransitionAnimator<?> animateResizePip( 1283 Rect currentBounds, Rect destinationBounds, Rect sourceHintRect, 1284 @PipAnimationController.TransitionDirection int direction, int durationMs, 1285 float startingAngle) { 1286 // Could happen when exitPip 1287 if (mToken == null || mLeash == null) { 1288 Log.w(TAG, "Abort animation, invalid leash"); 1289 return null; 1290 } 1291 final int rotationDelta = mWaitForFixedRotation 1292 ? deltaRotation(mCurrentRotation, mNextRotation) 1293 : Surface.ROTATION_0; 1294 if (rotationDelta != Surface.ROTATION_0) { 1295 sourceHintRect = computeRotatedBounds(rotationDelta, direction, destinationBounds, 1296 sourceHintRect); 1297 } 1298 Rect baseBounds = direction == TRANSITION_DIRECTION_SNAP_AFTER_RESIZE 1299 ? mPipBoundsState.getBounds() : currentBounds; 1300 final PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController 1301 .getAnimator(mTaskInfo, mLeash, baseBounds, currentBounds, destinationBounds, 1302 sourceHintRect, direction, startingAngle, rotationDelta); 1303 animator.setTransitionDirection(direction) 1304 .setPipAnimationCallback(mPipAnimationCallback) 1305 .setPipTransactionHandler(mPipTransactionHandler) 1306 .setDuration(durationMs); 1307 if (isInPipDirection(direction)) { 1308 // Similar to auto-enter-pip transition, we use content overlay when there is no 1309 // source rect hint to enter PiP use bounds animation. 1310 if (sourceHintRect == null) animator.setUseContentOverlay(mContext); 1311 // The destination bounds are used for the end rect of animation and the final bounds 1312 // after animation finishes. So after the animation is started, the destination bounds 1313 // can be updated to new rotation (computeRotatedBounds has changed the DisplayLayout 1314 // without affecting the animation. 1315 if (rotationDelta != Surface.ROTATION_0) { 1316 animator.setDestinationBounds(mPipBoundsAlgorithm.getEntryDestinationBounds()); 1317 } 1318 } 1319 animator.start(); 1320 return animator; 1321 } 1322 1323 /** Computes destination bounds in old rotation and returns source hint rect if available. */ computeRotatedBounds(int rotationDelta, int direction, Rect outDestinationBounds, Rect sourceHintRect)1324 private @Nullable Rect computeRotatedBounds(int rotationDelta, int direction, 1325 Rect outDestinationBounds, Rect sourceHintRect) { 1326 if (direction == TRANSITION_DIRECTION_TO_PIP) { 1327 mPipBoundsState.getDisplayLayout().rotateTo(mContext.getResources(), mNextRotation); 1328 final Rect displayBounds = mPipBoundsState.getDisplayBounds(); 1329 outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds()); 1330 // Transform the destination bounds to current display coordinates. 1331 rotateBounds(outDestinationBounds, displayBounds, mNextRotation, mCurrentRotation); 1332 // When entering PiP (from button navigation mode), adjust the source rect hint by 1333 // display cutout if applicable. 1334 if (sourceHintRect != null && mTaskInfo.displayCutoutInsets != null) { 1335 if (rotationDelta == Surface.ROTATION_270) { 1336 sourceHintRect.offset(mTaskInfo.displayCutoutInsets.left, 1337 mTaskInfo.displayCutoutInsets.top); 1338 } 1339 } 1340 } else if (direction == TRANSITION_DIRECTION_LEAVE_PIP) { 1341 final Rect rotatedDestinationBounds = new Rect(outDestinationBounds); 1342 rotateBounds(rotatedDestinationBounds, mPipBoundsState.getDisplayBounds(), 1343 rotationDelta); 1344 return PipBoundsAlgorithm.getValidSourceHintRect(mPictureInPictureParams, 1345 rotatedDestinationBounds); 1346 } 1347 return sourceHintRect; 1348 } 1349 1350 /** 1351 * Sync with {@link LegacySplitScreenController} on destination bounds if PiP is going to split 1352 * screen. 1353 * 1354 * @param destinationBoundsOut contain the updated destination bounds if applicable 1355 * @return {@code true} if destinationBounds is altered for split screen 1356 */ syncWithSplitScreenBounds(Rect destinationBoundsOut)1357 private boolean syncWithSplitScreenBounds(Rect destinationBoundsOut) { 1358 if (!mSplitScreenOptional.isPresent()) { 1359 return false; 1360 } 1361 1362 LegacySplitScreenController legacySplitScreen = mSplitScreenOptional.get(); 1363 if (!legacySplitScreen.isDividerVisible()) { 1364 // fail early if system is not in split screen mode 1365 return false; 1366 } 1367 1368 // PiP window will go to split-secondary mode instead of fullscreen, populates the 1369 // split screen bounds here. 1370 destinationBoundsOut.set(legacySplitScreen.getDividerView() 1371 .getNonMinimizedSplitScreenSecondaryBounds()); 1372 return true; 1373 } 1374 1375 /** 1376 * Fades out and removes an overlay surface. 1377 */ fadeOutAndRemoveOverlay(SurfaceControl surface, Runnable callback, boolean withStartDelay)1378 private void fadeOutAndRemoveOverlay(SurfaceControl surface, Runnable callback, 1379 boolean withStartDelay) { 1380 if (surface == null) { 1381 return; 1382 } 1383 1384 final ValueAnimator animator = ValueAnimator.ofFloat(1.0f, 0.0f); 1385 animator.setDuration(mCrossFadeAnimationDuration); 1386 animator.addUpdateListener(animation -> { 1387 if (mState == State.UNDEFINED) { 1388 // Could happen if onTaskVanished happens during the animation since we may have 1389 // set a start delay on this animation. 1390 Log.d(TAG, "Task vanished, skip fadeOutAndRemoveOverlay"); 1391 animation.removeAllListeners(); 1392 animation.removeAllUpdateListeners(); 1393 animation.cancel(); 1394 } else { 1395 final float alpha = (float) animation.getAnimatedValue(); 1396 final SurfaceControl.Transaction transaction = 1397 mSurfaceControlTransactionFactory.getTransaction(); 1398 transaction.setAlpha(surface, alpha); 1399 transaction.apply(); 1400 } 1401 }); 1402 animator.addListener(new AnimatorListenerAdapter() { 1403 @Override 1404 public void onAnimationEnd(Animator animation) { 1405 removeContentOverlay(surface, callback); 1406 } 1407 }); 1408 animator.setStartDelay(withStartDelay ? CONTENT_OVERLAY_FADE_OUT_DELAY_MS : 0); 1409 animator.start(); 1410 } 1411 removeContentOverlay(SurfaceControl surface, Runnable callback)1412 private void removeContentOverlay(SurfaceControl surface, Runnable callback) { 1413 if (mState == State.UNDEFINED) { 1414 // Avoid double removal, which is fatal. 1415 return; 1416 } 1417 final SurfaceControl.Transaction tx = 1418 mSurfaceControlTransactionFactory.getTransaction(); 1419 tx.remove(surface); 1420 tx.apply(); 1421 if (callback != null) callback.run(); 1422 } 1423 1424 /** 1425 * Dumps internal states. 1426 */ 1427 @Override dump(PrintWriter pw, String prefix)1428 public void dump(PrintWriter pw, String prefix) { 1429 final String innerPrefix = prefix + " "; 1430 pw.println(prefix + TAG); 1431 pw.println(innerPrefix + "mTaskInfo=" + mTaskInfo); 1432 pw.println(innerPrefix + "mToken=" + mToken 1433 + " binder=" + (mToken != null ? mToken.asBinder() : null)); 1434 pw.println(innerPrefix + "mLeash=" + mLeash); 1435 pw.println(innerPrefix + "mState=" + mState); 1436 pw.println(innerPrefix + "mOneShotAnimationType=" + mOneShotAnimationType); 1437 pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams); 1438 } 1439 1440 @Override toString()1441 public String toString() { 1442 return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_PIP); 1443 } 1444 } 1445