• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.wm.shell.pip2.phone;
18 
19 import android.app.PictureInPictureParams;
20 import android.content.Context;
21 import android.graphics.Matrix;
22 import android.graphics.Rect;
23 import android.os.Bundle;
24 import android.os.SystemProperties;
25 import android.view.SurfaceControl;
26 import android.window.WindowContainerToken;
27 import android.window.WindowContainerTransaction;
28 
29 import androidx.annotation.NonNull;
30 import androidx.annotation.Nullable;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.internal.protolog.ProtoLog;
34 import com.android.wm.shell.common.ScreenshotUtils;
35 import com.android.wm.shell.common.ShellExecutor;
36 import com.android.wm.shell.common.pip.PipBoundsState;
37 import com.android.wm.shell.common.pip.PipDesktopState;
38 import com.android.wm.shell.pip.PipTransitionController;
39 import com.android.wm.shell.pip2.PipSurfaceTransactionHelper;
40 import com.android.wm.shell.pip2.animation.PipAlphaAnimator;
41 import com.android.wm.shell.protolog.ShellProtoLogGroup;
42 import com.android.wm.shell.shared.split.SplitScreenConstants;
43 import com.android.wm.shell.splitscreen.SplitScreenController;
44 
45 import java.util.Optional;
46 import java.util.function.Supplier;
47 
48 /**
49  * Scheduler for Shell initiated PiP transitions and animations.
50  */
51 public class PipScheduler implements PipTransitionState.PipTransitionStateChangedListener {
52     private static final String TAG = PipScheduler.class.getSimpleName();
53 
54     /**
55      * The fixed start delay in ms when fading out the content overlay from bounds animation.
56      * The fadeout animation is guaranteed to start after the client has drawn under the new config.
57      */
58     public static final int EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS =
59             SystemProperties.getInt(
60                     "persist.wm.debug.extra_content_overlay_fade_out_delay_ms", 400);
61     private static final int CONTENT_OVERLAY_FADE_OUT_DURATION_MS = 500;
62 
63     private final Context mContext;
64     private final PipBoundsState mPipBoundsState;
65     private final ShellExecutor mMainExecutor;
66     private final PipTransitionState mPipTransitionState;
67     private final PipDesktopState mPipDesktopState;
68     private final Optional<SplitScreenController> mSplitScreenControllerOptional;
69     private PipTransitionController mPipTransitionController;
70     private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
71             mSurfaceControlTransactionFactory;
72     private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper;
73 
74     @Nullable private Runnable mUpdateMovementBoundsRunnable;
75     @Nullable private PipAlphaAnimator mOverlayFadeoutAnimator;
76 
77     private PipAlphaAnimatorSupplier mPipAlphaAnimatorSupplier;
78     private Supplier<PictureInPictureParams> mPipParamsSupplier;
79 
PipScheduler(Context context, PipBoundsState pipBoundsState, ShellExecutor mainExecutor, PipTransitionState pipTransitionState, Optional<SplitScreenController> splitScreenControllerOptional, PipDesktopState pipDesktopState)80     public PipScheduler(Context context,
81             PipBoundsState pipBoundsState,
82             ShellExecutor mainExecutor,
83             PipTransitionState pipTransitionState,
84             Optional<SplitScreenController> splitScreenControllerOptional,
85             PipDesktopState pipDesktopState) {
86         mContext = context;
87         mPipBoundsState = pipBoundsState;
88         mMainExecutor = mainExecutor;
89         mPipTransitionState = pipTransitionState;
90         mPipTransitionState.addPipTransitionStateChangedListener(this);
91         mPipDesktopState = pipDesktopState;
92         mSplitScreenControllerOptional = splitScreenControllerOptional;
93 
94         mSurfaceControlTransactionFactory =
95                 new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
96         mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(mContext);
97         mPipAlphaAnimatorSupplier = PipAlphaAnimator::new;
98     }
99 
setPipTransitionController(PipTransitionController pipTransitionController)100     void setPipTransitionController(PipTransitionController pipTransitionController) {
101         mPipTransitionController = pipTransitionController;
102     }
103 
104     @Nullable
getExitPipViaExpandTransaction()105     private WindowContainerTransaction getExitPipViaExpandTransaction() {
106         WindowContainerToken pipTaskToken = mPipTransitionState.getPipTaskToken();
107         if (pipTaskToken == null) {
108             return null;
109         }
110         WindowContainerTransaction wct = new WindowContainerTransaction();
111         // final expanded bounds to be inherited from the parent
112         wct.setBounds(pipTaskToken, null);
113         // if we are hitting a multi-activity case
114         // windowing mode change will reparent to original host task
115         wct.setWindowingMode(pipTaskToken, mPipDesktopState.getOutPipWindowingMode());
116         return wct;
117     }
118 
119     /**
120      * Schedules exit PiP via expand transition.
121      */
scheduleExitPipViaExpand()122     public void scheduleExitPipViaExpand() {
123         mMainExecutor.execute(() -> {
124             if (!mPipTransitionState.isInPip()) return;
125 
126             final WindowContainerTransaction expandWct = getExitPipViaExpandTransaction();
127             if (expandWct == null) return;
128 
129             final WindowContainerTransaction wct = new WindowContainerTransaction();
130             mSplitScreenControllerOptional.ifPresent(splitScreenController -> {
131                 int lastParentTaskId = mPipTransitionState.getPipTaskInfo()
132                         .lastParentTaskIdBeforePip;
133                 if (splitScreenController.isTaskInSplitScreen(lastParentTaskId)) {
134                     splitScreenController.prepareEnterSplitScreen(wct,
135                             null /* taskInfo */, SplitScreenConstants.SPLIT_POSITION_UNDEFINED);
136                 }
137             });
138 
139             boolean toSplit = !wct.isEmpty();
140             wct.merge(expandWct, true /* transfer */);
141             mPipTransitionController.startExpandTransition(wct, toSplit);
142         });
143     }
144 
145     /** Schedules remove PiP transition. */
scheduleRemovePip(boolean withFadeout)146     public void scheduleRemovePip(boolean withFadeout) {
147         mMainExecutor.execute(() -> {
148             if (!mPipTransitionState.isInPip()) return;
149             mPipTransitionController.startRemoveTransition(withFadeout);
150         });
151     }
152 
153     /**
154      * Animates resizing of the pinned stack given the duration.
155      */
scheduleAnimateResizePip(Rect toBounds)156     public void scheduleAnimateResizePip(Rect toBounds) {
157         scheduleAnimateResizePip(toBounds, false /* configAtEnd */);
158     }
159 
160     /**
161      * Animates resizing of the pinned stack given the duration.
162      *
163      * @param configAtEnd true if we are delaying config updates until the transition ends.
164      */
scheduleAnimateResizePip(Rect toBounds, boolean configAtEnd)165     public void scheduleAnimateResizePip(Rect toBounds, boolean configAtEnd) {
166         scheduleAnimateResizePip(toBounds, configAtEnd,
167                 PipTransition.BOUNDS_CHANGE_JUMPCUT_DURATION);
168     }
169 
170     /**
171      * Animates resizing of the pinned stack given the duration.
172      *
173      * @param configAtEnd true if we are delaying config updates until the transition ends.
174      * @param duration    the suggested duration to run the animation; the component responsible
175      *                    for running the animator will get this as an extra.
176      */
scheduleAnimateResizePip(Rect toBounds, boolean configAtEnd, int duration)177     public void scheduleAnimateResizePip(Rect toBounds, boolean configAtEnd, int duration) {
178         WindowContainerToken pipTaskToken = mPipTransitionState.getPipTaskToken();
179         if (pipTaskToken == null || !mPipTransitionState.isInPip()) {
180             return;
181         }
182         WindowContainerTransaction wct = new WindowContainerTransaction();
183         if (configAtEnd) {
184             wct.deferConfigToTransitionEnd(pipTaskToken);
185 
186             if (mPipBoundsState.getBounds().width() == toBounds.width()
187                     && mPipBoundsState.getBounds().height() == toBounds.height()) {
188                 // TODO (b/393159816): Config-at-End causes a flicker without size change.
189                 // If PiP size isn't changing enforce a minimal one-pixel change as a workaround.
190                 --toBounds.bottom;
191             }
192         }
193         wct.setBounds(pipTaskToken, toBounds);
194         mPipTransitionController.startResizeTransition(wct, duration);
195     }
196 
197     /**
198      * Signals to Core to finish the PiP resize transition.
199      * Note that we do not allow any actual WM Core changes at this point.
200      *
201      * @param toBounds destination bounds used only for internal state updates - not sent to Core.
202      */
scheduleFinishResizePip(Rect toBounds)203     public void scheduleFinishResizePip(Rect toBounds) {
204         // Make updates to the internal state to reflect new bounds before updating any transitions
205         // related state; transition state updates can trigger callbacks that use the cached bounds.
206         onFinishingPipResize(toBounds);
207         mPipTransitionController.finishTransition();
208     }
209 
210     /**
211      * Directly perform a scaled matrix transformation on the leash. This will not perform any
212      * {@link WindowContainerTransaction}.
213      */
scheduleUserResizePip(Rect toBounds)214     public void scheduleUserResizePip(Rect toBounds) {
215         scheduleUserResizePip(toBounds, 0f /* degrees */);
216     }
217 
218     /**
219      * Directly perform a scaled matrix transformation on the leash. This will not perform any
220      * {@link WindowContainerTransaction}.
221      *
222      * @param degrees the angle to rotate the bounds to.
223      */
scheduleUserResizePip(Rect toBounds, float degrees)224     public void scheduleUserResizePip(Rect toBounds, float degrees) {
225         if (toBounds.isEmpty() || !mPipTransitionState.isInPip()) {
226             ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
227                     "%s: Attempted to user resize PIP in invalid state, aborting;"
228                             + "toBounds=%s, mPipTransitionState=%s",
229                     TAG, toBounds, mPipTransitionState);
230             return;
231         }
232         SurfaceControl leash = mPipTransitionState.getPinnedTaskLeash();
233         final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction();
234 
235         Matrix transformTensor = new Matrix();
236         final float[] mMatrixTmp = new float[9];
237         final float scale = (float) toBounds.width() / mPipBoundsState.getBounds().width();
238 
239         transformTensor.setScale(scale, scale);
240         transformTensor.postTranslate(toBounds.left, toBounds.top);
241         transformTensor.postRotate(degrees, toBounds.centerX(), toBounds.centerY());
242 
243         mPipSurfaceTransactionHelper.round(tx, leash, mPipBoundsState.getBounds(), toBounds);
244 
245         tx.setMatrix(leash, transformTensor, mMatrixTmp);
246         tx.apply();
247     }
248 
startOverlayFadeoutAnimation(@onNull SurfaceControl overlayLeash, boolean withStartDelay, @NonNull Runnable onAnimationEnd)249     void startOverlayFadeoutAnimation(@NonNull SurfaceControl overlayLeash,
250             boolean withStartDelay, @NonNull Runnable onAnimationEnd) {
251         mOverlayFadeoutAnimator = mPipAlphaAnimatorSupplier.get(mContext, overlayLeash,
252                 null /* startTx */, null /* finishTx */, PipAlphaAnimator.FADE_OUT);
253         mOverlayFadeoutAnimator.setDuration(CONTENT_OVERLAY_FADE_OUT_DURATION_MS);
254         mOverlayFadeoutAnimator.setStartDelay(withStartDelay
255                 ? EXTRA_CONTENT_OVERLAY_FADE_OUT_DELAY_MS : 0);
256         mOverlayFadeoutAnimator.setAnimationEndCallback(() -> {
257             onAnimationEnd.run();
258             mOverlayFadeoutAnimator = null;
259         });
260         mOverlayFadeoutAnimator.start();
261     }
262 
setUpdateMovementBoundsRunnable(@ullable Runnable updateMovementBoundsRunnable)263     void setUpdateMovementBoundsRunnable(@Nullable Runnable updateMovementBoundsRunnable) {
264         mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable;
265     }
266 
maybeUpdateMovementBounds()267     private void maybeUpdateMovementBounds() {
268         if (mUpdateMovementBoundsRunnable != null)  {
269             mUpdateMovementBoundsRunnable.run();
270         }
271     }
272 
onFinishingPipResize(Rect newBounds)273     private void onFinishingPipResize(Rect newBounds) {
274         if (mPipBoundsState.getBounds().equals(newBounds)) {
275             return;
276         }
277 
278         // Take a screenshot of PiP and fade it out after resize is finished if seamless resize
279         // is off and if the PiP size is changing.
280         boolean animateCrossFadeResize = !getPipParams().isSeamlessResizeEnabled()
281                 && !(mPipBoundsState.getBounds().width() == newBounds.width()
282                 && mPipBoundsState.getBounds().height() == newBounds.height());
283         if (animateCrossFadeResize) {
284             final Rect crop = new Rect(newBounds);
285             crop.offsetTo(0, 0);
286             // Note: Put this at layer=MAX_VALUE-2 since the input consumer for PIP is placed at
287             //       MAX_VALUE-1
288             final SurfaceControl snapshotSurface = ScreenshotUtils.takeScreenshot(
289                     mSurfaceControlTransactionFactory.getTransaction(),
290                     mPipTransitionState.getPinnedTaskLeash(), crop, Integer.MAX_VALUE - 2);
291             startOverlayFadeoutAnimation(snapshotSurface, false /* withStartDelay */, () -> {
292                 mSurfaceControlTransactionFactory.getTransaction().remove(snapshotSurface).apply();
293             });
294         }
295 
296         mPipBoundsState.setBounds(newBounds);
297         maybeUpdateMovementBounds();
298     }
299 
300     @VisibleForTesting
setSurfaceControlTransactionFactory( @onNull PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory)301     void setSurfaceControlTransactionFactory(
302             @NonNull PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) {
303         mSurfaceControlTransactionFactory = factory;
304     }
305 
306     @Override
onPipTransitionStateChanged(@ipTransitionState.TransitionState int oldState, @PipTransitionState.TransitionState int newState, @android.annotation.Nullable Bundle extra)307     public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState,
308             @PipTransitionState.TransitionState int newState,
309             @android.annotation.Nullable Bundle extra) {
310         switch (newState) {
311             case PipTransitionState.EXITING_PIP:
312             case PipTransitionState.SCHEDULED_BOUNDS_CHANGE:
313                 if (mOverlayFadeoutAnimator != null && mOverlayFadeoutAnimator.isStarted()) {
314                     mOverlayFadeoutAnimator.end();
315                     mOverlayFadeoutAnimator = null;
316                 }
317                 break;
318         }
319     }
320 
321     @VisibleForTesting
322     interface PipAlphaAnimatorSupplier {
get(@onNull Context context, SurfaceControl leash, SurfaceControl.Transaction startTransaction, SurfaceControl.Transaction finishTransaction, @PipAlphaAnimator.Fade int direction)323         PipAlphaAnimator get(@NonNull Context context,
324                 SurfaceControl leash,
325                 SurfaceControl.Transaction startTransaction,
326                 SurfaceControl.Transaction finishTransaction,
327                 @PipAlphaAnimator.Fade int direction);
328     }
329 
330     @VisibleForTesting
setPipAlphaAnimatorSupplier(@onNull PipAlphaAnimatorSupplier supplier)331     void setPipAlphaAnimatorSupplier(@NonNull PipAlphaAnimatorSupplier supplier) {
332         mPipAlphaAnimatorSupplier = supplier;
333     }
334 
335     @VisibleForTesting
setOverlayFadeoutAnimator(@onNull PipAlphaAnimator animator)336     void setOverlayFadeoutAnimator(@NonNull PipAlphaAnimator animator) {
337         mOverlayFadeoutAnimator = animator;
338     }
339 
340     @VisibleForTesting
341     @Nullable
getOverlayFadeoutAnimator()342     PipAlphaAnimator getOverlayFadeoutAnimator() {
343         return mOverlayFadeoutAnimator;
344     }
345 
setPipParamsSupplier(@onNull Supplier<PictureInPictureParams> pipParamsSupplier)346     void setPipParamsSupplier(@NonNull Supplier<PictureInPictureParams> pipParamsSupplier) {
347         mPipParamsSupplier = pipParamsSupplier;
348     }
349 
350     @NonNull
getPipParams()351     private PictureInPictureParams getPipParams() {
352         if (mPipParamsSupplier == null) return new PictureInPictureParams.Builder().build();
353         return mPipParamsSupplier.get();
354     }
355 }
356