• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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