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