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