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