• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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