Home | History | Annotate | Download | only in wm
      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 
     17 package com.android.server.wm;
     18 
     19 import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
     20 import static android.util.TypedValue.COMPLEX_UNIT_DIP;
     21 
     22 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
     23 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
     24 
     25 import android.app.RemoteAction;
     26 import android.content.pm.ParceledListSlice;
     27 import android.content.res.Resources;
     28 import android.graphics.Point;
     29 import android.graphics.Rect;
     30 import android.os.Handler;
     31 import android.os.IBinder;
     32 import android.os.RemoteException;
     33 import android.util.DisplayMetrics;
     34 import android.util.Log;
     35 import android.util.Size;
     36 import android.util.Slog;
     37 import android.util.TypedValue;
     38 import android.view.DisplayInfo;
     39 import android.view.Gravity;
     40 import android.view.IPinnedStackController;
     41 import android.view.IPinnedStackListener;
     42 
     43 import com.android.internal.policy.PipSnapAlgorithm;
     44 import com.android.server.UiThread;
     45 
     46 import java.io.PrintWriter;
     47 import java.util.ArrayList;
     48 import 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  */
     66 class 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