PinnedStackController.java revision 7075d79cabb3c23540bdcbd07b772705a14e932f
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;
21import static android.view.Display.DEFAULT_DISPLAY;
22
23import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
24import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
25
26import android.animation.ValueAnimator;
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.os.BackgroundThread;
45import com.android.internal.policy.PipMotionHelper;
46import com.android.internal.policy.PipSnapAlgorithm;
47import com.android.server.UiThread;
48
49import java.io.PrintWriter;
50
51/**
52 * Holds the common state of the pinned stack between the system and SystemUI.
53 */
54class PinnedStackController {
55
56    private static final String TAG = TAG_WITH_CLASS_NAME ? "PinnedStackController" : TAG_WM;
57
58    private final WindowManagerService mService;
59    private final DisplayContent mDisplayContent;
60    private final Handler mHandler = UiThread.getHandler();
61
62    private IPinnedStackListener mPinnedStackListener;
63    private final PinnedStackListenerDeathHandler mPinnedStackListenerDeathHandler =
64            new PinnedStackListenerDeathHandler();
65
66    private final PinnedStackControllerCallback mCallbacks = new PinnedStackControllerCallback();
67    private final PipSnapAlgorithm mSnapAlgorithm;
68    private final PipMotionHelper mMotionHelper;
69
70    // States that affect how the PIP can be manipulated
71    private boolean mInInteractiveMode;
72    private boolean mIsImeShowing;
73    private int mImeHeight;
74    private ValueAnimator mBoundsAnimator = null;
75
76    // Used to calculate stack bounds across rotations
77    private final DisplayInfo mDisplayInfo = new DisplayInfo();
78
79    // The size and position information that describes where the pinned stack will go by default.
80    private int mDefaultStackGravity;
81    private Size mDefaultStackSize;
82    private Point mScreenEdgeInsets;
83
84    // Temp vars for calculation
85    private final DisplayMetrics mTmpMetrics = new DisplayMetrics();
86    private final Rect mTmpInsets = new Rect();
87    private final Rect mTmpRect = new Rect();
88
89    /**
90     * The callback object passed to listeners for them to notify the controller of state changes.
91     */
92    private class PinnedStackControllerCallback extends IPinnedStackController.Stub {
93
94        @Override
95        public void setInInteractiveMode(final boolean inInteractiveMode) {
96            mHandler.post(() -> {
97                // Cancel any existing animations on the PIP once the user starts dragging it
98                if (mBoundsAnimator != null && inInteractiveMode) {
99                    mBoundsAnimator.cancel();
100                }
101                mInInteractiveMode = inInteractiveMode;
102            });
103        }
104
105        @Override
106        public void setSnapToEdge(final boolean snapToEdge) {
107            mHandler.post(() -> {
108                mSnapAlgorithm.setSnapToEdge(snapToEdge);
109            });
110        }
111    }
112
113    /**
114     * Handler for the case where the listener dies.
115     */
116    private class PinnedStackListenerDeathHandler implements IBinder.DeathRecipient {
117
118        @Override
119        public void binderDied() {
120            // Clean up the state if the listener dies
121            mInInteractiveMode = false;
122            mPinnedStackListener = null;
123        }
124    }
125
126    PinnedStackController(WindowManagerService service, DisplayContent displayContent) {
127        mService = service;
128        mDisplayContent = displayContent;
129        mSnapAlgorithm = new PipSnapAlgorithm(service.mContext);
130        mMotionHelper = new PipMotionHelper(BackgroundThread.getHandler());
131        mDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo());
132        reloadResources();
133    }
134
135    void onConfigurationChanged() {
136        reloadResources();
137    }
138
139    /**
140     * Reloads all the resources for the current configuration.
141     */
142    void reloadResources() {
143        final Resources res = mService.mContext.getResources();
144        final Size defaultSizeDp = Size.parseSize(res.getString(
145                com.android.internal.R.string.config_defaultPictureInPictureSize));
146        final Size screenEdgeInsetsDp = Size.parseSize(res.getString(
147                com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets));
148        mDefaultStackGravity = res.getInteger(
149                com.android.internal.R.integer.config_defaultPictureInPictureGravity);
150        mDisplayContent.getDisplay().getRealMetrics(mTmpMetrics);
151        mDefaultStackSize = new Size(dpToPx(defaultSizeDp.getWidth(), mTmpMetrics),
152                dpToPx(defaultSizeDp.getHeight(), mTmpMetrics));
153        mScreenEdgeInsets = new Point(dpToPx(screenEdgeInsetsDp.getWidth(), mTmpMetrics),
154                dpToPx(screenEdgeInsetsDp.getHeight(), mTmpMetrics));
155    }
156
157    /**
158     * Registers a pinned stack listener.
159     */
160    void registerPinnedStackListener(IPinnedStackListener listener) {
161        try {
162            listener.asBinder().linkToDeath(mPinnedStackListenerDeathHandler, 0);
163            listener.onListenerRegistered(mCallbacks);
164            mPinnedStackListener = listener;
165            notifyBoundsChanged(mIsImeShowing);
166        } catch (RemoteException e) {
167            Log.e(TAG, "Failed to register pinned stack listener", e);
168        }
169    }
170
171    /**
172     * Returns the current bounds (or the default bounds if there are no current bounds) with the
173     * specified aspect ratio.
174     */
175    Rect getAspectRatioBounds(Rect stackBounds, float aspectRatio) {
176        // Save the snap fraction, calculate the aspect ratio based on the current bounds
177        final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds,
178                getMovementBounds(stackBounds));
179        final float radius = PointF.length(stackBounds.width(), stackBounds.height());
180        final int height = (int) Math.round(Math.sqrt((radius * radius) /
181                (aspectRatio * aspectRatio + 1)));
182        final int width = Math.round(height * aspectRatio);
183        final int left = (int) (stackBounds.centerX() - width / 2f);
184        final int top = (int) (stackBounds.centerY() - height / 2f);
185        stackBounds.set(left, top, left + width, top + height);
186        mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction);
187        return stackBounds;
188    }
189
190    /**
191     * @return the default bounds to show the PIP when there is no active PIP.
192     */
193    Rect getDefaultBounds() {
194        final Rect insetBounds = new Rect();
195        getInsetBounds(insetBounds);
196
197        final Rect defaultBounds = new Rect();
198        Gravity.apply(mDefaultStackGravity, mDefaultStackSize.getWidth(),
199                mDefaultStackSize.getHeight(), insetBounds, 0, 0, defaultBounds);
200        return defaultBounds;
201    }
202
203    /**
204     * @return the movement bounds for the given {@param stackBounds} and the current state of the
205     *         controller.
206     */
207    Rect getMovementBounds(Rect stackBounds) {
208        return getMovementBounds(stackBounds, true /* adjustForIme */);
209    }
210
211    /**
212     * @return the movement bounds for the given {@param stackBounds} and the current state of the
213     *         controller.
214     */
215    Rect getMovementBounds(Rect stackBounds, boolean adjustForIme) {
216        final Rect movementBounds = new Rect();
217        getInsetBounds(movementBounds);
218
219        // Adjust the right/bottom to ensure the stack bounds never goes offscreen
220        movementBounds.right = Math.max(movementBounds.left, movementBounds.right -
221                stackBounds.width());
222        movementBounds.bottom = Math.max(movementBounds.top, movementBounds.bottom -
223                stackBounds.height());
224
225        // Apply the movement bounds adjustments based on the current state
226        if (adjustForIme) {
227            if (mIsImeShowing) {
228                movementBounds.bottom -= mImeHeight;
229            }
230        }
231        return movementBounds;
232    }
233
234    /**
235     * @return the repositioned PIP bounds given it's pre-change bounds, and the new display info.
236     */
237    Rect onDisplayChanged(Rect preChangeStackBounds, DisplayInfo displayInfo) {
238        final Rect postChangeStackBounds = new Rect(preChangeStackBounds);
239        if (!mDisplayInfo.equals(displayInfo)) {
240            // Calculate the snap fraction of the current stack along the old movement bounds, and
241            // then update the stack bounds to the same fraction along the rotated movement bounds.
242            final Rect preChangeMovementBounds = getMovementBounds(preChangeStackBounds);
243            final float snapFraction = mSnapAlgorithm.getSnapFraction(preChangeStackBounds,
244                    preChangeMovementBounds);
245            mDisplayInfo.copyFrom(displayInfo);
246
247            final Rect postChangeMovementBounds = getMovementBounds(preChangeStackBounds,
248                    false /* adjustForIme */);
249            mSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds,
250                    snapFraction);
251        }
252        return postChangeStackBounds;
253    }
254
255    /**
256     * Sets the Ime state and height.
257     */
258    void setAdjustedForIme(boolean adjustedForIme, int imeHeight) {
259        // Return early if there is no state change
260        if (mIsImeShowing == adjustedForIme && mImeHeight == imeHeight) {
261            return;
262        }
263
264        final Rect stackBounds = new Rect();
265        mService.getStackBounds(PINNED_STACK_ID, stackBounds);
266        final Rect prevMovementBounds = getMovementBounds(stackBounds);
267        mIsImeShowing = adjustedForIme;
268        mImeHeight = imeHeight;
269        if (mInInteractiveMode) {
270            // If the user is currently interacting with the PIP and the ime state changes, then
271            // don't adjust the bounds and defer that to after the interaction
272            notifyBoundsChanged(adjustedForIme /* adjustedForIme */);
273        } else {
274            // Otherwise, we can move the PIP to a sane location to ensure that it does not block
275            // the user from interacting with the IME
276            final Rect movementBounds = getMovementBounds(stackBounds);
277            final Rect toBounds = new Rect(stackBounds);
278            if (adjustedForIme) {
279                // IME visible
280                toBounds.offset(0, Math.min(0, movementBounds.bottom - stackBounds.top));
281            } else {
282                // IME hidden
283                if (stackBounds.top == prevMovementBounds.bottom) {
284                    // If the PIP is resting on top of the IME, then adjust it with the hiding IME
285                    toBounds.offsetTo(toBounds.left, movementBounds.bottom);
286                }
287            }
288            if (!toBounds.equals(stackBounds)) {
289                if (mBoundsAnimator != null) {
290                    mBoundsAnimator.cancel();
291                }
292                mBoundsAnimator = mMotionHelper.createAnimationToBounds(stackBounds, toBounds);
293                mBoundsAnimator.start();
294            }
295        }
296    }
297
298    /**
299     * Sends a broadcast that the PIP movement bounds have changed.
300     */
301    private void notifyBoundsChanged(boolean adjustedForIme) {
302        if (mPinnedStackListener != null) {
303            try {
304                mPinnedStackListener.onBoundsChanged(adjustedForIme);
305            } catch (RemoteException e) {
306                Slog.e(TAG_WM, "Error delivering bounds changed event.", e);
307            }
308        }
309    }
310
311    /**
312     * @return the bounds on the screen that the PIP can be visible in.
313     */
314    private void getInsetBounds(Rect outRect) {
315        mService.mPolicy.getStableInsetsLw(mDisplayInfo.rotation, mDisplayInfo.logicalWidth,
316                mDisplayInfo.logicalHeight, mTmpInsets);
317        outRect.set(mTmpInsets.left + mScreenEdgeInsets.x, mTmpInsets.top + mScreenEdgeInsets.y,
318                mDisplayInfo.logicalWidth - mTmpInsets.right - mScreenEdgeInsets.x,
319                mDisplayInfo.logicalHeight - mTmpInsets.bottom - mScreenEdgeInsets.y);
320    }
321
322    /**
323     * @return the pixels for a given dp value.
324     */
325    private int dpToPx(float dpValue, DisplayMetrics dm) {
326        return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm);
327    }
328
329    void dump(String prefix, PrintWriter pw) {
330        pw.println(prefix + "PinnedStackController");
331        pw.print(prefix + "  defaultBounds="); getDefaultBounds().printShortString(pw);
332        pw.println();
333        mService.getStackBounds(PINNED_STACK_ID, mTmpRect);
334        pw.print(prefix + "  movementBounds="); getMovementBounds(mTmpRect).printShortString(pw);
335        pw.println();
336        pw.println(prefix + "  mIsImeShowing=" + mIsImeShowing);
337        pw.println(prefix + "  mInInteractiveMode=" + mInInteractiveMode);
338    }
339}
340