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