/*
|
* Copyright (C) 2015 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.wm;
|
|
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.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
|
import static com.android.server.wm.ActivityTaskManagerDebugConfig.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.content.res.Configuration;
|
import android.graphics.Rect;
|
import android.os.Build;
|
import android.util.Slog;
|
import android.view.Gravity;
|
import android.view.View;
|
|
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.server.wm.LaunchParamsController.LaunchParams;
|
import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
|
|
import java.util.ArrayList;
|
import java.util.List;
|
|
/**
|
* 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_ATM;
|
private static final boolean DEBUG = false;
|
|
// 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;
|
|
// 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;
|
|
// Allowance of size matching.
|
private static final int EPSILON = 2;
|
|
// 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;
|
|
private final ActivityStackSupervisor mSupervisor;
|
private final Rect mTmpBounds = new Rect();
|
private final int[] mTmpDirections = new int[2];
|
|
private StringBuilder mLogBuilder;
|
|
TaskLaunchParamsModifier(ActivityStackSupervisor supervisor) {
|
mSupervisor = supervisor;
|
}
|
|
@VisibleForTesting
|
int onCalculate(TaskRecord task, ActivityInfo.WindowLayout layout, ActivityRecord activity,
|
ActivityRecord source, ActivityOptions options, LaunchParams currentParams,
|
LaunchParams outParams) {
|
return onCalculate(task, layout, activity, source, options, PHASE_BOUNDS, currentParams,
|
outParams);
|
}
|
|
@Override
|
public int onCalculate(TaskRecord task, ActivityInfo.WindowLayout layout,
|
ActivityRecord activity, ActivityRecord source, ActivityOptions options,
|
int phase, LaunchParams currentParams, LaunchParams outParams) {
|
initLogBuilder(task, activity);
|
final int result = calculate(task, layout, activity, source, options, phase, currentParams,
|
outParams);
|
outputLog();
|
return result;
|
}
|
|
private int calculate(TaskRecord task, ActivityInfo.WindowLayout layout,
|
ActivityRecord activity, ActivityRecord source, ActivityOptions options, int phase,
|
LaunchParams currentParams, LaunchParams outParams) {
|
final ActivityRecord root;
|
if (task != null) {
|
root = task.getRootActivity() == null ? activity : task.getRootActivity();
|
} else {
|
root = activity;
|
}
|
|
// TODO: Investigate whether we can safely ignore all cases where we don't have root
|
// activity available. Note we can't know if the bounds are valid if we're not sure of the
|
// requested orientation of the root activity. Therefore if we found such a case we may need
|
// to pass the activity into this modifier in that case.
|
if (root == null) {
|
// There is a case that can lead us here. The caller is moving the top activity that is
|
// in a task that has multiple activities to PIP mode. For that the caller is creating a
|
// new task to host the activity so that we only move the top activity to PIP mode and
|
// keep other activities in the previous task. There is no point to apply the launch
|
// logic in this case.
|
return RESULT_SKIP;
|
}
|
|
// STEP 1: Determine the display to launch the activity/task.
|
final int displayId = getPreferredLaunchDisplay(task, options, source, currentParams);
|
outParams.mPreferredDisplayId = displayId;
|
ActivityDisplay display = mSupervisor.mRootActivityContainer.getActivityDisplay(displayId);
|
if (DEBUG) {
|
appendLog("display-id=" + outParams.mPreferredDisplayId + " display-windowing-mode="
|
+ display.getWindowingMode());
|
}
|
|
if (phase == PHASE_DISPLAY) {
|
return RESULT_CONTINUE;
|
}
|
|
// 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 treats 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, launchMode);
|
if (mSupervisor.canUseActivityOptionsLaunchBounds(options)
|
&& (canApplyFreeformPolicy || canApplyPipWindowPolicy(launchMode))) {
|
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");
|
}
|
}
|
|
// 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 (!currentParams.mBounds.isEmpty()) {
|
// Carry over bounds from callers regardless of launch mode because bounds is still
|
// used to restore last non-fullscreen bounds when launch mode is not freeform.
|
// Therefore it's not a resolution step for non-freeform launch mode and only
|
// consider it fully resolved only when launch mode is freeform.
|
outParams.mBounds.set(currentParams.mBounds);
|
if (launchMode == WINDOWING_MODE_FREEFORM) {
|
fullyResolvedCurrentParam = true;
|
if (DEBUG) appendLog("inherit-bounds=" + outParams.mBounds);
|
}
|
}
|
}
|
|
// 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.
|
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 (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;
|
|
if (phase == PHASE_WINDOWING_MODE) {
|
return RESULT_CONTINUE;
|
}
|
|
// 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.
|
final int resolvedMode = (launchMode != WINDOWING_MODE_UNDEFINED) ? launchMode
|
: display.getWindowingMode();
|
if (fullyResolvedCurrentParam) {
|
if (resolvedMode == WINDOWING_MODE_FREEFORM) {
|
// Make sure bounds are in the display if it's possibly in a different display.
|
if (currentParams.mPreferredDisplayId != displayId) {
|
adjustBoundsToFitInDisplay(display, outParams.mBounds);
|
}
|
// Even though we want to keep original bounds, we still don't want it to stomp on
|
// an existing task.
|
adjustBoundsToAvoidConflictInDisplay(display, outParams.mBounds);
|
}
|
} else {
|
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;
|
}
|
|
private int getPreferredLaunchDisplay(@Nullable TaskRecord task,
|
@Nullable ActivityOptions options, ActivityRecord source, LaunchParams currentParams) {
|
if (!mSupervisor.mService.mSupportsMultiDisplay) {
|
return DEFAULT_DISPLAY;
|
}
|
|
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 the source activity is a no-display activity, pass on the launch display id from
|
// source activity as currently preferred.
|
if (displayId == INVALID_DISPLAY && source != null && source.noDisplay) {
|
displayId = source.mHandoverLaunchDisplayId;
|
if (DEBUG) appendLog("display-from-no-display-source=" + displayId);
|
}
|
|
ActivityStack stack =
|
(displayId == INVALID_DISPLAY && task != null) ? task.getStack() : null;
|
if (stack != null) {
|
if (DEBUG) appendLog("display-from-task=" + stack.mDisplayId);
|
displayId = stack.mDisplayId;
|
}
|
|
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.mRootActivityContainer.getActivityDisplay(displayId) == null) {
|
displayId = currentParams.mPreferredDisplayId;
|
}
|
displayId = (displayId == INVALID_DISPLAY) ? currentParams.mPreferredDisplayId : displayId;
|
|
return (displayId != INVALID_DISPLAY
|
&& mSupervisor.mRootActivityContainer.getActivityDisplay(displayId) != null)
|
? displayId : DEFAULT_DISPLAY;
|
}
|
|
private boolean canApplyFreeformWindowPolicy(@NonNull ActivityDisplay display, int launchMode) {
|
return mSupervisor.mService.mSupportsFreeformWindowManagement
|
&& (display.inFreeformWindowingMode() || launchMode == WINDOWING_MODE_FREEFORM);
|
}
|
|
private boolean canApplyPipWindowPolicy(int launchMode) {
|
return mSupervisor.mService.mSupportsPictureInPicture
|
&& launchMode == WINDOWING_MODE_PINNED;
|
}
|
|
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);
|
}
|
|
/**
|
* 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();
|
}
|
|
/**
|
* 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;
|
}
|
|
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;
|
}
|
|
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));
|
}
|
|
// 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);
|
}
|
|
// Lastly we adjust bounds to avoid conflicts with other tasks as much as possible.
|
adjustBoundsToAvoidConflictInDisplay(display, inOutBounds);
|
}
|
|
private int convertOrientationToScreenOrientation(int orientation) {
|
switch (orientation) {
|
case Configuration.ORIENTATION_LANDSCAPE:
|
return SCREEN_ORIENTATION_LANDSCAPE;
|
case Configuration.ORIENTATION_PORTRAIT:
|
return SCREEN_ORIENTATION_PORTRAIT;
|
default:
|
return SCREEN_ORIENTATION_UNSPECIFIED;
|
}
|
}
|
|
private int resolveOrientation(@NonNull ActivityRecord root, @NonNull ActivityDisplay display,
|
@NonNull Rect bounds) {
|
int orientation = resolveOrientation(root);
|
|
if (orientation == SCREEN_ORIENTATION_LOCKED) {
|
orientation = bounds.isEmpty()
|
? convertOrientationToScreenOrientation(display.getConfiguration().orientation)
|
: orientationFromBounds(bounds);
|
if (DEBUG) {
|
appendLog(bounds.isEmpty() ? "locked-orientation-from-display=" + orientation
|
: "locked-orientation-from-bounds=" + bounds);
|
}
|
}
|
|
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 orientation;
|
}
|
|
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);
|
}
|
|
/**
|
* 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 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. Just move the start to align with the display.
|
final int layoutDirection =
|
mSupervisor.mRootActivityContainer.getConfiguration().getLayoutDirection();
|
final int left = layoutDirection == View.LAYOUT_DIRECTION_RTL
|
? displayBounds.width() - inOutBounds.width()
|
: 0;
|
inOutBounds.offsetTo(left, 0 /* newTop */);
|
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;
|
}
|
|
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);
|
}
|
|
/**
|
* 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 adjustBoundsToAvoidConflictInDisplay(@NonNull ActivityDisplay display,
|
@NonNull Rect inOutBounds) {
|
final List<Rect> taskBoundsToCheck = new ArrayList<>();
|
for (int i = 0; i < display.getChildCount(); ++i) {
|
final ActivityStack stack = display.getChildAt(i);
|
if (!stack.inFreeformWindowingMode()) {
|
continue;
|
}
|
|
for (int j = 0; j < stack.getChildCount(); ++j) {
|
taskBoundsToCheck.add(stack.getChildAt(j).getBounds());
|
}
|
}
|
adjustBoundsToAvoidConflict(display.getBounds(), taskBoundsToCheck, inOutBounds);
|
}
|
|
/**
|
* Adjusts input bounds to avoid conflict with provided display bounds and list of tasks bounds
|
* for the display.
|
*
|
* Scans the bounds in directions to find a candidate location that does not conflict with the
|
* provided list of task bounds. If starting bounds are outside the display bounds or if no
|
* suitable candidate bounds are found, the method returns the input bounds.
|
*
|
* @param displayBounds display bounds used to restrict the candidate bounds
|
* @param taskBoundsToCheck list of task bounds to check for conflict
|
* @param inOutBounds the bounds used to input initial bounds and output result bounds
|
*/
|
@VisibleForTesting
|
void adjustBoundsToAvoidConflict(@NonNull Rect displayBounds,
|
@NonNull List<Rect> taskBoundsToCheck,
|
@NonNull Rect inOutBounds) {
|
if (!displayBounds.contains(inOutBounds)) {
|
// The initial bounds are already out of display. The scanning algorithm below doesn't
|
// work so well with them.
|
return;
|
}
|
|
if (!boundsConflict(taskBoundsToCheck, 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(taskBoundsToCheck, mTmpBounds)
|
&& displayBounds.contains(mTmpBounds)) {
|
shiftBounds(direction, displayBounds, mTmpBounds);
|
}
|
|
if (!boundsConflict(taskBoundsToCheck, 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.
|
}
|
|
// We failed to find a conflict free location. Just keep the original result.
|
}
|
|
/**
|
* 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 boolean boundsConflict(@NonNull List<Rect> taskBoundsToCheck,
|
@NonNull Rect candidateBounds) {
|
for (Rect taskBounds : taskBoundsToCheck) {
|
final boolean leftClose = Math.abs(taskBounds.left - candidateBounds.left)
|
< BOUNDS_CONFLICT_THRESHOLD;
|
final boolean topClose = Math.abs(taskBounds.top - candidateBounds.top)
|
< BOUNDS_CONFLICT_THRESHOLD;
|
final boolean rightClose = Math.abs(taskBounds.right - candidateBounds.right)
|
< BOUNDS_CONFLICT_THRESHOLD;
|
final boolean bottomClose = Math.abs(taskBounds.bottom - candidateBounds.bottom)
|
< BOUNDS_CONFLICT_THRESHOLD;
|
|
if ((leftClose && topClose) || (leftClose && bottomClose) || (rightClose && topClose)
|
|| (rightClose && bottomClose)) {
|
return true;
|
}
|
}
|
|
return false;
|
}
|
|
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 Gravity.RIGHT:
|
horizontalOffset = Math.max(MINIMAL_STEP, availableRect.width() / STEP_DENOMINATOR);
|
break;
|
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 void initLogBuilder(TaskRecord task, ActivityRecord activity) {
|
if (DEBUG) {
|
mLogBuilder = new StringBuilder("TaskLaunchParamsModifier:task=" + task
|
+ " activity=" + activity);
|
}
|
}
|
|
private void appendLog(String log) {
|
if (DEBUG) mLogBuilder.append(" ").append(log);
|
}
|
|
private void outputLog() {
|
if (DEBUG) Slog.d(TAG, mLogBuilder.toString());
|
}
|
|
private static int orientationFromBounds(Rect bounds) {
|
return bounds.width() > bounds.height() ? SCREEN_ORIENTATION_LANDSCAPE
|
: SCREEN_ORIENTATION_PORTRAIT;
|
}
|
|
private static boolean sizeMatches(Rect left, Rect right) {
|
return (Math.abs(right.width() - left.width()) < EPSILON)
|
&& (Math.abs(right.height() - left.height()) < EPSILON);
|
}
|
}
|