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