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