blob: 4e7a9bd2881a3290f34dcaa1a3fd5983700009d9 [file] [log] [blame]
/*
* 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_MULTI_WINDOW;
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 android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
import static com.android.server.wm.ActivityStarter.Request;
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.res.Configuration;
import android.graphics.Rect;
import android.util.Size;
import android.util.Slog;
import android.view.Gravity;
import android.window.WindowContainerToken;
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;
// 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 ActivityTaskSupervisor mSupervisor;
private final Rect mTmpBounds = new Rect();
private final Rect mTmpStableBounds = new Rect();
private final int[] mTmpDirections = new int[2];
private TaskDisplayArea mTmpDisplayArea;
private StringBuilder mLogBuilder;
TaskLaunchParamsModifier(ActivityTaskSupervisor supervisor) {
mSupervisor = supervisor;
}
@Override
public int onCalculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout,
@Nullable ActivityRecord activity, @Nullable ActivityRecord source,
@Nullable ActivityOptions options, @Nullable Request request, int phase,
LaunchParams currentParams, LaunchParams outParams) {
initLogBuilder(task, activity);
final int result = calculate(task, layout, activity, source, options, request, phase,
currentParams, outParams);
outputLog();
return result;
}
private int calculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout,
@Nullable ActivityRecord activity, @Nullable ActivityRecord source,
@Nullable ActivityOptions options, @Nullable Request request, int phase,
LaunchParams currentParams, LaunchParams outParams) {
final ActivityRecord root;
if (task != null) {
root = task.getRootActivity() == null ? activity : task.getRootActivity();
} else {
root = activity;
}
if (root == null && phase != PHASE_DISPLAY) {
// 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.
// However, for PHASE_DISPLAY the root may be null, but we still want to get a hint of
// what the suggested launch display area would be.
return RESULT_SKIP;
}
// STEP 1: Determine the suggested display area to launch the activity/task.
final TaskDisplayArea suggestedDisplayArea = getPreferredLaunchTaskDisplayArea(task,
options, source, currentParams, activity, request);
outParams.mPreferredTaskDisplayArea = suggestedDisplayArea;
final DisplayContent display = suggestedDisplayArea.mDisplayContent;
if (DEBUG) {
appendLog("display-id=" + display.getDisplayId()
+ " task-display-area-windowing-mode=" + suggestedDisplayArea.getWindowingMode()
+ " suggested-display-area=" + suggestedDisplayArea);
}
if (phase == PHASE_DISPLAY) {
return RESULT_CONTINUE;
}
// STEP 2: Resolve launch windowing mode.
// STEP 2.1: Determine if any parameter can specify initial bounds/windowing mode. That
// might be the launch bounds from activity options, or size/gravity passed in layout. It
// also treats the launch windowing mode in options and source activity windowing mode in
// some cases as a suggestion for future resolution.
int launchMode = options != null ? options.getLaunchWindowingMode()
: WINDOWING_MODE_UNDEFINED;
// In some cases we want to use the source's windowing mode as the default value, e.g. when
// source is a freeform window in a fullscreen display launching an activity on the same
// display.
if (launchMode == WINDOWING_MODE_UNDEFINED
&& canInheritWindowingModeFromSource(display, suggestedDisplayArea, source)) {
// The source's windowing mode may be different from its task, e.g. activity is set
// to fullscreen and its task is pinned windowing mode when the activity is entering
// pip.
launchMode = source.getTask().getWindowingMode();
if (DEBUG) {
appendLog("inherit-from-source="
+ WindowConfiguration.windowingModeToString(launchMode));
}
}
// If the launch windowing mode is still undefined, inherit from the target task if the
// task is already on the right display area (otherwise, the task may be on a different
// display area that has incompatible windowing mode or the task organizer request to
// disassociate the leaf task if relaunched and reparented it to TDA as root task).
if (launchMode == WINDOWING_MODE_UNDEFINED
&& task != null && task.getTaskDisplayArea() == suggestedDisplayArea
&& !task.getRootTask().mReparentLeafTaskIfRelaunch) {
launchMode = task.getWindowingMode();
if (DEBUG) {
appendLog("inherit-from-task="
+ WindowConfiguration.windowingModeToString(launchMode));
}
}
// 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;
// hasInitialBoundsForSuggestedDisplayAreaInFreeformWindow is set if the outParams.mBounds
// is set with the suggestedDisplayArea. If it is set, but the eventual TaskDisplayArea is
// different, we should recalculating the bounds.
boolean hasInitialBoundsForSuggestedDisplayAreaInFreeformWindow = false;
// Note that initial bounds needs to be set to fullscreen tasks too as it's used as restore
// bounds.
final boolean canCalculateBoundsForFullscreenTask =
canCalculateBoundsForFullscreenTask(suggestedDisplayArea, launchMode);
final boolean canApplyFreeformWindowPolicy =
canApplyFreeformWindowPolicy(suggestedDisplayArea, launchMode);
final boolean canApplyWindowLayout = layout != null
&& (canApplyFreeformWindowPolicy || canCalculateBoundsForFullscreenTask);
final boolean canApplyBoundsFromActivityOptions =
mSupervisor.canUseActivityOptionsLaunchBounds(options)
&& (canApplyFreeformWindowPolicy
|| canApplyPipWindowPolicy(launchMode)
|| canCalculateBoundsForFullscreenTask);
if (canApplyBoundsFromActivityOptions) {
hasInitialBounds = true;
// |launchMode| at this point can be fullscreen, PIP, MultiWindow, etc. Only set
// freeform windowing mode if appropriate by checking |canApplyFreeformWindowPolicy|.
launchMode = launchMode == WINDOWING_MODE_UNDEFINED && canApplyFreeformWindowPolicy
? WINDOWING_MODE_FREEFORM
: launchMode;
outParams.mBounds.set(options.getLaunchBounds());
if (DEBUG) appendLog("activity-options-bounds=" + outParams.mBounds);
} else if (canApplyWindowLayout) {
mTmpBounds.set(currentParams.mBounds);
getLayoutBounds(suggestedDisplayArea, root, layout, mTmpBounds);
if (!mTmpBounds.isEmpty()) {
launchMode = canApplyFreeformWindowPolicy ? WINDOWING_MODE_FREEFORM : launchMode;
outParams.mBounds.set(mTmpBounds);
hasInitialBounds = true;
hasInitialBoundsForSuggestedDisplayAreaInFreeformWindow = true;
if (DEBUG) appendLog("bounds-from-layout=" + outParams.mBounds);
} else {
if (DEBUG) appendLog("empty-window-layout");
}
} else if (launchMode == WINDOWING_MODE_MULTI_WINDOW
&& options != null && options.getLaunchBounds() != null) {
// TODO: Investigate whether we can migrate this clause to the
// |canApplyBoundsFromActivityOptions| case above.
outParams.mBounds.set(options.getLaunchBounds());
hasInitialBounds = true;
if (DEBUG) appendLog("multiwindow-activity-options-bounds=" + 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.
// We only check if display matches because display area can be changed later.
if (!currentParams.isEmpty() && !hasInitialBounds
&& (currentParams.mPreferredTaskDisplayArea == null
|| currentParams.mPreferredTaskDisplayArea.getDisplayId()
== display.getDisplayId())) {
// Only set windowing mode if display is in freeform. If the display is in fullscreen
// mode we should only launch a task in fullscreen mode.
if (currentParams.hasWindowingMode()
&& suggestedDisplayArea.inFreeformWindowingMode()) {
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.
outParams.mBounds.set(currentParams.mBounds);
fullyResolvedCurrentParam = true;
if (launchMode == WINDOWING_MODE_FREEFORM) {
if (DEBUG) appendLog("inherit-bounds=" + outParams.mBounds);
}
}
}
// STEP 2.3: Adjust launch parameters as needed for freeform display. We enforce the
// policies related to unresizable apps here. If an app is unresizable and the freeform
// size-compat mode is enabled, it can be launched in freeform depending on other properties
// such as orientation. Otherwise, the app is forcefully launched in 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.
// hasInitialBoundsForSuggestedDisplayAreaInFreeformMode is set if the outParams.mBounds
// is set with the suggestedDisplayArea. If it is set, but the eventual TaskDisplayArea is
// different, we should recalcuating the bounds.
boolean hasInitialBoundsForSuggestedDisplayAreaInFreeformMode = false;
if (suggestedDisplayArea.inFreeformWindowingMode()) {
if (launchMode == WINDOWING_MODE_PINNED) {
if (DEBUG) appendLog("picture-in-picture");
} else if (!root.isResizeable()) {
if (shouldLaunchUnresizableAppInFreeform(root, suggestedDisplayArea, options)) {
launchMode = WINDOWING_MODE_FREEFORM;
if (outParams.mBounds.isEmpty()) {
getTaskBounds(root, suggestedDisplayArea, layout, launchMode,
hasInitialBounds, outParams.mBounds);
hasInitialBoundsForSuggestedDisplayAreaInFreeformMode = true;
}
if (DEBUG) appendLog("unresizable-freeform");
} else {
launchMode = WINDOWING_MODE_FULLSCREEN;
outParams.mBounds.setEmpty();
if (DEBUG) appendLog("unresizable-forced-maximize");
}
}
} else {
if (DEBUG) appendLog("non-freeform-task-display-area");
}
// If launch mode matches display windowing mode, let it inherit from display.
outParams.mWindowingMode = launchMode == suggestedDisplayArea.getWindowingMode()
? WINDOWING_MODE_UNDEFINED : launchMode;
if (phase == PHASE_WINDOWING_MODE) {
return RESULT_CONTINUE;
}
// STEP 3: Finalize the display area. Here we allow WM shell route all launches that match
// certain criteria to specific task display areas.
final int resolvedMode = (launchMode != WINDOWING_MODE_UNDEFINED) ? launchMode
: suggestedDisplayArea.getWindowingMode();
TaskDisplayArea taskDisplayArea = suggestedDisplayArea;
// If launch task display area is set in options we should just use it. We assume the
// suggestedDisplayArea has the right one in this case.
if (options == null || (options.getLaunchTaskDisplayArea() == null
&& options.getLaunchTaskDisplayAreaFeatureId() == FEATURE_UNDEFINED)) {
final int activityType =
mSupervisor.mRootWindowContainer.resolveActivityType(root, options, task);
display.forAllTaskDisplayAreas(displayArea -> {
final Task launchRoot = displayArea.getLaunchRootTask(
resolvedMode, activityType, null /* ActivityOptions */,
null /* sourceTask*/, 0 /* launchFlags */);
if (launchRoot == null) {
return false;
}
mTmpDisplayArea = displayArea;
return true;
});
// We may need to recalculate the bounds and the windowing mode if the new
// TaskDisplayArea is different from the suggested one we used to calculate the two
// configurations.
if (mTmpDisplayArea != null && mTmpDisplayArea != suggestedDisplayArea) {
outParams.mWindowingMode = (launchMode == mTmpDisplayArea.getWindowingMode())
? WINDOWING_MODE_UNDEFINED : launchMode;
if (hasInitialBoundsForSuggestedDisplayAreaInFreeformWindow) {
outParams.mBounds.setEmpty();
getLayoutBounds(mTmpDisplayArea, root, layout, outParams.mBounds);
hasInitialBounds = !outParams.mBounds.isEmpty();
} else if (hasInitialBoundsForSuggestedDisplayAreaInFreeformMode) {
outParams.mBounds.setEmpty();
getTaskBounds(root, mTmpDisplayArea, layout, launchMode,
hasInitialBounds, outParams.mBounds);
}
}
if (mTmpDisplayArea != null) {
taskDisplayArea = mTmpDisplayArea;
mTmpDisplayArea = null;
appendLog("overridden-display-area=["
+ WindowConfiguration.activityTypeToString(activityType) + ", "
+ WindowConfiguration.windowingModeToString(resolvedMode) + ", "
+ taskDisplayArea + "]");
}
}
appendLog("display-area=" + taskDisplayArea);
outParams.mPreferredTaskDisplayArea = taskDisplayArea;
if (phase == PHASE_DISPLAY_AREA) {
return RESULT_CONTINUE;
}
// STEP 4: 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.
if (fullyResolvedCurrentParam) {
if (resolvedMode == WINDOWING_MODE_FREEFORM) {
// Make sure bounds are in the displayArea.
if (currentParams.mPreferredTaskDisplayArea != taskDisplayArea) {
adjustBoundsToFitInDisplayArea(taskDisplayArea, layout, outParams.mBounds);
}
// Even though we want to keep original bounds, we still don't want it to stomp on
// an existing task.
adjustBoundsToAvoidConflictInDisplayArea(taskDisplayArea, outParams.mBounds);
}
} else {
if (source != null && source.inFreeformWindowingMode()
&& resolvedMode == WINDOWING_MODE_FREEFORM
&& outParams.mBounds.isEmpty()
&& source.getDisplayArea() == taskDisplayArea) {
// Set bounds to be not very far from source activity.
cascadeBounds(source.getConfiguration().windowConfiguration.getBounds(),
taskDisplayArea, outParams.mBounds);
}
getTaskBounds(root, taskDisplayArea, layout, resolvedMode, hasInitialBounds,
outParams.mBounds);
}
return RESULT_CONTINUE;
}
private TaskDisplayArea getPreferredLaunchTaskDisplayArea(@Nullable Task task,
@Nullable ActivityOptions options, @Nullable ActivityRecord source,
@Nullable LaunchParams currentParams, @Nullable ActivityRecord activityRecord,
@Nullable Request request) {
TaskDisplayArea taskDisplayArea = null;
final WindowContainerToken optionLaunchTaskDisplayAreaToken = options != null
? options.getLaunchTaskDisplayArea() : null;
if (optionLaunchTaskDisplayAreaToken != null) {
taskDisplayArea = (TaskDisplayArea) WindowContainer.fromBinder(
optionLaunchTaskDisplayAreaToken.asBinder());
if (DEBUG) appendLog("display-area-token-from-option=" + taskDisplayArea);
}
if (taskDisplayArea == null && options != null) {
final int launchTaskDisplayAreaFeatureId = options.getLaunchTaskDisplayAreaFeatureId();
if (launchTaskDisplayAreaFeatureId != FEATURE_UNDEFINED) {
final int launchDisplayId = options.getLaunchDisplayId() == INVALID_DISPLAY
? DEFAULT_DISPLAY : options.getLaunchDisplayId();
final DisplayContent dc = mSupervisor.mRootWindowContainer
.getDisplayContent(launchDisplayId);
if (dc != null) {
taskDisplayArea = dc.getItemFromTaskDisplayAreas(tda ->
tda.mFeatureId == launchTaskDisplayAreaFeatureId ? tda : null);
if (DEBUG) appendLog("display-area-feature-from-option=" + taskDisplayArea);
}
}
}
// If task display area is not specified in options - try display id
if (taskDisplayArea == null) {
final int optionLaunchId =
options != null ? options.getLaunchDisplayId() : INVALID_DISPLAY;
if (optionLaunchId != INVALID_DISPLAY) {
final DisplayContent dc = mSupervisor.mRootWindowContainer
.getDisplayContent(optionLaunchId);
if (dc != null) {
taskDisplayArea = dc.getDefaultTaskDisplayArea();
if (DEBUG) appendLog("display-from-option=" + optionLaunchId);
}
}
}
// If the source activity is a no-display activity, pass on the launch display area token
// from source activity as currently preferred.
if (taskDisplayArea == null && source != null && source.noDisplay) {
taskDisplayArea = source.mHandoverTaskDisplayArea;
if (taskDisplayArea != null) {
if (DEBUG) appendLog("display-area-from-no-display-source=" + taskDisplayArea);
} else {
// Try handover display id
final int displayId = source.mHandoverLaunchDisplayId;
final DisplayContent dc =
mSupervisor.mRootWindowContainer.getDisplayContent(displayId);
if (dc != null) {
taskDisplayArea = dc.getDefaultTaskDisplayArea();
if (DEBUG) appendLog("display-from-no-display-source=" + displayId);
}
}
}
if (taskDisplayArea == null && source != null) {
final TaskDisplayArea sourceDisplayArea = source.getDisplayArea();
if (DEBUG) appendLog("display-area-from-source=" + sourceDisplayArea);
taskDisplayArea = sourceDisplayArea;
}
Task rootTask = (taskDisplayArea == null && task != null)
? task.getRootTask() : null;
if (rootTask != null) {
if (DEBUG) appendLog("display-from-task=" + rootTask.getDisplayId());
taskDisplayArea = rootTask.getDisplayArea();
}
if (taskDisplayArea == null && options != null) {
final int callerDisplayId = options.getCallerDisplayId();
final DisplayContent dc =
mSupervisor.mRootWindowContainer.getDisplayContent(callerDisplayId);
if (dc != null) {
taskDisplayArea = dc.getDefaultTaskDisplayArea();
if (DEBUG) appendLog("display-from-caller=" + callerDisplayId);
}
}
if (taskDisplayArea == null && currentParams != null) {
taskDisplayArea = currentParams.mPreferredTaskDisplayArea;
if (DEBUG) appendLog("display-area-from-current-params=" + taskDisplayArea);
}
// Re-route to default display if the device didn't declare support for multi-display
if (taskDisplayArea != null && !mSupervisor.mService.mSupportsMultiDisplay
&& taskDisplayArea.getDisplayId() != DEFAULT_DISPLAY) {
taskDisplayArea = mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea();
if (DEBUG) appendLog("display-area-from-no-multidisplay=" + taskDisplayArea);
}
// Re-route to default display if the home activity doesn't support multi-display
if (taskDisplayArea != null && activityRecord != null && activityRecord.isActivityTypeHome()
&& !mSupervisor.mRootWindowContainer.canStartHomeOnDisplayArea(activityRecord.info,
taskDisplayArea, false /* allowInstrumenting */)) {
taskDisplayArea = mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea();
if (DEBUG) appendLog("display-area-from-home=" + taskDisplayArea);
}
return (taskDisplayArea != null)
? taskDisplayArea
: getFallbackDisplayAreaForActivity(activityRecord, request);
}
/**
* Calculates the default {@link TaskDisplayArea} for a task. We attempt to put the activity
* within the same display area if possible. The strategy is to find the display in the
* following order:
*
* <ol>
* <li>The display area of the top activity from the launching process will be used</li>
* <li>The display area of the top activity from the real launching process will be used
* </li>
* <li>Default display area from the associated root window container.</li>
* </ol>
* @param activityRecord the activity being started
* @param request optional {@link Request} made to start the activity record
* @return {@link TaskDisplayArea} to house the task
*/
private TaskDisplayArea getFallbackDisplayAreaForActivity(
@Nullable ActivityRecord activityRecord, @Nullable Request request) {
if (activityRecord != null) {
WindowProcessController controllerFromLaunchingRecord =
mSupervisor.mService.getProcessController(
activityRecord.launchedFromPid, activityRecord.launchedFromUid);
if (controllerFromLaunchingRecord != null) {
final TaskDisplayArea taskDisplayAreaForLaunchingRecord =
controllerFromLaunchingRecord.getTopActivityDisplayArea();
if (taskDisplayAreaForLaunchingRecord != null) {
if (DEBUG) {
appendLog("display-area-for-launching-record="
+ taskDisplayAreaForLaunchingRecord);
}
return taskDisplayAreaForLaunchingRecord;
}
}
WindowProcessController controllerFromProcess =
mSupervisor.mService.getProcessController(
activityRecord.getProcessName(), activityRecord.getUid());
if (controllerFromProcess != null) {
final TaskDisplayArea displayAreaForRecord =
controllerFromProcess.getTopActivityDisplayArea();
if (displayAreaForRecord != null) {
if (DEBUG) appendLog("display-area-for-record=" + displayAreaForRecord);
return displayAreaForRecord;
}
}
}
if (request != null) {
WindowProcessController controllerFromRequest =
mSupervisor.mService.getProcessController(
request.realCallingPid, request.realCallingUid);
if (controllerFromRequest != null) {
final TaskDisplayArea displayAreaFromSourceProcess =
controllerFromRequest.getTopActivityDisplayArea();
if (displayAreaFromSourceProcess != null) {
if (DEBUG) {
appendLog("display-area-source-process=" + displayAreaFromSourceProcess);
}
return displayAreaFromSourceProcess;
}
}
}
final TaskDisplayArea defaultTaskDisplayArea =
mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea();
if (DEBUG) appendLog("display-area-from-default-fallback=" + defaultTaskDisplayArea);
return defaultTaskDisplayArea;
}
private boolean canInheritWindowingModeFromSource(@NonNull DisplayContent display,
TaskDisplayArea suggestedDisplayArea, @Nullable ActivityRecord source) {
if (source == null) {
return false;
}
// There is not really any strong reason to tie the launching windowing mode and the source
// on freeform displays. The launching windowing mode is more tied to the content of the new
// activities.
if (suggestedDisplayArea.inFreeformWindowingMode()) {
return false;
}
final int sourceWindowingMode = source.getTask().getWindowingMode();
if (sourceWindowingMode != WINDOWING_MODE_FULLSCREEN
&& sourceWindowingMode != WINDOWING_MODE_FREEFORM) {
return false;
}
// Only inherit windowing mode if both source and target activities are on the same display.
// Otherwise we may have unintended freeform windows showing up if an activity in freeform
// window launches an activity on a fullscreen display by specifying display ID.
return display.getDisplayId() == source.getDisplayId();
}
private boolean canCalculateBoundsForFullscreenTask(@NonNull TaskDisplayArea displayArea,
int launchMode) {
return mSupervisor.mService.mSupportsFreeformWindowManagement
&& ((displayArea.getWindowingMode() == WINDOWING_MODE_FULLSCREEN
&& launchMode == WINDOWING_MODE_UNDEFINED)
|| launchMode == WINDOWING_MODE_FULLSCREEN);
}
private boolean canApplyFreeformWindowPolicy(@NonNull TaskDisplayArea suggestedDisplayArea,
int launchMode) {
return mSupervisor.mService.mSupportsFreeformWindowManagement
&& ((suggestedDisplayArea.inFreeformWindowingMode()
&& launchMode == WINDOWING_MODE_UNDEFINED)
|| launchMode == WINDOWING_MODE_FREEFORM);
}
private boolean canApplyPipWindowPolicy(int launchMode) {
return mSupervisor.mService.mSupportsPictureInPicture
&& launchMode == WINDOWING_MODE_PINNED;
}
private void getLayoutBounds(@NonNull TaskDisplayArea displayArea, @NonNull ActivityRecord root,
@NonNull ActivityInfo.WindowLayout windowLayout, @NonNull Rect inOutBounds) {
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) {
inOutBounds.setEmpty();
return;
}
// Use stable frame instead of raw frame to avoid launching freeform windows on top of
// stable insets, which usually are system widgets such as sysbar & navbar.
final Rect stableBounds = mTmpStableBounds;
displayArea.getStableRect(stableBounds);
final int defaultWidth = stableBounds.width();
final int defaultHeight = stableBounds.height();
int width;
int height;
if (!windowLayout.hasSpecifiedSize()) {
if (!inOutBounds.isEmpty()) {
// If the bounds is resolved already and WindowLayout doesn't have any opinion on
// its size, use the already resolved size and apply the gravity to it.
width = inOutBounds.width();
height = inOutBounds.height();
} else {
getTaskBounds(root, displayArea, windowLayout, WINDOWING_MODE_FREEFORM,
/* hasInitialBounds */ false, inOutBounds);
width = inOutBounds.width();
height = inOutBounds.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;
}
inOutBounds.set(0, 0, width, height);
inOutBounds.offset(stableBounds.left, stableBounds.top);
final int xOffset = (int) (fractionOfHorizontalOffset * (defaultWidth - width));
final int yOffset = (int) (fractionOfVerticalOffset * (defaultHeight - height));
inOutBounds.offset(xOffset, yOffset);
}
private boolean shouldLaunchUnresizableAppInFreeform(ActivityRecord activity,
TaskDisplayArea displayArea, @Nullable ActivityOptions options) {
if (options != null && options.getLaunchWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
// Do not launch the activity in freeform if it explicitly requested fullscreen mode.
return false;
}
if (!activity.supportsFreeformInDisplayArea(displayArea) || activity.isResizeable()) {
return false;
}
final int displayOrientation = orientationFromBounds(displayArea.getBounds());
final int activityOrientation = resolveOrientation(activity, displayArea,
displayArea.getBounds());
if (displayArea.getWindowingMode() == WINDOWING_MODE_FREEFORM
&& displayOrientation != activityOrientation) {
return true;
}
return false;
}
/**
* 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 TaskDisplayArea displayArea,
@NonNull Rect outBounds) {
outBounds.set(srcBounds);
float density = (float) displayArea.getConfiguration().densityDpi / DENSITY_DEFAULT;
final int defaultOffset = (int) (CASCADING_OFFSET_DP * density + 0.5f);
displayArea.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 TaskDisplayArea displayArea,
@NonNull ActivityInfo.WindowLayout layout, int resolvedMode, boolean hasInitialBounds,
@NonNull Rect inOutBounds) {
if (resolvedMode != WINDOWING_MODE_FREEFORM
&& resolvedMode != WINDOWING_MODE_FULLSCREEN) {
// This function should be used only for freeform bounds adjustment. Freeform bounds
// needs to be set to fullscreen tasks too as restore bounds.
if (DEBUG) {
appendLog("skip-bounds-" + WindowConfiguration.windowingModeToString(resolvedMode));
}
return;
}
final int orientation = resolveOrientation(root, displayArea, 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.
displayArea.getStableRect(mTmpStableBounds);
final Size defaultSize = LaunchParamsUtil.getDefaultFreeformSize(root, displayArea,
layout, orientation, mTmpStableBounds);
mTmpBounds.set(0, 0, defaultSize.getWidth(), defaultSize.getHeight());
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.
LaunchParamsUtil.centerBounds(displayArea, 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 displayArea if no suggested bounds). The
// default size might be too big to center to source activity bounds in displayArea, so
// we may need to move it back to the displayArea.
adjustBoundsToFitInDisplayArea(displayArea, layout, mTmpBounds);
inOutBounds.setEmpty();
LaunchParamsUtil.centerBounds(displayArea, mTmpBounds.width(), mTmpBounds.height(),
inOutBounds);
if (DEBUG) appendLog("freeform-size-mismatch=" + inOutBounds);
}
// Lastly we adjust bounds to avoid conflicts with other tasks as much as possible.
adjustBoundsToAvoidConflictInDisplayArea(displayArea, 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 TaskDisplayArea displayArea, @NonNull Rect bounds) {
int orientation = resolveOrientation(root);
if (orientation == SCREEN_ORIENTATION_LOCKED) {
orientation = bounds.isEmpty()
? convertOrientationToScreenOrientation(
displayArea.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 adjustBoundsToFitInDisplayArea(@NonNull TaskDisplayArea displayArea,
@NonNull ActivityInfo.WindowLayout layout,
@NonNull Rect inOutBounds) {
final int layoutDirection = mSupervisor.mRootWindowContainer.getConfiguration()
.getLayoutDirection();
LaunchParamsUtil.adjustBoundsToFitInDisplayArea(displayArea, layoutDirection, layout,
inOutBounds);
}
/**
* Adjusts input bounds to avoid conflict with existing tasks in the displayArea.
*
* 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 displayArea without conflict
* with any other tasks.
*
* It doesn't try to adjust bounds that's not fully in the given displayArea.
*
* @param displayArea the displayArea which tasks are to check
* @param inOutBounds the bounds used to input initial bounds and output result bounds
*/
private void adjustBoundsToAvoidConflictInDisplayArea(@NonNull TaskDisplayArea displayArea,
@NonNull Rect inOutBounds) {
final List<Rect> taskBoundsToCheck = new ArrayList<>();
displayArea.forAllRootTasks(task -> {
if (!task.inFreeformWindowingMode()) {
return;
}
for (int j = 0; j < task.getChildCount(); ++j) {
taskBoundsToCheck.add(task.getChildAt(j).getBounds());
}
}, false /* traverseTopToBottom */);
adjustBoundsToAvoidConflict(displayArea.getBounds(), taskBoundsToCheck, inOutBounds);
}
/**
* Adjusts input bounds to avoid conflict with provided displayArea bounds and list of tasks
* bounds for the displayArea.
*
* 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 displayArea bounds or if no
* suitable candidate bounds are found, the method returns the input bounds.
*
* @param displayAreaBounds displayArea 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 displayAreaBounds,
@NonNull List<Rect> taskBoundsToCheck,
@NonNull Rect inOutBounds) {
if (!displayAreaBounds.contains(inOutBounds)) {
// The initial bounds are already out of displayArea. 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(displayAreaBounds, inOutBounds);
for (int direction : mTmpDirections) {
if (direction == Gravity.NO_GRAVITY) {
// We exhausted candidate directions, give up.
break;
}
mTmpBounds.set(inOutBounds);
while (boundsConflict(taskBoundsToCheck, mTmpBounds)
&& displayAreaBounds.contains(mTmpBounds)) {
shiftBounds(direction, displayAreaBounds, mTmpBounds);
}
if (!boundsConflict(taskBoundsToCheck, mTmpBounds)
&& displayAreaBounds.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(Task 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);
}
}