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