blob: f3050a903c70827822dd29b5a2175ca95336ad67 [file] [log] [blame]
Filip Gruszczynskie5390e72015-08-18 16:39:00 -07001/*
2 * Copyright (C) 2015 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
Wale Ogunwale59507092018-10-29 09:00:30 -070017package com.android.server.wm;
Filip Gruszczynskie5390e72015-08-18 16:39:00 -070018
Garfield Tanb5cc09f2018-09-28 10:06:52 -070019import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
20import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
21import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
22import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
23import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
24import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
25import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
26import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
27import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
28import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
29import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
30import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT;
31import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
32import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE;
33import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT;
34import static android.util.DisplayMetrics.DENSITY_DEFAULT;
35import static android.view.Display.DEFAULT_DISPLAY;
36import static android.view.Display.INVALID_DISPLAY;
37
Wale Ogunwale59507092018-10-29 09:00:30 -070038import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
39import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -070040
Garfield Tanb5cc09f2018-09-28 10:06:52 -070041import android.annotation.NonNull;
42import android.annotation.Nullable;
Bryce Leedacefc42017-10-10 12:56:02 -070043import android.app.ActivityOptions;
Garfield Tanb5cc09f2018-09-28 10:06:52 -070044import android.app.WindowConfiguration;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -070045import android.content.pm.ActivityInfo;
Garfield Tanb5cc09f2018-09-28 10:06:52 -070046import android.content.pm.ApplicationInfo;
Garfield Tanbb0270f2018-12-05 11:30:27 -080047import android.content.res.Configuration;
Filip Gruszczynskie5390e72015-08-18 16:39:00 -070048import android.graphics.Rect;
Garfield Tanb5cc09f2018-09-28 10:06:52 -070049import android.os.Build;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -070050import android.util.Slog;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -070051import android.view.Gravity;
Garfield Tan891146c2018-10-09 12:14:00 -070052import android.view.View;
Garfield Tanb5cc09f2018-09-28 10:06:52 -070053
Louis Chang6fb1e842018-12-03 16:07:50 +080054import com.android.internal.annotations.VisibleForTesting;
Wale Ogunwale59507092018-10-29 09:00:30 -070055import com.android.server.wm.LaunchParamsController.LaunchParams;
56import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
Filip Gruszczynskie5390e72015-08-18 16:39:00 -070057
58import java.util.ArrayList;
Garfield Tanb5cc09f2018-09-28 10:06:52 -070059import java.util.List;
Filip Gruszczynskie5390e72015-08-18 16:39:00 -070060
61/**
Garfield Tanb5cc09f2018-09-28 10:06:52 -070062 * The class that defines the default launch params for tasks.
Filip Gruszczynskie5390e72015-08-18 16:39:00 -070063 */
Bryce Leeec55eb02017-12-05 20:51:27 -080064class TaskLaunchParamsModifier implements LaunchParamsModifier {
Wale Ogunwale98875612018-10-12 07:53:02 -070065 private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskLaunchParamsModifier" : TAG_ATM;
Garfield Tanb5cc09f2018-09-28 10:06:52 -070066 private static final boolean DEBUG = false;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -070067
Garfield Tanb5cc09f2018-09-28 10:06:52 -070068 // A mask for SUPPORTS_SCREEN that indicates the activity supports resize.
69 private static final int SUPPORTS_SCREEN_RESIZEABLE_MASK =
70 ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES
71 | ApplicationInfo.FLAG_SUPPORTS_LARGE_SCREENS
72 | ApplicationInfo.FLAG_SUPPORTS_SMALL_SCREENS
73 | ApplicationInfo.FLAG_RESIZEABLE_FOR_SCREENS
74 | ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES
75 | ApplicationInfo.FLAG_SUPPORTS_XLARGE_SCREENS;
Filip Gruszczynskie5390e72015-08-18 16:39:00 -070076
Garfield Tanb5cc09f2018-09-28 10:06:52 -070077 // Screen size of Nexus 5x
78 private static final int DEFAULT_PORTRAIT_PHONE_WIDTH_DP = 412;
79 private static final int DEFAULT_PORTRAIT_PHONE_HEIGHT_DP = 732;
Filip Gruszczynskie5390e72015-08-18 16:39:00 -070080
Garfield Tanb5cc09f2018-09-28 10:06:52 -070081 // Allowance of size matching.
82 private static final int EPSILON = 2;
Filip Gruszczynskie5390e72015-08-18 16:39:00 -070083
Garfield Tanb5cc09f2018-09-28 10:06:52 -070084 // Cascade window offset.
85 private static final int CASCADING_OFFSET_DP = 75;
86
87 // Threshold how close window corners have to be to call them colliding.
88 private static final int BOUNDS_CONFLICT_THRESHOLD = 4;
89
90 // Divide display size by this number to get each step to adjust bounds to avoid conflict.
Filip Gruszczynskie5390e72015-08-18 16:39:00 -070091 private static final int STEP_DENOMINATOR = 16;
92
93 // We always want to step by at least this.
94 private static final int MINIMAL_STEP = 1;
95
Garfield Tanb5cc09f2018-09-28 10:06:52 -070096 private final ActivityStackSupervisor mSupervisor;
97 private final Rect mTmpBounds = new Rect();
98 private final int[] mTmpDirections = new int[2];
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -070099
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700100 private StringBuilder mLogBuilder;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700101
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700102 TaskLaunchParamsModifier(ActivityStackSupervisor supervisor) {
103 mSupervisor = supervisor;
104 }
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700105
Louis Chang6fb1e842018-12-03 16:07:50 +0800106 @VisibleForTesting
107 int onCalculate(TaskRecord task, ActivityInfo.WindowLayout layout, ActivityRecord activity,
108 ActivityRecord source, ActivityOptions options, LaunchParams currentParams,
109 LaunchParams outParams) {
110 return onCalculate(task, layout, activity, source, options, PHASE_BOUNDS, currentParams,
111 outParams);
112 }
113
Bryce Leedacefc42017-10-10 12:56:02 -0700114 @Override
Bryce Leeec55eb02017-12-05 20:51:27 -0800115 public int onCalculate(TaskRecord task, ActivityInfo.WindowLayout layout,
116 ActivityRecord activity, ActivityRecord source, ActivityOptions options,
Louis Chang6fb1e842018-12-03 16:07:50 +0800117 int phase, LaunchParams currentParams, LaunchParams outParams) {
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700118 initLogBuilder(task, activity);
Louis Chang6fb1e842018-12-03 16:07:50 +0800119 final int result = calculate(task, layout, activity, source, options, phase, currentParams,
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700120 outParams);
121 outputLog();
122 return result;
123 }
124
125 private int calculate(TaskRecord task, ActivityInfo.WindowLayout layout,
Louis Chang6fb1e842018-12-03 16:07:50 +0800126 ActivityRecord activity, ActivityRecord source, ActivityOptions options, int phase,
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700127 LaunchParams currentParams, LaunchParams outParams) {
Garfield Tan21d7e172018-10-16 18:07:27 -0700128 final ActivityRecord root;
129 if (task != null) {
130 root = task.getRootActivity() == null ? activity : task.getRootActivity();
131 } else {
132 root = activity;
133 }
134
135 // TODO: Investigate whether we can safely ignore all cases where we don't have root
136 // activity available. Note we can't know if the bounds are valid if we're not sure of the
137 // requested orientation of the root activity. Therefore if we found such a case we may need
138 // to pass the activity into this modifier in that case.
139 if (root == null) {
140 // There is a case that can lead us here. The caller is moving the top activity that is
141 // in a task that has multiple activities to PIP mode. For that the caller is creating a
142 // new task to host the activity so that we only move the top activity to PIP mode and
143 // keep other activities in the previous task. There is no point to apply the launch
144 // logic in this case.
145 return RESULT_SKIP;
146 }
147
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700148 // STEP 1: Determine the display to launch the activity/task.
Louis Chang40750092018-10-24 21:04:51 +0800149 final int displayId = getPreferredLaunchDisplay(task, options, source, currentParams);
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700150 outParams.mPreferredDisplayId = displayId;
Wale Ogunwaled32da472018-11-16 07:19:28 -0800151 ActivityDisplay display = mSupervisor.mRootActivityContainer.getActivityDisplay(displayId);
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700152 if (DEBUG) {
153 appendLog("display-id=" + outParams.mPreferredDisplayId + " display-windowing-mode="
154 + display.getWindowingMode());
Bryce Leedacefc42017-10-10 12:56:02 -0700155 }
156
Louis Chang6fb1e842018-12-03 16:07:50 +0800157 if (phase == PHASE_DISPLAY) {
158 return RESULT_CONTINUE;
159 }
160
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700161 // STEP 2: Resolve launch windowing mode.
162 // STEP 2.1: Determine if any parameter has specified initial bounds. That might be the
Garfield Tan706dbcb2018-10-15 11:33:02 -0700163 // launch bounds from activity options, or size/gravity passed in layout. It also treats the
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700164 // launch windowing mode in options as a suggestion for future resolution.
165 int launchMode = options != null ? options.getLaunchWindowingMode()
166 : WINDOWING_MODE_UNDEFINED;
167 // hasInitialBounds is set if either activity options or layout has specified bounds. If
168 // that's set we'll skip some adjustments later to avoid overriding the initial bounds.
169 boolean hasInitialBounds = false;
Garfield Tan706dbcb2018-10-15 11:33:02 -0700170 final boolean canApplyFreeformPolicy = canApplyFreeformWindowPolicy(display, launchMode);
171 if (mSupervisor.canUseActivityOptionsLaunchBounds(options)
172 && (canApplyFreeformPolicy || canApplyPipWindowPolicy(launchMode))) {
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700173 hasInitialBounds = true;
174 launchMode = launchMode == WINDOWING_MODE_UNDEFINED
175 ? WINDOWING_MODE_FREEFORM
176 : launchMode;
177 outParams.mBounds.set(options.getLaunchBounds());
178 if (DEBUG) appendLog("activity-options-bounds=" + outParams.mBounds);
179 } else if (launchMode == WINDOWING_MODE_PINNED) {
180 // System controls PIP window's bounds, so don't apply launch bounds.
181 if (DEBUG) appendLog("empty-window-layout-for-pip");
182 } else if (launchMode == WINDOWING_MODE_FULLSCREEN) {
183 if (DEBUG) appendLog("activity-options-fullscreen=" + outParams.mBounds);
184 } else if (layout != null && canApplyFreeformPolicy) {
185 getLayoutBounds(display, root, layout, mTmpBounds);
186 if (!mTmpBounds.isEmpty()) {
187 launchMode = WINDOWING_MODE_FREEFORM;
188 outParams.mBounds.set(mTmpBounds);
189 hasInitialBounds = true;
190 if (DEBUG) appendLog("bounds-from-layout=" + outParams.mBounds);
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700191 } else {
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700192 if (DEBUG) appendLog("empty-window-layout");
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700193 }
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700194 }
195
196 // STEP 2.2: Check if previous modifier or the controller (referred as "callers" below) has
197 // some opinions on launch mode and launch bounds. If they have opinions and there is no
198 // initial bounds set in parameters. Note the check on display ID is also input param
199 // related because we always defer to callers' suggestion if there is no specific display ID
200 // in options or from source activity.
201 //
202 // If opinions from callers don't need any further resolution, we try to honor that as is as
203 // much as possible later.
204
205 // Flag to indicate if current param needs no further resolution. It's true it current
206 // param isn't freeform mode, or it already has launch bounds.
207 boolean fullyResolvedCurrentParam = false;
208 // We inherit launch params from previous modifiers or LaunchParamsController if options,
209 // layout and display conditions are not contradictory to their suggestions. It's important
210 // to carry over their values because LaunchParamsController doesn't automatically do that.
211 if (!currentParams.isEmpty() && !hasInitialBounds
212 && (!currentParams.hasPreferredDisplay()
213 || displayId == currentParams.mPreferredDisplayId)) {
214 if (currentParams.hasWindowingMode()) {
215 launchMode = currentParams.mWindowingMode;
Garfield Tan891146c2018-10-09 12:14:00 -0700216 fullyResolvedCurrentParam = launchMode != WINDOWING_MODE_FREEFORM;
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700217 if (DEBUG) {
218 appendLog("inherit-" + WindowConfiguration.windowingModeToString(launchMode));
219 }
220 }
221
Garfield Tand5972d12019-01-03 10:31:59 -0800222 if (!currentParams.mBounds.isEmpty()) {
223 // Carry over bounds from callers regardless of launch mode because bounds is still
224 // used to restore last non-fullscreen bounds when launch mode is not freeform.
225 // Therefore it's not a resolution step for non-freeform launch mode and only
226 // consider it fully resolved only when launch mode is freeform.
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700227 outParams.mBounds.set(currentParams.mBounds);
Garfield Tand5972d12019-01-03 10:31:59 -0800228 if (launchMode == WINDOWING_MODE_FREEFORM) {
229 fullyResolvedCurrentParam = true;
230 if (DEBUG) appendLog("inherit-bounds=" + outParams.mBounds);
231 }
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700232 }
233 }
234
235 // STEP 2.3: Adjust launch parameters as needed for freeform display. We enforce the policy
236 // that legacy (pre-D) apps and those apps that can't handle multiple screen density well
237 // are forced to be maximized. The rest of this step is to define the default policy when
238 // there is no initial bounds or a fully resolved current params from callers. Right now we
239 // launch all possible tasks/activities that can handle freeform into freeform mode.
240 if (display.inFreeformWindowingMode()) {
241 if (launchMode == WINDOWING_MODE_PINNED) {
242 if (DEBUG) appendLog("picture-in-picture");
243 } else if (isTaskForcedMaximized(root)) {
244 // We're launching an activity that probably can't handle resizing nicely, so force
245 // it to be maximized even someone suggests launching it in freeform using launch
246 // options.
247 launchMode = WINDOWING_MODE_FULLSCREEN;
248 outParams.mBounds.setEmpty();
249 if (DEBUG) appendLog("forced-maximize");
250 } else if (fullyResolvedCurrentParam) {
251 // Don't adjust launch mode if that's inherited, except when we're launching an
252 // activity that should be forced to maximize.
253 if (DEBUG) appendLog("skip-adjustment-fully-resolved-params");
254 } else if (launchMode != WINDOWING_MODE_FREEFORM
255 && (isNOrGreater(root) || isPreNResizeable(root))) {
256 // We're launching a pre-N and post-D activity that supports resizing, or a post-N
257 // activity. They can handle freeform nicely so launch them in freeform.
258 // Use undefined because we know we're in a freeform display.
259 launchMode = WINDOWING_MODE_UNDEFINED;
260 if (DEBUG) appendLog("should-be-freeform");
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700261 }
262 } else {
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700263 if (DEBUG) appendLog("non-freeform-display");
264 }
265 // If launch mode matches display windowing mode, let it inherit from display.
266 outParams.mWindowingMode = launchMode == display.getWindowingMode()
267 ? WINDOWING_MODE_UNDEFINED : launchMode;
268
Louis Chang6fb1e842018-12-03 16:07:50 +0800269 if (phase == PHASE_WINDOWING_MODE) {
270 return RESULT_CONTINUE;
271 }
272
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700273 // STEP 3: Determine final launch bounds based on resolved windowing mode and activity
274 // requested orientation. We set bounds to empty for fullscreen mode and keep bounds as is
275 // for all other windowing modes that's not freeform mode. One can read comments in
276 // relevant methods to further understand this step.
277 //
Garfield Tan891146c2018-10-09 12:14:00 -0700278 // We skip making adjustments if the params are fully resolved from previous results.
279 final int resolvedMode = (launchMode != WINDOWING_MODE_UNDEFINED) ? launchMode
280 : display.getWindowingMode();
281 if (fullyResolvedCurrentParam) {
282 if (resolvedMode == WINDOWING_MODE_FREEFORM) {
283 // Make sure bounds are in the display if it's possibly in a different display.
284 if (currentParams.mPreferredDisplayId != displayId) {
285 adjustBoundsToFitInDisplay(display, outParams.mBounds);
286 }
287 // Even though we want to keep original bounds, we still don't want it to stomp on
288 // an existing task.
289 adjustBoundsToAvoidConflict(display, outParams.mBounds);
290 }
291 } else {
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700292 if (source != null && source.inFreeformWindowingMode()
293 && resolvedMode == WINDOWING_MODE_FREEFORM
294 && outParams.mBounds.isEmpty()
295 && source.getDisplayId() == display.mDisplayId) {
296 // Set bounds to be not very far from source activity.
297 cascadeBounds(source.getBounds(), display, outParams.mBounds);
298 }
299 getTaskBounds(root, display, layout, resolvedMode, hasInitialBounds, outParams.mBounds);
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700300 }
Bryce Leedacefc42017-10-10 12:56:02 -0700301
302 return RESULT_CONTINUE;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700303 }
304
Louis Chang40750092018-10-24 21:04:51 +0800305 private int getPreferredLaunchDisplay(@Nullable TaskRecord task,
306 @Nullable ActivityOptions options, ActivityRecord source, LaunchParams currentParams) {
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700307 int displayId = INVALID_DISPLAY;
308 final int optionLaunchId = options != null ? options.getLaunchDisplayId() : INVALID_DISPLAY;
309 if (optionLaunchId != INVALID_DISPLAY) {
310 if (DEBUG) appendLog("display-from-option=" + optionLaunchId);
311 displayId = optionLaunchId;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700312 }
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700313
Louis Changd58cb672018-12-24 17:45:16 +0800314 // If the source activity is a no-display activity, pass on the launch display id from
315 // source activity as currently preferred.
316 if (displayId == INVALID_DISPLAY && source != null && source.noDisplay) {
317 displayId = source.mHandoverLaunchDisplayId;
318 if (DEBUG) appendLog("display-from-no-display-source=" + displayId);
319 }
320
Louis Chang40750092018-10-24 21:04:51 +0800321 ActivityStack stack =
322 (displayId == INVALID_DISPLAY && task != null) ? task.getStack() : null;
323 if (stack != null) {
324 if (DEBUG) appendLog("display-from-task=" + stack.mDisplayId);
325 displayId = stack.mDisplayId;
326 }
327
Louis Chang6fb1e842018-12-03 16:07:50 +0800328 if (displayId == INVALID_DISPLAY && source != null) {
329 final int sourceDisplayId = source.getDisplayId();
330 if (DEBUG) appendLog("display-from-source=" + sourceDisplayId);
331 displayId = sourceDisplayId;
332 }
333
Wale Ogunwaled32da472018-11-16 07:19:28 -0800334 if (displayId != INVALID_DISPLAY
335 && mSupervisor.mRootActivityContainer.getActivityDisplay(displayId) == null) {
Garfield Tan891146c2018-10-09 12:14:00 -0700336 displayId = currentParams.mPreferredDisplayId;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700337 }
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700338 displayId = (displayId == INVALID_DISPLAY) ? currentParams.mPreferredDisplayId : displayId;
339
Wale Ogunwaled32da472018-11-16 07:19:28 -0800340 return (displayId != INVALID_DISPLAY
341 && mSupervisor.mRootActivityContainer.getActivityDisplay(displayId) != null)
Garfield Tan891146c2018-10-09 12:14:00 -0700342 ? displayId : DEFAULT_DISPLAY;
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700343 }
344
Garfield Tan706dbcb2018-10-15 11:33:02 -0700345 private boolean canApplyFreeformWindowPolicy(@NonNull ActivityDisplay display, int launchMode) {
346 return mSupervisor.mService.mSupportsFreeformWindowManagement
347 && (display.inFreeformWindowingMode() || launchMode == WINDOWING_MODE_FREEFORM);
348 }
349
350 private boolean canApplyPipWindowPolicy(int launchMode) {
351 return mSupervisor.mService.mSupportsPictureInPicture
352 && launchMode == WINDOWING_MODE_PINNED;
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700353 }
354
355 private void getLayoutBounds(@NonNull ActivityDisplay display, @NonNull ActivityRecord root,
356 @NonNull ActivityInfo.WindowLayout windowLayout, @NonNull Rect outBounds) {
357 final int verticalGravity = windowLayout.gravity & Gravity.VERTICAL_GRAVITY_MASK;
358 final int horizontalGravity = windowLayout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
359 if (!windowLayout.hasSpecifiedSize() && verticalGravity == 0 && horizontalGravity == 0) {
360 outBounds.setEmpty();
361 return;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700362 }
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700363
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700364 final Rect bounds = display.getBounds();
365 final int defaultWidth = bounds.width();
366 final int defaultHeight = bounds.height();
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700367
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700368 int width;
369 int height;
370 if (!windowLayout.hasSpecifiedSize()) {
371 outBounds.setEmpty();
372 getTaskBounds(root, display, windowLayout, WINDOWING_MODE_FREEFORM,
373 /* hasInitialBounds */ false, outBounds);
374 width = outBounds.width();
375 height = outBounds.height();
376 } else {
377 width = defaultWidth;
378 if (windowLayout.width > 0 && windowLayout.width < defaultWidth) {
379 width = windowLayout.width;
380 } else if (windowLayout.widthFraction > 0 && windowLayout.widthFraction < 1.0f) {
381 width = (int) (width * windowLayout.widthFraction);
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700382 }
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700383
384 height = defaultHeight;
385 if (windowLayout.height > 0 && windowLayout.height < defaultHeight) {
386 height = windowLayout.height;
387 } else if (windowLayout.heightFraction > 0 && windowLayout.heightFraction < 1.0f) {
388 height = (int) (height * windowLayout.heightFraction);
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700389 }
390 }
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700391
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700392 final float fractionOfHorizontalOffset;
393 switch (horizontalGravity) {
394 case Gravity.LEFT:
395 fractionOfHorizontalOffset = 0f;
396 break;
397 case Gravity.RIGHT:
398 fractionOfHorizontalOffset = 1f;
399 break;
400 default:
401 fractionOfHorizontalOffset = 0.5f;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700402 }
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700403
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700404 final float fractionOfVerticalOffset;
405 switch (verticalGravity) {
406 case Gravity.TOP:
407 fractionOfVerticalOffset = 0f;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700408 break;
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700409 case Gravity.BOTTOM:
410 fractionOfVerticalOffset = 1f;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700411 break;
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700412 default:
413 fractionOfVerticalOffset = 0.5f;
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700414 }
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700415
416 outBounds.set(0, 0, width, height);
417 final int xOffset = (int) (fractionOfHorizontalOffset * (defaultWidth - width));
418 final int yOffset = (int) (fractionOfVerticalOffset * (defaultHeight - height));
419 outBounds.offset(xOffset, yOffset);
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700420 }
421
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700422 /**
423 * Returns if task is forced to maximize.
424 *
425 * There are several cases where we force a task to maximize:
426 * 1) Root activity is targeting pre-Donut, which by default can't handle multiple screen
427 * densities, so resizing will likely cause issues;
428 * 2) Root activity doesn't declare any flag that it supports any screen density, so resizing
429 * may also cause issues;
430 * 3) Root activity is not resizeable, for which we shouldn't allow user resize it.
431 *
432 * @param root the root activity to check against.
433 * @return {@code true} if it should be forced to maximize; {@code false} otherwise.
434 */
435 private boolean isTaskForcedMaximized(@NonNull ActivityRecord root) {
436 if (root.appInfo.targetSdkVersion < Build.VERSION_CODES.DONUT
437 || (root.appInfo.flags & SUPPORTS_SCREEN_RESIZEABLE_MASK) == 0) {
438 return true;
439 }
440
441 return !root.isResizeable();
442 }
443
444 private boolean isNOrGreater(@NonNull ActivityRecord root) {
445 return root.appInfo.targetSdkVersion >= Build.VERSION_CODES.N;
446 }
447
448 /**
449 * Resolves activity requested orientation to 4 categories:
450 * 1) {@link ActivityInfo#SCREEN_ORIENTATION_LOCKED} indicating app wants to lock down
451 * orientation;
452 * 2) {@link ActivityInfo#SCREEN_ORIENTATION_LANDSCAPE} indicating app wants to be in landscape;
453 * 3) {@link ActivityInfo#SCREEN_ORIENTATION_PORTRAIT} indicating app wants to be in portrait;
454 * 4) {@link ActivityInfo#SCREEN_ORIENTATION_UNSPECIFIED} indicating app can handle any
455 * orientation.
456 *
457 * @param activity the activity to check
458 * @return corresponding resolved orientation value.
459 */
460 private int resolveOrientation(@NonNull ActivityRecord activity) {
461 int orientation = activity.info.screenOrientation;
462 switch (orientation) {
463 case SCREEN_ORIENTATION_NOSENSOR:
464 case SCREEN_ORIENTATION_LOCKED:
465 orientation = SCREEN_ORIENTATION_LOCKED;
466 break;
467 case SCREEN_ORIENTATION_SENSOR_LANDSCAPE:
468 case SCREEN_ORIENTATION_REVERSE_LANDSCAPE:
469 case SCREEN_ORIENTATION_USER_LANDSCAPE:
470 case SCREEN_ORIENTATION_LANDSCAPE:
471 if (DEBUG) appendLog("activity-requested-landscape");
472 orientation = SCREEN_ORIENTATION_LANDSCAPE;
473 break;
474 case SCREEN_ORIENTATION_SENSOR_PORTRAIT:
475 case SCREEN_ORIENTATION_REVERSE_PORTRAIT:
476 case SCREEN_ORIENTATION_USER_PORTRAIT:
477 case SCREEN_ORIENTATION_PORTRAIT:
478 if (DEBUG) appendLog("activity-requested-portrait");
479 orientation = SCREEN_ORIENTATION_PORTRAIT;
480 break;
481 default:
482 orientation = SCREEN_ORIENTATION_UNSPECIFIED;
483 }
484
485 return orientation;
486 }
487
488 private boolean isPreNResizeable(ActivityRecord root) {
489 return root.appInfo.targetSdkVersion < Build.VERSION_CODES.N && root.isResizeable();
490 }
491
492 private void cascadeBounds(@NonNull Rect srcBounds, @NonNull ActivityDisplay display,
493 @NonNull Rect outBounds) {
494 outBounds.set(srcBounds);
495 float density = (float) display.getConfiguration().densityDpi / DENSITY_DEFAULT;
496 final int defaultOffset = (int) (CASCADING_OFFSET_DP * density + 0.5f);
497
498 display.getBounds(mTmpBounds);
499 final int dx = Math.min(defaultOffset, Math.max(0, mTmpBounds.right - srcBounds.right));
500 final int dy = Math.min(defaultOffset, Math.max(0, mTmpBounds.bottom - srcBounds.bottom));
501 outBounds.offset(dx, dy);
502 }
503
504 private void getTaskBounds(@NonNull ActivityRecord root, @NonNull ActivityDisplay display,
505 @NonNull ActivityInfo.WindowLayout layout, int resolvedMode, boolean hasInitialBounds,
506 @NonNull Rect inOutBounds) {
507 if (resolvedMode == WINDOWING_MODE_FULLSCREEN) {
508 // We don't handle letterboxing here. Letterboxing will be handled by valid checks
509 // later.
510 inOutBounds.setEmpty();
511 if (DEBUG) appendLog("maximized-bounds");
512 return;
513 }
514
515 if (resolvedMode != WINDOWING_MODE_FREEFORM) {
516 // We don't apply freeform bounds adjustment to other windowing modes.
517 if (DEBUG) {
518 appendLog("skip-bounds-" + WindowConfiguration.windowingModeToString(resolvedMode));
519 }
520 return;
521 }
522
523 final int orientation = resolveOrientation(root, display, inOutBounds);
524 if (orientation != SCREEN_ORIENTATION_PORTRAIT
525 && orientation != SCREEN_ORIENTATION_LANDSCAPE) {
526 throw new IllegalStateException(
527 "Orientation must be one of portrait or landscape, but it's "
528 + ActivityInfo.screenOrientationToString(orientation));
529 }
530
531 // First we get the default size we want.
532 getDefaultFreeformSize(display, layout, orientation, mTmpBounds);
533 if (hasInitialBounds || sizeMatches(inOutBounds, mTmpBounds)) {
534 // We're here because either input parameters specified initial bounds, or the suggested
535 // bounds have the same size of the default freeform size. We should use the suggested
536 // bounds if possible -- so if app can handle the orientation we just use it, and if not
537 // we transpose the suggested bounds in-place.
538 if (orientation == orientationFromBounds(inOutBounds)) {
539 if (DEBUG) appendLog("freeform-size-orientation-match=" + inOutBounds);
540 } else {
541 // Meh, orientation doesn't match. Let's rotate inOutBounds in-place.
542 centerBounds(display, inOutBounds.height(), inOutBounds.width(), inOutBounds);
543 if (DEBUG) appendLog("freeform-orientation-mismatch=" + inOutBounds);
544 }
545 } else {
546 // We are here either because there is no suggested bounds, or the suggested bounds is
547 // a cascade from source activity. We should use the default freeform size and center it
548 // to the center of suggested bounds (or the display if no suggested bounds). The
549 // default size might be too big to center to source activity bounds in display, so we
550 // may need to move it back to the display.
551 centerBounds(display, mTmpBounds.width(), mTmpBounds.height(), inOutBounds);
552 adjustBoundsToFitInDisplay(display, inOutBounds);
553 if (DEBUG) appendLog("freeform-size-mismatch=" + inOutBounds);
554 }
555
556 // Lastly we adjust bounds to avoid conflicts with other tasks as much as possible.
557 adjustBoundsToAvoidConflict(display, inOutBounds);
558 }
559
Garfield Tanbb0270f2018-12-05 11:30:27 -0800560 private int convertOrientationToScreenOrientation(int orientation) {
561 switch (orientation) {
562 case Configuration.ORIENTATION_LANDSCAPE:
563 return SCREEN_ORIENTATION_LANDSCAPE;
564 case Configuration.ORIENTATION_PORTRAIT:
565 return SCREEN_ORIENTATION_PORTRAIT;
566 default:
567 return SCREEN_ORIENTATION_UNSPECIFIED;
568 }
569 }
570
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700571 private int resolveOrientation(@NonNull ActivityRecord root, @NonNull ActivityDisplay display,
572 @NonNull Rect bounds) {
573 int orientation = resolveOrientation(root);
574
575 if (orientation == SCREEN_ORIENTATION_LOCKED) {
Garfield Tanbb0270f2018-12-05 11:30:27 -0800576 orientation = bounds.isEmpty()
577 ? convertOrientationToScreenOrientation(display.getConfiguration().orientation)
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700578 : orientationFromBounds(bounds);
579 if (DEBUG) {
580 appendLog(bounds.isEmpty() ? "locked-orientation-from-display=" + orientation
581 : "locked-orientation-from-bounds=" + bounds);
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700582 }
583 }
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700584
585 if (orientation == SCREEN_ORIENTATION_UNSPECIFIED) {
586 orientation = bounds.isEmpty() ? SCREEN_ORIENTATION_PORTRAIT
587 : orientationFromBounds(bounds);
588 if (DEBUG) {
589 appendLog(bounds.isEmpty() ? "default-portrait"
590 : "orientation-from-bounds=" + bounds);
591 }
592 }
593
594 return orientation;
595 }
596
597 private void getDefaultFreeformSize(@NonNull ActivityDisplay display,
598 @NonNull ActivityInfo.WindowLayout layout, int orientation, @NonNull Rect bounds) {
599 // Default size, which is letterboxing/pillarboxing in display. That's to say the large
600 // dimension of default size is the small dimension of display size, and the small dimension
601 // of default size is calculated to keep the same aspect ratio as the display's.
602 Rect displayBounds = display.getBounds();
603 final int portraitHeight = Math.min(displayBounds.width(), displayBounds.height());
604 final int otherDimension = Math.max(displayBounds.width(), displayBounds.height());
605 final int portraitWidth = (portraitHeight * portraitHeight) / otherDimension;
606 final int defaultWidth = (orientation == SCREEN_ORIENTATION_LANDSCAPE) ? portraitHeight
607 : portraitWidth;
608 final int defaultHeight = (orientation == SCREEN_ORIENTATION_LANDSCAPE) ? portraitWidth
609 : portraitHeight;
610
611 // Get window size based on Nexus 5x screen, we assume that this is enough to show content
612 // of activities.
613 final float density = (float) display.getConfiguration().densityDpi / DENSITY_DEFAULT;
614 final int phonePortraitWidth = (int) (DEFAULT_PORTRAIT_PHONE_WIDTH_DP * density + 0.5f);
615 final int phonePortraitHeight = (int) (DEFAULT_PORTRAIT_PHONE_HEIGHT_DP * density + 0.5f);
616 final int phoneWidth = (orientation == SCREEN_ORIENTATION_LANDSCAPE) ? phonePortraitHeight
617 : phonePortraitWidth;
618 final int phoneHeight = (orientation == SCREEN_ORIENTATION_LANDSCAPE) ? phonePortraitWidth
619 : phonePortraitHeight;
620
621 // Minimum layout requirements.
622 final int layoutMinWidth = (layout == null) ? -1 : layout.minWidth;
623 final int layoutMinHeight = (layout == null) ? -1 : layout.minHeight;
624
625 // Final result.
626 final int width = Math.min(defaultWidth, Math.max(phoneWidth, layoutMinWidth));
627 final int height = Math.min(defaultHeight, Math.max(phoneHeight, layoutMinHeight));
628
629 bounds.set(0, 0, width, height);
630 }
631
632 /**
633 * Gets centered bounds of width x height. If inOutBounds is not empty, the result bounds
634 * centers at its center or display's center if inOutBounds is empty.
635 */
636 private void centerBounds(@NonNull ActivityDisplay display, int width, int height,
637 @NonNull Rect inOutBounds) {
638 if (inOutBounds.isEmpty()) {
639 display.getBounds(inOutBounds);
640 }
641 final int left = inOutBounds.centerX() - width / 2;
642 final int top = inOutBounds.centerY() - height / 2;
643 inOutBounds.set(left, top, left + width, top + height);
644 }
645
646 private void adjustBoundsToFitInDisplay(@NonNull ActivityDisplay display,
647 @NonNull Rect inOutBounds) {
648 final Rect displayBounds = display.getBounds();
649
650 if (displayBounds.width() < inOutBounds.width()
651 || displayBounds.height() < inOutBounds.height()) {
652 // There is no way for us to fit the bounds in the display without changing width
Garfield Tan891146c2018-10-09 12:14:00 -0700653 // or height. Just move the start to align with the display.
Wale Ogunwaled32da472018-11-16 07:19:28 -0800654 final int layoutDirection =
655 mSupervisor.mRootActivityContainer.getConfiguration().getLayoutDirection();
Garfield Tan891146c2018-10-09 12:14:00 -0700656 final int left = layoutDirection == View.LAYOUT_DIRECTION_RTL
657 ? displayBounds.width() - inOutBounds.width()
658 : 0;
659 inOutBounds.offsetTo(left, 0 /* newTop */);
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700660 return;
661 }
662
663 final int dx;
664 if (inOutBounds.right > displayBounds.right) {
665 // Right edge is out of display.
666 dx = displayBounds.right - inOutBounds.right;
667 } else if (inOutBounds.left < displayBounds.left) {
668 // Left edge is out of display.
669 dx = displayBounds.left - inOutBounds.left;
670 } else {
671 // Vertical edges are all in display.
672 dx = 0;
673 }
674
675 final int dy;
676 if (inOutBounds.top < displayBounds.top) {
677 // Top edge is out of display.
678 dy = displayBounds.top - inOutBounds.top;
679 } else if (inOutBounds.bottom > displayBounds.bottom) {
680 // Bottom edge is out of display.
681 dy = displayBounds.bottom - inOutBounds.bottom;
682 } else {
683 // Horizontal edges are all in display.
684 dy = 0;
685 }
686 inOutBounds.offset(dx, dy);
687 }
688
689 /**
690 * Adjusts input bounds to avoid conflict with existing tasks in the display.
691 *
692 * If the input bounds conflict with existing tasks, this method scans the bounds in a series of
693 * directions to find a location where the we can put the bounds in display without conflict
694 * with any other tasks.
695 *
696 * It doesn't try to adjust bounds that's not fully in the given display.
697 *
698 * @param display the display which tasks are to check
699 * @param inOutBounds the bounds used to input initial bounds and output result bounds
700 */
701 private void adjustBoundsToAvoidConflict(@NonNull ActivityDisplay display,
702 @NonNull Rect inOutBounds) {
703 final Rect displayBounds = display.getBounds();
704 if (!displayBounds.contains(inOutBounds)) {
705 // The initial bounds are already out of display. The scanning algorithm below doesn't
706 // work so well with them.
707 return;
708 }
709
710 final List<TaskRecord> tasksToCheck = new ArrayList<>();
711 for (int i = 0; i < display.getChildCount(); ++i) {
Yunfan Chen279f5582018-12-12 15:24:50 -0800712 final ActivityStack stack = display.getChildAt(i);
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700713 if (!stack.inFreeformWindowingMode()) {
714 continue;
715 }
716
717 for (int j = 0; j < stack.getChildCount(); ++j) {
718 tasksToCheck.add(stack.getChildAt(j));
719 }
720 }
721
722 if (!boundsConflict(tasksToCheck, inOutBounds)) {
723 // Current proposal doesn't conflict with any task. Early return to avoid unnecessary
724 // calculation.
725 return;
726 }
727
728 calculateCandidateShiftDirections(displayBounds, inOutBounds);
729 for (int direction : mTmpDirections) {
730 if (direction == Gravity.NO_GRAVITY) {
731 // We exhausted candidate directions, give up.
732 break;
733 }
734
735 mTmpBounds.set(inOutBounds);
736 while (boundsConflict(tasksToCheck, mTmpBounds) && displayBounds.contains(mTmpBounds)) {
737 shiftBounds(direction, displayBounds, mTmpBounds);
738 }
739
740 if (!boundsConflict(tasksToCheck, mTmpBounds) && displayBounds.contains(mTmpBounds)) {
741 // Found a candidate. Just use this.
742 inOutBounds.set(mTmpBounds);
743 if (DEBUG) appendLog("avoid-bounds-conflict=" + inOutBounds);
744 return;
745 }
746
747 // Didn't find a conflict free bounds here. Try the next candidate direction.
748 }
749
750 // We failed to find a conflict free location. Just keep the original result.
751 }
752
753 /**
754 * Determines scanning directions and their priorities to avoid bounds conflict.
755 *
756 * @param availableBounds bounds that the result must be in
757 * @param initialBounds initial bounds when start scanning
758 */
759 private void calculateCandidateShiftDirections(@NonNull Rect availableBounds,
760 @NonNull Rect initialBounds) {
761 for (int i = 0; i < mTmpDirections.length; ++i) {
762 mTmpDirections[i] = Gravity.NO_GRAVITY;
763 }
764
765 final int oneThirdWidth = (2 * availableBounds.left + availableBounds.right) / 3;
766 final int twoThirdWidth = (availableBounds.left + 2 * availableBounds.right) / 3;
767 final int centerX = initialBounds.centerX();
768 if (centerX < oneThirdWidth) {
769 // Too close to left, just scan to the right.
770 mTmpDirections[0] = Gravity.RIGHT;
771 return;
772 } else if (centerX > twoThirdWidth) {
773 // Too close to right, just scan to the left.
774 mTmpDirections[0] = Gravity.LEFT;
775 return;
776 }
777
778 final int oneThirdHeight = (2 * availableBounds.top + availableBounds.bottom) / 3;
779 final int twoThirdHeight = (availableBounds.top + 2 * availableBounds.bottom) / 3;
780 final int centerY = initialBounds.centerY();
781 if (centerY < oneThirdHeight || centerY > twoThirdHeight) {
782 // Too close to top or bottom boundary and we're in the middle horizontally, scan
783 // horizontally in both directions.
784 mTmpDirections[0] = Gravity.RIGHT;
785 mTmpDirections[1] = Gravity.LEFT;
786 return;
787 }
788
789 // We're in the center region both horizontally and vertically. Scan in both directions of
790 // primary diagonal.
791 mTmpDirections[0] = Gravity.BOTTOM | Gravity.RIGHT;
792 mTmpDirections[1] = Gravity.TOP | Gravity.LEFT;
793 }
794
795 private boolean boundsConflict(@NonNull List<TaskRecord> tasks, @NonNull Rect bounds) {
796 for (TaskRecord task : tasks) {
797 final Rect taskBounds = task.getBounds();
798 final boolean leftClose = Math.abs(taskBounds.left - bounds.left)
799 < BOUNDS_CONFLICT_THRESHOLD;
800 final boolean topClose = Math.abs(taskBounds.top - bounds.top)
801 < BOUNDS_CONFLICT_THRESHOLD;
802 final boolean rightClose = Math.abs(taskBounds.right - bounds.right)
803 < BOUNDS_CONFLICT_THRESHOLD;
804 final boolean bottomClose = Math.abs(taskBounds.bottom - bounds.bottom)
805 < BOUNDS_CONFLICT_THRESHOLD;
806
807 if ((leftClose && topClose) || (leftClose && bottomClose) || (rightClose && topClose)
808 || (rightClose && bottomClose)) {
809 return true;
810 }
811 }
812
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700813 return false;
814 }
815
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700816 private void shiftBounds(int direction, @NonNull Rect availableRect,
817 @NonNull Rect inOutBounds) {
818 final int horizontalOffset;
819 switch (direction & Gravity.HORIZONTAL_GRAVITY_MASK) {
820 case Gravity.LEFT:
821 horizontalOffset = -Math.max(MINIMAL_STEP,
822 availableRect.width() / STEP_DENOMINATOR);
823 break;
824 case Gravity.RIGHT:
825 horizontalOffset = Math.max(MINIMAL_STEP, availableRect.width() / STEP_DENOMINATOR);
826 break;
827 default:
828 horizontalOffset = 0;
829 }
830
831 final int verticalOffset;
832 switch (direction & Gravity.VERTICAL_GRAVITY_MASK) {
833 case Gravity.TOP:
834 verticalOffset = -Math.max(MINIMAL_STEP, availableRect.height() / STEP_DENOMINATOR);
835 break;
836 case Gravity.BOTTOM:
837 verticalOffset = Math.max(MINIMAL_STEP, availableRect.height() / STEP_DENOMINATOR);
838 break;
839 default:
840 verticalOffset = 0;
841 }
842
843 inOutBounds.offset(horizontalOffset, verticalOffset);
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700844 }
845
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700846 private void initLogBuilder(TaskRecord task, ActivityRecord activity) {
847 if (DEBUG) {
848 mLogBuilder = new StringBuilder("TaskLaunchParamsModifier:task=" + task
849 + " activity=" + activity);
850 }
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700851 }
852
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700853 private void appendLog(String log) {
854 if (DEBUG) mLogBuilder.append(" ").append(log);
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700855 }
856
Garfield Tanb5cc09f2018-09-28 10:06:52 -0700857 private void outputLog() {
858 if (DEBUG) Slog.d(TAG, mLogBuilder.toString());
859 }
860
861 private static int orientationFromBounds(Rect bounds) {
862 return bounds.width() > bounds.height() ? SCREEN_ORIENTATION_LANDSCAPE
863 : SCREEN_ORIENTATION_PORTRAIT;
864 }
865
866 private static boolean sizeMatches(Rect left, Rect right) {
867 return (Math.abs(right.width() - left.width()) < EPSILON)
868 && (Math.abs(right.height() - left.height()) < EPSILON);
Filip Gruszczynski9b1ce522015-08-20 18:37:19 -0700869 }
Filip Gruszczynskie5390e72015-08-18 16:39:00 -0700870}