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 setIsMinimized(final boolean isMinimized)118 public void setIsMinimized(final boolean isMinimized) { 119 mHandler.post(() -> { 120 mIsMinimized = isMinimized; 121 mSnapAlgorithm.setMinimized(isMinimized); 122 }); 123 } 124 125 @Override setMinEdgeSize(int minEdgeSize)126 public void setMinEdgeSize(int minEdgeSize) { 127 mHandler.post(() -> { 128 mCurrentMinSize = Math.max(mDefaultMinSize, minEdgeSize); 129 }); 130 } 131 132 @Override getDisplayRotation()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 binderDied()146 public void binderDied() { 147 // Clean up the state if the listener dies 148 mPinnedStackListener = null; 149 } 150 } 151 PinnedStackController(WindowManagerService service, DisplayContent displayContent)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 onConfigurationChanged()164 void onConfigurationChanged() { 165 reloadResources(); 166 } 167 168 /** 169 * Reloads all the resources for the current configuration. 170 */ reloadResources()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 */ registerPinnedStackListener(IPinnedStackListener listener)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 */ isValidPictureInPictureAspectRatio(float aspectRatio)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 */ transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio, boolean useCurrentMinEdgeSize)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 */ getDefaultBounds()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 */ onDisplayInfoChanged()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 */ onTaskStackBoundsChanged(Rect targetBounds, Rect outBounds)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 */ setAdjustedForIme(boolean adjustedForIme, int imeHeight)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 */ setAspectRatio(float aspectRatio)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 */ getAspectRatio()345 float getAspectRatio() { 346 return mAspectRatio; 347 } 348 349 /** 350 * Sets the current set of actions. 351 */ setActions(List<RemoteAction> actions)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 */ notifyImeVisibilityChanged(boolean imeVisible, int imeHeight)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 */ notifyMinimizeChanged(boolean isMinimized)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 */ notifyActionsChanged(List<RemoteAction> actions)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 */ notifyMovementBoundsChanged(boolean fromImeAdjustement)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 */ getInsetBounds(Rect outRect)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 */ getMovementBounds(Rect stackBounds)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 */ getMovementBounds(Rect stackBounds, boolean adjustForIme)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 */ applyMinimizedOffset(Rect stackBounds, Rect movementBounds)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 */ dpToPx(float dpValue, DisplayMetrics dm)483 private int dpToPx(float dpValue, DisplayMetrics dm) { 484 return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm); 485 } 486 dump(String prefix, PrintWriter pw)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