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.ActivityTaskManager.INVALID_TASK_ID; 20 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 21 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; 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.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; 29 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; 30 import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; 31 import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_BOUNDS; 32 import static com.android.wm.shell.pip.PipAnimationController.FRACTION_START; 33 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; 34 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; 35 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN; 36 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_NONE; 37 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; 38 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SAME; 39 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SNAP_AFTER_RESIZE; 40 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; 41 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE; 42 import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection; 43 import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; 44 import static com.android.wm.shell.pip.PipAnimationController.isRemovePipDirection; 45 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; 46 import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; 47 import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; 48 import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; 49 50 import android.animation.Animator; 51 import android.animation.AnimatorListenerAdapter; 52 import android.animation.ValueAnimator; 53 import android.annotation.NonNull; 54 import android.annotation.Nullable; 55 import android.app.ActivityManager; 56 import android.app.ActivityTaskManager; 57 import android.app.PictureInPictureParams; 58 import android.app.TaskInfo; 59 import android.content.ComponentName; 60 import android.content.Context; 61 import android.content.pm.ActivityInfo; 62 import android.content.res.Configuration; 63 import android.graphics.Rect; 64 import android.os.RemoteException; 65 import android.os.SystemClock; 66 import android.os.SystemProperties; 67 import android.util.Log; 68 import android.view.Choreographer; 69 import android.view.Display; 70 import android.view.Surface; 71 import android.view.SurfaceControl; 72 import android.window.TaskOrganizer; 73 import android.window.TaskSnapshot; 74 import android.window.WindowContainerToken; 75 import android.window.WindowContainerTransaction; 76 77 import com.android.internal.annotations.VisibleForTesting; 78 import com.android.internal.protolog.common.ProtoLog; 79 import com.android.wm.shell.R; 80 import com.android.wm.shell.ShellTaskOrganizer; 81 import com.android.wm.shell.animation.Interpolators; 82 import com.android.wm.shell.common.DisplayController; 83 import com.android.wm.shell.common.DisplayLayout; 84 import com.android.wm.shell.common.ScreenshotUtils; 85 import com.android.wm.shell.common.ShellExecutor; 86 import com.android.wm.shell.common.SyncTransactionQueue; 87 import com.android.wm.shell.common.annotations.ShellMainThread; 88 import com.android.wm.shell.pip.phone.PipMotionHelper; 89 import com.android.wm.shell.pip.phone.PipSizeSpecHandler; 90 import com.android.wm.shell.protolog.ShellProtoLogGroup; 91 import com.android.wm.shell.splitscreen.SplitScreenController; 92 import com.android.wm.shell.transition.Transitions; 93 94 import java.io.PrintWriter; 95 import java.util.Objects; 96 import java.util.Optional; 97 import java.util.function.Consumer; 98 import java.util.function.IntConsumer; 99 100 /** 101 * Manages PiP tasks such as resize and offset. 102 * 103 * This class listens on {@link TaskOrganizer} callbacks for windowing mode change 104 * both to and from PiP and issues corresponding animation if applicable. 105 * Normally, we apply series of {@link SurfaceControl.Transaction} when the animator is running 106 * and files a final {@link WindowContainerTransaction} at the end of the transition. 107 * 108 * This class is also responsible for general resize/offset PiP operations within SysUI component, 109 * see also {@link PipMotionHelper}. 110 */ 111 public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, 112 DisplayController.OnDisplaysChangedListener, ShellTaskOrganizer.FocusListener { 113 private static final String TAG = PipTaskOrganizer.class.getSimpleName(); 114 private static final boolean DEBUG = false; 115 /** 116 * The alpha type is set for swiping to home. But the swiped task may not enter PiP. And if 117 * another task enters PiP by non-swipe ways, e.g. call API in foreground or switch to 3-button 118 * navigation, then the alpha type is unexpected. 119 */ 120 private static final int ONE_SHOT_ALPHA_ANIMATION_TIMEOUT_MS = 1000; 121 122 /** 123 * The fixed start delay in ms when fading out the content overlay from bounds animation. 124 * This is to overcome the flicker caused by configuration change when rotating from landscape 125 * to portrait PiP in button navigation mode. 126 */ 127 private static final int CONTENT_OVERLAY_FADE_OUT_DELAY_MS = 500; 128 129 private final Context mContext; 130 private final SyncTransactionQueue mSyncTransactionQueue; 131 private final PipBoundsState mPipBoundsState; 132 private final PipSizeSpecHandler mPipSizeSpecHandler; 133 private final PipBoundsAlgorithm mPipBoundsAlgorithm; 134 private final @NonNull PipMenuController mPipMenuController; 135 private final PipAnimationController mPipAnimationController; 136 protected final PipTransitionController mPipTransitionController; 137 protected final PipParamsChangedForwarder mPipParamsChangedForwarder; 138 private final PipUiEventLogger mPipUiEventLoggerLogger; 139 private final int mEnterAnimationDuration; 140 private final int mExitAnimationDuration; 141 private final int mCrossFadeAnimationDuration; 142 private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; 143 private final Optional<SplitScreenController> mSplitScreenOptional; 144 protected final ShellTaskOrganizer mTaskOrganizer; 145 protected final ShellExecutor mMainExecutor; 146 147 // These callbacks are called on the update thread 148 private final PipAnimationController.PipAnimationCallback mPipAnimationCallback = 149 new PipAnimationController.PipAnimationCallback() { 150 private boolean mIsCancelled; 151 @Override 152 public void onPipAnimationStart(TaskInfo taskInfo, 153 PipAnimationController.PipTransitionAnimator animator) { 154 final int direction = animator.getTransitionDirection(); 155 mIsCancelled = false; 156 sendOnPipTransitionStarted(direction); 157 } 158 159 @Override 160 public void onPipAnimationEnd(TaskInfo taskInfo, SurfaceControl.Transaction tx, 161 PipAnimationController.PipTransitionAnimator animator) { 162 final int direction = animator.getTransitionDirection(); 163 if (mIsCancelled) { 164 sendOnPipTransitionFinished(direction); 165 return; 166 } 167 final int animationType = animator.getAnimationType(); 168 final Rect destinationBounds = animator.getDestinationBounds(); 169 if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) { 170 fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(), 171 animator::clearContentOverlay, true /* withStartDelay*/); 172 } 173 if (mWaitForFixedRotation && animationType == ANIM_TYPE_BOUNDS 174 && direction == TRANSITION_DIRECTION_TO_PIP) { 175 // Notify the display to continue the deferred orientation change. 176 final WindowContainerTransaction wct = new WindowContainerTransaction(); 177 wct.scheduleFinishEnterPip(mToken, destinationBounds); 178 mTaskOrganizer.applyTransaction(wct); 179 // The final task bounds will be applied by onFixedRotationFinished so that all 180 // coordinates are in new rotation. 181 mSurfaceTransactionHelper.round(tx, mLeash, isInPip()); 182 mDeferredAnimEndTransaction = tx; 183 return; 184 } 185 final boolean isExitPipDirection = isOutPipDirection(direction) 186 || isRemovePipDirection(direction); 187 if (mPipTransitionState.getTransitionState() != PipTransitionState.EXITING_PIP 188 || isExitPipDirection) { 189 // Finish resize as long as we're not exiting PIP, or, if we are, only if this is 190 // the end of an exit PIP animation. 191 // This is necessary in case there was a resize animation ongoing when exit PIP 192 // started, in which case the first resize will be skipped to let the exit 193 // operation handle the final resize out of PIP mode. See b/185306679. 194 finishResizeDelayedIfNeeded(() -> { 195 finishResize(tx, destinationBounds, direction, animationType); 196 sendOnPipTransitionFinished(direction); 197 }); 198 } 199 } 200 201 @Override 202 public void onPipAnimationCancel(TaskInfo taskInfo, 203 PipAnimationController.PipTransitionAnimator animator) { 204 final int direction = animator.getTransitionDirection(); 205 mIsCancelled = true; 206 if (isInPipDirection(direction) && animator.getContentOverlayLeash() != null) { 207 fadeOutAndRemoveOverlay(animator.getContentOverlayLeash(), 208 animator::clearContentOverlay, true /* withStartDelay */); 209 } 210 sendOnPipTransitionCancelled(direction); 211 } 212 }; 213 214 /** 215 * Finishes resizing the PiP, delaying the operation if it has to be synced with the PiP menu. 216 * 217 * This is done to avoid a race condition between the last transaction applied in 218 * onPipAnimationUpdate and the finishResize in onPipAnimationEnd. The transaction in 219 * onPipAnimationUpdate is applied directly from WmShell, while onPipAnimationEnd creates a 220 * WindowContainerTransaction in finishResize, which is to be applied by WmCore later. Normally, 221 * the WCT should be the last transaction to finish the animation. However, it may happen that 222 * it gets applied *before* the transaction created by the last onPipAnimationUpdate. This 223 * happens only when the PiP surface transaction has to be synced with the PiP menu due to the 224 * necessity for a delay when syncing the PiP surface animation with the PiP menu surface 225 * animation and redrawing the PiP menu contents. As a result, the PiP surface gets scaled after 226 * the new bounds are applied by WmCore, which makes the PiP surface have unexpected bounds. 227 * 228 * To avoid this, we delay the finishResize operation until 229 * the next frame. This aligns the last onAnimationUpdate transaction with the WCT application. 230 */ finishResizeDelayedIfNeeded(Runnable finishResizeRunnable)231 private void finishResizeDelayedIfNeeded(Runnable finishResizeRunnable) { 232 if (!shouldSyncPipTransactionWithMenu()) { 233 finishResizeRunnable.run(); 234 return; 235 } 236 237 // Delay the finishResize to the next frame 238 Choreographer.getInstance().postCallback(Choreographer.CALLBACK_COMMIT, () -> { 239 mMainExecutor.execute(finishResizeRunnable); 240 }, null); 241 } 242 shouldSyncPipTransactionWithMenu()243 private boolean shouldSyncPipTransactionWithMenu() { 244 return mPipMenuController.isMenuVisible(); 245 } 246 247 @VisibleForTesting 248 final PipTransitionController.PipTransitionCallback mPipTransitionCallback = 249 new PipTransitionController.PipTransitionCallback() { 250 @Override 251 public void onPipTransitionStarted(int direction, Rect pipBounds) {} 252 253 @Override 254 public void onPipTransitionFinished(int direction) { 255 // Apply the deferred RunningTaskInfo if applicable after all proper callbacks 256 // are sent. 257 if (direction == TRANSITION_DIRECTION_TO_PIP && mDeferredTaskInfo != null) { 258 onTaskInfoChanged(mDeferredTaskInfo); 259 mDeferredTaskInfo = null; 260 } 261 } 262 263 @Override 264 public void onPipTransitionCanceled(int direction) {} 265 }; 266 267 private final PipAnimationController.PipTransactionHandler mPipTransactionHandler = 268 new PipAnimationController.PipTransactionHandler() { 269 @Override 270 public boolean handlePipTransaction(SurfaceControl leash, 271 SurfaceControl.Transaction tx, Rect destinationBounds) { 272 if (shouldSyncPipTransactionWithMenu()) { 273 mPipMenuController.movePipMenu(leash, tx, destinationBounds); 274 return true; 275 } 276 return false; 277 } 278 }; 279 280 private ActivityManager.RunningTaskInfo mTaskInfo; 281 // To handle the edge case that onTaskInfoChanged callback is received during the entering 282 // PiP transition, where we do not want to intercept the transition but still want to apply the 283 // changed RunningTaskInfo when it finishes. 284 private ActivityManager.RunningTaskInfo mDeferredTaskInfo; 285 private WindowContainerToken mToken; 286 private SurfaceControl mLeash; 287 protected PipTransitionState mPipTransitionState; 288 private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS; 289 private long mLastOneShotAlphaAnimationTime; 290 private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory 291 mSurfaceControlTransactionFactory; 292 protected PictureInPictureParams mPictureInPictureParams; 293 private IntConsumer mOnDisplayIdChangeCallback; 294 /** 295 * The end transaction of PiP animation for switching between PiP and fullscreen with 296 * orientation change. The transaction should be applied after the display is rotated. 297 */ 298 private SurfaceControl.Transaction mDeferredAnimEndTransaction; 299 /** Whether the existing PiP is hidden by alpha. */ 300 private boolean mHasFadeOut; 301 302 /** 303 * If set to {@code true}, the entering animation will be skipped and we will wait for 304 * {@link #onFixedRotationFinished(int)} callback to actually enter PiP. 305 */ 306 private boolean mWaitForFixedRotation; 307 308 /** 309 * The rotation that the display will apply after expanding PiP to fullscreen. This is only 310 * meaningful if {@link #mWaitForFixedRotation} is true. 311 */ 312 private @Surface.Rotation int mNextRotation; 313 314 private @Surface.Rotation int mCurrentRotation; 315 316 /** 317 * An optional overlay used to mask content changing between an app in/out of PiP, only set if 318 * {@link PipTransitionState#getInSwipePipToHomeTransition()} is true. 319 */ 320 @Nullable 321 SurfaceControl mSwipePipToHomeOverlay; 322 PipTaskOrganizer(Context context, @NonNull SyncTransactionQueue syncTransactionQueue, @NonNull PipTransitionState pipTransitionState, @NonNull PipBoundsState pipBoundsState, @NonNull PipSizeSpecHandler pipSizeSpecHandler, @NonNull PipBoundsAlgorithm boundsHandler, @NonNull PipMenuController pipMenuController, @NonNull PipAnimationController pipAnimationController, @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, @NonNull PipTransitionController pipTransitionController, @NonNull PipParamsChangedForwarder pipParamsChangedForwarder, Optional<SplitScreenController> splitScreenOptional, @NonNull DisplayController displayController, @NonNull PipUiEventLogger pipUiEventLogger, @NonNull ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor)323 public PipTaskOrganizer(Context context, 324 @NonNull SyncTransactionQueue syncTransactionQueue, 325 @NonNull PipTransitionState pipTransitionState, 326 @NonNull PipBoundsState pipBoundsState, 327 @NonNull PipSizeSpecHandler pipSizeSpecHandler, 328 @NonNull PipBoundsAlgorithm boundsHandler, 329 @NonNull PipMenuController pipMenuController, 330 @NonNull PipAnimationController pipAnimationController, 331 @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, 332 @NonNull PipTransitionController pipTransitionController, 333 @NonNull PipParamsChangedForwarder pipParamsChangedForwarder, 334 Optional<SplitScreenController> splitScreenOptional, 335 @NonNull DisplayController displayController, 336 @NonNull PipUiEventLogger pipUiEventLogger, 337 @NonNull ShellTaskOrganizer shellTaskOrganizer, 338 @ShellMainThread ShellExecutor mainExecutor) { 339 mContext = context; 340 mSyncTransactionQueue = syncTransactionQueue; 341 mPipTransitionState = pipTransitionState; 342 mPipBoundsState = pipBoundsState; 343 mPipSizeSpecHandler = pipSizeSpecHandler; 344 mPipBoundsAlgorithm = boundsHandler; 345 mPipMenuController = pipMenuController; 346 mPipTransitionController = pipTransitionController; 347 mPipParamsChangedForwarder = pipParamsChangedForwarder; 348 mEnterAnimationDuration = context.getResources() 349 .getInteger(R.integer.config_pipEnterAnimationDuration); 350 mExitAnimationDuration = context.getResources() 351 .getInteger(R.integer.config_pipExitAnimationDuration); 352 mCrossFadeAnimationDuration = context.getResources() 353 .getInteger(R.integer.config_pipCrossfadeAnimationDuration); 354 mSurfaceTransactionHelper = surfaceTransactionHelper; 355 mPipAnimationController = pipAnimationController; 356 mPipUiEventLoggerLogger = pipUiEventLogger; 357 mSurfaceControlTransactionFactory = 358 new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); 359 mSplitScreenOptional = splitScreenOptional; 360 mTaskOrganizer = shellTaskOrganizer; 361 mMainExecutor = mainExecutor; 362 363 // TODO: Can be removed once wm components are created on the shell-main thread 364 mMainExecutor.execute(() -> { 365 mTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_PIP); 366 }); 367 mTaskOrganizer.addFocusListener(this); 368 mPipTransitionController.setPipOrganizer(this); 369 displayController.addDisplayWindowListener(this); 370 pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback); 371 } 372 getTransitionController()373 public PipTransitionController getTransitionController() { 374 return mPipTransitionController; 375 } 376 getCurrentOrAnimatingBounds()377 public Rect getCurrentOrAnimatingBounds() { 378 PipAnimationController.PipTransitionAnimator animator = 379 mPipAnimationController.getCurrentAnimator(); 380 if (animator != null && animator.isRunning()) { 381 return new Rect(animator.getDestinationBounds()); 382 } 383 return mPipBoundsState.getBounds(); 384 } 385 isInPip()386 public boolean isInPip() { 387 return mPipTransitionState.isInPip(); 388 } 389 isLaunchIntoPipTask()390 private boolean isLaunchIntoPipTask() { 391 return mPictureInPictureParams != null && mPictureInPictureParams.isLaunchIntoPip(); 392 } 393 394 /** 395 * Returns whether the entry animation is waiting to be started. 396 */ isEntryScheduled()397 public boolean isEntryScheduled() { 398 return mPipTransitionState.getTransitionState() == PipTransitionState.ENTRY_SCHEDULED; 399 } 400 401 /** 402 * Registers a callback when a display change has been detected when we enter PiP. 403 */ registerOnDisplayIdChangeCallback(IntConsumer onDisplayIdChangeCallback)404 public void registerOnDisplayIdChangeCallback(IntConsumer onDisplayIdChangeCallback) { 405 mOnDisplayIdChangeCallback = onDisplayIdChangeCallback; 406 } 407 408 /** 409 * Sets the preferred animation type for one time. 410 * This is typically used to set the animation type to 411 * {@link PipAnimationController#ANIM_TYPE_ALPHA}. 412 */ setOneShotAnimationType(@ipAnimationController.AnimationType int animationType)413 public void setOneShotAnimationType(@PipAnimationController.AnimationType int animationType) { 414 mOneShotAnimationType = animationType; 415 if (animationType == ANIM_TYPE_ALPHA) { 416 mLastOneShotAlphaAnimationTime = SystemClock.uptimeMillis(); 417 } 418 } 419 420 /** 421 * Callback when Launcher starts swipe-pip-to-home operation. 422 * @return {@link Rect} for destination bounds. 423 */ startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, PictureInPictureParams pictureInPictureParams)424 public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, 425 PictureInPictureParams pictureInPictureParams) { 426 mPipTransitionState.setInSwipePipToHomeTransition(true); 427 sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP); 428 setBoundsStateForEntry(componentName, pictureInPictureParams, activityInfo); 429 return mPipBoundsAlgorithm.getEntryDestinationBounds(); 430 } 431 432 /** 433 * Callback when launcher finishes swipe-pip-to-home operation. 434 * Expect {@link #onTaskAppeared(ActivityManager.RunningTaskInfo, SurfaceControl)} afterwards. 435 */ stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds, SurfaceControl overlay)436 public void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds, 437 SurfaceControl overlay) { 438 // do nothing if there is no startSwipePipToHome being called before 439 if (!mPipTransitionState.getInSwipePipToHomeTransition()) { 440 return; 441 } 442 mPipBoundsState.setBounds(destinationBounds); 443 mSwipePipToHomeOverlay = overlay; 444 if (ENABLE_SHELL_TRANSITIONS && overlay != null) { 445 // With Shell transition, the overlay was attached to the remote transition leash, which 446 // will be removed when the current transition is finished, so we need to reparent it 447 // to the actual Task surface now. 448 // PipTransition is responsible to fade it out and cleanup when finishing the enter PIP 449 // transition. 450 final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); 451 mTaskOrganizer.reparentChildSurfaceToTask(taskId, overlay, t); 452 t.setLayer(overlay, Integer.MAX_VALUE); 453 t.apply(); 454 } 455 } 456 getTaskInfo()457 public ActivityManager.RunningTaskInfo getTaskInfo() { 458 return mTaskInfo; 459 } 460 getSurfaceControl()461 public SurfaceControl getSurfaceControl() { 462 return mLeash; 463 } 464 setBoundsStateForEntry(ComponentName componentName, PictureInPictureParams params, ActivityInfo activityInfo)465 private void setBoundsStateForEntry(ComponentName componentName, 466 PictureInPictureParams params, ActivityInfo activityInfo) { 467 mPipBoundsState.setBoundsStateForEntry(componentName, activityInfo, params, 468 mPipBoundsAlgorithm); 469 } 470 471 /** 472 * Expands PiP to the previous bounds, this is done in two phases using 473 * {@link WindowContainerTransaction} 474 * - setActivityWindowingMode to either fullscreen or split-secondary at beginning of the 475 * transaction. without changing the windowing mode of the Task itself. This makes sure the 476 * activity render it's final configuration while the Task is still in PiP. 477 * - setWindowingMode to undefined at the end of transition 478 * @param animationDurationMs duration in millisecond for the exiting PiP transition 479 * @param requestEnterSplit whether the enterSplit button is pressed on PiP or not. 480 * Indicate the user wishes to directly put PiP into split screen 481 * mode. 482 */ exitPip(int animationDurationMs, boolean requestEnterSplit)483 public void exitPip(int animationDurationMs, boolean requestEnterSplit) { 484 if (!mPipTransitionState.isInPip() 485 || mPipTransitionState.getTransitionState() == PipTransitionState.EXITING_PIP 486 || mToken == null) { 487 ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 488 "%s: Not allowed to exitPip in current state" 489 + " mState=%d mToken=%s", TAG, mPipTransitionState.getTransitionState(), 490 mToken); 491 return; 492 } 493 494 final WindowContainerTransaction wct = new WindowContainerTransaction(); 495 if (isLaunchIntoPipTask()) { 496 exitLaunchIntoPipTask(wct); 497 return; 498 } 499 500 if (ENABLE_SHELL_TRANSITIONS) { 501 if (requestEnterSplit && mSplitScreenOptional.isPresent()) { 502 mSplitScreenOptional.get().prepareEnterSplitScreen(wct, mTaskInfo, 503 isPipTopLeft() 504 ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT); 505 mPipTransitionController.startExitTransition( 506 TRANSIT_EXIT_PIP_TO_SPLIT, wct, null /* destinationBounds */); 507 return; 508 } 509 } 510 511 final Rect destinationBounds = getExitDestinationBounds(); 512 final int direction = syncWithSplitScreenBounds(destinationBounds, requestEnterSplit) 513 ? TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN 514 : TRANSITION_DIRECTION_LEAVE_PIP; 515 516 if (Transitions.ENABLE_SHELL_TRANSITIONS && direction == TRANSITION_DIRECTION_LEAVE_PIP) { 517 // When exit to fullscreen with Shell transition enabled, we update the Task windowing 518 // mode directly so that it can also trigger display rotation and visibility update in 519 // the same transition if there will be any. 520 wct.setWindowingMode(mToken, getOutPipWindowingMode()); 521 // We can inherit the parent bounds as it is going to be fullscreen. The 522 // destinationBounds calculated above will be incorrect if this is with rotation. 523 wct.setBounds(mToken, null); 524 } else { 525 final SurfaceControl.Transaction tx = 526 mSurfaceControlTransactionFactory.getTransaction(); 527 mSurfaceTransactionHelper.scale(tx, mLeash, destinationBounds, 528 mPipBoundsState.getBounds()); 529 tx.setWindowCrop(mLeash, destinationBounds.width(), destinationBounds.height()); 530 // We set to fullscreen here for now, but later it will be set to UNDEFINED for 531 // the proper windowing mode to take place. See #applyWindowingModeChangeOnExit. 532 wct.setActivityWindowingMode(mToken, WINDOWING_MODE_FULLSCREEN); 533 wct.setBounds(mToken, destinationBounds); 534 wct.setBoundsChangeTransaction(mToken, tx); 535 } 536 537 // Cancel the existing animator if there is any. 538 // TODO(b/232439933): this is disabled temporarily to unblock b/234502692. 539 // cancelCurrentAnimator(); 540 541 // Set the exiting state first so if there is fixed rotation later, the running animation 542 // won't be interrupted by alpha animation for existing PiP. 543 mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP); 544 545 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 546 mPipTransitionController.startExitTransition(TRANSIT_EXIT_PIP, wct, destinationBounds); 547 return; 548 } 549 if (mSplitScreenOptional.isPresent()) { 550 // If pip activity will reparent to origin task case and if the origin task still under 551 // split root, just exit split screen here to ensure it could expand to fullscreen. 552 SplitScreenController split = mSplitScreenOptional.get(); 553 if (split.isTaskInSplitScreen(mTaskInfo.lastParentTaskIdBeforePip)) { 554 split.exitSplitScreen(INVALID_TASK_ID, 555 SplitScreenController.EXIT_REASON_APP_FINISHED); 556 } 557 } 558 mSyncTransactionQueue.queue(wct); 559 mSyncTransactionQueue.runInSync(t -> { 560 // Make sure to grab the latest source hint rect as it could have been 561 // updated right after applying the windowing mode change. 562 final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( 563 mPictureInPictureParams, destinationBounds); 564 final PipAnimationController.PipTransitionAnimator<?> animator = 565 animateResizePip(mPipBoundsState.getBounds(), destinationBounds, sourceHintRect, 566 direction, animationDurationMs, 0 /* startingAngle */); 567 if (animator != null) { 568 // Even though the animation was started above, re-apply the transaction for the 569 // first frame using the SurfaceControl.Transaction supplied by the 570 // SyncTransactionQueue. This is necessary because the initial surface transform 571 // may not be applied until the next frame if a different Transaction than the one 572 // supplied is used, resulting in 1 frame not being cropped to the source rect 573 // hint during expansion that causes a visible jank/flash. See b/184166183. 574 animator.applySurfaceControlTransaction(mLeash, t, FRACTION_START); 575 } 576 }); 577 } 578 579 /** Returns the bounds to restore to when exiting PIP mode. */ getExitDestinationBounds()580 public Rect getExitDestinationBounds() { 581 return mPipBoundsState.getDisplayBounds(); 582 } 583 exitLaunchIntoPipTask(WindowContainerTransaction wct)584 private void exitLaunchIntoPipTask(WindowContainerTransaction wct) { 585 wct.startTask(mTaskInfo.launchIntoPipHostTaskId, null /* ActivityOptions */); 586 mTaskOrganizer.applyTransaction(wct); 587 588 // Remove the PiP with fade-out animation right after the host Task is brought to front. 589 removePip(); 590 } 591 applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction)592 private void applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction) { 593 // Reset the final windowing mode. 594 wct.setWindowingMode(mToken, getOutPipWindowingMode()); 595 // Simply reset the activity mode set prior to the animation running. 596 wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); 597 } 598 599 /** 600 * Removes PiP immediately. 601 */ removePip()602 public void removePip() { 603 if (!mPipTransitionState.isInPip() || mToken == null) { 604 ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 605 "%s: Not allowed to removePip in current state" 606 + " mState=%d mToken=%s", TAG, mPipTransitionState.getTransitionState(), 607 mToken); 608 return; 609 } 610 611 // removePipImmediately is expected when the following animation finishes. 612 ValueAnimator animator = mPipAnimationController 613 .getAnimator(mTaskInfo, mLeash, mPipBoundsState.getBounds(), 614 1f /* alphaStart */, 0f /* alphaEnd */) 615 .setTransitionDirection(TRANSITION_DIRECTION_REMOVE_STACK) 616 .setPipTransactionHandler(mPipTransactionHandler) 617 .setPipAnimationCallback(mPipAnimationCallback); 618 animator.setDuration(mExitAnimationDuration); 619 animator.setInterpolator(Interpolators.ALPHA_OUT); 620 animator.start(); 621 mPipTransitionState.setTransitionState(PipTransitionState.EXITING_PIP); 622 } 623 removePipImmediately()624 private void removePipImmediately() { 625 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 626 final WindowContainerTransaction wct = new WindowContainerTransaction(); 627 wct.setBounds(mToken, null); 628 wct.setWindowingMode(mToken, getOutPipWindowingMode()); 629 wct.reorder(mToken, false); 630 mPipTransitionController.startExitTransition(TRANSIT_REMOVE_PIP, wct, 631 null /* destinationBounds */); 632 return; 633 } 634 635 try { 636 // Reset the task bounds first to ensure the activity configuration is reset as well 637 final WindowContainerTransaction wct = new WindowContainerTransaction(); 638 wct.setBounds(mToken, null); 639 mTaskOrganizer.applyTransaction(wct); 640 641 ActivityTaskManager.getService().removeRootTasksInWindowingModes( 642 new int[]{ WINDOWING_MODE_PINNED }); 643 } catch (RemoteException e) { 644 ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 645 "%s: Failed to remove PiP, %s", 646 TAG, e); 647 } 648 } 649 650 @Override onTaskAppeared(ActivityManager.RunningTaskInfo info, SurfaceControl leash)651 public void onTaskAppeared(ActivityManager.RunningTaskInfo info, SurfaceControl leash) { 652 Objects.requireNonNull(info, "Requires RunningTaskInfo"); 653 mTaskInfo = info; 654 mToken = mTaskInfo.token; 655 mPipTransitionState.setTransitionState(PipTransitionState.TASK_APPEARED); 656 mLeash = leash; 657 mPictureInPictureParams = mTaskInfo.pictureInPictureParams; 658 setBoundsStateForEntry(mTaskInfo.topActivity, mPictureInPictureParams, 659 mTaskInfo.topActivityInfo); 660 if (mPictureInPictureParams != null) { 661 mPipParamsChangedForwarder.notifyActionsChanged(mPictureInPictureParams.getActions(), 662 mPictureInPictureParams.getCloseAction()); 663 mPipParamsChangedForwarder.notifyTitleChanged( 664 mPictureInPictureParams.getTitle()); 665 mPipParamsChangedForwarder.notifySubtitleChanged( 666 mPictureInPictureParams.getSubtitle()); 667 } 668 669 mPipUiEventLoggerLogger.setTaskInfo(mTaskInfo); 670 671 // If the displayId of the task is different than what PipBoundsHandler has, then update 672 // it. This is possible if we entered PiP on an external display. 673 if (info.displayId != mPipBoundsState.getDisplayId() 674 && mOnDisplayIdChangeCallback != null) { 675 mOnDisplayIdChangeCallback.accept(info.displayId); 676 } 677 678 // UiEvent logging. 679 final PipUiEventLogger.PipUiEventEnum uiEventEnum; 680 if (isLaunchIntoPipTask()) { 681 uiEventEnum = PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER_CONTENT_PIP; 682 } else if (mPipTransitionState.getInSwipePipToHomeTransition()) { 683 uiEventEnum = PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_AUTO_ENTER; 684 } else { 685 uiEventEnum = PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER; 686 } 687 mPipUiEventLoggerLogger.log(uiEventEnum); 688 689 if (mPipTransitionState.getInSwipePipToHomeTransition()) { 690 if (!mWaitForFixedRotation) { 691 onEndOfSwipePipToHomeTransition(); 692 } else { 693 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 694 "%s: Defer onTaskAppeared-SwipePipToHome until end of fixed rotation.", 695 TAG); 696 } 697 return; 698 } 699 700 if (mOneShotAnimationType == ANIM_TYPE_ALPHA 701 && SystemClock.uptimeMillis() - mLastOneShotAlphaAnimationTime 702 > ONE_SHOT_ALPHA_ANIMATION_TIMEOUT_MS) { 703 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 704 "%s: Alpha animation is expired. Use bounds animation.", TAG); 705 mOneShotAnimationType = ANIM_TYPE_BOUNDS; 706 } 707 708 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 709 // For Shell transition, we will animate the window in PipTransition#startAnimation 710 // instead of #onTaskAppeared. 711 return; 712 } 713 714 if (mWaitForFixedRotation) { 715 onTaskAppearedWithFixedRotation(); 716 return; 717 } 718 719 final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); 720 Objects.requireNonNull(destinationBounds, "Missing destination bounds"); 721 final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); 722 723 if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { 724 mPipMenuController.attach(mLeash); 725 final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( 726 info.pictureInPictureParams, currentBounds); 727 scheduleAnimateResizePip(currentBounds, destinationBounds, 0 /* startingAngle */, 728 sourceHintRect, TRANSITION_DIRECTION_TO_PIP, mEnterAnimationDuration, 729 null /* updateBoundsCallback */); 730 mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); 731 } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { 732 enterPipWithAlphaAnimation(destinationBounds, mEnterAnimationDuration); 733 mOneShotAnimationType = ANIM_TYPE_BOUNDS; 734 } else { 735 throw new RuntimeException("Unrecognized animation type: " + mOneShotAnimationType); 736 } 737 } 738 onTaskAppearedWithFixedRotation()739 private void onTaskAppearedWithFixedRotation() { 740 if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { 741 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 742 "%s: Defer entering PiP alpha animation, fixed rotation is ongoing", TAG); 743 // If deferred, hside the surface till fixed rotation is completed. 744 final SurfaceControl.Transaction tx = 745 mSurfaceControlTransactionFactory.getTransaction(); 746 tx.setAlpha(mLeash, 0f); 747 tx.show(mLeash); 748 tx.apply(); 749 mOneShotAnimationType = ANIM_TYPE_BOUNDS; 750 return; 751 } 752 final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); 753 final Rect sourceHintRect = PipBoundsAlgorithm.getValidSourceHintRect( 754 mPictureInPictureParams, currentBounds); 755 final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); 756 animateResizePip(currentBounds, destinationBounds, sourceHintRect, 757 TRANSITION_DIRECTION_TO_PIP, mEnterAnimationDuration, 0 /* startingAngle */); 758 mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); 759 } 760 761 /** 762 * Called when the display rotation handling is skipped (e.g. when rotation happens while in 763 * the middle of an entry transition). 764 */ onDisplayRotationSkipped()765 public void onDisplayRotationSkipped() { 766 if (isEntryScheduled()) { 767 // The PIP animation is scheduled to start with the previous orientation's bounds, 768 // re-calculate the entry bounds and restart the alpha animation. 769 final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); 770 enterPipWithAlphaAnimation(destinationBounds, mEnterAnimationDuration); 771 } 772 } 773 774 @VisibleForTesting enterPipWithAlphaAnimation(Rect destinationBounds, long durationMs)775 void enterPipWithAlphaAnimation(Rect destinationBounds, long durationMs) { 776 // If we are fading the PIP in, then we should move the pip to the final location as 777 // soon as possible, but set the alpha immediately since the transaction can take a 778 // while to process 779 final SurfaceControl.Transaction tx = 780 mSurfaceControlTransactionFactory.getTransaction(); 781 tx.setAlpha(mLeash, 0f); 782 tx.apply(); 783 784 // When entering PiP this transaction will be applied within WindowContainerTransaction and 785 // ensure that the PiP has rounded corners. 786 final SurfaceControl.Transaction boundsChangeTx = 787 mSurfaceControlTransactionFactory.getTransaction(); 788 mSurfaceTransactionHelper 789 .crop(boundsChangeTx, mLeash, destinationBounds) 790 .round(boundsChangeTx, mLeash, true /* applyCornerRadius */); 791 792 mPipTransitionState.setTransitionState(PipTransitionState.ENTRY_SCHEDULED); 793 applyEnterPipSyncTransaction(destinationBounds, () -> { 794 mPipAnimationController 795 .getAnimator(mTaskInfo, mLeash, destinationBounds, 0f, 1f) 796 .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) 797 .setPipAnimationCallback(mPipAnimationCallback) 798 .setPipTransactionHandler(mPipTransactionHandler) 799 .setDuration(durationMs) 800 .start(); 801 // mState is set right after the animation is kicked off to block any resize 802 // requests such as offsetPip that may have been called prior to the transition. 803 mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); 804 }, boundsChangeTx); 805 } 806 onEndOfSwipePipToHomeTransition()807 private void onEndOfSwipePipToHomeTransition() { 808 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 809 return; 810 } 811 812 final Rect destinationBounds = mPipBoundsState.getBounds(); 813 final SurfaceControl swipeToHomeOverlay = mSwipePipToHomeOverlay; 814 final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); 815 mSurfaceTransactionHelper 816 .resetScale(tx, mLeash, destinationBounds) 817 .crop(tx, mLeash, destinationBounds) 818 .round(tx, mLeash, isInPip()); 819 // The animation is finished in the Launcher and here we directly apply the final touch. 820 applyEnterPipSyncTransaction(destinationBounds, () -> { 821 // Ensure menu's settled in its final bounds first. 822 finishResizeForMenu(destinationBounds); 823 sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); 824 825 // Remove the swipe to home overlay 826 if (swipeToHomeOverlay != null) { 827 fadeOutAndRemoveOverlay(swipeToHomeOverlay, 828 null /* callback */, false /* withStartDelay */); 829 } 830 }, tx); 831 mPipTransitionState.setInSwipePipToHomeTransition(false); 832 mSwipePipToHomeOverlay = null; 833 } 834 applyEnterPipSyncTransaction(Rect destinationBounds, Runnable runnable, @Nullable SurfaceControl.Transaction boundsChangeTransaction)835 private void applyEnterPipSyncTransaction(Rect destinationBounds, Runnable runnable, 836 @Nullable SurfaceControl.Transaction boundsChangeTransaction) { 837 // PiP menu is attached late in the process here to avoid any artifacts on the leash 838 // caused by addShellRoot when in gesture navigation mode. 839 mPipMenuController.attach(mLeash); 840 final WindowContainerTransaction wct = new WindowContainerTransaction(); 841 wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); 842 wct.setBounds(mToken, destinationBounds); 843 if (boundsChangeTransaction != null) { 844 wct.setBoundsChangeTransaction(mToken, boundsChangeTransaction); 845 } 846 mSyncTransactionQueue.queue(wct); 847 if (runnable != null) { 848 mSyncTransactionQueue.runInSync(t -> runnable.run()); 849 } 850 } 851 sendOnPipTransitionStarted( @ipAnimationController.TransitionDirection int direction)852 private void sendOnPipTransitionStarted( 853 @PipAnimationController.TransitionDirection int direction) { 854 if (direction == TRANSITION_DIRECTION_TO_PIP) { 855 mPipTransitionState.setTransitionState(PipTransitionState.ENTERING_PIP); 856 } 857 mPipTransitionController.sendOnPipTransitionStarted(direction); 858 } 859 860 @VisibleForTesting sendOnPipTransitionFinished( @ipAnimationController.TransitionDirection int direction)861 void sendOnPipTransitionFinished( 862 @PipAnimationController.TransitionDirection int direction) { 863 if (direction == TRANSITION_DIRECTION_TO_PIP) { 864 mPipTransitionState.setTransitionState(PipTransitionState.ENTERED_PIP); 865 } 866 mPipTransitionController.sendOnPipTransitionFinished(direction); 867 } 868 sendOnPipTransitionCancelled( @ipAnimationController.TransitionDirection int direction)869 private void sendOnPipTransitionCancelled( 870 @PipAnimationController.TransitionDirection int direction) { 871 mPipTransitionController.sendOnPipTransitionCancelled(direction); 872 } 873 874 /** 875 * Note that dismissing PiP is now originated from SystemUI, see {@link #exitPip(int, boolean)}. 876 * Meanwhile this callback is invoked whenever the task is removed. For instance: 877 * - as a result of removeRootTasksInWindowingModes from WM 878 * - activity itself is died 879 * Nevertheless, we simply update the internal state here as all the heavy lifting should 880 * have been done in WM. 881 */ 882 @Override onTaskVanished(ActivityManager.RunningTaskInfo info)883 public void onTaskVanished(ActivityManager.RunningTaskInfo info) { 884 if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { 885 return; 886 } 887 if (Transitions.ENABLE_SHELL_TRANSITIONS 888 && mPipTransitionState.getTransitionState() == PipTransitionState.EXITING_PIP) { 889 // With Shell transition, we do the cleanup in PipTransition after exiting PIP. 890 return; 891 } 892 final WindowContainerToken token = info.token; 893 Objects.requireNonNull(token, "Requires valid WindowContainerToken"); 894 if (token.asBinder() != mToken.asBinder()) { 895 ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 896 "%s: Unrecognized token: %s", TAG, token); 897 return; 898 } 899 900 cancelCurrentAnimator(); 901 onExitPipFinished(info); 902 903 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 904 mPipTransitionController.forceFinishTransition(); 905 } 906 } 907 908 @Override onTaskInfoChanged(ActivityManager.RunningTaskInfo info)909 public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) { 910 Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken"); 911 if (mPipTransitionState.getTransitionState() != PipTransitionState.ENTERED_PIP 912 && mPipTransitionState.getTransitionState() != PipTransitionState.EXITING_PIP) { 913 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 914 "%s: Defer onTaskInfoChange in current state: %d", TAG, 915 mPipTransitionState.getTransitionState()); 916 // Defer applying PiP parameters if the task is entering PiP to avoid disturbing 917 // the animation. 918 mDeferredTaskInfo = info; 919 return; 920 } 921 mPipBoundsState.setLastPipComponentName(info.topActivity); 922 mPipBoundsState.setOverrideMinSize( 923 mPipBoundsAlgorithm.getMinimalSize(info.topActivityInfo)); 924 final PictureInPictureParams newParams = info.pictureInPictureParams; 925 926 // mPictureInPictureParams is only null if there is no PiP 927 if (newParams == null || mPictureInPictureParams == null) { 928 return; 929 } 930 applyNewPictureInPictureParams(newParams); 931 mPictureInPictureParams = newParams; 932 } 933 934 @Override onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo)935 public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { 936 mPipMenuController.onFocusTaskChanged(taskInfo); 937 } 938 939 @Override supportCompatUI()940 public boolean supportCompatUI() { 941 // PIP doesn't support compat. 942 return false; 943 } 944 945 @Override attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b)946 public void attachChildSurfaceToTask(int taskId, SurfaceControl.Builder b) { 947 b.setParent(findTaskSurface(taskId)); 948 } 949 950 @Override reparentChildSurfaceToTask(int taskId, SurfaceControl sc, SurfaceControl.Transaction t)951 public void reparentChildSurfaceToTask(int taskId, SurfaceControl sc, 952 SurfaceControl.Transaction t) { 953 t.reparent(sc, findTaskSurface(taskId)); 954 } 955 findTaskSurface(int taskId)956 private SurfaceControl findTaskSurface(int taskId) { 957 if (mTaskInfo == null || mLeash == null || mTaskInfo.taskId != taskId) { 958 throw new IllegalArgumentException("There is no surface for taskId=" + taskId); 959 } 960 return mLeash; 961 } 962 963 @Override onFixedRotationStarted(int displayId, int newRotation)964 public void onFixedRotationStarted(int displayId, int newRotation) { 965 mNextRotation = newRotation; 966 mWaitForFixedRotation = true; 967 968 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 969 // The fixed rotation will also be included in the transition info. However, if it is 970 // not a PIP transition (such as open another app to different orientation), 971 // PIP transition handler may not be aware of the fixed rotation start. 972 // Notify the PIP transition handler so that it can fade out the PIP window early for 973 // fixed transition of other windows. 974 mPipTransitionController.onFixedRotationStarted(); 975 return; 976 } 977 978 if (mPipTransitionState.isInPip()) { 979 // Fade out the existing PiP to avoid jump cut during seamless rotation. 980 fadeExistingPip(false /* show */); 981 } 982 } 983 984 @Override onFixedRotationFinished(int displayId)985 public void onFixedRotationFinished(int displayId) { 986 if (!mWaitForFixedRotation) { 987 return; 988 } 989 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 990 clearWaitForFixedRotation(); 991 return; 992 } 993 if (mPipTransitionState.getTransitionState() == PipTransitionState.TASK_APPEARED) { 994 if (mPipTransitionState.getInSwipePipToHomeTransition()) { 995 onEndOfSwipePipToHomeTransition(); 996 } else { 997 // Schedule a regular animation to ensure all the callbacks are still being sent. 998 enterPipWithAlphaAnimation(mPipBoundsAlgorithm.getEntryDestinationBounds(), 999 mEnterAnimationDuration); 1000 } 1001 } else if (mPipTransitionState.getTransitionState() == PipTransitionState.ENTERED_PIP 1002 && mHasFadeOut) { 1003 fadeExistingPip(true /* show */); 1004 } else if (mPipTransitionState.getTransitionState() == PipTransitionState.ENTERING_PIP 1005 && mDeferredAnimEndTransaction != null) { 1006 final PipAnimationController.PipTransitionAnimator<?> animator = 1007 mPipAnimationController.getCurrentAnimator(); 1008 final Rect destinationBounds = animator.getDestinationBounds(); 1009 mPipBoundsState.setBounds(destinationBounds); 1010 applyEnterPipSyncTransaction(destinationBounds, () -> { 1011 finishResizeForMenu(destinationBounds); 1012 sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); 1013 }, mDeferredAnimEndTransaction); 1014 } 1015 clearWaitForFixedRotation(); 1016 } 1017 1018 /** Called when exiting PIP transition is finished to do the state cleanup. */ onExitPipFinished(TaskInfo info)1019 void onExitPipFinished(TaskInfo info) { 1020 if (mLeash == null) { 1021 // TODO(239461594): Remove once the double call to onExitPipFinished() is fixed 1022 Log.w(TAG, "Warning, onExitPipFinished() called multiple times in the same sessino"); 1023 return; 1024 } 1025 1026 clearWaitForFixedRotation(); 1027 if (mSwipePipToHomeOverlay != null) { 1028 removeContentOverlay(mSwipePipToHomeOverlay, null /* callback */); 1029 mSwipePipToHomeOverlay = null; 1030 } 1031 resetShadowRadius(); 1032 mPipTransitionState.setInSwipePipToHomeTransition(false); 1033 mPictureInPictureParams = null; 1034 mPipTransitionState.setTransitionState(PipTransitionState.UNDEFINED); 1035 // Re-set the PIP bounds to none. 1036 mPipBoundsState.setBounds(new Rect()); 1037 mPipUiEventLoggerLogger.setTaskInfo(null); 1038 mPipMenuController.detach(); 1039 mLeash = null; 1040 1041 if (info.displayId != Display.DEFAULT_DISPLAY && mOnDisplayIdChangeCallback != null) { 1042 mOnDisplayIdChangeCallback.accept(Display.DEFAULT_DISPLAY); 1043 } 1044 } 1045 fadeExistingPip(boolean show)1046 private void fadeExistingPip(boolean show) { 1047 if (mLeash == null || !mLeash.isValid()) { 1048 ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 1049 "%s: Invalid leash on fadeExistingPip: %s", TAG, mLeash); 1050 return; 1051 } 1052 final float alphaStart = show ? 0 : 1; 1053 final float alphaEnd = show ? 1 : 0; 1054 mPipAnimationController 1055 .getAnimator(mTaskInfo, mLeash, mPipBoundsState.getBounds(), alphaStart, alphaEnd) 1056 .setTransitionDirection(TRANSITION_DIRECTION_SAME) 1057 .setPipTransactionHandler(mPipTransactionHandler) 1058 .setDuration(show ? mEnterAnimationDuration : mExitAnimationDuration) 1059 .start(); 1060 mHasFadeOut = !show; 1061 } 1062 clearWaitForFixedRotation()1063 private void clearWaitForFixedRotation() { 1064 mWaitForFixedRotation = false; 1065 mDeferredAnimEndTransaction = null; 1066 } 1067 1068 /** Explicitly set the visibility of PiP window. */ setPipVisibility(boolean visible)1069 public void setPipVisibility(boolean visible) { 1070 if (!isInPip()) { 1071 return; 1072 } 1073 if (mLeash == null || !mLeash.isValid()) { 1074 ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 1075 "%s: Invalid leash on setPipVisibility: %s", TAG, mLeash); 1076 return; 1077 } 1078 final SurfaceControl.Transaction tx = 1079 mSurfaceControlTransactionFactory.getTransaction(); 1080 mSurfaceTransactionHelper.alpha(tx, mLeash, visible ? 1f : 0f); 1081 tx.apply(); 1082 } 1083 1084 @Override onDisplayConfigurationChanged(int displayId, Configuration newConfig)1085 public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { 1086 mCurrentRotation = newConfig.windowConfiguration.getRotation(); 1087 } 1088 1089 /** 1090 * Called when display size or font size of settings changed 1091 */ onDensityOrFontScaleChanged(Context context)1092 public void onDensityOrFontScaleChanged(Context context) { 1093 mSurfaceTransactionHelper.onDensityOrFontScaleChanged(context); 1094 } 1095 1096 /** 1097 * TODO(b/152809058): consolidate the display info handling logic in SysUI 1098 * 1099 * @param destinationBoundsOut the current destination bounds will be populated to this param 1100 */ 1101 @SuppressWarnings("unchecked") onMovementBoundsChanged(Rect destinationBoundsOut, boolean fromRotation, boolean fromImeAdjustment, boolean fromShelfAdjustment, WindowContainerTransaction wct)1102 public void onMovementBoundsChanged(Rect destinationBoundsOut, boolean fromRotation, 1103 boolean fromImeAdjustment, boolean fromShelfAdjustment, 1104 WindowContainerTransaction wct) { 1105 // note that this can be called when swipe-to-home or fixed-rotation is happening. 1106 // Skip this entirely if that's the case. 1107 final boolean waitForFixedRotationOnEnteringPip = mWaitForFixedRotation 1108 && (mPipTransitionState.getTransitionState() != PipTransitionState.ENTERED_PIP); 1109 if ((mPipTransitionState.getInSwipePipToHomeTransition() 1110 || waitForFixedRotationOnEnteringPip) && fromRotation) { 1111 if (DEBUG) { 1112 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 1113 "%s: Skip onMovementBoundsChanged on rotation change" 1114 + " InSwipePipToHomeTransition=%b" 1115 + " mWaitForFixedRotation=%b" 1116 + " getTransitionState=%d", TAG, 1117 mPipTransitionState.getInSwipePipToHomeTransition(), mWaitForFixedRotation, 1118 mPipTransitionState.getTransitionState()); 1119 } 1120 return; 1121 } 1122 final PipAnimationController.PipTransitionAnimator animator = 1123 mPipAnimationController.getCurrentAnimator(); 1124 if (animator == null || !animator.isRunning() 1125 || animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) { 1126 final boolean rotatingPip = mPipTransitionState.isInPip() && fromRotation; 1127 if (rotatingPip && Transitions.ENABLE_SHELL_TRANSITIONS) { 1128 // The animation and surface update will be handled by the shell transition handler. 1129 mPipBoundsState.setBounds(destinationBoundsOut); 1130 } else if (rotatingPip && mWaitForFixedRotation && mHasFadeOut) { 1131 // The position will be used by fade-in animation when the fixed rotation is done. 1132 mPipBoundsState.setBounds(destinationBoundsOut); 1133 } else if (rotatingPip) { 1134 // Update bounds state to final destination first. It's important to do this 1135 // before finishing & cancelling the transition animation so that the MotionHelper 1136 // bounds are synchronized to the destination bounds when the animation ends. 1137 mPipBoundsState.setBounds(destinationBoundsOut); 1138 // If we are rotating while there is a current animation, immediately cancel the 1139 // animation (remove the listeners so we don't trigger the normal finish resize 1140 // call that should only happen on the update thread) 1141 int direction = TRANSITION_DIRECTION_NONE; 1142 if (animator != null) { 1143 direction = animator.getTransitionDirection(); 1144 PipAnimationController.quietCancel(animator); 1145 // Do notify the listeners that this was canceled 1146 sendOnPipTransitionCancelled(direction); 1147 sendOnPipTransitionFinished(direction); 1148 } 1149 1150 // Create a reset surface transaction for the new bounds and update the window 1151 // container transaction 1152 final SurfaceControl.Transaction tx = createFinishResizeSurfaceTransaction( 1153 destinationBoundsOut); 1154 prepareFinishResizeTransaction(destinationBoundsOut, direction, tx, wct); 1155 } else { 1156 // There could be an animation on-going. If there is one on-going, last-reported 1157 // bounds isn't yet updated. We'll use the animator's bounds instead. 1158 if (animator != null && animator.isRunning()) { 1159 if (!animator.getDestinationBounds().isEmpty()) { 1160 destinationBoundsOut.set(animator.getDestinationBounds()); 1161 } 1162 } else { 1163 if (!mPipBoundsState.getBounds().isEmpty()) { 1164 destinationBoundsOut.set(mPipBoundsState.getBounds()); 1165 } 1166 } 1167 } 1168 return; 1169 } 1170 1171 final Rect currentDestinationBounds = animator.getDestinationBounds(); 1172 destinationBoundsOut.set(currentDestinationBounds); 1173 if (!fromImeAdjustment && !fromShelfAdjustment 1174 && mPipBoundsState.getDisplayBounds().contains(currentDestinationBounds)) { 1175 // no need to update the destination bounds, bail early 1176 return; 1177 } 1178 1179 final Rect newDestinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); 1180 if (newDestinationBounds.equals(currentDestinationBounds)) return; 1181 updateAnimatorBounds(newDestinationBounds); 1182 destinationBoundsOut.set(newDestinationBounds); 1183 } 1184 1185 /** 1186 * Directly update the animator bounds. 1187 */ updateAnimatorBounds(Rect bounds)1188 public void updateAnimatorBounds(Rect bounds) { 1189 final PipAnimationController.PipTransitionAnimator animator = 1190 mPipAnimationController.getCurrentAnimator(); 1191 if (animator != null && animator.isRunning()) { 1192 if (animator.getAnimationType() == ANIM_TYPE_BOUNDS) { 1193 if (mWaitForFixedRotation) { 1194 // The new destination bounds are in next rotation (DisplayLayout has been 1195 // rotated in computeRotatedBounds). The animation runs in previous rotation so 1196 // the end bounds need to be transformed. 1197 final Rect displayBounds = mPipBoundsState.getDisplayBounds(); 1198 final Rect rotatedEndBounds = new Rect(bounds); 1199 rotateBounds(rotatedEndBounds, displayBounds, mNextRotation, mCurrentRotation); 1200 animator.updateEndValue(rotatedEndBounds); 1201 } else { 1202 animator.updateEndValue(bounds); 1203 } 1204 } 1205 animator.setDestinationBounds(bounds); 1206 } 1207 } 1208 1209 /** 1210 * Handles all changes to the PictureInPictureParams. 1211 */ applyNewPictureInPictureParams(@onNull PictureInPictureParams params)1212 protected void applyNewPictureInPictureParams(@NonNull PictureInPictureParams params) { 1213 if (mDeferredTaskInfo != null || PipUtils.aspectRatioChanged(params.getAspectRatioFloat(), 1214 mPictureInPictureParams.getAspectRatioFloat())) { 1215 if (mPipBoundsAlgorithm.isValidPictureInPictureAspectRatio( 1216 params.getAspectRatioFloat())) { 1217 mPipParamsChangedForwarder.notifyAspectRatioChanged(params.getAspectRatioFloat()); 1218 } else { 1219 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 1220 "%s: New aspect ratio is not valid." 1221 + " hasAspectRatio=%b" 1222 + " aspectRatio=%f", 1223 TAG, params.hasSetAspectRatio(), params.getAspectRatioFloat()); 1224 } 1225 } 1226 if (mDeferredTaskInfo != null 1227 || PipUtils.remoteActionsChanged(params.getActions(), 1228 mPictureInPictureParams.getActions()) 1229 || !PipUtils.remoteActionsMatch(params.getCloseAction(), 1230 mPictureInPictureParams.getCloseAction())) { 1231 mPipParamsChangedForwarder.notifyActionsChanged(params.getActions(), 1232 params.getCloseAction()); 1233 } 1234 } 1235 1236 /** 1237 * Animates resizing of the pinned stack given the duration. 1238 */ scheduleAnimateResizePip(Rect toBounds, int duration, Consumer<Rect> updateBoundsCallback)1239 public void scheduleAnimateResizePip(Rect toBounds, int duration, 1240 Consumer<Rect> updateBoundsCallback) { 1241 scheduleAnimateResizePip(toBounds, duration, TRANSITION_DIRECTION_NONE, 1242 updateBoundsCallback); 1243 } 1244 1245 /** 1246 * Animates resizing of the pinned stack given the duration. 1247 */ scheduleAnimateResizePip(Rect toBounds, int duration, @PipAnimationController.TransitionDirection int direction, Consumer<Rect> updateBoundsCallback)1248 public void scheduleAnimateResizePip(Rect toBounds, int duration, 1249 @PipAnimationController.TransitionDirection int direction, 1250 Consumer<Rect> updateBoundsCallback) { 1251 if (mWaitForFixedRotation) { 1252 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 1253 "%s: skip scheduleAnimateResizePip, entering pip deferred", TAG); 1254 return; 1255 } 1256 scheduleAnimateResizePip(mPipBoundsState.getBounds(), toBounds, 0 /* startingAngle */, 1257 null /* sourceHintRect */, direction, duration, updateBoundsCallback); 1258 } 1259 1260 /** 1261 * Animates resizing of the pinned stack given the duration and start bounds. 1262 * This is used when the starting bounds is not the current PiP bounds. 1263 */ scheduleAnimateResizePip(Rect fromBounds, Rect toBounds, int duration, float startingAngle, Consumer<Rect> updateBoundsCallback)1264 public void scheduleAnimateResizePip(Rect fromBounds, Rect toBounds, int duration, 1265 float startingAngle, Consumer<Rect> updateBoundsCallback) { 1266 if (mWaitForFixedRotation) { 1267 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 1268 "%s: skip scheduleAnimateResizePip, entering pip deferred", TAG); 1269 return; 1270 } 1271 scheduleAnimateResizePip(fromBounds, toBounds, startingAngle, null /* sourceHintRect */, 1272 TRANSITION_DIRECTION_SNAP_AFTER_RESIZE, duration, updateBoundsCallback); 1273 } 1274 1275 /** 1276 * Animates resizing of the pinned stack given the duration and start bounds. 1277 * This always animates the angle to zero from the starting angle. 1278 */ scheduleAnimateResizePip( Rect currentBounds, Rect destinationBounds, float startingAngle, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, int durationMs, Consumer<Rect> updateBoundsCallback)1279 private @Nullable PipAnimationController.PipTransitionAnimator<?> scheduleAnimateResizePip( 1280 Rect currentBounds, Rect destinationBounds, float startingAngle, Rect sourceHintRect, 1281 @PipAnimationController.TransitionDirection int direction, int durationMs, 1282 Consumer<Rect> updateBoundsCallback) { 1283 if (!mPipTransitionState.isInPip()) { 1284 // TODO: tend to use shouldBlockResizeRequest here as well but need to consider 1285 // the fact that when in exitPip, scheduleAnimateResizePip is executed in the window 1286 // container transaction callback and we want to set the mState immediately. 1287 return null; 1288 } 1289 1290 final PipAnimationController.PipTransitionAnimator<?> animator = animateResizePip( 1291 currentBounds, destinationBounds, sourceHintRect, direction, durationMs, 1292 startingAngle); 1293 if (updateBoundsCallback != null) { 1294 updateBoundsCallback.accept(destinationBounds); 1295 } 1296 return animator; 1297 } 1298 1299 /** 1300 * Directly perform manipulation/resize on the leash. This will not perform any 1301 * {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called. 1302 */ scheduleResizePip(Rect toBounds, Consumer<Rect> updateBoundsCallback)1303 public void scheduleResizePip(Rect toBounds, Consumer<Rect> updateBoundsCallback) { 1304 // Could happen when exitPip 1305 if (mToken == null || mLeash == null) { 1306 ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 1307 "%s: Abort animation, invalid leash", TAG); 1308 return; 1309 } 1310 mPipBoundsState.setBounds(toBounds); 1311 final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); 1312 mSurfaceTransactionHelper 1313 .crop(tx, mLeash, toBounds) 1314 .round(tx, mLeash, mPipTransitionState.isInPip()); 1315 if (shouldSyncPipTransactionWithMenu()) { 1316 mPipMenuController.resizePipMenu(mLeash, tx, toBounds); 1317 } else { 1318 tx.apply(); 1319 } 1320 if (updateBoundsCallback != null) { 1321 updateBoundsCallback.accept(toBounds); 1322 } 1323 } 1324 1325 /** 1326 * Directly perform manipulation/resize on the leash, along with rotation. This will not perform 1327 * any {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called. 1328 */ scheduleUserResizePip(Rect startBounds, Rect toBounds, Consumer<Rect> updateBoundsCallback)1329 public void scheduleUserResizePip(Rect startBounds, Rect toBounds, 1330 Consumer<Rect> updateBoundsCallback) { 1331 scheduleUserResizePip(startBounds, toBounds, 0 /* degrees */, updateBoundsCallback); 1332 } 1333 1334 /** 1335 * Directly perform a scaled matrix transformation on the leash. This will not perform any 1336 * {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called. 1337 */ scheduleUserResizePip(Rect startBounds, Rect toBounds, float degrees, Consumer<Rect> updateBoundsCallback)1338 public void scheduleUserResizePip(Rect startBounds, Rect toBounds, float degrees, 1339 Consumer<Rect> updateBoundsCallback) { 1340 // Could happen when exitPip 1341 if (mToken == null || mLeash == null) { 1342 ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 1343 "%s: Abort animation, invalid leash", TAG); 1344 return; 1345 } 1346 1347 if (startBounds.isEmpty() || toBounds.isEmpty()) { 1348 ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 1349 "%s: Attempted to user resize PIP to or from empty bounds, aborting.", TAG); 1350 return; 1351 } 1352 1353 final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); 1354 mSurfaceTransactionHelper 1355 .scale(tx, mLeash, startBounds, toBounds, degrees) 1356 .round(tx, mLeash, startBounds, toBounds); 1357 if (shouldSyncPipTransactionWithMenu()) { 1358 mPipMenuController.movePipMenu(mLeash, tx, toBounds); 1359 } else { 1360 tx.apply(); 1361 } 1362 if (updateBoundsCallback != null) { 1363 updateBoundsCallback.accept(toBounds); 1364 } 1365 } 1366 1367 /** 1368 * Finish an intermediate resize operation. This is expected to be called after 1369 * {@link #scheduleResizePip}. 1370 */ scheduleFinishResizePip(Rect destinationBounds)1371 public void scheduleFinishResizePip(Rect destinationBounds) { 1372 scheduleFinishResizePip(destinationBounds, null /* updateBoundsCallback */); 1373 } 1374 1375 /** 1376 * Same as {@link #scheduleFinishResizePip} but with a callback. 1377 */ scheduleFinishResizePip(Rect destinationBounds, Consumer<Rect> updateBoundsCallback)1378 public void scheduleFinishResizePip(Rect destinationBounds, 1379 Consumer<Rect> updateBoundsCallback) { 1380 scheduleFinishResizePip(destinationBounds, TRANSITION_DIRECTION_NONE, updateBoundsCallback); 1381 } 1382 1383 /** 1384 * Finish an intermediate resize operation. This is expected to be called after 1385 * {@link #scheduleResizePip}. 1386 * 1387 * @param destinationBounds the final bounds of the PIP after resizing 1388 * @param direction the transition direction 1389 * @param updateBoundsCallback a callback to invoke after finishing the resize 1390 */ scheduleFinishResizePip(Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, Consumer<Rect> updateBoundsCallback)1391 public void scheduleFinishResizePip(Rect destinationBounds, 1392 @PipAnimationController.TransitionDirection int direction, 1393 Consumer<Rect> updateBoundsCallback) { 1394 if (mPipTransitionState.shouldBlockResizeRequest()) { 1395 return; 1396 } 1397 1398 if (mLeash == null || !mLeash.isValid()) { 1399 Log.e(TAG, String.format("scheduleFinishResizePip with null leash! mState=%d", 1400 mPipTransitionState.getTransitionState())); 1401 return; 1402 } 1403 1404 finishResize(createFinishResizeSurfaceTransaction(destinationBounds), destinationBounds, 1405 direction, -1); 1406 if (updateBoundsCallback != null) { 1407 updateBoundsCallback.accept(destinationBounds); 1408 } 1409 } 1410 createFinishResizeSurfaceTransaction( Rect destinationBounds)1411 private SurfaceControl.Transaction createFinishResizeSurfaceTransaction( 1412 Rect destinationBounds) { 1413 final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); 1414 mSurfaceTransactionHelper 1415 .crop(tx, mLeash, destinationBounds) 1416 .resetScale(tx, mLeash, destinationBounds) 1417 .round(tx, mLeash, mPipTransitionState.isInPip()); 1418 return tx; 1419 } 1420 1421 /** 1422 * Offset the PiP window by a given offset on Y-axis, triggered also from screen rotation. 1423 */ scheduleOffsetPip(Rect originalBounds, int offset, int duration, Consumer<Rect> updateBoundsCallback)1424 public void scheduleOffsetPip(Rect originalBounds, int offset, int duration, 1425 Consumer<Rect> updateBoundsCallback) { 1426 if (mPipTransitionState.shouldBlockResizeRequest() 1427 || mPipTransitionState.getInSwipePipToHomeTransition()) { 1428 return; 1429 } 1430 if (mWaitForFixedRotation) { 1431 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 1432 "%s: skip scheduleOffsetPip, entering pip deferred", TAG); 1433 return; 1434 } 1435 offsetPip(originalBounds, 0 /* xOffset */, offset, duration); 1436 Rect toBounds = new Rect(originalBounds); 1437 toBounds.offset(0, offset); 1438 if (updateBoundsCallback != null) { 1439 updateBoundsCallback.accept(toBounds); 1440 } 1441 } 1442 offsetPip(Rect originalBounds, int xOffset, int yOffset, int durationMs)1443 private void offsetPip(Rect originalBounds, int xOffset, int yOffset, int durationMs) { 1444 if (mTaskInfo == null) { 1445 ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: mTaskInfo is not set", 1446 TAG); 1447 return; 1448 } 1449 final Rect destinationBounds = new Rect(originalBounds); 1450 destinationBounds.offset(xOffset, yOffset); 1451 animateResizePip(originalBounds, destinationBounds, null /* sourceHintRect */, 1452 TRANSITION_DIRECTION_SAME, durationMs, 0); 1453 } 1454 finishResize(SurfaceControl.Transaction tx, Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, @PipAnimationController.AnimationType int type)1455 private void finishResize(SurfaceControl.Transaction tx, Rect destinationBounds, 1456 @PipAnimationController.TransitionDirection int direction, 1457 @PipAnimationController.AnimationType int type) { 1458 final Rect preResizeBounds = new Rect(mPipBoundsState.getBounds()); 1459 mPipBoundsState.setBounds(destinationBounds); 1460 if (direction == TRANSITION_DIRECTION_REMOVE_STACK) { 1461 removePipImmediately(); 1462 return; 1463 } else if (isInPipDirection(direction) && type == ANIM_TYPE_ALPHA) { 1464 // TODO: Synchronize this correctly in #applyEnterPipSyncTransaction 1465 finishResizeForMenu(destinationBounds); 1466 return; 1467 } 1468 1469 WindowContainerTransaction wct = new WindowContainerTransaction(); 1470 prepareFinishResizeTransaction(destinationBounds, direction, tx, wct); 1471 1472 // Only corner drag, pinch or expand/un-expand resizing may lead to animating the finish 1473 // resize operation. 1474 final boolean mayAnimateFinishResize = direction == TRANSITION_DIRECTION_USER_RESIZE 1475 || direction == TRANSITION_DIRECTION_SNAP_AFTER_RESIZE 1476 || direction == TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; 1477 // Animate with a cross-fade if enabled and seamless resize is disables by the app. 1478 final boolean animateCrossFadeResize = mayAnimateFinishResize 1479 && mPictureInPictureParams != null 1480 && !mPictureInPictureParams.isSeamlessResizeEnabled(); 1481 if (animateCrossFadeResize) { 1482 // Take a snapshot of the PIP task and show it. We'll fade it out after the wct 1483 // transaction is applied and the activity is laid out again. 1484 preResizeBounds.offsetTo(0, 0); 1485 final Rect snapshotDest = new Rect(0, 0, destinationBounds.width(), 1486 destinationBounds.height()); 1487 // Note: Put this at layer=MAX_VALUE-2 since the input consumer for PIP is placed at 1488 // MAX_VALUE-1 1489 final SurfaceControl snapshotSurface = ScreenshotUtils.takeScreenshot( 1490 mSurfaceControlTransactionFactory.getTransaction(), mLeash, preResizeBounds, 1491 Integer.MAX_VALUE - 2); 1492 if (snapshotSurface != null) { 1493 mSyncTransactionQueue.queue(wct); 1494 mSyncTransactionQueue.runInSync(t -> { 1495 // Scale the snapshot from its pre-resize bounds to the post-resize bounds. 1496 mSurfaceTransactionHelper.scale(t, snapshotSurface, preResizeBounds, 1497 snapshotDest); 1498 1499 // Start animation to fade out the snapshot. 1500 fadeOutAndRemoveOverlay(snapshotSurface, 1501 null /* callback */, false /* withStartDelay */); 1502 }); 1503 } else { 1504 applyFinishBoundsResize(wct, direction, false); 1505 } 1506 } else { 1507 applyFinishBoundsResize(wct, direction, isPipToTopLeft()); 1508 // Use sync transaction to apply finish transaction for enter split case. 1509 if (direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { 1510 mSyncTransactionQueue.runInSync(t -> { 1511 t.merge(tx); 1512 }); 1513 } 1514 } 1515 1516 finishResizeForMenu(destinationBounds); 1517 } 1518 1519 /** Moves the PiP menu to the destination bounds. */ finishResizeForMenu(Rect destinationBounds)1520 public void finishResizeForMenu(Rect destinationBounds) { 1521 if (!isInPip()) { 1522 return; 1523 } 1524 mPipMenuController.movePipMenu(null, null, destinationBounds); 1525 mPipMenuController.updateMenuBounds(destinationBounds); 1526 } 1527 prepareFinishResizeTransaction(Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, SurfaceControl.Transaction tx, WindowContainerTransaction wct)1528 private void prepareFinishResizeTransaction(Rect destinationBounds, 1529 @PipAnimationController.TransitionDirection int direction, 1530 SurfaceControl.Transaction tx, 1531 WindowContainerTransaction wct) { 1532 final Rect taskBounds; 1533 if (isInPipDirection(direction)) { 1534 // If we are animating from fullscreen using a bounds animation, then reset the 1535 // activity windowing mode set by WM, and set the task bounds to the final bounds 1536 taskBounds = destinationBounds; 1537 wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); 1538 } else if (isOutPipDirection(direction)) { 1539 // If we are animating to fullscreen or split screen, then we need to reset the 1540 // override bounds on the task to ensure that the task "matches" the parent's bounds. 1541 taskBounds = null; 1542 applyWindowingModeChangeOnExit(wct, direction); 1543 } else { 1544 // Just a resize in PIP 1545 taskBounds = destinationBounds; 1546 } 1547 mSurfaceTransactionHelper.round(tx, mLeash, isInPip()); 1548 1549 wct.setBounds(mToken, taskBounds); 1550 // Pip to split should use sync transaction to sync split bounds change. 1551 if (direction != TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { 1552 wct.setBoundsChangeTransaction(mToken, tx); 1553 } 1554 } 1555 1556 /** 1557 * Applies the window container transaction to finish a bounds resize. 1558 * 1559 * Called by {@link #finishResize(SurfaceControl.Transaction, Rect, int, int)}} once it has 1560 * finished preparing the transaction. It allows subclasses to modify the transaction before 1561 * applying it. 1562 */ applyFinishBoundsResize(@onNull WindowContainerTransaction wct, @PipAnimationController.TransitionDirection int direction, boolean wasPipTopLeft)1563 public void applyFinishBoundsResize(@NonNull WindowContainerTransaction wct, 1564 @PipAnimationController.TransitionDirection int direction, boolean wasPipTopLeft) { 1565 if (direction == TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN) { 1566 mSplitScreenOptional.ifPresent(splitScreenController -> 1567 splitScreenController.enterSplitScreen(mTaskInfo.taskId, wasPipTopLeft, wct)); 1568 } else { 1569 mTaskOrganizer.applyTransaction(wct); 1570 } 1571 } 1572 isPipTopLeft()1573 private boolean isPipTopLeft() { 1574 if (!mSplitScreenOptional.isPresent()) { 1575 return false; 1576 } 1577 final Rect topLeft = new Rect(); 1578 final Rect bottomRight = new Rect(); 1579 mSplitScreenOptional.get().getStageBounds(topLeft, bottomRight); 1580 1581 return topLeft.contains(mPipBoundsState.getBounds()); 1582 } 1583 isPipToTopLeft()1584 private boolean isPipToTopLeft() { 1585 if (!mSplitScreenOptional.isPresent()) { 1586 return false; 1587 } 1588 return mSplitScreenOptional.get().getActivateSplitPosition(mTaskInfo) 1589 == SPLIT_POSITION_TOP_OR_LEFT; 1590 } 1591 1592 /** 1593 * The windowing mode to restore to when resizing out of PIP direction. Defaults to undefined 1594 * and can be overridden to restore to an alternate windowing mode. 1595 */ getOutPipWindowingMode()1596 public int getOutPipWindowingMode() { 1597 // By default, simply reset the windowing mode to undefined. 1598 return WINDOWING_MODE_UNDEFINED; 1599 } 1600 animateResizePip( Rect currentBounds, Rect destinationBounds, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, int durationMs, float startingAngle)1601 private @Nullable PipAnimationController.PipTransitionAnimator<?> animateResizePip( 1602 Rect currentBounds, Rect destinationBounds, Rect sourceHintRect, 1603 @PipAnimationController.TransitionDirection int direction, int durationMs, 1604 float startingAngle) { 1605 // Could happen when exitPip 1606 if (mToken == null || mLeash == null) { 1607 ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 1608 "%s: Abort animation, invalid leash", TAG); 1609 return null; 1610 } 1611 if (isInPipDirection(direction) 1612 && !isSourceRectHintValidForEnterPip(sourceHintRect, destinationBounds)) { 1613 // The given source rect hint is too small for enter PiP animation, reset it to null. 1614 sourceHintRect = null; 1615 } 1616 final int rotationDelta = mWaitForFixedRotation 1617 ? deltaRotation(mCurrentRotation, mNextRotation) 1618 : Surface.ROTATION_0; 1619 if (rotationDelta != Surface.ROTATION_0) { 1620 sourceHintRect = computeRotatedBounds(rotationDelta, direction, destinationBounds, 1621 sourceHintRect); 1622 } 1623 Rect baseBounds = direction == TRANSITION_DIRECTION_SNAP_AFTER_RESIZE 1624 ? mPipBoundsState.getBounds() : currentBounds; 1625 final boolean existingAnimatorRunning = mPipAnimationController.getCurrentAnimator() != null 1626 && mPipAnimationController.getCurrentAnimator().isRunning(); 1627 final PipAnimationController.PipTransitionAnimator<?> animator = mPipAnimationController 1628 .getAnimator(mTaskInfo, mLeash, baseBounds, currentBounds, destinationBounds, 1629 sourceHintRect, direction, startingAngle, rotationDelta); 1630 animator.setTransitionDirection(direction) 1631 .setPipTransactionHandler(mPipTransactionHandler) 1632 .setDuration(durationMs); 1633 if (!existingAnimatorRunning) { 1634 animator.setPipAnimationCallback(mPipAnimationCallback); 1635 } 1636 if (isInPipDirection(direction)) { 1637 // Similar to auto-enter-pip transition, we use content overlay when there is no 1638 // source rect hint to enter PiP use bounds animation. 1639 if (sourceHintRect == null) { 1640 if (SystemProperties.getBoolean( 1641 "persist.wm.debug.enable_pip_app_icon_overlay", true)) { 1642 animator.setAppIconContentOverlay( 1643 mContext, currentBounds, mTaskInfo.topActivityInfo, 1644 mPipBoundsState.getLauncherState().getAppIconSizePx()); 1645 } else { 1646 animator.setColorContentOverlay(mContext); 1647 } 1648 } else { 1649 final TaskSnapshot snapshot = PipUtils.getTaskSnapshot( 1650 mTaskInfo.launchIntoPipHostTaskId, false /* isLowResolution */); 1651 if (snapshot != null) { 1652 // use the task snapshot during the animation, this is for 1653 // launch-into-pip aka. content-pip use case. 1654 animator.setSnapshotContentOverlay(snapshot, sourceHintRect); 1655 } 1656 } 1657 // The destination bounds are used for the end rect of animation and the final bounds 1658 // after animation finishes. So after the animation is started, the destination bounds 1659 // can be updated to new rotation (computeRotatedBounds has changed the DisplayLayout 1660 // without affecting the animation. 1661 if (rotationDelta != Surface.ROTATION_0) { 1662 animator.setDestinationBounds(mPipBoundsAlgorithm.getEntryDestinationBounds()); 1663 } 1664 } 1665 animator.start(); 1666 return animator; 1667 } 1668 1669 /** Computes destination bounds in old rotation and returns source hint rect if available. */ computeRotatedBounds(int rotationDelta, int direction, Rect outDestinationBounds, Rect sourceHintRect)1670 private @Nullable Rect computeRotatedBounds(int rotationDelta, int direction, 1671 Rect outDestinationBounds, Rect sourceHintRect) { 1672 if (direction == TRANSITION_DIRECTION_TO_PIP) { 1673 DisplayLayout layoutCopy = mPipBoundsState.getDisplayLayout(); 1674 1675 layoutCopy.rotateTo(mContext.getResources(), mNextRotation); 1676 mPipBoundsState.setDisplayLayout(layoutCopy); 1677 mPipSizeSpecHandler.setDisplayLayout(layoutCopy); 1678 1679 final Rect displayBounds = mPipBoundsState.getDisplayBounds(); 1680 outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds()); 1681 // Transform the destination bounds to current display coordinates. 1682 rotateBounds(outDestinationBounds, displayBounds, mNextRotation, mCurrentRotation); 1683 // When entering PiP (from button navigation mode), adjust the source rect hint by 1684 // display cutout if applicable. 1685 if (sourceHintRect != null && mTaskInfo.displayCutoutInsets != null) { 1686 if (rotationDelta == Surface.ROTATION_270) { 1687 sourceHintRect.offset(mTaskInfo.displayCutoutInsets.left, 1688 mTaskInfo.displayCutoutInsets.top); 1689 } 1690 } 1691 } else if (direction == TRANSITION_DIRECTION_LEAVE_PIP) { 1692 final Rect rotatedDestinationBounds = new Rect(outDestinationBounds); 1693 rotateBounds(rotatedDestinationBounds, mPipBoundsState.getDisplayBounds(), 1694 rotationDelta); 1695 return PipBoundsAlgorithm.getValidSourceHintRect(mPictureInPictureParams, 1696 rotatedDestinationBounds); 1697 } 1698 return sourceHintRect; 1699 } 1700 1701 /** 1702 * This is a situation in which the source rect hint on at least one axis is smaller 1703 * than the destination bounds, which represents a problem because we would have to scale 1704 * up that axis to fit the bounds. So instead, just fallback to the non-source hint 1705 * animation in this case. 1706 * 1707 * @return {@code false} if the given source is too small to use for the entering animation. 1708 */ isSourceRectHintValidForEnterPip(Rect sourceRectHint, Rect destinationBounds)1709 private boolean isSourceRectHintValidForEnterPip(Rect sourceRectHint, Rect destinationBounds) { 1710 return sourceRectHint != null 1711 && sourceRectHint.width() > destinationBounds.width() 1712 && sourceRectHint.height() > destinationBounds.height(); 1713 } 1714 1715 /** 1716 * Sync with {@link SplitScreenController} on destination bounds if PiP is going to 1717 * split screen. 1718 * 1719 * @param destinationBoundsOut contain the updated destination bounds if applicable 1720 * @return {@code true} if destinationBounds is altered for split screen 1721 */ syncWithSplitScreenBounds(Rect destinationBoundsOut, boolean enterSplit)1722 private boolean syncWithSplitScreenBounds(Rect destinationBoundsOut, boolean enterSplit) { 1723 if (!enterSplit || !mSplitScreenOptional.isPresent()) { 1724 return false; 1725 } 1726 final Rect topLeft = new Rect(); 1727 final Rect bottomRight = new Rect(); 1728 mSplitScreenOptional.get().getStageBounds(topLeft, bottomRight); 1729 destinationBoundsOut.set(isPipToTopLeft() ? topLeft : bottomRight); 1730 return true; 1731 } 1732 1733 /** 1734 * Fades out and removes an overlay surface. 1735 */ fadeOutAndRemoveOverlay(SurfaceControl surface, Runnable callback, boolean withStartDelay)1736 void fadeOutAndRemoveOverlay(SurfaceControl surface, Runnable callback, 1737 boolean withStartDelay) { 1738 if (surface == null || !surface.isValid()) { 1739 return; 1740 } 1741 1742 final ValueAnimator animator = ValueAnimator.ofFloat(1.0f, 0.0f); 1743 animator.setDuration(mCrossFadeAnimationDuration); 1744 animator.addUpdateListener(animation -> { 1745 if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { 1746 // Could happen if onTaskVanished happens during the animation since we may have 1747 // set a start delay on this animation. 1748 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 1749 "%s: Task vanished, skip fadeOutAndRemoveOverlay", TAG); 1750 PipAnimationController.quietCancel(animation); 1751 } else if (surface.isValid()) { 1752 final float alpha = (float) animation.getAnimatedValue(); 1753 final SurfaceControl.Transaction transaction = 1754 mSurfaceControlTransactionFactory.getTransaction(); 1755 transaction.setAlpha(surface, alpha); 1756 transaction.apply(); 1757 } 1758 }); 1759 animator.addListener(new AnimatorListenerAdapter() { 1760 @Override 1761 public void onAnimationEnd(Animator animation) { 1762 removeContentOverlay(surface, callback); 1763 } 1764 }); 1765 animator.setStartDelay(withStartDelay ? CONTENT_OVERLAY_FADE_OUT_DELAY_MS : 0); 1766 animator.start(); 1767 } 1768 removeContentOverlay(SurfaceControl surface, Runnable callback)1769 private void removeContentOverlay(SurfaceControl surface, Runnable callback) { 1770 if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { 1771 // Avoid double removal, which is fatal. 1772 return; 1773 } 1774 if (surface == null || !surface.isValid()) { 1775 ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, 1776 "%s: trying to remove invalid content overlay (%s)", TAG, surface); 1777 return; 1778 } 1779 final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); 1780 tx.remove(surface); 1781 tx.apply(); 1782 if (callback != null) callback.run(); 1783 } 1784 resetShadowRadius()1785 private void resetShadowRadius() { 1786 if (mPipTransitionState.getTransitionState() == PipTransitionState.UNDEFINED) { 1787 // mLeash is undefined when in PipTransitionState.UNDEFINED 1788 return; 1789 } 1790 final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); 1791 tx.setShadowRadius(mLeash, 0f); 1792 tx.apply(); 1793 } 1794 cancelCurrentAnimator()1795 private void cancelCurrentAnimator() { 1796 final PipAnimationController.PipTransitionAnimator<?> animator = 1797 mPipAnimationController.getCurrentAnimator(); 1798 if (animator != null) { 1799 if (animator.getContentOverlayLeash() != null) { 1800 removeContentOverlay(animator.getContentOverlayLeash(), 1801 animator::clearContentOverlay); 1802 } 1803 PipAnimationController.quietCancel(animator); 1804 } 1805 } 1806 1807 @VisibleForTesting setSurfaceControlTransactionFactory( PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory)1808 public void setSurfaceControlTransactionFactory( 1809 PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { 1810 mSurfaceControlTransactionFactory = factory; 1811 } 1812 isLaunchToSplit(TaskInfo taskInfo)1813 public boolean isLaunchToSplit(TaskInfo taskInfo) { 1814 return mSplitScreenOptional.isPresent() 1815 && mSplitScreenOptional.get().isLaunchToSplit(taskInfo); 1816 } 1817 1818 /** 1819 * Dumps internal states. 1820 */ 1821 @Override dump(PrintWriter pw, String prefix)1822 public void dump(PrintWriter pw, String prefix) { 1823 final String innerPrefix = prefix + " "; 1824 pw.println(prefix + TAG); 1825 pw.println(innerPrefix + "mTaskInfo=" + mTaskInfo); 1826 pw.println(innerPrefix + "mToken=" + mToken 1827 + " binder=" + (mToken != null ? mToken.asBinder() : null)); 1828 pw.println(innerPrefix + "mLeash=" + mLeash); 1829 pw.println(innerPrefix + "mState=" + mPipTransitionState.getTransitionState()); 1830 pw.println(innerPrefix + "mOneShotAnimationType=" + mOneShotAnimationType); 1831 pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams); 1832 } 1833 1834 @Override toString()1835 public String toString() { 1836 return TAG + ":" + taskListenerTypeToString(TASK_LISTENER_TYPE_PIP); 1837 } 1838 } 1839