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