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.ActivityManager.StackId.PINNED_STACK_ID;
20 import static android.util.TypedValue.COMPLEX_UNIT_DIP;
21 
22 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
23 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
24 
25 import android.app.RemoteAction;
26 import android.content.pm.ParceledListSlice;
27 import android.content.res.Resources;
28 import android.graphics.Point;
29 import android.graphics.Rect;
30 import android.os.Handler;
31 import android.os.IBinder;
32 import android.os.RemoteException;
33 import android.util.DisplayMetrics;
34 import android.util.Log;
35 import android.util.Size;
36 import android.util.Slog;
37 import android.util.TypedValue;
38 import android.view.DisplayInfo;
39 import android.view.Gravity;
40 import android.view.IPinnedStackController;
41 import android.view.IPinnedStackListener;
42 
43 import com.android.internal.policy.PipSnapAlgorithm;
44 import com.android.server.UiThread;
45 
46 import java.io.PrintWriter;
47 import java.util.ArrayList;
48 import java.util.List;
49 
50 /**
51  * Holds the common state of the pinned stack between the system and SystemUI. If SystemUI ever
52  * needs to be restarted, it will be notified with the last known state.
53  *
54  * Changes to the pinned stack also flow through this controller, and generally, the system only
55  * changes the pinned stack bounds through this controller in two ways:
56  *
57  * 1) When first entering PiP: the controller returns the valid bounds given, taking aspect ratio
58  *    and IME state into account.
59  * 2) When rotating the device: the controller calculates the new bounds in the new orientation,
60  *    taking the minimized and IME state into account. In this case, we currently ignore the
61  *    SystemUI adjustments (ie. expanded for menu, interaction, etc).
62  *
63  * Other changes in the system, including adjustment of IME, configuration change, and more are
64  * handled by SystemUI (similar to the docked stack divider).
65  */
66 class PinnedStackController {
67 
68     private static final String TAG = TAG_WITH_CLASS_NAME ? "PinnedStackController" : TAG_WM;
69 
70     private final WindowManagerService mService;
71     private final DisplayContent mDisplayContent;
72     private final Handler mHandler = UiThread.getHandler();
73 
74     private IPinnedStackListener mPinnedStackListener;
75     private final PinnedStackListenerDeathHandler mPinnedStackListenerDeathHandler =
76             new PinnedStackListenerDeathHandler();
77 
78     private final PinnedStackControllerCallback mCallbacks = new PinnedStackControllerCallback();
79     private final PipSnapAlgorithm mSnapAlgorithm;
80 
81     // States that affect how the PIP can be manipulated
82     private boolean mIsMinimized;
83     private boolean mIsImeShowing;
84     private int mImeHeight;
85 
86     // The set of actions and aspect-ratio for the that are currently allowed on the PiP activity
87     private ArrayList<RemoteAction> mActions = new ArrayList<>();
88     private float mAspectRatio = -1f;
89 
90     // Used to calculate stack bounds across rotations
91     private final DisplayInfo mDisplayInfo = new DisplayInfo();
92     private final Rect mStableInsets = new Rect();
93 
94     // The size and position information that describes where the pinned stack will go by default.
95     private int mDefaultMinSize;
96     private int mDefaultStackGravity;
97     private float mDefaultAspectRatio;
98     private Point mScreenEdgeInsets;
99     private int mCurrentMinSize;
100 
101     // The aspect ratio bounds of the PIP.
102     private float mMinAspectRatio;
103     private float mMaxAspectRatio;
104 
105     // Temp vars for calculation
106     private final DisplayMetrics mTmpMetrics = new DisplayMetrics();
107     private final Rect mTmpInsets = new Rect();
108     private final Rect mTmpRect = new Rect();
109     private final Rect mTmpAnimatingBoundsRect = new Rect();
110     private final Point mTmpDisplaySize = new Point();
111 
112     /**
113      * The callback object passed to listeners for them to notify the controller of state changes.
114      */
115     private class PinnedStackControllerCallback extends IPinnedStackController.Stub {
116 
117         @Override
setIsMinimized(final boolean isMinimized)118         public void setIsMinimized(final boolean isMinimized) {
119             mHandler.post(() -> {
120                 mIsMinimized = isMinimized;
121                 mSnapAlgorithm.setMinimized(isMinimized);
122             });
123         }
124 
125         @Override
setMinEdgeSize(int minEdgeSize)126         public void setMinEdgeSize(int minEdgeSize) {
127             mHandler.post(() -> {
128                 mCurrentMinSize = Math.max(mDefaultMinSize, minEdgeSize);
129             });
130         }
131 
132         @Override
getDisplayRotation()133         public int getDisplayRotation() {
134             synchronized (mService.mWindowMap) {
135                 return mDisplayInfo.rotation;
136             }
137         }
138     }
139 
140     /**
141      * Handler for the case where the listener dies.
142      */
143     private class PinnedStackListenerDeathHandler implements IBinder.DeathRecipient {
144 
145         @Override
binderDied()146         public void binderDied() {
147             // Clean up the state if the listener dies
148             mPinnedStackListener = null;
149         }
150     }
151 
PinnedStackController(WindowManagerService service, DisplayContent displayContent)152     PinnedStackController(WindowManagerService service, DisplayContent displayContent) {
153         mService = service;
154         mDisplayContent = displayContent;
155         mSnapAlgorithm = new PipSnapAlgorithm(service.mContext);
156         mDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo());
157         reloadResources();
158         // Initialize the aspect ratio to the default aspect ratio.  Don't do this in reload
159         // resources as it would clobber mAspectRatio when entering PiP from fullscreen which
160         // triggers a configuration change and the resources to be reloaded.
161         mAspectRatio = mDefaultAspectRatio;
162     }
163 
onConfigurationChanged()164     void onConfigurationChanged() {
165         reloadResources();
166     }
167 
168     /**
169      * Reloads all the resources for the current configuration.
170      */
reloadResources()171     private void reloadResources() {
172         final Resources res = mService.mContext.getResources();
173         mDefaultMinSize = res.getDimensionPixelSize(
174                 com.android.internal.R.dimen.default_minimal_size_pip_resizable_task);
175         mCurrentMinSize = mDefaultMinSize;
176         mDefaultAspectRatio = res.getFloat(
177                 com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio);
178         final String screenEdgeInsetsDpString = res.getString(
179                 com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets);
180         final Size screenEdgeInsetsDp = !screenEdgeInsetsDpString.isEmpty()
181                 ? Size.parseSize(screenEdgeInsetsDpString)
182                 : null;
183         mDefaultStackGravity = res.getInteger(
184                 com.android.internal.R.integer.config_defaultPictureInPictureGravity);
185         mDisplayContent.getDisplay().getRealMetrics(mTmpMetrics);
186         mScreenEdgeInsets = screenEdgeInsetsDp == null ? new Point()
187                 : new Point(dpToPx(screenEdgeInsetsDp.getWidth(), mTmpMetrics),
188                         dpToPx(screenEdgeInsetsDp.getHeight(), mTmpMetrics));
189         mMinAspectRatio = res.getFloat(
190                 com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio);
191         mMaxAspectRatio = res.getFloat(
192                 com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio);
193     }
194 
195     /**
196      * Registers a pinned stack listener.
197      */
registerPinnedStackListener(IPinnedStackListener listener)198     void registerPinnedStackListener(IPinnedStackListener listener) {
199         try {
200             listener.asBinder().linkToDeath(mPinnedStackListenerDeathHandler, 0);
201             listener.onListenerRegistered(mCallbacks);
202             mPinnedStackListener = listener;
203             notifyImeVisibilityChanged(mIsImeShowing, mImeHeight);
204             // The movement bounds notification needs to be sent before the minimized state, since
205             // SystemUI may use the bounds to retore the minimized position
206             notifyMovementBoundsChanged(false /* fromImeAdjustment */);
207             notifyActionsChanged(mActions);
208             notifyMinimizeChanged(mIsMinimized);
209         } catch (RemoteException e) {
210             Log.e(TAG, "Failed to register pinned stack listener", e);
211         }
212     }
213 
214     /**
215      * @return whether the given {@param aspectRatio} is valid.
216      */
isValidPictureInPictureAspectRatio(float aspectRatio)217     public boolean isValidPictureInPictureAspectRatio(float aspectRatio) {
218         return Float.compare(mMinAspectRatio, aspectRatio) <= 0 &&
219                 Float.compare(aspectRatio, mMaxAspectRatio) <= 0;
220     }
221 
222     /**
223      * Returns the current bounds (or the default bounds if there are no current bounds) with the
224      * specified aspect ratio.
225      */
transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio, boolean useCurrentMinEdgeSize)226     Rect transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio,
227             boolean useCurrentMinEdgeSize) {
228         // Save the snap fraction, calculate the aspect ratio based on screen size
229         final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds,
230                 getMovementBounds(stackBounds));
231 
232         final int minEdgeSize = useCurrentMinEdgeSize ? mCurrentMinSize : mDefaultMinSize;
233         final Size size = mSnapAlgorithm.getSizeForAspectRatio(aspectRatio, minEdgeSize,
234                 mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
235         final int left = (int) (stackBounds.centerX() - size.getWidth() / 2f);
236         final int top = (int) (stackBounds.centerY() - size.getHeight() / 2f);
237         stackBounds.set(left, top, left + size.getWidth(), top + size.getHeight());
238         mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction);
239         if (mIsMinimized) {
240             applyMinimizedOffset(stackBounds, getMovementBounds(stackBounds));
241         }
242         return stackBounds;
243     }
244 
245     /**
246      * @return the default bounds to show the PIP when there is no active PIP.
247      */
getDefaultBounds()248     Rect getDefaultBounds() {
249         synchronized (mService.mWindowMap) {
250             final Rect insetBounds = new Rect();
251             getInsetBounds(insetBounds);
252 
253             final Rect defaultBounds = new Rect();
254             final Size size = mSnapAlgorithm.getSizeForAspectRatio(mDefaultAspectRatio,
255                     mDefaultMinSize, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
256             Gravity.apply(mDefaultStackGravity, size.getWidth(), size.getHeight(), insetBounds,
257                     0, mIsImeShowing ? mImeHeight : 0, defaultBounds);
258             return defaultBounds;
259         }
260     }
261 
262     /**
263      * In the case where the display rotation is changed but there is no stack, we can't depend on
264      * onTaskStackBoundsChanged() to be called.  But we still should update our known display info
265      * with the new state so that we can update SystemUI.
266      */
onDisplayInfoChanged()267     synchronized void onDisplayInfoChanged() {
268         mDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo());
269         notifyMovementBoundsChanged(false /* fromImeAdjustment */);
270     }
271 
272     /**
273      * Updates the display info, calculating and returning the new stack and movement bounds in the
274      * new orientation of the device if necessary.
275      */
onTaskStackBoundsChanged(Rect targetBounds, Rect outBounds)276     boolean onTaskStackBoundsChanged(Rect targetBounds, Rect outBounds) {
277         synchronized (mService.mWindowMap) {
278             final DisplayInfo displayInfo = mDisplayContent.getDisplayInfo();
279             if (mDisplayInfo.equals(displayInfo)) {
280                 // We are already in the right orientation, ignore
281                 outBounds.setEmpty();
282                 return false;
283             } else if (targetBounds.isEmpty()) {
284                 // The stack is null, we are just initializing the stack, so just store the display
285                 // info and ignore
286                 mDisplayInfo.copyFrom(displayInfo);
287                 outBounds.setEmpty();
288                 return false;
289             }
290 
291             mTmpRect.set(targetBounds);
292             final Rect postChangeStackBounds = mTmpRect;
293 
294             // Calculate the snap fraction of the current stack along the old movement bounds
295             final Rect preChangeMovementBounds = getMovementBounds(postChangeStackBounds);
296             final float snapFraction = mSnapAlgorithm.getSnapFraction(postChangeStackBounds,
297                     preChangeMovementBounds);
298             mDisplayInfo.copyFrom(displayInfo);
299 
300             // Calculate the stack bounds in the new orientation to the same same fraction along the
301             // rotated movement bounds.
302             final Rect postChangeMovementBounds = getMovementBounds(postChangeStackBounds,
303                     false /* adjustForIme */);
304             mSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds,
305                     snapFraction);
306             if (mIsMinimized) {
307                 applyMinimizedOffset(postChangeStackBounds, postChangeMovementBounds);
308             }
309 
310             notifyMovementBoundsChanged(false /* fromImeAdjustment */);
311 
312             outBounds.set(postChangeStackBounds);
313             return true;
314         }
315     }
316 
317     /**
318      * Sets the Ime state and height.
319      */
setAdjustedForIme(boolean adjustedForIme, int imeHeight)320     void setAdjustedForIme(boolean adjustedForIme, int imeHeight) {
321         // Return early if there is no state change
322         if (mIsImeShowing == adjustedForIme && mImeHeight == imeHeight) {
323             return;
324         }
325 
326         mIsImeShowing = adjustedForIme;
327         mImeHeight = imeHeight;
328         notifyImeVisibilityChanged(adjustedForIme, imeHeight);
329         notifyMovementBoundsChanged(true /* fromImeAdjustment */);
330     }
331 
332     /**
333      * Sets the current aspect ratio.
334      */
setAspectRatio(float aspectRatio)335     void setAspectRatio(float aspectRatio) {
336         if (Float.compare(mAspectRatio, aspectRatio) != 0) {
337             mAspectRatio = aspectRatio;
338             notifyMovementBoundsChanged(false /* fromImeAdjustment */);
339         }
340     }
341 
342     /**
343      * @return the current aspect ratio.
344      */
getAspectRatio()345     float getAspectRatio() {
346         return mAspectRatio;
347     }
348 
349     /**
350      * Sets the current set of actions.
351      */
setActions(List<RemoteAction> actions)352     void setActions(List<RemoteAction> actions) {
353         mActions.clear();
354         if (actions != null) {
355             mActions.addAll(actions);
356         }
357         notifyActionsChanged(mActions);
358     }
359 
360     /**
361      * Notifies listeners that the PIP needs to be adjusted for the IME.
362      */
notifyImeVisibilityChanged(boolean imeVisible, int imeHeight)363     private void notifyImeVisibilityChanged(boolean imeVisible, int imeHeight) {
364         if (mPinnedStackListener != null) {
365             try {
366                 mPinnedStackListener.onImeVisibilityChanged(imeVisible, imeHeight);
367             } catch (RemoteException e) {
368                 Slog.e(TAG_WM, "Error delivering bounds changed event.", e);
369             }
370         }
371     }
372 
373     /**
374      * Notifies listeners that the PIP minimized state has changed.
375      */
notifyMinimizeChanged(boolean isMinimized)376     private void notifyMinimizeChanged(boolean isMinimized) {
377         if (mPinnedStackListener != null) {
378             try {
379                 mPinnedStackListener.onMinimizedStateChanged(isMinimized);
380             } catch (RemoteException e) {
381                 Slog.e(TAG_WM, "Error delivering minimize changed event.", e);
382             }
383         }
384     }
385 
386     /**
387      * Notifies listeners that the PIP actions have changed.
388      */
notifyActionsChanged(List<RemoteAction> actions)389     private void notifyActionsChanged(List<RemoteAction> actions) {
390         if (mPinnedStackListener != null) {
391             try {
392                 mPinnedStackListener.onActionsChanged(new ParceledListSlice(actions));
393             } catch (RemoteException e) {
394                 Slog.e(TAG_WM, "Error delivering actions changed event.", e);
395             }
396         }
397     }
398 
399     /**
400      * Notifies listeners that the PIP movement bounds have changed.
401      */
notifyMovementBoundsChanged(boolean fromImeAdjustement)402     private void notifyMovementBoundsChanged(boolean fromImeAdjustement) {
403         synchronized (mService.mWindowMap) {
404             if (mPinnedStackListener != null) {
405                 try {
406                     final Rect insetBounds = new Rect();
407                     getInsetBounds(insetBounds);
408                     final Rect normalBounds = getDefaultBounds();
409                     if (isValidPictureInPictureAspectRatio(mAspectRatio)) {
410                         transformBoundsToAspectRatio(normalBounds, mAspectRatio,
411                                 false /* useCurrentMinEdgeSize */);
412                     }
413                     final Rect animatingBounds = mTmpAnimatingBoundsRect;
414                     final TaskStack pinnedStack = mDisplayContent.getStackById(PINNED_STACK_ID);
415                     if (pinnedStack != null) {
416                         pinnedStack.getAnimationOrCurrentBounds(animatingBounds);
417                     } else {
418                         animatingBounds.set(normalBounds);
419                     }
420                     mPinnedStackListener.onMovementBoundsChanged(insetBounds, normalBounds,
421                             animatingBounds, fromImeAdjustement, mDisplayInfo.rotation);
422                 } catch (RemoteException e) {
423                     Slog.e(TAG_WM, "Error delivering actions changed event.", e);
424                 }
425             }
426         }
427     }
428 
429     /**
430      * @return the bounds on the screen that the PIP can be visible in.
431      */
getInsetBounds(Rect outRect)432     private void getInsetBounds(Rect outRect) {
433         synchronized (mService.mWindowMap) {
434             mService.mPolicy.getStableInsetsLw(mDisplayInfo.rotation, mDisplayInfo.logicalWidth,
435                     mDisplayInfo.logicalHeight, mTmpInsets);
436             outRect.set(mTmpInsets.left + mScreenEdgeInsets.x, mTmpInsets.top + mScreenEdgeInsets.y,
437                     mDisplayInfo.logicalWidth - mTmpInsets.right - mScreenEdgeInsets.x,
438                     mDisplayInfo.logicalHeight - mTmpInsets.bottom - mScreenEdgeInsets.y);
439         }
440     }
441 
442     /**
443      * @return the movement bounds for the given {@param stackBounds} and the current state of the
444      *         controller.
445      */
getMovementBounds(Rect stackBounds)446     private Rect getMovementBounds(Rect stackBounds) {
447         synchronized (mService.mWindowMap) {
448             return getMovementBounds(stackBounds, true /* adjustForIme */);
449         }
450     }
451 
452     /**
453      * @return the movement bounds for the given {@param stackBounds} and the current state of the
454      *         controller.
455      */
getMovementBounds(Rect stackBounds, boolean adjustForIme)456     private Rect getMovementBounds(Rect stackBounds, boolean adjustForIme) {
457         synchronized (mService.mWindowMap) {
458             final Rect movementBounds = new Rect();
459             getInsetBounds(movementBounds);
460 
461             // Apply the movement bounds adjustments based on the current state
462             mSnapAlgorithm.getMovementBounds(stackBounds, movementBounds, movementBounds,
463                     (adjustForIme && mIsImeShowing) ? mImeHeight : 0);
464             return movementBounds;
465         }
466     }
467 
468     /**
469      * Applies the minimized offsets to the given stack bounds.
470      */
applyMinimizedOffset(Rect stackBounds, Rect movementBounds)471     private void applyMinimizedOffset(Rect stackBounds, Rect movementBounds) {
472         synchronized (mService.mWindowMap) {
473             mTmpDisplaySize.set(mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
474             mService.getStableInsetsLocked(mDisplayContent.getDisplayId(), mStableInsets);
475             mSnapAlgorithm.applyMinimizedOffset(stackBounds, movementBounds, mTmpDisplaySize,
476                     mStableInsets);
477         }
478     }
479 
480     /**
481      * @return the pixels for a given dp value.
482      */
dpToPx(float dpValue, DisplayMetrics dm)483     private int dpToPx(float dpValue, DisplayMetrics dm) {
484         return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm);
485     }
486 
dump(String prefix, PrintWriter pw)487     void dump(String prefix, PrintWriter pw) {
488         pw.println(prefix + "PinnedStackController");
489         pw.print(prefix + "  defaultBounds="); getDefaultBounds().printShortString(pw);
490         pw.println();
491         mService.getStackBounds(PINNED_STACK_ID, mTmpRect);
492         pw.print(prefix + "  movementBounds="); getMovementBounds(mTmpRect).printShortString(pw);
493         pw.println();
494         pw.println(prefix + "  mIsImeShowing=" + mIsImeShowing);
495         pw.println(prefix + "  mIsMinimized=" + mIsMinimized);
496         if (mActions.isEmpty()) {
497             pw.println(prefix + "  mActions=[]");
498         } else {
499             pw.println(prefix + "  mActions=[");
500             for (int i = 0; i < mActions.size(); i++) {
501                 RemoteAction action = mActions.get(i);
502                 pw.print(prefix + "    Action[" + i + "]: ");
503                 action.dump("", pw);
504             }
505             pw.println(prefix + "  ]");
506         }
507     }
508 }
509