blob: 21b042745fd034f72ab87750d6e121a9bb530c65 [file] [log] [blame]
Winson Chung655332c2016-10-31 13:14:28 -07001/*
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;
Jorim Jaggiad5d2842016-11-01 18:22:53 -070021import static android.view.Display.DEFAULT_DISPLAY;
Winson Chung655332c2016-10-31 13:14:28 -070022
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.Display;
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 final Rect mPreImeShowingBounds = new Rect();
73 private ValueAnimator mBoundsAnimator = null;
74
75 // The size and position information that describes where the pinned stack will go by default.
76 private int mDefaultStackGravity;
77 private Size mDefaultStackSize;
78 private Point mScreenEdgeInsets;
79
80 // Temp vars for calculation
81 private final DisplayMetrics mTmpMetrics = new DisplayMetrics();
82 private final Rect mTmpInsets = new Rect();
Jorim Jaggiad5d2842016-11-01 18:22:53 -070083 private final Rect mTmpRect = new Rect();
Winson Chung655332c2016-10-31 13:14:28 -070084
85 /**
86 * The callback object passed to listeners for them to notify the controller of state changes.
87 */
88 private class PinnedStackControllerCallback extends IPinnedStackController.Stub {
89
90 @Override
91 public void setInInteractiveMode(final boolean inInteractiveMode) {
92 mHandler.post(() -> {
93 // Cancel any existing animations on the PIP once the user starts dragging it
94 if (mBoundsAnimator != null && inInteractiveMode) {
95 mBoundsAnimator.cancel();
96 }
97 mInInteractiveMode = inInteractiveMode;
98 mPreImeShowingBounds.setEmpty();
99 });
100 }
101 }
102
103 /**
104 * Handler for the case where the listener dies.
105 */
106 private class PinnedStackListenerDeathHandler implements IBinder.DeathRecipient {
107
108 @Override
109 public void binderDied() {
110 // Clean up the state if the listener dies
111 mInInteractiveMode = false;
112 mPinnedStackListener = null;
113 }
114 }
115
116 PinnedStackController(WindowManagerService service, DisplayContent displayContent) {
117 mService = service;
118 mDisplayContent = displayContent;
119 mSnapAlgorithm = new PipSnapAlgorithm(service.mContext);
120 mMotionHelper = new PipMotionHelper(BackgroundThread.getHandler());
121 reloadResources();
122 }
123
124 void onConfigurationChanged() {
125 reloadResources();
126 }
127
128 /**
129 * Reloads all the resources for the current configuration.
130 */
131 void reloadResources() {
132 final Resources res = mService.mContext.getResources();
133 final Size defaultSizeDp = Size.parseSize(res.getString(
134 com.android.internal.R.string.config_defaultPictureInPictureSize));
135 final Size screenEdgeInsetsDp = Size.parseSize(res.getString(
136 com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets));
137 mDefaultStackGravity = res.getInteger(
138 com.android.internal.R.integer.config_defaultPictureInPictureGravity);
139 mDisplayContent.getDisplay().getRealMetrics(mTmpMetrics);
140 mDefaultStackSize = new Size(dpToPx(defaultSizeDp.getWidth(), mTmpMetrics),
141 dpToPx(defaultSizeDp.getHeight(), mTmpMetrics));
142 mScreenEdgeInsets = new Point(dpToPx(screenEdgeInsetsDp.getWidth(), mTmpMetrics),
143 dpToPx(screenEdgeInsetsDp.getHeight(), mTmpMetrics));
144 }
145
146 /**
147 * Registers a pinned stack listener.
148 */
149 void registerPinnedStackListener(IPinnedStackListener listener) {
150 try {
151 listener.asBinder().linkToDeath(mPinnedStackListenerDeathHandler, 0);
152 listener.onListenerRegistered(mCallbacks);
153 mPinnedStackListener = listener;
154 notifyBoundsChanged(mIsImeShowing);
155 } catch (RemoteException e) {
156 Log.e(TAG, "Failed to register pinned stack listener", e);
157 }
158 }
159
160 /**
161 * @return the default bounds to show the PIP when there is no active PIP.
162 */
163 Rect getDefaultBounds() {
164 final Display display = mDisplayContent.getDisplay();
165 final Rect insetBounds = new Rect();
166 final Point displaySize = new Point();
167 display.getRealSize(displaySize);
168 mService.getStableInsetsLocked(mDisplayContent.getDisplayId(), mTmpInsets);
169 getInsetBounds(displaySize, mTmpInsets, insetBounds);
170
171 final Rect defaultBounds = new Rect();
172 Gravity.apply(mDefaultStackGravity, mDefaultStackSize.getWidth(),
173 mDefaultStackSize.getHeight(), insetBounds, 0, 0, defaultBounds);
174 return defaultBounds;
175 }
176
177 /**
178 * @return the movement bounds for the given {@param stackBounds} and the current state of the
179 * controller.
180 */
181 Rect getMovementBounds(Rect stackBounds) {
182 final Display display = mDisplayContent.getDisplay();
183 final Rect movementBounds = new Rect();
184 final Point displaySize = new Point();
185 display.getRealSize(displaySize);
186 mService.getStableInsetsLocked(mDisplayContent.getDisplayId(), mTmpInsets);
187 getInsetBounds(displaySize, mTmpInsets, movementBounds);
188
189 // Adjust the right/bottom to ensure the stack bounds never goes offscreen
190 movementBounds.right = Math.max(movementBounds.left, movementBounds.right -
191 stackBounds.width());
192 movementBounds.bottom = Math.max(movementBounds.top, movementBounds.bottom -
193 stackBounds.height());
194
195 // Adjust the top if the ime is open
196 if (mIsImeShowing) {
197 movementBounds.bottom -= mImeHeight;
198 }
199
200 return movementBounds;
201 }
202
203 /**
204 * @return the PIP bounds given it's bounds pre-rotation, and post-rotation (with as applied
205 * by the display content, which currently transposes the dimensions but keeps each stack in
206 * the same physical space on the device).
207 */
208 Rect getPostRotationBounds(Rect preRotationStackBounds, Rect postRotationStackBounds) {
209 // Keep the pinned stack in the same aspect ratio as in the old orientation, but
210 // move it into the position in the rotated space, and snap to the closest space
211 // in the new orientation.
212 final Rect movementBounds = getMovementBounds(preRotationStackBounds);
213 final int stackWidth = preRotationStackBounds.width();
214 final int stackHeight = preRotationStackBounds.height();
215 final int left = postRotationStackBounds.centerX() - (stackWidth / 2);
216 final int top = postRotationStackBounds.centerY() - (stackHeight / 2);
217 final Rect postRotBounds = new Rect(left, top, left + stackWidth, top + stackHeight);
218 return mSnapAlgorithm.findClosestSnapBounds(movementBounds, postRotBounds);
219 }
220
221 /**
222 * Sets the Ime state and height.
223 */
224 void setAdjustedForIme(boolean adjustedForIme, int imeHeight) {
225 // Return early if there is no state change
226 if (mIsImeShowing == adjustedForIme && mImeHeight == imeHeight) {
227 return;
228 }
229
230 final Rect stackBounds = new Rect();
231 mService.getStackBounds(PINNED_STACK_ID, stackBounds);
232 final Rect prevMovementBounds = getMovementBounds(stackBounds);
233 final boolean wasAdjustedForIme = mIsImeShowing;
234 mIsImeShowing = adjustedForIme;
235 mImeHeight = imeHeight;
236 if (mInInteractiveMode) {
237 // If the user is currently interacting with the PIP and the ime state changes, then
238 // don't adjust the bounds and defer that to after the interaction
239 notifyBoundsChanged(adjustedForIme /* adjustedForIme */);
240 } else {
241 // Otherwise, we can move the PIP to a sane location to ensure that it does not block
242 // the user from interacting with the IME
243 Rect toBounds;
244 if (!wasAdjustedForIme && adjustedForIme) {
245 // If we are showing the IME, then store the previous bounds
246 mPreImeShowingBounds.set(stackBounds);
247 toBounds = adjustBoundsInMovementBounds(stackBounds);
248 } else if (wasAdjustedForIme && !adjustedForIme) {
249 if (!mPreImeShowingBounds.isEmpty()) {
250 // If we are hiding the IME and the user is not interacting with the PIP, restore
251 // the previous bounds
252 toBounds = mPreImeShowingBounds;
253 } else {
254 if (stackBounds.top == prevMovementBounds.bottom) {
255 // If the PIP is resting on top of the IME, then adjust it with the hiding
256 // of the IME
257 final Rect movementBounds = getMovementBounds(stackBounds);
258 toBounds = new Rect(stackBounds);
259 toBounds.offsetTo(toBounds.left, movementBounds.bottom);
260 } else {
261 // Otherwise, leave the PIP in place
262 toBounds = stackBounds;
263 }
264 }
265 } else {
266 // Otherwise, the IME bounds have changed so we need to adjust the PIP bounds also
267 toBounds = adjustBoundsInMovementBounds(stackBounds);
268 }
269 if (!toBounds.equals(stackBounds)) {
270 if (mBoundsAnimator != null) {
271 mBoundsAnimator.cancel();
272 }
273 mBoundsAnimator = mMotionHelper.createAnimationToBounds(stackBounds, toBounds);
274 mBoundsAnimator.start();
275 }
276 }
277 }
278
279 /**
280 * @return the adjusted {@param stackBounds} such that they are in the movement bounds.
281 */
282 private Rect adjustBoundsInMovementBounds(Rect stackBounds) {
283 final Rect movementBounds = getMovementBounds(stackBounds);
284 final Rect adjustedBounds = new Rect(stackBounds);
285 adjustedBounds.offset(0, Math.min(0, movementBounds.bottom - stackBounds.top));
286 return adjustedBounds;
287 }
288
289 /**
290 * Sends a broadcast that the PIP movement bounds have changed.
291 */
292 private void notifyBoundsChanged(boolean adjustedForIme) {
293 if (mPinnedStackListener != null) {
294 try {
295 mPinnedStackListener.onBoundsChanged(adjustedForIme);
296 } catch (RemoteException e) {
297 Slog.e(TAG_WM, "Error delivering bounds changed event.", e);
298 }
299 }
300 }
301
302 /**
303 * @return the bounds on the screen that the PIP can be visible in.
304 */
305 private void getInsetBounds(Point displaySize, Rect insets, Rect outRect) {
306 outRect.set(insets.left + mScreenEdgeInsets.x, insets.top + mScreenEdgeInsets.y,
307 displaySize.x - insets.right - mScreenEdgeInsets.x,
308 displaySize.y - insets.bottom - mScreenEdgeInsets.y);
309 }
310
311 /**
312 * @return the pixels for a given dp value.
313 */
314 private int dpToPx(float dpValue, DisplayMetrics dm) {
315 return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm);
316 }
317
318 void dump(String prefix, PrintWriter pw) {
319 pw.println(prefix + "PinnedStackController");
Jorim Jaggiad5d2842016-11-01 18:22:53 -0700320 pw.print(prefix + " defaultBounds="); getDefaultBounds().printShortString(pw);
321 pw.println();
322 mService.getStackBounds(PINNED_STACK_ID, mTmpRect);
323 pw.print(prefix + " movementBounds="); getMovementBounds(mTmpRect).printShortString(pw);
324 pw.println();
Winson Chung655332c2016-10-31 13:14:28 -0700325 pw.println(prefix + " mIsImeShowing=" + mIsImeShowing);
326 pw.println(prefix + " mInInteractiveMode=" + mInInteractiveMode);
327 }
328}