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.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; 20 21 import android.app.Activity; 22 import android.app.ActivityThread; 23 import android.app.WindowConfiguration.WindowingMode; 24 import android.content.Intent; 25 import android.graphics.Rect; 26 import android.os.Binder; 27 import android.os.IBinder; 28 import android.util.Size; 29 import android.window.TaskFragmentAnimationParams; 30 import android.window.TaskFragmentInfo; 31 import android.window.WindowContainerTransaction; 32 33 import androidx.annotation.GuardedBy; 34 import androidx.annotation.NonNull; 35 import androidx.annotation.Nullable; 36 37 import com.android.internal.annotations.VisibleForTesting; 38 39 import java.util.ArrayList; 40 import java.util.Iterator; 41 import java.util.List; 42 43 /** 44 * Client-side container for a stack of activities. Corresponds to an instance of TaskFragment 45 * on the server side. 46 */ 47 // Suppress GuardedBy warning because all the TaskFragmentContainers are stored in 48 // SplitController.mTaskContainers which is guarded. 49 @SuppressWarnings("GuardedBy") 50 class TaskFragmentContainer { 51 private static final int APPEAR_EMPTY_TIMEOUT_MS = 3000; 52 53 @NonNull 54 private final SplitController mController; 55 56 /** 57 * Client-created token that uniquely identifies the task fragment container instance. 58 */ 59 @NonNull 60 private final IBinder mToken; 61 62 /** Parent leaf Task. */ 63 @NonNull 64 private final TaskContainer mTaskContainer; 65 66 /** 67 * Server-provided task fragment information. 68 */ 69 @VisibleForTesting 70 TaskFragmentInfo mInfo; 71 72 /** 73 * Activity tokens that are being reparented or being started to this container, but haven't 74 * been added to {@link #mInfo} yet. 75 */ 76 @VisibleForTesting 77 final ArrayList<IBinder> mPendingAppearedActivities = new ArrayList<>(); 78 79 /** 80 * When this container is created for an {@link Intent} to start within, we store that Intent 81 * until the container becomes non-empty on the server side, so that we can use it to check 82 * rules associated with this container. 83 */ 84 @Nullable 85 private Intent mPendingAppearedIntent; 86 87 /** Containers that are dependent on this one and should be completely destroyed on exit. */ 88 private final List<TaskFragmentContainer> mContainersToFinishOnExit = 89 new ArrayList<>(); 90 91 /** 92 * Individual associated activity tokens in different containers that should be finished on 93 * exit. 94 */ 95 private final List<IBinder> mActivitiesToFinishOnExit = new ArrayList<>(); 96 97 /** Indicates whether the container was cleaned up after the last activity was removed. */ 98 private boolean mIsFinished; 99 100 /** 101 * Bounds that were requested last via {@link android.window.WindowContainerTransaction}. 102 */ 103 private final Rect mLastRequestedBounds = new Rect(); 104 105 /** 106 * Windowing mode that was requested last via {@link android.window.WindowContainerTransaction}. 107 */ 108 @WindowingMode 109 private int mLastRequestedWindowingMode = WINDOWING_MODE_UNDEFINED; 110 111 /** 112 * TaskFragmentAnimationParams that was requested last via 113 * {@link android.window.WindowContainerTransaction}. 114 */ 115 @NonNull 116 private TaskFragmentAnimationParams mLastAnimationParams = TaskFragmentAnimationParams.DEFAULT; 117 118 /** 119 * When the TaskFragment has appeared in server, but is empty, we should remove the TaskFragment 120 * if it is still empty after the timeout. 121 */ 122 @VisibleForTesting 123 @Nullable 124 Runnable mAppearEmptyTimeout; 125 126 /** 127 * Creates a container with an existing activity that will be re-parented to it in a window 128 * container transaction. 129 * @param pairedPrimaryContainer when it is set, the new container will be add right above it 130 */ TaskFragmentContainer(@ullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @NonNull TaskContainer taskContainer, @NonNull SplitController controller, @Nullable TaskFragmentContainer pairedPrimaryContainer)131 TaskFragmentContainer(@Nullable Activity pendingAppearedActivity, 132 @Nullable Intent pendingAppearedIntent, @NonNull TaskContainer taskContainer, 133 @NonNull SplitController controller, 134 @Nullable TaskFragmentContainer pairedPrimaryContainer) { 135 if ((pendingAppearedActivity == null && pendingAppearedIntent == null) 136 || (pendingAppearedActivity != null && pendingAppearedIntent != null)) { 137 throw new IllegalArgumentException( 138 "One and only one of pending activity and intent must be non-null"); 139 } 140 mController = controller; 141 mToken = new Binder("TaskFragmentContainer"); 142 mTaskContainer = taskContainer; 143 if (pairedPrimaryContainer != null) { 144 // The TaskFragment will be positioned right above the paired container. 145 if (pairedPrimaryContainer.getTaskContainer() != taskContainer) { 146 throw new IllegalArgumentException( 147 "pairedPrimaryContainer must be in the same Task"); 148 } 149 final int primaryIndex = taskContainer.mContainers.indexOf(pairedPrimaryContainer); 150 taskContainer.mContainers.add(primaryIndex + 1, this); 151 } else if (pendingAppearedActivity != null) { 152 // The TaskFragment will be positioned right above the pending appeared Activity. If any 153 // existing TaskFragment is empty with pending Intent, it is likely that the Activity of 154 // the pending Intent hasn't been created yet, so the new Activity should be below the 155 // empty TaskFragment. 156 int i = taskContainer.mContainers.size() - 1; 157 for (; i >= 0; i--) { 158 final TaskFragmentContainer container = taskContainer.mContainers.get(i); 159 if (!container.isEmpty() || container.getPendingAppearedIntent() == null) { 160 break; 161 } 162 } 163 taskContainer.mContainers.add(i + 1, this); 164 } else { 165 taskContainer.mContainers.add(this); 166 } 167 if (pendingAppearedActivity != null) { 168 addPendingAppearedActivity(pendingAppearedActivity); 169 } 170 mPendingAppearedIntent = pendingAppearedIntent; 171 } 172 173 /** 174 * Returns the client-created token that uniquely identifies this container. 175 */ 176 @NonNull getTaskFragmentToken()177 IBinder getTaskFragmentToken() { 178 return mToken; 179 } 180 181 /** List of non-finishing activities that belong to this container and live in this process. */ 182 @NonNull collectNonFinishingActivities()183 List<Activity> collectNonFinishingActivities() { 184 final List<Activity> allActivities = new ArrayList<>(); 185 if (mInfo != null) { 186 // Add activities reported from the server. 187 for (IBinder token : mInfo.getActivities()) { 188 final Activity activity = mController.getActivity(token); 189 if (activity != null && !activity.isFinishing()) { 190 allActivities.add(activity); 191 } 192 } 193 } 194 195 // Add the re-parenting activity, in case the server has not yet reported the task 196 // fragment info update with it placed in this container. We still want to apply rules 197 // in this intermediate state. 198 // Place those on top of the list since they will be on the top after reported from the 199 // server. 200 for (IBinder token : mPendingAppearedActivities) { 201 final Activity activity = mController.getActivity(token); 202 if (activity != null && !activity.isFinishing()) { 203 allActivities.add(activity); 204 } 205 } 206 return allActivities; 207 } 208 209 /** Whether this TaskFragment is visible. */ isVisible()210 boolean isVisible() { 211 return mInfo != null && mInfo.isVisible(); 212 } 213 214 /** Whether the TaskFragment is in an intermediate state waiting for the server update.*/ isInIntermediateState()215 boolean isInIntermediateState() { 216 if (mInfo == null) { 217 // Haven't received onTaskFragmentAppeared event. 218 return true; 219 } 220 if (mInfo.isEmpty()) { 221 // Empty TaskFragment will be removed or will have activity launched into it soon. 222 return true; 223 } 224 if (!mPendingAppearedActivities.isEmpty()) { 225 // Reparented activity hasn't appeared. 226 return true; 227 } 228 // Check if there is any reported activity that is no longer alive. 229 for (IBinder token : mInfo.getActivities()) { 230 final Activity activity = mController.getActivity(token); 231 if (activity == null && !mTaskContainer.isVisible()) { 232 // Activity can be null if the activity is not attached to process yet. That can 233 // happen when the activity is started in background. 234 continue; 235 } 236 if (activity == null || activity.isFinishing()) { 237 // One of the reported activity is no longer alive, wait for the server update. 238 return true; 239 } 240 } 241 return false; 242 } 243 244 @NonNull toActivityStack()245 ActivityStack toActivityStack() { 246 return new ActivityStack(collectNonFinishingActivities(), isEmpty()); 247 } 248 249 /** Adds the activity that will be reparented to this container. */ addPendingAppearedActivity(@onNull Activity pendingAppearedActivity)250 void addPendingAppearedActivity(@NonNull Activity pendingAppearedActivity) { 251 final IBinder activityToken = pendingAppearedActivity.getActivityToken(); 252 if (hasActivity(activityToken)) { 253 return; 254 } 255 // Remove the pending activity from other TaskFragments in case the activity is reparented 256 // again before the server update. 257 mTaskContainer.cleanupPendingAppearedActivity(activityToken); 258 mPendingAppearedActivities.add(activityToken); 259 updateActivityClientRecordTaskFragmentToken(activityToken); 260 } 261 262 /** 263 * Updates the {@link ActivityThread.ActivityClientRecord#mTaskFragmentToken} for the 264 * activity. This makes sure the token is up-to-date if the activity is relaunched later. 265 */ updateActivityClientRecordTaskFragmentToken(@onNull IBinder activityToken)266 private void updateActivityClientRecordTaskFragmentToken(@NonNull IBinder activityToken) { 267 final ActivityThread.ActivityClientRecord record = ActivityThread 268 .currentActivityThread().getActivityClient(activityToken); 269 if (record != null) { 270 record.mTaskFragmentToken = mToken; 271 } 272 } 273 removePendingAppearedActivity(@onNull IBinder activityToken)274 void removePendingAppearedActivity(@NonNull IBinder activityToken) { 275 mPendingAppearedActivities.remove(activityToken); 276 } 277 278 @GuardedBy("mController.mLock") clearPendingAppearedActivities()279 void clearPendingAppearedActivities() { 280 final List<IBinder> cleanupActivities = new ArrayList<>(mPendingAppearedActivities); 281 // Clear mPendingAppearedActivities so that #getContainerWithActivity won't return the 282 // current TaskFragment. 283 mPendingAppearedActivities.clear(); 284 mPendingAppearedIntent = null; 285 286 // For removed pending activities, we need to update the them to their previous containers. 287 for (IBinder activityToken : cleanupActivities) { 288 final TaskFragmentContainer curContainer = mController.getContainerWithActivity( 289 activityToken); 290 if (curContainer != null) { 291 curContainer.updateActivityClientRecordTaskFragmentToken(activityToken); 292 } 293 } 294 } 295 296 /** Called when the activity is destroyed. */ onActivityDestroyed(@onNull IBinder activityToken)297 void onActivityDestroyed(@NonNull IBinder activityToken) { 298 removePendingAppearedActivity(activityToken); 299 if (mInfo != null) { 300 // Remove the activity now because there can be a delay before the server callback. 301 mInfo.getActivities().remove(activityToken); 302 } 303 mActivitiesToFinishOnExit.remove(activityToken); 304 } 305 306 @Nullable getPendingAppearedIntent()307 Intent getPendingAppearedIntent() { 308 return mPendingAppearedIntent; 309 } 310 setPendingAppearedIntent(@ullable Intent intent)311 void setPendingAppearedIntent(@Nullable Intent intent) { 312 mPendingAppearedIntent = intent; 313 } 314 315 /** 316 * Clears the pending appeared Intent if it is the same as given Intent. Otherwise, the 317 * pending appeared Intent is cleared when TaskFragmentInfo is set and is not empty (has 318 * running activities). 319 */ clearPendingAppearedIntentIfNeeded(@onNull Intent intent)320 void clearPendingAppearedIntentIfNeeded(@NonNull Intent intent) { 321 if (mPendingAppearedIntent == null || mPendingAppearedIntent != intent) { 322 return; 323 } 324 mPendingAppearedIntent = null; 325 } 326 hasActivity(@onNull IBinder activityToken)327 boolean hasActivity(@NonNull IBinder activityToken) { 328 // Instead of using (hasAppearedActivity() || hasPendingAppearedActivity), we want to make 329 // sure the controller considers this container as the one containing the activity. 330 // This is needed when the activity is added as pending appeared activity to one 331 // TaskFragment while it is also an appeared activity in another. 332 return mController.getContainerWithActivity(activityToken) == this; 333 } 334 335 /** Whether this activity has appeared in the TaskFragment on the server side. */ hasAppearedActivity(@onNull IBinder activityToken)336 boolean hasAppearedActivity(@NonNull IBinder activityToken) { 337 return mInfo != null && mInfo.getActivities().contains(activityToken); 338 } 339 340 /** 341 * Whether we are waiting for this activity to appear in the TaskFragment on the server side. 342 */ hasPendingAppearedActivity(@onNull IBinder activityToken)343 boolean hasPendingAppearedActivity(@NonNull IBinder activityToken) { 344 return mPendingAppearedActivities.contains(activityToken); 345 } 346 getRunningActivityCount()347 int getRunningActivityCount() { 348 int count = mPendingAppearedActivities.size(); 349 if (mInfo != null) { 350 count += mInfo.getRunningActivityCount(); 351 } 352 return count; 353 } 354 355 /** Whether we are waiting for the TaskFragment to appear and become non-empty. */ isWaitingActivityAppear()356 boolean isWaitingActivityAppear() { 357 return !mIsFinished && (mInfo == null || mAppearEmptyTimeout != null); 358 } 359 360 @Nullable getInfo()361 TaskFragmentInfo getInfo() { 362 return mInfo; 363 } 364 365 @GuardedBy("mController.mLock") setInfo(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentInfo info)366 void setInfo(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentInfo info) { 367 if (!mIsFinished && mInfo == null && info.isEmpty()) { 368 // onTaskFragmentAppeared with empty info. We will remove the TaskFragment if no 369 // pending appeared intent/activities. Otherwise, wait and removing the TaskFragment if 370 // it is still empty after timeout. 371 if (mPendingAppearedIntent != null || !mPendingAppearedActivities.isEmpty()) { 372 mAppearEmptyTimeout = () -> { 373 synchronized (mController.mLock) { 374 mAppearEmptyTimeout = null; 375 // Call without the pass-in wct when timeout. We need to applyWct directly 376 // in this case. 377 mController.onTaskFragmentAppearEmptyTimeout(this); 378 } 379 }; 380 mController.getHandler().postDelayed(mAppearEmptyTimeout, APPEAR_EMPTY_TIMEOUT_MS); 381 } else { 382 mAppearEmptyTimeout = null; 383 mController.onTaskFragmentAppearEmptyTimeout(wct, this); 384 } 385 } else if (mAppearEmptyTimeout != null && !info.isEmpty()) { 386 mController.getHandler().removeCallbacks(mAppearEmptyTimeout); 387 mAppearEmptyTimeout = null; 388 } 389 390 mInfo = info; 391 if (mInfo == null || mInfo.isEmpty()) { 392 return; 393 } 394 // Only track the pending Intent when the container is empty. 395 mPendingAppearedIntent = null; 396 if (mPendingAppearedActivities.isEmpty()) { 397 return; 398 } 399 // Cleanup activities that were being re-parented 400 List<IBinder> infoActivities = mInfo.getActivities(); 401 for (int i = mPendingAppearedActivities.size() - 1; i >= 0; --i) { 402 final IBinder activityToken = mPendingAppearedActivities.get(i); 403 if (infoActivities.contains(activityToken)) { 404 mPendingAppearedActivities.remove(i); 405 } 406 } 407 } 408 409 @Nullable getTopNonFinishingActivity()410 Activity getTopNonFinishingActivity() { 411 final List<Activity> activities = collectNonFinishingActivities(); 412 return activities.isEmpty() ? null : activities.get(activities.size() - 1); 413 } 414 415 @Nullable getBottomMostActivity()416 Activity getBottomMostActivity() { 417 final List<Activity> activities = collectNonFinishingActivities(); 418 return activities.isEmpty() ? null : activities.get(0); 419 } 420 isEmpty()421 boolean isEmpty() { 422 return mPendingAppearedActivities.isEmpty() && (mInfo == null || mInfo.isEmpty()); 423 } 424 425 /** 426 * Adds a container that should be finished when this container is finished. 427 */ addContainerToFinishOnExit(@onNull TaskFragmentContainer containerToFinish)428 void addContainerToFinishOnExit(@NonNull TaskFragmentContainer containerToFinish) { 429 if (mIsFinished) { 430 return; 431 } 432 mContainersToFinishOnExit.add(containerToFinish); 433 } 434 435 /** 436 * Removes a container that should be finished when this container is finished. 437 */ removeContainerToFinishOnExit(@onNull TaskFragmentContainer containerToRemove)438 void removeContainerToFinishOnExit(@NonNull TaskFragmentContainer containerToRemove) { 439 if (mIsFinished) { 440 return; 441 } 442 mContainersToFinishOnExit.remove(containerToRemove); 443 } 444 445 /** 446 * Adds an activity that should be finished when this container is finished. 447 */ addActivityToFinishOnExit(@onNull Activity activityToFinish)448 void addActivityToFinishOnExit(@NonNull Activity activityToFinish) { 449 if (mIsFinished) { 450 return; 451 } 452 mActivitiesToFinishOnExit.add(activityToFinish.getActivityToken()); 453 } 454 455 /** 456 * Removes an activity that should be finished when this container is finished. 457 */ removeActivityToFinishOnExit(@onNull Activity activityToRemove)458 void removeActivityToFinishOnExit(@NonNull Activity activityToRemove) { 459 if (mIsFinished) { 460 return; 461 } 462 mActivitiesToFinishOnExit.remove(activityToRemove.getActivityToken()); 463 } 464 465 /** Removes all dependencies that should be finished when this container is finished. */ resetDependencies()466 void resetDependencies() { 467 if (mIsFinished) { 468 return; 469 } 470 mContainersToFinishOnExit.clear(); 471 mActivitiesToFinishOnExit.clear(); 472 } 473 474 /** 475 * Removes all activities that belong to this process and finishes other containers/activities 476 * configured to finish together. 477 */ 478 @GuardedBy("mController.mLock") finish(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, @NonNull WindowContainerTransaction wct, @NonNull SplitController controller)479 void finish(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, 480 @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) { 481 if (!mIsFinished) { 482 mIsFinished = true; 483 if (mAppearEmptyTimeout != null) { 484 mController.getHandler().removeCallbacks(mAppearEmptyTimeout); 485 mAppearEmptyTimeout = null; 486 } 487 finishActivities(shouldFinishDependent, presenter, wct, controller); 488 } 489 490 if (mInfo == null) { 491 // Defer removal the container and wait until TaskFragment appeared. 492 return; 493 } 494 495 // Cleanup the visuals 496 presenter.deleteTaskFragment(wct, getTaskFragmentToken()); 497 // Cleanup the records 498 controller.removeContainer(this); 499 // Clean up task fragment information 500 mInfo = null; 501 } 502 503 @GuardedBy("mController.mLock") finishActivities(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, @NonNull WindowContainerTransaction wct, @NonNull SplitController controller)504 private void finishActivities(boolean shouldFinishDependent, @NonNull SplitPresenter presenter, 505 @NonNull WindowContainerTransaction wct, @NonNull SplitController controller) { 506 // Finish own activities 507 for (Activity activity : collectNonFinishingActivities()) { 508 if (!activity.isFinishing() 509 // In case we have requested to reparent the activity to another container (as 510 // pendingAppeared), we don't want to finish it with this container. 511 && mController.getContainerWithActivity(activity) == this) { 512 wct.finishActivity(activity.getActivityToken()); 513 } 514 } 515 516 if (!shouldFinishDependent) { 517 // Always finish the placeholder when the primary is finished. 518 finishPlaceholderIfAny(wct, presenter); 519 return; 520 } 521 522 // Finish dependent containers 523 for (TaskFragmentContainer container : mContainersToFinishOnExit) { 524 if (container.mIsFinished 525 || controller.shouldRetainAssociatedContainer(this, container)) { 526 continue; 527 } 528 container.finish(true /* shouldFinishDependent */, presenter, 529 wct, controller); 530 } 531 mContainersToFinishOnExit.clear(); 532 533 // Finish associated activities 534 for (IBinder activityToken : mActivitiesToFinishOnExit) { 535 final Activity activity = mController.getActivity(activityToken); 536 if (activity == null || activity.isFinishing() 537 || controller.shouldRetainAssociatedActivity(this, activity)) { 538 continue; 539 } 540 wct.finishActivity(activity.getActivityToken()); 541 } 542 mActivitiesToFinishOnExit.clear(); 543 } 544 545 @GuardedBy("mController.mLock") finishPlaceholderIfAny(@onNull WindowContainerTransaction wct, @NonNull SplitPresenter presenter)546 private void finishPlaceholderIfAny(@NonNull WindowContainerTransaction wct, 547 @NonNull SplitPresenter presenter) { 548 final List<TaskFragmentContainer> containersToRemove = new ArrayList<>(); 549 for (TaskFragmentContainer container : mContainersToFinishOnExit) { 550 if (container.mIsFinished) { 551 continue; 552 } 553 final SplitContainer splitContainer = mController.getActiveSplitForContainers( 554 this, container); 555 if (splitContainer != null && splitContainer.isPlaceholderContainer() 556 && splitContainer.getSecondaryContainer() == container) { 557 // Remove the placeholder secondary TaskFragment. 558 containersToRemove.add(container); 559 } 560 } 561 mContainersToFinishOnExit.removeAll(containersToRemove); 562 for (TaskFragmentContainer container : containersToRemove) { 563 container.finish(false /* shouldFinishDependent */, presenter, wct, mController); 564 } 565 } 566 isFinished()567 boolean isFinished() { 568 return mIsFinished; 569 } 570 571 /** 572 * Checks if last requested bounds are equal to the provided value. 573 */ areLastRequestedBoundsEqual(@ullable Rect bounds)574 boolean areLastRequestedBoundsEqual(@Nullable Rect bounds) { 575 return (bounds == null && mLastRequestedBounds.isEmpty()) 576 || mLastRequestedBounds.equals(bounds); 577 } 578 579 /** 580 * Updates the last requested bounds. 581 */ setLastRequestedBounds(@ullable Rect bounds)582 void setLastRequestedBounds(@Nullable Rect bounds) { 583 if (bounds == null) { 584 mLastRequestedBounds.setEmpty(); 585 } else { 586 mLastRequestedBounds.set(bounds); 587 } 588 } 589 590 @NonNull getLastRequestedBounds()591 Rect getLastRequestedBounds() { 592 return mLastRequestedBounds; 593 } 594 595 /** 596 * Checks if last requested windowing mode is equal to the provided value. 597 */ isLastRequestedWindowingModeEqual(@indowingMode int windowingMode)598 boolean isLastRequestedWindowingModeEqual(@WindowingMode int windowingMode) { 599 return mLastRequestedWindowingMode == windowingMode; 600 } 601 602 /** 603 * Updates the last requested windowing mode. 604 */ setLastRequestedWindowingMode(@indowingMode int windowingModes)605 void setLastRequestedWindowingMode(@WindowingMode int windowingModes) { 606 mLastRequestedWindowingMode = windowingModes; 607 } 608 609 /** 610 * Checks if last requested {@link TaskFragmentAnimationParams} are equal to the provided value. 611 */ areLastRequestedAnimationParamsEqual( @onNull TaskFragmentAnimationParams animationParams)612 boolean areLastRequestedAnimationParamsEqual( 613 @NonNull TaskFragmentAnimationParams animationParams) { 614 return mLastAnimationParams.equals(animationParams); 615 } 616 617 /** 618 * Updates the last requested {@link TaskFragmentAnimationParams}. 619 */ setLastRequestAnimationParams(@onNull TaskFragmentAnimationParams animationParams)620 void setLastRequestAnimationParams(@NonNull TaskFragmentAnimationParams animationParams) { 621 mLastAnimationParams = animationParams; 622 } 623 624 /** Gets the parent leaf Task id. */ getTaskId()625 int getTaskId() { 626 return mTaskContainer.getTaskId(); 627 } 628 629 /** Gets the parent Task. */ 630 @NonNull getTaskContainer()631 TaskContainer getTaskContainer() { 632 return mTaskContainer; 633 } 634 635 @Nullable getMinDimensions()636 Size getMinDimensions() { 637 if (mInfo == null) { 638 return null; 639 } 640 int maxMinWidth = mInfo.getMinimumWidth(); 641 int maxMinHeight = mInfo.getMinimumHeight(); 642 for (IBinder activityToken : mPendingAppearedActivities) { 643 final Activity activity = mController.getActivity(activityToken); 644 final Size minDimensions = SplitPresenter.getMinDimensions(activity); 645 if (minDimensions == null) { 646 continue; 647 } 648 maxMinWidth = Math.max(maxMinWidth, minDimensions.getWidth()); 649 maxMinHeight = Math.max(maxMinHeight, minDimensions.getHeight()); 650 } 651 if (mPendingAppearedIntent != null) { 652 final Size minDimensions = SplitPresenter.getMinDimensions(mPendingAppearedIntent); 653 if (minDimensions != null) { 654 maxMinWidth = Math.max(maxMinWidth, minDimensions.getWidth()); 655 maxMinHeight = Math.max(maxMinHeight, minDimensions.getHeight()); 656 } 657 } 658 return new Size(maxMinWidth, maxMinHeight); 659 } 660 661 /** Whether the current TaskFragment is above the {@code other} TaskFragment. */ isAbove(@onNull TaskFragmentContainer other)662 boolean isAbove(@NonNull TaskFragmentContainer other) { 663 if (mTaskContainer != other.mTaskContainer) { 664 throw new IllegalArgumentException( 665 "Trying to compare two TaskFragments in different Task."); 666 } 667 if (this == other) { 668 throw new IllegalArgumentException("Trying to compare a TaskFragment with itself."); 669 } 670 return mTaskContainer.indexOf(this) > mTaskContainer.indexOf(other); 671 } 672 673 @Override toString()674 public String toString() { 675 return toString(true /* includeContainersToFinishOnExit */); 676 } 677 678 /** 679 * @return string for this TaskFragmentContainer and includes containers to finish on exit 680 * based on {@code includeContainersToFinishOnExit}. If containers to finish on exit are always 681 * included in the string, then calling {@link #toString()} on a container that mutually 682 * finishes with another container would cause a stack overflow. 683 */ toString(boolean includeContainersToFinishOnExit)684 private String toString(boolean includeContainersToFinishOnExit) { 685 return "TaskFragmentContainer{" 686 + " parentTaskId=" + getTaskId() 687 + " token=" + mToken 688 + " topNonFinishingActivity=" + getTopNonFinishingActivity() 689 + " runningActivityCount=" + getRunningActivityCount() 690 + " isFinished=" + mIsFinished 691 + " lastRequestedBounds=" + mLastRequestedBounds 692 + " pendingAppearedActivities=" + mPendingAppearedActivities 693 + (includeContainersToFinishOnExit ? " containersToFinishOnExit=" 694 + containersToFinishOnExitToString() : "") 695 + " activitiesToFinishOnExit=" + mActivitiesToFinishOnExit 696 + " info=" + mInfo 697 + "}"; 698 } 699 containersToFinishOnExitToString()700 private String containersToFinishOnExitToString() { 701 StringBuilder sb = new StringBuilder("["); 702 Iterator<TaskFragmentContainer> containerIterator = mContainersToFinishOnExit.iterator(); 703 while (containerIterator.hasNext()) { 704 sb.append(containerIterator.next().toString( 705 false /* includeContainersToFinishOnExit */)); 706 if (containerIterator.hasNext()) { 707 sb.append(", "); 708 } 709 } 710 return sb.append("]").toString(); 711 } 712 } 713