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