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