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.systemui.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 24 import static com.android.systemui.pip.PipAnimationController.ANIM_TYPE_ALPHA; 25 import static com.android.systemui.pip.PipAnimationController.ANIM_TYPE_BOUNDS; 26 import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_NONE; 27 import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_SAME; 28 import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_FULLSCREEN; 29 import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; 30 import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_SPLIT_SCREEN; 31 import static com.android.systemui.pip.PipAnimationController.isInPipDirection; 32 import static com.android.systemui.pip.PipAnimationController.isOutPipDirection; 33 34 import android.annotation.NonNull; 35 import android.annotation.Nullable; 36 import android.app.ActivityManager; 37 import android.app.ActivityTaskManager; 38 import android.app.PictureInPictureParams; 39 import android.content.ComponentName; 40 import android.content.Context; 41 import android.content.pm.ActivityInfo; 42 import android.content.res.Configuration; 43 import android.graphics.Rect; 44 import android.os.Handler; 45 import android.os.IBinder; 46 import android.os.Looper; 47 import android.os.RemoteException; 48 import android.util.Log; 49 import android.util.Size; 50 import android.view.SurfaceControl; 51 import android.window.TaskOrganizer; 52 import android.window.WindowContainerToken; 53 import android.window.WindowContainerTransaction; 54 import android.window.WindowContainerTransactionCallback; 55 import android.window.WindowOrganizer; 56 57 import com.android.internal.os.SomeArgs; 58 import com.android.systemui.R; 59 import com.android.systemui.pip.phone.PipUpdateThread; 60 import com.android.systemui.stackdivider.Divider; 61 import com.android.systemui.wm.DisplayController; 62 63 import java.io.PrintWriter; 64 import java.util.ArrayList; 65 import java.util.HashMap; 66 import java.util.List; 67 import java.util.Map; 68 import java.util.Objects; 69 import java.util.function.Consumer; 70 71 import javax.inject.Inject; 72 import javax.inject.Singleton; 73 74 /** 75 * Manages PiP tasks such as resize and offset. 76 * 77 * This class listens on {@link TaskOrganizer} callbacks for windowing mode change 78 * both to and from PiP and issues corresponding animation if applicable. 79 * Normally, we apply series of {@link SurfaceControl.Transaction} when the animator is running 80 * and files a final {@link WindowContainerTransaction} at the end of the transition. 81 * 82 * This class is also responsible for general resize/offset PiP operations within SysUI component, 83 * see also {@link com.android.systemui.pip.phone.PipMotionHelper}. 84 */ 85 @Singleton 86 public class PipTaskOrganizer extends TaskOrganizer implements 87 DisplayController.OnDisplaysChangedListener { 88 private static final String TAG = PipTaskOrganizer.class.getSimpleName(); 89 private static final boolean DEBUG = false; 90 91 private static final int MSG_RESIZE_IMMEDIATE = 1; 92 private static final int MSG_RESIZE_ANIMATE = 2; 93 private static final int MSG_OFFSET_ANIMATE = 3; 94 private static final int MSG_FINISH_RESIZE = 4; 95 private static final int MSG_RESIZE_USER = 5; 96 97 private final Handler mMainHandler; 98 private final Handler mUpdateHandler; 99 private final PipBoundsHandler mPipBoundsHandler; 100 private final PipAnimationController mPipAnimationController; 101 private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); 102 private final Rect mLastReportedBounds = new Rect(); 103 private final int mEnterExitAnimationDuration; 104 private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; 105 private final Map<IBinder, Configuration> mInitialState = new HashMap<>(); 106 private final Divider mSplitDivider; 107 108 // These callbacks are called on the update thread 109 private final PipAnimationController.PipAnimationCallback mPipAnimationCallback = 110 new PipAnimationController.PipAnimationCallback() { 111 @Override 112 public void onPipAnimationStart(PipAnimationController.PipTransitionAnimator animator) { 113 sendOnPipTransitionStarted(animator.getTransitionDirection()); 114 } 115 116 @Override 117 public void onPipAnimationEnd(SurfaceControl.Transaction tx, 118 PipAnimationController.PipTransitionAnimator animator) { 119 finishResize(tx, animator.getDestinationBounds(), animator.getTransitionDirection(), 120 animator.getAnimationType()); 121 sendOnPipTransitionFinished(animator.getTransitionDirection()); 122 } 123 124 @Override 125 public void onPipAnimationCancel(PipAnimationController.PipTransitionAnimator animator) { 126 sendOnPipTransitionCancelled(animator.getTransitionDirection()); 127 } 128 }; 129 130 @SuppressWarnings("unchecked") 131 private final Handler.Callback mUpdateCallbacks = (msg) -> { 132 SomeArgs args = (SomeArgs) msg.obj; 133 Consumer<Rect> updateBoundsCallback = (Consumer<Rect>) args.arg1; 134 switch (msg.what) { 135 case MSG_RESIZE_IMMEDIATE: { 136 Rect toBounds = (Rect) args.arg2; 137 resizePip(toBounds); 138 if (updateBoundsCallback != null) { 139 updateBoundsCallback.accept(toBounds); 140 } 141 break; 142 } 143 case MSG_RESIZE_ANIMATE: { 144 Rect currentBounds = (Rect) args.arg2; 145 Rect toBounds = (Rect) args.arg3; 146 Rect sourceHintRect = (Rect) args.arg4; 147 int duration = args.argi2; 148 animateResizePip(currentBounds, toBounds, sourceHintRect, 149 args.argi1 /* direction */, duration); 150 if (updateBoundsCallback != null) { 151 updateBoundsCallback.accept(toBounds); 152 } 153 break; 154 } 155 case MSG_OFFSET_ANIMATE: { 156 Rect originalBounds = (Rect) args.arg2; 157 final int offset = args.argi1; 158 final int duration = args.argi2; 159 offsetPip(originalBounds, 0 /* xOffset */, offset, duration); 160 Rect toBounds = new Rect(originalBounds); 161 toBounds.offset(0, offset); 162 if (updateBoundsCallback != null) { 163 updateBoundsCallback.accept(toBounds); 164 } 165 break; 166 } 167 case MSG_FINISH_RESIZE: { 168 SurfaceControl.Transaction tx = (SurfaceControl.Transaction) args.arg2; 169 Rect toBounds = (Rect) args.arg3; 170 finishResize(tx, toBounds, args.argi1 /* direction */, -1); 171 if (updateBoundsCallback != null) { 172 updateBoundsCallback.accept(toBounds); 173 } 174 break; 175 } 176 case MSG_RESIZE_USER: { 177 Rect startBounds = (Rect) args.arg2; 178 Rect toBounds = (Rect) args.arg3; 179 userResizePip(startBounds, toBounds); 180 break; 181 } 182 } 183 args.recycle(); 184 return true; 185 }; 186 187 private ActivityManager.RunningTaskInfo mTaskInfo; 188 private WindowContainerToken mToken; 189 private SurfaceControl mLeash; 190 private boolean mInPip; 191 private boolean mExitingPip; 192 private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS; 193 private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory 194 mSurfaceControlTransactionFactory; 195 private PictureInPictureParams mPictureInPictureParams; 196 197 /** 198 * If set to {@code true}, the entering animation will be skipped and we will wait for 199 * {@link #onFixedRotationFinished(int)} callback to actually enter PiP. 200 */ 201 private boolean mShouldDeferEnteringPip; 202 203 @Inject PipTaskOrganizer(Context context, @NonNull PipBoundsHandler boundsHandler, @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, @Nullable Divider divider, @NonNull DisplayController displayController, @NonNull PipAnimationController pipAnimationController)204 public PipTaskOrganizer(Context context, @NonNull PipBoundsHandler boundsHandler, 205 @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, 206 @Nullable Divider divider, 207 @NonNull DisplayController displayController, 208 @NonNull PipAnimationController pipAnimationController) { 209 mMainHandler = new Handler(Looper.getMainLooper()); 210 mUpdateHandler = new Handler(PipUpdateThread.get().getLooper(), mUpdateCallbacks); 211 mPipBoundsHandler = boundsHandler; 212 mEnterExitAnimationDuration = context.getResources() 213 .getInteger(R.integer.config_pipResizeAnimationDuration); 214 mSurfaceTransactionHelper = surfaceTransactionHelper; 215 mPipAnimationController = pipAnimationController; 216 mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; 217 mSplitDivider = divider; 218 displayController.addDisplayWindowListener(this); 219 } 220 getUpdateHandler()221 public Handler getUpdateHandler() { 222 return mUpdateHandler; 223 } 224 getLastReportedBounds()225 public Rect getLastReportedBounds() { 226 return new Rect(mLastReportedBounds); 227 } 228 getCurrentOrAnimatingBounds()229 public Rect getCurrentOrAnimatingBounds() { 230 PipAnimationController.PipTransitionAnimator animator = 231 mPipAnimationController.getCurrentAnimator(); 232 if (animator != null && animator.isRunning()) { 233 return new Rect(animator.getDestinationBounds()); 234 } 235 return getLastReportedBounds(); 236 } 237 isInPip()238 public boolean isInPip() { 239 return mInPip; 240 } 241 isDeferringEnterPipAnimation()242 public boolean isDeferringEnterPipAnimation() { 243 return mInPip && mShouldDeferEnteringPip; 244 } 245 246 /** 247 * Registers {@link PipTransitionCallback} to receive transition callbacks. 248 */ registerPipTransitionCallback(PipTransitionCallback callback)249 public void registerPipTransitionCallback(PipTransitionCallback callback) { 250 mPipTransitionCallbacks.add(callback); 251 } 252 253 /** 254 * Sets the preferred animation type for one time. 255 * This is typically used to set the animation type to 256 * {@link PipAnimationController#ANIM_TYPE_ALPHA}. 257 */ setOneShotAnimationType(@ipAnimationController.AnimationType int animationType)258 public void setOneShotAnimationType(@PipAnimationController.AnimationType int animationType) { 259 mOneShotAnimationType = animationType; 260 } 261 262 /** 263 * Expands PiP to the previous bounds, this is done in two phases using 264 * {@link WindowContainerTransaction} 265 * - setActivityWindowingMode to either fullscreen or split-secondary at beginning of the 266 * transaction. without changing the windowing mode of the Task itself. This makes sure the 267 * activity render it's final configuration while the Task is still in PiP. 268 * - setWindowingMode to undefined at the end of transition 269 * @param animationDurationMs duration in millisecond for the exiting PiP transition 270 */ exitPip(int animationDurationMs)271 public void exitPip(int animationDurationMs) { 272 if (!mInPip || mExitingPip || mToken == null) { 273 Log.wtf(TAG, "Not allowed to exitPip in current state" 274 + " mInPip=" + mInPip + " mExitingPip=" + mExitingPip + " mToken=" + mToken); 275 return; 276 } 277 278 final Configuration initialConfig = mInitialState.remove(mToken.asBinder()); 279 final boolean orientationDiffers = initialConfig.windowConfiguration.getRotation() 280 != mPipBoundsHandler.getDisplayRotation(); 281 final WindowContainerTransaction wct = new WindowContainerTransaction(); 282 final Rect destinationBounds = initialConfig.windowConfiguration.getBounds(); 283 final int direction = syncWithSplitScreenBounds(destinationBounds) 284 ? TRANSITION_DIRECTION_TO_SPLIT_SCREEN 285 : TRANSITION_DIRECTION_TO_FULLSCREEN; 286 if (orientationDiffers) { 287 // Send started callback though animation is ignored. 288 sendOnPipTransitionStarted(direction); 289 // Don't bother doing an animation if the display rotation differs or if it's in 290 // a non-supported windowing mode 291 applyWindowingModeChangeOnExit(wct, direction); 292 WindowOrganizer.applyTransaction(wct); 293 // Send finished callback though animation is ignored. 294 sendOnPipTransitionFinished(direction); 295 mInPip = false; 296 } else { 297 final SurfaceControl.Transaction tx = 298 mSurfaceControlTransactionFactory.getTransaction(); 299 mSurfaceTransactionHelper.scale(tx, mLeash, destinationBounds, 300 mLastReportedBounds); 301 tx.setWindowCrop(mLeash, destinationBounds.width(), destinationBounds.height()); 302 wct.setActivityWindowingMode(mToken, direction == TRANSITION_DIRECTION_TO_SPLIT_SCREEN 303 ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY 304 : WINDOWING_MODE_FULLSCREEN); 305 wct.setBounds(mToken, destinationBounds); 306 wct.setBoundsChangeTransaction(mToken, tx); 307 applySyncTransaction(wct, new WindowContainerTransactionCallback() { 308 @Override 309 public void onTransactionReady(int id, SurfaceControl.Transaction t) { 310 t.apply(); 311 scheduleAnimateResizePip(mLastReportedBounds, destinationBounds, 312 null /* sourceHintRect */, direction, animationDurationMs, 313 null /* updateBoundsCallback */); 314 mInPip = false; 315 } 316 }); 317 } 318 mExitingPip = true; 319 } 320 applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction)321 private void applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction) { 322 // Reset the final windowing mode. 323 wct.setWindowingMode(mToken, getOutPipWindowingMode()); 324 // Simply reset the activity mode set prior to the animation running. 325 wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); 326 if (mSplitDivider != null && direction == TRANSITION_DIRECTION_TO_SPLIT_SCREEN) { 327 wct.reparent(mToken, mSplitDivider.getSecondaryRoot(), true /* onTop */); 328 } 329 } 330 331 /** 332 * Removes PiP immediately. 333 */ removePip()334 public void removePip() { 335 if (!mInPip || mExitingPip || mToken == null) { 336 Log.wtf(TAG, "Not allowed to removePip in current state" 337 + " mInPip=" + mInPip + " mExitingPip=" + mExitingPip + " mToken=" + mToken); 338 return; 339 } 340 getUpdateHandler().post(() -> { 341 try { 342 // Reset the task bounds first to ensure the activity configuration is reset as well 343 final WindowContainerTransaction wct = new WindowContainerTransaction(); 344 wct.setBounds(mToken, null); 345 WindowOrganizer.applyTransaction(wct); 346 347 ActivityTaskManager.getService().removeStacksInWindowingModes( 348 new int[]{ WINDOWING_MODE_PINNED }); 349 } catch (RemoteException e) { 350 Log.e(TAG, "Failed to remove PiP", e); 351 } 352 }); 353 mInitialState.remove(mToken.asBinder()); 354 mExitingPip = true; 355 } 356 357 @Override onTaskAppeared(ActivityManager.RunningTaskInfo info, SurfaceControl leash)358 public void onTaskAppeared(ActivityManager.RunningTaskInfo info, SurfaceControl leash) { 359 Objects.requireNonNull(info, "Requires RunningTaskInfo"); 360 mTaskInfo = info; 361 mToken = mTaskInfo.token; 362 mInPip = true; 363 mExitingPip = false; 364 mLeash = leash; 365 mInitialState.put(mToken.asBinder(), new Configuration(mTaskInfo.configuration)); 366 mPictureInPictureParams = mTaskInfo.pictureInPictureParams; 367 368 if (mShouldDeferEnteringPip) { 369 if (DEBUG) Log.d(TAG, "Defer entering PiP animation, fixed rotation is ongoing"); 370 // if deferred, hide the surface till fixed rotation is completed 371 final SurfaceControl.Transaction tx = 372 mSurfaceControlTransactionFactory.getTransaction(); 373 tx.setAlpha(mLeash, 0f); 374 tx.show(mLeash); 375 tx.apply(); 376 return; 377 } 378 379 final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( 380 mTaskInfo.topActivity, getAspectRatioOrDefault(mPictureInPictureParams), 381 null /* bounds */, getMinimalSize(mTaskInfo.topActivityInfo)); 382 Objects.requireNonNull(destinationBounds, "Missing destination bounds"); 383 final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); 384 385 if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { 386 final Rect sourceHintRect = getValidSourceHintRect(info, currentBounds); 387 scheduleAnimateResizePip(currentBounds, destinationBounds, sourceHintRect, 388 TRANSITION_DIRECTION_TO_PIP, mEnterExitAnimationDuration, 389 null /* updateBoundsCallback */); 390 } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { 391 enterPipWithAlphaAnimation(destinationBounds, mEnterExitAnimationDuration); 392 mOneShotAnimationType = ANIM_TYPE_BOUNDS; 393 } else { 394 throw new RuntimeException("Unrecognized animation type: " + mOneShotAnimationType); 395 } 396 } 397 398 /** 399 * Returns the source hint rect if it is valid (if provided and is contained by the current 400 * task bounds). 401 */ getValidSourceHintRect(ActivityManager.RunningTaskInfo info, Rect sourceBounds)402 private Rect getValidSourceHintRect(ActivityManager.RunningTaskInfo info, Rect sourceBounds) { 403 final Rect sourceHintRect = info.pictureInPictureParams != null 404 && info.pictureInPictureParams.hasSourceBoundsHint() 405 ? info.pictureInPictureParams.getSourceRectHint() 406 : null; 407 if (sourceHintRect != null && sourceBounds.contains(sourceHintRect)) { 408 return sourceHintRect; 409 } 410 return null; 411 } 412 enterPipWithAlphaAnimation(Rect destinationBounds, long durationMs)413 private void enterPipWithAlphaAnimation(Rect destinationBounds, long durationMs) { 414 // If we are fading the PIP in, then we should move the pip to the final location as 415 // soon as possible, but set the alpha immediately since the transaction can take a 416 // while to process 417 final SurfaceControl.Transaction tx = 418 mSurfaceControlTransactionFactory.getTransaction(); 419 tx.setAlpha(mLeash, 0f); 420 tx.apply(); 421 final WindowContainerTransaction wct = new WindowContainerTransaction(); 422 wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); 423 wct.setBounds(mToken, destinationBounds); 424 wct.scheduleFinishEnterPip(mToken, destinationBounds); 425 applySyncTransaction(wct, new WindowContainerTransactionCallback() { 426 @Override 427 public void onTransactionReady(int id, SurfaceControl.Transaction t) { 428 t.apply(); 429 mUpdateHandler.post(() -> mPipAnimationController 430 .getAnimator(mLeash, destinationBounds, 0f, 1f) 431 .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) 432 .setPipAnimationCallback(mPipAnimationCallback) 433 .setDuration(durationMs) 434 .start()); 435 } 436 }); 437 } 438 sendOnPipTransitionStarted( @ipAnimationController.TransitionDirection int direction)439 private void sendOnPipTransitionStarted( 440 @PipAnimationController.TransitionDirection int direction) { 441 runOnMainHandler(() -> { 442 for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { 443 final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); 444 callback.onPipTransitionStarted(mTaskInfo.baseActivity, direction); 445 } 446 }); 447 } 448 sendOnPipTransitionFinished( @ipAnimationController.TransitionDirection int direction)449 private void sendOnPipTransitionFinished( 450 @PipAnimationController.TransitionDirection int direction) { 451 runOnMainHandler(() -> { 452 for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { 453 final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); 454 callback.onPipTransitionFinished(mTaskInfo.baseActivity, direction); 455 } 456 }); 457 } 458 sendOnPipTransitionCancelled( @ipAnimationController.TransitionDirection int direction)459 private void sendOnPipTransitionCancelled( 460 @PipAnimationController.TransitionDirection int direction) { 461 runOnMainHandler(() -> { 462 for (int i = mPipTransitionCallbacks.size() - 1; i >= 0; i--) { 463 final PipTransitionCallback callback = mPipTransitionCallbacks.get(i); 464 callback.onPipTransitionCanceled(mTaskInfo.baseActivity, direction); 465 } 466 }); 467 } 468 runOnMainHandler(Runnable r)469 private void runOnMainHandler(Runnable r) { 470 if (Looper.getMainLooper() == Looper.myLooper()) { 471 r.run(); 472 } else { 473 mMainHandler.post(r); 474 } 475 } 476 477 /** 478 * Note that dismissing PiP is now originated from SystemUI, see {@link #exitPip(int)}. 479 * Meanwhile this callback is invoked whenever the task is removed. For instance: 480 * - as a result of removeStacksInWindowingModes from WM 481 * - activity itself is died 482 * Nevertheless, we simply update the internal state here as all the heavy lifting should 483 * have been done in WM. 484 */ 485 @Override onTaskVanished(ActivityManager.RunningTaskInfo info)486 public void onTaskVanished(ActivityManager.RunningTaskInfo info) { 487 if (!mInPip) { 488 return; 489 } 490 final WindowContainerToken token = info.token; 491 Objects.requireNonNull(token, "Requires valid WindowContainerToken"); 492 if (token.asBinder() != mToken.asBinder()) { 493 Log.wtf(TAG, "Unrecognized token: " + token); 494 return; 495 } 496 mShouldDeferEnteringPip = false; 497 mPictureInPictureParams = null; 498 mInPip = false; 499 mExitingPip = false; 500 } 501 502 @Override onTaskInfoChanged(ActivityManager.RunningTaskInfo info)503 public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) { 504 Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken"); 505 final PictureInPictureParams newParams = info.pictureInPictureParams; 506 if (newParams == null || !applyPictureInPictureParams(newParams)) { 507 Log.d(TAG, "Ignored onTaskInfoChanged with PiP param: " + newParams); 508 return; 509 } 510 final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( 511 info.topActivity, getAspectRatioOrDefault(newParams), 512 mLastReportedBounds, getMinimalSize(info.topActivityInfo), 513 true /* userCurrentMinEdgeSize */); 514 Objects.requireNonNull(destinationBounds, "Missing destination bounds"); 515 scheduleAnimateResizePip(destinationBounds, mEnterExitAnimationDuration, 516 null /* updateBoundsCallback */); 517 } 518 519 @Override onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo)520 public void onBackPressedOnTaskRoot(ActivityManager.RunningTaskInfo taskInfo) { 521 // Do nothing 522 } 523 524 @Override onFixedRotationStarted(int displayId, int newRotation)525 public void onFixedRotationStarted(int displayId, int newRotation) { 526 mShouldDeferEnteringPip = true; 527 } 528 529 @Override onFixedRotationFinished(int displayId)530 public void onFixedRotationFinished(int displayId) { 531 if (mShouldDeferEnteringPip && mInPip) { 532 final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( 533 mTaskInfo.topActivity, getAspectRatioOrDefault(mPictureInPictureParams), 534 null /* bounds */, getMinimalSize(mTaskInfo.topActivityInfo)); 535 // schedule a regular animation to ensure all the callbacks are still being sent 536 enterPipWithAlphaAnimation(destinationBounds, 0 /* durationMs */); 537 } 538 mShouldDeferEnteringPip = false; 539 } 540 541 /** 542 * TODO(b/152809058): consolidate the display info handling logic in SysUI 543 * 544 * @param destinationBoundsOut the current destination bounds will be populated to this param 545 */ 546 @SuppressWarnings("unchecked") onMovementBoundsChanged(Rect destinationBoundsOut, boolean fromRotation, boolean fromImeAdjustment, boolean fromShelfAdjustment, WindowContainerTransaction wct)547 public void onMovementBoundsChanged(Rect destinationBoundsOut, boolean fromRotation, 548 boolean fromImeAdjustment, boolean fromShelfAdjustment, 549 WindowContainerTransaction wct) { 550 final PipAnimationController.PipTransitionAnimator animator = 551 mPipAnimationController.getCurrentAnimator(); 552 if (animator == null || !animator.isRunning() 553 || animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) { 554 if (mInPip && fromRotation) { 555 // If we are rotating while there is a current animation, immediately cancel the 556 // animation (remove the listeners so we don't trigger the normal finish resize 557 // call that should only happen on the update thread) 558 int direction = TRANSITION_DIRECTION_NONE; 559 if (animator != null) { 560 direction = animator.getTransitionDirection(); 561 animator.removeAllUpdateListeners(); 562 animator.removeAllListeners(); 563 animator.cancel(); 564 // Do notify the listeners that this was canceled 565 sendOnPipTransitionCancelled(direction); 566 sendOnPipTransitionFinished(direction); 567 } 568 mLastReportedBounds.set(destinationBoundsOut); 569 570 // Create a reset surface transaction for the new bounds and update the window 571 // container transaction 572 final SurfaceControl.Transaction tx = createFinishResizeSurfaceTransaction( 573 destinationBoundsOut); 574 prepareFinishResizeTransaction(destinationBoundsOut, direction, tx, wct); 575 } else { 576 // There could be an animation on-going. If there is one on-going, last-reported 577 // bounds isn't yet updated. We'll use the animator's bounds instead. 578 if (animator != null && animator.isRunning()) { 579 if (!animator.getDestinationBounds().isEmpty()) { 580 destinationBoundsOut.set(animator.getDestinationBounds()); 581 } 582 } else { 583 if (!mLastReportedBounds.isEmpty()) { 584 destinationBoundsOut.set(mLastReportedBounds); 585 } 586 } 587 } 588 return; 589 } 590 591 final Rect currentDestinationBounds = animator.getDestinationBounds(); 592 destinationBoundsOut.set(currentDestinationBounds); 593 if (!fromImeAdjustment && !fromShelfAdjustment 594 && mPipBoundsHandler.getDisplayBounds().contains(currentDestinationBounds)) { 595 // no need to update the destination bounds, bail early 596 return; 597 } 598 599 final Rect newDestinationBounds = mPipBoundsHandler.getDestinationBounds( 600 mTaskInfo.topActivity, getAspectRatioOrDefault(mPictureInPictureParams), 601 null /* bounds */, getMinimalSize(mTaskInfo.topActivityInfo)); 602 if (newDestinationBounds.equals(currentDestinationBounds)) return; 603 if (animator.getAnimationType() == ANIM_TYPE_BOUNDS) { 604 animator.updateEndValue(newDestinationBounds); 605 } 606 animator.setDestinationBounds(newDestinationBounds); 607 destinationBoundsOut.set(newDestinationBounds); 608 } 609 610 /** 611 * @return {@code true} if the aspect ratio is changed since no other parameters within 612 * {@link PictureInPictureParams} would affect the bounds. 613 */ applyPictureInPictureParams(@onNull PictureInPictureParams params)614 private boolean applyPictureInPictureParams(@NonNull PictureInPictureParams params) { 615 final boolean changed = (mPictureInPictureParams == null) ? true : !Objects.equals( 616 mPictureInPictureParams.getAspectRatioRational(), params.getAspectRatioRational()); 617 if (changed) { 618 mPictureInPictureParams = params; 619 mPipBoundsHandler.onAspectRatioChanged(params.getAspectRatio()); 620 } 621 return changed; 622 } 623 624 /** 625 * Animates resizing of the pinned stack given the duration. 626 */ scheduleAnimateResizePip(Rect toBounds, int duration, Consumer<Rect> updateBoundsCallback)627 public void scheduleAnimateResizePip(Rect toBounds, int duration, 628 Consumer<Rect> updateBoundsCallback) { 629 if (mShouldDeferEnteringPip) { 630 Log.d(TAG, "skip scheduleAnimateResizePip, entering pip deferred"); 631 return; 632 } 633 scheduleAnimateResizePip(mLastReportedBounds, toBounds, null /* sourceHintRect */, 634 TRANSITION_DIRECTION_NONE, duration, updateBoundsCallback); 635 } 636 scheduleAnimateResizePip(Rect currentBounds, Rect destinationBounds, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, int durationMs, Consumer<Rect> updateBoundsCallback)637 private void scheduleAnimateResizePip(Rect currentBounds, Rect destinationBounds, 638 Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, 639 int durationMs, Consumer<Rect> updateBoundsCallback) { 640 if (!mInPip) { 641 // TODO: tend to use shouldBlockResizeRequest here as well but need to consider 642 // the fact that when in exitPip, scheduleAnimateResizePip is executed in the window 643 // container transaction callback and we want to set the mExitingPip immediately. 644 return; 645 } 646 647 SomeArgs args = SomeArgs.obtain(); 648 args.arg1 = updateBoundsCallback; 649 args.arg2 = currentBounds; 650 args.arg3 = destinationBounds; 651 args.arg4 = sourceHintRect; 652 args.argi1 = direction; 653 args.argi2 = durationMs; 654 mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_ANIMATE, args)); 655 } 656 657 /** 658 * Directly perform manipulation/resize on the leash. This will not perform any 659 * {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called. 660 */ scheduleResizePip(Rect toBounds, Consumer<Rect> updateBoundsCallback)661 public void scheduleResizePip(Rect toBounds, Consumer<Rect> updateBoundsCallback) { 662 SomeArgs args = SomeArgs.obtain(); 663 args.arg1 = updateBoundsCallback; 664 args.arg2 = toBounds; 665 mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_IMMEDIATE, args)); 666 } 667 668 /** 669 * Directly perform a scaled matrix transformation on the leash. This will not perform any 670 * {@link WindowContainerTransaction} until {@link #scheduleFinishResizePip} is called. 671 */ scheduleUserResizePip(Rect startBounds, Rect toBounds, Consumer<Rect> updateBoundsCallback)672 public void scheduleUserResizePip(Rect startBounds, Rect toBounds, 673 Consumer<Rect> updateBoundsCallback) { 674 SomeArgs args = SomeArgs.obtain(); 675 args.arg1 = updateBoundsCallback; 676 args.arg2 = startBounds; 677 args.arg3 = toBounds; 678 mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_USER, args)); 679 } 680 681 /** 682 * Finish an intermediate resize operation. This is expected to be called after 683 * {@link #scheduleResizePip}. 684 */ scheduleFinishResizePip(Rect destinationBounds)685 public void scheduleFinishResizePip(Rect destinationBounds) { 686 scheduleFinishResizePip(destinationBounds, null /* updateBoundsCallback */); 687 } 688 689 /** 690 * Same as {@link #scheduleFinishResizePip} but with a callback. 691 */ scheduleFinishResizePip(Rect destinationBounds, Consumer<Rect> updateBoundsCallback)692 public void scheduleFinishResizePip(Rect destinationBounds, 693 Consumer<Rect> updateBoundsCallback) { 694 scheduleFinishResizePip(destinationBounds, TRANSITION_DIRECTION_NONE, updateBoundsCallback); 695 } 696 scheduleFinishResizePip(Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, Consumer<Rect> updateBoundsCallback)697 private void scheduleFinishResizePip(Rect destinationBounds, 698 @PipAnimationController.TransitionDirection int direction, 699 Consumer<Rect> updateBoundsCallback) { 700 if (shouldBlockResizeRequest()) { 701 return; 702 } 703 704 SomeArgs args = SomeArgs.obtain(); 705 args.arg1 = updateBoundsCallback; 706 args.arg2 = createFinishResizeSurfaceTransaction( 707 destinationBounds); 708 args.arg3 = destinationBounds; 709 args.argi1 = direction; 710 mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_FINISH_RESIZE, args)); 711 } 712 createFinishResizeSurfaceTransaction( Rect destinationBounds)713 private SurfaceControl.Transaction createFinishResizeSurfaceTransaction( 714 Rect destinationBounds) { 715 final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); 716 mSurfaceTransactionHelper 717 .crop(tx, mLeash, destinationBounds) 718 .resetScale(tx, mLeash, destinationBounds) 719 .round(tx, mLeash, mInPip); 720 return tx; 721 } 722 723 /** 724 * Offset the PiP window by a given offset on Y-axis, triggered also from screen rotation. 725 */ scheduleOffsetPip(Rect originalBounds, int offset, int duration, Consumer<Rect> updateBoundsCallback)726 public void scheduleOffsetPip(Rect originalBounds, int offset, int duration, 727 Consumer<Rect> updateBoundsCallback) { 728 if (shouldBlockResizeRequest()) { 729 return; 730 } 731 if (mShouldDeferEnteringPip) { 732 Log.d(TAG, "skip scheduleOffsetPip, entering pip deferred"); 733 return; 734 } 735 SomeArgs args = SomeArgs.obtain(); 736 args.arg1 = updateBoundsCallback; 737 args.arg2 = originalBounds; 738 // offset would be zero if triggered from screen rotation. 739 args.argi1 = offset; 740 args.argi2 = duration; 741 mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_OFFSET_ANIMATE, args)); 742 } 743 offsetPip(Rect originalBounds, int xOffset, int yOffset, int durationMs)744 private void offsetPip(Rect originalBounds, int xOffset, int yOffset, int durationMs) { 745 if (Looper.myLooper() != mUpdateHandler.getLooper()) { 746 throw new RuntimeException("Callers should call scheduleOffsetPip() instead of this " 747 + "directly"); 748 } 749 if (mTaskInfo == null) { 750 Log.w(TAG, "mTaskInfo is not set"); 751 return; 752 } 753 final Rect destinationBounds = new Rect(originalBounds); 754 destinationBounds.offset(xOffset, yOffset); 755 animateResizePip(originalBounds, destinationBounds, null /* sourceHintRect */, 756 TRANSITION_DIRECTION_SAME, durationMs); 757 } 758 resizePip(Rect destinationBounds)759 private void resizePip(Rect destinationBounds) { 760 if (Looper.myLooper() != mUpdateHandler.getLooper()) { 761 throw new RuntimeException("Callers should call scheduleResizePip() instead of this " 762 + "directly"); 763 } 764 // Could happen when exitPip 765 if (mToken == null || mLeash == null) { 766 Log.w(TAG, "Abort animation, invalid leash"); 767 return; 768 } 769 mLastReportedBounds.set(destinationBounds); 770 final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); 771 mSurfaceTransactionHelper 772 .crop(tx, mLeash, destinationBounds) 773 .round(tx, mLeash, mInPip); 774 tx.apply(); 775 } 776 userResizePip(Rect startBounds, Rect destinationBounds)777 private void userResizePip(Rect startBounds, Rect destinationBounds) { 778 if (Looper.myLooper() != mUpdateHandler.getLooper()) { 779 throw new RuntimeException("Callers should call scheduleUserResizePip() instead of " 780 + "this directly"); 781 } 782 // Could happen when exitPip 783 if (mToken == null || mLeash == null) { 784 Log.w(TAG, "Abort animation, invalid leash"); 785 return; 786 } 787 788 if (startBounds.isEmpty() || destinationBounds.isEmpty()) { 789 Log.w(TAG, "Attempted to user resize PIP to or from empty bounds, aborting."); 790 return; 791 } 792 793 final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); 794 mSurfaceTransactionHelper.scale(tx, mLeash, startBounds, destinationBounds); 795 tx.apply(); 796 } 797 finishResize(SurfaceControl.Transaction tx, Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, @PipAnimationController.AnimationType int type)798 private void finishResize(SurfaceControl.Transaction tx, Rect destinationBounds, 799 @PipAnimationController.TransitionDirection int direction, 800 @PipAnimationController.AnimationType int type) { 801 if (Looper.myLooper() != mUpdateHandler.getLooper()) { 802 throw new RuntimeException("Callers should call scheduleResizePip() instead of this " 803 + "directly"); 804 } 805 mLastReportedBounds.set(destinationBounds); 806 if (isInPipDirection(direction) && type == ANIM_TYPE_ALPHA) { 807 return; 808 } 809 810 WindowContainerTransaction wct = new WindowContainerTransaction(); 811 prepareFinishResizeTransaction(destinationBounds, direction, tx, wct); 812 applyFinishBoundsResize(wct, direction); 813 } 814 prepareFinishResizeTransaction(Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, SurfaceControl.Transaction tx, WindowContainerTransaction wct)815 private void prepareFinishResizeTransaction(Rect destinationBounds, 816 @PipAnimationController.TransitionDirection int direction, 817 SurfaceControl.Transaction tx, 818 WindowContainerTransaction wct) { 819 final Rect taskBounds; 820 if (isInPipDirection(direction)) { 821 // If we are animating from fullscreen using a bounds animation, then reset the 822 // activity windowing mode set by WM, and set the task bounds to the final bounds 823 taskBounds = destinationBounds; 824 wct.setActivityWindowingMode(mToken, WINDOWING_MODE_UNDEFINED); 825 wct.scheduleFinishEnterPip(mToken, destinationBounds); 826 } else if (isOutPipDirection(direction)) { 827 // If we are animating to fullscreen, then we need to reset the override bounds 828 // on the task to ensure that the task "matches" the parent's bounds. 829 taskBounds = (direction == TRANSITION_DIRECTION_TO_FULLSCREEN) 830 ? null : destinationBounds; 831 applyWindowingModeChangeOnExit(wct, direction); 832 } else { 833 // Just a resize in PIP 834 taskBounds = destinationBounds; 835 } 836 837 wct.setBounds(mToken, taskBounds); 838 wct.setBoundsChangeTransaction(mToken, tx); 839 } 840 841 /** 842 * Applies the window container transaction to finish a bounds resize. 843 * 844 * Called by {@link #finishResize(SurfaceControl.Transaction, Rect, int, int)}} once it has 845 * finished preparing the transaction. It allows subclasses to modify the transaction before 846 * applying it. 847 */ applyFinishBoundsResize(@onNull WindowContainerTransaction wct, @PipAnimationController.TransitionDirection int direction)848 public void applyFinishBoundsResize(@NonNull WindowContainerTransaction wct, 849 @PipAnimationController.TransitionDirection int direction) { 850 WindowOrganizer.applyTransaction(wct); 851 } 852 853 /** 854 * The windowing mode to restore to when resizing out of PIP direction. Defaults to undefined 855 * and can be overridden to restore to an alternate windowing mode. 856 */ getOutPipWindowingMode()857 public int getOutPipWindowingMode() { 858 // By default, simply reset the windowing mode to undefined. 859 return WINDOWING_MODE_UNDEFINED; 860 } 861 862 animateResizePip(Rect currentBounds, Rect destinationBounds, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, int durationMs)863 private void animateResizePip(Rect currentBounds, Rect destinationBounds, Rect sourceHintRect, 864 @PipAnimationController.TransitionDirection int direction, int durationMs) { 865 if (Looper.myLooper() != mUpdateHandler.getLooper()) { 866 throw new RuntimeException("Callers should call scheduleAnimateResizePip() instead of " 867 + "this directly"); 868 } 869 // Could happen when exitPip 870 if (mToken == null || mLeash == null) { 871 Log.w(TAG, "Abort animation, invalid leash"); 872 return; 873 } 874 mPipAnimationController 875 .getAnimator(mLeash, currentBounds, destinationBounds, sourceHintRect) 876 .setTransitionDirection(direction) 877 .setPipAnimationCallback(mPipAnimationCallback) 878 .setDuration(durationMs) 879 .start(); 880 } 881 getMinimalSize(ActivityInfo activityInfo)882 private Size getMinimalSize(ActivityInfo activityInfo) { 883 if (activityInfo == null || activityInfo.windowLayout == null) { 884 return null; 885 } 886 final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout; 887 // -1 will be populated if an activity specifies defaultWidth/defaultHeight in <layout> 888 // without minWidth/minHeight 889 if (windowLayout.minWidth > 0 && windowLayout.minHeight > 0) { 890 return new Size(windowLayout.minWidth, windowLayout.minHeight); 891 } 892 return null; 893 } 894 getAspectRatioOrDefault(@ullable PictureInPictureParams params)895 private float getAspectRatioOrDefault(@Nullable PictureInPictureParams params) { 896 return params == null || !params.hasSetAspectRatio() 897 ? mPipBoundsHandler.getDefaultAspectRatio() 898 : params.getAspectRatio(); 899 } 900 901 /** 902 * Resize request can be initiated in other component, ignore if we are no longer in PIP 903 * or we're exiting from it. 904 * 905 * @return {@code true} if the resize request should be blocked/ignored. 906 */ shouldBlockResizeRequest()907 private boolean shouldBlockResizeRequest() { 908 return !mInPip || mExitingPip; 909 } 910 911 /** 912 * Sync with {@link #mSplitDivider} on destination bounds if PiP is going to split screen. 913 * 914 * @param destinationBoundsOut contain the updated destination bounds if applicable 915 * @return {@code true} if destinationBounds is altered for split screen 916 */ syncWithSplitScreenBounds(Rect destinationBoundsOut)917 private boolean syncWithSplitScreenBounds(Rect destinationBoundsOut) { 918 if (mSplitDivider == null || !mSplitDivider.isDividerVisible()) { 919 // bail early if system is not in split screen mode 920 return false; 921 } 922 // PiP window will go to split-secondary mode instead of fullscreen, populates the 923 // split screen bounds here. 924 destinationBoundsOut.set( 925 mSplitDivider.getView().getNonMinimizedSplitScreenSecondaryBounds()); 926 return true; 927 } 928 929 /** 930 * Dumps internal states. 931 */ dump(PrintWriter pw, String prefix)932 public void dump(PrintWriter pw, String prefix) { 933 final String innerPrefix = prefix + " "; 934 pw.println(prefix + TAG); 935 pw.println(innerPrefix + "mTaskInfo=" + mTaskInfo); 936 pw.println(innerPrefix + "mToken=" + mToken 937 + " binder=" + (mToken != null ? mToken.asBinder() : null)); 938 pw.println(innerPrefix + "mLeash=" + mLeash); 939 pw.println(innerPrefix + "mInPip=" + mInPip); 940 pw.println(innerPrefix + "mOneShotAnimationType=" + mOneShotAnimationType); 941 pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams); 942 pw.println(innerPrefix + "mLastReportedBounds=" + mLastReportedBounds); 943 pw.println(innerPrefix + "mInitialState:"); 944 for (Map.Entry<IBinder, Configuration> e : mInitialState.entrySet()) { 945 pw.println(innerPrefix + " binder=" + e.getKey() 946 + " winConfig=" + e.getValue().windowConfiguration); 947 } 948 } 949 950 /** 951 * Callback interface for PiP transitions (both from and to PiP mode) 952 */ 953 public interface PipTransitionCallback { 954 /** 955 * Callback when the pip transition is started. 956 */ onPipTransitionStarted(ComponentName activity, int direction)957 void onPipTransitionStarted(ComponentName activity, int direction); 958 959 /** 960 * Callback when the pip transition is finished. 961 */ onPipTransitionFinished(ComponentName activity, int direction)962 void onPipTransitionFinished(ComponentName activity, int direction); 963 964 /** 965 * Callback when the pip transition is cancelled. 966 */ onPipTransitionCanceled(ComponentName activity, int direction)967 void onPipTransitionCanceled(ComponentName activity, int direction); 968 } 969 } 970