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