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