PinnedStackController.java revision a71febe2aaa2796cde538aa21c3e2ff006e7d3f3
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.Rect;
30import android.os.Handler;
31import android.os.IBinder;
32import android.os.RemoteException;
33import android.util.DisplayMetrics;
34import android.util.Log;
35import android.util.Size;
36import android.util.Slog;
37import android.util.TypedValue;
38import android.view.DisplayInfo;
39import android.view.Gravity;
40import android.view.IPinnedStackController;
41import android.view.IPinnedStackListener;
42
43import com.android.internal.policy.PipSnapAlgorithm;
44import com.android.server.UiThread;
45
46import java.io.PrintWriter;
47import java.util.ArrayList;
48import 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 */
66class 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
118        public void setIsMinimized(final boolean isMinimized) {
119            mHandler.post(() -> {
120                mIsMinimized = isMinimized;
121                mSnapAlgorithm.setMinimized(isMinimized);
122            });
123        }
124
125        @Override
126        public void setMinEdgeSize(int minEdgeSize) {
127            mHandler.post(() -> {
128                mCurrentMinSize = Math.max(mDefaultMinSize, minEdgeSize);
129            });
130        }
131
132        @Override
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
146        public void binderDied() {
147            // Clean up the state if the listener dies
148            mPinnedStackListener = null;
149        }
150    }
151
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    }
159
160    void onConfigurationChanged() {
161        reloadResources();
162    }
163
164    /**
165     * Reloads all the resources for the current configuration.
166     */
167    private void reloadResources() {
168        final Resources res = mService.mContext.getResources();
169        mDefaultMinSize = res.getDimensionPixelSize(
170                com.android.internal.R.dimen.default_minimal_size_pip_resizable_task);
171        mCurrentMinSize = mDefaultMinSize;
172        mDefaultAspectRatio = res.getFloat(
173                com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio);
174        mAspectRatio = mDefaultAspectRatio;
175        final String screenEdgeInsetsDpString = res.getString(
176                com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets);
177        final Size screenEdgeInsetsDp = !screenEdgeInsetsDpString.isEmpty()
178                ? Size.parseSize(screenEdgeInsetsDpString)
179                : null;
180        mDefaultStackGravity = res.getInteger(
181                com.android.internal.R.integer.config_defaultPictureInPictureGravity);
182        mDisplayContent.getDisplay().getRealMetrics(mTmpMetrics);
183        mScreenEdgeInsets = screenEdgeInsetsDp == null ? new Point()
184                : new Point(dpToPx(screenEdgeInsetsDp.getWidth(), mTmpMetrics),
185                        dpToPx(screenEdgeInsetsDp.getHeight(), mTmpMetrics));
186        mMinAspectRatio = res.getFloat(
187                com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio);
188        mMaxAspectRatio = res.getFloat(
189                com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio);
190    }
191
192    /**
193     * Registers a pinned stack listener.
194     */
195    void registerPinnedStackListener(IPinnedStackListener listener) {
196        try {
197            listener.asBinder().linkToDeath(mPinnedStackListenerDeathHandler, 0);
198            listener.onListenerRegistered(mCallbacks);
199            mPinnedStackListener = listener;
200            notifyImeVisibilityChanged(mIsImeShowing, mImeHeight);
201            // The movement bounds notification needs to be sent before the minimized state, since
202            // SystemUI may use the bounds to retore the minimized position
203            notifyMovementBoundsChanged(false /* fromImeAdjustment */);
204            notifyActionsChanged(mActions);
205            notifyMinimizeChanged(mIsMinimized);
206        } catch (RemoteException e) {
207            Log.e(TAG, "Failed to register pinned stack listener", e);
208        }
209    }
210
211    /**
212     * @return whether the given {@param aspectRatio} is valid.
213     */
214    public boolean isValidPictureInPictureAspectRatio(float aspectRatio) {
215        return Float.compare(mMinAspectRatio, aspectRatio) <= 0 &&
216                Float.compare(aspectRatio, mMaxAspectRatio) <= 0;
217    }
218
219    /**
220     * Returns the current bounds (or the default bounds if there are no current bounds) with the
221     * specified aspect ratio.
222     */
223    Rect transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio,
224            boolean useCurrentMinEdgeSize) {
225        // Save the snap fraction, calculate the aspect ratio based on screen size
226        final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds,
227                getMovementBounds(stackBounds));
228
229        final int minEdgeSize = useCurrentMinEdgeSize ? mCurrentMinSize : mDefaultMinSize;
230        final Size size = mSnapAlgorithm.getSizeForAspectRatio(aspectRatio, minEdgeSize,
231                mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
232        final int left = (int) (stackBounds.centerX() - size.getWidth() / 2f);
233        final int top = (int) (stackBounds.centerY() - size.getHeight() / 2f);
234        stackBounds.set(left, top, left + size.getWidth(), top + size.getHeight());
235        mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction);
236        if (mIsMinimized) {
237            applyMinimizedOffset(stackBounds, getMovementBounds(stackBounds));
238        }
239        return stackBounds;
240    }
241
242    /**
243     * @return the default bounds to show the PIP when there is no active PIP.
244     */
245    Rect getDefaultBounds() {
246        synchronized (mService.mWindowMap) {
247            final Rect insetBounds = new Rect();
248            getInsetBounds(insetBounds);
249
250            final Rect defaultBounds = new Rect();
251            final Size size = mSnapAlgorithm.getSizeForAspectRatio(mDefaultAspectRatio,
252                    mDefaultMinSize, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
253            Gravity.apply(mDefaultStackGravity, size.getWidth(), size.getHeight(), insetBounds,
254                    0, mIsImeShowing ? mImeHeight : 0, defaultBounds);
255            return defaultBounds;
256        }
257    }
258
259    /**
260     * In the case where the display rotation is changed but there is no stack, we can't depend on
261     * onTaskStackBoundsChanged() to be called.  But we still should update our known display info
262     * with the new state so that we can update SystemUI.
263     */
264    synchronized void onDisplayInfoChanged() {
265        mDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo());
266        notifyMovementBoundsChanged(false /* fromImeAdjustment */);
267    }
268
269    /**
270     * Updates the display info, calculating and returning the new stack and movement bounds in the
271     * new orientation of the device if necessary.
272     */
273    boolean onTaskStackBoundsChanged(Rect targetBounds, Rect outBounds) {
274        synchronized (mService.mWindowMap) {
275            final DisplayInfo displayInfo = mDisplayContent.getDisplayInfo();
276            if (mDisplayInfo.equals(displayInfo)) {
277                // We are already in the right orientation, ignore
278                outBounds.setEmpty();
279                return false;
280            } else if (targetBounds.isEmpty()) {
281                // The stack is null, we are just initializing the stack, so just store the display
282                // info and ignore
283                mDisplayInfo.copyFrom(displayInfo);
284                outBounds.setEmpty();
285                return false;
286            }
287
288            mTmpRect.set(targetBounds);
289            final Rect postChangeStackBounds = mTmpRect;
290
291            // Calculate the snap fraction of the current stack along the old movement bounds
292            final Rect preChangeMovementBounds = getMovementBounds(postChangeStackBounds);
293            final float snapFraction = mSnapAlgorithm.getSnapFraction(postChangeStackBounds,
294                    preChangeMovementBounds);
295            mDisplayInfo.copyFrom(displayInfo);
296
297            // Calculate the stack bounds in the new orientation to the same same fraction along the
298            // rotated movement bounds.
299            final Rect postChangeMovementBounds = getMovementBounds(postChangeStackBounds,
300                    false /* adjustForIme */);
301            mSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds,
302                    snapFraction);
303            if (mIsMinimized) {
304                applyMinimizedOffset(postChangeStackBounds, postChangeMovementBounds);
305            }
306
307            notifyMovementBoundsChanged(false /* fromImeAdjustment */);
308
309            outBounds.set(postChangeStackBounds);
310            return true;
311        }
312    }
313
314    /**
315     * Sets the Ime state and height.
316     */
317    void setAdjustedForIme(boolean adjustedForIme, int imeHeight) {
318        // Return early if there is no state change
319        if (mIsImeShowing == adjustedForIme && mImeHeight == imeHeight) {
320            return;
321        }
322
323        mIsImeShowing = adjustedForIme;
324        mImeHeight = imeHeight;
325        notifyImeVisibilityChanged(adjustedForIme, imeHeight);
326        notifyMovementBoundsChanged(true /* fromImeAdjustment */);
327    }
328
329    /**
330     * Sets the current aspect ratio.
331     */
332    void setAspectRatio(float aspectRatio) {
333        if (Float.compare(mAspectRatio, aspectRatio) != 0) {
334            mAspectRatio = aspectRatio;
335            notifyMovementBoundsChanged(false /* fromImeAdjustment */);
336        }
337    }
338
339    /**
340     * @return the current aspect ratio.
341     */
342    float getAspectRatio() {
343        return mAspectRatio;
344    }
345
346    /**
347     * Sets the current set of actions.
348     */
349    void setActions(List<RemoteAction> actions) {
350        mActions.clear();
351        if (actions != null) {
352            mActions.addAll(actions);
353        }
354        notifyActionsChanged(mActions);
355    }
356
357    /**
358     * Notifies listeners that the PIP needs to be adjusted for the IME.
359     */
360    private void notifyImeVisibilityChanged(boolean imeVisible, int imeHeight) {
361        if (mPinnedStackListener != null) {
362            try {
363                mPinnedStackListener.onImeVisibilityChanged(imeVisible, imeHeight);
364            } catch (RemoteException e) {
365                Slog.e(TAG_WM, "Error delivering bounds changed event.", e);
366            }
367        }
368    }
369
370    /**
371     * Notifies listeners that the PIP minimized state has changed.
372     */
373    private void notifyMinimizeChanged(boolean isMinimized) {
374        if (mPinnedStackListener != null) {
375            try {
376                mPinnedStackListener.onMinimizedStateChanged(isMinimized);
377            } catch (RemoteException e) {
378                Slog.e(TAG_WM, "Error delivering minimize changed event.", e);
379            }
380        }
381    }
382
383    /**
384     * Notifies listeners that the PIP actions have changed.
385     */
386    private void notifyActionsChanged(List<RemoteAction> actions) {
387        if (mPinnedStackListener != null) {
388            try {
389                mPinnedStackListener.onActionsChanged(new ParceledListSlice(actions));
390            } catch (RemoteException e) {
391                Slog.e(TAG_WM, "Error delivering actions changed event.", e);
392            }
393        }
394    }
395
396    /**
397     * Notifies listeners that the PIP movement bounds have changed.
398     */
399    private void notifyMovementBoundsChanged(boolean fromImeAdjustement) {
400        synchronized (mService.mWindowMap) {
401            if (mPinnedStackListener != null) {
402                try {
403                    final Rect insetBounds = new Rect();
404                    getInsetBounds(insetBounds);
405                    final Rect normalBounds = getDefaultBounds();
406                    if (isValidPictureInPictureAspectRatio(mAspectRatio)) {
407                        transformBoundsToAspectRatio(normalBounds, mAspectRatio,
408                                false /* useCurrentMinEdgeSize */);
409                    }
410                    final Rect animatingBounds = mTmpAnimatingBoundsRect;
411                    final TaskStack pinnedStack = mDisplayContent.getStackById(PINNED_STACK_ID);
412                    if (pinnedStack != null) {
413                        pinnedStack.getAnimationOrCurrentBounds(animatingBounds);
414                    } else {
415                        animatingBounds.set(normalBounds);
416                    }
417                    mPinnedStackListener.onMovementBoundsChanged(insetBounds, normalBounds,
418                            animatingBounds, fromImeAdjustement, mDisplayInfo.rotation);
419                } catch (RemoteException e) {
420                    Slog.e(TAG_WM, "Error delivering actions changed event.", e);
421                }
422            }
423        }
424    }
425
426    /**
427     * @return the bounds on the screen that the PIP can be visible in.
428     */
429    private void getInsetBounds(Rect outRect) {
430        synchronized (mService.mWindowMap) {
431            mService.mPolicy.getStableInsetsLw(mDisplayInfo.rotation, mDisplayInfo.logicalWidth,
432                    mDisplayInfo.logicalHeight, mTmpInsets);
433            outRect.set(mTmpInsets.left + mScreenEdgeInsets.x, mTmpInsets.top + mScreenEdgeInsets.y,
434                    mDisplayInfo.logicalWidth - mTmpInsets.right - mScreenEdgeInsets.x,
435                    mDisplayInfo.logicalHeight - mTmpInsets.bottom - mScreenEdgeInsets.y);
436        }
437    }
438
439    /**
440     * @return the movement bounds for the given {@param stackBounds} and the current state of the
441     *         controller.
442     */
443    private Rect getMovementBounds(Rect stackBounds) {
444        synchronized (mService.mWindowMap) {
445            return getMovementBounds(stackBounds, true /* adjustForIme */);
446        }
447    }
448
449    /**
450     * @return the movement bounds for the given {@param stackBounds} and the current state of the
451     *         controller.
452     */
453    private Rect getMovementBounds(Rect stackBounds, boolean adjustForIme) {
454        synchronized (mService.mWindowMap) {
455            final Rect movementBounds = new Rect();
456            getInsetBounds(movementBounds);
457
458            // Apply the movement bounds adjustments based on the current state
459            mSnapAlgorithm.getMovementBounds(stackBounds, movementBounds, movementBounds,
460                    (adjustForIme && mIsImeShowing) ? mImeHeight : 0);
461            return movementBounds;
462        }
463    }
464
465    /**
466     * Applies the minimized offsets to the given stack bounds.
467     */
468    private void applyMinimizedOffset(Rect stackBounds, Rect movementBounds) {
469        synchronized (mService.mWindowMap) {
470            mTmpDisplaySize.set(mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
471            mService.getStableInsetsLocked(mDisplayContent.getDisplayId(), mStableInsets);
472            mSnapAlgorithm.applyMinimizedOffset(stackBounds, movementBounds, mTmpDisplaySize,
473                    mStableInsets);
474        }
475    }
476
477    /**
478     * @return the pixels for a given dp value.
479     */
480    private int dpToPx(float dpValue, DisplayMetrics dm) {
481        return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm);
482    }
483
484    void dump(String prefix, PrintWriter pw) {
485        pw.println(prefix + "PinnedStackController");
486        pw.print(prefix + "  defaultBounds="); getDefaultBounds().printShortString(pw);
487        pw.println();
488        mService.getStackBounds(PINNED_STACK_ID, mTmpRect);
489        pw.print(prefix + "  movementBounds="); getMovementBounds(mTmpRect).printShortString(pw);
490        pw.println();
491        pw.println(prefix + "  mIsImeShowing=" + mIsImeShowing);
492        pw.println(prefix + "  mIsMinimized=" + mIsMinimized);
493        if (mActions.isEmpty()) {
494            pw.println(prefix + "  mActions=[]");
495        } else {
496            pw.println(prefix + "  mActions=[");
497            for (int i = 0; i < mActions.size(); i++) {
498                RemoteAction action = mActions.get(i);
499                pw.print(prefix + "    Action[" + i + "]: ");
500                action.dump("", pw);
501            }
502            pw.println(prefix + "  ]");
503        }
504    }
505}
506