PinnedStackController.java revision f1f72f6562244b87f76275fb56c34bf3b0e3cec9
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.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.policy.PipSnapAlgorithm;
45import com.android.server.UiThread;
46
47import java.io.PrintWriter;
48import java.util.ArrayList;
49import java.util.List;
50
51/**
52 * Holds the common state of the pinned stack between the system and SystemUI. If SystemUI ever
53 * needs to be restarted, it will be notified with the last known state.
54 *
55 * Changes to the pinned stack also flow through this controller, and generally, the system only
56 * changes the pinned stack bounds through this controller in two ways:
57 *
58 * 1) When first entering PiP: the controller returns the valid bounds given, taking aspect ratio
59 *    and IME state into account.
60 * 2) When rotating the device: the controller calculates the new bounds in the new orientation,
61 *    taking the minimized and IME state into account. In this case, we currently ignore the
62 *    SystemUI adjustments (ie. expanded for menu, interaction, etc).
63 *
64 * Other changes in the system, including adjustment of IME, configuration change, and more are
65 * handled by SystemUI (similar to the docked stack divider).
66 */
67class PinnedStackController {
68
69    private static final String TAG = TAG_WITH_CLASS_NAME ? "PinnedStackController" : TAG_WM;
70
71    private final WindowManagerService mService;
72    private final DisplayContent mDisplayContent;
73    private final Handler mHandler = UiThread.getHandler();
74
75    private IPinnedStackListener mPinnedStackListener;
76    private final PinnedStackListenerDeathHandler mPinnedStackListenerDeathHandler =
77            new PinnedStackListenerDeathHandler();
78
79    private final PinnedStackControllerCallback mCallbacks = new PinnedStackControllerCallback();
80    private final PipSnapAlgorithm mSnapAlgorithm;
81
82    // States that affect how the PIP can be manipulated
83    private boolean mIsMinimized;
84    private boolean mIsImeShowing;
85    private int mImeHeight;
86
87    // The set of actions and aspect-ratio for the that are currently allowed on the PiP activity
88    private ArrayList<RemoteAction> mActions = new ArrayList<>();
89    private float mAspectRatio = -1f;
90
91    // Used to calculate stack bounds across rotations
92    private final DisplayInfo mDisplayInfo = new DisplayInfo();
93    private final Rect mStableInsets = new Rect();
94
95    // The size and position information that describes where the pinned stack will go by default.
96    private int mDefaultStackGravity;
97    private Size mDefaultStackSize;
98    private Point mScreenEdgeInsets;
99
100    // The aspect ratio bounds of the PIP.
101    private float mMinAspectRatio;
102    private float mMaxAspectRatio;
103
104    // Temp vars for calculation
105    private final DisplayMetrics mTmpMetrics = new DisplayMetrics();
106    private final Rect mTmpInsets = new Rect();
107    private final Rect mTmpRect = new Rect();
108    private final Point mTmpDisplaySize = new Point();
109
110    /**
111     * The callback object passed to listeners for them to notify the controller of state changes.
112     */
113    private class PinnedStackControllerCallback extends IPinnedStackController.Stub {
114
115        @Override
116        public void setIsMinimized(final boolean isMinimized) {
117            mHandler.post(() -> {
118                mIsMinimized = isMinimized;
119                mSnapAlgorithm.setMinimized(isMinimized);
120            });
121        }
122    }
123
124    /**
125     * Handler for the case where the listener dies.
126     */
127    private class PinnedStackListenerDeathHandler implements IBinder.DeathRecipient {
128
129        @Override
130        public void binderDied() {
131            // Clean up the state if the listener dies
132            mPinnedStackListener = null;
133        }
134    }
135
136    PinnedStackController(WindowManagerService service, DisplayContent displayContent) {
137        mService = service;
138        mDisplayContent = displayContent;
139        mSnapAlgorithm = new PipSnapAlgorithm(service.mContext);
140        mDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo());
141        reloadResources();
142    }
143
144    void onConfigurationChanged() {
145        reloadResources();
146        notifyMovementBoundsChanged(false /* fromImeAdjustment */);
147    }
148
149    /**
150     * Reloads all the resources for the current configuration.
151     */
152    void reloadResources() {
153        final Resources res = mService.mContext.getResources();
154        final Size defaultSizeDp = Size.parseSize(res.getString(
155                com.android.internal.R.string.config_defaultPictureInPictureSize));
156        final Size screenEdgeInsetsDp = Size.parseSize(res.getString(
157                com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets));
158        mDefaultStackGravity = res.getInteger(
159                com.android.internal.R.integer.config_defaultPictureInPictureGravity);
160        mDisplayContent.getDisplay().getRealMetrics(mTmpMetrics);
161        mDefaultStackSize = new Size(dpToPx(defaultSizeDp.getWidth(), mTmpMetrics),
162                dpToPx(defaultSizeDp.getHeight(), mTmpMetrics));
163        mScreenEdgeInsets = new Point(dpToPx(screenEdgeInsetsDp.getWidth(), mTmpMetrics),
164                dpToPx(screenEdgeInsetsDp.getHeight(), mTmpMetrics));
165        mMinAspectRatio = res.getFloat(
166                com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio);
167        mMaxAspectRatio = res.getFloat(
168                com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio);
169    }
170
171    /**
172     * Registers a pinned stack listener.
173     */
174    void registerPinnedStackListener(IPinnedStackListener listener) {
175        try {
176            listener.asBinder().linkToDeath(mPinnedStackListenerDeathHandler, 0);
177            listener.onListenerRegistered(mCallbacks);
178            mPinnedStackListener = listener;
179            notifyImeVisibilityChanged(mIsImeShowing, mImeHeight);
180            // The movement bounds notification needs to be sent before the minimized state, since
181            // SystemUI may use the bounds to retore the minimized position
182            notifyMovementBoundsChanged(false /* fromImeAdjustment */);
183            notifyActionsChanged(mActions);
184            notifyMinimizeChanged(mIsMinimized);
185        } catch (RemoteException e) {
186            Log.e(TAG, "Failed to register pinned stack listener", e);
187        }
188    }
189
190    /**
191     * @return whether the given {@param aspectRatio} is valid.
192     */
193    public boolean isValidPictureInPictureAspectRatio(float aspectRatio) {
194        return mMinAspectRatio <= aspectRatio && aspectRatio <= mMaxAspectRatio;
195    }
196
197    /**
198     * Returns the current bounds (or the default bounds if there are no current bounds) with the
199     * specified aspect ratio.
200     */
201    Rect transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio) {
202        // Save the snap fraction, calculate the aspect ratio based on the current bounds
203        final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds,
204                getMovementBounds(stackBounds));
205        final float radius = PointF.length(stackBounds.width(), stackBounds.height());
206        final int height = (int) Math.round(Math.sqrt((radius * radius) /
207                (aspectRatio * aspectRatio + 1)));
208        final int width = Math.round(height * aspectRatio);
209        final int left = (int) (stackBounds.centerX() - width / 2f);
210        final int top = (int) (stackBounds.centerY() - height / 2f);
211        stackBounds.set(left, top, left + width, top + height);
212        mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction);
213        if (mIsMinimized) {
214            applyMinimizedOffset(stackBounds, getMovementBounds(stackBounds));
215        }
216        return stackBounds;
217    }
218
219    /**
220     * @return the default bounds to show the PIP when there is no active PIP.
221     */
222    Rect getDefaultBounds() {
223        final Rect insetBounds = new Rect();
224        getInsetBounds(insetBounds);
225
226        final Rect defaultBounds = new Rect();
227        Gravity.apply(mDefaultStackGravity, mDefaultStackSize.getWidth(),
228                mDefaultStackSize.getHeight(), insetBounds, 0, 0, defaultBounds);
229        return defaultBounds;
230    }
231
232    /**
233     * @return the movement bounds for the given {@param stackBounds} and the current state of the
234     *         controller.
235     */
236    Rect getMovementBounds(Rect stackBounds) {
237        return getMovementBounds(stackBounds, true /* adjustForIme */);
238    }
239
240    /**
241     * @return the movement bounds for the given {@param stackBounds} and the current state of the
242     *         controller.
243     */
244    Rect getMovementBounds(Rect stackBounds, boolean adjustForIme) {
245        final Rect movementBounds = new Rect();
246        getInsetBounds(movementBounds);
247
248        // Apply the movement bounds adjustments based on the current state
249        mSnapAlgorithm.getMovementBounds(stackBounds, movementBounds, movementBounds,
250                (adjustForIme && mIsImeShowing) ? mImeHeight : 0);
251        return movementBounds;
252    }
253
254    /**
255     * @param preChangeTargetBounds The final bounds of the stack if it is currently animating
256     * @return the repositioned PIP bounds given it's pre-change bounds, and the new display
257     *         content.
258     */
259    Rect onDisplayChanged(Rect preChangeStackBounds, Rect preChangeTargetBounds,
260            DisplayContent displayContent) {
261        final Rect postChangeStackBounds = new Rect(preChangeTargetBounds);
262        final DisplayInfo displayInfo = displayContent.getDisplayInfo();
263        if (!mDisplayInfo.equals(displayInfo)) {
264            // Calculate the snap fraction of the current stack along the old movement bounds, and
265            // then update the stack bounds to the same fraction along the rotated movement bounds.
266            final Rect preChangeMovementBounds = getMovementBounds(preChangeStackBounds);
267            final float snapFraction = mSnapAlgorithm.getSnapFraction(preChangeStackBounds,
268                    preChangeMovementBounds);
269            mDisplayInfo.copyFrom(displayInfo);
270
271            final Rect postChangeMovementBounds = getMovementBounds(preChangeStackBounds,
272                    false /* adjustForIme */);
273            mSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds,
274                    snapFraction);
275            if (mIsMinimized) {
276                applyMinimizedOffset(postChangeStackBounds, postChangeMovementBounds);
277            }
278            notifyMovementBoundsChanged(false /* fromImeAdjustment */);
279        }
280        return postChangeStackBounds;
281    }
282
283    /**
284     * Sets the Ime state and height.
285     */
286    void setAdjustedForIme(boolean adjustedForIme, int imeHeight) {
287        // Return early if there is no state change
288        if (mIsImeShowing == adjustedForIme && mImeHeight == imeHeight) {
289            return;
290        }
291
292        mIsImeShowing = adjustedForIme;
293        mImeHeight = imeHeight;
294        notifyImeVisibilityChanged(adjustedForIme, imeHeight);
295        notifyMovementBoundsChanged(true /* fromImeAdjustment */);
296    }
297
298    /**
299     * Sets the current aspect ratio.
300     */
301    void setAspectRatio(float aspectRatio) {
302        if (Float.compare(mAspectRatio, aspectRatio) != 0) {
303            mAspectRatio = aspectRatio;
304            notifyMovementBoundsChanged(false /* fromImeAdjustment */);
305        }
306    }
307
308    /**
309     * Sets the current set of actions.
310     */
311    void setActions(List<RemoteAction> actions) {
312        mActions.clear();
313        if (actions != null) {
314            mActions.addAll(actions);
315        }
316        notifyActionsChanged(mActions);
317    }
318
319    /**
320     * Notifies listeners that the PIP needs to be adjusted for the IME.
321     */
322    private void notifyImeVisibilityChanged(boolean imeVisible, int imeHeight) {
323        if (mPinnedStackListener != null) {
324            try {
325                mPinnedStackListener.onImeVisibilityChanged(imeVisible, imeHeight);
326            } catch (RemoteException e) {
327                Slog.e(TAG_WM, "Error delivering bounds changed event.", e);
328            }
329        }
330    }
331
332    /**
333     * Notifies listeners that the PIP minimized state has changed.
334     */
335    private void notifyMinimizeChanged(boolean isMinimized) {
336        if (mPinnedStackListener != null) {
337            try {
338                mPinnedStackListener.onMinimizedStateChanged(isMinimized);
339            } catch (RemoteException e) {
340                Slog.e(TAG_WM, "Error delivering minimize changed event.", e);
341            }
342        }
343    }
344
345    /**
346     * Notifies listeners that the PIP actions have changed.
347     */
348    private void notifyActionsChanged(List<RemoteAction> actions) {
349        if (mPinnedStackListener != null) {
350            try {
351                mPinnedStackListener.onActionsChanged(new ParceledListSlice(actions));
352            } catch (RemoteException e) {
353                Slog.e(TAG_WM, "Error delivering actions changed event.", e);
354            }
355        }
356    }
357
358    /**
359     * Notifies listeners that the PIP movement bounds have changed.
360     */
361    private void notifyMovementBoundsChanged(boolean fromImeAdjustement) {
362        if (mPinnedStackListener != null) {
363            try {
364                Rect insetBounds = new Rect();
365                getInsetBounds(insetBounds);
366                Rect normalBounds = getDefaultBounds();
367                if (isValidPictureInPictureAspectRatio(mAspectRatio)) {
368                    transformBoundsToAspectRatio(normalBounds, mAspectRatio);
369                }
370                mPinnedStackListener.onMovementBoundsChanged(insetBounds, normalBounds,
371                        fromImeAdjustement);
372            } catch (RemoteException e) {
373                Slog.e(TAG_WM, "Error delivering actions changed event.", e);
374            }
375        }
376    }
377
378    /**
379     * @return the bounds on the screen that the PIP can be visible in.
380     */
381    private void getInsetBounds(Rect outRect) {
382        mService.mPolicy.getStableInsetsLw(mDisplayInfo.rotation, mDisplayInfo.logicalWidth,
383                mDisplayInfo.logicalHeight, mTmpInsets);
384        outRect.set(mTmpInsets.left + mScreenEdgeInsets.x, mTmpInsets.top + mScreenEdgeInsets.y,
385                mDisplayInfo.logicalWidth - mTmpInsets.right - mScreenEdgeInsets.x,
386                mDisplayInfo.logicalHeight - mTmpInsets.bottom - mScreenEdgeInsets.y);
387    }
388
389    /**
390     * Applies the minimized offsets to the given stack bounds.
391     */
392    private void applyMinimizedOffset(Rect stackBounds, Rect movementBounds) {
393        mTmpDisplaySize.set(mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
394        mService.getStableInsetsLocked(mDisplayContent.getDisplayId(), mStableInsets);
395        mSnapAlgorithm.applyMinimizedOffset(stackBounds, movementBounds, mTmpDisplaySize,
396                mStableInsets);
397    }
398
399    /**
400     * @return the pixels for a given dp value.
401     */
402    private int dpToPx(float dpValue, DisplayMetrics dm) {
403        return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm);
404    }
405
406    void dump(String prefix, PrintWriter pw) {
407        pw.println(prefix + "PinnedStackController");
408        pw.print(prefix + "  defaultBounds="); getDefaultBounds().printShortString(pw);
409        pw.println();
410        mService.getStackBounds(PINNED_STACK_ID, mTmpRect);
411        pw.print(prefix + "  movementBounds="); getMovementBounds(mTmpRect).printShortString(pw);
412        pw.println();
413        pw.println(prefix + "  mIsImeShowing=" + mIsImeShowing);
414        pw.println(prefix + "  mIsMinimized=" + mIsMinimized);
415        if (mActions.isEmpty()) {
416            pw.println(prefix + "  mActions=[]");
417        } else {
418            pw.println(prefix + "  mActions=[");
419            for (int i = 0; i < mActions.size(); i++) {
420                RemoteAction action = mActions.get(i);
421                pw.print(prefix + "    Action[" + i + "]: ");
422                action.dump("", pw);
423            }
424            pw.println(prefix + "  ]");
425        }
426    }
427}
428