1 /* 2 * Copyright (C) 2021 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 androidx.window.extensions.embedding; 18 19 import static android.content.pm.PackageManager.MATCH_ALL; 20 21 import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider; 22 import static androidx.window.extensions.embedding.SplitAttributesHelper.isReversedLayout; 23 import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK; 24 25 import android.app.Activity; 26 import android.app.ActivityThread; 27 import android.app.WindowConfiguration; 28 import android.app.WindowConfiguration.WindowingMode; 29 import android.content.Intent; 30 import android.content.pm.ActivityInfo; 31 import android.content.pm.PackageManager; 32 import android.content.pm.ResolveInfo; 33 import android.content.res.Configuration; 34 import android.graphics.Rect; 35 import android.os.Bundle; 36 import android.os.IBinder; 37 import android.util.Pair; 38 import android.util.Size; 39 import android.view.View; 40 import android.view.WindowMetrics; 41 import android.window.TaskFragmentAnimationParams; 42 import android.window.TaskFragmentCreationParams; 43 import android.window.WindowContainerTransaction; 44 45 import androidx.annotation.IntDef; 46 import androidx.annotation.NonNull; 47 import androidx.annotation.Nullable; 48 import androidx.window.extensions.core.util.function.Function; 49 import androidx.window.extensions.embedding.SplitAttributes.SplitType; 50 import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType; 51 import androidx.window.extensions.embedding.SplitAttributes.SplitType.HingeSplitType; 52 import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType; 53 import androidx.window.extensions.embedding.TaskContainer.TaskProperties; 54 import androidx.window.extensions.layout.DisplayFeature; 55 import androidx.window.extensions.layout.FoldingFeature; 56 import androidx.window.extensions.layout.WindowLayoutComponentImpl; 57 import androidx.window.extensions.layout.WindowLayoutInfo; 58 59 import com.android.internal.annotations.VisibleForTesting; 60 import com.android.window.flags.Flags; 61 62 import java.util.ArrayList; 63 import java.util.List; 64 import java.util.Objects; 65 import java.util.concurrent.Executor; 66 67 /** 68 * Controls the visual presentation of the splits according to the containers formed by 69 * {@link SplitController}. 70 * 71 * Note that all calls into this class must hold the {@link SplitController} internal lock. 72 */ 73 @SuppressWarnings("GuardedBy") 74 class SplitPresenter extends JetpackTaskFragmentOrganizer { 75 @VisibleForTesting 76 static final int POSITION_START = 0; 77 @VisibleForTesting 78 static final int POSITION_END = 1; 79 @VisibleForTesting 80 static final int POSITION_FILL = 2; 81 82 @IntDef(value = { 83 POSITION_START, 84 POSITION_END, 85 POSITION_FILL, 86 }) 87 private @interface Position {} 88 89 static final int CONTAINER_POSITION_LEFT = 0; 90 static final int CONTAINER_POSITION_TOP = 1; 91 static final int CONTAINER_POSITION_RIGHT = 2; 92 static final int CONTAINER_POSITION_BOTTOM = 3; 93 94 @IntDef(value = { 95 CONTAINER_POSITION_LEFT, 96 CONTAINER_POSITION_TOP, 97 CONTAINER_POSITION_RIGHT, 98 CONTAINER_POSITION_BOTTOM, 99 }) 100 @interface ContainerPosition {} 101 102 /** 103 * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, 104 * Activity, Activity, Intent)}. 105 * No need to expand the splitContainer because screen is big enough to 106 * {@link #shouldShowSplit(SplitAttributes)} and minimum dimensions is 107 * satisfied. 108 */ 109 static final int RESULT_NOT_EXPANDED = 0; 110 /** 111 * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, 112 * Activity, Activity, Intent)}. 113 * The splitContainer should be expanded. It is usually because minimum dimensions is not 114 * satisfied. 115 * @see #shouldShowSplit(SplitAttributes) 116 */ 117 static final int RESULT_EXPANDED = 1; 118 /** 119 * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, 120 * Activity, Activity, Intent)}. 121 * The splitContainer should be expanded, but the client side hasn't received 122 * {@link android.window.TaskFragmentInfo} yet. Fallback to create new expanded SplitContainer 123 * instead. 124 */ 125 static final int RESULT_EXPAND_FAILED_NO_TF_INFO = 2; 126 127 /** 128 * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, 129 * Activity, Activity, Intent)} 130 */ 131 @IntDef(value = { 132 RESULT_NOT_EXPANDED, 133 RESULT_EXPANDED, 134 RESULT_EXPAND_FAILED_NO_TF_INFO, 135 }) 136 private @interface ResultCode {} 137 138 @VisibleForTesting 139 static final SplitAttributes EXPAND_CONTAINERS_ATTRIBUTES = 140 new SplitAttributes.Builder() 141 .setSplitType(new ExpandContainersSplitType()) 142 .build(); 143 144 private final WindowLayoutComponentImpl mWindowLayoutComponent; 145 private final SplitController mController; 146 SplitPresenter(@onNull Executor executor, @NonNull WindowLayoutComponentImpl windowLayoutComponent, @NonNull SplitController controller)147 SplitPresenter(@NonNull Executor executor, 148 @NonNull WindowLayoutComponentImpl windowLayoutComponent, 149 @NonNull SplitController controller) { 150 super(executor, controller); 151 mWindowLayoutComponent = windowLayoutComponent; 152 mController = controller; 153 registerOrganizer(); 154 if (!SplitController.ENABLE_SHELL_TRANSITIONS) { 155 // TODO(b/207070762): cleanup with legacy app transition 156 // Animation will be handled by WM Shell when Shell transition is enabled. 157 overrideSplitAnimation(); 158 } 159 } 160 161 /** 162 * Deletes the specified container and all other associated and dependent containers in the same 163 * transaction. 164 */ cleanupContainer(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, boolean shouldFinishDependent)165 void cleanupContainer(@NonNull WindowContainerTransaction wct, 166 @NonNull TaskFragmentContainer container, boolean shouldFinishDependent) { 167 container.finish(shouldFinishDependent, this, wct, mController); 168 // Make sure the containers in the Task is up-to-date. 169 mController.updateContainersInTaskIfVisible(wct, container.getTaskId()); 170 } 171 172 /** 173 * Creates a new split with the primary activity and an empty secondary container. 174 * @return The newly created secondary container. 175 */ 176 @NonNull createNewSplitWithEmptySideContainer( @onNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @NonNull Intent secondaryIntent, @NonNull SplitPairRule rule, @NonNull SplitAttributes splitAttributes)177 TaskFragmentContainer createNewSplitWithEmptySideContainer( 178 @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, 179 @NonNull Intent secondaryIntent, @NonNull SplitPairRule rule, 180 @NonNull SplitAttributes splitAttributes) { 181 final TaskProperties taskProperties = getTaskProperties(primaryActivity); 182 final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties, 183 splitAttributes); 184 final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, 185 primaryActivity, primaryRelBounds, splitAttributes, null /* containerToAvoid */); 186 187 // Create new empty task fragment 188 final int taskId = primaryContainer.getTaskId(); 189 final TaskFragmentContainer secondaryContainer = 190 new TaskFragmentContainer.Builder(mController, taskId, primaryActivity) 191 .setPendingAppearedIntent(secondaryIntent).build(); 192 final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties, 193 splitAttributes); 194 final int windowingMode = mController.getTaskContainer(taskId) 195 .getWindowingModeForTaskFragment(secondaryRelBounds); 196 createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(), 197 primaryActivity.getActivityToken(), secondaryRelBounds, windowingMode); 198 updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes); 199 200 // Set adjacent to each other so that the containers below will be invisible. 201 setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, 202 splitAttributes); 203 204 mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule, 205 splitAttributes); 206 207 return secondaryContainer; 208 } 209 210 /** 211 * Creates a new split container with the two provided activities. 212 * @param primaryActivity An activity that should be in the primary container. If it is not 213 * currently in an existing container, a new one will be created and the 214 * activity will be re-parented to it. 215 * @param secondaryActivity An activity that should be in the secondary container. If it is not 216 * currently in an existing container, or if it is currently in the 217 * same container as the primary activity, a new container will be 218 * created and the activity will be re-parented to it. 219 * @param rule The split rule to be applied to the container. 220 * @param splitAttributes The {@link SplitAttributes} to apply 221 */ createNewSplitContainer(@onNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, @NonNull SplitPairRule rule, @NonNull SplitAttributes splitAttributes)222 void createNewSplitContainer(@NonNull WindowContainerTransaction wct, 223 @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, 224 @NonNull SplitPairRule rule, @NonNull SplitAttributes splitAttributes) { 225 final TaskProperties taskProperties = getTaskProperties(primaryActivity); 226 final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties, 227 splitAttributes); 228 final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, 229 primaryActivity, primaryRelBounds, splitAttributes, null /* containerToAvoid */); 230 231 final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties, 232 splitAttributes); 233 final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity( 234 secondaryActivity); 235 TaskFragmentContainer containerToAvoid = primaryContainer; 236 if (curSecondaryContainer != null && curSecondaryContainer != primaryContainer 237 && (rule.shouldClearTop() || primaryContainer.isAbove(curSecondaryContainer))) { 238 // Do not reuse the current TaskFragment if the rule is to clear top, or if it is below 239 // the primary TaskFragment. 240 containerToAvoid = curSecondaryContainer; 241 } 242 final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct, 243 secondaryActivity, secondaryRelBounds, splitAttributes, containerToAvoid); 244 245 // Set adjacent to each other so that the containers below will be invisible. 246 setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, 247 splitAttributes); 248 249 mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule, 250 splitAttributes); 251 } 252 253 /** 254 * Creates a new container or resizes an existing container for activity to the provided bounds. 255 * @param activity The activity to be re-parented to the container if necessary. 256 * @param containerToAvoid Re-parent from this container if an activity is already in it. 257 */ prepareContainerForActivity( @onNull WindowContainerTransaction wct, @NonNull Activity activity, @NonNull Rect relBounds, @NonNull SplitAttributes splitAttributes, @Nullable TaskFragmentContainer containerToAvoid)258 private TaskFragmentContainer prepareContainerForActivity( 259 @NonNull WindowContainerTransaction wct, @NonNull Activity activity, 260 @NonNull Rect relBounds, @NonNull SplitAttributes splitAttributes, 261 @Nullable TaskFragmentContainer containerToAvoid) { 262 TaskFragmentContainer container = mController.getContainerWithActivity(activity); 263 final int taskId = container != null ? container.getTaskId() : activity.getTaskId(); 264 if (container == null || container == containerToAvoid) { 265 container = new TaskFragmentContainer.Builder(mController, taskId, activity) 266 .setPendingAppearedActivity(activity).build(); 267 final int windowingMode = mController.getTaskContainer(taskId) 268 .getWindowingModeForTaskFragment(relBounds); 269 final IBinder reparentActivityToken = activity.getActivityToken(); 270 createTaskFragment(wct, container.getTaskFragmentToken(), reparentActivityToken, 271 relBounds, windowingMode, reparentActivityToken); 272 wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(), 273 reparentActivityToken); 274 } else { 275 resizeTaskFragmentIfRegistered(wct, container, relBounds); 276 final int windowingMode = mController.getTaskContainer(taskId) 277 .getWindowingModeForTaskFragment(relBounds); 278 updateTaskFragmentWindowingModeIfRegistered(wct, container, windowingMode); 279 } 280 updateAnimationParams(wct, container.getTaskFragmentToken(), splitAttributes); 281 282 return container; 283 } 284 285 /** 286 * Starts a new activity to the side, creating a new split container. A new container will be 287 * created for the activity that will be started. 288 * @param launchingActivity An activity that should be in the primary container. If it is not 289 * currently in an existing container, a new one will be created and 290 * the activity will be re-parented to it. 291 * @param activityIntent The intent to start the new activity. 292 * @param activityOptions The options to apply to new activity start. 293 * @param rule The split rule to be applied to the container. 294 * @param isPlaceholder Whether the launch is a placeholder. 295 */ startActivityToSide(@onNull WindowContainerTransaction wct, @NonNull Activity launchingActivity, @NonNull Intent activityIntent, @Nullable Bundle activityOptions, @NonNull SplitRule rule, @NonNull SplitAttributes splitAttributes, boolean isPlaceholder)296 void startActivityToSide(@NonNull WindowContainerTransaction wct, 297 @NonNull Activity launchingActivity, @NonNull Intent activityIntent, 298 @Nullable Bundle activityOptions, @NonNull SplitRule rule, 299 @NonNull SplitAttributes splitAttributes, boolean isPlaceholder) { 300 final TaskProperties taskProperties = getTaskProperties(launchingActivity); 301 final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties, 302 splitAttributes); 303 final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties, 304 splitAttributes); 305 306 TaskFragmentContainer primaryContainer = mController.getContainerWithActivity( 307 launchingActivity); 308 if (primaryContainer == null) { 309 primaryContainer = new TaskFragmentContainer.Builder(mController, 310 launchingActivity.getTaskId(), launchingActivity) 311 .setPendingAppearedActivity(launchingActivity).build(); 312 } 313 314 final int taskId = primaryContainer.getTaskId(); 315 final TaskFragmentContainer secondaryContainer = 316 new TaskFragmentContainer.Builder(mController, taskId, launchingActivity) 317 .setPendingAppearedIntent(activityIntent) 318 // Pass in the primary container to make sure it is added right above the 319 // primary. 320 .setPairedPrimaryContainer(primaryContainer) 321 .build(); 322 final TaskContainer taskContainer = mController.getTaskContainer(taskId); 323 final int windowingMode = taskContainer.getWindowingModeForTaskFragment( 324 primaryRelBounds); 325 mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer, 326 rule, splitAttributes); 327 startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRelBounds, 328 launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRelBounds, 329 activityIntent, activityOptions, rule, windowingMode, splitAttributes); 330 if (isPlaceholder) { 331 // When placeholder is launched in split, we should keep the focus on the primary. 332 wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken()); 333 } 334 } 335 336 /** 337 * Updates the positions of containers in an existing split. 338 * @param splitContainer The split container to be updated. 339 * @param wct WindowContainerTransaction that this update should be performed with. 340 */ updateSplitContainer(@onNull SplitContainer splitContainer, @NonNull WindowContainerTransaction wct)341 void updateSplitContainer(@NonNull SplitContainer splitContainer, 342 @NonNull WindowContainerTransaction wct) { 343 // Getting the parent configuration using the updated container - it will have the recent 344 // value. 345 final SplitRule rule = splitContainer.getSplitRule(); 346 final TaskFragmentContainer primaryContainer = splitContainer.getPrimaryContainer(); 347 final TaskContainer taskContainer = splitContainer.getTaskContainer(); 348 final TaskProperties taskProperties = taskContainer.getTaskProperties(); 349 final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes(); 350 final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties, 351 splitAttributes); 352 final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties, 353 splitAttributes); 354 final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); 355 // Whether the placeholder is becoming side-by-side with the primary from fullscreen. 356 final boolean isPlaceholderBecomingSplit = splitContainer.isPlaceholderContainer() 357 && secondaryContainer.areLastRequestedBoundsEqual(null /* bounds */) 358 && !secondaryRelBounds.isEmpty(); 359 360 // TODO(b/243518738): remove usages of XXXIfRegistered. 361 // If the task fragments are not registered yet, the positions will be updated after they 362 // are created again. 363 resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRelBounds); 364 resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRelBounds); 365 setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, 366 splitAttributes); 367 if (isPlaceholderBecomingSplit) { 368 // When placeholder is shown in split, we should keep the focus on the primary. 369 wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken()); 370 } 371 final int windowingMode = taskContainer.getWindowingModeForTaskFragment( 372 primaryRelBounds); 373 updateTaskFragmentWindowingModeIfRegistered(wct, primaryContainer, windowingMode); 374 updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode); 375 updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes); 376 updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes); 377 mController.updateDivider(wct, taskContainer); 378 } 379 setAdjacentTaskFragments(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer, @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule, @NonNull SplitAttributes splitAttributes)380 private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, 381 @NonNull TaskFragmentContainer primaryContainer, 382 @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule, 383 @NonNull SplitAttributes splitAttributes) { 384 // Clear adjacent TaskFragments if the container is shown in fullscreen, or the 385 // secondaryContainer could not be finished. 386 boolean isStacked = !shouldShowSplit(splitAttributes); 387 if (isStacked) { 388 clearAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken()); 389 } else { 390 setAdjacentTaskFragmentsWithRule(wct, primaryContainer.getTaskFragmentToken(), 391 secondaryContainer.getTaskFragmentToken(), splitRule); 392 } 393 setCompanionTaskFragment(wct, primaryContainer.getTaskFragmentToken(), 394 secondaryContainer.getTaskFragmentToken(), splitRule, isStacked); 395 396 // Sets the dim area when the two TaskFragments are adjacent. 397 final boolean dimOnTask = !isStacked 398 && splitAttributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK 399 && Flags.fullscreenDimFlag(); 400 setTaskFragmentDimOnTask(wct, primaryContainer.getTaskFragmentToken(), dimOnTask); 401 setTaskFragmentDimOnTask(wct, secondaryContainer.getTaskFragmentToken(), dimOnTask); 402 403 // Setting isolated navigation and clear non-sticky pinned container if needed. 404 final SplitPinRule splitPinRule = 405 splitRule instanceof SplitPinRule ? (SplitPinRule) splitRule : null; 406 if (splitPinRule == null) { 407 return; 408 } 409 410 setTaskFragmentPinned(wct, secondaryContainer, !isStacked /* pinned */); 411 if (isStacked && !splitPinRule.isSticky()) { 412 secondaryContainer.getTaskContainer().removeSplitPinContainer(); 413 } 414 } 415 416 /** 417 * Sets whether to enable isolated navigation for this {@link TaskFragmentContainer}. 418 * <p> 419 * If a container enables isolated navigation, activities can't be launched to this container 420 * unless explicitly requested to be launched to. 421 * 422 * @see TaskFragmentContainer#isOverlayWithActivityAssociation() 423 */ setTaskFragmentIsolatedNavigation(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, boolean isolatedNavigationEnabled)424 void setTaskFragmentIsolatedNavigation(@NonNull WindowContainerTransaction wct, 425 @NonNull TaskFragmentContainer container, 426 boolean isolatedNavigationEnabled) { 427 if (!Flags.activityEmbeddingOverlayPresentationFlag() && container.isOverlay()) { 428 return; 429 } 430 if (container.isIsolatedNavigationEnabled() == isolatedNavigationEnabled) { 431 return; 432 } 433 container.setIsolatedNavigationEnabled(isolatedNavigationEnabled); 434 setTaskFragmentIsolatedNavigation(wct, container.getTaskFragmentToken(), 435 isolatedNavigationEnabled); 436 } 437 438 /** 439 * Sets whether to pin this {@link TaskFragmentContainer}. 440 * <p> 441 * If a container is pinned, it won't be chosen as the launch target unless it's the launching 442 * container. 443 * 444 * @see TaskFragmentContainer#isAlwaysOnTopOverlay() 445 * @see TaskContainer#getSplitPinContainer() 446 */ setTaskFragmentPinned(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, boolean pinned)447 void setTaskFragmentPinned(@NonNull WindowContainerTransaction wct, 448 @NonNull TaskFragmentContainer container, 449 boolean pinned) { 450 if (!Flags.activityEmbeddingOverlayPresentationFlag() && container.isOverlay()) { 451 return; 452 } 453 if (container.isPinned() == pinned) { 454 return; 455 } 456 container.setPinned(pinned); 457 setTaskFragmentPinned(wct, container.getTaskFragmentToken(), pinned); 458 } 459 460 /** 461 * Resizes the task fragment if it was already registered. Skips the operation if the container 462 * creation has not been reported from the server yet. 463 */ 464 // TODO(b/190433398): Handle resize if the fragment hasn't appeared yet. 465 @VisibleForTesting resizeTaskFragmentIfRegistered(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, @Nullable Rect relBounds)466 void resizeTaskFragmentIfRegistered(@NonNull WindowContainerTransaction wct, 467 @NonNull TaskFragmentContainer container, 468 @Nullable Rect relBounds) { 469 if (container.getInfo() == null) { 470 return; 471 } 472 resizeTaskFragment(wct, container.getTaskFragmentToken(), relBounds); 473 } 474 475 @VisibleForTesting updateTaskFragmentWindowingModeIfRegistered( @onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, @WindowingMode int windowingMode)476 void updateTaskFragmentWindowingModeIfRegistered( 477 @NonNull WindowContainerTransaction wct, 478 @NonNull TaskFragmentContainer container, 479 @WindowingMode int windowingMode) { 480 if (container.getInfo() != null) { 481 updateWindowingMode(wct, container.getTaskFragmentToken(), windowingMode); 482 } 483 } 484 485 @Override createTaskFragment(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentCreationParams fragmentOptions)486 void createTaskFragment(@NonNull WindowContainerTransaction wct, 487 @NonNull TaskFragmentCreationParams fragmentOptions) { 488 final TaskFragmentContainer container = mController.getContainer( 489 fragmentOptions.getFragmentToken()); 490 if (container == null) { 491 throw new IllegalStateException( 492 "Creating a TaskFragment that is not registered with controller."); 493 } 494 495 container.setLastRequestedBounds(fragmentOptions.getInitialRelativeBounds()); 496 container.setLastRequestedWindowingMode(fragmentOptions.getWindowingMode()); 497 super.createTaskFragment(wct, fragmentOptions); 498 499 // Reorders the pinned TaskFragment to front to ensure it is the front-most TaskFragment. 500 final SplitPinContainer pinnedContainer = 501 container.getTaskContainer().getSplitPinContainer(); 502 if (pinnedContainer != null) { 503 reorderTaskFragmentToFront(wct, 504 pinnedContainer.getSecondaryContainer().getTaskFragmentToken()); 505 } 506 final TaskFragmentContainer alwaysOnTopOverlayContainer = container.getTaskContainer() 507 .getAlwaysOnTopOverlayContainer(); 508 if (alwaysOnTopOverlayContainer != null) { 509 reorderTaskFragmentToFront(wct, alwaysOnTopOverlayContainer.getTaskFragmentToken()); 510 } 511 } 512 513 @Override resizeTaskFragment(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @Nullable Rect relBounds)514 void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, 515 @Nullable Rect relBounds) { 516 TaskFragmentContainer container = mController.getContainer(fragmentToken); 517 if (container == null) { 518 throw new IllegalStateException( 519 "Resizing a TaskFragment that is not registered with controller."); 520 } 521 522 if (container.areLastRequestedBoundsEqual(relBounds)) { 523 // Return early if the provided bounds were already requested 524 return; 525 } 526 527 container.setLastRequestedBounds(relBounds); 528 super.resizeTaskFragment(wct, fragmentToken, relBounds); 529 } 530 531 @Override updateWindowingMode(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @WindowingMode int windowingMode)532 void updateWindowingMode(@NonNull WindowContainerTransaction wct, 533 @NonNull IBinder fragmentToken, @WindowingMode int windowingMode) { 534 final TaskFragmentContainer container = mController.getContainer(fragmentToken); 535 if (container == null) { 536 throw new IllegalStateException("Setting windowing mode for a TaskFragment that is" 537 + " not registered with controller."); 538 } 539 540 if (container.isLastRequestedWindowingModeEqual(windowingMode)) { 541 // Return early if the windowing mode were already requested 542 return; 543 } 544 545 container.setLastRequestedWindowingMode(windowingMode); 546 super.updateWindowingMode(wct, fragmentToken, windowingMode); 547 } 548 549 @Override updateAnimationParams(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @NonNull TaskFragmentAnimationParams animationParams)550 void updateAnimationParams(@NonNull WindowContainerTransaction wct, 551 @NonNull IBinder fragmentToken, @NonNull TaskFragmentAnimationParams animationParams) { 552 final TaskFragmentContainer container = mController.getContainer(fragmentToken); 553 if (container == null) { 554 throw new IllegalStateException("Setting animation params for a TaskFragment that is" 555 + " not registered with controller."); 556 } 557 558 if (container.areLastRequestedAnimationParamsEqual(animationParams)) { 559 // Return early if the animation params were already requested 560 return; 561 } 562 563 container.setLastRequestAnimationParams(animationParams); 564 super.updateAnimationParams(wct, fragmentToken, animationParams); 565 } 566 567 @Override setAdjacentTaskFragments(@onNull WindowContainerTransaction wct, @NonNull IBinder primary, @NonNull IBinder secondary, @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams)568 void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, 569 @NonNull IBinder primary, @NonNull IBinder secondary, 570 @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams) { 571 final TaskFragmentContainer primaryContainer = mController.getContainer(primary); 572 final TaskFragmentContainer secondaryContainer = mController.getContainer(secondary); 573 if (primaryContainer == null || secondaryContainer == null) { 574 throw new IllegalStateException("setAdjacentTaskFragments on TaskFragment that is" 575 + " not registered with controller."); 576 } 577 578 if (primaryContainer.isLastAdjacentTaskFragmentEqual(secondary, adjacentParams) 579 && secondaryContainer.isLastAdjacentTaskFragmentEqual(primary, adjacentParams)) { 580 // Return early if the same adjacent TaskFragments were already requested 581 return; 582 } 583 584 primaryContainer.setLastAdjacentTaskFragment(secondary, adjacentParams); 585 secondaryContainer.setLastAdjacentTaskFragment(primary, adjacentParams); 586 super.setAdjacentTaskFragments(wct, primary, secondary, adjacentParams); 587 } 588 589 @Override clearAdjacentTaskFragments(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken)590 void clearAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, 591 @NonNull IBinder fragmentToken) { 592 final TaskFragmentContainer container = mController.getContainer(fragmentToken); 593 if (container == null) { 594 throw new IllegalStateException("clearAdjacentTaskFragments on TaskFragment that is" 595 + " not registered with controller."); 596 } 597 598 if (container.isLastAdjacentTaskFragmentEqual(null /* fragmentToken*/, null /* params */)) { 599 // Return early if no adjacent TaskFragment was yet requested 600 return; 601 } 602 603 container.clearLastAdjacentTaskFragment(); 604 super.clearAdjacentTaskFragments(wct, fragmentToken); 605 } 606 607 @Override setCompanionTaskFragment(@onNull WindowContainerTransaction wct, @NonNull IBinder primary, @Nullable IBinder secondary)608 void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder primary, 609 @Nullable IBinder secondary) { 610 final TaskFragmentContainer container = mController.getContainer(primary); 611 if (container == null) { 612 throw new IllegalStateException("setCompanionTaskFragment on TaskFragment that is" 613 + " not registered with controller."); 614 } 615 616 if (container.isLastCompanionTaskFragmentEqual(secondary)) { 617 // Return early if the same companion TaskFragment was already requested 618 return; 619 } 620 621 container.setLastCompanionTaskFragment(secondary); 622 super.setCompanionTaskFragment(wct, primary, secondary); 623 } 624 625 /** 626 * Applies the {@code attributes} to a standalone {@code container}. 627 * 628 * @param minDimensions the minimum dimension of the container. 629 */ applyActivityStackAttributes( @onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, @NonNull ActivityStackAttributes attributes, @Nullable Size minDimensions)630 void applyActivityStackAttributes( 631 @NonNull WindowContainerTransaction wct, 632 @NonNull TaskFragmentContainer container, 633 @NonNull ActivityStackAttributes attributes, 634 @Nullable Size minDimensions) { 635 final Rect relativeBounds = sanitizeBounds(attributes.getRelativeBounds(), minDimensions, 636 container); 637 final boolean isFillParent = relativeBounds.isEmpty(); 638 final boolean dimOnTask = !isFillParent 639 && Flags.fullscreenDimFlag() 640 && attributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK; 641 final IBinder fragmentToken = container.getTaskFragmentToken(); 642 643 if (container.isAlwaysOnTopOverlay()) { 644 setTaskFragmentPinned(wct, container, !isFillParent); 645 } else if (container.isOverlayWithActivityAssociation()) { 646 setTaskFragmentIsolatedNavigation(wct, container, !isFillParent); 647 } 648 649 // TODO(b/243518738): Update to resizeTaskFragment after we migrate WCT#setRelativeBounds 650 // and WCT#setWindowingMode to take fragmentToken. 651 resizeTaskFragmentIfRegistered(wct, container, relativeBounds); 652 int windowingMode = container.getTaskContainer().getWindowingModeForTaskFragment( 653 relativeBounds); 654 updateTaskFragmentWindowingModeIfRegistered(wct, container, windowingMode); 655 // Always use default animation for standalone ActivityStack. 656 updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT); 657 setTaskFragmentDimOnTask(wct, fragmentToken, dimOnTask); 658 } 659 660 /** 661 * Returns the expanded bounds if the {@code relBounds} violate minimum dimension or are not 662 * fully covered by the task bounds. Otherwise, returns {@code relBounds}. 663 */ 664 @NonNull sanitizeBounds(@onNull Rect relBounds, @Nullable Size minDimension, @NonNull TaskFragmentContainer container)665 static Rect sanitizeBounds(@NonNull Rect relBounds, @Nullable Size minDimension, 666 @NonNull TaskFragmentContainer container) { 667 if (relBounds.isEmpty()) { 668 // Don't need to check if the bounds follows the task bounds. 669 return relBounds; 670 } 671 if (boundsSmallerThanMinDimensions(relBounds, minDimension)) { 672 // Expand the bounds if the bounds are smaller than minimum dimensions. 673 return new Rect(); 674 } 675 final TaskContainer taskContainer = container.getTaskContainer(); 676 final Rect relTaskBounds = new Rect(taskContainer.getBounds()); 677 relTaskBounds.offsetTo(0, 0); 678 if (!relTaskBounds.contains(relBounds)) { 679 // Expand the bounds if the bounds exceed the task bounds. 680 return new Rect(); 681 } 682 return relBounds; 683 } 684 685 @Override setTaskFragmentDimOnTask(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, boolean dimOnTask)686 void setTaskFragmentDimOnTask(@NonNull WindowContainerTransaction wct, 687 @NonNull IBinder fragmentToken, boolean dimOnTask) { 688 final TaskFragmentContainer container = mController.getContainer(fragmentToken); 689 if (container == null) { 690 throw new IllegalStateException("setTaskFragmentDimOnTask on TaskFragment that is" 691 + " not registered with controller."); 692 } 693 694 if (container.isLastDimOnTask() == dimOnTask) { 695 return; 696 } 697 698 container.setLastDimOnTask(dimOnTask); 699 super.setTaskFragmentDimOnTask(wct, fragmentToken, dimOnTask); 700 } 701 702 /** 703 * Expands the split container if the current split bounds are smaller than the Activity or 704 * Intent that is added to the container. 705 * 706 * @return the {@link ResultCode} based on 707 * {@link #shouldShowSplit(SplitAttributes)} and if 708 * {@link android.window.TaskFragmentInfo} has reported to the client side. 709 */ 710 @ResultCode expandSplitContainerIfNeeded(@onNull WindowContainerTransaction wct, @NonNull SplitContainer splitContainer, @NonNull Activity primaryActivity, @Nullable Activity secondaryActivity, @Nullable Intent secondaryIntent)711 int expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct, 712 @NonNull SplitContainer splitContainer, @NonNull Activity primaryActivity, 713 @Nullable Activity secondaryActivity, @Nullable Intent secondaryIntent) { 714 if (secondaryActivity == null && secondaryIntent == null) { 715 throw new IllegalArgumentException("Either secondaryActivity or secondaryIntent must be" 716 + " non-null."); 717 } 718 final Pair<Size, Size> minDimensionsPair; 719 if (secondaryActivity != null) { 720 minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity); 721 } else { 722 minDimensionsPair = getActivityIntentMinDimensionsPair(primaryActivity, 723 secondaryIntent); 724 } 725 // Expand the splitContainer if minimum dimensions are not satisfied. 726 final TaskContainer taskContainer = splitContainer.getTaskContainer(); 727 final SplitAttributes splitAttributes = sanitizeSplitAttributes( 728 taskContainer.getTaskProperties(), splitContainer.getCurrentSplitAttributes(), 729 minDimensionsPair); 730 splitContainer.updateCurrentSplitAttributes(splitAttributes); 731 if (!shouldShowSplit(splitAttributes)) { 732 // If the client side hasn't received TaskFragmentInfo yet, we can't change TaskFragment 733 // bounds. Return failure to create a new SplitContainer which fills task bounds. 734 if (splitContainer.getPrimaryContainer().getInfo() == null 735 || splitContainer.getSecondaryContainer().getInfo() == null) { 736 return RESULT_EXPAND_FAILED_NO_TF_INFO; 737 } 738 final IBinder primaryToken = 739 splitContainer.getPrimaryContainer().getTaskFragmentToken(); 740 final IBinder secondaryToken = 741 splitContainer.getSecondaryContainer().getTaskFragmentToken(); 742 expandTaskFragment(wct, splitContainer.getPrimaryContainer()); 743 expandTaskFragment(wct, splitContainer.getSecondaryContainer()); 744 // Set the companion TaskFragment when the two containers stacked. 745 setCompanionTaskFragment(wct, primaryToken, secondaryToken, 746 splitContainer.getSplitRule(), true /* isStacked */); 747 return RESULT_EXPANDED; 748 } 749 return RESULT_NOT_EXPANDED; 750 } 751 752 /** 753 * Expands an existing TaskFragment to fill parent. 754 * @param wct WindowContainerTransaction in which the task fragment should be resized. 755 * @param container the {@link TaskFragmentContainer} to be expanded. 756 */ expandTaskFragment(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container)757 void expandTaskFragment(@NonNull WindowContainerTransaction wct, 758 @NonNull TaskFragmentContainer container) { 759 super.expandTaskFragment(wct, container); 760 mController.updateDivider(wct, container.getTaskContainer()); 761 } 762 shouldShowSplit(@onNull SplitContainer splitContainer)763 static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) { 764 return shouldShowSplit(splitContainer.getCurrentSplitAttributes()); 765 } 766 shouldShowSplit(@onNull SplitAttributes splitAttributes)767 static boolean shouldShowSplit(@NonNull SplitAttributes splitAttributes) { 768 return !(splitAttributes.getSplitType() instanceof ExpandContainersSplitType); 769 } 770 shouldShowPlaceholderWhenExpanded(@onNull SplitAttributes splitAttributes)771 static boolean shouldShowPlaceholderWhenExpanded(@NonNull SplitAttributes splitAttributes) { 772 // The placeholder should be kept if the expand split type is a result of user dragging 773 // the divider. 774 return SplitAttributesHelper.isDraggableExpandType(splitAttributes); 775 } 776 777 @NonNull computeSplitAttributes(@onNull TaskProperties taskProperties, @NonNull SplitRule rule, @NonNull SplitAttributes defaultSplitAttributes, @Nullable Pair<Size, Size> minDimensionsPair)778 SplitAttributes computeSplitAttributes(@NonNull TaskProperties taskProperties, 779 @NonNull SplitRule rule, @NonNull SplitAttributes defaultSplitAttributes, 780 @Nullable Pair<Size, Size> minDimensionsPair) { 781 final Configuration taskConfiguration = taskProperties.getConfiguration(); 782 final WindowMetrics taskWindowMetrics = taskProperties.getTaskMetrics(); 783 final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator = 784 mController.getSplitAttributesCalculator(); 785 final boolean areDefaultConstraintsSatisfied = rule.checkParentMetrics(taskWindowMetrics); 786 if (calculator == null) { 787 if (!areDefaultConstraintsSatisfied) { 788 return EXPAND_CONTAINERS_ATTRIBUTES; 789 } 790 return sanitizeSplitAttributes(taskProperties, defaultSplitAttributes, 791 minDimensionsPair); 792 } 793 final WindowLayoutInfo windowLayoutInfo = mWindowLayoutComponent 794 .getCurrentWindowLayoutInfo(taskProperties.getDisplayId(), 795 taskConfiguration.windowConfiguration); 796 final SplitAttributesCalculatorParams params = new SplitAttributesCalculatorParams( 797 taskWindowMetrics, taskConfiguration, windowLayoutInfo, defaultSplitAttributes, 798 areDefaultConstraintsSatisfied, rule.getTag()); 799 final SplitAttributes splitAttributes = calculator.apply(params); 800 return sanitizeSplitAttributes(taskProperties, splitAttributes, minDimensionsPair); 801 } 802 803 /** 804 * Returns {@link #EXPAND_CONTAINERS_ATTRIBUTES} if the passed {@link SplitAttributes} doesn't 805 * meet the minimum dimensions set in {@link ActivityInfo.WindowLayout}. Otherwise, returns 806 * the passed {@link SplitAttributes}. 807 */ 808 @NonNull sanitizeSplitAttributes(@onNull TaskProperties taskProperties, @NonNull SplitAttributes splitAttributes, @Nullable Pair<Size, Size> minDimensionsPair)809 private SplitAttributes sanitizeSplitAttributes(@NonNull TaskProperties taskProperties, 810 @NonNull SplitAttributes splitAttributes, 811 @Nullable Pair<Size, Size> minDimensionsPair) { 812 // Sanitize the DividerAttributes and set default values. 813 if (splitAttributes.getDividerAttributes() != null) { 814 splitAttributes = new SplitAttributes.Builder(splitAttributes) 815 .setDividerAttributes( 816 DividerPresenter.sanitizeDividerAttributes( 817 splitAttributes.getDividerAttributes()) 818 ).build(); 819 } 820 821 if (minDimensionsPair == null) { 822 return splitAttributes; 823 } 824 final FoldingFeature foldingFeature = getFoldingFeatureForHingeType( 825 taskProperties, splitAttributes); 826 final Configuration taskConfiguration = taskProperties.getConfiguration(); 827 final Rect primaryBounds = getPrimaryBounds(taskConfiguration, splitAttributes, 828 foldingFeature); 829 final Rect secondaryBounds = getSecondaryBounds(taskConfiguration, splitAttributes, 830 foldingFeature); 831 if (boundsSmallerThanMinDimensions(primaryBounds, minDimensionsPair.first) 832 || boundsSmallerThanMinDimensions(secondaryBounds, minDimensionsPair.second)) { 833 return EXPAND_CONTAINERS_ATTRIBUTES; 834 } 835 return splitAttributes; 836 } 837 838 @NonNull getActivitiesMinDimensionsPair( @onNull Activity primaryActivity, @NonNull Activity secondaryActivity)839 static Pair<Size, Size> getActivitiesMinDimensionsPair( 840 @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { 841 return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryActivity)); 842 } 843 844 @NonNull getActivityIntentMinDimensionsPair(@onNull Activity primaryActivity, @NonNull Intent secondaryIntent)845 static Pair<Size, Size> getActivityIntentMinDimensionsPair(@NonNull Activity primaryActivity, 846 @NonNull Intent secondaryIntent) { 847 return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryIntent)); 848 } 849 850 @Nullable getMinDimensions(@ullable Activity activity)851 static Size getMinDimensions(@Nullable Activity activity) { 852 if (activity == null) { 853 return null; 854 } 855 final ActivityInfo.WindowLayout windowLayout = activity.getActivityInfo().windowLayout; 856 if (windowLayout == null) { 857 return null; 858 } 859 return new Size(windowLayout.minWidth, windowLayout.minHeight); 860 } 861 862 // TODO(b/232871351): find a light-weight approach for this check. 863 @Nullable getMinDimensions(@ullable Intent intent)864 static Size getMinDimensions(@Nullable Intent intent) { 865 if (intent == null) { 866 return null; 867 } 868 final PackageManager packageManager = ActivityThread.currentActivityThread() 869 .getApplication().getPackageManager(); 870 final ResolveInfo resolveInfo = packageManager.resolveActivity(intent, 871 PackageManager.ResolveInfoFlags.of(MATCH_ALL)); 872 if (resolveInfo == null) { 873 return null; 874 } 875 final ActivityInfo activityInfo = resolveInfo.activityInfo; 876 if (activityInfo == null) { 877 return null; 878 } 879 final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout; 880 if (windowLayout == null) { 881 return null; 882 } 883 return new Size(windowLayout.minWidth, windowLayout.minHeight); 884 } 885 boundsSmallerThanMinDimensions(@onNull Rect bounds, @Nullable Size minDimensions)886 static boolean boundsSmallerThanMinDimensions(@NonNull Rect bounds, 887 @Nullable Size minDimensions) { 888 if (minDimensions == null) { 889 return false; 890 } 891 // Empty bounds mean the bounds follow the parent host task's bounds. Skip the check. 892 if (bounds.isEmpty()) { 893 return false; 894 } 895 return bounds.width() < minDimensions.getWidth() 896 || bounds.height() < minDimensions.getHeight(); 897 } 898 899 @VisibleForTesting 900 @NonNull getRelBoundsForPosition(@osition int position, @NonNull TaskProperties taskProperties, @NonNull SplitAttributes splitAttributes)901 Rect getRelBoundsForPosition(@Position int position, @NonNull TaskProperties taskProperties, 902 @NonNull SplitAttributes splitAttributes) { 903 final Configuration taskConfiguration = taskProperties.getConfiguration(); 904 final FoldingFeature foldingFeature = getFoldingFeatureForHingeType( 905 taskProperties, splitAttributes); 906 if (!shouldShowSplit(splitAttributes)) { 907 return new Rect(); 908 } 909 final Rect bounds; 910 switch (position) { 911 case POSITION_START: 912 bounds = getPrimaryBounds(taskConfiguration, splitAttributes, foldingFeature); 913 break; 914 case POSITION_END: 915 bounds = getSecondaryBounds(taskConfiguration, splitAttributes, foldingFeature); 916 break; 917 case POSITION_FILL: 918 default: 919 bounds = new Rect(); 920 } 921 // Convert to relative bounds in parent coordinate. This is to avoid flicker when the Task 922 // resized before organizer requests have been applied. 923 taskProperties.translateAbsoluteBoundsToRelativeBounds(bounds); 924 return bounds; 925 } 926 927 @NonNull getPrimaryBounds(@onNull Configuration taskConfiguration, @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature)928 private Rect getPrimaryBounds(@NonNull Configuration taskConfiguration, 929 @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { 930 final SplitAttributes computedSplitAttributes = updateSplitAttributesType(splitAttributes, 931 computeSplitType(splitAttributes, taskConfiguration, foldingFeature)); 932 if (!shouldShowSplit(computedSplitAttributes)) { 933 return new Rect(); 934 } 935 switch (computedSplitAttributes.getLayoutDirection()) { 936 case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: { 937 return getLeftContainerBounds(taskConfiguration, computedSplitAttributes, 938 foldingFeature); 939 } 940 case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: { 941 return getRightContainerBounds(taskConfiguration, computedSplitAttributes, 942 foldingFeature); 943 } 944 case SplitAttributes.LayoutDirection.LOCALE: { 945 final boolean isLtr = taskConfiguration.getLayoutDirection() 946 == View.LAYOUT_DIRECTION_LTR; 947 return isLtr 948 ? getLeftContainerBounds(taskConfiguration, computedSplitAttributes, 949 foldingFeature) 950 : getRightContainerBounds(taskConfiguration, computedSplitAttributes, 951 foldingFeature); 952 } 953 case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: { 954 return getTopContainerBounds(taskConfiguration, computedSplitAttributes, 955 foldingFeature); 956 } 957 case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: { 958 return getBottomContainerBounds(taskConfiguration, computedSplitAttributes, 959 foldingFeature); 960 } 961 default: 962 throw new IllegalArgumentException("Unknown layout direction:" 963 + computedSplitAttributes.getLayoutDirection()); 964 } 965 } 966 967 @NonNull getSecondaryBounds(@onNull Configuration taskConfiguration, @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature)968 private Rect getSecondaryBounds(@NonNull Configuration taskConfiguration, 969 @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { 970 final SplitAttributes computedSplitAttributes = updateSplitAttributesType(splitAttributes, 971 computeSplitType(splitAttributes, taskConfiguration, foldingFeature)); 972 if (!shouldShowSplit(computedSplitAttributes)) { 973 return new Rect(); 974 } 975 switch (computedSplitAttributes.getLayoutDirection()) { 976 case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: { 977 return getRightContainerBounds(taskConfiguration, computedSplitAttributes, 978 foldingFeature); 979 } 980 case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: { 981 return getLeftContainerBounds(taskConfiguration, computedSplitAttributes, 982 foldingFeature); 983 } 984 case SplitAttributes.LayoutDirection.LOCALE: { 985 final boolean isLtr = taskConfiguration.getLayoutDirection() 986 == View.LAYOUT_DIRECTION_LTR; 987 return isLtr 988 ? getRightContainerBounds(taskConfiguration, computedSplitAttributes, 989 foldingFeature) 990 : getLeftContainerBounds(taskConfiguration, computedSplitAttributes, 991 foldingFeature); 992 } 993 case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: { 994 return getBottomContainerBounds(taskConfiguration, computedSplitAttributes, 995 foldingFeature); 996 } 997 case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: { 998 return getTopContainerBounds(taskConfiguration, computedSplitAttributes, 999 foldingFeature); 1000 } 1001 default: 1002 throw new IllegalArgumentException("Unknown layout direction:" 1003 + splitAttributes.getLayoutDirection()); 1004 } 1005 } 1006 1007 /** 1008 * Returns the {@link SplitAttributes} that update the {@link SplitType} to 1009 * {@code splitTypeToUpdate}. 1010 */ updateSplitAttributesType( @onNull SplitAttributes splitAttributes, @NonNull SplitType splitTypeToUpdate)1011 private static SplitAttributes updateSplitAttributesType( 1012 @NonNull SplitAttributes splitAttributes, @NonNull SplitType splitTypeToUpdate) { 1013 return new SplitAttributes.Builder(splitAttributes) 1014 .setSplitType(splitTypeToUpdate) 1015 .build(); 1016 } 1017 1018 @NonNull getLeftContainerBounds(@onNull Configuration taskConfiguration, @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature)1019 private Rect getLeftContainerBounds(@NonNull Configuration taskConfiguration, 1020 @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { 1021 final int dividerOffset = getBoundsOffsetForDivider( 1022 splitAttributes, CONTAINER_POSITION_LEFT); 1023 final int right = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, 1024 CONTAINER_POSITION_LEFT, foldingFeature) + dividerOffset; 1025 final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds(); 1026 return new Rect(taskBounds.left, taskBounds.top, right, taskBounds.bottom); 1027 } 1028 1029 @NonNull getRightContainerBounds(@onNull Configuration taskConfiguration, @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature)1030 private Rect getRightContainerBounds(@NonNull Configuration taskConfiguration, 1031 @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { 1032 final int dividerOffset = getBoundsOffsetForDivider( 1033 splitAttributes, CONTAINER_POSITION_RIGHT); 1034 final int left = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, 1035 CONTAINER_POSITION_RIGHT, foldingFeature) + dividerOffset; 1036 final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); 1037 return new Rect(left, parentBounds.top, parentBounds.right, parentBounds.bottom); 1038 } 1039 1040 @NonNull getTopContainerBounds(@onNull Configuration taskConfiguration, @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature)1041 private Rect getTopContainerBounds(@NonNull Configuration taskConfiguration, 1042 @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { 1043 final int dividerOffset = getBoundsOffsetForDivider( 1044 splitAttributes, CONTAINER_POSITION_TOP); 1045 final int bottom = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, 1046 CONTAINER_POSITION_TOP, foldingFeature) + dividerOffset; 1047 final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); 1048 return new Rect(parentBounds.left, parentBounds.top, parentBounds.right, bottom); 1049 } 1050 1051 @NonNull getBottomContainerBounds(@onNull Configuration taskConfiguration, @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature)1052 private Rect getBottomContainerBounds(@NonNull Configuration taskConfiguration, 1053 @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { 1054 final int dividerOffset = getBoundsOffsetForDivider( 1055 splitAttributes, CONTAINER_POSITION_BOTTOM); 1056 final int top = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes, 1057 CONTAINER_POSITION_BOTTOM, foldingFeature) + dividerOffset; 1058 final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); 1059 return new Rect(parentBounds.left, top, parentBounds.right, parentBounds.bottom); 1060 } 1061 1062 /** 1063 * Computes the boundary position between the primary and the secondary containers for the given 1064 * {@link ContainerPosition} with {@link SplitAttributes}, current window and device states. 1065 * <ol> 1066 * <li>For {@link #CONTAINER_POSITION_TOP}, it computes the boundary with the bottom 1067 * container, which is {@link Rect#bottom} of the top container bounds.</li> 1068 * <li>For {@link #CONTAINER_POSITION_BOTTOM}, it computes the boundary with the top 1069 * container, which is {@link Rect#top} of the bottom container bounds.</li> 1070 * <li>For {@link #CONTAINER_POSITION_LEFT}, it computes the boundary with the right 1071 * container, which is {@link Rect#right} of the left container bounds.</li> 1072 * <li>For {@link #CONTAINER_POSITION_RIGHT}, it computes the boundary with the bottom 1073 * container, which is {@link Rect#left} of the right container bounds.</li> 1074 * </ol> 1075 * 1076 * @see #getTopContainerBounds(Configuration, SplitAttributes, FoldingFeature) 1077 * @see #getBottomContainerBounds(Configuration, SplitAttributes, FoldingFeature) 1078 * @see #getLeftContainerBounds(Configuration, SplitAttributes, FoldingFeature) 1079 * @see #getRightContainerBounds(Configuration, SplitAttributes, FoldingFeature) 1080 */ computeBoundaryBetweenContainers(@onNull Configuration taskConfiguration, @NonNull SplitAttributes splitAttributes, @ContainerPosition int position, @Nullable FoldingFeature foldingFeature)1081 private int computeBoundaryBetweenContainers(@NonNull Configuration taskConfiguration, 1082 @NonNull SplitAttributes splitAttributes, @ContainerPosition int position, 1083 @Nullable FoldingFeature foldingFeature) { 1084 final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds(); 1085 final int startPoint = shouldSplitHorizontally(splitAttributes) 1086 ? parentBounds.top 1087 : parentBounds.left; 1088 final int dimen = shouldSplitHorizontally(splitAttributes) 1089 ? parentBounds.height() 1090 : parentBounds.width(); 1091 final SplitType splitType = splitAttributes.getSplitType(); 1092 if (splitType instanceof RatioSplitType) { 1093 final RatioSplitType splitRatio = (RatioSplitType) splitType; 1094 return (int) (startPoint + dimen * splitRatio.getRatio()); 1095 } 1096 // At this point, SplitType must be a HingeSplitType and foldingFeature must be 1097 // non-null. RatioSplitType and ExpandContainerSplitType have been handled earlier. 1098 Objects.requireNonNull(foldingFeature); 1099 if (!(splitType instanceof HingeSplitType)) { 1100 throw new IllegalArgumentException("Unknown splitType:" + splitType); 1101 } 1102 final Rect hingeArea = foldingFeature.getBounds(); 1103 switch (position) { 1104 case CONTAINER_POSITION_LEFT: 1105 return hingeArea.left; 1106 case CONTAINER_POSITION_TOP: 1107 return hingeArea.top; 1108 case CONTAINER_POSITION_RIGHT: 1109 return hingeArea.right; 1110 case CONTAINER_POSITION_BOTTOM: 1111 return hingeArea.bottom; 1112 default: 1113 throw new IllegalArgumentException("Unknown position:" + position); 1114 } 1115 } 1116 1117 @Nullable getFoldingFeatureForHingeType( @onNull TaskProperties taskProperties, @NonNull SplitAttributes splitAttributes)1118 private FoldingFeature getFoldingFeatureForHingeType( 1119 @NonNull TaskProperties taskProperties, 1120 @NonNull SplitAttributes splitAttributes) { 1121 SplitType splitType = splitAttributes.getSplitType(); 1122 if (!(splitType instanceof HingeSplitType)) { 1123 return null; 1124 } 1125 return getFoldingFeature(taskProperties); 1126 } 1127 1128 @Nullable 1129 @VisibleForTesting getFoldingFeature(@onNull TaskProperties taskProperties)1130 FoldingFeature getFoldingFeature(@NonNull TaskProperties taskProperties) { 1131 final int displayId = taskProperties.getDisplayId(); 1132 final WindowConfiguration windowConfiguration = taskProperties.getConfiguration() 1133 .windowConfiguration; 1134 final WindowLayoutInfo info = mWindowLayoutComponent 1135 .getCurrentWindowLayoutInfo(displayId, windowConfiguration); 1136 final List<DisplayFeature> displayFeatures = info.getDisplayFeatures(); 1137 if (displayFeatures.isEmpty()) { 1138 return null; 1139 } 1140 final List<FoldingFeature> foldingFeatures = new ArrayList<>(); 1141 for (DisplayFeature displayFeature : displayFeatures) { 1142 if (displayFeature instanceof FoldingFeature) { 1143 foldingFeatures.add((FoldingFeature) displayFeature); 1144 } 1145 } 1146 // TODO(b/240219484): Support device with multiple hinges. 1147 if (foldingFeatures.size() != 1) { 1148 return null; 1149 } 1150 return foldingFeatures.get(0); 1151 } 1152 1153 /** 1154 * Indicates that this {@link SplitAttributes} splits the task horizontally. Returns 1155 * {@code false} if this {@link SplitAttributes} splits the task vertically. 1156 */ shouldSplitHorizontally(SplitAttributes splitAttributes)1157 private static boolean shouldSplitHorizontally(SplitAttributes splitAttributes) { 1158 switch (splitAttributes.getLayoutDirection()) { 1159 case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: 1160 case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: 1161 return true; 1162 default: 1163 return false; 1164 } 1165 } 1166 1167 /** 1168 * Computes the {@link SplitType} with the {@link SplitAttributes} and the current device and 1169 * window state. 1170 * If passed {@link SplitAttributes#getSplitType} is a {@link RatioSplitType}. It reversed 1171 * the ratio if the computed {@link SplitAttributes#getLayoutDirection} is 1172 * {@link SplitAttributes.LayoutDirection.LEFT_TO_RIGHT} or 1173 * {@link SplitAttributes.LayoutDirection.BOTTOM_TO_TOP} to make the bounds calculation easier. 1174 * If passed {@link SplitAttributes#getSplitType} is a {@link HingeSplitType}, it checks 1175 * the current device and window states to determine whether the split container should split 1176 * by hinge or use {@link HingeSplitType#getFallbackSplitType}. 1177 */ computeSplitType(@onNull SplitAttributes splitAttributes, @NonNull Configuration taskConfiguration, @Nullable FoldingFeature foldingFeature)1178 private SplitType computeSplitType(@NonNull SplitAttributes splitAttributes, 1179 @NonNull Configuration taskConfiguration, @Nullable FoldingFeature foldingFeature) { 1180 final SplitType splitType = splitAttributes.getSplitType(); 1181 if (splitType instanceof ExpandContainersSplitType) { 1182 return splitType; 1183 } else if (splitType instanceof RatioSplitType) { 1184 final RatioSplitType splitRatio = (RatioSplitType) splitType; 1185 // Reverse the ratio for RIGHT_TO_LEFT and BOTTOM_TO_TOP to make the boundary 1186 // computation have the same direction, which is from (top, left) to (bottom, right). 1187 final SplitType reversedSplitType = new RatioSplitType(1 - splitRatio.getRatio()); 1188 return isReversedLayout(splitAttributes, taskConfiguration) 1189 ? reversedSplitType 1190 : splitType; 1191 } else if (splitType instanceof HingeSplitType) { 1192 final HingeSplitType hinge = (HingeSplitType) splitType; 1193 @WindowingMode 1194 final int windowingMode = taskConfiguration.windowConfiguration.getWindowingMode(); 1195 return shouldSplitByHinge(splitAttributes, foldingFeature, windowingMode) 1196 ? hinge : hinge.getFallbackSplitType(); 1197 } 1198 throw new IllegalArgumentException("Unknown SplitType:" + splitType); 1199 } 1200 shouldSplitByHinge(@onNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature, @WindowingMode int taskWindowingMode)1201 private static boolean shouldSplitByHinge(@NonNull SplitAttributes splitAttributes, 1202 @Nullable FoldingFeature foldingFeature, @WindowingMode int taskWindowingMode) { 1203 // Only HingeSplitType may split the task bounds by hinge. 1204 if (!(splitAttributes.getSplitType() instanceof HingeSplitType)) { 1205 return false; 1206 } 1207 // Device is not foldable, so there's no hinge to match. 1208 if (foldingFeature == null) { 1209 return false; 1210 } 1211 // The task is in multi-window mode. Match hinge doesn't make sense because current task 1212 // bounds may not fit display bounds. 1213 if (WindowConfiguration.inMultiWindowMode(taskWindowingMode)) { 1214 return false; 1215 } 1216 // Return true if how the split attributes split the task bounds matches the orientation of 1217 // folding area orientation. 1218 return shouldSplitHorizontally(splitAttributes) == isFoldingAreaHorizontal(foldingFeature); 1219 } 1220 isFoldingAreaHorizontal(@onNull FoldingFeature foldingFeature)1221 private static boolean isFoldingAreaHorizontal(@NonNull FoldingFeature foldingFeature) { 1222 final Rect bounds = foldingFeature.getBounds(); 1223 return bounds.width() > bounds.height(); 1224 } 1225 1226 @NonNull getTaskProperties(@onNull Activity activity)1227 TaskProperties getTaskProperties(@NonNull Activity activity) { 1228 final TaskContainer taskContainer = mController.getTaskContainer( 1229 mController.getTaskId(activity)); 1230 if (taskContainer != null) { 1231 return taskContainer.getTaskProperties(); 1232 } 1233 return TaskProperties.getTaskPropertiesFromActivity(activity); 1234 } 1235 1236 @NonNull getTaskWindowMetrics(@onNull Activity activity)1237 WindowMetrics getTaskWindowMetrics(@NonNull Activity activity) { 1238 return getTaskProperties(activity).getTaskMetrics(); 1239 } 1240 1241 @NonNull createParentContainerInfoFromTaskProperties( @onNull TaskProperties taskProperties)1242 ParentContainerInfo createParentContainerInfoFromTaskProperties( 1243 @NonNull TaskProperties taskProperties) { 1244 final Configuration configuration = taskProperties.getConfiguration(); 1245 final WindowLayoutInfo windowLayoutInfo = mWindowLayoutComponent 1246 .getCurrentWindowLayoutInfo(taskProperties.getDisplayId(), 1247 configuration.windowConfiguration); 1248 return new ParentContainerInfo(taskProperties.getTaskMetrics(), configuration, 1249 windowLayoutInfo); 1250 } 1251 } 1252