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