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