1 /* 2 * Copyright (C) 2022 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.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; 20 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; 21 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; 22 import static android.app.WindowConfiguration.inMultiWindowMode; 23 24 import static androidx.window.extensions.embedding.DividerPresenter.RATIO_EXPANDED_PRIMARY; 25 import static androidx.window.extensions.embedding.DividerPresenter.RATIO_EXPANDED_SECONDARY; 26 27 import android.app.Activity; 28 import android.app.ActivityClient; 29 import android.app.WindowConfiguration; 30 import android.app.WindowConfiguration.WindowingMode; 31 import android.content.res.Configuration; 32 import android.graphics.Rect; 33 import android.os.IBinder; 34 import android.util.ArrayMap; 35 import android.util.ArraySet; 36 import android.util.DisplayMetrics; 37 import android.util.Log; 38 import android.view.WindowInsets; 39 import android.view.WindowMetrics; 40 import android.window.TaskFragmentInfo; 41 import android.window.TaskFragmentParentInfo; 42 import android.window.WindowContainerTransaction; 43 44 import androidx.annotation.NonNull; 45 import androidx.annotation.Nullable; 46 import androidx.window.extensions.core.util.function.Predicate; 47 import androidx.window.extensions.embedding.SplitAttributes.SplitType; 48 import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType; 49 import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType; 50 51 import java.util.ArrayList; 52 import java.util.List; 53 import java.util.Set; 54 55 /** Represents TaskFragments and split pairs below a Task. */ 56 class TaskContainer { 57 private static final String TAG = TaskContainer.class.getSimpleName(); 58 59 /** Parcelable data of this TaskContainer. */ 60 @NonNull 61 private final ParcelableTaskContainerData mParcelableTaskContainerData; 62 63 /** Active TaskFragments in this Task. */ 64 @NonNull 65 private final List<TaskFragmentContainer> mContainers = new ArrayList<>(); 66 67 /** Active split pairs in this Task. */ 68 @NonNull 69 private final List<SplitContainer> mSplitContainers = new ArrayList<>(); 70 71 /** Active pin split pair in this Task. */ 72 @Nullable 73 private SplitPinContainer mSplitPinContainer; 74 75 /** 76 * The {@link TaskFragmentContainer#isAlwaysOnTopOverlay()} in the Task. 77 */ 78 @Nullable 79 private TaskFragmentContainer mAlwaysOnTopOverlayContainer; 80 81 @NonNull 82 private TaskFragmentParentInfo mInfo; 83 84 @NonNull 85 private SplitController mSplitController; 86 87 /** 88 * TaskFragments that the organizer has requested to be closed. They should be removed when 89 * the organizer receives 90 * {@link SplitController#onTaskFragmentVanished(WindowContainerTransaction, TaskFragmentInfo)} 91 * event for them. 92 */ 93 final Set<IBinder> mFinishedContainer = new ArraySet<>(); 94 95 /** 96 * The {@link RatioSplitType} that will be applied to newly added containers. This is to ensure 97 * the required UX that, after user dragging the divider, the split ratio is persistent after 98 * launching a new activity into a new TaskFragment in the same Task. 99 */ 100 private RatioSplitType mOverrideSplitType; 101 102 /** 103 * If {@code true}, suppress the placeholder rules in the {@link TaskContainer}. 104 * <p> 105 * This is used in case the user drags the divider to fully expand the primary container and 106 * dismiss the secondary container while a {@link SplitPlaceholderRule} is used. Without this 107 * flag, after dismissing the secondary container, a placeholder will be launched again. 108 * <p> 109 * This flag is set true when the primary container is fully expanded and cleared when a new 110 * split is added to the {@link TaskContainer}. 111 */ 112 private boolean mPlaceholderRuleSuppressed; 113 114 /** 115 * {@code true} if the TaskFragments in this Task needs to be updated next time the Task 116 * becomes visible. See {@link #shouldUpdateContainer(TaskFragmentParentInfo)} 117 */ 118 boolean mTaskFragmentContainersNeedsUpdate; 119 120 /** 121 * The {@link TaskContainer} constructor 122 * 123 * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with 124 * {@code activityInTask}. 125 * @param activityInTask The {@link Activity} in the Task with {@code taskId}. It is used to 126 * initialize the {@link TaskContainer} properties. 127 * @param splitController The {@link SplitController}. 128 */ TaskContainer(int taskId, @NonNull Activity activityInTask, @NonNull SplitController splitController)129 TaskContainer(int taskId, @NonNull Activity activityInTask, 130 @NonNull SplitController splitController) { 131 mParcelableTaskContainerData = new ParcelableTaskContainerData(taskId, this); 132 133 final TaskProperties taskProperties = TaskProperties 134 .getTaskPropertiesFromActivity(activityInTask); 135 mInfo = new TaskFragmentParentInfo( 136 taskProperties.getConfiguration(), 137 taskProperties.getDisplayId(), 138 taskId, 139 // Note that it is always called when there's a new Activity is started, which 140 // implies the host task is visible and has an activity in the task. 141 true /* visible */, 142 true /* hasDirectActivity */, 143 null /* decorSurface */); 144 mSplitController = splitController; 145 } 146 147 /** This is only used when restoring it from a {@link ParcelableTaskContainerData}. */ TaskContainer(@onNull ParcelableTaskContainerData data, @NonNull SplitController splitController, @NonNull ArrayMap<IBinder, TaskFragmentInfo> taskFragmentInfoMap)148 TaskContainer(@NonNull ParcelableTaskContainerData data, 149 @NonNull SplitController splitController, 150 @NonNull ArrayMap<IBinder, TaskFragmentInfo> taskFragmentInfoMap) { 151 mParcelableTaskContainerData = new ParcelableTaskContainerData(data, this); 152 mInfo = new TaskFragmentParentInfo(new Configuration(), 0 /* displayId */, -1 /* taskId */, 153 false /* visible */, false /* hasDirectActivity */, null /* decorSurface */); 154 mSplitController = splitController; 155 for (ParcelableTaskFragmentContainerData tfData : 156 data.getParcelableTaskFragmentContainerDataList()) { 157 final TaskFragmentInfo info = taskFragmentInfoMap.remove(tfData.mToken); 158 if (info != null && !info.isEmpty()) { 159 final TaskFragmentContainer container = 160 new TaskFragmentContainer(tfData, splitController, this); 161 container.setInfo(new WindowContainerTransaction(), info); 162 mContainers.add(container); 163 } else { 164 Log.d(TAG, "Drop " + tfData + " while restoring Task " + data.mTaskId); 165 } 166 } 167 } 168 169 @NonNull getParcelableData()170 ParcelableTaskContainerData getParcelableData() { 171 return mParcelableTaskContainerData; 172 } 173 174 @NonNull getParcelableTaskFragmentContainerDataList()175 List<ParcelableTaskFragmentContainerData> getParcelableTaskFragmentContainerDataList() { 176 final List<ParcelableTaskFragmentContainerData> data = new ArrayList<>(mContainers.size()); 177 for (TaskFragmentContainer container : mContainers) { 178 data.add(container.getParcelableData()); 179 } 180 return data; 181 } 182 183 @NonNull getParcelableSplitContainerDataList()184 List<ParcelableSplitContainerData> getParcelableSplitContainerDataList() { 185 final int size = 186 mSplitPinContainer != null ? mSplitContainers.size() - 1 : mSplitContainers.size(); 187 final List<ParcelableSplitContainerData> data = new ArrayList<>(size); 188 for (SplitContainer splitContainer : mSplitContainers) { 189 if (splitContainer == mSplitPinContainer) { 190 // Skip SplitPinContainer as it cannot be restored because the SplitPinRule is 191 // set while pinning the container in runtime. 192 continue; 193 } 194 data.add(splitContainer.getParcelableData()); 195 } 196 return data; 197 } 198 getTaskId()199 int getTaskId() { 200 return mParcelableTaskContainerData.mTaskId; 201 } 202 getDisplayId()203 int getDisplayId() { 204 return mInfo.getDisplayId(); 205 } 206 isVisible()207 boolean isVisible() { 208 return mInfo.isVisible(); 209 } 210 setInvisible()211 void setInvisible() { 212 mInfo = new TaskFragmentParentInfo(mInfo.getConfiguration(), mInfo.getDisplayId(), 213 mInfo.getTaskId(), false /* visible */, mInfo.hasDirectActivity(), 214 mInfo.getDecorSurface()); 215 } 216 hasDirectActivity()217 boolean hasDirectActivity() { 218 return mInfo.hasDirectActivity(); 219 } 220 221 @NonNull getBounds()222 Rect getBounds() { 223 return mInfo.getConfiguration().windowConfiguration.getBounds(); 224 } 225 226 @NonNull getTaskProperties()227 TaskProperties getTaskProperties() { 228 return new TaskProperties(mInfo.getDisplayId(), mInfo.getConfiguration()); 229 } 230 updateTaskFragmentParentInfo(@onNull TaskFragmentParentInfo info)231 void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) { 232 mInfo = info; 233 } 234 235 @NonNull getTaskFragmentParentInfo()236 TaskFragmentParentInfo getTaskFragmentParentInfo() { 237 return mInfo; 238 } 239 240 /** 241 * Returns {@code true} if the container should be updated with {@code info}. 242 */ shouldUpdateContainer(@onNull TaskFragmentParentInfo info)243 boolean shouldUpdateContainer(@NonNull TaskFragmentParentInfo info) { 244 final Configuration configuration = info.getConfiguration(); 245 246 if (isInPictureInPicture(configuration)) { 247 // No need to update presentation in PIP until the Task exit PIP. 248 return false; 249 } 250 251 // If the task properties equals regardless of starting position, don't 252 // need to update the container. 253 return mTaskFragmentContainersNeedsUpdate 254 || mInfo.getConfiguration().diffPublicOnly(configuration) != 0 255 || mInfo.getDisplayId() != info.getDisplayId(); 256 } 257 258 /** 259 * Returns the windowing mode for the TaskFragments below this Task, which should be split with 260 * other TaskFragments. 261 * 262 * @param taskFragmentBounds Requested bounds for the TaskFragment. It will be empty when 263 * the pair of TaskFragments are stacked due to the limited space. 264 */ 265 @WindowingMode getWindowingModeForTaskFragment(@ullable Rect taskFragmentBounds)266 int getWindowingModeForTaskFragment(@Nullable Rect taskFragmentBounds) { 267 // Only set to multi-windowing mode if the pair are showing side-by-side. Otherwise, it 268 // will be set to UNDEFINED which will then inherit the Task windowing mode. 269 if (taskFragmentBounds == null || taskFragmentBounds.isEmpty() || isInPictureInPicture()) { 270 return WINDOWING_MODE_UNDEFINED; 271 } 272 // We use WINDOWING_MODE_MULTI_WINDOW when the Task is fullscreen. 273 // However, when the Task is in other multi windowing mode, such as Freeform, we need to 274 // have the activity windowing mode to match the Task, otherwise things like 275 // DecorCaptionView won't work correctly. As a result, have the TaskFragment to be in the 276 // Task windowing mode if the Task is in multi window. 277 // TODO we won't need this anymore after we migrate Freeform caption to WM Shell. 278 return isInMultiWindow() ? getWindowingMode() : WINDOWING_MODE_MULTI_WINDOW; 279 } 280 isInPictureInPicture()281 boolean isInPictureInPicture() { 282 return isInPictureInPicture(mInfo.getConfiguration()); 283 } 284 isInPictureInPicture(@onNull Configuration configuration)285 private static boolean isInPictureInPicture(@NonNull Configuration configuration) { 286 return configuration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_PINNED; 287 } 288 isInMultiWindow()289 boolean isInMultiWindow() { 290 return WindowConfiguration.inMultiWindowMode(getWindowingMode()); 291 } 292 293 @WindowingMode getWindowingMode()294 private int getWindowingMode() { 295 return mInfo.getConfiguration().windowConfiguration.getWindowingMode(); 296 } 297 298 /** Whether there is any {@link TaskFragmentContainer} below this Task. */ isEmpty()299 boolean isEmpty() { 300 return mContainers.isEmpty() && mFinishedContainer.isEmpty(); 301 } 302 303 /** Called when the activity {@link Activity#isFinishing()} and paused. */ onFinishingActivityPaused(@onNull WindowContainerTransaction wct, @NonNull IBinder activityToken)304 void onFinishingActivityPaused(@NonNull WindowContainerTransaction wct, 305 @NonNull IBinder activityToken) { 306 for (TaskFragmentContainer container : mContainers) { 307 container.onFinishingActivityPaused(wct, activityToken); 308 } 309 } 310 311 /** Called when the activity is destroyed. */ onActivityDestroyed(@onNull WindowContainerTransaction wct, @NonNull IBinder activityToken)312 void onActivityDestroyed(@NonNull WindowContainerTransaction wct, 313 @NonNull IBinder activityToken) { 314 for (TaskFragmentContainer container : mContainers) { 315 container.onActivityDestroyed(wct, activityToken); 316 } 317 } 318 319 /** Removes the pending appeared activity from all TaskFragments in this Task. */ cleanupPendingAppearedActivity(@onNull IBinder activityToken)320 void cleanupPendingAppearedActivity(@NonNull IBinder activityToken) { 321 for (TaskFragmentContainer container : mContainers) { 322 container.removePendingAppearedActivity(activityToken); 323 } 324 } 325 326 @Nullable getTopNonFinishingTaskFragmentContainer()327 TaskFragmentContainer getTopNonFinishingTaskFragmentContainer() { 328 return getTopNonFinishingTaskFragmentContainer(true /* includePin */); 329 } 330 331 @Nullable getTopNonFinishingTaskFragmentContainer(boolean includePin)332 TaskFragmentContainer getTopNonFinishingTaskFragmentContainer(boolean includePin) { 333 return getTopNonFinishingTaskFragmentContainer(includePin, false /* includeOverlay */); 334 } 335 336 @Nullable getTopNonFinishingTaskFragmentContainer(boolean includePin, boolean includeOverlay)337 TaskFragmentContainer getTopNonFinishingTaskFragmentContainer(boolean includePin, 338 boolean includeOverlay) { 339 for (int i = mContainers.size() - 1; i >= 0; i--) { 340 final TaskFragmentContainer container = mContainers.get(i); 341 if (!includePin && isTaskFragmentContainerPinned(container)) { 342 continue; 343 } 344 if (!includeOverlay && container.isOverlay()) { 345 continue; 346 } 347 if (!container.isFinished()) { 348 return container; 349 } 350 } 351 return null; 352 } 353 354 /** Gets a non-finishing container below the given one. */ 355 @Nullable getNonFinishingTaskFragmentContainerBelow( @onNull TaskFragmentContainer current)356 TaskFragmentContainer getNonFinishingTaskFragmentContainerBelow( 357 @NonNull TaskFragmentContainer current) { 358 final int index = mContainers.indexOf(current); 359 for (int i = index - 1; i >= 0; i--) { 360 final TaskFragmentContainer container = mContainers.get(i); 361 if (!container.isFinished()) { 362 return container; 363 } 364 } 365 return null; 366 } 367 368 @Nullable getTopNonFinishingActivity(boolean includeOverlay)369 Activity getTopNonFinishingActivity(boolean includeOverlay) { 370 for (int i = mContainers.size() - 1; i >= 0; i--) { 371 final TaskFragmentContainer container = mContainers.get(i); 372 if (!includeOverlay && container.isOverlay()) { 373 continue; 374 } 375 final Activity activity = container.getTopNonFinishingActivity(); 376 if (activity != null) { 377 return activity; 378 } 379 } 380 return null; 381 } 382 383 @Nullable getContainerWithActivity(@onNull IBinder activityToken)384 TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { 385 // When the new activity is launched to the topmost TF because the source activity 386 // was in that TF, and the source activity is finished before resolving the new activity, 387 // we will try to see if the new activity match a rule with the split activities below. 388 // If matched, it can be reparented. 389 final TaskFragmentContainer taskFragmentContainer 390 = getContainer(container -> container.hasPendingAppearedActivity(activityToken)); 391 if (taskFragmentContainer != null) { 392 return taskFragmentContainer; 393 } 394 return getContainer(container -> container.hasAppearedActivity(activityToken)); 395 } 396 397 @Nullable getContainer(@onNull Predicate<TaskFragmentContainer> predicate)398 TaskFragmentContainer getContainer(@NonNull Predicate<TaskFragmentContainer> predicate) { 399 for (int i = mContainers.size() - 1; i >= 0; i--) { 400 final TaskFragmentContainer container = mContainers.get(i); 401 if (predicate.test(container)) { 402 return container; 403 } 404 } 405 return null; 406 } 407 408 @Nullable getActiveSplitForContainer(@ullable TaskFragmentContainer container)409 SplitContainer getActiveSplitForContainer(@Nullable TaskFragmentContainer container) { 410 if (container == null) { 411 return null; 412 } 413 for (int i = mSplitContainers.size() - 1; i >= 0; i--) { 414 final SplitContainer splitContainer = mSplitContainers.get(i); 415 if (container.equals(splitContainer.getSecondaryContainer()) 416 || container.equals(splitContainer.getPrimaryContainer())) { 417 return splitContainer; 418 } 419 } 420 return null; 421 } 422 423 /** 424 * Returns the always-on-top overlay container in the task, or {@code null} if it doesn't exist. 425 */ 426 @Nullable getAlwaysOnTopOverlayContainer()427 TaskFragmentContainer getAlwaysOnTopOverlayContainer() { 428 return mAlwaysOnTopOverlayContainer; 429 } 430 indexOf(@onNull TaskFragmentContainer child)431 int indexOf(@NonNull TaskFragmentContainer child) { 432 return mContainers.indexOf(child); 433 } 434 435 /** Whether the Task is in an intermediate state waiting for the server update. */ isInIntermediateState()436 boolean isInIntermediateState() { 437 for (TaskFragmentContainer container : mContainers) { 438 if (container.isInIntermediateState()) { 439 // We are in an intermediate state to wait for server update on this TaskFragment. 440 return true; 441 } 442 } 443 return false; 444 } 445 446 /** 447 * Returns a list of {@link SplitContainer}. Do not modify the containers directly on the 448 * returned list. Use {@link #addSplitContainer} or {@link #removeSplitContainers} instead. 449 */ 450 @NonNull getSplitContainers()451 List<SplitContainer> getSplitContainers() { 452 return mSplitContainers; 453 } 454 addSplitContainer(@onNull SplitContainer splitContainer)455 void addSplitContainer(@NonNull SplitContainer splitContainer) { 456 // Reset the placeholder rule suppression when a new split container is added. 457 mPlaceholderRuleSuppressed = false; 458 459 applyOverrideSplitTypeIfNeeded(splitContainer); 460 461 if (splitContainer instanceof SplitPinContainer) { 462 mSplitPinContainer = (SplitPinContainer) splitContainer; 463 mSplitContainers.add(splitContainer); 464 return; 465 } 466 467 // Keeps the SplitPinContainer on the top of the list. 468 mSplitContainers.remove(mSplitPinContainer); 469 mSplitContainers.add(splitContainer); 470 if (mSplitPinContainer != null) { 471 mSplitContainers.add(mSplitPinContainer); 472 } 473 } 474 isPlaceholderRuleSuppressed()475 boolean isPlaceholderRuleSuppressed() { 476 return mPlaceholderRuleSuppressed; 477 } 478 479 // If there is an override SplitType due to user dragging the divider, the split ratio should 480 // be applied to newly added SplitContainers. applyOverrideSplitTypeIfNeeded(@onNull SplitContainer splitContainer)481 private void applyOverrideSplitTypeIfNeeded(@NonNull SplitContainer splitContainer) { 482 if (mOverrideSplitType == null) { 483 return; 484 } 485 final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes(); 486 final DividerAttributes dividerAttributes = splitAttributes.getDividerAttributes(); 487 if (!(splitAttributes.getSplitType() instanceof RatioSplitType)) { 488 // Skip if the original split type is not a ratio type. 489 return; 490 } 491 if (dividerAttributes == null 492 || dividerAttributes.getDividerType() != DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { 493 // Skip if the split does not have a draggable divider. 494 return; 495 } 496 updateDefaultSplitAttributes(splitContainer, mOverrideSplitType); 497 } 498 updateDefaultSplitAttributes( @onNull SplitContainer splitContainer, @NonNull SplitType overrideSplitType)499 private static void updateDefaultSplitAttributes( 500 @NonNull SplitContainer splitContainer, @NonNull SplitType overrideSplitType) { 501 splitContainer.updateDefaultSplitAttributes( 502 new SplitAttributes.Builder(splitContainer.getDefaultSplitAttributes()) 503 .setSplitType(overrideSplitType) 504 .build() 505 ); 506 } 507 removeSplitContainers(@onNull List<SplitContainer> containers)508 void removeSplitContainers(@NonNull List<SplitContainer> containers) { 509 mSplitContainers.removeAll(containers); 510 } 511 removeSplitPinContainer()512 void removeSplitPinContainer() { 513 if (mSplitPinContainer == null) { 514 return; 515 } 516 517 final TaskFragmentContainer primaryContainer = mSplitPinContainer.getPrimaryContainer(); 518 final TaskFragmentContainer secondaryContainer = mSplitPinContainer.getSecondaryContainer(); 519 mSplitContainers.remove(mSplitPinContainer); 520 mSplitPinContainer = null; 521 522 // Remove the other SplitContainers that contains the unpinned container (unless it 523 // is the current top-most split-pair), since the state are no longer valid. 524 final List<SplitContainer> splitsToRemove = new ArrayList<>(); 525 for (SplitContainer splitContainer : mSplitContainers) { 526 if (splitContainer.getSecondaryContainer().equals(secondaryContainer) 527 && !splitContainer.getPrimaryContainer().equals(primaryContainer)) { 528 splitsToRemove.add(splitContainer); 529 } 530 } 531 removeSplitContainers(splitsToRemove); 532 } 533 534 @Nullable getSplitPinContainer()535 SplitPinContainer getSplitPinContainer() { 536 return mSplitPinContainer; 537 } 538 isTaskFragmentContainerPinned(@onNull TaskFragmentContainer taskFragmentContainer)539 boolean isTaskFragmentContainerPinned(@NonNull TaskFragmentContainer taskFragmentContainer) { 540 return mSplitPinContainer != null 541 && mSplitPinContainer.getSecondaryContainer() == taskFragmentContainer; 542 } 543 addTaskFragmentContainer(@onNull TaskFragmentContainer taskFragmentContainer)544 void addTaskFragmentContainer(@NonNull TaskFragmentContainer taskFragmentContainer) { 545 mContainers.add(taskFragmentContainer); 546 onTaskFragmentContainerUpdated(); 547 } 548 addTaskFragmentContainer(int index, @NonNull TaskFragmentContainer taskFragmentContainer)549 void addTaskFragmentContainer(int index, @NonNull TaskFragmentContainer taskFragmentContainer) { 550 mContainers.add(index, taskFragmentContainer); 551 onTaskFragmentContainerUpdated(); 552 } 553 removeTaskFragmentContainer(@onNull TaskFragmentContainer taskFragmentContainer)554 void removeTaskFragmentContainer(@NonNull TaskFragmentContainer taskFragmentContainer) { 555 mContainers.remove(taskFragmentContainer); 556 onTaskFragmentContainerUpdated(); 557 } 558 removeTaskFragmentContainers(@onNull List<TaskFragmentContainer> taskFragmentContainers)559 void removeTaskFragmentContainers(@NonNull List<TaskFragmentContainer> taskFragmentContainers) { 560 mContainers.removeAll(taskFragmentContainers); 561 onTaskFragmentContainerUpdated(); 562 } 563 clearTaskFragmentContainer()564 void clearTaskFragmentContainer() { 565 mContainers.clear(); 566 onTaskFragmentContainerUpdated(); 567 } 568 569 /** 570 * Returns a list of {@link TaskFragmentContainer}. Do not modify the containers directly on 571 * the returned list. Use {@link #addTaskFragmentContainer}, 572 * {@link #removeTaskFragmentContainer} or other related methods instead. 573 */ 574 @NonNull getTaskFragmentContainers()575 List<TaskFragmentContainer> getTaskFragmentContainers() { 576 return mContainers; 577 } 578 updateTopSplitContainerForDivider( @onNull DividerPresenter dividerPresenter, @NonNull List<TaskFragmentContainer> outContainersToFinish)579 void updateTopSplitContainerForDivider( 580 @NonNull DividerPresenter dividerPresenter, 581 @NonNull List<TaskFragmentContainer> outContainersToFinish) { 582 final SplitContainer topSplitContainer = getTopNonFinishingSplitContainer(); 583 if (topSplitContainer == null) { 584 return; 585 } 586 final TaskFragmentContainer primaryContainer = topSplitContainer.getPrimaryContainer(); 587 final float newRatio = dividerPresenter.calculateNewSplitRatio(); 588 589 // If the primary container is fully expanded, we should finish all the associated 590 // secondary containers. 591 if (newRatio == RATIO_EXPANDED_PRIMARY) { 592 for (final SplitContainer splitContainer : mSplitContainers) { 593 if (primaryContainer == splitContainer.getPrimaryContainer()) { 594 outContainersToFinish.add(splitContainer.getSecondaryContainer()); 595 } 596 } 597 598 // Temporarily suppress the placeholder rule in the TaskContainer. This will be restored 599 // if a new split is added into the TaskContainer. 600 mPlaceholderRuleSuppressed = true; 601 602 mOverrideSplitType = null; 603 return; 604 } 605 606 final SplitType newSplitType; 607 if (newRatio == RATIO_EXPANDED_SECONDARY) { 608 newSplitType = new ExpandContainersSplitType(); 609 // We do not want to apply ExpandContainersSplitType to new split containers. 610 mOverrideSplitType = null; 611 } else { 612 // We save the override RatioSplitType and apply to new split containers. 613 newSplitType = mOverrideSplitType = new RatioSplitType(newRatio); 614 } 615 for (final SplitContainer splitContainer : mSplitContainers) { 616 if (primaryContainer == splitContainer.getPrimaryContainer()) { 617 updateDefaultSplitAttributes(splitContainer, newSplitType); 618 } 619 } 620 } 621 622 @Nullable getTopNonFinishingSplitContainer()623 SplitContainer getTopNonFinishingSplitContainer() { 624 for (int i = mSplitContainers.size() - 1; i >= 0; i--) { 625 final SplitContainer splitContainer = mSplitContainers.get(i); 626 if (!splitContainer.getPrimaryContainer().isFinished() 627 && !splitContainer.getSecondaryContainer().isFinished()) { 628 return splitContainer; 629 } 630 } 631 return null; 632 } 633 onTaskFragmentContainerUpdated()634 private void onTaskFragmentContainerUpdated() { 635 // TODO(b/300211704): Find a better mechanism to handle the z-order in case we introduce 636 // another special container that should also be on top in the future. 637 updateSplitPinContainerIfNecessary(); 638 // Update overlay container after split pin container since the overlay should be on top of 639 // pin container. 640 updateAlwaysOnTopOverlayIfNecessary(); 641 642 mSplitController.scheduleBackup(); 643 } 644 updateAlwaysOnTopOverlayIfNecessary()645 private void updateAlwaysOnTopOverlayIfNecessary() { 646 final List<TaskFragmentContainer> alwaysOnTopOverlays = mContainers 647 .stream().filter(TaskFragmentContainer::isAlwaysOnTopOverlay).toList(); 648 if (alwaysOnTopOverlays.size() > 1) { 649 throw new IllegalStateException("There must be at most one always-on-top overlay " 650 + "container per Task"); 651 } 652 mAlwaysOnTopOverlayContainer = alwaysOnTopOverlays.isEmpty() 653 ? null : alwaysOnTopOverlays.getFirst(); 654 if (mAlwaysOnTopOverlayContainer != null) { 655 moveContainerToLastIfNecessary(mAlwaysOnTopOverlayContainer); 656 } 657 } 658 updateSplitPinContainerIfNecessary()659 private void updateSplitPinContainerIfNecessary() { 660 if (mSplitPinContainer == null) { 661 return; 662 } 663 664 final TaskFragmentContainer pinnedContainer = mSplitPinContainer.getSecondaryContainer(); 665 final int pinnedContainerIndex = mContainers.indexOf(pinnedContainer); 666 if (pinnedContainerIndex <= 0) { 667 removeSplitPinContainer(); 668 return; 669 } 670 671 // Ensure the pinned container is top-most. 672 moveContainerToLastIfNecessary(pinnedContainer); 673 674 // Update the primary container adjacent to the pinned container if needed. 675 final TaskFragmentContainer adjacentContainer = 676 getNonFinishingTaskFragmentContainerBelow(pinnedContainer); 677 if (adjacentContainer == null) { 678 removeSplitPinContainer(); 679 } else if (mSplitPinContainer.getPrimaryContainer() != adjacentContainer) { 680 mSplitPinContainer.setPrimaryContainer(adjacentContainer); 681 } 682 } 683 684 /** Moves the {@code container} to the last to align taskFragments' z-order. */ moveContainerToLastIfNecessary(@onNull TaskFragmentContainer container)685 private void moveContainerToLastIfNecessary(@NonNull TaskFragmentContainer container) { 686 final int index = mContainers.indexOf(container); 687 if (index < 0) { 688 Log.w(TAG, "The container:" + container + " is not in the container list!"); 689 return; 690 } 691 if (index != mContainers.size() - 1) { 692 mContainers.remove(container); 693 mContainers.add(container); 694 } 695 } 696 697 /** 698 * Gets the descriptors of split states in this Task. 699 * 700 * @return a list of {@code SplitInfo} if all the SplitContainers are stable, or {@code null} if 701 * any SplitContainer is in an intermediate state. 702 */ 703 @Nullable getSplitStatesIfStable()704 List<SplitInfo> getSplitStatesIfStable() { 705 final List<SplitInfo> splitStates = new ArrayList<>(); 706 for (SplitContainer container : mSplitContainers) { 707 final SplitInfo splitInfo = container.toSplitInfoIfStable(); 708 if (splitInfo == null) { 709 return null; 710 } 711 splitStates.add(splitInfo); 712 } 713 return splitStates; 714 } 715 716 // TODO(b/317358445): Makes ActivityStack and SplitInfo callback more stable. 717 /** 718 * Returns a list of currently active {@link ActivityStack activityStacks}. 719 * 720 * @return a list of {@link ActivityStack activityStacks} if all the containers are in 721 * a stable state, or {@code null} otherwise. 722 */ 723 @Nullable getActivityStacksIfStable()724 List<ActivityStack> getActivityStacksIfStable() { 725 final List<ActivityStack> activityStacks = new ArrayList<>(); 726 for (TaskFragmentContainer container : mContainers) { 727 final ActivityStack activityStack = container.toActivityStackIfStable(); 728 if (activityStack == null) { 729 return null; 730 } 731 activityStacks.add(activityStack); 732 } 733 return activityStacks; 734 } 735 736 /** A wrapper class which contains the information of {@link TaskContainer} */ 737 static final class TaskProperties { 738 private final int mDisplayId; 739 @NonNull 740 private final Configuration mConfiguration; 741 TaskProperties(int displayId, @NonNull Configuration configuration)742 TaskProperties(int displayId, @NonNull Configuration configuration) { 743 mDisplayId = displayId; 744 mConfiguration = configuration; 745 } 746 getDisplayId()747 int getDisplayId() { 748 return mDisplayId; 749 } 750 751 @NonNull getConfiguration()752 Configuration getConfiguration() { 753 return mConfiguration; 754 } 755 756 /** A helper method to return task {@link WindowMetrics} from this {@link TaskProperties} */ 757 @NonNull getTaskMetrics()758 WindowMetrics getTaskMetrics() { 759 final Rect taskBounds = mConfiguration.windowConfiguration.getBounds(); 760 // TODO(b/190433398): Supply correct insets. 761 final float density = mConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE; 762 return new WindowMetrics(taskBounds, WindowInsets.CONSUMED, density); 763 } 764 765 /** Translates the given absolute bounds to relative bounds in this Task coordinate. */ translateAbsoluteBoundsToRelativeBounds(@onNull Rect inOutBounds)766 void translateAbsoluteBoundsToRelativeBounds(@NonNull Rect inOutBounds) { 767 if (inOutBounds.isEmpty()) { 768 return; 769 } 770 final Rect taskBounds = mConfiguration.windowConfiguration.getBounds(); 771 inOutBounds.offset(-taskBounds.left, -taskBounds.top); 772 } 773 774 /** 775 * Obtains the {@link TaskProperties} for the task that the provided {@link Activity} is 776 * associated with. 777 * <p> 778 * Note that for most case, caller should use 779 * {@link SplitPresenter#getTaskProperties(Activity)} instead. This method is used before 780 * the {@code activity} goes into split. 781 * </p><p> 782 * If the {@link Activity} is in fullscreen, override 783 * {@link WindowConfiguration#getBounds()} with {@link WindowConfiguration#getMaxBounds()} 784 * in case the {@link Activity} is letterboxed. Otherwise, get the Task 785 * {@link Configuration} from the server side or use {@link Activity}'s 786 * {@link Configuration} as a fallback if the Task {@link Configuration} cannot be obtained. 787 */ 788 @NonNull getTaskPropertiesFromActivity(@onNull Activity activity)789 static TaskProperties getTaskPropertiesFromActivity(@NonNull Activity activity) { 790 final int displayId = activity.getDisplayId(); 791 // Use a copy of configuration because activity's configuration may be updated later, 792 // or we may get unexpected TaskContainer's configuration if Activity's configuration is 793 // updated. An example is Activity is going to be in split. 794 final Configuration activityConfig = new Configuration( 795 activity.getResources().getConfiguration()); 796 final WindowConfiguration windowConfiguration = activityConfig.windowConfiguration; 797 final int windowingMode = windowConfiguration.getWindowingMode(); 798 if (!inMultiWindowMode(windowingMode)) { 799 // Use the max bounds in fullscreen in case the Activity is letterboxed. 800 windowConfiguration.setBounds(windowConfiguration.getMaxBounds()); 801 return new TaskProperties(displayId, activityConfig); 802 } 803 final Configuration taskConfig = ActivityClient.getInstance() 804 .getTaskConfiguration(activity.getActivityToken()); 805 if (taskConfig == null) { 806 Log.w(TAG, "Could not obtain task configuration for activity:" + activity); 807 // Still report activity config if task config cannot be obtained from the server 808 // side. 809 return new TaskProperties(displayId, activityConfig); 810 } 811 return new TaskProperties(displayId, taskConfig); 812 } 813 } 814 } 815