1 /*
2  * Copyright (C) 2016 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.server.wm;
18 
19 import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
20 
21 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
22 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
23 
24 import android.app.PictureInPictureParams;
25 import android.content.ComponentName;
26 import android.content.res.Resources;
27 import android.graphics.Insets;
28 import android.graphics.Matrix;
29 import android.graphics.Rect;
30 import android.os.IBinder;
31 import android.os.RemoteException;
32 import android.util.Log;
33 import android.util.RotationUtils;
34 import android.util.Slog;
35 import android.view.IPinnedTaskListener;
36 import android.view.Surface;
37 import android.view.SurfaceControl;
38 import android.window.PictureInPictureSurfaceTransaction;
39 
40 import java.io.PrintWriter;
41 
42 /**
43  * Holds the common state of the pinned task between the system and SystemUI. If SystemUI ever
44  * needs to be restarted, it will be notified with the last known state.
45  *
46  * Changes to the pinned task also flow through this controller, and generally, the system only
47  * changes the pinned task bounds through this controller in two ways:
48  *
49  * 1) When first entering PiP: the controller returns the valid bounds given, taking aspect ratio
50  *    and IME state into account.
51  * 2) When rotating the device: the controller calculates the new bounds in the new orientation,
52  *    taking the IME state into account. In this case, we currently ignore the
53  *    SystemUI adjustments (ie. expanded for menu, interaction, etc).
54  *
55  * Other changes in the system, including adjustment of IME, configuration change, and more are
56  * handled by SystemUI (similar to the docked task divider).
57  */
58 class PinnedTaskController {
59 
60     private static final String TAG = TAG_WITH_CLASS_NAME ? "PinnedTaskController" : TAG_WM;
61     private static final int DEFER_ORIENTATION_CHANGE_TIMEOUT_MS = 1000;
62 
63     private final WindowManagerService mService;
64     private final DisplayContent mDisplayContent;
65 
66     private IPinnedTaskListener mPinnedTaskListener;
67     private final PinnedTaskListenerDeathHandler mPinnedTaskListenerDeathHandler =
68             new PinnedTaskListenerDeathHandler();
69 
70     /**
71      * Non-null if the entering PiP task will cause display rotation to change. The bounds are
72      * based on the new rotation.
73      */
74     private Rect mDestRotatedBounds;
75     /**
76      * Non-null if the entering PiP task from recents animation will cause display rotation to
77      * change. The transaction is based on the old rotation.
78      */
79     private PictureInPictureSurfaceTransaction mPipTransaction;
80     /** Whether to skip task configuration change once. */
81     private boolean mFreezingTaskConfig;
82     /** Defer display orientation change if the PiP task is animating across orientations. */
83     private boolean mDeferOrientationChanging;
84     private final Runnable mDeferOrientationTimeoutRunnable;
85 
86     private boolean mIsImeShowing;
87     private int mImeHeight;
88 
89     // The aspect ratio bounds of the PIP.
90     private float mMinAspectRatio;
91     private float mMaxAspectRatio;
92 
93     /**
94      * Handler for the case where the listener dies.
95      */
96     private class PinnedTaskListenerDeathHandler implements IBinder.DeathRecipient {
97 
98         @Override
binderDied()99         public void binderDied() {
100             synchronized (mService.mGlobalLock) {
101                 mPinnedTaskListener = null;
102                 mFreezingTaskConfig = false;
103                 mDeferOrientationTimeoutRunnable.run();
104             }
105         }
106     }
107 
PinnedTaskController(WindowManagerService service, DisplayContent displayContent)108     PinnedTaskController(WindowManagerService service, DisplayContent displayContent) {
109         mService = service;
110         mDisplayContent = displayContent;
111         mDeferOrientationTimeoutRunnable = () -> {
112             synchronized (mService.mGlobalLock) {
113                 if (mDeferOrientationChanging) {
114                     continueOrientationChange();
115                     mService.mWindowPlacerLocked.requestTraversal();
116                 }
117             }
118         };
119         reloadResources();
120     }
121 
122     /** Updates the resources used by pinned controllers.  */
onPostDisplayConfigurationChanged()123     void onPostDisplayConfigurationChanged() {
124         reloadResources();
125         mFreezingTaskConfig = false;
126     }
127 
128     /**
129      * Reloads all the resources for the current configuration.
130      */
reloadResources()131     private void reloadResources() {
132         final Resources res = mService.mContext.getResources();
133         mMinAspectRatio = res.getFloat(
134                 com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio);
135         mMaxAspectRatio = res.getFloat(
136                 com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio);
137     }
138 
139     /**
140      * Registers a pinned task listener.
141      */
registerPinnedTaskListener(IPinnedTaskListener listener)142     void registerPinnedTaskListener(IPinnedTaskListener listener) {
143         try {
144             listener.asBinder().linkToDeath(mPinnedTaskListenerDeathHandler, 0);
145             mPinnedTaskListener = listener;
146             notifyImeVisibilityChanged(mIsImeShowing, mImeHeight);
147             notifyMovementBoundsChanged(false /* fromImeAdjustment */);
148         } catch (RemoteException e) {
149             Log.e(TAG, "Failed to register pinned task listener", e);
150         }
151     }
152 
153     /**
154      * @return whether the given {@param aspectRatio} is valid, i.e. min <= ratio <= max.
155      */
isValidPictureInPictureAspectRatio(float aspectRatio)156     public boolean isValidPictureInPictureAspectRatio(float aspectRatio) {
157         return Float.compare(mMinAspectRatio, aspectRatio) <= 0
158                 && Float.compare(aspectRatio, mMaxAspectRatio) <= 0;
159     }
160 
161     /**
162      * @return whether the given {@param aspectRatio} is valid, i.e. ratio < min or ratio > max.
163      */
isValidExpandedPictureInPictureAspectRatio(float aspectRatio)164     public boolean isValidExpandedPictureInPictureAspectRatio(float aspectRatio) {
165         return Float.compare(mMinAspectRatio, aspectRatio) > 0
166                 || Float.compare(aspectRatio, mMaxAspectRatio) > 0;
167     }
168 
169     /**
170      * Called when a fullscreen task is entering PiP with display orientation change. This is used
171      * to avoid flickering when running PiP animation across different orientations.
172      */
deferOrientationChangeForEnteringPipFromFullScreenIfNeeded()173     void deferOrientationChangeForEnteringPipFromFullScreenIfNeeded() {
174         final ActivityRecord topFullscreen = mDisplayContent.getActivity(
175                 a -> a.providesOrientation() && !a.getTask().inMultiWindowMode());
176         if (topFullscreen == null || topFullscreen.hasFixedRotationTransform()) {
177             return;
178         }
179         final int rotation = mDisplayContent.rotationForActivityInDifferentOrientation(
180                 topFullscreen);
181         if (rotation == ROTATION_UNDEFINED) {
182             return;
183         }
184         // If the next top activity will change the orientation of display, start fixed rotation to
185         // notify PipTaskOrganizer before it receives task appeared. And defer display orientation
186         // update until the new PiP bounds are set.
187         mDisplayContent.setFixedRotationLaunchingApp(topFullscreen, rotation);
188         mDeferOrientationChanging = true;
189         mService.mH.removeCallbacks(mDeferOrientationTimeoutRunnable);
190         final float animatorScale = Math.max(1, mService.getCurrentAnimatorScale());
191         mService.mH.postDelayed(mDeferOrientationTimeoutRunnable,
192                 (int) (animatorScale * DEFER_ORIENTATION_CHANGE_TIMEOUT_MS));
193     }
194 
195     /** Defers orientation change while there is a top fixed rotation activity. */
shouldDeferOrientationChange()196     boolean shouldDeferOrientationChange() {
197         return mDeferOrientationChanging;
198     }
199 
200     /**
201      * Sets the bounds for {@link #startSeamlessRotationIfNeeded} if the orientation of display
202      * will be changed.
203      */
setEnterPipBounds(Rect bounds)204     void setEnterPipBounds(Rect bounds) {
205         if (!mDeferOrientationChanging) {
206             return;
207         }
208         mFreezingTaskConfig = true;
209         mDestRotatedBounds = new Rect(bounds);
210         if (!mDisplayContent.mTransitionController.isShellTransitionsEnabled()) {
211             continueOrientationChange();
212         }
213     }
214 
215     /**
216      * Sets the transaction for {@link #startSeamlessRotationIfNeeded} if the orientation of display
217      * will be changed. This is only called when finishing recents animation with pending
218      * orientation change that will be handled by
219      * {@link DisplayContent.FixedRotationTransitionListener#onFinishRecentsAnimation}.
220      */
setEnterPipTransaction(PictureInPictureSurfaceTransaction tx)221     void setEnterPipTransaction(PictureInPictureSurfaceTransaction tx) {
222         mFreezingTaskConfig = true;
223         mPipTransaction = tx;
224     }
225 
226     /** Called when the activity in PiP task has PiP windowing mode (at the end of animation). */
continueOrientationChange()227     private void continueOrientationChange() {
228         mDeferOrientationChanging = false;
229         mService.mH.removeCallbacks(mDeferOrientationTimeoutRunnable);
230         final WindowContainer<?> orientationSource = mDisplayContent.getLastOrientationSource();
231         if (orientationSource != null && !orientationSource.isAppTransitioning()) {
232             mDisplayContent.continueUpdateOrientationForDiffOrienLaunchingApp();
233         }
234     }
235 
236     /**
237      * Resets rotation and applies scale and position to PiP task surface to match the current
238      * rotation of display. The final surface matrix will be replaced by PiPTaskOrganizer after it
239      * receives the callback of fixed rotation completion.
240      */
startSeamlessRotationIfNeeded(SurfaceControl.Transaction t, int oldRotation, int newRotation)241     void startSeamlessRotationIfNeeded(SurfaceControl.Transaction t,
242             int oldRotation, int newRotation) {
243         final Rect bounds = mDestRotatedBounds;
244         final PictureInPictureSurfaceTransaction pipTx = mPipTransaction;
245         final boolean emptyPipPositionTx = pipTx == null || pipTx.mPosition == null;
246         if (bounds == null && emptyPipPositionTx) {
247             return;
248         }
249         final TaskDisplayArea taskArea = mDisplayContent.getDefaultTaskDisplayArea();
250         final Task pinnedTask = taskArea.getRootPinnedTask();
251         if (pinnedTask == null) {
252             return;
253         }
254 
255         mDestRotatedBounds = null;
256         mPipTransaction = null;
257         final Rect areaBounds = taskArea.getBounds();
258         if (!emptyPipPositionTx) {
259             // The transaction from recents animation is in old rotation. So the position needs to
260             // be rotated.
261             float dx = pipTx.mPosition.x;
262             float dy = pipTx.mPosition.y;
263             final Matrix matrix = pipTx.getMatrix();
264             if (pipTx.mRotation == 90) {
265                 dx = pipTx.mPosition.y;
266                 dy = areaBounds.right - pipTx.mPosition.x;
267                 matrix.postRotate(-90);
268             } else if (pipTx.mRotation == -90) {
269                 dx = areaBounds.bottom - pipTx.mPosition.y;
270                 dy = pipTx.mPosition.x;
271                 matrix.postRotate(90);
272             }
273             matrix.postTranslate(dx, dy);
274             final SurfaceControl leash = pinnedTask.getSurfaceControl();
275             t.setMatrix(leash, matrix, new float[9]);
276             if (pipTx.hasCornerRadiusSet()) {
277                 t.setCornerRadius(leash, pipTx.mCornerRadius);
278             }
279             Slog.i(TAG, "Seamless rotation PiP tx=" + pipTx + " pos=" + dx + "," + dy);
280             return;
281         }
282 
283         final PictureInPictureParams params = pinnedTask.getPictureInPictureParams();
284         final Rect sourceHintRect = params != null && params.hasSourceBoundsHint()
285                 ? params.getSourceRectHint()
286                 : null;
287         Slog.i(TAG, "Seamless rotation PiP bounds=" + bounds + " hintRect=" + sourceHintRect);
288         final int rotationDelta = RotationUtils.deltaRotation(oldRotation, newRotation);
289         // Adjust for display cutout if applicable.
290         if (sourceHintRect != null && rotationDelta == Surface.ROTATION_270) {
291             if (pinnedTask.getDisplayCutoutInsets() != null) {
292                 final int rotationBackDelta = RotationUtils.deltaRotation(newRotation, oldRotation);
293                 final Rect displayCutoutInsets = RotationUtils.rotateInsets(
294                         Insets.of(pinnedTask.getDisplayCutoutInsets()), rotationBackDelta).toRect();
295                 sourceHintRect.offset(displayCutoutInsets.left, displayCutoutInsets.top);
296             }
297         }
298         final Rect contentBounds = sourceHintRect != null && areaBounds.contains(sourceHintRect)
299                 ? sourceHintRect : areaBounds;
300         final int w = contentBounds.width();
301         final int h = contentBounds.height();
302         final float scale = w <= h ? (float) bounds.width() / w : (float) bounds.height() / h;
303         final int insetLeft = (int) ((contentBounds.left - areaBounds.left) * scale + .5f);
304         final int insetTop = (int) ((contentBounds.top - areaBounds.top) * scale + .5f);
305         final Matrix matrix = new Matrix();
306         matrix.setScale(scale, scale);
307         matrix.postTranslate(bounds.left - insetLeft, bounds.top - insetTop);
308         t.setMatrix(pinnedTask.getSurfaceControl(), matrix, new float[9]);
309     }
310 
311     /**
312      * Returns {@code true} to skip {@link Task#onConfigurationChanged} because it is expected that
313      * there will be a orientation change and a PiP configuration change.
314      */
isFreezingTaskConfig(Task task)315     boolean isFreezingTaskConfig(Task task) {
316         return mFreezingTaskConfig
317                 && task == mDisplayContent.getDefaultTaskDisplayArea().getRootPinnedTask();
318     }
319 
320     /** Resets the states which were used to perform fixed rotation with PiP task. */
onCancelFixedRotationTransform()321     void onCancelFixedRotationTransform() {
322         mFreezingTaskConfig = false;
323         mDeferOrientationChanging = false;
324         mDestRotatedBounds = null;
325         mPipTransaction = null;
326     }
327 
328     /**
329      * Activity is hidden (either stopped or removed), resets the last saved snap fraction
330      * so that the default bounds will be returned for the next session.
331      */
onActivityHidden(ComponentName componentName)332     void onActivityHidden(ComponentName componentName) {
333         if (mPinnedTaskListener == null) return;
334         try {
335             mPinnedTaskListener.onActivityHidden(componentName);
336         } catch (RemoteException e) {
337             Slog.e(TAG_WM, "Error delivering reset reentry fraction event.", e);
338         }
339     }
340 
341     /**
342      * Sets the Ime state and height.
343      */
setAdjustedForIme(boolean adjustedForIme, int imeHeight)344     void setAdjustedForIme(boolean adjustedForIme, int imeHeight) {
345         // Due to the order of callbacks from the system, we may receive an ime height even when
346         // {@param adjustedForIme} is false, and also a zero height when {@param adjustedForIme}
347         // is true.  Instead, ensure that the ime state changes with the height and if the ime is
348         // showing, then the height is non-zero.
349         final boolean imeShowing = adjustedForIme && imeHeight > 0;
350         imeHeight = imeShowing ? imeHeight : 0;
351         if (imeShowing == mIsImeShowing && imeHeight == mImeHeight) {
352             return;
353         }
354 
355         mIsImeShowing = imeShowing;
356         mImeHeight = imeHeight;
357         notifyImeVisibilityChanged(imeShowing, imeHeight);
358         notifyMovementBoundsChanged(true /* fromImeAdjustment */);
359     }
360 
361     /**
362      * Notifies listeners that the PIP needs to be adjusted for the IME.
363      */
notifyImeVisibilityChanged(boolean imeVisible, int imeHeight)364     private void notifyImeVisibilityChanged(boolean imeVisible, int imeHeight) {
365         if (mPinnedTaskListener != null) {
366             try {
367                 mPinnedTaskListener.onImeVisibilityChanged(imeVisible, imeHeight);
368             } catch (RemoteException e) {
369                 Slog.e(TAG_WM, "Error delivering bounds changed event.", e);
370             }
371         }
372     }
373 
374     /**
375      * Notifies listeners that the PIP movement bounds have changed.
376      */
notifyMovementBoundsChanged(boolean fromImeAdjustment)377     private void notifyMovementBoundsChanged(boolean fromImeAdjustment) {
378         synchronized (mService.mGlobalLock) {
379             if (mPinnedTaskListener == null) {
380                 return;
381             }
382             try {
383                 mPinnedTaskListener.onMovementBoundsChanged(fromImeAdjustment);
384             } catch (RemoteException e) {
385                 Slog.e(TAG_WM, "Error delivering actions changed event.", e);
386             }
387         }
388     }
389 
dump(String prefix, PrintWriter pw)390     void dump(String prefix, PrintWriter pw) {
391         pw.println(prefix + "PinnedTaskController");
392         if (mDeferOrientationChanging) pw.println(prefix + "  mDeferOrientationChanging=true");
393         if (mFreezingTaskConfig) pw.println(prefix + "  mFreezingTaskConfig=true");
394         if (mDestRotatedBounds != null) {
395             pw.println(prefix + "  mPendingBounds=" + mDestRotatedBounds);
396         }
397         if (mPipTransaction != null) {
398             pw.println(prefix + "  mPipTransaction=" + mPipTransaction);
399         }
400         pw.println(prefix + "  mIsImeShowing=" + mIsImeShowing);
401         pw.println(prefix + "  mImeHeight=" + mImeHeight);
402         pw.println(prefix + "  mMinAspectRatio=" + mMinAspectRatio);
403         pw.println(prefix + "  mMaxAspectRatio=" + mMaxAspectRatio);
404     }
405 }
406