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 static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
20 
21 import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
22 
23 import android.content.BroadcastReceiver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.graphics.Matrix;
28 import android.graphics.Rect;
29 import android.view.SurfaceControl;
30 import android.window.WindowContainerTransaction;
31 
32 import androidx.annotation.IntDef;
33 import androidx.annotation.Nullable;
34 import androidx.core.content.ContextCompat;
35 
36 import com.android.internal.protolog.common.ProtoLog;
37 import com.android.wm.shell.common.ShellExecutor;
38 import com.android.wm.shell.common.pip.PipBoundsState;
39 import com.android.wm.shell.common.pip.PipUtils;
40 import com.android.wm.shell.pip.PipTransitionController;
41 import com.android.wm.shell.protolog.ShellProtoLogGroup;
42 
43 import java.lang.annotation.Retention;
44 import java.lang.annotation.RetentionPolicy;
45 
46 /**
47  * Scheduler for Shell initiated PiP transitions and animations.
48  */
49 public class PipScheduler {
50     private static final String TAG = PipScheduler.class.getSimpleName();
51     private static final String BROADCAST_FILTER = PipScheduler.class.getCanonicalName();
52 
53     private final Context mContext;
54     private final PipBoundsState mPipBoundsState;
55     private final ShellExecutor mMainExecutor;
56     private final PipTransitionState mPipTransitionState;
57     private PipSchedulerReceiver mSchedulerReceiver;
58     private PipTransitionController mPipTransitionController;
59 
60     /**
61      * Temporary PiP CUJ codes to schedule PiP related transitions directly from Shell.
62      * This is used for a broadcast receiver to resolve intents. This should be removed once
63      * there is an equivalent of PipTouchHandler and PipResizeGestureHandler for PiP2.
64      */
65     private static final int PIP_EXIT_VIA_EXPAND_CODE = 0;
66     private static final int PIP_DOUBLE_TAP = 1;
67 
68     @IntDef(value = {
69             PIP_EXIT_VIA_EXPAND_CODE,
70             PIP_DOUBLE_TAP
71     })
72     @Retention(RetentionPolicy.SOURCE)
73     @interface PipUserJourneyCode {}
74 
75     /**
76      * A temporary broadcast receiver to initiate PiP CUJs.
77      */
78     private class PipSchedulerReceiver extends BroadcastReceiver {
79         @Override
onReceive(Context context, Intent intent)80         public void onReceive(Context context, Intent intent) {
81             int userJourneyCode = intent.getIntExtra("cuj_code_extra", 0);
82             switch (userJourneyCode) {
83                 case PIP_EXIT_VIA_EXPAND_CODE:
84                     scheduleExitPipViaExpand();
85                     break;
86                 case PIP_DOUBLE_TAP:
87                     scheduleDoubleTapToResize();
88                     break;
89                 default:
90                     throw new IllegalStateException("unexpected CUJ code=" + userJourneyCode);
91             }
92         }
93     }
94 
PipScheduler(Context context, PipBoundsState pipBoundsState, ShellExecutor mainExecutor, PipTransitionState pipTransitionState)95     public PipScheduler(Context context,
96             PipBoundsState pipBoundsState,
97             ShellExecutor mainExecutor,
98             PipTransitionState pipTransitionState) {
99         mContext = context;
100         mPipBoundsState = pipBoundsState;
101         mMainExecutor = mainExecutor;
102         mPipTransitionState = pipTransitionState;
103 
104         if (PipUtils.isPip2ExperimentEnabled()) {
105             // temporary broadcast receiver to initiate exit PiP via expand
106             mSchedulerReceiver = new PipSchedulerReceiver();
107             ContextCompat.registerReceiver(mContext, mSchedulerReceiver,
108                     new IntentFilter(BROADCAST_FILTER), ContextCompat.RECEIVER_EXPORTED);
109         }
110     }
111 
getMainExecutor()112     ShellExecutor getMainExecutor() {
113         return mMainExecutor;
114     }
115 
setPipTransitionController(PipTransitionController pipTransitionController)116     void setPipTransitionController(PipTransitionController pipTransitionController) {
117         mPipTransitionController = pipTransitionController;
118     }
119 
120     @Nullable
getExitPipViaExpandTransaction()121     private WindowContainerTransaction getExitPipViaExpandTransaction() {
122         if (mPipTransitionState.mPipTaskToken == null) {
123             return null;
124         }
125         WindowContainerTransaction wct = new WindowContainerTransaction();
126         // final expanded bounds to be inherited from the parent
127         wct.setBounds(mPipTransitionState.mPipTaskToken, null);
128         // if we are hitting a multi-activity case
129         // windowing mode change will reparent to original host task
130         wct.setWindowingMode(mPipTransitionState.mPipTaskToken, WINDOWING_MODE_UNDEFINED);
131         return wct;
132     }
133 
134     /**
135      * Schedules exit PiP via expand transition.
136      */
scheduleExitPipViaExpand()137     public void scheduleExitPipViaExpand() {
138         WindowContainerTransaction wct = getExitPipViaExpandTransaction();
139         if (wct != null) {
140             mMainExecutor.execute(() -> {
141                 mPipTransitionController.startExitTransition(TRANSIT_EXIT_PIP, wct,
142                         null /* destinationBounds */);
143             });
144         }
145     }
146 
147     /**
148      * Schedules resize PiP via double tap.
149      */
scheduleDoubleTapToResize()150     public void scheduleDoubleTapToResize() {}
151 
152     /**
153      * Animates resizing of the pinned stack given the duration.
154      */
scheduleAnimateResizePip(Rect toBounds)155     public void scheduleAnimateResizePip(Rect toBounds) {
156         scheduleAnimateResizePip(toBounds, false /* configAtEnd */);
157     }
158 
159     /**
160      * Animates resizing of the pinned stack given the duration.
161      *
162      * @param configAtEnd true if we are delaying config updates until the transition ends.
163      */
scheduleAnimateResizePip(Rect toBounds, boolean configAtEnd)164     public void scheduleAnimateResizePip(Rect toBounds, boolean configAtEnd) {
165         if (mPipTransitionState.mPipTaskToken == null || !mPipTransitionState.isInPip()) {
166             return;
167         }
168         WindowContainerTransaction wct = new WindowContainerTransaction();
169         wct.setBounds(mPipTransitionState.mPipTaskToken, toBounds);
170         if (configAtEnd) {
171             wct.deferConfigToTransitionEnd(mPipTransitionState.mPipTaskToken);
172         }
173         mPipTransitionController.startResizeTransition(wct);
174     }
175 
176     /**
177      * Signals to Core to finish the PiP resize transition.
178      * Note that we do not allow any actual WM Core changes at this point.
179      *
180      * @param configAtEnd true if we are waiting for config updates at the end of the transition.
181      */
scheduleFinishResizePip(boolean configAtEnd)182     public void scheduleFinishResizePip(boolean configAtEnd) {
183         SurfaceControl.Transaction tx = null;
184         if (configAtEnd) {
185             tx = new SurfaceControl.Transaction();
186             tx.addTransactionCommittedListener(mMainExecutor, () -> {
187                 mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS);
188             });
189         } else {
190             mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS);
191         }
192         mPipTransitionController.finishTransition(tx);
193     }
194 
195     /**
196      * Directly perform a scaled matrix transformation on the leash. This will not perform any
197      * {@link WindowContainerTransaction}.
198      */
scheduleUserResizePip(Rect toBounds)199     public void scheduleUserResizePip(Rect toBounds) {
200         scheduleUserResizePip(toBounds, 0f /* degrees */);
201     }
202 
203     /**
204      * Directly perform a scaled matrix transformation on the leash. This will not perform any
205      * {@link WindowContainerTransaction}.
206      *
207      * @param degrees the angle to rotate the bounds to.
208      */
scheduleUserResizePip(Rect toBounds, float degrees)209     public void scheduleUserResizePip(Rect toBounds, float degrees) {
210         if (toBounds.isEmpty()) {
211             ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
212                     "%s: Attempted to user resize PIP to empty bounds, aborting.", TAG);
213             return;
214         }
215         SurfaceControl leash = mPipTransitionState.mPinnedTaskLeash;
216         final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
217 
218         Matrix transformTensor = new Matrix();
219         final float[] mMatrixTmp = new float[9];
220         final float scale = (float) toBounds.width() / mPipBoundsState.getBounds().width();
221 
222         transformTensor.setScale(scale, scale);
223         transformTensor.postTranslate(toBounds.left, toBounds.top);
224         transformTensor.postRotate(degrees, toBounds.centerX(), toBounds.centerY());
225 
226         tx.setMatrix(leash, transformTensor, mMatrixTmp);
227         tx.apply();
228     }
229 }
230