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