PinnedStackController.java revision baa7b721dc8ee22f8e5f8c373fe6ab1630446f0f
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.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.policy.PipSnapAlgorithm; 45import com.android.server.UiThread; 46 47import java.io.PrintWriter; 48import java.util.ArrayList; 49import java.util.List; 50 51/** 52 * Holds the common state of the pinned stack between the system and SystemUI. If SystemUI ever 53 * needs to be restarted, it will be notified with the last known state. 54 * 55 * Changes to the pinned stack also flow through this controller, and generally, the system only 56 * changes the pinned stack bounds through this controller in two ways: 57 * 58 * 1) When first entering PiP: the controller returns the valid bounds given, taking aspect ratio 59 * and IME state into account. 60 * 2) When rotating the device: the controller calculates the new bounds in the new orientation, 61 * taking the minimized and IME state into account. In this case, we currently ignore the 62 * SystemUI adjustments (ie. expanded for menu, interaction, etc). 63 * 64 * Other changes in the system, including adjustment of IME, configuration change, and more are 65 * handled by SystemUI (similar to the docked stack divider). 66 */ 67class PinnedStackController { 68 69 private static final String TAG = TAG_WITH_CLASS_NAME ? "PinnedStackController" : TAG_WM; 70 71 private final WindowManagerService mService; 72 private final DisplayContent mDisplayContent; 73 private final Handler mHandler = UiThread.getHandler(); 74 75 private IPinnedStackListener mPinnedStackListener; 76 private final PinnedStackListenerDeathHandler mPinnedStackListenerDeathHandler = 77 new PinnedStackListenerDeathHandler(); 78 79 private final PinnedStackControllerCallback mCallbacks = new PinnedStackControllerCallback(); 80 private final PipSnapAlgorithm mSnapAlgorithm; 81 82 // States that affect how the PIP can be manipulated 83 private boolean mIsMinimized; 84 private boolean mIsImeShowing; 85 private int mImeHeight; 86 87 // The set of actions and aspect-ratio for the that are currently allowed on the PiP activity 88 private ArrayList<RemoteAction> mActions = new ArrayList<>(); 89 private float mAspectRatio = -1f; 90 91 // Used to calculate stack bounds across rotations 92 private final DisplayInfo mDisplayInfo = new DisplayInfo(); 93 private final Rect mStableInsets = new Rect(); 94 95 // The size and position information that describes where the pinned stack will go by default. 96 private int mDefaultStackGravity; 97 private float mDefaultAspectRatio; 98 private Point mScreenEdgeInsets; 99 100 // The aspect ratio bounds of the PIP. 101 private float mMinAspectRatio; 102 private float mMaxAspectRatio; 103 104 // The minimum edge size of the normal PiP bounds. 105 private int mMinSize; 106 107 // Temp vars for calculation 108 private final DisplayMetrics mTmpMetrics = new DisplayMetrics(); 109 private final Rect mTmpInsets = new Rect(); 110 private final Rect mTmpRect = 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 127 /** 128 * Handler for the case where the listener dies. 129 */ 130 private class PinnedStackListenerDeathHandler implements IBinder.DeathRecipient { 131 132 @Override 133 public void binderDied() { 134 // Clean up the state if the listener dies 135 mPinnedStackListener = null; 136 } 137 } 138 139 PinnedStackController(WindowManagerService service, DisplayContent displayContent) { 140 mService = service; 141 mDisplayContent = displayContent; 142 mSnapAlgorithm = new PipSnapAlgorithm(service.mContext); 143 mDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo()); 144 reloadResources(); 145 } 146 147 void onConfigurationChanged() { 148 reloadResources(); 149 notifyMovementBoundsChanged(false /* fromImeAdjustment */); 150 } 151 152 /** 153 * Reloads all the resources for the current configuration. 154 */ 155 void reloadResources() { 156 final Resources res = mService.mContext.getResources(); 157 mMinSize = res.getDimensionPixelSize( 158 com.android.internal.R.dimen.default_minimal_size_pip_resizable_task); 159 mDefaultAspectRatio = res.getFloat( 160 com.android.internal.R.dimen.config_pictureInPictureDefaultAspectRatio); 161 final Size screenEdgeInsetsDp = Size.parseSize(res.getString( 162 com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets)); 163 mDefaultStackGravity = res.getInteger( 164 com.android.internal.R.integer.config_defaultPictureInPictureGravity); 165 mDisplayContent.getDisplay().getRealMetrics(mTmpMetrics); 166 mScreenEdgeInsets = new Point(dpToPx(screenEdgeInsetsDp.getWidth(), mTmpMetrics), 167 dpToPx(screenEdgeInsetsDp.getHeight(), mTmpMetrics)); 168 mMinAspectRatio = res.getFloat( 169 com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio); 170 mMaxAspectRatio = res.getFloat( 171 com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio); 172 } 173 174 /** 175 * Registers a pinned stack listener. 176 */ 177 void registerPinnedStackListener(IPinnedStackListener listener) { 178 try { 179 listener.asBinder().linkToDeath(mPinnedStackListenerDeathHandler, 0); 180 listener.onListenerRegistered(mCallbacks); 181 mPinnedStackListener = listener; 182 notifyImeVisibilityChanged(mIsImeShowing, mImeHeight); 183 // The movement bounds notification needs to be sent before the minimized state, since 184 // SystemUI may use the bounds to retore the minimized position 185 notifyMovementBoundsChanged(false /* fromImeAdjustment */); 186 notifyActionsChanged(mActions); 187 notifyMinimizeChanged(mIsMinimized); 188 } catch (RemoteException e) { 189 Log.e(TAG, "Failed to register pinned stack listener", e); 190 } 191 } 192 193 /** 194 * @return whether the given {@param aspectRatio} is valid. 195 */ 196 public boolean isValidPictureInPictureAspectRatio(float aspectRatio) { 197 return mMinAspectRatio <= aspectRatio && aspectRatio <= mMaxAspectRatio; 198 } 199 200 /** 201 * Returns the current bounds (or the default bounds if there are no current bounds) with the 202 * specified aspect ratio. 203 */ 204 Rect transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio) { 205 // Save the snap fraction, calculate the aspect ratio based on screen size 206 final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds, 207 getMovementBounds(stackBounds)); 208 final Size size = getSize(aspectRatio); 209 final int left = (int) (stackBounds.centerX() - size.getWidth() / 2f); 210 final int top = (int) (stackBounds.centerY() - size.getHeight() / 2f); 211 stackBounds.set(left, top, left + size.getWidth(), top + size.getHeight()); 212 mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction); 213 if (mIsMinimized) { 214 applyMinimizedOffset(stackBounds, getMovementBounds(stackBounds)); 215 } 216 return stackBounds; 217 } 218 219 /** 220 * @return the size of the PIP based on the given {@param aspectRatio}. 221 */ 222 Size getSize(float aspectRatio) { 223 return mSnapAlgorithm.getSizeForAspectRatio(aspectRatio, mMinSize, 224 mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight); 225 } 226 227 /** 228 * @return the default bounds to show the PIP when there is no active PIP. 229 */ 230 Rect getDefaultBounds() { 231 final Rect insetBounds = new Rect(); 232 getInsetBounds(insetBounds); 233 234 final Rect defaultBounds = new Rect(); 235 final Size size = getSize(mDefaultAspectRatio); 236 Gravity.apply(mDefaultStackGravity, size.getWidth(), size.getHeight(), insetBounds, 237 0, mIsImeShowing ? mImeHeight : 0, defaultBounds); 238 return defaultBounds; 239 } 240 241 /** 242 * Updates the display info, calculating and returning the new stack and movement bounds in the 243 * new orientation of the device if necessary. 244 */ 245 void onTaskStackBoundsChanged(Rect targetBounds, Rect outBounds) { 246 final DisplayInfo displayInfo = mDisplayContent.getDisplayInfo(); 247 if (mDisplayInfo.equals(displayInfo)) { 248 return; 249 } 250 251 mTmpRect.set(targetBounds); 252 final Rect postChangeStackBounds = mTmpRect; 253 254 // Calculate the snap fraction of the current stack along the old movement bounds 255 final Rect preChangeMovementBounds = getMovementBounds(postChangeStackBounds); 256 final float snapFraction = mSnapAlgorithm.getSnapFraction(postChangeStackBounds, 257 preChangeMovementBounds); 258 mDisplayInfo.copyFrom(displayInfo); 259 260 // Calculate the stack bounds in the new orientation to the same same fraction along the 261 // rotated movement bounds. 262 final Rect postChangeMovementBounds = getMovementBounds(postChangeStackBounds, 263 false /* adjustForIme */); 264 mSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds, 265 snapFraction); 266 if (mIsMinimized) { 267 applyMinimizedOffset(postChangeStackBounds, postChangeMovementBounds); 268 } 269 270 notifyMovementBoundsChanged(false /* fromImeAdjustment */); 271 272 outBounds.set(postChangeStackBounds); 273 } 274 275 /** 276 * Sets the Ime state and height. 277 */ 278 void setAdjustedForIme(boolean adjustedForIme, int imeHeight) { 279 // Return early if there is no state change 280 if (mIsImeShowing == adjustedForIme && mImeHeight == imeHeight) { 281 return; 282 } 283 284 mIsImeShowing = adjustedForIme; 285 mImeHeight = imeHeight; 286 notifyImeVisibilityChanged(adjustedForIme, imeHeight); 287 notifyMovementBoundsChanged(true /* fromImeAdjustment */); 288 } 289 290 /** 291 * Sets the current aspect ratio. 292 */ 293 void setAspectRatio(float aspectRatio) { 294 if (Float.compare(mAspectRatio, aspectRatio) != 0) { 295 mAspectRatio = aspectRatio; 296 notifyMovementBoundsChanged(false /* fromImeAdjustment */); 297 } 298 } 299 300 /** 301 * Sets the current set of actions. 302 */ 303 void setActions(List<RemoteAction> actions) { 304 mActions.clear(); 305 if (actions != null) { 306 mActions.addAll(actions); 307 } 308 notifyActionsChanged(mActions); 309 } 310 311 /** 312 * Notifies listeners that the PIP needs to be adjusted for the IME. 313 */ 314 private void notifyImeVisibilityChanged(boolean imeVisible, int imeHeight) { 315 if (mPinnedStackListener != null) { 316 try { 317 mPinnedStackListener.onImeVisibilityChanged(imeVisible, imeHeight); 318 } catch (RemoteException e) { 319 Slog.e(TAG_WM, "Error delivering bounds changed event.", e); 320 } 321 } 322 } 323 324 /** 325 * Notifies listeners that the PIP minimized state has changed. 326 */ 327 private void notifyMinimizeChanged(boolean isMinimized) { 328 if (mPinnedStackListener != null) { 329 try { 330 mPinnedStackListener.onMinimizedStateChanged(isMinimized); 331 } catch (RemoteException e) { 332 Slog.e(TAG_WM, "Error delivering minimize changed event.", e); 333 } 334 } 335 } 336 337 /** 338 * Notifies listeners that the PIP actions have changed. 339 */ 340 private void notifyActionsChanged(List<RemoteAction> actions) { 341 if (mPinnedStackListener != null) { 342 try { 343 mPinnedStackListener.onActionsChanged(new ParceledListSlice(actions)); 344 } catch (RemoteException e) { 345 Slog.e(TAG_WM, "Error delivering actions changed event.", e); 346 } 347 } 348 } 349 350 /** 351 * Notifies listeners that the PIP movement bounds have changed. 352 */ 353 private void notifyMovementBoundsChanged(boolean fromImeAdjustement) { 354 if (mPinnedStackListener != null) { 355 try { 356 final Rect insetBounds = new Rect(); 357 getInsetBounds(insetBounds); 358 final Rect normalBounds = getDefaultBounds(); 359 if (isValidPictureInPictureAspectRatio(mAspectRatio)) { 360 transformBoundsToAspectRatio(normalBounds, mAspectRatio); 361 } 362 final Rect animatingBounds = mTmpRect; 363 final TaskStack pinnedStack = mDisplayContent.getStackById(PINNED_STACK_ID); 364 if (pinnedStack != null) { 365 pinnedStack.getAnimatingBounds(animatingBounds); 366 } else { 367 animatingBounds.set(normalBounds); 368 } 369 mPinnedStackListener.onMovementBoundsChanged(insetBounds, normalBounds, 370 animatingBounds, fromImeAdjustement); 371 } catch (RemoteException e) { 372 Slog.e(TAG_WM, "Error delivering actions changed event.", e); 373 } 374 } 375 } 376 377 /** 378 * @return the bounds on the screen that the PIP can be visible in. 379 */ 380 private void getInsetBounds(Rect outRect) { 381 mService.mPolicy.getStableInsetsLw(mDisplayInfo.rotation, mDisplayInfo.logicalWidth, 382 mDisplayInfo.logicalHeight, mTmpInsets); 383 outRect.set(mTmpInsets.left + mScreenEdgeInsets.x, mTmpInsets.top + mScreenEdgeInsets.y, 384 mDisplayInfo.logicalWidth - mTmpInsets.right - mScreenEdgeInsets.x, 385 mDisplayInfo.logicalHeight - mTmpInsets.bottom - mScreenEdgeInsets.y); 386 } 387 388 /** 389 * @return the movement bounds for the given {@param stackBounds} and the current state of the 390 * controller. 391 */ 392 private Rect getMovementBounds(Rect stackBounds) { 393 return getMovementBounds(stackBounds, true /* adjustForIme */); 394 } 395 396 /** 397 * @return the movement bounds for the given {@param stackBounds} and the current state of the 398 * controller. 399 */ 400 private Rect getMovementBounds(Rect stackBounds, boolean adjustForIme) { 401 final Rect movementBounds = new Rect(); 402 getInsetBounds(movementBounds); 403 404 // Apply the movement bounds adjustments based on the current state 405 mSnapAlgorithm.getMovementBounds(stackBounds, movementBounds, movementBounds, 406 (adjustForIme && mIsImeShowing) ? mImeHeight : 0); 407 return movementBounds; 408 } 409 410 /** 411 * Applies the minimized offsets to the given stack bounds. 412 */ 413 private void applyMinimizedOffset(Rect stackBounds, Rect movementBounds) { 414 mTmpDisplaySize.set(mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight); 415 mService.getStableInsetsLocked(mDisplayContent.getDisplayId(), mStableInsets); 416 mSnapAlgorithm.applyMinimizedOffset(stackBounds, movementBounds, mTmpDisplaySize, 417 mStableInsets); 418 } 419 420 /** 421 * @return the pixels for a given dp value. 422 */ 423 private int dpToPx(float dpValue, DisplayMetrics dm) { 424 return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm); 425 } 426 427 void dump(String prefix, PrintWriter pw) { 428 pw.println(prefix + "PinnedStackController"); 429 pw.print(prefix + " defaultBounds="); getDefaultBounds().printShortString(pw); 430 pw.println(); 431 mService.getStackBounds(PINNED_STACK_ID, mTmpRect); 432 pw.print(prefix + " movementBounds="); getMovementBounds(mTmpRect).printShortString(pw); 433 pw.println(); 434 pw.println(prefix + " mIsImeShowing=" + mIsImeShowing); 435 pw.println(prefix + " mIsMinimized=" + mIsMinimized); 436 if (mActions.isEmpty()) { 437 pw.println(prefix + " mActions=[]"); 438 } else { 439 pw.println(prefix + " mActions=["); 440 for (int i = 0; i < mActions.size(); i++) { 441 RemoteAction action = mActions.get(i); 442 pw.print(prefix + " Action[" + i + "]: "); 443 action.dump("", pw); 444 } 445 pw.println(prefix + " ]"); 446 } 447 } 448} 449