1 /*
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 
17 package com.android.server.wm;
18 
19 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
21 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
22 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
23 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
24 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
25 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
26 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
27 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
28 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
29 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
30 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
31 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT;
32 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
33 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE;
34 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT;
35 import static android.util.DisplayMetrics.DENSITY_DEFAULT;
36 import static android.view.Display.DEFAULT_DISPLAY;
37 import static android.view.Display.INVALID_DISPLAY;
38 import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
39 
40 import static com.android.server.wm.ActivityStarter.Request;
41 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
42 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
43 
44 import android.annotation.NonNull;
45 import android.annotation.Nullable;
46 import android.app.ActivityOptions;
47 import android.app.WindowConfiguration;
48 import android.content.pm.ActivityInfo;
49 import android.content.res.Configuration;
50 import android.graphics.Rect;
51 import android.util.Size;
52 import android.util.Slog;
53 import android.view.Gravity;
54 import android.window.WindowContainerToken;
55 
56 import com.android.internal.annotations.VisibleForTesting;
57 import com.android.server.wm.LaunchParamsController.LaunchParams;
58 import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
59 
60 import java.util.ArrayList;
61 import java.util.List;
62 
63 /**
64  * The class that defines the default launch params for tasks.
65  */
66 class TaskLaunchParamsModifier implements LaunchParamsModifier {
67     private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskLaunchParamsModifier" : TAG_ATM;
68     private static final boolean DEBUG = false;
69 
70     // Allowance of size matching.
71     private static final int EPSILON = 2;
72 
73     // Cascade window offset.
74     private static final int CASCADING_OFFSET_DP = 75;
75 
76     // Threshold how close window corners have to be to call them colliding.
77     private static final int BOUNDS_CONFLICT_THRESHOLD = 4;
78 
79     // Divide display size by this number to get each step to adjust bounds to avoid conflict.
80     private static final int STEP_DENOMINATOR = 16;
81 
82     // We always want to step by at least this.
83     private static final int MINIMAL_STEP = 1;
84 
85     private final ActivityTaskSupervisor mSupervisor;
86     private final Rect mTmpBounds = new Rect();
87     private final Rect mTmpStableBounds = new Rect();
88     private final int[] mTmpDirections = new int[2];
89 
90     private TaskDisplayArea mTmpDisplayArea;
91 
92     private StringBuilder mLogBuilder;
93 
TaskLaunchParamsModifier(ActivityTaskSupervisor supervisor)94     TaskLaunchParamsModifier(ActivityTaskSupervisor supervisor) {
95         mSupervisor = supervisor;
96     }
97 
98     @Override
onCalculate(@ullable Task task, @Nullable ActivityInfo.WindowLayout layout, @Nullable ActivityRecord activity, @Nullable ActivityRecord source, @Nullable ActivityOptions options, @Nullable Request request, int phase, LaunchParams currentParams, LaunchParams outParams)99     public int onCalculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout,
100             @Nullable ActivityRecord activity, @Nullable ActivityRecord source,
101             @Nullable ActivityOptions options, @Nullable Request request, int phase,
102             LaunchParams currentParams, LaunchParams outParams) {
103         initLogBuilder(task, activity);
104         final int result = calculate(task, layout, activity, source, options, request, phase,
105                 currentParams, outParams);
106         outputLog();
107         return result;
108     }
109 
calculate(@ullable Task task, @Nullable ActivityInfo.WindowLayout layout, @Nullable ActivityRecord activity, @Nullable ActivityRecord source, @Nullable ActivityOptions options, @Nullable Request request, int phase, LaunchParams currentParams, LaunchParams outParams)110     private int calculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout,
111             @Nullable ActivityRecord activity, @Nullable ActivityRecord source,
112             @Nullable ActivityOptions options, @Nullable Request request, int phase,
113             LaunchParams currentParams, LaunchParams outParams) {
114         final ActivityRecord root;
115         if (task != null) {
116             root = task.getRootActivity() == null ? activity : task.getRootActivity();
117         } else {
118             root = activity;
119         }
120 
121         if (root == null) {
122             // There is a case that can lead us here. The caller is moving the top activity that is
123             // in a task that has multiple activities to PIP mode. For that the caller is creating a
124             // new task to host the activity so that we only move the top activity to PIP mode and
125             // keep other activities in the previous task. There is no point to apply the launch
126             // logic in this case.
127             return RESULT_SKIP;
128         }
129 
130         // STEP 1: Determine the suggested display area to launch the activity/task.
131         final TaskDisplayArea suggestedDisplayArea = getPreferredLaunchTaskDisplayArea(task,
132                 options, source, currentParams, activity, request);
133         outParams.mPreferredTaskDisplayArea = suggestedDisplayArea;
134         final DisplayContent display = suggestedDisplayArea.mDisplayContent;
135         if (DEBUG) {
136             appendLog("display-id=" + display.getDisplayId()
137                     + " task-display-area-windowing-mode=" + suggestedDisplayArea.getWindowingMode()
138                     + " suggested-display-area=" + suggestedDisplayArea);
139         }
140 
141         if (phase == PHASE_DISPLAY) {
142             return RESULT_CONTINUE;
143         }
144 
145         // STEP 2: Resolve launch windowing mode.
146         // STEP 2.1: Determine if any parameter can specify initial bounds/windowing mode. That
147         // might be the launch bounds from activity options, or size/gravity passed in layout. It
148         // also treats the launch windowing mode in options and source activity windowing mode in
149         // some cases as a suggestion for future resolution.
150         int launchMode = options != null ? options.getLaunchWindowingMode()
151                 : WINDOWING_MODE_UNDEFINED;
152         // In some cases we want to use the source's windowing mode as the default value, e.g. when
153         // source is a freeform window in a fullscreen display launching an activity on the same
154         // display.
155         if (launchMode == WINDOWING_MODE_UNDEFINED
156                 && canInheritWindowingModeFromSource(display, suggestedDisplayArea, source)) {
157             // The source's windowing mode may be different from its task, e.g. activity is set
158             // to fullscreen and its task is pinned windowing mode when the activity is entering
159             // pip.
160             launchMode = source.getTask().getWindowingMode();
161             if (DEBUG) {
162                 appendLog("inherit-from-source="
163                         + WindowConfiguration.windowingModeToString(launchMode));
164             }
165         }
166         // If the launch windowing mode is still undefined, inherit from the target task if the
167         // task is already on the right display area (otherwise, the task may be on a different
168         // display area that has incompatible windowing mode or the task organizer request to
169         // disassociate the leaf task if relaunched and reparented it to TDA as root task).
170         if (launchMode == WINDOWING_MODE_UNDEFINED
171                 && task != null && task.getTaskDisplayArea() == suggestedDisplayArea
172                 && !task.getRootTask().mReparentLeafTaskIfRelaunch) {
173             launchMode = task.getWindowingMode();
174             if (DEBUG) {
175                 appendLog("inherit-from-task="
176                         + WindowConfiguration.windowingModeToString(launchMode));
177             }
178         }
179         // hasInitialBounds is set if either activity options or layout has specified bounds. If
180         // that's set we'll skip some adjustments later to avoid overriding the initial bounds.
181         boolean hasInitialBounds = false;
182         // hasInitialBoundsForSuggestedDisplayAreaInFreeformWindow is set if the outParams.mBounds
183         // is set with the suggestedDisplayArea. If it is set, but the eventual TaskDisplayArea is
184         // different, we should recalculating the bounds.
185         boolean hasInitialBoundsForSuggestedDisplayAreaInFreeformWindow = false;
186         // Note that initial bounds needs to be set to fullscreen tasks too as it's used as restore
187         // bounds.
188         final boolean canCalculateBoundsForFullscreenTask =
189                 canCalculateBoundsForFullscreenTask(suggestedDisplayArea, launchMode);
190         final boolean canApplyFreeformWindowPolicy =
191                 canApplyFreeformWindowPolicy(suggestedDisplayArea, launchMode);
192         final boolean canApplyWindowLayout = layout != null
193                 && (canApplyFreeformWindowPolicy || canCalculateBoundsForFullscreenTask);
194         final boolean canApplyBoundsFromActivityOptions =
195                 mSupervisor.canUseActivityOptionsLaunchBounds(options)
196                         && (canApplyFreeformWindowPolicy
197                         || canApplyPipWindowPolicy(launchMode)
198                         || canCalculateBoundsForFullscreenTask);
199 
200         if (canApplyBoundsFromActivityOptions) {
201             hasInitialBounds = true;
202             // |launchMode| at this point can be fullscreen, PIP, MultiWindow, etc. Only set
203             // freeform windowing mode if appropriate by checking |canApplyFreeformWindowPolicy|.
204             launchMode = launchMode == WINDOWING_MODE_UNDEFINED && canApplyFreeformWindowPolicy
205                     ? WINDOWING_MODE_FREEFORM
206                     : launchMode;
207             outParams.mBounds.set(options.getLaunchBounds());
208             if (DEBUG) appendLog("activity-options-bounds=" + outParams.mBounds);
209         } else if (canApplyWindowLayout) {
210             mTmpBounds.set(currentParams.mBounds);
211             getLayoutBounds(suggestedDisplayArea, root, layout, mTmpBounds);
212             if (!mTmpBounds.isEmpty()) {
213                 launchMode = canApplyFreeformWindowPolicy ? WINDOWING_MODE_FREEFORM : launchMode;
214                 outParams.mBounds.set(mTmpBounds);
215                 hasInitialBounds = true;
216                 hasInitialBoundsForSuggestedDisplayAreaInFreeformWindow = true;
217                 if (DEBUG) appendLog("bounds-from-layout=" + outParams.mBounds);
218             } else {
219                 if (DEBUG) appendLog("empty-window-layout");
220             }
221         } else if (launchMode == WINDOWING_MODE_MULTI_WINDOW
222                 && options != null && options.getLaunchBounds() != null) {
223             // TODO: Investigate whether we can migrate this clause to the
224             //  |canApplyBoundsFromActivityOptions| case above.
225             outParams.mBounds.set(options.getLaunchBounds());
226             hasInitialBounds = true;
227             if (DEBUG) appendLog("multiwindow-activity-options-bounds=" + outParams.mBounds);
228         }
229 
230         // STEP 2.2: Check if previous modifier or the controller (referred as "callers" below) has
231         // some opinions on launch mode and launch bounds. If they have opinions and there is no
232         // initial bounds set in parameters. Note the check on display ID is also input param
233         // related because we always defer to callers' suggestion if there is no specific display ID
234         // in options or from source activity.
235         //
236         // If opinions from callers don't need any further resolution, we try to honor that as is as
237         // much as possible later.
238 
239         // Flag to indicate if current param needs no further resolution. It's true it current
240         // param isn't freeform mode, or it already has launch bounds.
241         boolean fullyResolvedCurrentParam = false;
242         // We inherit launch params from previous modifiers or LaunchParamsController if options,
243         // layout and display conditions are not contradictory to their suggestions. It's important
244         // to carry over their values because LaunchParamsController doesn't automatically do that.
245         // We only check if display matches because display area can be changed later.
246         if (!currentParams.isEmpty() && !hasInitialBounds
247                 && (currentParams.mPreferredTaskDisplayArea == null
248                     || currentParams.mPreferredTaskDisplayArea.getDisplayId()
249                         == display.getDisplayId())) {
250             // Only set windowing mode if display is in freeform. If the display is in fullscreen
251             // mode we should only launch a task in fullscreen mode.
252             if (currentParams.hasWindowingMode()
253                     && suggestedDisplayArea.inFreeformWindowingMode()) {
254                 launchMode = currentParams.mWindowingMode;
255                 fullyResolvedCurrentParam = launchMode != WINDOWING_MODE_FREEFORM;
256                 if (DEBUG) {
257                     appendLog("inherit-" + WindowConfiguration.windowingModeToString(launchMode));
258                 }
259             }
260 
261             if (!currentParams.mBounds.isEmpty()) {
262                 // Carry over bounds from callers regardless of launch mode because bounds is still
263                 // used to restore last non-fullscreen bounds when launch mode is not freeform.
264                 outParams.mBounds.set(currentParams.mBounds);
265                 fullyResolvedCurrentParam = true;
266                 if (launchMode == WINDOWING_MODE_FREEFORM) {
267                     if (DEBUG) appendLog("inherit-bounds=" + outParams.mBounds);
268                 }
269             }
270         }
271 
272         // STEP 2.3: Adjust launch parameters as needed for freeform display. We enforce the
273         // policies related to unresizable apps here. If an app is unresizable and the freeform
274         // size-compat mode is enabled, it can be launched in freeform depending on other properties
275         // such as orientation. Otherwise, the app is forcefully launched in maximized. The rest of
276         // this step is to define the default policy when there is no initial bounds or a fully
277         // resolved current params from callers.
278 
279         // hasInitialBoundsForSuggestedDisplayAreaInFreeformMode is set if the outParams.mBounds
280         // is set with the suggestedDisplayArea. If it is set, but the eventual TaskDisplayArea is
281         // different, we should recalcuating the bounds.
282         boolean hasInitialBoundsForSuggestedDisplayAreaInFreeformMode = false;
283         if (suggestedDisplayArea.inFreeformWindowingMode()) {
284             if (launchMode == WINDOWING_MODE_PINNED) {
285                 if (DEBUG) appendLog("picture-in-picture");
286             } else if (!root.isResizeable()) {
287                 if (shouldLaunchUnresizableAppInFreeform(root, suggestedDisplayArea, options)) {
288                     launchMode = WINDOWING_MODE_FREEFORM;
289                     if (outParams.mBounds.isEmpty()) {
290                         getTaskBounds(root, suggestedDisplayArea, layout, launchMode,
291                                 hasInitialBounds, outParams.mBounds);
292                         hasInitialBoundsForSuggestedDisplayAreaInFreeformMode = true;
293                     }
294                     if (DEBUG) appendLog("unresizable-freeform");
295                 } else {
296                     launchMode = WINDOWING_MODE_FULLSCREEN;
297                     outParams.mBounds.setEmpty();
298                     if (DEBUG) appendLog("unresizable-forced-maximize");
299                 }
300             }
301         } else {
302             if (DEBUG) appendLog("non-freeform-task-display-area");
303         }
304         // If launch mode matches display windowing mode, let it inherit from display.
305         outParams.mWindowingMode = launchMode == suggestedDisplayArea.getWindowingMode()
306                 ? WINDOWING_MODE_UNDEFINED : launchMode;
307 
308         if (phase == PHASE_WINDOWING_MODE) {
309             return RESULT_CONTINUE;
310         }
311 
312         // STEP 3: Finalize the display area. Here we allow WM shell route all launches that match
313         // certain criteria to specific task display areas.
314         final int resolvedMode = (launchMode != WINDOWING_MODE_UNDEFINED) ? launchMode
315                 : suggestedDisplayArea.getWindowingMode();
316         TaskDisplayArea taskDisplayArea = suggestedDisplayArea;
317         // If launch task display area is set in options we should just use it. We assume the
318         // suggestedDisplayArea has the right one in this case.
319         if (options == null || (options.getLaunchTaskDisplayArea() == null
320                 && options.getLaunchTaskDisplayAreaFeatureId() == FEATURE_UNDEFINED)) {
321             final int activityType =
322                     mSupervisor.mRootWindowContainer.resolveActivityType(root, options, task);
323             display.forAllTaskDisplayAreas(displayArea -> {
324                 final Task launchRoot = displayArea.getLaunchRootTask(
325                         resolvedMode, activityType, null /* ActivityOptions */,
326                         null /* sourceTask*/, 0 /* launchFlags */);
327                 if (launchRoot == null) {
328                     return false;
329                 }
330                 mTmpDisplayArea = displayArea;
331                 return true;
332             });
333             // We may need to recalculate the bounds and the windowing mode if the new
334             // TaskDisplayArea is different from the suggested one we used to calculate the two
335             // configurations.
336             if (mTmpDisplayArea != null && mTmpDisplayArea != suggestedDisplayArea) {
337                 outParams.mWindowingMode = (launchMode == mTmpDisplayArea.getWindowingMode())
338                         ? WINDOWING_MODE_UNDEFINED : launchMode;
339                 if (hasInitialBoundsForSuggestedDisplayAreaInFreeformWindow) {
340                     outParams.mBounds.setEmpty();
341                     getLayoutBounds(mTmpDisplayArea, root, layout, outParams.mBounds);
342                     hasInitialBounds = !outParams.mBounds.isEmpty();
343                 } else if (hasInitialBoundsForSuggestedDisplayAreaInFreeformMode) {
344                     outParams.mBounds.setEmpty();
345                     getTaskBounds(root, mTmpDisplayArea, layout, launchMode,
346                             hasInitialBounds, outParams.mBounds);
347                 }
348             }
349 
350             if (mTmpDisplayArea != null) {
351                 taskDisplayArea = mTmpDisplayArea;
352                 mTmpDisplayArea = null;
353                 appendLog("overridden-display-area=["
354                         + WindowConfiguration.activityTypeToString(activityType) + ", "
355                         + WindowConfiguration.windowingModeToString(resolvedMode) + ", "
356                         + taskDisplayArea + "]");
357             }
358         }
359         appendLog("display-area=" + taskDisplayArea);
360         outParams.mPreferredTaskDisplayArea = taskDisplayArea;
361 
362         if (phase == PHASE_DISPLAY_AREA) {
363             return RESULT_CONTINUE;
364         }
365 
366         // STEP 4: Determine final launch bounds based on resolved windowing mode and activity
367         // requested orientation. We set bounds to empty for fullscreen mode and keep bounds as is
368         // for all other windowing modes that's not freeform mode. One can read comments in
369         // relevant methods to further understand this step.
370         //
371         // We skip making adjustments if the params are fully resolved from previous results.
372         if (fullyResolvedCurrentParam) {
373             if (resolvedMode == WINDOWING_MODE_FREEFORM) {
374                 // Make sure bounds are in the displayArea.
375                 if (currentParams.mPreferredTaskDisplayArea != taskDisplayArea) {
376                     adjustBoundsToFitInDisplayArea(taskDisplayArea, layout, outParams.mBounds);
377                 }
378                 // Even though we want to keep original bounds, we still don't want it to stomp on
379                 // an existing task.
380                 adjustBoundsToAvoidConflictInDisplayArea(taskDisplayArea, outParams.mBounds);
381             }
382         } else {
383             if (source != null && source.inFreeformWindowingMode()
384                     && resolvedMode == WINDOWING_MODE_FREEFORM
385                     && outParams.mBounds.isEmpty()
386                     && source.getDisplayArea() == taskDisplayArea) {
387                 // Set bounds to be not very far from source activity.
388                 cascadeBounds(source.getConfiguration().windowConfiguration.getBounds(),
389                         taskDisplayArea, outParams.mBounds);
390             }
391             getTaskBounds(root, taskDisplayArea, layout, resolvedMode, hasInitialBounds,
392                     outParams.mBounds);
393         }
394         return RESULT_CONTINUE;
395     }
396 
getPreferredLaunchTaskDisplayArea(@ullable Task task, @Nullable ActivityOptions options, ActivityRecord source, LaunchParams currentParams, @NonNull ActivityRecord activityRecord, @Nullable Request request)397     private TaskDisplayArea getPreferredLaunchTaskDisplayArea(@Nullable Task task,
398             @Nullable ActivityOptions options, ActivityRecord source, LaunchParams currentParams,
399             @NonNull ActivityRecord activityRecord, @Nullable Request request) {
400         TaskDisplayArea taskDisplayArea = null;
401 
402         final WindowContainerToken optionLaunchTaskDisplayAreaToken = options != null
403                 ? options.getLaunchTaskDisplayArea() : null;
404         if (optionLaunchTaskDisplayAreaToken != null) {
405             taskDisplayArea = (TaskDisplayArea) WindowContainer.fromBinder(
406                     optionLaunchTaskDisplayAreaToken.asBinder());
407             if (DEBUG) appendLog("display-area-token-from-option=" + taskDisplayArea);
408         }
409 
410         if (taskDisplayArea == null && options != null) {
411             final int launchTaskDisplayAreaFeatureId = options.getLaunchTaskDisplayAreaFeatureId();
412             if (launchTaskDisplayAreaFeatureId != FEATURE_UNDEFINED) {
413                 final int launchDisplayId = options.getLaunchDisplayId() == INVALID_DISPLAY
414                         ? DEFAULT_DISPLAY : options.getLaunchDisplayId();
415                 final DisplayContent dc = mSupervisor.mRootWindowContainer
416                         .getDisplayContent(launchDisplayId);
417                 if (dc != null) {
418                     taskDisplayArea = dc.getItemFromTaskDisplayAreas(tda ->
419                             tda.mFeatureId == launchTaskDisplayAreaFeatureId ? tda : null);
420                     if (DEBUG) appendLog("display-area-feature-from-option=" + taskDisplayArea);
421                 }
422             }
423         }
424 
425         // If task display area is not specified in options - try display id
426         if (taskDisplayArea == null) {
427             final int optionLaunchId =
428                     options != null ? options.getLaunchDisplayId() : INVALID_DISPLAY;
429             if (optionLaunchId != INVALID_DISPLAY) {
430                 final DisplayContent dc = mSupervisor.mRootWindowContainer
431                         .getDisplayContent(optionLaunchId);
432                 if (dc != null) {
433                     taskDisplayArea = dc.getDefaultTaskDisplayArea();
434                     if (DEBUG) appendLog("display-from-option=" + optionLaunchId);
435                 }
436             }
437         }
438 
439         // If the source activity is a no-display activity, pass on the launch display area token
440         // from source activity as currently preferred.
441         if (taskDisplayArea == null && source != null
442                 && source.noDisplay) {
443             taskDisplayArea = source.mHandoverTaskDisplayArea;
444             if (taskDisplayArea != null) {
445                 if (DEBUG) appendLog("display-area-from-no-display-source=" + taskDisplayArea);
446             } else {
447                 // Try handover display id
448                 final int displayId = source.mHandoverLaunchDisplayId;
449                 final DisplayContent dc =
450                         mSupervisor.mRootWindowContainer.getDisplayContent(displayId);
451                 if (dc != null) {
452                     taskDisplayArea = dc.getDefaultTaskDisplayArea();
453                     if (DEBUG) appendLog("display-from-no-display-source=" + displayId);
454                 }
455             }
456         }
457 
458         if (taskDisplayArea == null && source != null) {
459             final TaskDisplayArea sourceDisplayArea = source.getDisplayArea();
460             if (DEBUG) appendLog("display-area-from-source=" + sourceDisplayArea);
461             taskDisplayArea = sourceDisplayArea;
462         }
463 
464         Task rootTask = (taskDisplayArea == null && task != null)
465                 ? task.getRootTask() : null;
466         if (rootTask != null) {
467             if (DEBUG) appendLog("display-from-task=" + rootTask.getDisplayId());
468             taskDisplayArea = rootTask.getDisplayArea();
469         }
470 
471         if (taskDisplayArea == null && options != null) {
472             final int callerDisplayId = options.getCallerDisplayId();
473             final DisplayContent dc =
474                     mSupervisor.mRootWindowContainer.getDisplayContent(callerDisplayId);
475             if (dc != null) {
476                 taskDisplayArea = dc.getDefaultTaskDisplayArea();
477                 if (DEBUG) appendLog("display-from-caller=" + callerDisplayId);
478             }
479         }
480 
481         if (taskDisplayArea == null) {
482             taskDisplayArea = currentParams.mPreferredTaskDisplayArea;
483         }
484 
485         // Re-route to default display if the device didn't declare support for multi-display
486         if (taskDisplayArea != null && !mSupervisor.mService.mSupportsMultiDisplay
487                 && taskDisplayArea.getDisplayId() != DEFAULT_DISPLAY) {
488             taskDisplayArea = mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea();
489         }
490 
491         // Re-route to default display if the home activity doesn't support multi-display
492         if (taskDisplayArea != null && activityRecord.isActivityTypeHome()
493                 && !mSupervisor.mRootWindowContainer.canStartHomeOnDisplayArea(activityRecord.info,
494                         taskDisplayArea, false /* allowInstrumenting */)) {
495             taskDisplayArea = mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea();
496         }
497 
498         return (taskDisplayArea != null)
499                 ? taskDisplayArea
500                 : getFallbackDisplayAreaForActivity(activityRecord, request);
501     }
502 
503     /**
504      * Calculates the default {@link TaskDisplayArea} for a task. We attempt to put the activity
505      * within the same display area if possible. The strategy is to find the display in the
506      * following order:
507      *
508      * <ol>
509      *     <li>The display area of the top activity from the launching process will be used</li>
510      *     <li>The display area of the top activity from the real launching process will be used
511      *     </li>
512      *     <li>Default display area from the associated root window container.</li>
513      * </ol>
514      * @param activityRecord the activity being started
515      * @param request optional {@link Request} made to start the activity record
516      * @return {@link TaskDisplayArea} to house the task
517      */
getFallbackDisplayAreaForActivity( @onNull ActivityRecord activityRecord, @Nullable Request request)518     private TaskDisplayArea getFallbackDisplayAreaForActivity(
519             @NonNull ActivityRecord activityRecord, @Nullable Request request) {
520 
521         WindowProcessController controllerFromLaunchingRecord = mSupervisor.mService
522                 .getProcessController(activityRecord.launchedFromPid,
523                         activityRecord.launchedFromUid);
524         final TaskDisplayArea displayAreaForLaunchingRecord = controllerFromLaunchingRecord == null
525                 ? null : controllerFromLaunchingRecord.getTopActivityDisplayArea();
526         if (displayAreaForLaunchingRecord != null) {
527             return displayAreaForLaunchingRecord;
528         }
529 
530         WindowProcessController controllerFromProcess = mSupervisor.mService.getProcessController(
531                 activityRecord.getProcessName(), activityRecord.getUid());
532         final TaskDisplayArea displayAreaForRecord = controllerFromProcess == null ? null
533                 : controllerFromProcess.getTopActivityDisplayArea();
534         if (displayAreaForRecord != null) {
535             return displayAreaForRecord;
536         }
537 
538         WindowProcessController controllerFromRequest = request == null ? null : mSupervisor
539                 .mService.getProcessController(request.realCallingPid, request.realCallingUid);
540         final TaskDisplayArea displayAreaFromSourceProcess = controllerFromRequest == null ? null
541                 : controllerFromRequest.getTopActivityDisplayArea();
542         if (displayAreaFromSourceProcess != null) {
543             return displayAreaFromSourceProcess;
544         }
545 
546         return mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea();
547     }
548 
canInheritWindowingModeFromSource(@onNull DisplayContent display, TaskDisplayArea suggestedDisplayArea, @Nullable ActivityRecord source)549     private boolean canInheritWindowingModeFromSource(@NonNull DisplayContent display,
550             TaskDisplayArea suggestedDisplayArea, @Nullable ActivityRecord source) {
551         if (source == null) {
552             return false;
553         }
554 
555         // There is not really any strong reason to tie the launching windowing mode and the source
556         // on freeform displays. The launching windowing mode is more tied to the content of the new
557         // activities.
558         if (suggestedDisplayArea.inFreeformWindowingMode()) {
559             return false;
560         }
561 
562         final int sourceWindowingMode = source.getTask().getWindowingMode();
563         if (sourceWindowingMode != WINDOWING_MODE_FULLSCREEN
564                 && sourceWindowingMode != WINDOWING_MODE_FREEFORM) {
565             return false;
566         }
567 
568         // Only inherit windowing mode if both source and target activities are on the same display.
569         // Otherwise we may have unintended freeform windows showing up if an activity in freeform
570         // window launches an activity on a fullscreen display by specifying display ID.
571         return display.getDisplayId() == source.getDisplayId();
572     }
573 
canCalculateBoundsForFullscreenTask(@onNull TaskDisplayArea displayArea, int launchMode)574     private boolean canCalculateBoundsForFullscreenTask(@NonNull TaskDisplayArea displayArea,
575                                                         int launchMode) {
576         return mSupervisor.mService.mSupportsFreeformWindowManagement
577                 && ((displayArea.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
578                 && launchMode == WINDOWING_MODE_UNDEFINED)
579                 || launchMode == WINDOWING_MODE_FULLSCREEN);
580     }
581 
canApplyFreeformWindowPolicy(@onNull TaskDisplayArea suggestedDisplayArea, int launchMode)582     private boolean canApplyFreeformWindowPolicy(@NonNull TaskDisplayArea suggestedDisplayArea,
583             int launchMode) {
584         return mSupervisor.mService.mSupportsFreeformWindowManagement
585                 && ((suggestedDisplayArea.inFreeformWindowingMode()
586                 && launchMode == WINDOWING_MODE_UNDEFINED)
587                 || launchMode == WINDOWING_MODE_FREEFORM);
588     }
589 
canApplyPipWindowPolicy(int launchMode)590     private boolean canApplyPipWindowPolicy(int launchMode) {
591         return mSupervisor.mService.mSupportsPictureInPicture
592                 && launchMode == WINDOWING_MODE_PINNED;
593     }
594 
getLayoutBounds(@onNull TaskDisplayArea displayArea, @NonNull ActivityRecord root, @NonNull ActivityInfo.WindowLayout windowLayout, @NonNull Rect inOutBounds)595     private void getLayoutBounds(@NonNull TaskDisplayArea displayArea, @NonNull ActivityRecord root,
596             @NonNull ActivityInfo.WindowLayout windowLayout, @NonNull Rect inOutBounds) {
597         final int verticalGravity = windowLayout.gravity & Gravity.VERTICAL_GRAVITY_MASK;
598         final int horizontalGravity = windowLayout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
599         if (!windowLayout.hasSpecifiedSize() && verticalGravity == 0 && horizontalGravity == 0) {
600             inOutBounds.setEmpty();
601             return;
602         }
603 
604         // Use stable frame instead of raw frame to avoid launching freeform windows on top of
605         // stable insets, which usually are system widgets such as sysbar & navbar.
606         final Rect stableBounds = mTmpStableBounds;
607         displayArea.getStableRect(stableBounds);
608         final int defaultWidth = stableBounds.width();
609         final int defaultHeight = stableBounds.height();
610 
611         int width;
612         int height;
613         if (!windowLayout.hasSpecifiedSize()) {
614             if (!inOutBounds.isEmpty()) {
615                 // If the bounds is resolved already and WindowLayout doesn't have any opinion on
616                 // its size, use the already resolved size and apply the gravity to it.
617                 width = inOutBounds.width();
618                 height = inOutBounds.height();
619             } else {
620                 getTaskBounds(root, displayArea, windowLayout, WINDOWING_MODE_FREEFORM,
621                         /* hasInitialBounds */ false, inOutBounds);
622                 width = inOutBounds.width();
623                 height = inOutBounds.height();
624             }
625         } else {
626             width = defaultWidth;
627             if (windowLayout.width > 0 && windowLayout.width < defaultWidth) {
628                 width = windowLayout.width;
629             } else if (windowLayout.widthFraction > 0 && windowLayout.widthFraction < 1.0f) {
630                 width = (int) (width * windowLayout.widthFraction);
631             }
632 
633             height = defaultHeight;
634             if (windowLayout.height > 0 && windowLayout.height < defaultHeight) {
635                 height = windowLayout.height;
636             } else if (windowLayout.heightFraction > 0 && windowLayout.heightFraction < 1.0f) {
637                 height = (int) (height * windowLayout.heightFraction);
638             }
639         }
640 
641         final float fractionOfHorizontalOffset;
642         switch (horizontalGravity) {
643             case Gravity.LEFT:
644                 fractionOfHorizontalOffset = 0f;
645                 break;
646             case Gravity.RIGHT:
647                 fractionOfHorizontalOffset = 1f;
648                 break;
649             default:
650                 fractionOfHorizontalOffset = 0.5f;
651         }
652 
653         final float fractionOfVerticalOffset;
654         switch (verticalGravity) {
655             case Gravity.TOP:
656                 fractionOfVerticalOffset = 0f;
657                 break;
658             case Gravity.BOTTOM:
659                 fractionOfVerticalOffset = 1f;
660                 break;
661             default:
662                 fractionOfVerticalOffset = 0.5f;
663         }
664 
665         inOutBounds.set(0, 0, width, height);
666         inOutBounds.offset(stableBounds.left, stableBounds.top);
667         final int xOffset = (int) (fractionOfHorizontalOffset * (defaultWidth - width));
668         final int yOffset = (int) (fractionOfVerticalOffset * (defaultHeight - height));
669         inOutBounds.offset(xOffset, yOffset);
670     }
671 
shouldLaunchUnresizableAppInFreeform(ActivityRecord activity, TaskDisplayArea displayArea, @Nullable ActivityOptions options)672     private boolean shouldLaunchUnresizableAppInFreeform(ActivityRecord activity,
673             TaskDisplayArea displayArea, @Nullable ActivityOptions options) {
674         if (options != null && options.getLaunchWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
675             // Do not launch the activity in freeform if it explicitly requested fullscreen mode.
676             return false;
677         }
678         if (!activity.supportsFreeformInDisplayArea(displayArea) || activity.isResizeable()) {
679             return false;
680         }
681 
682         final int displayOrientation = orientationFromBounds(displayArea.getBounds());
683         final int activityOrientation = resolveOrientation(activity, displayArea,
684                 displayArea.getBounds());
685         if (displayArea.getWindowingMode() == WINDOWING_MODE_FREEFORM
686                 && displayOrientation != activityOrientation) {
687             return true;
688         }
689 
690         return false;
691     }
692 
693     /**
694      * Resolves activity requested orientation to 4 categories:
695      * 1) {@link ActivityInfo#SCREEN_ORIENTATION_LOCKED} indicating app wants to lock down
696      *    orientation;
697      * 2) {@link ActivityInfo#SCREEN_ORIENTATION_LANDSCAPE} indicating app wants to be in landscape;
698      * 3) {@link ActivityInfo#SCREEN_ORIENTATION_PORTRAIT} indicating app wants to be in portrait;
699      * 4) {@link ActivityInfo#SCREEN_ORIENTATION_UNSPECIFIED} indicating app can handle any
700      *    orientation.
701      *
702      * @param activity the activity to check
703      * @return corresponding resolved orientation value.
704      */
resolveOrientation(@onNull ActivityRecord activity)705     private int resolveOrientation(@NonNull ActivityRecord activity) {
706         int orientation = activity.info.screenOrientation;
707         switch (orientation) {
708             case SCREEN_ORIENTATION_NOSENSOR:
709             case SCREEN_ORIENTATION_LOCKED:
710                 orientation = SCREEN_ORIENTATION_LOCKED;
711                 break;
712             case SCREEN_ORIENTATION_SENSOR_LANDSCAPE:
713             case SCREEN_ORIENTATION_REVERSE_LANDSCAPE:
714             case SCREEN_ORIENTATION_USER_LANDSCAPE:
715             case SCREEN_ORIENTATION_LANDSCAPE:
716                 if (DEBUG) appendLog("activity-requested-landscape");
717                 orientation = SCREEN_ORIENTATION_LANDSCAPE;
718                 break;
719             case SCREEN_ORIENTATION_SENSOR_PORTRAIT:
720             case SCREEN_ORIENTATION_REVERSE_PORTRAIT:
721             case SCREEN_ORIENTATION_USER_PORTRAIT:
722             case SCREEN_ORIENTATION_PORTRAIT:
723                 if (DEBUG) appendLog("activity-requested-portrait");
724                 orientation = SCREEN_ORIENTATION_PORTRAIT;
725                 break;
726             default:
727                 orientation = SCREEN_ORIENTATION_UNSPECIFIED;
728         }
729 
730         return orientation;
731     }
732 
cascadeBounds(@onNull Rect srcBounds, @NonNull TaskDisplayArea displayArea, @NonNull Rect outBounds)733     private void cascadeBounds(@NonNull Rect srcBounds, @NonNull TaskDisplayArea displayArea,
734             @NonNull Rect outBounds) {
735         outBounds.set(srcBounds);
736         float density = (float) displayArea.getConfiguration().densityDpi / DENSITY_DEFAULT;
737         final int defaultOffset = (int) (CASCADING_OFFSET_DP * density + 0.5f);
738 
739         displayArea.getBounds(mTmpBounds);
740         final int dx = Math.min(defaultOffset, Math.max(0, mTmpBounds.right - srcBounds.right));
741         final int dy = Math.min(defaultOffset, Math.max(0, mTmpBounds.bottom - srcBounds.bottom));
742         outBounds.offset(dx, dy);
743     }
744 
getTaskBounds(@onNull ActivityRecord root, @NonNull TaskDisplayArea displayArea, @NonNull ActivityInfo.WindowLayout layout, int resolvedMode, boolean hasInitialBounds, @NonNull Rect inOutBounds)745     private void getTaskBounds(@NonNull ActivityRecord root, @NonNull TaskDisplayArea displayArea,
746             @NonNull ActivityInfo.WindowLayout layout, int resolvedMode, boolean hasInitialBounds,
747             @NonNull Rect inOutBounds) {
748         if (resolvedMode != WINDOWING_MODE_FREEFORM
749                 && resolvedMode != WINDOWING_MODE_FULLSCREEN) {
750             // This function should be used only for freeform bounds adjustment. Freeform bounds
751             // needs to be set to fullscreen tasks too as restore bounds.
752             if (DEBUG) {
753                 appendLog("skip-bounds-" + WindowConfiguration.windowingModeToString(resolvedMode));
754             }
755             return;
756         }
757 
758         final int orientation = resolveOrientation(root, displayArea, inOutBounds);
759         if (orientation != SCREEN_ORIENTATION_PORTRAIT
760                 && orientation != SCREEN_ORIENTATION_LANDSCAPE) {
761             throw new IllegalStateException(
762                     "Orientation must be one of portrait or landscape, but it's "
763                     + ActivityInfo.screenOrientationToString(orientation));
764         }
765 
766         // First we get the default size we want.
767         displayArea.getStableRect(mTmpStableBounds);
768         final Size defaultSize = LaunchParamsUtil.getDefaultFreeformSize(root, displayArea,
769                 layout, orientation, mTmpStableBounds);
770         mTmpBounds.set(0, 0, defaultSize.getWidth(), defaultSize.getHeight());
771         if (hasInitialBounds || sizeMatches(inOutBounds, mTmpBounds)) {
772             // We're here because either input parameters specified initial bounds, or the suggested
773             // bounds have the same size of the default freeform size. We should use the suggested
774             // bounds if possible -- so if app can handle the orientation we just use it, and if not
775             // we transpose the suggested bounds in-place.
776             if (orientation == orientationFromBounds(inOutBounds)) {
777                 if (DEBUG) appendLog("freeform-size-orientation-match=" + inOutBounds);
778             } else {
779                 // Meh, orientation doesn't match. Let's rotate inOutBounds in-place.
780                 LaunchParamsUtil.centerBounds(displayArea, inOutBounds.height(),
781                         inOutBounds.width(), inOutBounds);
782                 if (DEBUG) appendLog("freeform-orientation-mismatch=" + inOutBounds);
783             }
784         } else {
785             // We are here either because there is no suggested bounds, or the suggested bounds is
786             // a cascade from source activity. We should use the default freeform size and center it
787             // to the center of suggested bounds (or the displayArea if no suggested bounds). The
788             // default size might be too big to center to source activity bounds in displayArea, so
789             // we may need to move it back to the displayArea.
790             adjustBoundsToFitInDisplayArea(displayArea, layout, mTmpBounds);
791             inOutBounds.setEmpty();
792             LaunchParamsUtil.centerBounds(displayArea, mTmpBounds.width(), mTmpBounds.height(),
793                     inOutBounds);
794             if (DEBUG) appendLog("freeform-size-mismatch=" + inOutBounds);
795         }
796 
797         // Lastly we adjust bounds to avoid conflicts with other tasks as much as possible.
798         adjustBoundsToAvoidConflictInDisplayArea(displayArea, inOutBounds);
799     }
800 
convertOrientationToScreenOrientation(int orientation)801     private int convertOrientationToScreenOrientation(int orientation) {
802         switch (orientation) {
803             case Configuration.ORIENTATION_LANDSCAPE:
804                 return SCREEN_ORIENTATION_LANDSCAPE;
805             case Configuration.ORIENTATION_PORTRAIT:
806                 return SCREEN_ORIENTATION_PORTRAIT;
807             default:
808                 return SCREEN_ORIENTATION_UNSPECIFIED;
809         }
810     }
811 
resolveOrientation(@onNull ActivityRecord root, @NonNull TaskDisplayArea displayArea, @NonNull Rect bounds)812     private int resolveOrientation(@NonNull ActivityRecord root,
813             @NonNull TaskDisplayArea displayArea, @NonNull Rect bounds) {
814         int orientation = resolveOrientation(root);
815 
816         if (orientation == SCREEN_ORIENTATION_LOCKED) {
817             orientation = bounds.isEmpty()
818                     ? convertOrientationToScreenOrientation(
819                             displayArea.getConfiguration().orientation)
820                     : orientationFromBounds(bounds);
821             if (DEBUG) {
822                 appendLog(bounds.isEmpty() ? "locked-orientation-from-display=" + orientation
823                         : "locked-orientation-from-bounds=" + bounds);
824             }
825         }
826 
827         if (orientation == SCREEN_ORIENTATION_UNSPECIFIED) {
828             orientation = bounds.isEmpty() ? SCREEN_ORIENTATION_PORTRAIT
829                     : orientationFromBounds(bounds);
830             if (DEBUG) {
831                 appendLog(bounds.isEmpty() ? "default-portrait"
832                         : "orientation-from-bounds=" + bounds);
833             }
834         }
835 
836         return orientation;
837     }
838 
adjustBoundsToFitInDisplayArea(@onNull TaskDisplayArea displayArea, @NonNull ActivityInfo.WindowLayout layout, @NonNull Rect inOutBounds)839     private void adjustBoundsToFitInDisplayArea(@NonNull TaskDisplayArea displayArea,
840                                                 @NonNull ActivityInfo.WindowLayout layout,
841                                                 @NonNull Rect inOutBounds) {
842         final int layoutDirection = mSupervisor.mRootWindowContainer.getConfiguration()
843                 .getLayoutDirection();
844         LaunchParamsUtil.adjustBoundsToFitInDisplayArea(displayArea, layoutDirection, layout,
845                 inOutBounds);
846     }
847 
848     /**
849      * Adjusts input bounds to avoid conflict with existing tasks in the displayArea.
850      *
851      * If the input bounds conflict with existing tasks, this method scans the bounds in a series of
852      * directions to find a location where the we can put the bounds in displayArea without conflict
853      * with any other tasks.
854      *
855      * It doesn't try to adjust bounds that's not fully in the given displayArea.
856      *
857      * @param displayArea the displayArea which tasks are to check
858      * @param inOutBounds the bounds used to input initial bounds and output result bounds
859      */
adjustBoundsToAvoidConflictInDisplayArea(@onNull TaskDisplayArea displayArea, @NonNull Rect inOutBounds)860     private void adjustBoundsToAvoidConflictInDisplayArea(@NonNull TaskDisplayArea displayArea,
861             @NonNull Rect inOutBounds) {
862         final List<Rect> taskBoundsToCheck = new ArrayList<>();
863         displayArea.forAllRootTasks(task -> {
864             if (!task.inFreeformWindowingMode()) {
865                 return;
866             }
867 
868             for (int j = 0; j < task.getChildCount(); ++j) {
869                 taskBoundsToCheck.add(task.getChildAt(j).getBounds());
870             }
871         }, false /* traverseTopToBottom */);
872         adjustBoundsToAvoidConflict(displayArea.getBounds(), taskBoundsToCheck, inOutBounds);
873     }
874 
875     /**
876      * Adjusts input bounds to avoid conflict with provided displayArea bounds and list of tasks
877      * bounds for the displayArea.
878      *
879      * Scans the bounds in directions to find a candidate location that does not conflict with the
880      * provided list of task bounds. If starting bounds are outside the displayArea bounds or if no
881      * suitable candidate bounds are found, the method returns the input bounds.
882      *
883      * @param displayAreaBounds displayArea bounds used to restrict the candidate bounds
884      * @param taskBoundsToCheck list of task bounds to check for conflict
885      * @param inOutBounds the bounds used to input initial bounds and output result bounds
886      */
887     @VisibleForTesting
adjustBoundsToAvoidConflict(@onNull Rect displayAreaBounds, @NonNull List<Rect> taskBoundsToCheck, @NonNull Rect inOutBounds)888     void adjustBoundsToAvoidConflict(@NonNull Rect displayAreaBounds,
889             @NonNull List<Rect> taskBoundsToCheck,
890             @NonNull Rect inOutBounds) {
891         if (!displayAreaBounds.contains(inOutBounds)) {
892             // The initial bounds are already out of displayArea. The scanning algorithm below
893             // doesn't work so well with them.
894             return;
895         }
896 
897         if (!boundsConflict(taskBoundsToCheck, inOutBounds)) {
898             // Current proposal doesn't conflict with any task. Early return to avoid unnecessary
899             // calculation.
900             return;
901         }
902 
903         calculateCandidateShiftDirections(displayAreaBounds, inOutBounds);
904         for (int direction : mTmpDirections) {
905             if (direction == Gravity.NO_GRAVITY) {
906                 // We exhausted candidate directions, give up.
907                 break;
908             }
909 
910             mTmpBounds.set(inOutBounds);
911             while (boundsConflict(taskBoundsToCheck, mTmpBounds)
912                     && displayAreaBounds.contains(mTmpBounds)) {
913                 shiftBounds(direction, displayAreaBounds, mTmpBounds);
914             }
915 
916             if (!boundsConflict(taskBoundsToCheck, mTmpBounds)
917                     && displayAreaBounds.contains(mTmpBounds)) {
918                 // Found a candidate. Just use this.
919                 inOutBounds.set(mTmpBounds);
920                 if (DEBUG) appendLog("avoid-bounds-conflict=" + inOutBounds);
921                 return;
922             }
923 
924             // Didn't find a conflict free bounds here. Try the next candidate direction.
925         }
926 
927         // We failed to find a conflict free location. Just keep the original result.
928     }
929 
930     /**
931      * Determines scanning directions and their priorities to avoid bounds conflict.
932      *
933      * @param availableBounds bounds that the result must be in
934      * @param initialBounds initial bounds when start scanning
935      */
calculateCandidateShiftDirections(@onNull Rect availableBounds, @NonNull Rect initialBounds)936     private void calculateCandidateShiftDirections(@NonNull Rect availableBounds,
937             @NonNull Rect initialBounds) {
938         for (int i = 0; i < mTmpDirections.length; ++i) {
939             mTmpDirections[i] = Gravity.NO_GRAVITY;
940         }
941 
942         final int oneThirdWidth = (2 * availableBounds.left + availableBounds.right) / 3;
943         final int twoThirdWidth = (availableBounds.left + 2 * availableBounds.right) / 3;
944         final int centerX = initialBounds.centerX();
945         if (centerX < oneThirdWidth) {
946             // Too close to left, just scan to the right.
947             mTmpDirections[0] = Gravity.RIGHT;
948             return;
949         } else if (centerX > twoThirdWidth) {
950             // Too close to right, just scan to the left.
951             mTmpDirections[0] = Gravity.LEFT;
952             return;
953         }
954 
955         final int oneThirdHeight = (2 * availableBounds.top + availableBounds.bottom) / 3;
956         final int twoThirdHeight = (availableBounds.top + 2 * availableBounds.bottom) / 3;
957         final int centerY = initialBounds.centerY();
958         if (centerY < oneThirdHeight || centerY > twoThirdHeight) {
959             // Too close to top or bottom boundary and we're in the middle horizontally, scan
960             // horizontally in both directions.
961             mTmpDirections[0] = Gravity.RIGHT;
962             mTmpDirections[1] = Gravity.LEFT;
963             return;
964         }
965 
966         // We're in the center region both horizontally and vertically. Scan in both directions of
967         // primary diagonal.
968         mTmpDirections[0] = Gravity.BOTTOM | Gravity.RIGHT;
969         mTmpDirections[1] = Gravity.TOP | Gravity.LEFT;
970     }
971 
boundsConflict(@onNull List<Rect> taskBoundsToCheck, @NonNull Rect candidateBounds)972     private boolean boundsConflict(@NonNull List<Rect> taskBoundsToCheck,
973                                    @NonNull Rect candidateBounds) {
974         for (Rect taskBounds : taskBoundsToCheck) {
975             final boolean leftClose = Math.abs(taskBounds.left - candidateBounds.left)
976                     < BOUNDS_CONFLICT_THRESHOLD;
977             final boolean topClose = Math.abs(taskBounds.top - candidateBounds.top)
978                     < BOUNDS_CONFLICT_THRESHOLD;
979             final boolean rightClose = Math.abs(taskBounds.right - candidateBounds.right)
980                     < BOUNDS_CONFLICT_THRESHOLD;
981             final boolean bottomClose = Math.abs(taskBounds.bottom - candidateBounds.bottom)
982                     < BOUNDS_CONFLICT_THRESHOLD;
983 
984             if ((leftClose && topClose) || (leftClose && bottomClose) || (rightClose && topClose)
985                     || (rightClose && bottomClose)) {
986                 return true;
987             }
988         }
989 
990         return false;
991     }
992 
993     private void shiftBounds(int direction, @NonNull Rect availableRect,
994             @NonNull Rect inOutBounds) {
995         final int horizontalOffset;
996         switch (direction & Gravity.HORIZONTAL_GRAVITY_MASK) {
997             case Gravity.LEFT:
998                 horizontalOffset = -Math.max(MINIMAL_STEP,
999                         availableRect.width() / STEP_DENOMINATOR);
1000                 break;
1001             case Gravity.RIGHT:
1002                 horizontalOffset = Math.max(MINIMAL_STEP, availableRect.width() / STEP_DENOMINATOR);
1003                 break;
1004             default:
1005                 horizontalOffset = 0;
1006         }
1007 
1008         final int verticalOffset;
1009         switch (direction & Gravity.VERTICAL_GRAVITY_MASK) {
1010             case Gravity.TOP:
1011                 verticalOffset = -Math.max(MINIMAL_STEP, availableRect.height() / STEP_DENOMINATOR);
1012                 break;
1013             case Gravity.BOTTOM:
1014                 verticalOffset = Math.max(MINIMAL_STEP, availableRect.height() / STEP_DENOMINATOR);
1015                 break;
1016             default:
1017                 verticalOffset = 0;
1018         }
1019 
1020         inOutBounds.offset(horizontalOffset, verticalOffset);
1021     }
1022 
1023     private void initLogBuilder(Task task, ActivityRecord activity) {
1024         if (DEBUG) {
1025             mLogBuilder = new StringBuilder("TaskLaunchParamsModifier:task=" + task
1026                     + " activity=" + activity);
1027         }
1028     }
1029 
1030     private void appendLog(String log) {
1031         if (DEBUG) mLogBuilder.append(" ").append(log);
1032     }
1033 
1034     private void outputLog() {
1035         if (DEBUG) Slog.d(TAG, mLogBuilder.toString());
1036     }
1037 
1038     private static int orientationFromBounds(Rect bounds) {
1039         return bounds.width() > bounds.height() ? SCREEN_ORIENTATION_LANDSCAPE
1040                 : SCREEN_ORIENTATION_PORTRAIT;
1041     }
1042 
1043     private static boolean sizeMatches(Rect left, Rect right) {
1044         return (Math.abs(right.width() - left.width()) < EPSILON)
1045                 && (Math.abs(right.height() - left.height()) < EPSILON);
1046     }
1047 }
1048