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