diff options
8 files changed, 1760 insertions, 642 deletions
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index 5926af6f9082..0a4f4eb8506c 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1408,5 +1408,13 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { * @attr ref android.R.styleable#AndroidManifestLayout_minHeight */ public final int minHeight; + + /** + * Returns if this {@link WindowLayout} has specified bounds. + * @hide + */ + public boolean hasSpecifiedSize() { + return width >= 0 || height >= 0 || widthFraction >= 0 || heightFraction >= 0; + } } } diff --git a/services/core/java/com/android/server/am/ActivityLaunchParamsModifier.java b/services/core/java/com/android/server/am/ActivityLaunchParamsModifier.java deleted file mode 100644 index f44ee7a234ca..000000000000 --- a/services/core/java/com/android/server/am/ActivityLaunchParamsModifier.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.server.am; - -import android.app.ActivityOptions; -import android.content.pm.ActivityInfo; -import android.graphics.Rect; - -import com.android.server.am.LaunchParamsController.LaunchParams; -import com.android.server.am.LaunchParamsController.LaunchParamsModifier; - -/** - * An implementation of {@link LaunchParamsModifier}, which applies the launch bounds specified - * inside {@link ActivityOptions#getLaunchBounds()}. - */ -public class ActivityLaunchParamsModifier implements LaunchParamsModifier { - private final ActivityStackSupervisor mSupervisor; - - ActivityLaunchParamsModifier(ActivityStackSupervisor activityStackSupervisor) { - mSupervisor = activityStackSupervisor; - } - - @Override - public int onCalculate(TaskRecord task, ActivityInfo.WindowLayout layout, - ActivityRecord activity, ActivityRecord source, ActivityOptions options, - LaunchParams currentParams, LaunchParams outParams) { - // We only care about figuring out bounds for activities. - if (activity == null) { - return RESULT_SKIP; - } - - // Activity must be resizeable in the specified task. - if (!(mSupervisor.canUseActivityOptionsLaunchBounds(options) - && (activity.isResizeable() || (task != null && task.isResizeable())))) { - return RESULT_SKIP; - } - - final Rect bounds = options.getLaunchBounds(); - - // Bounds weren't valid. - if (bounds == null || bounds.isEmpty()) { - return RESULT_SKIP; - } - - outParams.mBounds.set(bounds); - - // When this is the most explicit position specification so we should not allow further - // modification of the position. - return RESULT_DONE; - } -} diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java index ea807adc7d4f..a368fa5f6620 100644 --- a/services/core/java/com/android/server/am/ActivityStack.java +++ b/services/core/java/com/android/server/am/ActivityStack.java @@ -163,7 +163,6 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Set; @@ -228,7 +227,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai } @Override - protected ConfigurationContainer getChildAt(int index) { + protected TaskRecord getChildAt(int index) { return mTaskHistory.get(index); } diff --git a/services/core/java/com/android/server/am/ActivityStarter.java b/services/core/java/com/android/server/am/ActivityStarter.java index de3b9cf3ccde..0acd079c4217 100644 --- a/services/core/java/com/android/server/am/ActivityStarter.java +++ b/services/core/java/com/android/server/am/ActivityStarter.java @@ -1606,13 +1606,18 @@ class ActivityStarter { mVoiceSession = voiceSession; mVoiceInteractor = voiceInteractor; - mPreferredDisplayId = getPreferedDisplayId(mSourceRecord, mStartActivity, options); - mLaunchParams.reset(); mSupervisor.getLaunchParamsController().calculate(inTask, null /*layout*/, r, sourceRecord, options, mLaunchParams); + if (mLaunchParams.hasPreferredDisplay()) { + mPreferredDisplayId = mLaunchParams.mPreferredDisplayId; + } else { + mPreferredDisplayId = DEFAULT_DISPLAY; + } + ensureValidPreferredDisplayId(r); + mLaunchMode = r.launchMode; mLaunchFlags = adjustLaunchFlagsToDocumentMode( @@ -1704,6 +1709,24 @@ class ActivityStarter { mNoAnimation = (mLaunchFlags & FLAG_ACTIVITY_NO_ANIMATION) != 0; } + /** + * Ensure preferred display ID matches the starting activity. + */ + private void ensureValidPreferredDisplayId(ActivityRecord startingActivity) { + // Check if the Activity is a VR activity. If so, the activity should be launched in + // main display. + if (startingActivity != null && startingActivity.requestedVrComponent != null) { + mPreferredDisplayId = DEFAULT_DISPLAY; + } + + // Get the virtual display ID from ActivityStackManagerService. If that's set we should + // always use that. + final int displayId = mService.mVr2dDisplayId; + if (displayId != INVALID_DISPLAY) { + mPreferredDisplayId = displayId; + } + } + private void sendNewTaskResultRequestIfNeeded() { final ActivityStack sourceStack = mStartActivity.resultTo != null ? mStartActivity.resultTo.getStack() : null; @@ -1883,44 +1906,6 @@ class ActivityStarter { } /** - * Returns the ID of the display to use for a new activity. If the device is in VR mode, - * then return the Vr mode's virtual display ID. If not, if the activity was started with - * a launchDisplayId, use that. Otherwise, if the source activity has a explicit display ID - * set, use that to launch the activity. - */ - private int getPreferedDisplayId( - ActivityRecord sourceRecord, ActivityRecord startingActivity, ActivityOptions options) { - // Check if the Activity is a VR activity. If so, the activity should be launched in - // main display. - if (startingActivity != null && startingActivity.requestedVrComponent != null) { - return DEFAULT_DISPLAY; - } - - // Get the virtual display id from ActivityManagerService. - int displayId = mService.mVr2dDisplayId; - if (displayId != INVALID_DISPLAY) { - if (DEBUG_STACK) { - Slog.d(TAG, "getSourceDisplayId :" + displayId); - } - return displayId; - } - - // If the caller requested a display, prefer that display. - final int launchDisplayId = - (options != null) ? options.getLaunchDisplayId() : INVALID_DISPLAY; - if (launchDisplayId != INVALID_DISPLAY) { - return launchDisplayId; - } - - displayId = sourceRecord != null ? sourceRecord.getDisplayId() : INVALID_DISPLAY; - // If the activity has a displayId set explicitly, launch it on the same displayId. - if (displayId != INVALID_DISPLAY) { - return displayId; - } - return DEFAULT_DISPLAY; - } - - /** * Figure out which task and activity to bring to front when we have found an existing matching * activity record in history. May also clear the task if needed. * @param intentActivity Existing matching activity. diff --git a/services/core/java/com/android/server/am/LaunchParamsController.java b/services/core/java/com/android/server/am/LaunchParamsController.java index 6415c3ee7f72..218d9080c2c0 100644 --- a/services/core/java/com/android/server/am/LaunchParamsController.java +++ b/services/core/java/com/android/server/am/LaunchParamsController.java @@ -16,6 +16,13 @@ package com.android.server.am; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.view.Display.INVALID_DISPLAY; + +import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE; +import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_DONE; +import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP; + import android.annotation.IntDef; import android.app.ActivityOptions; import android.content.pm.ActivityInfo.WindowLayout; @@ -26,13 +33,6 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import static android.view.Display.INVALID_DISPLAY; - -import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE; -import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_DONE; -import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP; - /** * {@link LaunchParamsController} calculates the {@link LaunchParams} by coordinating between * registered {@link LaunchParamsModifier}s. @@ -58,11 +58,7 @@ class LaunchParamsController { */ void registerDefaultModifiers(ActivityStackSupervisor supervisor) { // {@link TaskLaunchParamsModifier} handles window layout preferences. - registerModifier(new TaskLaunchParamsModifier()); - - // {@link ActivityLaunchParamsModifier} is the most specific modifier and thus should be - // registered last (applied first) out of the defaults. - registerModifier(new ActivityLaunchParamsModifier(supervisor)); + registerModifier(new TaskLaunchParamsModifier(supervisor)); } /** @@ -226,27 +222,41 @@ class LaunchParamsController { @IntDef({RESULT_SKIP, RESULT_DONE, RESULT_CONTINUE}) @interface Result {} - // Returned when the modifier does not want to influence the bounds calculation + /** Returned when the modifier does not want to influence the bounds calculation */ int RESULT_SKIP = 0; - // Returned when the modifier has changed the bounds and would like its results to be the - // final bounds applied. + /** + * Returned when the modifier has changed the bounds and would like its results to be the + * final bounds applied. + */ int RESULT_DONE = 1; - // Returned when the modifier has changed the bounds but is okay with other modifiers - // influencing the bounds. + /** + * Returned when the modifier has changed the bounds but is okay with other modifiers + * influencing the bounds. + */ int RESULT_CONTINUE = 2; /** - * Called when asked to calculate {@link LaunchParams}. - * @param task The {@link TaskRecord} currently being positioned. - * @param layout The specified {@link WindowLayout}. - * @param activity The {@link ActivityRecord} currently being positioned. - * @param source The {@link ActivityRecord} activity was started from. - * @param options The {@link ActivityOptions} specified for the activity. - * @param currentParams The current {@link LaunchParams}. This can differ from the initial - * params as it represents the modified params up to this point. - * @param outParams The resulting {@link LaunchParams} after all calculations. - * @return A {@link Result} representing the result of the - * {@link LaunchParams} calculation. + * Returns the launch params that the provided activity launch params should be overridden + * to. {@link LaunchParamsModifier} can use this for various purposes, including: 1) + * Providing default bounds if the launch bounds have not been provided. 2) Repositioning + * the task so it doesn't get placed over an existing task. 3) Resizing the task so that its + * dimensions match the activity's requested orientation. + * + * @param task Can be: 1) the target task in which the source activity wants to + * launch the target activity; 2) a newly created task that Android + * gives a chance to override its launching bounds; 3) {@code null} if + * this is called to override an activity's launching bounds. + * @param layout Desired layout when activity is first launched. + * @param activity Activity that is being started. This can be {@code null} on + * re-parenting an activity to a new task (e.g. for + * Picture-In-Picture). Tasks being created because an activity was + * launched should have this be non-null. + * @param source the Activity that launched a new task. Could be {@code null}. + * @param options {@link ActivityOptions} used to start the activity with. + * @param currentParams launching params after the process of last {@link + * LaunchParamsModifier}. + * @param outParams the result params to be set. + * @return see {@link LaunchParamsModifier.Result} */ @Result int onCalculate(TaskRecord task, WindowLayout layout, ActivityRecord activity, diff --git a/services/core/java/com/android/server/am/TaskLaunchParamsModifier.java b/services/core/java/com/android/server/am/TaskLaunchParamsModifier.java index 92f1cc34be92..fd34d180ebc0 100644 --- a/services/core/java/com/android/server/am/TaskLaunchParamsModifier.java +++ b/services/core/java/com/android/server/am/TaskLaunchParamsModifier.java @@ -16,304 +16,770 @@ package com.android.server.am; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT; +import static android.util.DisplayMetrics.DENSITY_DEFAULT; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.INVALID_DISPLAY; + import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityOptions; +import android.app.WindowConfiguration; import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; import android.graphics.Rect; +import android.os.Build; import android.util.Slog; import android.view.Gravity; -import com.android.internal.annotations.VisibleForTesting; + import com.android.server.am.LaunchParamsController.LaunchParams; import com.android.server.am.LaunchParamsController.LaunchParamsModifier; import java.util.ArrayList; +import java.util.List; /** - * Determines where a launching task should be positioned and sized on the display. - * - * The modifier is fairly simple. For the new task it tries default position based on the gravity - * and compares corners of the task with corners of existing tasks. If some two pairs of corners are - * sufficiently close enough, it shifts the bounds of the new task and tries again. When it exhausts - * all possible shifts, it gives up and puts the task in the original position. - * - * Note that the only gravities of concern are the corners and the center. + * The class that defines the default launch params for tasks. */ class TaskLaunchParamsModifier implements LaunchParamsModifier { private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskLaunchParamsModifier" : TAG_AM; + private static final boolean DEBUG = false; - // Determines how close window frames/corners have to be to call them colliding. - private static final int BOUNDS_CONFLICT_MIN_DISTANCE = 4; + // A mask for SUPPORTS_SCREEN that indicates the activity supports resize. + private static final int SUPPORTS_SCREEN_RESIZEABLE_MASK = + ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES + | ApplicationInfo.FLAG_SUPPORTS_LARGE_SCREENS + | ApplicationInfo.FLAG_SUPPORTS_SMALL_SCREENS + | ApplicationInfo.FLAG_RESIZEABLE_FOR_SCREENS + | ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES + | ApplicationInfo.FLAG_SUPPORTS_XLARGE_SCREENS; - // Task will receive dimensions based on available dimensions divided by this. - private static final int WINDOW_SIZE_DENOMINATOR = 2; + // Screen size of Nexus 5x + private static final int DEFAULT_PORTRAIT_PHONE_WIDTH_DP = 412; + private static final int DEFAULT_PORTRAIT_PHONE_HEIGHT_DP = 732; - // Task will receive margins based on available dimensions divided by this. - private static final int MARGIN_SIZE_DENOMINATOR = 4; + // Allowance of size matching. + private static final int EPSILON = 2; - // If task bounds collide with some other, we will step and try again until we find a good - // position. The step will be determined by using dimensions and dividing it by this. + // Cascade window offset. + private static final int CASCADING_OFFSET_DP = 75; + + // Threshold how close window corners have to be to call them colliding. + private static final int BOUNDS_CONFLICT_THRESHOLD = 4; + + // Divide display size by this number to get each step to adjust bounds to avoid conflict. private static final int STEP_DENOMINATOR = 16; // We always want to step by at least this. private static final int MINIMAL_STEP = 1; - // Used to indicate if positioning algorithm is allowed to restart from the beginning, when it - // reaches the end of stack bounds. - private static final boolean ALLOW_RESTART = true; + private final ActivityStackSupervisor mSupervisor; + private final Rect mTmpBounds = new Rect(); + private final int[] mTmpDirections = new int[2]; - private static final int SHIFT_POLICY_DIAGONAL_DOWN = 1; - private static final int SHIFT_POLICY_HORIZONTAL_RIGHT = 2; - private static final int SHIFT_POLICY_HORIZONTAL_LEFT = 3; + private StringBuilder mLogBuilder; - private final Rect mAvailableRect = new Rect(); - private final Rect mTmpProposal = new Rect(); - private final Rect mTmpOriginal = new Rect(); + TaskLaunchParamsModifier(ActivityStackSupervisor supervisor) { + mSupervisor = supervisor; + } - /** - * Tries to set task's bound in a way that it won't collide with any other task. By colliding - * we mean that two tasks have left-top corner very close to each other, so one might get - * obfuscated by the other one. - */ @Override public int onCalculate(TaskRecord task, ActivityInfo.WindowLayout layout, ActivityRecord activity, ActivityRecord source, ActivityOptions options, LaunchParams currentParams, LaunchParams outParams) { - // We can only apply positioning if we're in a freeform stack. - if (task == null || task.getStack() == null || !task.inFreeformWindowingMode()) { - return RESULT_SKIP; - } + initLogBuilder(task, activity); + final int result = calculate(task, layout, activity, source, options, currentParams, + outParams); + outputLog(); + return result; + } - final ArrayList<TaskRecord> tasks = task.getStack().getAllTasks(); + private int calculate(TaskRecord task, ActivityInfo.WindowLayout layout, + ActivityRecord activity, ActivityRecord source, ActivityOptions options, + LaunchParams currentParams, LaunchParams outParams) { + // STEP 1: Determine the display to launch the activity/task. + final int displayId = getPreferredLaunchDisplay(options, source, currentParams); + outParams.mPreferredDisplayId = displayId; + ActivityDisplay display = mSupervisor.getActivityDisplay(displayId); + if (DEBUG) { + appendLog("display-id=" + outParams.mPreferredDisplayId + " display-windowing-mode=" + + display.getWindowingMode()); + } - mAvailableRect.set(task.getParent().getBounds()); + final ActivityRecord root; + if (task != null) { + root = (task.getRootActivity() == null ? activity : task.getRootActivity()); + } else { + root = activity; + } + // STEP 2: Resolve launch windowing mode. + // STEP 2.1: Determine if any parameter has specified initial bounds. That might be the + // launch bounds from activity options, or size/gravity passed in layout. It also treat the + // launch windowing mode in options as a suggestion for future resolution. + int launchMode = options != null ? options.getLaunchWindowingMode() + : WINDOWING_MODE_UNDEFINED; + // hasInitialBounds is set if either activity options or layout has specified bounds. If + // that's set we'll skip some adjustments later to avoid overriding the initial bounds. + boolean hasInitialBounds = false; + final boolean canApplyFreeformPolicy = + canApplyFreeformWindowPolicy(display, root, launchMode); + if (mSupervisor.canUseActivityOptionsLaunchBounds(options) && canApplyFreeformPolicy) { + hasInitialBounds = true; + launchMode = launchMode == WINDOWING_MODE_UNDEFINED + ? WINDOWING_MODE_FREEFORM + : launchMode; + outParams.mBounds.set(options.getLaunchBounds()); + if (DEBUG) appendLog("activity-options-bounds=" + outParams.mBounds); + } else if (launchMode == WINDOWING_MODE_PINNED) { + // System controls PIP window's bounds, so don't apply launch bounds. + if (DEBUG) appendLog("empty-window-layout-for-pip"); + } else if (launchMode == WINDOWING_MODE_FULLSCREEN) { + if (DEBUG) appendLog("activity-options-fullscreen=" + outParams.mBounds); + } else if (layout != null && canApplyFreeformPolicy) { + getLayoutBounds(display, root, layout, mTmpBounds); + if (!mTmpBounds.isEmpty()) { + launchMode = WINDOWING_MODE_FREEFORM; + outParams.mBounds.set(mTmpBounds); + hasInitialBounds = true; + if (DEBUG) appendLog("bounds-from-layout=" + outParams.mBounds); + } else { + if (DEBUG) appendLog("empty-window-layout"); + } + } - final Rect resultBounds = outParams.mBounds; + // STEP 2.2: Check if previous modifier or the controller (referred as "callers" below) has + // some opinions on launch mode and launch bounds. If they have opinions and there is no + // initial bounds set in parameters. Note the check on display ID is also input param + // related because we always defer to callers' suggestion if there is no specific display ID + // in options or from source activity. + // + // If opinions from callers don't need any further resolution, we try to honor that as is as + // much as possible later. + + // Flag to indicate if current param needs no further resolution. It's true it current + // param isn't freeform mode, or it already has launch bounds. + boolean fullyResolvedCurrentParam = false; + // We inherit launch params from previous modifiers or LaunchParamsController if options, + // layout and display conditions are not contradictory to their suggestions. It's important + // to carry over their values because LaunchParamsController doesn't automatically do that. + if (!currentParams.isEmpty() && !hasInitialBounds + && (!currentParams.hasPreferredDisplay() + || displayId == currentParams.mPreferredDisplayId)) { + if (currentParams.hasWindowingMode()) { + launchMode = currentParams.mWindowingMode; + fullyResolvedCurrentParam = (launchMode != WINDOWING_MODE_FREEFORM); + if (DEBUG) { + appendLog("inherit-" + WindowConfiguration.windowingModeToString(launchMode)); + } + } - if (layout == null) { - positionCenter(tasks, mAvailableRect, getFreeformWidth(mAvailableRect), - getFreeformHeight(mAvailableRect), resultBounds); - return RESULT_CONTINUE; + if (!currentParams.mBounds.isEmpty()) { + outParams.mBounds.set(currentParams.mBounds); + fullyResolvedCurrentParam = true; + if (DEBUG) appendLog("inherit-bounds=" + outParams.mBounds); + } } - int width = getFinalWidth(layout, mAvailableRect); - int height = getFinalHeight(layout, mAvailableRect); - int verticalGravity = layout.gravity & Gravity.VERTICAL_GRAVITY_MASK; - int horizontalGravity = layout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; - if (verticalGravity == Gravity.TOP) { - if (horizontalGravity == Gravity.RIGHT) { - positionTopRight(tasks, mAvailableRect, width, height, resultBounds); - } else { - positionTopLeft(tasks, mAvailableRect, width, height, resultBounds); - } - } else if (verticalGravity == Gravity.BOTTOM) { - if (horizontalGravity == Gravity.RIGHT) { - positionBottomRight(tasks, mAvailableRect, width, height, resultBounds); - } else { - positionBottomLeft(tasks, mAvailableRect, width, height, resultBounds); + // STEP 2.3: Adjust launch parameters as needed for freeform display. We enforce the policy + // that legacy (pre-D) apps and those apps that can't handle multiple screen density well + // are forced to be maximized. The rest of this step is to define the default policy when + // there is no initial bounds or a fully resolved current params from callers. Right now we + // launch all possible tasks/activities that can handle freeform into freeform mode. + if (display.inFreeformWindowingMode()) { + if (launchMode == WINDOWING_MODE_PINNED) { + if (DEBUG) appendLog("picture-in-picture"); + } else if (isTaskForcedMaximized(root)) { + // We're launching an activity that probably can't handle resizing nicely, so force + // it to be maximized even someone suggests launching it in freeform using launch + // options. + launchMode = WINDOWING_MODE_FULLSCREEN; + outParams.mBounds.setEmpty(); + if (DEBUG) appendLog("forced-maximize"); + } else if (fullyResolvedCurrentParam) { + // Don't adjust launch mode if that's inherited, except when we're launching an + // activity that should be forced to maximize. + if (DEBUG) appendLog("skip-adjustment-fully-resolved-params"); + } else if (launchMode != WINDOWING_MODE_FREEFORM + && (isNOrGreater(root) || isPreNResizeable(root))) { + // We're launching a pre-N and post-D activity that supports resizing, or a post-N + // activity. They can handle freeform nicely so launch them in freeform. + // Use undefined because we know we're in a freeform display. + launchMode = WINDOWING_MODE_UNDEFINED; + if (DEBUG) appendLog("should-be-freeform"); } } else { - // Some fancy gravity setting that we don't support yet. We just put the activity in the - // center. - Slog.w(TAG, "Received unsupported gravity: " + layout.gravity - + ", positioning in the center instead."); - positionCenter(tasks, mAvailableRect, width, height, resultBounds); + if (DEBUG) appendLog("non-freeform-display"); + } + // If launch mode matches display windowing mode, let it inherit from display. + outParams.mWindowingMode = launchMode == display.getWindowingMode() + ? WINDOWING_MODE_UNDEFINED : launchMode; + + // STEP 3: Determine final launch bounds based on resolved windowing mode and activity + // requested orientation. We set bounds to empty for fullscreen mode and keep bounds as is + // for all other windowing modes that's not freeform mode. One can read comments in + // relevant methods to further understand this step. + // + // We skip making adjustments if the params are fully resolved from previous results and + // trust that they are valid. + if (!fullyResolvedCurrentParam) { + final int resolvedMode = (launchMode != WINDOWING_MODE_UNDEFINED) ? launchMode + : display.getWindowingMode(); + if (source != null && source.inFreeformWindowingMode() + && resolvedMode == WINDOWING_MODE_FREEFORM + && outParams.mBounds.isEmpty() + && source.getDisplayId() == display.mDisplayId) { + // Set bounds to be not very far from source activity. + cascadeBounds(source.getBounds(), display, outParams.mBounds); + } + getTaskBounds(root, display, layout, resolvedMode, hasInitialBounds, outParams.mBounds); } return RESULT_CONTINUE; } - @VisibleForTesting - static int getFreeformStartLeft(Rect bounds) { - return bounds.left + bounds.width() / MARGIN_SIZE_DENOMINATOR; + private int getPreferredLaunchDisplay(@Nullable ActivityOptions options, + ActivityRecord source, LaunchParams currentParams) { + int displayId = INVALID_DISPLAY; + final int optionLaunchId = options != null ? options.getLaunchDisplayId() : INVALID_DISPLAY; + if (optionLaunchId != INVALID_DISPLAY) { + if (DEBUG) appendLog("display-from-option=" + optionLaunchId); + displayId = optionLaunchId; + } + + if (displayId == INVALID_DISPLAY && source != null) { + final int sourceDisplayId = source.getDisplayId(); + if (DEBUG) appendLog("display-from-source=" + sourceDisplayId); + displayId = sourceDisplayId; + } + + if (displayId != INVALID_DISPLAY && mSupervisor.getActivityDisplay(displayId) == null) { + displayId = INVALID_DISPLAY; + } + displayId = (displayId == INVALID_DISPLAY) ? currentParams.mPreferredDisplayId : displayId; + + displayId = (displayId == INVALID_DISPLAY) ? DEFAULT_DISPLAY : displayId; + + return displayId; + } + + private boolean canApplyFreeformWindowPolicy(@NonNull ActivityDisplay display, + @NonNull ActivityRecord root, int launchMode) { + return display.inFreeformWindowingMode() || launchMode == WINDOWING_MODE_FREEFORM + || root.isResizeable(); } - @VisibleForTesting - static int getFreeformStartTop(Rect bounds) { - return bounds.top + bounds.height() / MARGIN_SIZE_DENOMINATOR; + private void getLayoutBounds(@NonNull ActivityDisplay display, @NonNull ActivityRecord root, + @NonNull ActivityInfo.WindowLayout windowLayout, @NonNull Rect outBounds) { + final int verticalGravity = windowLayout.gravity & Gravity.VERTICAL_GRAVITY_MASK; + final int horizontalGravity = windowLayout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + if (!windowLayout.hasSpecifiedSize() && verticalGravity == 0 && horizontalGravity == 0) { + outBounds.setEmpty(); + return; + } + + final Rect bounds = display.getBounds(); + final int defaultWidth = bounds.width(); + final int defaultHeight = bounds.height(); + + int width; + int height; + if (!windowLayout.hasSpecifiedSize()) { + outBounds.setEmpty(); + getTaskBounds(root, display, windowLayout, WINDOWING_MODE_FREEFORM, + /* hasInitialBounds */ false, outBounds); + width = outBounds.width(); + height = outBounds.height(); + } else { + width = defaultWidth; + if (windowLayout.width > 0 && windowLayout.width < defaultWidth) { + width = windowLayout.width; + } else if (windowLayout.widthFraction > 0 && windowLayout.widthFraction < 1.0f) { + width = (int) (width * windowLayout.widthFraction); + } + + height = defaultHeight; + if (windowLayout.height > 0 && windowLayout.height < defaultHeight) { + height = windowLayout.height; + } else if (windowLayout.heightFraction > 0 && windowLayout.heightFraction < 1.0f) { + height = (int) (height * windowLayout.heightFraction); + } + } + + final float fractionOfHorizontalOffset; + switch (horizontalGravity) { + case Gravity.LEFT: + fractionOfHorizontalOffset = 0f; + break; + case Gravity.RIGHT: + fractionOfHorizontalOffset = 1f; + break; + default: + fractionOfHorizontalOffset = 0.5f; + } + + final float fractionOfVerticalOffset; + switch (verticalGravity) { + case Gravity.TOP: + fractionOfVerticalOffset = 0f; + break; + case Gravity.BOTTOM: + fractionOfVerticalOffset = 1f; + break; + default: + fractionOfVerticalOffset = 0.5f; + } + + outBounds.set(0, 0, width, height); + final int xOffset = (int) (fractionOfHorizontalOffset * (defaultWidth - width)); + final int yOffset = (int) (fractionOfVerticalOffset * (defaultHeight - height)); + outBounds.offset(xOffset, yOffset); } - @VisibleForTesting - static int getFreeformWidth(Rect bounds) { - return bounds.width() / WINDOW_SIZE_DENOMINATOR; + /** + * Returns if task is forced to maximize. + * + * There are several cases where we force a task to maximize: + * 1) Root activity is targeting pre-Donut, which by default can't handle multiple screen + * densities, so resizing will likely cause issues; + * 2) Root activity doesn't declare any flag that it supports any screen density, so resizing + * may also cause issues; + * 3) Root activity is not resizeable, for which we shouldn't allow user resize it. + * + * @param root the root activity to check against. + * @return {@code true} if it should be forced to maximize; {@code false} otherwise. + */ + private boolean isTaskForcedMaximized(@NonNull ActivityRecord root) { + if (root.appInfo.targetSdkVersion < Build.VERSION_CODES.DONUT + || (root.appInfo.flags & SUPPORTS_SCREEN_RESIZEABLE_MASK) == 0) { + return true; + } + + return !root.isResizeable(); } - @VisibleForTesting - static int getFreeformHeight(Rect bounds) { - return bounds.height() / WINDOW_SIZE_DENOMINATOR; + private boolean isNOrGreater(@NonNull ActivityRecord root) { + return root.appInfo.targetSdkVersion >= Build.VERSION_CODES.N; + } + + /** + * Resolves activity requested orientation to 4 categories: + * 1) {@link ActivityInfo#SCREEN_ORIENTATION_LOCKED} indicating app wants to lock down + * orientation; + * 2) {@link ActivityInfo#SCREEN_ORIENTATION_LANDSCAPE} indicating app wants to be in landscape; + * 3) {@link ActivityInfo#SCREEN_ORIENTATION_PORTRAIT} indicating app wants to be in portrait; + * 4) {@link ActivityInfo#SCREEN_ORIENTATION_UNSPECIFIED} indicating app can handle any + * orientation. + * + * @param activity the activity to check + * @return corresponding resolved orientation value. + */ + private int resolveOrientation(@NonNull ActivityRecord activity) { + int orientation = activity.info.screenOrientation; + switch (orientation) { + case SCREEN_ORIENTATION_NOSENSOR: + case SCREEN_ORIENTATION_LOCKED: + orientation = SCREEN_ORIENTATION_LOCKED; + break; + case SCREEN_ORIENTATION_SENSOR_LANDSCAPE: + case SCREEN_ORIENTATION_REVERSE_LANDSCAPE: + case SCREEN_ORIENTATION_USER_LANDSCAPE: + case SCREEN_ORIENTATION_LANDSCAPE: + if (DEBUG) appendLog("activity-requested-landscape"); + orientation = SCREEN_ORIENTATION_LANDSCAPE; + break; + case SCREEN_ORIENTATION_SENSOR_PORTRAIT: + case SCREEN_ORIENTATION_REVERSE_PORTRAIT: + case SCREEN_ORIENTATION_USER_PORTRAIT: + case SCREEN_ORIENTATION_PORTRAIT: + if (DEBUG) appendLog("activity-requested-portrait"); + orientation = SCREEN_ORIENTATION_PORTRAIT; + break; + default: + orientation = SCREEN_ORIENTATION_UNSPECIFIED; + } + + return orientation; } - @VisibleForTesting - static int getHorizontalStep(Rect bounds) { - return Math.max(bounds.width() / STEP_DENOMINATOR, MINIMAL_STEP); + private boolean isPreNResizeable(ActivityRecord root) { + return root.appInfo.targetSdkVersion < Build.VERSION_CODES.N && root.isResizeable(); } - @VisibleForTesting - static int getVerticalStep(Rect bounds) { - return Math.max(bounds.height() / STEP_DENOMINATOR, MINIMAL_STEP); + private void cascadeBounds(@NonNull Rect srcBounds, @NonNull ActivityDisplay display, + @NonNull Rect outBounds) { + outBounds.set(srcBounds); + float density = (float) display.getConfiguration().densityDpi / DENSITY_DEFAULT; + final int defaultOffset = (int) (CASCADING_OFFSET_DP * density + 0.5f); + + display.getBounds(mTmpBounds); + final int dx = Math.min(defaultOffset, Math.max(0, mTmpBounds.right - srcBounds.right)); + final int dy = Math.min(defaultOffset, Math.max(0, mTmpBounds.bottom - srcBounds.bottom)); + outBounds.offset(dx, dy); } + private void getTaskBounds(@NonNull ActivityRecord root, @NonNull ActivityDisplay display, + @NonNull ActivityInfo.WindowLayout layout, int resolvedMode, boolean hasInitialBounds, + @NonNull Rect inOutBounds) { + if (resolvedMode == WINDOWING_MODE_FULLSCREEN) { + // We don't handle letterboxing here. Letterboxing will be handled by valid checks + // later. + inOutBounds.setEmpty(); + if (DEBUG) appendLog("maximized-bounds"); + return; + } + if (resolvedMode != WINDOWING_MODE_FREEFORM) { + // We don't apply freeform bounds adjustment to other windowing modes. + if (DEBUG) { + appendLog("skip-bounds-" + WindowConfiguration.windowingModeToString(resolvedMode)); + } + return; + } - private int getFinalWidth(ActivityInfo.WindowLayout windowLayout, Rect availableRect) { - int width = getFreeformWidth(availableRect); - if (windowLayout.width > 0) { - width = windowLayout.width; + final int orientation = resolveOrientation(root, display, inOutBounds); + if (orientation != SCREEN_ORIENTATION_PORTRAIT + && orientation != SCREEN_ORIENTATION_LANDSCAPE) { + throw new IllegalStateException( + "Orientation must be one of portrait or landscape, but it's " + + ActivityInfo.screenOrientationToString(orientation)); } - if (windowLayout.widthFraction > 0) { - width = (int) (availableRect.width() * windowLayout.widthFraction); + + // First we get the default size we want. + getDefaultFreeformSize(display, layout, orientation, mTmpBounds); + if (hasInitialBounds || sizeMatches(inOutBounds, mTmpBounds)) { + // We're here because either input parameters specified initial bounds, or the suggested + // bounds have the same size of the default freeform size. We should use the suggested + // bounds if possible -- so if app can handle the orientation we just use it, and if not + // we transpose the suggested bounds in-place. + if (orientation == orientationFromBounds(inOutBounds)) { + if (DEBUG) appendLog("freeform-size-orientation-match=" + inOutBounds); + } else { + // Meh, orientation doesn't match. Let's rotate inOutBounds in-place. + centerBounds(display, inOutBounds.height(), inOutBounds.width(), inOutBounds); + if (DEBUG) appendLog("freeform-orientation-mismatch=" + inOutBounds); + } + } else { + // We are here either because there is no suggested bounds, or the suggested bounds is + // a cascade from source activity. We should use the default freeform size and center it + // to the center of suggested bounds (or the display if no suggested bounds). The + // default size might be too big to center to source activity bounds in display, so we + // may need to move it back to the display. + centerBounds(display, mTmpBounds.width(), mTmpBounds.height(), inOutBounds); + adjustBoundsToFitInDisplay(display, inOutBounds); + if (DEBUG) appendLog("freeform-size-mismatch=" + inOutBounds); } - return width; + + // Lastly we adjust bounds to avoid conflicts with other tasks as much as possible. + adjustBoundsToAvoidConflict(display, inOutBounds); } - private int getFinalHeight(ActivityInfo.WindowLayout windowLayout, Rect availableRect) { - int height = getFreeformHeight(availableRect); - if (windowLayout.height > 0) { - height = windowLayout.height; + private int resolveOrientation(@NonNull ActivityRecord root, @NonNull ActivityDisplay display, + @NonNull Rect bounds) { + int orientation = resolveOrientation(root); + + if (orientation == SCREEN_ORIENTATION_LOCKED) { + orientation = bounds.isEmpty() ? display.getConfiguration().orientation + : orientationFromBounds(bounds); + if (DEBUG) { + appendLog(bounds.isEmpty() ? "locked-orientation-from-display=" + orientation + : "locked-orientation-from-bounds=" + bounds); + } } - if (windowLayout.heightFraction > 0) { - height = (int) (availableRect.height() * windowLayout.heightFraction); + + if (orientation == SCREEN_ORIENTATION_UNSPECIFIED) { + orientation = bounds.isEmpty() ? SCREEN_ORIENTATION_PORTRAIT + : orientationFromBounds(bounds); + if (DEBUG) { + appendLog(bounds.isEmpty() ? "default-portrait" + : "orientation-from-bounds=" + bounds); + } } - return height; - } - private void positionBottomLeft(ArrayList<TaskRecord> tasks, Rect availableRect, int width, - int height, Rect result) { - mTmpProposal.set(availableRect.left, availableRect.bottom - height, - availableRect.left + width, availableRect.bottom); - position(tasks, availableRect, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_RIGHT, - result); + return orientation; } - private void positionBottomRight(ArrayList<TaskRecord> tasks, Rect availableRect, int width, - int height, Rect result) { - mTmpProposal.set(availableRect.right - width, availableRect.bottom - height, - availableRect.right, availableRect.bottom); - position(tasks, availableRect, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_LEFT, - result); + private void getDefaultFreeformSize(@NonNull ActivityDisplay display, + @NonNull ActivityInfo.WindowLayout layout, int orientation, @NonNull Rect bounds) { + // Default size, which is letterboxing/pillarboxing in display. That's to say the large + // dimension of default size is the small dimension of display size, and the small dimension + // of default size is calculated to keep the same aspect ratio as the display's. + Rect displayBounds = display.getBounds(); + final int portraitHeight = Math.min(displayBounds.width(), displayBounds.height()); + final int otherDimension = Math.max(displayBounds.width(), displayBounds.height()); + final int portraitWidth = (portraitHeight * portraitHeight) / otherDimension; + final int defaultWidth = (orientation == SCREEN_ORIENTATION_LANDSCAPE) ? portraitHeight + : portraitWidth; + final int defaultHeight = (orientation == SCREEN_ORIENTATION_LANDSCAPE) ? portraitWidth + : portraitHeight; + + // Get window size based on Nexus 5x screen, we assume that this is enough to show content + // of activities. + final float density = (float) display.getConfiguration().densityDpi / DENSITY_DEFAULT; + final int phonePortraitWidth = (int) (DEFAULT_PORTRAIT_PHONE_WIDTH_DP * density + 0.5f); + final int phonePortraitHeight = (int) (DEFAULT_PORTRAIT_PHONE_HEIGHT_DP * density + 0.5f); + final int phoneWidth = (orientation == SCREEN_ORIENTATION_LANDSCAPE) ? phonePortraitHeight + : phonePortraitWidth; + final int phoneHeight = (orientation == SCREEN_ORIENTATION_LANDSCAPE) ? phonePortraitWidth + : phonePortraitHeight; + + // Minimum layout requirements. + final int layoutMinWidth = (layout == null) ? -1 : layout.minWidth; + final int layoutMinHeight = (layout == null) ? -1 : layout.minHeight; + + // Final result. + final int width = Math.min(defaultWidth, Math.max(phoneWidth, layoutMinWidth)); + final int height = Math.min(defaultHeight, Math.max(phoneHeight, layoutMinHeight)); + + bounds.set(0, 0, width, height); } - private void positionTopLeft(ArrayList<TaskRecord> tasks, Rect availableRect, int width, - int height, Rect result) { - mTmpProposal.set(availableRect.left, availableRect.top, - availableRect.left + width, availableRect.top + height); - position(tasks, availableRect, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_RIGHT, - result); + /** + * Gets centered bounds of width x height. If inOutBounds is not empty, the result bounds + * centers at its center or display's center if inOutBounds is empty. + */ + private void centerBounds(@NonNull ActivityDisplay display, int width, int height, + @NonNull Rect inOutBounds) { + if (inOutBounds.isEmpty()) { + display.getBounds(inOutBounds); + } + final int left = inOutBounds.centerX() - width / 2; + final int top = inOutBounds.centerY() - height / 2; + inOutBounds.set(left, top, left + width, top + height); } - private void positionTopRight(ArrayList<TaskRecord> tasks, Rect availableRect, int width, - int height, Rect result) { - mTmpProposal.set(availableRect.right - width, availableRect.top, - availableRect.right, availableRect.top + height); - position(tasks, availableRect, mTmpProposal, !ALLOW_RESTART, SHIFT_POLICY_HORIZONTAL_LEFT, - result); - } + private void adjustBoundsToFitInDisplay(@NonNull ActivityDisplay display, + @NonNull Rect inOutBounds) { + final Rect displayBounds = display.getBounds(); + + if (displayBounds.width() < inOutBounds.width() + || displayBounds.height() < inOutBounds.height()) { + // There is no way for us to fit the bounds in the display without changing width + // or height. Don't even try it. + return; + } + + final int dx; + if (inOutBounds.right > displayBounds.right) { + // Right edge is out of display. + dx = displayBounds.right - inOutBounds.right; + } else if (inOutBounds.left < displayBounds.left) { + // Left edge is out of display. + dx = displayBounds.left - inOutBounds.left; + } else { + // Vertical edges are all in display. + dx = 0; + } - private void positionCenter(ArrayList<TaskRecord> tasks, Rect availableRect, int width, - int height, Rect result) { - final int defaultFreeformLeft = getFreeformStartLeft(availableRect); - final int defaultFreeformTop = getFreeformStartTop(availableRect); - mTmpProposal.set(defaultFreeformLeft, defaultFreeformTop, - defaultFreeformLeft + width, defaultFreeformTop + height); - position(tasks, availableRect, mTmpProposal, ALLOW_RESTART, SHIFT_POLICY_DIAGONAL_DOWN, - result); + final int dy; + if (inOutBounds.top < displayBounds.top) { + // Top edge is out of display. + dy = displayBounds.top - inOutBounds.top; + } else if (inOutBounds.bottom > displayBounds.bottom) { + // Bottom edge is out of display. + dy = displayBounds.bottom - inOutBounds.bottom; + } else { + // Horizontal edges are all in display. + dy = 0; + } + inOutBounds.offset(dx, dy); } - private void position(ArrayList<TaskRecord> tasks, Rect availableRect, - Rect proposal, boolean allowRestart, int shiftPolicy, Rect result) { - mTmpOriginal.set(proposal); - boolean restarted = false; - while (boundsConflict(proposal, tasks)) { - // Unfortunately there is already a task at that spot, so we need to look for some - // other place. - shiftStartingPoint(proposal, availableRect, shiftPolicy); - if (shiftedTooFar(proposal, availableRect, shiftPolicy)) { - // We don't want the task to go outside of the stack, because it won't look - // nice. Depending on the starting point we either restart, or immediately give up. - if (!allowRestart) { - proposal.set(mTmpOriginal); - break; - } - // We must have started not from the top. Let's restart from there because there - // might be some space there. - proposal.set(availableRect.left, availableRect.top, - availableRect.left + proposal.width(), - availableRect.top + proposal.height()); - restarted = true; + /** + * Adjusts input bounds to avoid conflict with existing tasks in the display. + * + * If the input bounds conflict with existing tasks, this method scans the bounds in a series of + * directions to find a location where the we can put the bounds in display without conflict + * with any other tasks. + * + * It doesn't try to adjust bounds that's not fully in the given display. + * + * @param display the display which tasks are to check + * @param inOutBounds the bounds used to input initial bounds and output result bounds + */ + private void adjustBoundsToAvoidConflict(@NonNull ActivityDisplay display, + @NonNull Rect inOutBounds) { + final Rect displayBounds = display.getBounds(); + if (!displayBounds.contains(inOutBounds)) { + // The initial bounds are already out of display. The scanning algorithm below doesn't + // work so well with them. + return; + } + + final List<TaskRecord> tasksToCheck = new ArrayList<>(); + for (int i = 0; i < display.getChildCount(); ++i) { + ActivityStack<?> stack = display.getChildAt(i); + if (!stack.inFreeformWindowingMode()) { + continue; } - if (restarted && (proposal.left > getFreeformStartLeft(availableRect) - || proposal.top > getFreeformStartTop(availableRect))) { - // If we restarted and crossed the initial position, let's not struggle anymore. - // The user already must have ton of tasks visible, we can just smack the new - // one in the center. - proposal.set(mTmpOriginal); + + for (int j = 0; j < stack.getChildCount(); ++j) { + tasksToCheck.add(stack.getChildAt(j)); + } + } + + if (!boundsConflict(tasksToCheck, inOutBounds)) { + // Current proposal doesn't conflict with any task. Early return to avoid unnecessary + // calculation. + return; + } + + calculateCandidateShiftDirections(displayBounds, inOutBounds); + for (int direction : mTmpDirections) { + if (direction == Gravity.NO_GRAVITY) { + // We exhausted candidate directions, give up. break; } + + mTmpBounds.set(inOutBounds); + while (boundsConflict(tasksToCheck, mTmpBounds) && displayBounds.contains(mTmpBounds)) { + shiftBounds(direction, displayBounds, mTmpBounds); + } + + if (!boundsConflict(tasksToCheck, mTmpBounds) && displayBounds.contains(mTmpBounds)) { + // Found a candidate. Just use this. + inOutBounds.set(mTmpBounds); + if (DEBUG) appendLog("avoid-bounds-conflict=" + inOutBounds); + return; + } + + // Didn't find a conflict free bounds here. Try the next candidate direction. } - result.set(proposal); + + // We failed to find a conflict free location. Just keep the original result. } - private boolean shiftedTooFar(Rect start, Rect availableRect, int shiftPolicy) { - switch (shiftPolicy) { - case SHIFT_POLICY_HORIZONTAL_LEFT: - return start.left < availableRect.left; - case SHIFT_POLICY_HORIZONTAL_RIGHT: - return start.right > availableRect.right; - default: // SHIFT_POLICY_DIAGONAL_DOWN - return start.right > availableRect.right || start.bottom > availableRect.bottom; + /** + * Determines scanning directions and their priorities to avoid bounds conflict. + * + * @param availableBounds bounds that the result must be in + * @param initialBounds initial bounds when start scanning + */ + private void calculateCandidateShiftDirections(@NonNull Rect availableBounds, + @NonNull Rect initialBounds) { + for (int i = 0; i < mTmpDirections.length; ++i) { + mTmpDirections[i] = Gravity.NO_GRAVITY; + } + + final int oneThirdWidth = (2 * availableBounds.left + availableBounds.right) / 3; + final int twoThirdWidth = (availableBounds.left + 2 * availableBounds.right) / 3; + final int centerX = initialBounds.centerX(); + if (centerX < oneThirdWidth) { + // Too close to left, just scan to the right. + mTmpDirections[0] = Gravity.RIGHT; + return; + } else if (centerX > twoThirdWidth) { + // Too close to right, just scan to the left. + mTmpDirections[0] = Gravity.LEFT; + return; } + + final int oneThirdHeight = (2 * availableBounds.top + availableBounds.bottom) / 3; + final int twoThirdHeight = (availableBounds.top + 2 * availableBounds.bottom) / 3; + final int centerY = initialBounds.centerY(); + if (centerY < oneThirdHeight || centerY > twoThirdHeight) { + // Too close to top or bottom boundary and we're in the middle horizontally, scan + // horizontally in both directions. + mTmpDirections[0] = Gravity.RIGHT; + mTmpDirections[1] = Gravity.LEFT; + return; + } + + // We're in the center region both horizontally and vertically. Scan in both directions of + // primary diagonal. + mTmpDirections[0] = Gravity.BOTTOM | Gravity.RIGHT; + mTmpDirections[1] = Gravity.TOP | Gravity.LEFT; } - private void shiftStartingPoint(Rect posposal, Rect availableRect, int shiftPolicy) { - final int defaultFreeformStepHorizontal = getHorizontalStep(availableRect); - final int defaultFreeformStepVertical = getVerticalStep(availableRect); + private boolean boundsConflict(@NonNull List<TaskRecord> tasks, @NonNull Rect bounds) { + for (TaskRecord task : tasks) { + final Rect taskBounds = task.getBounds(); + final boolean leftClose = Math.abs(taskBounds.left - bounds.left) + < BOUNDS_CONFLICT_THRESHOLD; + final boolean topClose = Math.abs(taskBounds.top - bounds.top) + < BOUNDS_CONFLICT_THRESHOLD; + final boolean rightClose = Math.abs(taskBounds.right - bounds.right) + < BOUNDS_CONFLICT_THRESHOLD; + final boolean bottomClose = Math.abs(taskBounds.bottom - bounds.bottom) + < BOUNDS_CONFLICT_THRESHOLD; + + if ((leftClose && topClose) || (leftClose && bottomClose) || (rightClose && topClose) + || (rightClose && bottomClose)) { + return true; + } + } + + return false; + } - switch (shiftPolicy) { - case SHIFT_POLICY_HORIZONTAL_LEFT: - posposal.offset(-defaultFreeformStepHorizontal, 0); + private void shiftBounds(int direction, @NonNull Rect availableRect, + @NonNull Rect inOutBounds) { + final int horizontalOffset; + switch (direction & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.LEFT: + horizontalOffset = -Math.max(MINIMAL_STEP, + availableRect.width() / STEP_DENOMINATOR); break; - case SHIFT_POLICY_HORIZONTAL_RIGHT: - posposal.offset(defaultFreeformStepHorizontal, 0); + case Gravity.RIGHT: + horizontalOffset = Math.max(MINIMAL_STEP, availableRect.width() / STEP_DENOMINATOR); break; - default: // SHIFT_POLICY_DIAGONAL_DOWN: - posposal.offset(defaultFreeformStepHorizontal, defaultFreeformStepVertical); + default: + horizontalOffset = 0; + } + + final int verticalOffset; + switch (direction & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.TOP: + verticalOffset = -Math.max(MINIMAL_STEP, availableRect.height() / STEP_DENOMINATOR); break; + case Gravity.BOTTOM: + verticalOffset = Math.max(MINIMAL_STEP, availableRect.height() / STEP_DENOMINATOR); + break; + default: + verticalOffset = 0; } + + inOutBounds.offset(horizontalOffset, verticalOffset); } - private static boolean boundsConflict(Rect proposal, ArrayList<TaskRecord> tasks) { - for (int i = tasks.size() - 1; i >= 0; i--) { - final TaskRecord task = tasks.get(i); - if (!task.mActivities.isEmpty() && !task.matchParentBounds()) { - final Rect bounds = task.getOverrideBounds(); - if (closeLeftTopCorner(proposal, bounds) || closeRightTopCorner(proposal, bounds) - || closeLeftBottomCorner(proposal, bounds) - || closeRightBottomCorner(proposal, bounds)) { - return true; - } - } + private void initLogBuilder(TaskRecord task, ActivityRecord activity) { + if (DEBUG) { + mLogBuilder = new StringBuilder("TaskLaunchParamsModifier:task=" + task + + " activity=" + activity); } - return false; } - private static final boolean closeLeftTopCorner(Rect first, Rect second) { - return Math.abs(first.left - second.left) < BOUNDS_CONFLICT_MIN_DISTANCE - && Math.abs(first.top - second.top) < BOUNDS_CONFLICT_MIN_DISTANCE; + private void appendLog(String log) { + if (DEBUG) mLogBuilder.append(" ").append(log); } - private static final boolean closeRightTopCorner(Rect first, Rect second) { - return Math.abs(first.right - second.right) < BOUNDS_CONFLICT_MIN_DISTANCE - && Math.abs(first.top - second.top) < BOUNDS_CONFLICT_MIN_DISTANCE; + private void outputLog() { + if (DEBUG) Slog.d(TAG, mLogBuilder.toString()); } - private static final boolean closeLeftBottomCorner(Rect first, Rect second) { - return Math.abs(first.left - second.left) < BOUNDS_CONFLICT_MIN_DISTANCE - && Math.abs(first.bottom - second.bottom) < BOUNDS_CONFLICT_MIN_DISTANCE; + private static int orientationFromBounds(Rect bounds) { + return bounds.width() > bounds.height() ? SCREEN_ORIENTATION_LANDSCAPE + : SCREEN_ORIENTATION_PORTRAIT; } - private static final boolean closeRightBottomCorner(Rect first, Rect second) { - return Math.abs(first.right - second.right) < BOUNDS_CONFLICT_MIN_DISTANCE - && Math.abs(first.bottom - second.bottom) < BOUNDS_CONFLICT_MIN_DISTANCE; + private static boolean sizeMatches(Rect left, Rect right) { + return (Math.abs(right.width() - left.width()) < EPSILON) + && (Math.abs(right.height() - left.height()) < EPSILON); } } diff --git a/services/tests/servicestests/src/com/android/server/am/ActivityLaunchParamsModifierTests.java b/services/tests/servicestests/src/com/android/server/am/ActivityLaunchParamsModifierTests.java deleted file mode 100644 index 9de64f2cf211..000000000000 --- a/services/tests/servicestests/src/com/android/server/am/ActivityLaunchParamsModifierTests.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.server.am; - -import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; - -import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_DONE; -import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import android.app.ActivityOptions; -import android.content.pm.ActivityInfo; -import android.graphics.Rect; -import android.platform.test.annotations.Presubmit; - -import androidx.test.filters.MediumTest; -import androidx.test.runner.AndroidJUnit4; - -import com.android.server.am.LaunchParamsController.LaunchParams; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Tests for exercising resizing bounds due to activity options. - * - * Build/Install/Run: - * atest FrameworksServicesTests:ActivityLaunchParamsModifierTests - */ -@MediumTest -@Presubmit -@RunWith(AndroidJUnit4.class) -public class ActivityLaunchParamsModifierTests extends ActivityTestsBase { - private ActivityLaunchParamsModifier mModifier; - private ActivityTaskManagerService mService; - private ActivityStack mStack; - private TaskRecord mTask; - private ActivityRecord mActivity; - - private LaunchParams mCurrent; - private LaunchParams mResult; - - @Before - @Override - public void setUp() throws Exception { - super.setUp(); - mService = createActivityTaskManagerService(); - mModifier = new ActivityLaunchParamsModifier(mService.mStackSupervisor); - mCurrent = new LaunchParams(); - mResult = new LaunchParams(); - - - mStack = mService.mStackSupervisor.getDefaultDisplay().createStack( - WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, true /* onTop */); - mTask = new TaskBuilder(mService.mStackSupervisor).setStack(mStack).build(); - mActivity = new ActivityBuilder(mService).setTask(mTask).build(); - } - - - @Test - public void testSkippedInvocations() throws Exception { - // No specified activity should be ignored - assertEquals(RESULT_SKIP, mModifier.onCalculate(null /*task*/, null /*layout*/, - null /*activity*/, null /*source*/, null /*options*/, mCurrent, mResult)); - - // No specified activity options should be ignored - assertEquals(RESULT_SKIP, mModifier.onCalculate(null /*task*/, null /*layout*/, - mActivity, null /*source*/, null /*options*/, mCurrent, mResult)); - - // launch bounds specified should be ignored. - final ActivityOptions options = ActivityOptions.makeBasic(); - assertEquals(RESULT_SKIP, mModifier.onCalculate(null /*task*/, null /*layout*/, - mActivity, null /*source*/, options /*options*/, mCurrent, mResult)); - - // Non-resizeable records should be ignored - mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_UNRESIZEABLE; - assertFalse(mActivity.isResizeable()); - assertEquals(RESULT_SKIP, mModifier.onCalculate(null /*task*/, null /*layout*/, - mActivity, null /*source*/, options /*options*/, mCurrent, mResult)); - - // make record resizeable - mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_RESIZEABLE; - assertTrue(mActivity.isResizeable()); - - assertEquals(RESULT_SKIP, mModifier.onCalculate(null /*task*/, null /*layout*/, - mActivity, null /*source*/, options /*options*/, mCurrent, mResult)); - - // Does not support freeform - mService.mSupportsFreeformWindowManagement = false; - assertFalse(mService.mStackSupervisor.canUseActivityOptionsLaunchBounds(options)); - assertEquals(RESULT_SKIP, mModifier.onCalculate(null /*task*/, null /*layout*/, - mActivity, null /*source*/, options /*options*/, mCurrent, mResult)); - - mService.mSupportsFreeformWindowManagement = true; - options.setLaunchBounds(new Rect()); - assertTrue(mService.mStackSupervisor.canUseActivityOptionsLaunchBounds(options)); - - // Invalid bounds - assertEquals(RESULT_SKIP, mModifier.onCalculate(null /*task*/, null /*layout*/, - mActivity, null /*source*/, options /*options*/, mCurrent, mResult)); - options.setLaunchBounds(new Rect(0, 0, -1, -1)); - assertEquals(RESULT_SKIP, mModifier.onCalculate(null /*task*/, null /*layout*/, - mActivity, null /*source*/, options /*options*/, mCurrent, mResult)); - - // Valid bounds should cause the positioner to be applied. - options.setLaunchBounds(new Rect(0, 0, 100, 100)); - assertEquals(RESULT_DONE, mModifier.onCalculate(null /*task*/, null /*layout*/, - mActivity, null /*source*/, options /*options*/, mCurrent, mResult)); - } - - @Test - public void testBoundsExtraction() throws Exception { - // Make activity resizeable and enable freeform mode. - mActivity.info.resizeMode = ActivityInfo.RESIZE_MODE_RESIZEABLE; - mService.mSupportsFreeformWindowManagement = true; - - ActivityOptions options = ActivityOptions.makeBasic(); - final Rect proposedBounds = new Rect(20, 30, 45, 40); - options.setLaunchBounds(proposedBounds); - - assertEquals(RESULT_DONE, mModifier.onCalculate(null /*task*/, null /*layout*/, - mActivity, null /*source*/, options /*options*/, mCurrent, mResult)); - assertEquals(mResult.mBounds, proposedBounds); - } -} diff --git a/services/tests/servicestests/src/com/android/server/am/TaskLaunchParamsModifierTests.java b/services/tests/servicestests/src/com/android/server/am/TaskLaunchParamsModifierTests.java index f5b8f78cfd53..0d1302f78316 100644 --- a/services/tests/servicestests/src/com/android/server/am/TaskLaunchParamsModifierTests.java +++ b/services/tests/servicestests/src/com/android/server/am/TaskLaunchParamsModifierTests.java @@ -11,239 +11,1098 @@ * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and - * limitations under the License + * limitations under the License. */ package com.android.server.am; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; +import static android.util.DisplayMetrics.DENSITY_DEFAULT; +import static android.view.Display.DEFAULT_DISPLAY; import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; -import android.content.pm.ActivityInfo.WindowLayout; +import android.app.ActivityOptions; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.res.Configuration; import android.graphics.Rect; +import android.os.Build; import android.platform.test.annotations.Presubmit; import android.view.Gravity; -import androidx.test.filters.MediumTest; +import androidx.test.filters.SmallTest; +import androidx.test.filters.FlakyTest; import androidx.test.runner.AndroidJUnit4; +import com.android.server.am.LaunchParamsController.LaunchParams; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Locale; + /** - * Tests for exercising resizing task bounds. + * Tests for default task bounds. * * Build/Install/Run: * atest FrameworksServicesTests:TaskLaunchParamsModifierTests */ -@MediumTest +@SmallTest @Presubmit @RunWith(AndroidJUnit4.class) +@FlakyTest(detail = "Confirm stable in post-submit before removing") public class TaskLaunchParamsModifierTests extends ActivityTestsBase { - private final static int STACK_WIDTH = 100; - private final static int STACK_HEIGHT = 200; - - private final static Rect STACK_BOUNDS = new Rect(0, 0, STACK_WIDTH, STACK_HEIGHT); - private ActivityTaskManagerService mService; - private ActivityStack mStack; - private TaskRecord mTask; + private ActivityRecord mActivity; - private TaskLaunchParamsModifier mPositioner; + private TaskLaunchParamsModifier mTarget; - private LaunchParamsController.LaunchParams mCurrent; - private LaunchParamsController.LaunchParams mResult; + private LaunchParams mCurrent; + private LaunchParams mResult; @Before @Override public void setUp() throws Exception { super.setUp(); - mService = createActivityTaskManagerService(); - mStack = mService.mStackSupervisor.getDefaultDisplay().createStack( - WINDOWING_MODE_FREEFORM, ACTIVITY_TYPE_STANDARD, true /* onTop */); - mStack.requestResize(STACK_BOUNDS); + setupActivityTaskManagerService(); + mService.mSupportsFreeformWindowManagement = true; + when(mSupervisor.canUseActivityOptionsLaunchBounds(any())).thenCallRealMethod(); + + mActivity = new ActivityBuilder(mService).build(); + mActivity.appInfo.targetSdkVersion = Build.VERSION_CODES.N_MR1; + mActivity.info.applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES; + + mTarget = new TaskLaunchParamsModifier(mSupervisor); + + mCurrent = new LaunchParams(); + mCurrent.reset(); + mResult = new LaunchParams(); + mResult.reset(); + } - // We must create the task after resizing to make sure it does not inherit the stack - // dimensions on resize. - mTask = new TaskBuilder(mService.mStackSupervisor).setStack(mStack).build(); + // ============================= + // Display ID Related Tests + // ============================= + @Test + public void testDefaultToPrimaryDisplay() { + createNewActivityDisplay(WINDOWING_MODE_FREEFORM); - mPositioner = new TaskLaunchParamsModifier(); + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, /* options */ null, mCurrent, mResult)); - mResult = new LaunchParamsController.LaunchParams(); - mCurrent = new LaunchParamsController.LaunchParams(); + assertEquals(DEFAULT_DISPLAY, mResult.mPreferredDisplayId); } - /** - * Ensures that the setup bounds are set as expected with the stack bounds set and the task - * bounds still {@code null}. - * @throws Exception - */ @Test - public void testInitialBounds() throws Exception { - assertEquals(mStack.getOverrideBounds(), STACK_BOUNDS); - assertEquals(mTask.getOverrideBounds(), new Rect()); + public void testUsesPreviousDisplayIdIfSet() { + createNewActivityDisplay(WINDOWING_MODE_FREEFORM); + final TestActivityDisplay display = createNewActivityDisplay(WINDOWING_MODE_FULLSCREEN); + + mCurrent.mPreferredDisplayId = display.mDisplayId; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(display.mDisplayId, mResult.mPreferredDisplayId); } - /** - * Ensures that a task positioned with no {@link WindowLayout} receives the default launch - * position. - * @throws Exception - */ @Test - public void testLaunchNoWindowLayout() throws Exception { - assertEquals(RESULT_CONTINUE, mPositioner.onCalculate(mTask, null /*layout*/, - null /*record*/, null /*source*/, null /*options*/, mCurrent, mResult)); - assertEquals(getDefaultBounds(Gravity.NO_GRAVITY), mResult.mBounds); + public void testUsesSourcesDisplayIdIfSet() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + final TestActivityDisplay fullscreenDisplay = createNewActivityDisplay( + WINDOWING_MODE_FULLSCREEN); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + ActivityRecord source = createSourceActivity(fullscreenDisplay); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, source, /* options */ null, mCurrent, mResult)); + + assertEquals(fullscreenDisplay.mDisplayId, mResult.mPreferredDisplayId); } - /** - * Ensures that a task positioned with an empty {@link WindowLayout} receives the default launch - * position. - * @throws Exception - */ @Test - public void testlaunchEmptyWindowLayout() throws Exception { - assertEquals(RESULT_CONTINUE, mPositioner.onCalculate(mTask, - new WindowLayout(0, 0, 0, 0, Gravity.NO_GRAVITY, 0, 0), null /*activity*/, - null /*source*/, null /*options*/, mCurrent, mResult)); - assertEquals(mResult.mBounds, getDefaultBounds(Gravity.NO_GRAVITY)); + public void testUsesOptionsDisplayIdIfSet() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + final TestActivityDisplay fullscreenDisplay = createNewActivityDisplay( + WINDOWING_MODE_FULLSCREEN); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + ActivityRecord source = createSourceActivity(freeformDisplay); + + ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(fullscreenDisplay.mDisplayId); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, source, options, mCurrent, mResult)); + + assertEquals(fullscreenDisplay.mDisplayId, mResult.mPreferredDisplayId); } - /** - * Ensures that a task positioned with a {@link WindowLayout} gravity specified is positioned - * according to specification. - * @throws Exception - */ + // ===================================== + // Launch Windowing Mode Related Tests + // ===================================== @Test - public void testlaunchWindowLayoutGravity() throws Exception { - // Unspecified gravity should be ignored - testGravity(Gravity.NO_GRAVITY); + public void testBoundsInOptionsInfersFreeformOnFreeformDisplay() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchBounds(new Rect(0, 0, 100, 100)); - // Unsupported gravity should be ignored - testGravity(Gravity.LEFT); - testGravity(Gravity.RIGHT); + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; - // Test defaults for vertical gravities - testGravity(Gravity.TOP); - testGravity(Gravity.BOTTOM); + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); - // Test corners - testGravity(Gravity.TOP | Gravity.LEFT); - testGravity(Gravity.TOP | Gravity.RIGHT); - testGravity(Gravity.BOTTOM | Gravity.LEFT); - testGravity(Gravity.BOTTOM | Gravity.RIGHT); + assertEquivalentWindowingMode(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode, + WINDOWING_MODE_FREEFORM); } - private void testGravity(int gravity) { - try { - assertEquals(RESULT_CONTINUE, mPositioner.onCalculate(mTask, - new WindowLayout(0, 0, 0, 0, gravity, 0, 0), null /*activity*/, - null /*source*/, null /*options*/, mCurrent, mResult)); - assertEquals(mResult.mBounds, getDefaultBounds(gravity)); - } finally { - mCurrent.reset(); - mResult.reset(); - } + @Test + public void testBoundsInOptionsInfersFreeformWithResizeableActivity() { + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchBounds(new Rect(0, 0, 100, 100)); + + mCurrent.mPreferredDisplayId = DEFAULT_DISPLAY; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode, + WINDOWING_MODE_FULLSCREEN); + } + + @Test + public void testKeepsPictureInPictureLaunchModeInOptions() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchWindowingMode(WINDOWING_MODE_PINNED); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_PINNED, mResult.mWindowingMode, + WINDOWING_MODE_FREEFORM); + } + + @Test + public void testKeepsPictureInPictureLaunchModeWithBoundsInOptions() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchWindowingMode(WINDOWING_MODE_PINNED); + options.setLaunchBounds(new Rect(0, 0, 100, 100)); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_PINNED, mResult.mWindowingMode, + WINDOWING_MODE_FREEFORM); + } + + @Test + public void testKeepsFullscreenLaunchModeInOptionsOnNonFreeformDisplay() { + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN); + + mCurrent.mPreferredDisplayId = DEFAULT_DISPLAY; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_FULLSCREEN, mResult.mWindowingMode, + WINDOWING_MODE_FULLSCREEN); + } + + @Test + public void testNonEmptyLayoutInfersFreeformOnFreeformDisplay() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setWidth(120).setHeight(80).build(); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode, + WINDOWING_MODE_FREEFORM); + } + + @Test + public void testNonEmptyLayoutInfersFreeformWithEmptySize() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setGravity(Gravity.LEFT).build(); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode, + WINDOWING_MODE_FREEFORM); + } + + @Test + public void testNonEmptyLayoutInfersFreeformWithResizeableActivity() { + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setWidth(120).setHeight(80).build(); + + mCurrent.mPreferredDisplayId = DEFAULT_DISPLAY; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode, + WINDOWING_MODE_FULLSCREEN); + } + + @Test + public void testRespectsFullyResolvedCurrentParam_Fullscreen() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + mCurrent.mWindowingMode = WINDOWING_MODE_FULLSCREEN; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_FULLSCREEN, mResult.mWindowingMode, + WINDOWING_MODE_FREEFORM); + } + + @Test + public void testRespectsModeFromFullyResolvedCurrentParam_NonEmptyBounds() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM; + mCurrent.mBounds.set(0, 0, 200, 100); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode, + WINDOWING_MODE_FREEFORM); + } + + @Test + public void testForceMaximizesPreDApp() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM); + options.setLaunchBounds(new Rect(0, 0, 200, 100)); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM; + mCurrent.mBounds.set(0, 0, 200, 100); + + mActivity.appInfo.targetSdkVersion = Build.VERSION_CODES.CUPCAKE; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_FULLSCREEN, mResult.mWindowingMode, + WINDOWING_MODE_FREEFORM); + } + + @Test + public void testForceMaximizesAppWithoutMultipleDensitySupport() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM); + options.setLaunchBounds(new Rect(0, 0, 200, 100)); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM; + mCurrent.mBounds.set(0, 0, 200, 100); + + mActivity.appInfo.flags = 0; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_FULLSCREEN, mResult.mWindowingMode, + WINDOWING_MODE_FREEFORM); + } + + @Test + public void testForceMaximizesUnresizeableApp() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM); + options.setLaunchBounds(new Rect(0, 0, 200, 100)); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM; + mCurrent.mBounds.set(0, 0, 200, 100); + + mActivity.info.resizeMode = RESIZE_MODE_UNRESIZEABLE; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_FULLSCREEN, mResult.mWindowingMode, + WINDOWING_MODE_FREEFORM); + } + + @Test + public void testSkipsForceMaximizingAppsOnNonFreeformDisplay() { + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM); + options.setLaunchBounds(new Rect(0, 0, 200, 100)); + + mCurrent.mPreferredDisplayId = DEFAULT_DISPLAY; + mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM; + mCurrent.mBounds.set(0, 0, 200, 100); + + mActivity.info.resizeMode = RESIZE_MODE_UNRESIZEABLE; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode, + WINDOWING_MODE_FULLSCREEN); + } + + @Test + public void testUsesFullscreenOnNonFreeformDisplay() { + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(DEFAULT_DISPLAY); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_FULLSCREEN, mResult.mWindowingMode, + WINDOWING_MODE_FULLSCREEN); + } + + @Test + public void testUsesFreeformByDefaultForPostNApp() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode, + WINDOWING_MODE_FREEFORM); + } + + @Test + public void testUsesFreeformByDefaultForPreNResizeableAppWithoutOrientationRequest() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + + mActivity.appInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_FREEFORM, mResult.mWindowingMode, + WINDOWING_MODE_FREEFORM); + } + + @Test + public void testSkipsFreeformForPreNResizeableAppOnNonFullscreenDisplay() { + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(DEFAULT_DISPLAY); + + mActivity.appInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquivalentWindowingMode(WINDOWING_MODE_FULLSCREEN, mResult.mWindowingMode, + WINDOWING_MODE_FULLSCREEN); + } + + // ================================ + // Launching Bounds Related Tests + // =============================== + @Test + public void testKeepsBoundsWithPictureInPictureLaunchModeInOptions() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchWindowingMode(WINDOWING_MODE_PINNED); + + final Rect expected = new Rect(0, 0, 100, 100); + options.setLaunchBounds(expected); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquals(expected, mResult.mBounds); + } + + @Test + public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_LeftGravity() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setGravity(Gravity.LEFT).build(); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(0, mResult.mBounds.left); + } + + @Test + public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_TopGravity() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setGravity(Gravity.TOP).build(); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(0, mResult.mBounds.top); + } + + @Test + public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_TopLeftGravity() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setGravity(Gravity.TOP | Gravity.LEFT).build(); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(0, mResult.mBounds.left); + assertEquals(0, mResult.mBounds.top); + } + + @Test + public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_RightGravity() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setGravity(Gravity.RIGHT).build(); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(1920, mResult.mBounds.right); + } + + @Test + public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_BottomGravity() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setGravity(Gravity.BOTTOM).build(); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(1080, mResult.mBounds.bottom); + } + + @Test + public void testNonEmptyLayoutBoundsRespectsGravityWithEmptySize_BottomRightGravity() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setGravity(Gravity.BOTTOM | Gravity.RIGHT).build(); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(1920, mResult.mBounds.right); + assertEquals(1080, mResult.mBounds.bottom); + } + + @Test + public void testNonEmptyLayoutBoundsOnFreeformDisplay_CenterToDisplay() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setWidth(120).setHeight(80).build(); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(new Rect(900, 500, 1020, 580), mResult.mBounds); + } + + @Test + public void testNonEmptyLayoutBoundsOnFreeformDisplay_LeftGravity() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setWidth(120).setHeight(80).setGravity(Gravity.LEFT).build(); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(new Rect(0, 500, 120, 580), mResult.mBounds); + } + + @Test + public void testNonEmptyLayoutBoundsOnFreeformDisplay_TopGravity() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setWidth(120).setHeight(80).setGravity(Gravity.TOP).build(); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(new Rect(900, 0, 1020, 80), mResult.mBounds); + } + + @Test + public void testNonEmptyLayoutBoundsOnFreeformDisplay_TopLeftGravity() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setWidth(120).setHeight(80).setGravity(Gravity.TOP | Gravity.LEFT).build(); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(new Rect(0, 0, 120, 80), mResult.mBounds); + } + + @Test + public void testNonEmptyLayoutBoundsOnFreeformDisplay_RightGravity() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setWidth(120).setHeight(80).setGravity(Gravity.RIGHT).build(); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(new Rect(1800, 500, 1920, 580), mResult.mBounds); + } + + @Test + public void testNonEmptyLayoutBoundsOnFreeformDisplay_BottomGravity() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setWidth(120).setHeight(80).setGravity(Gravity.BOTTOM).build(); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(new Rect(900, 1000, 1020, 1080), mResult.mBounds); + } + + @Test + public void testNonEmptyLayoutBoundsOnFreeformDisplay_RightBottomGravity() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setWidth(120).setHeight(80).setGravity(Gravity.BOTTOM | Gravity.RIGHT).build(); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(new Rect(1800, 1000, 1920, 1080), mResult.mBounds); + } + + @Test + public void testNonEmptyLayoutFractionBoundsOnFreeformDisplay() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setWidthFraction(0.0625f).setHeightFraction(0.1f).build(); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(new Rect(900, 486, 1020, 594), mResult.mBounds); + } + + @Test + public void testNonEmptyLayoutBoundsWithResizeableActivity() { + final ActivityDisplay display = mSupervisor.getActivityDisplay(DEFAULT_DISPLAY); + display.setBounds(new Rect(0, 0, 1920, 1080)); + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setWidth(120).setHeight(80).build(); + + mCurrent.mPreferredDisplayId = DEFAULT_DISPLAY; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(new Rect(900, 500, 1020, 580), mResult.mBounds); + } + + @Test + public void testRespectBoundsFromFullyResolvedCurrentParam_NonEmptyBounds() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId; + mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM; + mCurrent.mBounds.set(0, 0, 200, 100); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, /* options */ null, mCurrent, mResult)); + + assertEquals(new Rect(0, 0, 200, 100), mResult.mBounds); + } + + @Test + public void testDefaultSizeSmallerThanBigScreen() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + + mActivity.appInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + final int resultWidth = mResult.mBounds.width(); + final int displayWidth = freeformDisplay.getBounds().width(); + assertTrue("Result width " + resultWidth + " is not smaller than " + displayWidth, + resultWidth < displayWidth); + + final int resultHeight = mResult.mBounds.height(); + final int displayHeight = freeformDisplay.getBounds().height(); + assertTrue("Result width " + resultHeight + " is not smaller than " + + displayHeight, resultHeight < displayHeight); + } + + @Test + public void testDefaultFreeformSizeCenteredToDisplay() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + + mActivity.appInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + final Rect displayBounds = freeformDisplay.getBounds(); + assertEquals("Distance to left and right should be equal.", + mResult.mBounds.left - displayBounds.left, + displayBounds.right - mResult.mBounds.right); + assertEquals("Distance to top and bottom should be equal.", + mResult.mBounds.top - displayBounds.top, + displayBounds.bottom - mResult.mBounds.bottom); + } + + @Test + public void testCascadesToSourceSizeForFreeform() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + + final ActivityRecord source = createSourceActivity(freeformDisplay); + source.setBounds(0, 0, 412, 732); + + mActivity.appInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, source, options, mCurrent, mResult)); + + final Rect displayBounds = freeformDisplay.getBounds(); + assertTrue("Left bounds should be larger than 0.", mResult.mBounds.left > 0); + assertTrue("Top bounds should be larger than 0.", mResult.mBounds.top > 0); + assertTrue("Bounds should be centered at somewhere in the left half, but it's " + + "centerX is " + mResult.mBounds.centerX(), + mResult.mBounds.centerX() < displayBounds.centerX()); + assertTrue("Bounds should be centered at somewhere in the top half, but it's " + + "centerY is " + mResult.mBounds.centerY(), + mResult.mBounds.centerY() < displayBounds.centerY()); + } + + @Test + public void testAdjustBoundsToFitDisplay_TopLeftOutOfDisplay() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + + final ActivityRecord source = createSourceActivity(freeformDisplay); + source.setBounds(0, 0, 200, 400); + + mActivity.appInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, source, options, mCurrent, mResult)); + + final Rect displayBounds = freeformDisplay.getBounds(); + assertTrue("display bounds doesn't contain result. display bounds: " + + displayBounds + " result: " + mResult.mBounds, + displayBounds.contains(mResult.mBounds)); + } + + @Test + public void testAdjustBoundsToFitDisplay_BottomRightOutOfDisplay() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + + final ActivityRecord source = createSourceActivity(freeformDisplay); + source.setBounds(1720, 680, 1920, 1080); + + mActivity.appInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, source, options, mCurrent, mResult)); + + final Rect displayBounds = freeformDisplay.getBounds(); + assertTrue("display bounds doesn't contain result. display bounds: " + + displayBounds + " result: " + mResult.mBounds, + displayBounds.contains(mResult.mBounds)); + } + + @Test + public void testAdjustBoundsToFitDisplay_LargerThanDisplay() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + Configuration overrideConfig = new Configuration(); + overrideConfig.setTo(mSupervisor.getOverrideConfiguration()); + overrideConfig.setLayoutDirection(new Locale("ar")); + mSupervisor.onConfigurationChanged(overrideConfig); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + + final ActivityRecord source = createSourceActivity(freeformDisplay); + source.setBounds(1720, 680, 1920, 1080); + + mActivity.appInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, source, options, mCurrent, mResult)); + + final Rect displayBounds = freeformDisplay.getBounds(); + assertTrue("display bounds doesn't contain result. display bounds: " + + displayBounds + " result: " + mResult.mBounds, + displayBounds.contains(mResult.mBounds)); } - /** - * Ensures that a task which causes a conflict with another task when positioned is adjusted as - * expected. - * @throws Exception - */ - @Test - public void testLaunchWindowCenterConflict() throws Exception { - testConflict(Gravity.NO_GRAVITY); - testConflict(Gravity.TOP); - testConflict(Gravity.BOTTOM); - testConflict(Gravity.TOP | Gravity.LEFT); - testConflict(Gravity.TOP | Gravity.RIGHT); - testConflict(Gravity.BOTTOM | Gravity.LEFT); - testConflict(Gravity.BOTTOM | Gravity.RIGHT); - } - - private void testConflict(int gravity) { - final WindowLayout layout = new WindowLayout(0, 0, 0, 0, gravity, 0, 0); - - // layout first task - mService.mStackSupervisor.getLaunchParamsController().layoutTask(mTask, layout); - - // Second task will be laid out on top of the first so starting bounds is the same. - final Rect expectedBounds = new Rect(mTask.getOverrideBounds()); - - ActivityRecord activity = null; - TaskRecord secondTask = null; - - // wrap with try/finally to ensure cleanup of activity/stack. - try { - // empty tasks are ignored in conflicts - activity = new ActivityBuilder(mService).setTask(mTask).build(); - - // Create secondary task - secondTask = new TaskBuilder(mService.mStackSupervisor).setStack(mStack).build(); - - // layout second task - assertEquals(RESULT_CONTINUE, - mPositioner.onCalculate(secondTask, layout, null /*activity*/, - null /*source*/, null /*options*/, mCurrent, mResult)); - - if ((gravity & (Gravity.TOP | Gravity.RIGHT)) == (Gravity.TOP | Gravity.RIGHT) - || (gravity & (Gravity.BOTTOM | Gravity.RIGHT)) - == (Gravity.BOTTOM | Gravity.RIGHT)) { - expectedBounds.offset(-TaskLaunchParamsModifier.getHorizontalStep( - mStack.getOverrideBounds()), 0); - } else if ((gravity & Gravity.TOP) == Gravity.TOP - || (gravity & Gravity.BOTTOM) == Gravity.BOTTOM) { - expectedBounds.offset( - TaskLaunchParamsModifier.getHorizontalStep(mStack.getOverrideBounds()), 0); - } else { - expectedBounds.offset( - TaskLaunchParamsModifier.getHorizontalStep(mStack.getOverrideBounds()), - TaskLaunchParamsModifier.getVerticalStep(mStack.getOverrideBounds())); - } - - assertEquals(mResult.mBounds, expectedBounds); - } finally { - // Remove task and activity to prevent influencing future tests - if (activity != null) { - mTask.removeActivity(activity); - } - - if (secondTask != null) { - mStack.removeTask(secondTask, "cleanup", ActivityStack.REMOVE_TASK_MODE_DESTROYING); - } + @Test + public void testRespectsLayoutMinDimensions() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + + final ActivityInfo.WindowLayout layout = new WindowLayoutBuilder() + .setMinWidth(500).setMinHeight(800).build(); + + mActivity.appInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, layout, mActivity, + /* source */ null, options, mCurrent, mResult)); + + assertEquals(500, mResult.mBounds.width()); + assertEquals(800, mResult.mBounds.height()); + } + + @Test + public void testRotatesInPlaceInitialBoundsMismatchOrientation() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + options.setLaunchBounds(new Rect(100, 100, 500, 300)); + + mActivity.info.screenOrientation = SCREEN_ORIENTATION_PORTRAIT; + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquals(new Rect(200, 0, 400, 400), mResult.mBounds); + } + + @Test + public void testShiftsToRightForCloseToLeftBoundsWhenConflict() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + addFreeformTaskTo(freeformDisplay, new Rect(50, 50, 100, 150)); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + options.setLaunchBounds(new Rect(50, 50, 500, 300)); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquals(new Rect(170, 50, 620, 300), mResult.mBounds); + } + + @Test + public void testShiftsToLeftForCloseToRightBoundsWhenConflict() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + addFreeformTaskTo(freeformDisplay, new Rect(1720, 50, 1830, 150)); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + options.setLaunchBounds(new Rect(1720, 50, 1850, 300)); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquals(new Rect(1600, 50, 1730, 300), mResult.mBounds); + } + + @Test + public void testShiftsToRightFirstForHorizontallyCenteredAndCloseToTopWhenConflict() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + addFreeformTaskTo(freeformDisplay, new Rect(0, 0, 100, 300)); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + options.setLaunchBounds(new Rect(0, 0, 1800, 200)); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquals(new Rect(120, 0, 1920, 200), mResult.mBounds); + } + + @Test + public void testShiftsToLeftNoSpaceOnRightForHorizontallyCenteredAndCloseToTopWhenConflict() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + addFreeformTaskTo(freeformDisplay, new Rect(120, 0, 240, 300)); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + options.setLaunchBounds(new Rect(120, 0, 1860, 200)); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquals(new Rect(0, 0, 1740, 200), mResult.mBounds); + } + + @Test + public void testShiftsToBottomRightFirstForCenteredBoundsWhenConflict() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + addFreeformTaskTo(freeformDisplay, new Rect(120, 0, 240, 100)); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + options.setLaunchBounds(new Rect(120, 0, 1800, 1013)); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquals(new Rect(240, 67, 1920, 1080), mResult.mBounds); + } + + @Test + public void testShiftsToTopLeftIfNoSpaceOnBottomRightForCenteredBoundsWhenConflict() { + final TestActivityDisplay freeformDisplay = createNewActivityDisplay( + WINDOWING_MODE_FREEFORM); + + addFreeformTaskTo(freeformDisplay, new Rect(120, 67, 240, 100)); + + final ActivityOptions options = ActivityOptions.makeBasic(); + options.setLaunchDisplayId(freeformDisplay.mDisplayId); + options.setLaunchBounds(new Rect(120, 67, 1800, 1020)); + + assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null, + mActivity, /* source */ null, options, mCurrent, mResult)); + + assertEquals(new Rect(0, 0, 1680, + 953), mResult.mBounds); + } + + private TestActivityDisplay createNewActivityDisplay(int windowingMode) { + final TestActivityDisplay display = addNewActivityDisplayAt(ActivityDisplay.POSITION_TOP); + display.setWindowingMode(windowingMode); + display.setBounds(/* left */ 0, /* top */ 0, /* right */ 1920, /* bottom */ 1080); + display.getConfiguration().densityDpi = DENSITY_DEFAULT; + return display; + } + + private ActivityRecord createSourceActivity(TestActivityDisplay display) { + final TestActivityStack stack = display.createStack(display.getWindowingMode(), + ACTIVITY_TYPE_STANDARD, true); + return new ActivityBuilder(mService).setStack(stack).setCreateTask(true).build(); + } + + private void addFreeformTaskTo(TestActivityDisplay display, Rect bounds) { + final TestActivityStack stack = display.createStack(display.getWindowingMode(), + ACTIVITY_TYPE_STANDARD, true); + stack.setWindowingMode(WINDOWING_MODE_FREEFORM); + final TaskRecord task = new TaskBuilder(mSupervisor).setStack(stack).build(); + task.setBounds(bounds); + } + + private void assertEquivalentWindowingMode(int expected, int actual, int parentWindowingMode) { + if (expected != parentWindowingMode) { + assertEquals(expected, actual); + } else { + assertEquals(WINDOWING_MODE_UNDEFINED, actual); } } - private Rect getDefaultBounds(int gravity) { - final Rect bounds = new Rect(); - bounds.set(mStack.getOverrideBounds()); + private static class WindowLayoutBuilder { + private int mWidth = -1; + private int mHeight = -1; + private float mWidthFraction = -1f; + private float mHeightFraction = -1f; + private int mGravity = Gravity.NO_GRAVITY; + private int mMinWidth = -1; + private int mMinHeight = -1; - final int verticalInset = - TaskLaunchParamsModifier.getFreeformStartTop(mStack.getOverrideBounds()); - final int horizontalInset = - TaskLaunchParamsModifier.getFreeformStartLeft(mStack.getOverrideBounds()); + private WindowLayoutBuilder setWidth(int width) { + mWidth = width; + return this; + } + + private WindowLayoutBuilder setHeight(int height) { + mHeight = height; + return this; + } - bounds.inset(horizontalInset, verticalInset); + private WindowLayoutBuilder setWidthFraction(float widthFraction) { + mWidthFraction = widthFraction; + return this; + } + + private WindowLayoutBuilder setHeightFraction(float heightFraction) { + mHeightFraction = heightFraction; + return this; + } - if ((gravity & (Gravity.TOP | Gravity.RIGHT)) == (Gravity.TOP | Gravity.RIGHT)) { - bounds.offsetTo(horizontalInset * 2, 0); - } else if ((gravity & Gravity.TOP) == Gravity.TOP) { - bounds.offsetTo(0, 0); - } else if ((gravity & (Gravity.BOTTOM | Gravity.RIGHT)) - == (Gravity.BOTTOM | Gravity.RIGHT)) { - bounds.offsetTo(horizontalInset * 2, verticalInset * 2); - } else if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) { - bounds.offsetTo(0, verticalInset * 2); + private WindowLayoutBuilder setGravity(int gravity) { + mGravity = gravity; + return this; } - return bounds; + private WindowLayoutBuilder setMinWidth(int minWidth) { + mMinWidth = minWidth; + return this; + } + + private WindowLayoutBuilder setMinHeight(int minHeight) { + mMinHeight = minHeight; + return this; + } + + private ActivityInfo.WindowLayout build() { + return new ActivityInfo.WindowLayout(mWidth, mWidthFraction, mHeight, mHeightFraction, + mGravity, mMinWidth, mMinHeight); + } } } |