Filip Gruszczynski | e5390e7 | 2015-08-18 16:39:00 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2015 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
Wale Ogunwale | 5950709 | 2018-10-29 09:00:30 -0700 | [diff] [blame] | 17 | package com.android.server.wm; |
Filip Gruszczynski | e5390e7 | 2015-08-18 16:39:00 -0700 | [diff] [blame] | 18 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 19 | import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; |
| 20 | import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; |
| 21 | import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; |
| 22 | import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; |
| 23 | import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; |
| 24 | import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED; |
| 25 | import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; |
| 26 | import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; |
| 27 | import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; |
| 28 | import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; |
| 29 | import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; |
| 30 | import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; |
| 31 | import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; |
| 32 | import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE; |
| 33 | import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT; |
| 34 | import static android.util.DisplayMetrics.DENSITY_DEFAULT; |
| 35 | import static android.view.Display.DEFAULT_DISPLAY; |
| 36 | import static android.view.Display.INVALID_DISPLAY; |
| 37 | |
Wale Ogunwale | 5950709 | 2018-10-29 09:00:30 -0700 | [diff] [blame] | 38 | import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; |
| 39 | import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 40 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 41 | import android.annotation.NonNull; |
| 42 | import android.annotation.Nullable; |
Bryce Lee | dacefc4 | 2017-10-10 12:56:02 -0700 | [diff] [blame] | 43 | import android.app.ActivityOptions; |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 44 | import android.app.WindowConfiguration; |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 45 | import android.content.pm.ActivityInfo; |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 46 | import android.content.pm.ApplicationInfo; |
Garfield Tan | bb0270f | 2018-12-05 11:30:27 -0800 | [diff] [blame] | 47 | import android.content.res.Configuration; |
Filip Gruszczynski | e5390e7 | 2015-08-18 16:39:00 -0700 | [diff] [blame] | 48 | import android.graphics.Rect; |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 49 | import android.os.Build; |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 50 | import android.util.Slog; |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 51 | import android.view.Gravity; |
Garfield Tan | 891146c | 2018-10-09 12:14:00 -0700 | [diff] [blame] | 52 | import android.view.View; |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 53 | |
Louis Chang | 6fb1e84 | 2018-12-03 16:07:50 +0800 | [diff] [blame] | 54 | import com.android.internal.annotations.VisibleForTesting; |
Wale Ogunwale | 5950709 | 2018-10-29 09:00:30 -0700 | [diff] [blame] | 55 | import com.android.server.wm.LaunchParamsController.LaunchParams; |
| 56 | import com.android.server.wm.LaunchParamsController.LaunchParamsModifier; |
Filip Gruszczynski | e5390e7 | 2015-08-18 16:39:00 -0700 | [diff] [blame] | 57 | |
| 58 | import java.util.ArrayList; |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 59 | import java.util.List; |
Filip Gruszczynski | e5390e7 | 2015-08-18 16:39:00 -0700 | [diff] [blame] | 60 | |
| 61 | /** |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 62 | * The class that defines the default launch params for tasks. |
Filip Gruszczynski | e5390e7 | 2015-08-18 16:39:00 -0700 | [diff] [blame] | 63 | */ |
Bryce Lee | ec55eb0 | 2017-12-05 20:51:27 -0800 | [diff] [blame] | 64 | class TaskLaunchParamsModifier implements LaunchParamsModifier { |
Wale Ogunwale | 9887561 | 2018-10-12 07:53:02 -0700 | [diff] [blame] | 65 | private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskLaunchParamsModifier" : TAG_ATM; |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 66 | private static final boolean DEBUG = false; |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 67 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 68 | // A mask for SUPPORTS_SCREEN that indicates the activity supports resize. |
| 69 | private static final int SUPPORTS_SCREEN_RESIZEABLE_MASK = |
| 70 | ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES |
| 71 | | ApplicationInfo.FLAG_SUPPORTS_LARGE_SCREENS |
| 72 | | ApplicationInfo.FLAG_SUPPORTS_SMALL_SCREENS |
| 73 | | ApplicationInfo.FLAG_RESIZEABLE_FOR_SCREENS |
| 74 | | ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES |
| 75 | | ApplicationInfo.FLAG_SUPPORTS_XLARGE_SCREENS; |
Filip Gruszczynski | e5390e7 | 2015-08-18 16:39:00 -0700 | [diff] [blame] | 76 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 77 | // Screen size of Nexus 5x |
| 78 | private static final int DEFAULT_PORTRAIT_PHONE_WIDTH_DP = 412; |
| 79 | private static final int DEFAULT_PORTRAIT_PHONE_HEIGHT_DP = 732; |
Filip Gruszczynski | e5390e7 | 2015-08-18 16:39:00 -0700 | [diff] [blame] | 80 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 81 | // Allowance of size matching. |
| 82 | private static final int EPSILON = 2; |
Filip Gruszczynski | e5390e7 | 2015-08-18 16:39:00 -0700 | [diff] [blame] | 83 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 84 | // Cascade window offset. |
| 85 | private static final int CASCADING_OFFSET_DP = 75; |
| 86 | |
| 87 | // Threshold how close window corners have to be to call them colliding. |
| 88 | private static final int BOUNDS_CONFLICT_THRESHOLD = 4; |
| 89 | |
| 90 | // Divide display size by this number to get each step to adjust bounds to avoid conflict. |
Filip Gruszczynski | e5390e7 | 2015-08-18 16:39:00 -0700 | [diff] [blame] | 91 | private static final int STEP_DENOMINATOR = 16; |
| 92 | |
| 93 | // We always want to step by at least this. |
| 94 | private static final int MINIMAL_STEP = 1; |
| 95 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 96 | private final ActivityStackSupervisor mSupervisor; |
| 97 | private final Rect mTmpBounds = new Rect(); |
| 98 | private final int[] mTmpDirections = new int[2]; |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 99 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 100 | private StringBuilder mLogBuilder; |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 101 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 102 | TaskLaunchParamsModifier(ActivityStackSupervisor supervisor) { |
| 103 | mSupervisor = supervisor; |
| 104 | } |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 105 | |
Louis Chang | 6fb1e84 | 2018-12-03 16:07:50 +0800 | [diff] [blame] | 106 | @VisibleForTesting |
| 107 | int onCalculate(TaskRecord task, ActivityInfo.WindowLayout layout, ActivityRecord activity, |
| 108 | ActivityRecord source, ActivityOptions options, LaunchParams currentParams, |
| 109 | LaunchParams outParams) { |
| 110 | return onCalculate(task, layout, activity, source, options, PHASE_BOUNDS, currentParams, |
| 111 | outParams); |
| 112 | } |
| 113 | |
Bryce Lee | dacefc4 | 2017-10-10 12:56:02 -0700 | [diff] [blame] | 114 | @Override |
Bryce Lee | ec55eb0 | 2017-12-05 20:51:27 -0800 | [diff] [blame] | 115 | public int onCalculate(TaskRecord task, ActivityInfo.WindowLayout layout, |
| 116 | ActivityRecord activity, ActivityRecord source, ActivityOptions options, |
Louis Chang | 6fb1e84 | 2018-12-03 16:07:50 +0800 | [diff] [blame] | 117 | int phase, LaunchParams currentParams, LaunchParams outParams) { |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 118 | initLogBuilder(task, activity); |
Louis Chang | 6fb1e84 | 2018-12-03 16:07:50 +0800 | [diff] [blame] | 119 | final int result = calculate(task, layout, activity, source, options, phase, currentParams, |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 120 | outParams); |
| 121 | outputLog(); |
| 122 | return result; |
| 123 | } |
| 124 | |
| 125 | private int calculate(TaskRecord task, ActivityInfo.WindowLayout layout, |
Louis Chang | 6fb1e84 | 2018-12-03 16:07:50 +0800 | [diff] [blame] | 126 | ActivityRecord activity, ActivityRecord source, ActivityOptions options, int phase, |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 127 | LaunchParams currentParams, LaunchParams outParams) { |
Garfield Tan | 21d7e17 | 2018-10-16 18:07:27 -0700 | [diff] [blame] | 128 | final ActivityRecord root; |
| 129 | if (task != null) { |
| 130 | root = task.getRootActivity() == null ? activity : task.getRootActivity(); |
| 131 | } else { |
| 132 | root = activity; |
| 133 | } |
| 134 | |
| 135 | // TODO: Investigate whether we can safely ignore all cases where we don't have root |
| 136 | // activity available. Note we can't know if the bounds are valid if we're not sure of the |
| 137 | // requested orientation of the root activity. Therefore if we found such a case we may need |
| 138 | // to pass the activity into this modifier in that case. |
| 139 | if (root == null) { |
| 140 | // There is a case that can lead us here. The caller is moving the top activity that is |
| 141 | // in a task that has multiple activities to PIP mode. For that the caller is creating a |
| 142 | // new task to host the activity so that we only move the top activity to PIP mode and |
| 143 | // keep other activities in the previous task. There is no point to apply the launch |
| 144 | // logic in this case. |
| 145 | return RESULT_SKIP; |
| 146 | } |
| 147 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 148 | // STEP 1: Determine the display to launch the activity/task. |
Louis Chang | 4075009 | 2018-10-24 21:04:51 +0800 | [diff] [blame] | 149 | final int displayId = getPreferredLaunchDisplay(task, options, source, currentParams); |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 150 | outParams.mPreferredDisplayId = displayId; |
Wale Ogunwale | d32da47 | 2018-11-16 07:19:28 -0800 | [diff] [blame] | 151 | ActivityDisplay display = mSupervisor.mRootActivityContainer.getActivityDisplay(displayId); |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 152 | if (DEBUG) { |
| 153 | appendLog("display-id=" + outParams.mPreferredDisplayId + " display-windowing-mode=" |
| 154 | + display.getWindowingMode()); |
Bryce Lee | dacefc4 | 2017-10-10 12:56:02 -0700 | [diff] [blame] | 155 | } |
| 156 | |
Louis Chang | 6fb1e84 | 2018-12-03 16:07:50 +0800 | [diff] [blame] | 157 | if (phase == PHASE_DISPLAY) { |
| 158 | return RESULT_CONTINUE; |
| 159 | } |
| 160 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 161 | // STEP 2: Resolve launch windowing mode. |
| 162 | // STEP 2.1: Determine if any parameter has specified initial bounds. That might be the |
Garfield Tan | 706dbcb | 2018-10-15 11:33:02 -0700 | [diff] [blame] | 163 | // launch bounds from activity options, or size/gravity passed in layout. It also treats the |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 164 | // launch windowing mode in options as a suggestion for future resolution. |
| 165 | int launchMode = options != null ? options.getLaunchWindowingMode() |
| 166 | : WINDOWING_MODE_UNDEFINED; |
| 167 | // hasInitialBounds is set if either activity options or layout has specified bounds. If |
| 168 | // that's set we'll skip some adjustments later to avoid overriding the initial bounds. |
| 169 | boolean hasInitialBounds = false; |
Garfield Tan | 706dbcb | 2018-10-15 11:33:02 -0700 | [diff] [blame] | 170 | final boolean canApplyFreeformPolicy = canApplyFreeformWindowPolicy(display, launchMode); |
| 171 | if (mSupervisor.canUseActivityOptionsLaunchBounds(options) |
| 172 | && (canApplyFreeformPolicy || canApplyPipWindowPolicy(launchMode))) { |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 173 | hasInitialBounds = true; |
| 174 | launchMode = launchMode == WINDOWING_MODE_UNDEFINED |
| 175 | ? WINDOWING_MODE_FREEFORM |
| 176 | : launchMode; |
| 177 | outParams.mBounds.set(options.getLaunchBounds()); |
| 178 | if (DEBUG) appendLog("activity-options-bounds=" + outParams.mBounds); |
| 179 | } else if (launchMode == WINDOWING_MODE_PINNED) { |
| 180 | // System controls PIP window's bounds, so don't apply launch bounds. |
| 181 | if (DEBUG) appendLog("empty-window-layout-for-pip"); |
| 182 | } else if (launchMode == WINDOWING_MODE_FULLSCREEN) { |
| 183 | if (DEBUG) appendLog("activity-options-fullscreen=" + outParams.mBounds); |
| 184 | } else if (layout != null && canApplyFreeformPolicy) { |
| 185 | getLayoutBounds(display, root, layout, mTmpBounds); |
| 186 | if (!mTmpBounds.isEmpty()) { |
| 187 | launchMode = WINDOWING_MODE_FREEFORM; |
| 188 | outParams.mBounds.set(mTmpBounds); |
| 189 | hasInitialBounds = true; |
| 190 | if (DEBUG) appendLog("bounds-from-layout=" + outParams.mBounds); |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 191 | } else { |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 192 | if (DEBUG) appendLog("empty-window-layout"); |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 193 | } |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 194 | } |
| 195 | |
| 196 | // STEP 2.2: Check if previous modifier or the controller (referred as "callers" below) has |
| 197 | // some opinions on launch mode and launch bounds. If they have opinions and there is no |
| 198 | // initial bounds set in parameters. Note the check on display ID is also input param |
| 199 | // related because we always defer to callers' suggestion if there is no specific display ID |
| 200 | // in options or from source activity. |
| 201 | // |
| 202 | // If opinions from callers don't need any further resolution, we try to honor that as is as |
| 203 | // much as possible later. |
| 204 | |
| 205 | // Flag to indicate if current param needs no further resolution. It's true it current |
| 206 | // param isn't freeform mode, or it already has launch bounds. |
| 207 | boolean fullyResolvedCurrentParam = false; |
| 208 | // We inherit launch params from previous modifiers or LaunchParamsController if options, |
| 209 | // layout and display conditions are not contradictory to their suggestions. It's important |
| 210 | // to carry over their values because LaunchParamsController doesn't automatically do that. |
| 211 | if (!currentParams.isEmpty() && !hasInitialBounds |
| 212 | && (!currentParams.hasPreferredDisplay() |
| 213 | || displayId == currentParams.mPreferredDisplayId)) { |
| 214 | if (currentParams.hasWindowingMode()) { |
| 215 | launchMode = currentParams.mWindowingMode; |
Garfield Tan | 891146c | 2018-10-09 12:14:00 -0700 | [diff] [blame] | 216 | fullyResolvedCurrentParam = launchMode != WINDOWING_MODE_FREEFORM; |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 217 | if (DEBUG) { |
| 218 | appendLog("inherit-" + WindowConfiguration.windowingModeToString(launchMode)); |
| 219 | } |
| 220 | } |
| 221 | |
Garfield Tan | d5972d1 | 2019-01-03 10:31:59 -0800 | [diff] [blame] | 222 | if (!currentParams.mBounds.isEmpty()) { |
| 223 | // Carry over bounds from callers regardless of launch mode because bounds is still |
| 224 | // used to restore last non-fullscreen bounds when launch mode is not freeform. |
| 225 | // Therefore it's not a resolution step for non-freeform launch mode and only |
| 226 | // consider it fully resolved only when launch mode is freeform. |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 227 | outParams.mBounds.set(currentParams.mBounds); |
Garfield Tan | d5972d1 | 2019-01-03 10:31:59 -0800 | [diff] [blame] | 228 | if (launchMode == WINDOWING_MODE_FREEFORM) { |
| 229 | fullyResolvedCurrentParam = true; |
| 230 | if (DEBUG) appendLog("inherit-bounds=" + outParams.mBounds); |
| 231 | } |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 232 | } |
| 233 | } |
| 234 | |
| 235 | // STEP 2.3: Adjust launch parameters as needed for freeform display. We enforce the policy |
| 236 | // that legacy (pre-D) apps and those apps that can't handle multiple screen density well |
| 237 | // are forced to be maximized. The rest of this step is to define the default policy when |
erosky | 6e76a50 | 2019-02-15 17:12:29 +0900 | [diff] [blame] | 238 | // there is no initial bounds or a fully resolved current params from callers. |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 239 | if (display.inFreeformWindowingMode()) { |
| 240 | if (launchMode == WINDOWING_MODE_PINNED) { |
| 241 | if (DEBUG) appendLog("picture-in-picture"); |
| 242 | } else if (isTaskForcedMaximized(root)) { |
| 243 | // We're launching an activity that probably can't handle resizing nicely, so force |
| 244 | // it to be maximized even someone suggests launching it in freeform using launch |
| 245 | // options. |
| 246 | launchMode = WINDOWING_MODE_FULLSCREEN; |
| 247 | outParams.mBounds.setEmpty(); |
| 248 | if (DEBUG) appendLog("forced-maximize"); |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 249 | } |
| 250 | } else { |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 251 | if (DEBUG) appendLog("non-freeform-display"); |
| 252 | } |
| 253 | // If launch mode matches display windowing mode, let it inherit from display. |
| 254 | outParams.mWindowingMode = launchMode == display.getWindowingMode() |
| 255 | ? WINDOWING_MODE_UNDEFINED : launchMode; |
| 256 | |
Louis Chang | 6fb1e84 | 2018-12-03 16:07:50 +0800 | [diff] [blame] | 257 | if (phase == PHASE_WINDOWING_MODE) { |
| 258 | return RESULT_CONTINUE; |
| 259 | } |
| 260 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 261 | // STEP 3: Determine final launch bounds based on resolved windowing mode and activity |
| 262 | // requested orientation. We set bounds to empty for fullscreen mode and keep bounds as is |
| 263 | // for all other windowing modes that's not freeform mode. One can read comments in |
| 264 | // relevant methods to further understand this step. |
| 265 | // |
Garfield Tan | 891146c | 2018-10-09 12:14:00 -0700 | [diff] [blame] | 266 | // We skip making adjustments if the params are fully resolved from previous results. |
| 267 | final int resolvedMode = (launchMode != WINDOWING_MODE_UNDEFINED) ? launchMode |
| 268 | : display.getWindowingMode(); |
| 269 | if (fullyResolvedCurrentParam) { |
| 270 | if (resolvedMode == WINDOWING_MODE_FREEFORM) { |
| 271 | // Make sure bounds are in the display if it's possibly in a different display. |
| 272 | if (currentParams.mPreferredDisplayId != displayId) { |
| 273 | adjustBoundsToFitInDisplay(display, outParams.mBounds); |
| 274 | } |
| 275 | // Even though we want to keep original bounds, we still don't want it to stomp on |
| 276 | // an existing task. |
| 277 | adjustBoundsToAvoidConflict(display, outParams.mBounds); |
| 278 | } |
| 279 | } else { |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 280 | if (source != null && source.inFreeformWindowingMode() |
| 281 | && resolvedMode == WINDOWING_MODE_FREEFORM |
| 282 | && outParams.mBounds.isEmpty() |
| 283 | && source.getDisplayId() == display.mDisplayId) { |
| 284 | // Set bounds to be not very far from source activity. |
| 285 | cascadeBounds(source.getBounds(), display, outParams.mBounds); |
| 286 | } |
| 287 | getTaskBounds(root, display, layout, resolvedMode, hasInitialBounds, outParams.mBounds); |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 288 | } |
Bryce Lee | dacefc4 | 2017-10-10 12:56:02 -0700 | [diff] [blame] | 289 | |
| 290 | return RESULT_CONTINUE; |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 291 | } |
| 292 | |
Louis Chang | 4075009 | 2018-10-24 21:04:51 +0800 | [diff] [blame] | 293 | private int getPreferredLaunchDisplay(@Nullable TaskRecord task, |
| 294 | @Nullable ActivityOptions options, ActivityRecord source, LaunchParams currentParams) { |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 295 | int displayId = INVALID_DISPLAY; |
| 296 | final int optionLaunchId = options != null ? options.getLaunchDisplayId() : INVALID_DISPLAY; |
| 297 | if (optionLaunchId != INVALID_DISPLAY) { |
| 298 | if (DEBUG) appendLog("display-from-option=" + optionLaunchId); |
| 299 | displayId = optionLaunchId; |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 300 | } |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 301 | |
Louis Chang | d58cb67 | 2018-12-24 17:45:16 +0800 | [diff] [blame] | 302 | // If the source activity is a no-display activity, pass on the launch display id from |
| 303 | // source activity as currently preferred. |
| 304 | if (displayId == INVALID_DISPLAY && source != null && source.noDisplay) { |
| 305 | displayId = source.mHandoverLaunchDisplayId; |
| 306 | if (DEBUG) appendLog("display-from-no-display-source=" + displayId); |
| 307 | } |
| 308 | |
Louis Chang | 4075009 | 2018-10-24 21:04:51 +0800 | [diff] [blame] | 309 | ActivityStack stack = |
| 310 | (displayId == INVALID_DISPLAY && task != null) ? task.getStack() : null; |
| 311 | if (stack != null) { |
| 312 | if (DEBUG) appendLog("display-from-task=" + stack.mDisplayId); |
| 313 | displayId = stack.mDisplayId; |
| 314 | } |
| 315 | |
Louis Chang | 6fb1e84 | 2018-12-03 16:07:50 +0800 | [diff] [blame] | 316 | if (displayId == INVALID_DISPLAY && source != null) { |
| 317 | final int sourceDisplayId = source.getDisplayId(); |
| 318 | if (DEBUG) appendLog("display-from-source=" + sourceDisplayId); |
| 319 | displayId = sourceDisplayId; |
| 320 | } |
| 321 | |
Wale Ogunwale | d32da47 | 2018-11-16 07:19:28 -0800 | [diff] [blame] | 322 | if (displayId != INVALID_DISPLAY |
| 323 | && mSupervisor.mRootActivityContainer.getActivityDisplay(displayId) == null) { |
Garfield Tan | 891146c | 2018-10-09 12:14:00 -0700 | [diff] [blame] | 324 | displayId = currentParams.mPreferredDisplayId; |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 325 | } |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 326 | displayId = (displayId == INVALID_DISPLAY) ? currentParams.mPreferredDisplayId : displayId; |
| 327 | |
Wale Ogunwale | d32da47 | 2018-11-16 07:19:28 -0800 | [diff] [blame] | 328 | return (displayId != INVALID_DISPLAY |
| 329 | && mSupervisor.mRootActivityContainer.getActivityDisplay(displayId) != null) |
Garfield Tan | 891146c | 2018-10-09 12:14:00 -0700 | [diff] [blame] | 330 | ? displayId : DEFAULT_DISPLAY; |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 331 | } |
| 332 | |
Garfield Tan | 706dbcb | 2018-10-15 11:33:02 -0700 | [diff] [blame] | 333 | private boolean canApplyFreeformWindowPolicy(@NonNull ActivityDisplay display, int launchMode) { |
| 334 | return mSupervisor.mService.mSupportsFreeformWindowManagement |
| 335 | && (display.inFreeformWindowingMode() || launchMode == WINDOWING_MODE_FREEFORM); |
| 336 | } |
| 337 | |
| 338 | private boolean canApplyPipWindowPolicy(int launchMode) { |
| 339 | return mSupervisor.mService.mSupportsPictureInPicture |
| 340 | && launchMode == WINDOWING_MODE_PINNED; |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 341 | } |
| 342 | |
| 343 | private void getLayoutBounds(@NonNull ActivityDisplay display, @NonNull ActivityRecord root, |
| 344 | @NonNull ActivityInfo.WindowLayout windowLayout, @NonNull Rect outBounds) { |
| 345 | final int verticalGravity = windowLayout.gravity & Gravity.VERTICAL_GRAVITY_MASK; |
| 346 | final int horizontalGravity = windowLayout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; |
| 347 | if (!windowLayout.hasSpecifiedSize() && verticalGravity == 0 && horizontalGravity == 0) { |
| 348 | outBounds.setEmpty(); |
| 349 | return; |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 350 | } |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 351 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 352 | final Rect bounds = display.getBounds(); |
| 353 | final int defaultWidth = bounds.width(); |
| 354 | final int defaultHeight = bounds.height(); |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 355 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 356 | int width; |
| 357 | int height; |
| 358 | if (!windowLayout.hasSpecifiedSize()) { |
| 359 | outBounds.setEmpty(); |
| 360 | getTaskBounds(root, display, windowLayout, WINDOWING_MODE_FREEFORM, |
| 361 | /* hasInitialBounds */ false, outBounds); |
| 362 | width = outBounds.width(); |
| 363 | height = outBounds.height(); |
| 364 | } else { |
| 365 | width = defaultWidth; |
| 366 | if (windowLayout.width > 0 && windowLayout.width < defaultWidth) { |
| 367 | width = windowLayout.width; |
| 368 | } else if (windowLayout.widthFraction > 0 && windowLayout.widthFraction < 1.0f) { |
| 369 | width = (int) (width * windowLayout.widthFraction); |
Filip Gruszczynski | e5390e7 | 2015-08-18 16:39:00 -0700 | [diff] [blame] | 370 | } |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 371 | |
| 372 | height = defaultHeight; |
| 373 | if (windowLayout.height > 0 && windowLayout.height < defaultHeight) { |
| 374 | height = windowLayout.height; |
| 375 | } else if (windowLayout.heightFraction > 0 && windowLayout.heightFraction < 1.0f) { |
| 376 | height = (int) (height * windowLayout.heightFraction); |
Filip Gruszczynski | e5390e7 | 2015-08-18 16:39:00 -0700 | [diff] [blame] | 377 | } |
| 378 | } |
Filip Gruszczynski | e5390e7 | 2015-08-18 16:39:00 -0700 | [diff] [blame] | 379 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 380 | final float fractionOfHorizontalOffset; |
| 381 | switch (horizontalGravity) { |
| 382 | case Gravity.LEFT: |
| 383 | fractionOfHorizontalOffset = 0f; |
| 384 | break; |
| 385 | case Gravity.RIGHT: |
| 386 | fractionOfHorizontalOffset = 1f; |
| 387 | break; |
| 388 | default: |
| 389 | fractionOfHorizontalOffset = 0.5f; |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 390 | } |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 391 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 392 | final float fractionOfVerticalOffset; |
| 393 | switch (verticalGravity) { |
| 394 | case Gravity.TOP: |
| 395 | fractionOfVerticalOffset = 0f; |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 396 | break; |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 397 | case Gravity.BOTTOM: |
| 398 | fractionOfVerticalOffset = 1f; |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 399 | break; |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 400 | default: |
| 401 | fractionOfVerticalOffset = 0.5f; |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 402 | } |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 403 | |
| 404 | outBounds.set(0, 0, width, height); |
| 405 | final int xOffset = (int) (fractionOfHorizontalOffset * (defaultWidth - width)); |
| 406 | final int yOffset = (int) (fractionOfVerticalOffset * (defaultHeight - height)); |
| 407 | outBounds.offset(xOffset, yOffset); |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 408 | } |
| 409 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 410 | /** |
| 411 | * Returns if task is forced to maximize. |
| 412 | * |
| 413 | * There are several cases where we force a task to maximize: |
| 414 | * 1) Root activity is targeting pre-Donut, which by default can't handle multiple screen |
| 415 | * densities, so resizing will likely cause issues; |
| 416 | * 2) Root activity doesn't declare any flag that it supports any screen density, so resizing |
| 417 | * may also cause issues; |
| 418 | * 3) Root activity is not resizeable, for which we shouldn't allow user resize it. |
| 419 | * |
| 420 | * @param root the root activity to check against. |
| 421 | * @return {@code true} if it should be forced to maximize; {@code false} otherwise. |
| 422 | */ |
| 423 | private boolean isTaskForcedMaximized(@NonNull ActivityRecord root) { |
| 424 | if (root.appInfo.targetSdkVersion < Build.VERSION_CODES.DONUT |
| 425 | || (root.appInfo.flags & SUPPORTS_SCREEN_RESIZEABLE_MASK) == 0) { |
| 426 | return true; |
| 427 | } |
| 428 | |
| 429 | return !root.isResizeable(); |
| 430 | } |
| 431 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 432 | /** |
| 433 | * Resolves activity requested orientation to 4 categories: |
| 434 | * 1) {@link ActivityInfo#SCREEN_ORIENTATION_LOCKED} indicating app wants to lock down |
| 435 | * orientation; |
| 436 | * 2) {@link ActivityInfo#SCREEN_ORIENTATION_LANDSCAPE} indicating app wants to be in landscape; |
| 437 | * 3) {@link ActivityInfo#SCREEN_ORIENTATION_PORTRAIT} indicating app wants to be in portrait; |
| 438 | * 4) {@link ActivityInfo#SCREEN_ORIENTATION_UNSPECIFIED} indicating app can handle any |
| 439 | * orientation. |
| 440 | * |
| 441 | * @param activity the activity to check |
| 442 | * @return corresponding resolved orientation value. |
| 443 | */ |
| 444 | private int resolveOrientation(@NonNull ActivityRecord activity) { |
| 445 | int orientation = activity.info.screenOrientation; |
| 446 | switch (orientation) { |
| 447 | case SCREEN_ORIENTATION_NOSENSOR: |
| 448 | case SCREEN_ORIENTATION_LOCKED: |
| 449 | orientation = SCREEN_ORIENTATION_LOCKED; |
| 450 | break; |
| 451 | case SCREEN_ORIENTATION_SENSOR_LANDSCAPE: |
| 452 | case SCREEN_ORIENTATION_REVERSE_LANDSCAPE: |
| 453 | case SCREEN_ORIENTATION_USER_LANDSCAPE: |
| 454 | case SCREEN_ORIENTATION_LANDSCAPE: |
| 455 | if (DEBUG) appendLog("activity-requested-landscape"); |
| 456 | orientation = SCREEN_ORIENTATION_LANDSCAPE; |
| 457 | break; |
| 458 | case SCREEN_ORIENTATION_SENSOR_PORTRAIT: |
| 459 | case SCREEN_ORIENTATION_REVERSE_PORTRAIT: |
| 460 | case SCREEN_ORIENTATION_USER_PORTRAIT: |
| 461 | case SCREEN_ORIENTATION_PORTRAIT: |
| 462 | if (DEBUG) appendLog("activity-requested-portrait"); |
| 463 | orientation = SCREEN_ORIENTATION_PORTRAIT; |
| 464 | break; |
| 465 | default: |
| 466 | orientation = SCREEN_ORIENTATION_UNSPECIFIED; |
| 467 | } |
| 468 | |
| 469 | return orientation; |
| 470 | } |
| 471 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 472 | private void cascadeBounds(@NonNull Rect srcBounds, @NonNull ActivityDisplay display, |
| 473 | @NonNull Rect outBounds) { |
| 474 | outBounds.set(srcBounds); |
| 475 | float density = (float) display.getConfiguration().densityDpi / DENSITY_DEFAULT; |
| 476 | final int defaultOffset = (int) (CASCADING_OFFSET_DP * density + 0.5f); |
| 477 | |
| 478 | display.getBounds(mTmpBounds); |
| 479 | final int dx = Math.min(defaultOffset, Math.max(0, mTmpBounds.right - srcBounds.right)); |
| 480 | final int dy = Math.min(defaultOffset, Math.max(0, mTmpBounds.bottom - srcBounds.bottom)); |
| 481 | outBounds.offset(dx, dy); |
| 482 | } |
| 483 | |
| 484 | private void getTaskBounds(@NonNull ActivityRecord root, @NonNull ActivityDisplay display, |
| 485 | @NonNull ActivityInfo.WindowLayout layout, int resolvedMode, boolean hasInitialBounds, |
| 486 | @NonNull Rect inOutBounds) { |
| 487 | if (resolvedMode == WINDOWING_MODE_FULLSCREEN) { |
| 488 | // We don't handle letterboxing here. Letterboxing will be handled by valid checks |
| 489 | // later. |
| 490 | inOutBounds.setEmpty(); |
| 491 | if (DEBUG) appendLog("maximized-bounds"); |
| 492 | return; |
| 493 | } |
| 494 | |
| 495 | if (resolvedMode != WINDOWING_MODE_FREEFORM) { |
| 496 | // We don't apply freeform bounds adjustment to other windowing modes. |
| 497 | if (DEBUG) { |
| 498 | appendLog("skip-bounds-" + WindowConfiguration.windowingModeToString(resolvedMode)); |
| 499 | } |
| 500 | return; |
| 501 | } |
| 502 | |
| 503 | final int orientation = resolveOrientation(root, display, inOutBounds); |
| 504 | if (orientation != SCREEN_ORIENTATION_PORTRAIT |
| 505 | && orientation != SCREEN_ORIENTATION_LANDSCAPE) { |
| 506 | throw new IllegalStateException( |
| 507 | "Orientation must be one of portrait or landscape, but it's " |
| 508 | + ActivityInfo.screenOrientationToString(orientation)); |
| 509 | } |
| 510 | |
| 511 | // First we get the default size we want. |
| 512 | getDefaultFreeformSize(display, layout, orientation, mTmpBounds); |
| 513 | if (hasInitialBounds || sizeMatches(inOutBounds, mTmpBounds)) { |
| 514 | // We're here because either input parameters specified initial bounds, or the suggested |
| 515 | // bounds have the same size of the default freeform size. We should use the suggested |
| 516 | // bounds if possible -- so if app can handle the orientation we just use it, and if not |
| 517 | // we transpose the suggested bounds in-place. |
| 518 | if (orientation == orientationFromBounds(inOutBounds)) { |
| 519 | if (DEBUG) appendLog("freeform-size-orientation-match=" + inOutBounds); |
| 520 | } else { |
| 521 | // Meh, orientation doesn't match. Let's rotate inOutBounds in-place. |
| 522 | centerBounds(display, inOutBounds.height(), inOutBounds.width(), inOutBounds); |
| 523 | if (DEBUG) appendLog("freeform-orientation-mismatch=" + inOutBounds); |
| 524 | } |
| 525 | } else { |
| 526 | // We are here either because there is no suggested bounds, or the suggested bounds is |
| 527 | // a cascade from source activity. We should use the default freeform size and center it |
| 528 | // to the center of suggested bounds (or the display if no suggested bounds). The |
| 529 | // default size might be too big to center to source activity bounds in display, so we |
| 530 | // may need to move it back to the display. |
| 531 | centerBounds(display, mTmpBounds.width(), mTmpBounds.height(), inOutBounds); |
| 532 | adjustBoundsToFitInDisplay(display, inOutBounds); |
| 533 | if (DEBUG) appendLog("freeform-size-mismatch=" + inOutBounds); |
| 534 | } |
| 535 | |
| 536 | // Lastly we adjust bounds to avoid conflicts with other tasks as much as possible. |
| 537 | adjustBoundsToAvoidConflict(display, inOutBounds); |
| 538 | } |
| 539 | |
Garfield Tan | bb0270f | 2018-12-05 11:30:27 -0800 | [diff] [blame] | 540 | private int convertOrientationToScreenOrientation(int orientation) { |
| 541 | switch (orientation) { |
| 542 | case Configuration.ORIENTATION_LANDSCAPE: |
| 543 | return SCREEN_ORIENTATION_LANDSCAPE; |
| 544 | case Configuration.ORIENTATION_PORTRAIT: |
| 545 | return SCREEN_ORIENTATION_PORTRAIT; |
| 546 | default: |
| 547 | return SCREEN_ORIENTATION_UNSPECIFIED; |
| 548 | } |
| 549 | } |
| 550 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 551 | private int resolveOrientation(@NonNull ActivityRecord root, @NonNull ActivityDisplay display, |
| 552 | @NonNull Rect bounds) { |
| 553 | int orientation = resolveOrientation(root); |
| 554 | |
| 555 | if (orientation == SCREEN_ORIENTATION_LOCKED) { |
Garfield Tan | bb0270f | 2018-12-05 11:30:27 -0800 | [diff] [blame] | 556 | orientation = bounds.isEmpty() |
| 557 | ? convertOrientationToScreenOrientation(display.getConfiguration().orientation) |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 558 | : orientationFromBounds(bounds); |
| 559 | if (DEBUG) { |
| 560 | appendLog(bounds.isEmpty() ? "locked-orientation-from-display=" + orientation |
| 561 | : "locked-orientation-from-bounds=" + bounds); |
Filip Gruszczynski | e5390e7 | 2015-08-18 16:39:00 -0700 | [diff] [blame] | 562 | } |
| 563 | } |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 564 | |
| 565 | if (orientation == SCREEN_ORIENTATION_UNSPECIFIED) { |
| 566 | orientation = bounds.isEmpty() ? SCREEN_ORIENTATION_PORTRAIT |
| 567 | : orientationFromBounds(bounds); |
| 568 | if (DEBUG) { |
| 569 | appendLog(bounds.isEmpty() ? "default-portrait" |
| 570 | : "orientation-from-bounds=" + bounds); |
| 571 | } |
| 572 | } |
| 573 | |
| 574 | return orientation; |
| 575 | } |
| 576 | |
| 577 | private void getDefaultFreeformSize(@NonNull ActivityDisplay display, |
| 578 | @NonNull ActivityInfo.WindowLayout layout, int orientation, @NonNull Rect bounds) { |
| 579 | // Default size, which is letterboxing/pillarboxing in display. That's to say the large |
| 580 | // dimension of default size is the small dimension of display size, and the small dimension |
| 581 | // of default size is calculated to keep the same aspect ratio as the display's. |
| 582 | Rect displayBounds = display.getBounds(); |
| 583 | final int portraitHeight = Math.min(displayBounds.width(), displayBounds.height()); |
| 584 | final int otherDimension = Math.max(displayBounds.width(), displayBounds.height()); |
| 585 | final int portraitWidth = (portraitHeight * portraitHeight) / otherDimension; |
| 586 | final int defaultWidth = (orientation == SCREEN_ORIENTATION_LANDSCAPE) ? portraitHeight |
| 587 | : portraitWidth; |
| 588 | final int defaultHeight = (orientation == SCREEN_ORIENTATION_LANDSCAPE) ? portraitWidth |
| 589 | : portraitHeight; |
| 590 | |
| 591 | // Get window size based on Nexus 5x screen, we assume that this is enough to show content |
| 592 | // of activities. |
| 593 | final float density = (float) display.getConfiguration().densityDpi / DENSITY_DEFAULT; |
| 594 | final int phonePortraitWidth = (int) (DEFAULT_PORTRAIT_PHONE_WIDTH_DP * density + 0.5f); |
| 595 | final int phonePortraitHeight = (int) (DEFAULT_PORTRAIT_PHONE_HEIGHT_DP * density + 0.5f); |
| 596 | final int phoneWidth = (orientation == SCREEN_ORIENTATION_LANDSCAPE) ? phonePortraitHeight |
| 597 | : phonePortraitWidth; |
| 598 | final int phoneHeight = (orientation == SCREEN_ORIENTATION_LANDSCAPE) ? phonePortraitWidth |
| 599 | : phonePortraitHeight; |
| 600 | |
| 601 | // Minimum layout requirements. |
| 602 | final int layoutMinWidth = (layout == null) ? -1 : layout.minWidth; |
| 603 | final int layoutMinHeight = (layout == null) ? -1 : layout.minHeight; |
| 604 | |
| 605 | // Final result. |
| 606 | final int width = Math.min(defaultWidth, Math.max(phoneWidth, layoutMinWidth)); |
| 607 | final int height = Math.min(defaultHeight, Math.max(phoneHeight, layoutMinHeight)); |
| 608 | |
| 609 | bounds.set(0, 0, width, height); |
| 610 | } |
| 611 | |
| 612 | /** |
| 613 | * Gets centered bounds of width x height. If inOutBounds is not empty, the result bounds |
| 614 | * centers at its center or display's center if inOutBounds is empty. |
| 615 | */ |
| 616 | private void centerBounds(@NonNull ActivityDisplay display, int width, int height, |
| 617 | @NonNull Rect inOutBounds) { |
| 618 | if (inOutBounds.isEmpty()) { |
| 619 | display.getBounds(inOutBounds); |
| 620 | } |
| 621 | final int left = inOutBounds.centerX() - width / 2; |
| 622 | final int top = inOutBounds.centerY() - height / 2; |
| 623 | inOutBounds.set(left, top, left + width, top + height); |
| 624 | } |
| 625 | |
| 626 | private void adjustBoundsToFitInDisplay(@NonNull ActivityDisplay display, |
| 627 | @NonNull Rect inOutBounds) { |
| 628 | final Rect displayBounds = display.getBounds(); |
| 629 | |
| 630 | if (displayBounds.width() < inOutBounds.width() |
| 631 | || displayBounds.height() < inOutBounds.height()) { |
| 632 | // There is no way for us to fit the bounds in the display without changing width |
Garfield Tan | 891146c | 2018-10-09 12:14:00 -0700 | [diff] [blame] | 633 | // or height. Just move the start to align with the display. |
Wale Ogunwale | d32da47 | 2018-11-16 07:19:28 -0800 | [diff] [blame] | 634 | final int layoutDirection = |
| 635 | mSupervisor.mRootActivityContainer.getConfiguration().getLayoutDirection(); |
Garfield Tan | 891146c | 2018-10-09 12:14:00 -0700 | [diff] [blame] | 636 | final int left = layoutDirection == View.LAYOUT_DIRECTION_RTL |
| 637 | ? displayBounds.width() - inOutBounds.width() |
| 638 | : 0; |
| 639 | inOutBounds.offsetTo(left, 0 /* newTop */); |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 640 | return; |
| 641 | } |
| 642 | |
| 643 | final int dx; |
| 644 | if (inOutBounds.right > displayBounds.right) { |
| 645 | // Right edge is out of display. |
| 646 | dx = displayBounds.right - inOutBounds.right; |
| 647 | } else if (inOutBounds.left < displayBounds.left) { |
| 648 | // Left edge is out of display. |
| 649 | dx = displayBounds.left - inOutBounds.left; |
| 650 | } else { |
| 651 | // Vertical edges are all in display. |
| 652 | dx = 0; |
| 653 | } |
| 654 | |
| 655 | final int dy; |
| 656 | if (inOutBounds.top < displayBounds.top) { |
| 657 | // Top edge is out of display. |
| 658 | dy = displayBounds.top - inOutBounds.top; |
| 659 | } else if (inOutBounds.bottom > displayBounds.bottom) { |
| 660 | // Bottom edge is out of display. |
| 661 | dy = displayBounds.bottom - inOutBounds.bottom; |
| 662 | } else { |
| 663 | // Horizontal edges are all in display. |
| 664 | dy = 0; |
| 665 | } |
| 666 | inOutBounds.offset(dx, dy); |
| 667 | } |
| 668 | |
| 669 | /** |
| 670 | * Adjusts input bounds to avoid conflict with existing tasks in the display. |
| 671 | * |
| 672 | * If the input bounds conflict with existing tasks, this method scans the bounds in a series of |
| 673 | * directions to find a location where the we can put the bounds in display without conflict |
| 674 | * with any other tasks. |
| 675 | * |
| 676 | * It doesn't try to adjust bounds that's not fully in the given display. |
| 677 | * |
| 678 | * @param display the display which tasks are to check |
| 679 | * @param inOutBounds the bounds used to input initial bounds and output result bounds |
| 680 | */ |
| 681 | private void adjustBoundsToAvoidConflict(@NonNull ActivityDisplay display, |
| 682 | @NonNull Rect inOutBounds) { |
| 683 | final Rect displayBounds = display.getBounds(); |
| 684 | if (!displayBounds.contains(inOutBounds)) { |
| 685 | // The initial bounds are already out of display. The scanning algorithm below doesn't |
| 686 | // work so well with them. |
| 687 | return; |
| 688 | } |
| 689 | |
| 690 | final List<TaskRecord> tasksToCheck = new ArrayList<>(); |
| 691 | for (int i = 0; i < display.getChildCount(); ++i) { |
Yunfan Chen | 279f558 | 2018-12-12 15:24:50 -0800 | [diff] [blame] | 692 | final ActivityStack stack = display.getChildAt(i); |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 693 | if (!stack.inFreeformWindowingMode()) { |
| 694 | continue; |
| 695 | } |
| 696 | |
| 697 | for (int j = 0; j < stack.getChildCount(); ++j) { |
| 698 | tasksToCheck.add(stack.getChildAt(j)); |
| 699 | } |
| 700 | } |
| 701 | |
| 702 | if (!boundsConflict(tasksToCheck, inOutBounds)) { |
| 703 | // Current proposal doesn't conflict with any task. Early return to avoid unnecessary |
| 704 | // calculation. |
| 705 | return; |
| 706 | } |
| 707 | |
| 708 | calculateCandidateShiftDirections(displayBounds, inOutBounds); |
| 709 | for (int direction : mTmpDirections) { |
| 710 | if (direction == Gravity.NO_GRAVITY) { |
| 711 | // We exhausted candidate directions, give up. |
| 712 | break; |
| 713 | } |
| 714 | |
| 715 | mTmpBounds.set(inOutBounds); |
| 716 | while (boundsConflict(tasksToCheck, mTmpBounds) && displayBounds.contains(mTmpBounds)) { |
| 717 | shiftBounds(direction, displayBounds, mTmpBounds); |
| 718 | } |
| 719 | |
| 720 | if (!boundsConflict(tasksToCheck, mTmpBounds) && displayBounds.contains(mTmpBounds)) { |
| 721 | // Found a candidate. Just use this. |
| 722 | inOutBounds.set(mTmpBounds); |
| 723 | if (DEBUG) appendLog("avoid-bounds-conflict=" + inOutBounds); |
| 724 | return; |
| 725 | } |
| 726 | |
| 727 | // Didn't find a conflict free bounds here. Try the next candidate direction. |
| 728 | } |
| 729 | |
| 730 | // We failed to find a conflict free location. Just keep the original result. |
| 731 | } |
| 732 | |
| 733 | /** |
| 734 | * Determines scanning directions and their priorities to avoid bounds conflict. |
| 735 | * |
| 736 | * @param availableBounds bounds that the result must be in |
| 737 | * @param initialBounds initial bounds when start scanning |
| 738 | */ |
| 739 | private void calculateCandidateShiftDirections(@NonNull Rect availableBounds, |
| 740 | @NonNull Rect initialBounds) { |
| 741 | for (int i = 0; i < mTmpDirections.length; ++i) { |
| 742 | mTmpDirections[i] = Gravity.NO_GRAVITY; |
| 743 | } |
| 744 | |
| 745 | final int oneThirdWidth = (2 * availableBounds.left + availableBounds.right) / 3; |
| 746 | final int twoThirdWidth = (availableBounds.left + 2 * availableBounds.right) / 3; |
| 747 | final int centerX = initialBounds.centerX(); |
| 748 | if (centerX < oneThirdWidth) { |
| 749 | // Too close to left, just scan to the right. |
| 750 | mTmpDirections[0] = Gravity.RIGHT; |
| 751 | return; |
| 752 | } else if (centerX > twoThirdWidth) { |
| 753 | // Too close to right, just scan to the left. |
| 754 | mTmpDirections[0] = Gravity.LEFT; |
| 755 | return; |
| 756 | } |
| 757 | |
| 758 | final int oneThirdHeight = (2 * availableBounds.top + availableBounds.bottom) / 3; |
| 759 | final int twoThirdHeight = (availableBounds.top + 2 * availableBounds.bottom) / 3; |
| 760 | final int centerY = initialBounds.centerY(); |
| 761 | if (centerY < oneThirdHeight || centerY > twoThirdHeight) { |
| 762 | // Too close to top or bottom boundary and we're in the middle horizontally, scan |
| 763 | // horizontally in both directions. |
| 764 | mTmpDirections[0] = Gravity.RIGHT; |
| 765 | mTmpDirections[1] = Gravity.LEFT; |
| 766 | return; |
| 767 | } |
| 768 | |
| 769 | // We're in the center region both horizontally and vertically. Scan in both directions of |
| 770 | // primary diagonal. |
| 771 | mTmpDirections[0] = Gravity.BOTTOM | Gravity.RIGHT; |
| 772 | mTmpDirections[1] = Gravity.TOP | Gravity.LEFT; |
| 773 | } |
| 774 | |
| 775 | private boolean boundsConflict(@NonNull List<TaskRecord> tasks, @NonNull Rect bounds) { |
| 776 | for (TaskRecord task : tasks) { |
| 777 | final Rect taskBounds = task.getBounds(); |
| 778 | final boolean leftClose = Math.abs(taskBounds.left - bounds.left) |
| 779 | < BOUNDS_CONFLICT_THRESHOLD; |
| 780 | final boolean topClose = Math.abs(taskBounds.top - bounds.top) |
| 781 | < BOUNDS_CONFLICT_THRESHOLD; |
| 782 | final boolean rightClose = Math.abs(taskBounds.right - bounds.right) |
| 783 | < BOUNDS_CONFLICT_THRESHOLD; |
| 784 | final boolean bottomClose = Math.abs(taskBounds.bottom - bounds.bottom) |
| 785 | < BOUNDS_CONFLICT_THRESHOLD; |
| 786 | |
| 787 | if ((leftClose && topClose) || (leftClose && bottomClose) || (rightClose && topClose) |
| 788 | || (rightClose && bottomClose)) { |
| 789 | return true; |
| 790 | } |
| 791 | } |
| 792 | |
Filip Gruszczynski | e5390e7 | 2015-08-18 16:39:00 -0700 | [diff] [blame] | 793 | return false; |
| 794 | } |
| 795 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 796 | private void shiftBounds(int direction, @NonNull Rect availableRect, |
| 797 | @NonNull Rect inOutBounds) { |
| 798 | final int horizontalOffset; |
| 799 | switch (direction & Gravity.HORIZONTAL_GRAVITY_MASK) { |
| 800 | case Gravity.LEFT: |
| 801 | horizontalOffset = -Math.max(MINIMAL_STEP, |
| 802 | availableRect.width() / STEP_DENOMINATOR); |
| 803 | break; |
| 804 | case Gravity.RIGHT: |
| 805 | horizontalOffset = Math.max(MINIMAL_STEP, availableRect.width() / STEP_DENOMINATOR); |
| 806 | break; |
| 807 | default: |
| 808 | horizontalOffset = 0; |
| 809 | } |
| 810 | |
| 811 | final int verticalOffset; |
| 812 | switch (direction & Gravity.VERTICAL_GRAVITY_MASK) { |
| 813 | case Gravity.TOP: |
| 814 | verticalOffset = -Math.max(MINIMAL_STEP, availableRect.height() / STEP_DENOMINATOR); |
| 815 | break; |
| 816 | case Gravity.BOTTOM: |
| 817 | verticalOffset = Math.max(MINIMAL_STEP, availableRect.height() / STEP_DENOMINATOR); |
| 818 | break; |
| 819 | default: |
| 820 | verticalOffset = 0; |
| 821 | } |
| 822 | |
| 823 | inOutBounds.offset(horizontalOffset, verticalOffset); |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 824 | } |
| 825 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 826 | private void initLogBuilder(TaskRecord task, ActivityRecord activity) { |
| 827 | if (DEBUG) { |
| 828 | mLogBuilder = new StringBuilder("TaskLaunchParamsModifier:task=" + task |
| 829 | + " activity=" + activity); |
| 830 | } |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 831 | } |
| 832 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 833 | private void appendLog(String log) { |
| 834 | if (DEBUG) mLogBuilder.append(" ").append(log); |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 835 | } |
| 836 | |
Garfield Tan | b5cc09f | 2018-09-28 10:06:52 -0700 | [diff] [blame] | 837 | private void outputLog() { |
| 838 | if (DEBUG) Slog.d(TAG, mLogBuilder.toString()); |
| 839 | } |
| 840 | |
| 841 | private static int orientationFromBounds(Rect bounds) { |
| 842 | return bounds.width() > bounds.height() ? SCREEN_ORIENTATION_LANDSCAPE |
| 843 | : SCREEN_ORIENTATION_PORTRAIT; |
| 844 | } |
| 845 | |
| 846 | private static boolean sizeMatches(Rect left, Rect right) { |
| 847 | return (Math.abs(right.width() - left.width()) < EPSILON) |
| 848 | && (Math.abs(right.height() - left.height()) < EPSILON); |
Filip Gruszczynski | 9b1ce52 | 2015-08-20 18:37:19 -0700 | [diff] [blame] | 849 | } |
Filip Gruszczynski | e5390e7 | 2015-08-18 16:39:00 -0700 | [diff] [blame] | 850 | } |