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