PinnedStackController.java revision 655332c641ccb12ee1ae3ce89135ca847fba1abf
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.animation.ValueAnimator;
26import android.content.res.Resources;
27import android.graphics.Point;
28import android.graphics.Rect;
29import android.os.Handler;
30import android.os.IBinder;
31import android.os.RemoteException;
32import android.util.DisplayMetrics;
33import android.util.Log;
34import android.util.Size;
35import android.util.Slog;
36import android.util.TypedValue;
37import android.view.Display;
38import android.view.Gravity;
39import android.view.IPinnedStackController;
40import android.view.IPinnedStackListener;
41
42import com.android.internal.os.BackgroundThread;
43import com.android.internal.policy.PipMotionHelper;
44import com.android.internal.policy.PipSnapAlgorithm;
45
46import java.io.PrintWriter;
47
48/**
49 * Holds the common state of the pinned stack between the system and SystemUI.
50 */
51class PinnedStackController {
52
53    private static final String TAG = TAG_WITH_CLASS_NAME ? "PinnedStackController" : TAG_WM;
54
55    private final WindowManagerService mService;
56    private final DisplayContent mDisplayContent;
57    private final Handler mHandler = new Handler();
58
59    private IPinnedStackListener mPinnedStackListener;
60    private final PinnedStackListenerDeathHandler mPinnedStackListenerDeathHandler =
61            new PinnedStackListenerDeathHandler();
62
63    private final PinnedStackControllerCallback mCallbacks = new PinnedStackControllerCallback();
64    private final PipSnapAlgorithm mSnapAlgorithm;
65    private final PipMotionHelper mMotionHelper;
66
67    // States that affect how the PIP can be manipulated
68    private boolean mInInteractiveMode;
69    private boolean mIsImeShowing;
70    private int mImeHeight;
71    private final Rect mPreImeShowingBounds = new Rect();
72    private ValueAnimator mBoundsAnimator = null;
73
74    // The size and position information that describes where the pinned stack will go by default.
75    private int mDefaultStackGravity;
76    private Size mDefaultStackSize;
77    private Point mScreenEdgeInsets;
78
79    // Temp vars for calculation
80    private final DisplayMetrics mTmpMetrics = new DisplayMetrics();
81    private final Rect mTmpInsets = new Rect();
82
83    /**
84     * The callback object passed to listeners for them to notify the controller of state changes.
85     */
86    private class PinnedStackControllerCallback extends IPinnedStackController.Stub {
87
88        @Override
89        public void setInInteractiveMode(final boolean inInteractiveMode) {
90            mHandler.post(() -> {
91                // Cancel any existing animations on the PIP once the user starts dragging it
92                if (mBoundsAnimator != null && inInteractiveMode) {
93                    mBoundsAnimator.cancel();
94                }
95                mInInteractiveMode = inInteractiveMode;
96                mPreImeShowingBounds.setEmpty();
97            });
98        }
99    }
100
101    /**
102     * Handler for the case where the listener dies.
103     */
104    private class PinnedStackListenerDeathHandler implements IBinder.DeathRecipient {
105
106        @Override
107        public void binderDied() {
108            // Clean up the state if the listener dies
109            mInInteractiveMode = false;
110            mPinnedStackListener = null;
111        }
112    }
113
114    PinnedStackController(WindowManagerService service, DisplayContent displayContent) {
115        mService = service;
116        mDisplayContent = displayContent;
117        mSnapAlgorithm = new PipSnapAlgorithm(service.mContext);
118        mMotionHelper = new PipMotionHelper(BackgroundThread.getHandler());
119        reloadResources();
120    }
121
122    void onConfigurationChanged() {
123        reloadResources();
124    }
125
126    /**
127     * Reloads all the resources for the current configuration.
128     */
129    void reloadResources() {
130        final Resources res = mService.mContext.getResources();
131        final Size defaultSizeDp = Size.parseSize(res.getString(
132                com.android.internal.R.string.config_defaultPictureInPictureSize));
133        final Size screenEdgeInsetsDp = Size.parseSize(res.getString(
134                com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets));
135        mDefaultStackGravity = res.getInteger(
136                com.android.internal.R.integer.config_defaultPictureInPictureGravity);
137        mDisplayContent.getDisplay().getRealMetrics(mTmpMetrics);
138        mDefaultStackSize = new Size(dpToPx(defaultSizeDp.getWidth(), mTmpMetrics),
139                dpToPx(defaultSizeDp.getHeight(), mTmpMetrics));
140        mScreenEdgeInsets = new Point(dpToPx(screenEdgeInsetsDp.getWidth(), mTmpMetrics),
141                dpToPx(screenEdgeInsetsDp.getHeight(), mTmpMetrics));
142    }
143
144    /**
145     * Registers a pinned stack listener.
146     */
147    void registerPinnedStackListener(IPinnedStackListener listener) {
148        try {
149            listener.asBinder().linkToDeath(mPinnedStackListenerDeathHandler, 0);
150            listener.onListenerRegistered(mCallbacks);
151            mPinnedStackListener = listener;
152            notifyBoundsChanged(mIsImeShowing);
153        } catch (RemoteException e) {
154            Log.e(TAG, "Failed to register pinned stack listener", e);
155        }
156    }
157
158    /**
159     * @return the default bounds to show the PIP when there is no active PIP.
160     */
161    Rect getDefaultBounds() {
162        final Display display = mDisplayContent.getDisplay();
163        final Rect insetBounds = new Rect();
164        final Point displaySize = new Point();
165        display.getRealSize(displaySize);
166        mService.getStableInsetsLocked(mDisplayContent.getDisplayId(), mTmpInsets);
167        getInsetBounds(displaySize, mTmpInsets, insetBounds);
168
169        final Rect defaultBounds = new Rect();
170        Gravity.apply(mDefaultStackGravity, mDefaultStackSize.getWidth(),
171                mDefaultStackSize.getHeight(), insetBounds, 0, 0, defaultBounds);
172        return defaultBounds;
173    }
174
175    /**
176     * @return the movement bounds for the given {@param stackBounds} and the current state of the
177     *         controller.
178     */
179    Rect getMovementBounds(Rect stackBounds) {
180        final Display display = mDisplayContent.getDisplay();
181        final Rect movementBounds = new Rect();
182        final Point displaySize = new Point();
183        display.getRealSize(displaySize);
184        mService.getStableInsetsLocked(mDisplayContent.getDisplayId(), mTmpInsets);
185        getInsetBounds(displaySize, mTmpInsets, movementBounds);
186
187        // Adjust the right/bottom to ensure the stack bounds never goes offscreen
188        movementBounds.right = Math.max(movementBounds.left, movementBounds.right -
189                stackBounds.width());
190        movementBounds.bottom = Math.max(movementBounds.top, movementBounds.bottom -
191                stackBounds.height());
192
193        // Adjust the top if the ime is open
194        if (mIsImeShowing) {
195            movementBounds.bottom -= mImeHeight;
196        }
197
198        return movementBounds;
199    }
200
201    /**
202     * @return the PIP bounds given it's bounds pre-rotation, and post-rotation (with as applied
203     * by the display content, which currently transposes the dimensions but keeps each stack in
204     * the same physical space on the device).
205     */
206    Rect getPostRotationBounds(Rect preRotationStackBounds, Rect postRotationStackBounds) {
207        // Keep the pinned stack in the same aspect ratio as in the old orientation, but
208        // move it into the position in the rotated space, and snap to the closest space
209        // in the new orientation.
210        final Rect movementBounds = getMovementBounds(preRotationStackBounds);
211        final int stackWidth = preRotationStackBounds.width();
212        final int stackHeight = preRotationStackBounds.height();
213        final int left = postRotationStackBounds.centerX() - (stackWidth / 2);
214        final int top = postRotationStackBounds.centerY() - (stackHeight / 2);
215        final Rect postRotBounds = new Rect(left, top, left + stackWidth, top + stackHeight);
216        return mSnapAlgorithm.findClosestSnapBounds(movementBounds, postRotBounds);
217    }
218
219    /**
220     * Sets the Ime state and height.
221     */
222    void setAdjustedForIme(boolean adjustedForIme, int imeHeight) {
223        // Return early if there is no state change
224        if (mIsImeShowing == adjustedForIme && mImeHeight == imeHeight) {
225            return;
226        }
227
228        final Rect stackBounds = new Rect();
229        mService.getStackBounds(PINNED_STACK_ID, stackBounds);
230        final Rect prevMovementBounds = getMovementBounds(stackBounds);
231        final boolean wasAdjustedForIme = mIsImeShowing;
232        mIsImeShowing = adjustedForIme;
233        mImeHeight = imeHeight;
234        if (mInInteractiveMode) {
235            // If the user is currently interacting with the PIP and the ime state changes, then
236            // don't adjust the bounds and defer that to after the interaction
237            notifyBoundsChanged(adjustedForIme /* adjustedForIme */);
238        } else {
239            // Otherwise, we can move the PIP to a sane location to ensure that it does not block
240            // the user from interacting with the IME
241            Rect toBounds;
242            if (!wasAdjustedForIme && adjustedForIme) {
243                // If we are showing the IME, then store the previous bounds
244                mPreImeShowingBounds.set(stackBounds);
245                toBounds = adjustBoundsInMovementBounds(stackBounds);
246            } else if (wasAdjustedForIme && !adjustedForIme) {
247                if (!mPreImeShowingBounds.isEmpty()) {
248                    // If we are hiding the IME and the user is not interacting with the PIP, restore
249                    // the previous bounds
250                    toBounds = mPreImeShowingBounds;
251                } else {
252                    if (stackBounds.top == prevMovementBounds.bottom) {
253                        // If the PIP is resting on top of the IME, then adjust it with the hiding
254                        // of the IME
255                        final Rect movementBounds = getMovementBounds(stackBounds);
256                        toBounds = new Rect(stackBounds);
257                        toBounds.offsetTo(toBounds.left, movementBounds.bottom);
258                    } else {
259                        // Otherwise, leave the PIP in place
260                        toBounds = stackBounds;
261                    }
262                }
263            } else {
264                // Otherwise, the IME bounds have changed so we need to adjust the PIP bounds also
265                toBounds = adjustBoundsInMovementBounds(stackBounds);
266            }
267            if (!toBounds.equals(stackBounds)) {
268                if (mBoundsAnimator != null) {
269                    mBoundsAnimator.cancel();
270                }
271                mBoundsAnimator = mMotionHelper.createAnimationToBounds(stackBounds, toBounds);
272                mBoundsAnimator.start();
273            }
274        }
275    }
276
277    /**
278     * @return the adjusted {@param stackBounds} such that they are in the movement bounds.
279     */
280    private Rect adjustBoundsInMovementBounds(Rect stackBounds) {
281        final Rect movementBounds = getMovementBounds(stackBounds);
282        final Rect adjustedBounds = new Rect(stackBounds);
283        adjustedBounds.offset(0, Math.min(0, movementBounds.bottom - stackBounds.top));
284        return adjustedBounds;
285    }
286
287    /**
288     * Sends a broadcast that the PIP movement bounds have changed.
289     */
290    private void notifyBoundsChanged(boolean adjustedForIme) {
291        if (mPinnedStackListener != null) {
292            try {
293                mPinnedStackListener.onBoundsChanged(adjustedForIme);
294            } catch (RemoteException e) {
295                Slog.e(TAG_WM, "Error delivering bounds changed event.", e);
296            }
297        }
298    }
299
300    /**
301     * @return the bounds on the screen that the PIP can be visible in.
302     */
303    private void getInsetBounds(Point displaySize, Rect insets, Rect outRect) {
304        outRect.set(insets.left + mScreenEdgeInsets.x, insets.top + mScreenEdgeInsets.y,
305                displaySize.x - insets.right - mScreenEdgeInsets.x,
306                displaySize.y - insets.bottom - mScreenEdgeInsets.y);
307    }
308
309    /**
310     * @return the pixels for a given dp value.
311     */
312    private int dpToPx(float dpValue, DisplayMetrics dm) {
313        return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm);
314    }
315
316    void dump(String prefix, PrintWriter pw) {
317        pw.println(prefix + "PinnedStackController");
318        pw.println(prefix + "  mIsImeShowing=" + mIsImeShowing);
319        pw.println(prefix + "  mInInteractiveMode=" + mInInteractiveMode);
320    }
321}
322