PinnedStackController.java revision dff5c08bfd5c15f11b71ce953282ed519ff1b290
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; 21import static android.view.Display.DEFAULT_DISPLAY; 22 23import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; 24import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; 25 26import android.animation.ValueAnimator; 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.os.BackgroundThread; 44import com.android.internal.policy.PipMotionHelper; 45import com.android.internal.policy.PipSnapAlgorithm; 46 47import java.io.PrintWriter; 48 49/** 50 * Holds the common state of the pinned stack between the system and SystemUI. 51 */ 52class PinnedStackController { 53 54 private static final String TAG = TAG_WITH_CLASS_NAME ? "PinnedStackController" : TAG_WM; 55 56 private final WindowManagerService mService; 57 private final DisplayContent mDisplayContent; 58 private final Handler mHandler = new Handler(); 59 60 private IPinnedStackListener mPinnedStackListener; 61 private final PinnedStackListenerDeathHandler mPinnedStackListenerDeathHandler = 62 new PinnedStackListenerDeathHandler(); 63 64 private final PinnedStackControllerCallback mCallbacks = new PinnedStackControllerCallback(); 65 private final PipSnapAlgorithm mSnapAlgorithm; 66 private final PipMotionHelper mMotionHelper; 67 68 // States that affect how the PIP can be manipulated 69 private boolean mInInteractiveMode; 70 private boolean mIsImeShowing; 71 private int mImeHeight; 72 private ValueAnimator mBoundsAnimator = null; 73 74 // Used to calculate stack bounds across rotations 75 private final DisplayInfo mDisplayInfo = new DisplayInfo(); 76 77 // The size and position information that describes where the pinned stack will go by default. 78 private int mDefaultStackGravity; 79 private Size mDefaultStackSize; 80 private Point mScreenEdgeInsets; 81 82 // Temp vars for calculation 83 private final DisplayMetrics mTmpMetrics = new DisplayMetrics(); 84 private final Rect mTmpInsets = new Rect(); 85 private final Rect mTmpRect = new Rect(); 86 87 /** 88 * The callback object passed to listeners for them to notify the controller of state changes. 89 */ 90 private class PinnedStackControllerCallback extends IPinnedStackController.Stub { 91 92 @Override 93 public void setInInteractiveMode(final boolean inInteractiveMode) { 94 mHandler.post(() -> { 95 // Cancel any existing animations on the PIP once the user starts dragging it 96 if (mBoundsAnimator != null && inInteractiveMode) { 97 mBoundsAnimator.cancel(); 98 } 99 mInInteractiveMode = inInteractiveMode; 100 }); 101 } 102 103 @Override 104 public void setSnapToEdge(final boolean snapToEdge) { 105 mHandler.post(() -> { 106 mSnapAlgorithm.setSnapToEdge(snapToEdge); 107 }); 108 } 109 } 110 111 /** 112 * Handler for the case where the listener dies. 113 */ 114 private class PinnedStackListenerDeathHandler implements IBinder.DeathRecipient { 115 116 @Override 117 public void binderDied() { 118 // Clean up the state if the listener dies 119 mInInteractiveMode = false; 120 mPinnedStackListener = null; 121 } 122 } 123 124 PinnedStackController(WindowManagerService service, DisplayContent displayContent) { 125 mService = service; 126 mDisplayContent = displayContent; 127 mSnapAlgorithm = new PipSnapAlgorithm(service.mContext); 128 mMotionHelper = new PipMotionHelper(BackgroundThread.getHandler()); 129 mDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo()); 130 reloadResources(); 131 } 132 133 void onConfigurationChanged() { 134 reloadResources(); 135 } 136 137 /** 138 * Reloads all the resources for the current configuration. 139 */ 140 void reloadResources() { 141 final Resources res = mService.mContext.getResources(); 142 final Size defaultSizeDp = Size.parseSize(res.getString( 143 com.android.internal.R.string.config_defaultPictureInPictureSize)); 144 final Size screenEdgeInsetsDp = Size.parseSize(res.getString( 145 com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets)); 146 mDefaultStackGravity = res.getInteger( 147 com.android.internal.R.integer.config_defaultPictureInPictureGravity); 148 mDisplayContent.getDisplay().getRealMetrics(mTmpMetrics); 149 mDefaultStackSize = new Size(dpToPx(defaultSizeDp.getWidth(), mTmpMetrics), 150 dpToPx(defaultSizeDp.getHeight(), mTmpMetrics)); 151 mScreenEdgeInsets = new Point(dpToPx(screenEdgeInsetsDp.getWidth(), mTmpMetrics), 152 dpToPx(screenEdgeInsetsDp.getHeight(), mTmpMetrics)); 153 } 154 155 /** 156 * Registers a pinned stack listener. 157 */ 158 void registerPinnedStackListener(IPinnedStackListener listener) { 159 try { 160 listener.asBinder().linkToDeath(mPinnedStackListenerDeathHandler, 0); 161 listener.onListenerRegistered(mCallbacks); 162 mPinnedStackListener = listener; 163 notifyBoundsChanged(mIsImeShowing); 164 } catch (RemoteException e) { 165 Log.e(TAG, "Failed to register pinned stack listener", e); 166 } 167 } 168 169 /** 170 * @return the default bounds to show the PIP when there is no active PIP. 171 */ 172 Rect getDefaultBounds() { 173 final Rect insetBounds = new Rect(); 174 getInsetBounds(insetBounds); 175 176 final Rect defaultBounds = new Rect(); 177 Gravity.apply(mDefaultStackGravity, mDefaultStackSize.getWidth(), 178 mDefaultStackSize.getHeight(), insetBounds, 0, 0, defaultBounds); 179 return defaultBounds; 180 } 181 182 /** 183 * @return the movement bounds for the given {@param stackBounds} and the current state of the 184 * controller. 185 */ 186 Rect getMovementBounds(Rect stackBounds) { 187 return getMovementBounds(stackBounds, true /* adjustForIme */); 188 } 189 190 /** 191 * @return the movement bounds for the given {@param stackBounds} and the current state of the 192 * controller. 193 */ 194 Rect getMovementBounds(Rect stackBounds, boolean adjustForIme) { 195 final Rect movementBounds = new Rect(); 196 getInsetBounds(movementBounds); 197 198 // Adjust the right/bottom to ensure the stack bounds never goes offscreen 199 movementBounds.right = Math.max(movementBounds.left, movementBounds.right - 200 stackBounds.width()); 201 movementBounds.bottom = Math.max(movementBounds.top, movementBounds.bottom - 202 stackBounds.height()); 203 204 // Apply the movement bounds adjustments based on the current state 205 if (adjustForIme) { 206 if (mIsImeShowing) { 207 movementBounds.bottom -= mImeHeight; 208 } 209 } 210 return movementBounds; 211 } 212 213 /** 214 * @return the repositioned PIP bounds given it's pre-change bounds, and the new display info. 215 */ 216 Rect onDisplayChanged(Rect preChangeStackBounds, DisplayInfo displayInfo) { 217 final Rect postChangeStackBounds = new Rect(preChangeStackBounds); 218 if (!mDisplayInfo.equals(displayInfo)) { 219 // Calculate the snap fraction of the current stack along the old movement bounds, and 220 // then update the stack bounds to the same fraction along the rotated movement bounds. 221 final Rect preChangeMovementBounds = getMovementBounds(preChangeStackBounds); 222 final float snapFraction = mSnapAlgorithm.getSnapFraction(preChangeStackBounds, 223 preChangeMovementBounds); 224 mDisplayInfo.copyFrom(displayInfo); 225 226 final Rect postChangeMovementBounds = getMovementBounds(preChangeStackBounds, 227 false /* adjustForIme */); 228 mSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds, 229 snapFraction); 230 } 231 return postChangeStackBounds; 232 } 233 234 /** 235 * Sets the Ime state and height. 236 */ 237 void setAdjustedForIme(boolean adjustedForIme, int imeHeight) { 238 // Return early if there is no state change 239 if (mIsImeShowing == adjustedForIme && mImeHeight == imeHeight) { 240 return; 241 } 242 243 final Rect stackBounds = new Rect(); 244 mService.getStackBounds(PINNED_STACK_ID, stackBounds); 245 final Rect prevMovementBounds = getMovementBounds(stackBounds); 246 mIsImeShowing = adjustedForIme; 247 mImeHeight = imeHeight; 248 if (mInInteractiveMode) { 249 // If the user is currently interacting with the PIP and the ime state changes, then 250 // don't adjust the bounds and defer that to after the interaction 251 notifyBoundsChanged(adjustedForIme /* adjustedForIme */); 252 } else { 253 // Otherwise, we can move the PIP to a sane location to ensure that it does not block 254 // the user from interacting with the IME 255 final Rect movementBounds = getMovementBounds(stackBounds); 256 final Rect toBounds = new Rect(stackBounds); 257 if (adjustedForIme) { 258 // IME visible 259 toBounds.offset(0, Math.min(0, movementBounds.bottom - stackBounds.top)); 260 } else { 261 // IME hidden 262 if (stackBounds.top == prevMovementBounds.bottom) { 263 // If the PIP is resting on top of the IME, then adjust it with the hiding IME 264 toBounds.offsetTo(toBounds.left, movementBounds.bottom); 265 } 266 } 267 if (!toBounds.equals(stackBounds)) { 268 if (mBoundsAnimator != null) { 269 mBoundsAnimator.cancel(); 270 } 271 mBoundsAnimator = mMotionHelper.createAnimationToBounds(stackBounds, toBounds); 272 mBoundsAnimator.start(); 273 } 274 } 275 } 276 277 /** 278 * Sends a broadcast that the PIP movement bounds have changed. 279 */ 280 private void notifyBoundsChanged(boolean adjustedForIme) { 281 if (mPinnedStackListener != null) { 282 try { 283 mPinnedStackListener.onBoundsChanged(adjustedForIme); 284 } catch (RemoteException e) { 285 Slog.e(TAG_WM, "Error delivering bounds changed event.", e); 286 } 287 } 288 } 289 290 /** 291 * @return the bounds on the screen that the PIP can be visible in. 292 */ 293 private void getInsetBounds(Rect outRect) { 294 mService.mPolicy.getStableInsetsLw(mDisplayInfo.rotation, mDisplayInfo.logicalWidth, 295 mDisplayInfo.logicalHeight, mTmpInsets); 296 outRect.set(mTmpInsets.left + mScreenEdgeInsets.x, mTmpInsets.top + mScreenEdgeInsets.y, 297 mDisplayInfo.logicalWidth - mTmpInsets.right - mScreenEdgeInsets.x, 298 mDisplayInfo.logicalHeight - mTmpInsets.bottom - mScreenEdgeInsets.y); 299 } 300 301 /** 302 * @return the pixels for a given dp value. 303 */ 304 private int dpToPx(float dpValue, DisplayMetrics dm) { 305 return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm); 306 } 307 308 void dump(String prefix, PrintWriter pw) { 309 pw.println(prefix + "PinnedStackController"); 310 pw.print(prefix + " defaultBounds="); getDefaultBounds().printShortString(pw); 311 pw.println(); 312 mService.getStackBounds(PINNED_STACK_ID, mTmpRect); 313 pw.print(prefix + " movementBounds="); getMovementBounds(mTmpRect).printShortString(pw); 314 pw.println(); 315 pw.println(prefix + " mIsImeShowing=" + mIsImeShowing); 316 pw.println(prefix + " mInInteractiveMode=" + mInInteractiveMode); 317 } 318} 319