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