• 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.content.pm.PackageManager.MATCH_ALL;
20 
21 import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider;
22 import static androidx.window.extensions.embedding.SplitAttributesHelper.isReversedLayout;
23 import static androidx.window.extensions.embedding.SplitController.TAG;
24 import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
25 
26 import android.annotation.AnimRes;
27 import android.annotation.NonNull;
28 import android.app.Activity;
29 import android.app.ActivityThread;
30 import android.app.WindowConfiguration;
31 import android.app.WindowConfiguration.WindowingMode;
32 import android.content.Intent;
33 import android.content.pm.ActivityInfo;
34 import android.content.pm.PackageManager;
35 import android.content.pm.ResolveInfo;
36 import android.content.res.Configuration;
37 import android.content.res.Resources;
38 import android.graphics.Rect;
39 import android.os.Bundle;
40 import android.os.IBinder;
41 import android.util.Log;
42 import android.util.Pair;
43 import android.util.Size;
44 import android.view.View;
45 import android.view.WindowMetrics;
46 import android.window.TaskFragmentAnimationParams;
47 import android.window.TaskFragmentCreationParams;
48 import android.window.WindowContainerTransaction;
49 
50 import androidx.annotation.IntDef;
51 import androidx.annotation.Nullable;
52 import androidx.window.extensions.core.util.function.Function;
53 import androidx.window.extensions.embedding.SplitAttributes.SplitType;
54 import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType;
55 import androidx.window.extensions.embedding.SplitAttributes.SplitType.HingeSplitType;
56 import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType;
57 import androidx.window.extensions.embedding.TaskContainer.TaskProperties;
58 import androidx.window.extensions.layout.DisplayFeature;
59 import androidx.window.extensions.layout.FoldingFeature;
60 import androidx.window.extensions.layout.WindowLayoutComponentImpl;
61 import androidx.window.extensions.layout.WindowLayoutInfo;
62 
63 import com.android.internal.R;
64 import com.android.internal.annotations.VisibleForTesting;
65 
66 import java.util.ArrayList;
67 import java.util.List;
68 import java.util.Objects;
69 import java.util.Set;
70 import java.util.concurrent.Executor;
71 
72 /**
73  * Controls the visual presentation of the splits according to the containers formed by
74  * {@link SplitController}.
75  *
76  * Note that all calls into this class must hold the {@link SplitController} internal lock.
77  */
78 @SuppressWarnings("GuardedBy")
79 class SplitPresenter extends JetpackTaskFragmentOrganizer {
80     @VisibleForTesting
81     static final int POSITION_START = 0;
82     @VisibleForTesting
83     static final int POSITION_END = 1;
84     @VisibleForTesting
85     static final int POSITION_FILL = 2;
86 
87     @IntDef(value = {
88             POSITION_START,
89             POSITION_END,
90             POSITION_FILL,
91     })
92     private @interface Position {}
93 
94     static final int CONTAINER_POSITION_LEFT = 0;
95     static final int CONTAINER_POSITION_TOP = 1;
96     static final int CONTAINER_POSITION_RIGHT = 2;
97     static final int CONTAINER_POSITION_BOTTOM = 3;
98 
99     @IntDef(value = {
100             CONTAINER_POSITION_LEFT,
101             CONTAINER_POSITION_TOP,
102             CONTAINER_POSITION_RIGHT,
103             CONTAINER_POSITION_BOTTOM,
104     })
105     @interface ContainerPosition {}
106 
107     /**
108      * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
109      * Activity, Activity, Intent)}.
110      * No need to expand the splitContainer because screen is big enough to
111      * {@link #shouldShowSplit(SplitAttributes)} and minimum dimensions is
112      * satisfied.
113      */
114     static final int RESULT_NOT_EXPANDED = 0;
115     /**
116      * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
117      * Activity, Activity, Intent)}.
118      * The splitContainer should be expanded. It is usually because minimum dimensions is not
119      * satisfied.
120      * @see #shouldShowSplit(SplitAttributes)
121      */
122     static final int RESULT_EXPANDED = 1;
123     /**
124      * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
125      * Activity, Activity, Intent)}.
126      * The splitContainer should be expanded, but the client side hasn't received
127      * {@link android.window.TaskFragmentInfo} yet. Fallback to create new expanded SplitContainer
128      * instead.
129      */
130     static final int RESULT_EXPAND_FAILED_NO_TF_INFO = 2;
131 
132     /**
133      * The key of {@link ActivityStack} alignment relative to its parent container.
134      * <p>
135      * See {@link ContainerPosition} for possible values.
136      * <p>
137      * Note that this constants must align with the definition in WM Jetpack library.
138      */
139     private static final String KEY_ACTIVITY_STACK_ALIGNMENT =
140             "androidx.window.embedding.ActivityStackAlignment";
141 
142     /**
143      * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
144      * Activity, Activity, Intent)}
145      */
146     @IntDef(value = {
147             RESULT_NOT_EXPANDED,
148             RESULT_EXPANDED,
149             RESULT_EXPAND_FAILED_NO_TF_INFO,
150     })
151     private @interface ResultCode {}
152 
153     @VisibleForTesting
154     static final SplitAttributes EXPAND_CONTAINERS_ATTRIBUTES =
155             new SplitAttributes.Builder()
156             .setSplitType(new ExpandContainersSplitType())
157             .build();
158 
159     private final WindowLayoutComponentImpl mWindowLayoutComponent;
160     private final SplitController mController;
161     @NonNull
162     private final BackupHelper mBackupHelper;
163 
SplitPresenter(@onNull Executor executor, @NonNull WindowLayoutComponentImpl windowLayoutComponent, @NonNull SplitController controller)164     SplitPresenter(@NonNull Executor executor,
165             @NonNull WindowLayoutComponentImpl windowLayoutComponent,
166             @NonNull SplitController controller) {
167         super(executor, controller);
168         mWindowLayoutComponent = windowLayoutComponent;
169         mController = controller;
170         final Bundle outSavedState = new Bundle();
171         outSavedState.setClassLoader(ParcelableTaskContainerData.class.getClassLoader());
172         registerOrganizer(false /* isSystemOrganizer */, outSavedState);
173         mBackupHelper = new BackupHelper(controller, this, outSavedState);
174         if (!SplitController.ENABLE_SHELL_TRANSITIONS) {
175             // TODO(b/207070762): cleanup with legacy app transition
176             // Animation will be handled by WM Shell when Shell transition is enabled.
177             overrideSplitAnimation();
178         }
179     }
180 
setAutoSaveEmbeddingState(boolean saveEmbeddingState)181     void setAutoSaveEmbeddingState(boolean saveEmbeddingState) {
182         mBackupHelper.setAutoSaveEmbeddingState(saveEmbeddingState);
183     }
184 
scheduleBackup()185     void scheduleBackup() {
186         mBackupHelper.scheduleBackup();
187     }
188 
isWaitingToRebuildTaskContainers()189     boolean isWaitingToRebuildTaskContainers() {
190         return mBackupHelper.hasPendingStateToRestore();
191     }
192 
abortTaskContainerRebuilding(@onNull WindowContainerTransaction wct)193     void abortTaskContainerRebuilding(@NonNull WindowContainerTransaction wct) {
194         mBackupHelper.abortTaskContainerRebuilding(wct);
195     }
196 
rebuildTaskContainers(@onNull WindowContainerTransaction wct, @NonNull Set<EmbeddingRule> rules)197     boolean rebuildTaskContainers(@NonNull WindowContainerTransaction wct,
198             @NonNull Set<EmbeddingRule> rules) {
199         return mBackupHelper.rebuildTaskContainers(wct, rules);
200     }
201 
202     /**
203      * Deletes the specified container and all other associated and dependent containers in the same
204      * transaction.
205      */
cleanupContainer(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, boolean shouldFinishDependent)206     void cleanupContainer(@NonNull WindowContainerTransaction wct,
207             @NonNull TaskFragmentContainer container, boolean shouldFinishDependent) {
208         container.finish(shouldFinishDependent, this, wct, mController);
209         // Make sure the containers in the Task is up-to-date.
210         mController.updateContainersInTaskIfVisible(wct, container.getTaskId());
211     }
212 
213     /**
214      * Creates a new split with the primary activity and an empty secondary container.
215      * @return The newly created secondary container.
216      */
217     @NonNull
createNewSplitWithEmptySideContainer( @onNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @NonNull Intent secondaryIntent, @NonNull SplitPairRule rule, @NonNull SplitAttributes splitAttributes)218     TaskFragmentContainer createNewSplitWithEmptySideContainer(
219             @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity,
220             @NonNull Intent secondaryIntent, @NonNull SplitPairRule rule,
221             @NonNull SplitAttributes splitAttributes) {
222         final TaskProperties taskProperties = getTaskProperties(primaryActivity);
223         final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties,
224                 splitAttributes);
225         final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
226                 primaryActivity, primaryRelBounds, splitAttributes, null /* containerToAvoid */);
227 
228         // Create new empty task fragment
229         final int taskId = primaryContainer.getTaskId();
230         final TaskFragmentContainer secondaryContainer =
231                 new TaskFragmentContainer.Builder(mController, taskId, primaryActivity)
232                         .setPendingAppearedIntent(secondaryIntent).build();
233         final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties,
234                 splitAttributes);
235         final int windowingMode = mController.getTaskContainer(taskId)
236                 .getWindowingModeForTaskFragment(secondaryRelBounds);
237         createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(),
238                 primaryActivity.getActivityToken(), secondaryRelBounds, windowingMode);
239         updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes);
240 
241         // Set adjacent to each other so that the containers below will be invisible.
242         setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule,
243                 splitAttributes);
244 
245         mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule,
246                 splitAttributes);
247 
248         return secondaryContainer;
249     }
250 
251     /**
252      * Creates a new split container with the two provided activities.
253      * @param primaryActivity An activity that should be in the primary container. If it is not
254      *                        currently in an existing container, a new one will be created and the
255      *                        activity will be re-parented to it.
256      * @param secondaryActivity An activity that should be in the secondary container. If it is not
257      *                          currently in an existing container, or if it is currently in the
258      *                          same container as the primary activity, a new container will be
259      *                          created and the activity will be re-parented to it.
260      * @param rule The split rule to be applied to the container.
261      * @param splitAttributes The {@link SplitAttributes} to apply
262      */
createNewSplitContainer(@onNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, @NonNull SplitPairRule rule, @NonNull SplitAttributes splitAttributes)263     void createNewSplitContainer(@NonNull WindowContainerTransaction wct,
264             @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity,
265             @NonNull SplitPairRule rule, @NonNull SplitAttributes splitAttributes) {
266         final TaskProperties taskProperties = getTaskProperties(primaryActivity);
267         final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties,
268                 splitAttributes);
269         final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
270                 primaryActivity, primaryRelBounds, splitAttributes, null /* containerToAvoid */);
271 
272         final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties,
273                 splitAttributes);
274         final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity(
275                 secondaryActivity);
276         TaskFragmentContainer containerToAvoid = primaryContainer;
277         if (curSecondaryContainer != null && curSecondaryContainer != primaryContainer
278                 && (rule.shouldClearTop() || primaryContainer.isAbove(curSecondaryContainer))) {
279             // Do not reuse the current TaskFragment if the rule is to clear top, or if it is below
280             // the primary TaskFragment.
281             containerToAvoid = curSecondaryContainer;
282         }
283         final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct,
284                 secondaryActivity, secondaryRelBounds, splitAttributes, containerToAvoid);
285 
286         // Set adjacent to each other so that the containers below will be invisible.
287         setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule,
288                 splitAttributes);
289 
290         mController.registerSplit(wct, primaryContainer, primaryActivity, secondaryContainer, rule,
291                 splitAttributes);
292     }
293 
294     /**
295      * Creates a new container or resizes an existing container for activity to the provided bounds.
296      * @param activity The activity to be re-parented to the container if necessary.
297      * @param containerToAvoid Re-parent from this container if an activity is already in it.
298      */
prepareContainerForActivity( @onNull WindowContainerTransaction wct, @NonNull Activity activity, @NonNull Rect relBounds, @NonNull SplitAttributes splitAttributes, @Nullable TaskFragmentContainer containerToAvoid)299     private TaskFragmentContainer prepareContainerForActivity(
300             @NonNull WindowContainerTransaction wct, @NonNull Activity activity,
301             @NonNull Rect relBounds, @NonNull SplitAttributes splitAttributes,
302             @Nullable TaskFragmentContainer containerToAvoid) {
303         TaskFragmentContainer container = mController.getContainerWithActivity(activity);
304         final int taskId = container != null ? container.getTaskId() : activity.getTaskId();
305         if (container == null || container == containerToAvoid) {
306             container = new TaskFragmentContainer.Builder(mController, taskId, activity)
307                     .setPendingAppearedActivity(activity).build();
308             final int windowingMode = mController.getTaskContainer(taskId)
309                     .getWindowingModeForTaskFragment(relBounds);
310             final IBinder reparentActivityToken = activity.getActivityToken();
311             createTaskFragment(wct, container.getTaskFragmentToken(), reparentActivityToken,
312                     relBounds, windowingMode, reparentActivityToken);
313             wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(),
314                     reparentActivityToken);
315         } else {
316             resizeTaskFragmentIfRegistered(wct, container, relBounds);
317             final int windowingMode = mController.getTaskContainer(taskId)
318                     .getWindowingModeForTaskFragment(relBounds);
319             updateTaskFragmentWindowingModeIfRegistered(wct, container, windowingMode);
320         }
321         updateAnimationParams(wct, container.getTaskFragmentToken(), splitAttributes);
322 
323         return container;
324     }
325 
326     /**
327      * Starts a new activity to the side, creating a new split container. A new container will be
328      * created for the activity that will be started.
329      * @param launchingActivity An activity that should be in the primary container. If it is not
330      *                          currently in an existing container, a new one will be created and
331      *                          the activity will be re-parented to it.
332      * @param activityIntent    The intent to start the new activity.
333      * @param activityOptions   The options to apply to new activity start.
334      * @param rule              The split rule to be applied to the container.
335      * @param isPlaceholder     Whether the launch is a placeholder.
336      */
startActivityToSide(@onNull WindowContainerTransaction wct, @NonNull Activity launchingActivity, @NonNull Intent activityIntent, @Nullable Bundle activityOptions, @NonNull SplitRule rule, @NonNull SplitAttributes splitAttributes, boolean isPlaceholder)337     void startActivityToSide(@NonNull WindowContainerTransaction wct,
338             @NonNull Activity launchingActivity, @NonNull Intent activityIntent,
339             @Nullable Bundle activityOptions, @NonNull SplitRule rule,
340             @NonNull SplitAttributes splitAttributes, boolean isPlaceholder) {
341         final TaskProperties taskProperties = getTaskProperties(launchingActivity);
342         final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties,
343                 splitAttributes);
344         final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties,
345                 splitAttributes);
346 
347         TaskFragmentContainer primaryContainer = mController.getContainerWithActivity(
348                 launchingActivity);
349         if (primaryContainer == null) {
350             primaryContainer = new TaskFragmentContainer.Builder(mController,
351                     launchingActivity.getTaskId(), launchingActivity)
352                     .setPendingAppearedActivity(launchingActivity).build();
353         }
354 
355         final int taskId = primaryContainer.getTaskId();
356         final TaskFragmentContainer secondaryContainer =
357                 new TaskFragmentContainer.Builder(mController, taskId, launchingActivity)
358                         .setPendingAppearedIntent(activityIntent)
359                         // Pass in the primary container to make sure it is added right above the
360                         // primary.
361                         .setPairedPrimaryContainer(primaryContainer)
362                         .build();
363         final TaskContainer taskContainer = mController.getTaskContainer(taskId);
364         final int windowingMode = taskContainer.getWindowingModeForTaskFragment(
365                 primaryRelBounds);
366         mController.registerSplit(wct, primaryContainer, launchingActivity, secondaryContainer,
367                 rule, splitAttributes);
368         startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRelBounds,
369                 launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRelBounds,
370                 activityIntent, activityOptions, rule, windowingMode, splitAttributes);
371         if (isPlaceholder) {
372             // When placeholder is launched in split, we should keep the focus on the primary.
373             wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken());
374         }
375     }
376 
377     /**
378      * Updates the positions of containers in an existing split.
379      * @param splitContainer The split container to be updated.
380      * @param wct WindowContainerTransaction that this update should be performed with.
381      */
updateSplitContainer(@onNull SplitContainer splitContainer, @NonNull WindowContainerTransaction wct)382     void updateSplitContainer(@NonNull SplitContainer splitContainer,
383             @NonNull WindowContainerTransaction wct) {
384         // Getting the parent configuration using the updated container - it will have the recent
385         // value.
386         final SplitRule rule = splitContainer.getSplitRule();
387         final TaskFragmentContainer primaryContainer = splitContainer.getPrimaryContainer();
388         final TaskContainer taskContainer = splitContainer.getTaskContainer();
389         final TaskProperties taskProperties = taskContainer.getTaskProperties();
390         final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes();
391         final Rect primaryRelBounds = getRelBoundsForPosition(POSITION_START, taskProperties,
392                 splitAttributes);
393         final Rect secondaryRelBounds = getRelBoundsForPosition(POSITION_END, taskProperties,
394                 splitAttributes);
395         final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer();
396         // Whether the placeholder is becoming side-by-side with the primary from fullscreen.
397         final boolean isPlaceholderBecomingSplit = splitContainer.isPlaceholderContainer()
398                 && secondaryContainer.areLastRequestedBoundsEqual(null /* bounds */)
399                 && !secondaryRelBounds.isEmpty();
400 
401         // TODO(b/243518738): remove usages of XXXIfRegistered.
402         // If the task fragments are not registered yet, the positions will be updated after they
403         // are created again.
404         resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRelBounds);
405         resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRelBounds);
406         setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule,
407                 splitAttributes);
408         if (isPlaceholderBecomingSplit) {
409             // When placeholder is shown in split, we should keep the focus on the primary.
410             wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken());
411         }
412         final int windowingMode = taskContainer.getWindowingModeForTaskFragment(
413                 primaryRelBounds);
414         updateTaskFragmentWindowingModeIfRegistered(wct, primaryContainer, windowingMode);
415         updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode);
416         updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes);
417         updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes);
418         mController.updateDivider(wct, taskContainer, false /* isTaskFragmentVanished */);
419     }
420 
setAdjacentTaskFragments(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer, @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule, @NonNull SplitAttributes splitAttributes)421     private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
422             @NonNull TaskFragmentContainer primaryContainer,
423             @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule,
424             @NonNull SplitAttributes splitAttributes) {
425         // Clear adjacent TaskFragments if the container is shown in fullscreen, or the
426         // secondaryContainer could not be finished.
427         boolean isStacked = !shouldShowSplit(splitAttributes);
428         if (isStacked) {
429             clearAdjacentTaskFragments(wct, primaryContainer.getTaskFragmentToken());
430         } else {
431             setAdjacentTaskFragmentsWithRule(wct, primaryContainer.getTaskFragmentToken(),
432                     secondaryContainer.getTaskFragmentToken(), splitRule);
433         }
434         setCompanionTaskFragment(wct, primaryContainer.getTaskFragmentToken(),
435                 secondaryContainer.getTaskFragmentToken(), splitRule, isStacked);
436 
437         // Sets the dim area when the two TaskFragments are adjacent.
438         final boolean dimOnTask = !isStacked
439                 && splitAttributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK;
440         setTaskFragmentDimOnTask(wct, primaryContainer.getTaskFragmentToken(), dimOnTask);
441         setTaskFragmentDimOnTask(wct, secondaryContainer.getTaskFragmentToken(), dimOnTask);
442 
443         // Setting isolated navigation and clear non-sticky pinned container if needed.
444         final SplitPinRule splitPinRule =
445                 splitRule instanceof SplitPinRule ? (SplitPinRule) splitRule : null;
446         if (splitPinRule == null) {
447             return;
448         }
449 
450         setTaskFragmentPinned(wct, secondaryContainer, !isStacked /* pinned */);
451         if (isStacked && !splitPinRule.isSticky()) {
452             secondaryContainer.getTaskContainer().removeSplitPinContainer();
453         }
454     }
455 
456     /**
457      * Sets whether to enable isolated navigation for this {@link TaskFragmentContainer}.
458      * <p>
459      * If a container enables isolated navigation, activities can't be launched to this container
460      * unless explicitly requested to be launched to.
461      *
462      * @see TaskFragmentContainer#isOverlayWithActivityAssociation()
463      */
setTaskFragmentIsolatedNavigation(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, boolean isolatedNavigationEnabled)464     void setTaskFragmentIsolatedNavigation(@NonNull WindowContainerTransaction wct,
465                                            @NonNull TaskFragmentContainer container,
466                                            boolean isolatedNavigationEnabled) {
467         if (container.isIsolatedNavigationEnabled() == isolatedNavigationEnabled) {
468             return;
469         }
470         container.setIsolatedNavigationEnabled(isolatedNavigationEnabled);
471         setTaskFragmentIsolatedNavigation(wct, container.getTaskFragmentToken(),
472                 isolatedNavigationEnabled);
473     }
474 
475     /**
476      * Sets whether to pin this {@link TaskFragmentContainer}.
477      * <p>
478      * If a container is pinned, it won't be chosen as the launch target unless it's the launching
479      * container.
480      *
481      * @see TaskFragmentContainer#isAlwaysOnTopOverlay()
482      * @see TaskContainer#getSplitPinContainer()
483      */
setTaskFragmentPinned(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, boolean pinned)484     void setTaskFragmentPinned(@NonNull WindowContainerTransaction wct,
485                                @NonNull TaskFragmentContainer container,
486                                boolean pinned) {
487         if (container.isPinned() == pinned) {
488             return;
489         }
490         container.setPinned(pinned);
491         setTaskFragmentPinned(wct, container.getTaskFragmentToken(), pinned);
492     }
493 
494     /**
495      * Resizes the task fragment if it was already registered. Skips the operation if the container
496      * creation has not been reported from the server yet.
497      */
498     // TODO(b/190433398): Handle resize if the fragment hasn't appeared yet.
499     @VisibleForTesting
resizeTaskFragmentIfRegistered(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, @Nullable Rect relBounds)500     void resizeTaskFragmentIfRegistered(@NonNull WindowContainerTransaction wct,
501             @NonNull TaskFragmentContainer container,
502             @Nullable Rect relBounds) {
503         if (container.getInfo() == null) {
504             return;
505         }
506         resizeTaskFragment(wct, container.getTaskFragmentToken(), relBounds);
507     }
508 
509     @VisibleForTesting
updateTaskFragmentWindowingModeIfRegistered( @onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, @WindowingMode int windowingMode)510     void updateTaskFragmentWindowingModeIfRegistered(
511             @NonNull WindowContainerTransaction wct,
512             @NonNull TaskFragmentContainer container,
513             @WindowingMode int windowingMode) {
514         if (container.getInfo() != null) {
515             updateWindowingMode(wct, container.getTaskFragmentToken(), windowingMode);
516         }
517     }
518 
519     @Override
createTaskFragment(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentCreationParams fragmentOptions)520     void createTaskFragment(@NonNull WindowContainerTransaction wct,
521             @NonNull TaskFragmentCreationParams fragmentOptions) {
522         final TaskFragmentContainer container = mController.getContainer(
523                 fragmentOptions.getFragmentToken());
524         if (container == null) {
525             throw new IllegalStateException(
526                     "Creating a TaskFragment that is not registered with controller.");
527         }
528 
529         container.setLastRequestedBounds(fragmentOptions.getInitialRelativeBounds());
530         container.setLastRequestedWindowingMode(fragmentOptions.getWindowingMode());
531         super.createTaskFragment(wct, fragmentOptions);
532 
533         // Reorders the pinned TaskFragment to front to ensure it is the front-most TaskFragment.
534         final SplitPinContainer pinnedContainer =
535                 container.getTaskContainer().getSplitPinContainer();
536         if (pinnedContainer != null) {
537             reorderTaskFragmentToFront(wct,
538                     pinnedContainer.getSecondaryContainer().getTaskFragmentToken());
539         }
540         final TaskFragmentContainer alwaysOnTopOverlayContainer = container.getTaskContainer()
541                 .getAlwaysOnTopOverlayContainer();
542         if (alwaysOnTopOverlayContainer != null) {
543             reorderTaskFragmentToFront(wct, alwaysOnTopOverlayContainer.getTaskFragmentToken());
544         }
545     }
546 
547     @Override
resizeTaskFragment(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @Nullable Rect relBounds)548     void resizeTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken,
549             @Nullable Rect relBounds) {
550         TaskFragmentContainer container = mController.getContainer(fragmentToken);
551         if (container == null) {
552             throw new IllegalStateException(
553                     "Resizing a TaskFragment that is not registered with controller.");
554         }
555 
556         if (container.areLastRequestedBoundsEqual(relBounds)) {
557             // Return early if the provided bounds were already requested
558             return;
559         }
560 
561         container.setLastRequestedBounds(relBounds);
562         super.resizeTaskFragment(wct, fragmentToken, relBounds);
563     }
564 
565     @Override
updateWindowingMode(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @WindowingMode int windowingMode)566     void updateWindowingMode(@NonNull WindowContainerTransaction wct,
567             @NonNull IBinder fragmentToken, @WindowingMode int windowingMode) {
568         final TaskFragmentContainer container = mController.getContainer(fragmentToken);
569         if (container == null) {
570             throw new IllegalStateException("Setting windowing mode for a TaskFragment that is"
571                     + " not registered with controller.");
572         }
573 
574         if (container.isLastRequestedWindowingModeEqual(windowingMode)) {
575             // Return early if the windowing mode were already requested
576             return;
577         }
578 
579         container.setLastRequestedWindowingMode(windowingMode);
580         super.updateWindowingMode(wct, fragmentToken, windowingMode);
581     }
582 
583     @Override
updateAnimationParams(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken, @NonNull TaskFragmentAnimationParams animationParams)584     void updateAnimationParams(@NonNull WindowContainerTransaction wct,
585             @NonNull IBinder fragmentToken, @NonNull TaskFragmentAnimationParams animationParams) {
586         final TaskFragmentContainer container = mController.getContainer(fragmentToken);
587         if (container == null) {
588             throw new IllegalStateException("Setting animation params for a TaskFragment that is"
589                     + " not registered with controller.");
590         }
591 
592         if (container.areLastRequestedAnimationParamsEqual(animationParams)) {
593             // Return early if the animation params were already requested
594             return;
595         }
596 
597         container.setLastRequestAnimationParams(animationParams);
598         super.updateAnimationParams(wct, fragmentToken, animationParams);
599     }
600 
601     @Override
setAdjacentTaskFragments(@onNull WindowContainerTransaction wct, @NonNull IBinder primary, @NonNull IBinder secondary, @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams)602     void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
603             @NonNull IBinder primary, @NonNull IBinder secondary,
604             @Nullable WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams) {
605         final TaskFragmentContainer primaryContainer = mController.getContainer(primary);
606         final TaskFragmentContainer secondaryContainer = mController.getContainer(secondary);
607         if (primaryContainer == null || secondaryContainer == null) {
608             throw new IllegalStateException("setAdjacentTaskFragments on TaskFragment that is"
609                     + " not registered with controller.");
610         }
611 
612         if (primaryContainer.isLastAdjacentTaskFragmentEqual(secondary, adjacentParams)
613                 && secondaryContainer.isLastAdjacentTaskFragmentEqual(primary, adjacentParams)) {
614             // Return early if the same adjacent TaskFragments were already requested
615             return;
616         }
617 
618         primaryContainer.setLastAdjacentTaskFragment(secondary, adjacentParams);
619         secondaryContainer.setLastAdjacentTaskFragment(primary, adjacentParams);
620         super.setAdjacentTaskFragments(wct, primary, secondary, adjacentParams);
621     }
622 
623     @Override
clearAdjacentTaskFragments(@onNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken)624     void clearAdjacentTaskFragments(@NonNull WindowContainerTransaction wct,
625             @NonNull IBinder fragmentToken) {
626         final TaskFragmentContainer container = mController.getContainer(fragmentToken);
627         if (container == null) {
628             throw new IllegalStateException("clearAdjacentTaskFragments on TaskFragment that is"
629                     + " not registered with controller.");
630         }
631 
632         if (container.isLastAdjacentTaskFragmentEqual(null /* fragmentToken*/, null /* params */)) {
633             // Return early if no adjacent TaskFragment was yet requested
634             return;
635         }
636 
637         container.clearLastAdjacentTaskFragment();
638         super.clearAdjacentTaskFragments(wct, fragmentToken);
639     }
640 
641     @Override
setCompanionTaskFragment(@onNull WindowContainerTransaction wct, @NonNull IBinder primary, @Nullable IBinder secondary)642     void setCompanionTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder primary,
643                                   @Nullable IBinder secondary) {
644         final TaskFragmentContainer container = mController.getContainer(primary);
645         if (container == null) {
646             throw new IllegalStateException("setCompanionTaskFragment on TaskFragment that is"
647                     + " not registered with controller.");
648         }
649 
650         if (container.isLastCompanionTaskFragmentEqual(secondary)) {
651             // Return early if the same companion TaskFragment was already requested
652             return;
653         }
654 
655         container.setLastCompanionTaskFragment(secondary);
656         super.setCompanionTaskFragment(wct, primary, secondary);
657     }
658 
659     /**
660      * Applies the {@code attributes} to a standalone {@code container}.
661      *
662      * @param minDimensions the minimum dimension of the container.
663      */
applyActivityStackAttributes( @onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, @NonNull ActivityStackAttributes attributes, @Nullable Size minDimensions)664     void applyActivityStackAttributes(
665             @NonNull WindowContainerTransaction wct,
666             @NonNull TaskFragmentContainer container,
667             @NonNull ActivityStackAttributes attributes,
668             @Nullable Size minDimensions) {
669         final Rect relativeBounds = sanitizeBounds(attributes.getRelativeBounds(), minDimensions,
670                 container);
671         final boolean isFillParent = relativeBounds.isEmpty();
672         final boolean dimOnTask = !isFillParent
673                 && attributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK;
674         final IBinder fragmentToken = container.getTaskFragmentToken();
675 
676         if (container.isAlwaysOnTopOverlay()) {
677             setTaskFragmentPinned(wct, container, !isFillParent);
678         } else if (container.isOverlayWithActivityAssociation()) {
679             setTaskFragmentIsolatedNavigation(wct, container, !isFillParent);
680         }
681 
682         // TODO(b/243518738): Update to resizeTaskFragment after we migrate WCT#setRelativeBounds
683         //  and WCT#setWindowingMode to take fragmentToken.
684         resizeTaskFragmentIfRegistered(wct, container, relativeBounds);
685         final TaskContainer taskContainer = container.getTaskContainer();
686         final int windowingMode = taskContainer.getWindowingModeForTaskFragment(relativeBounds);
687         updateTaskFragmentWindowingModeIfRegistered(wct, container, windowingMode);
688         if (container.isOverlay()) {
689             // Use the overlay transition for the overlay container if it's supported.
690             final TaskFragmentAnimationParams params = createOverlayAnimationParams(relativeBounds,
691                     taskContainer.getBounds(), container);
692             updateAnimationParams(wct, fragmentToken, params);
693         } else {
694             // Otherwise, fallabck to use the default animation params.
695             updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT);
696         }
697         setTaskFragmentDimOnTask(wct, fragmentToken, dimOnTask);
698     }
699 
700     @NonNull
createOverlayAnimationParams( @onNull Rect relativeBounds, @NonNull Rect parentContainerBounds, @NonNull TaskFragmentContainer container)701     private static TaskFragmentAnimationParams createOverlayAnimationParams(
702             @NonNull Rect relativeBounds, @NonNull Rect parentContainerBounds,
703             @NonNull TaskFragmentContainer container) {
704         if (relativeBounds.isEmpty()) {
705             return TaskFragmentAnimationParams.DEFAULT;
706         }
707 
708         final int positionFromOptions = container.getLaunchOptions()
709                 .getInt(KEY_ACTIVITY_STACK_ALIGNMENT , -1);
710         final int position = positionFromOptions != -1 ? positionFromOptions
711                 // Fallback to calculate from bounds if the info can't be retrieved from options.
712                 : getOverlayPosition(relativeBounds, parentContainerBounds);
713 
714         return new TaskFragmentAnimationParams.Builder()
715                 .setOpenAnimationResId(getOpenAnimationResourcesId(position))
716                 .setChangeAnimationResId(R.anim.overlay_task_fragment_change)
717                 .setCloseAnimationResId(getCloseAnimationResourcesId(position))
718                 .build();
719     }
720 
721     @VisibleForTesting
722     @ContainerPosition
getOverlayPosition( @onNull Rect relativeBounds, @NonNull Rect parentContainerBounds)723     static int getOverlayPosition(
724             @NonNull Rect relativeBounds, @NonNull Rect parentContainerBounds) {
725         final Rect relativeParentBounds = new Rect(parentContainerBounds);
726         relativeParentBounds.offsetTo(0, 0);
727         final int leftMatch = (relativeParentBounds.left == relativeBounds.left) ? 1 : 0;
728         final int topMatch = (relativeParentBounds.top == relativeBounds.top) ? 1 : 0;
729         final int rightMatch = (relativeParentBounds.right == relativeBounds.right) ? 1 : 0;
730         final int bottomMatch = (relativeParentBounds.bottom == relativeBounds.bottom) ? 1 : 0;
731 
732         // Flag format: {left|top|right|bottom}. Note that overlay container could be shrunk and
733         // centered, which makes only one of overlay container edge matches the parent container.
734         final int directionFlag = (leftMatch << 3) + (topMatch << 2) + (rightMatch << 1)
735                 + bottomMatch;
736 
737         final int position = switch (directionFlag) {
738             // Only the left edge match or only the right edge not match: should be on the left of
739             // the parent container.
740             case 0b1000, 0b1101 -> CONTAINER_POSITION_LEFT;
741             // Only the top edge match or only the bottom edge not match: should be on the top of
742             // the parent container.
743             case 0b0100, 0b1110 -> CONTAINER_POSITION_TOP;
744             // Only the right edge match or only the left edge not match: should be on the right of
745             // the parent container.
746             case 0b0010, 0b0111 -> CONTAINER_POSITION_RIGHT;
747             // Only the bottom edge match or only the top edge not match: should be on the bottom of
748             // the parent container.
749             case 0b0001, 0b1011 -> CONTAINER_POSITION_BOTTOM;
750             default -> {
751                 Log.w(TAG, "Unsupported position:" + Integer.toBinaryString(directionFlag)
752                         + " fallback to treat it as right. Relative parent bounds: "
753                         + relativeParentBounds + ", relative overlay bounds:" + relativeBounds);
754                 yield CONTAINER_POSITION_RIGHT;
755             }
756         };
757         return position;
758     }
759 
760     @AnimRes
getOpenAnimationResourcesId(@ontainerPosition int position)761     private static int getOpenAnimationResourcesId(@ContainerPosition int position) {
762         return switch (position) {
763             case CONTAINER_POSITION_LEFT -> R.anim.overlay_task_fragment_open_from_left;
764             case CONTAINER_POSITION_TOP -> R.anim.overlay_task_fragment_open_from_top;
765             case CONTAINER_POSITION_RIGHT -> R.anim.overlay_task_fragment_open_from_right;
766             case CONTAINER_POSITION_BOTTOM -> R.anim.overlay_task_fragment_open_from_bottom;
767             default -> {
768                 Log.w(TAG, "Unknown position:" + position);
769                 yield Resources.ID_NULL;
770             }
771         };
772     }
773 
774     @AnimRes
getCloseAnimationResourcesId(@ontainerPosition int position)775     private static int getCloseAnimationResourcesId(@ContainerPosition int position) {
776         return switch (position) {
777             case CONTAINER_POSITION_LEFT -> R.anim.overlay_task_fragment_close_to_left;
778             case CONTAINER_POSITION_TOP -> R.anim.overlay_task_fragment_close_to_top;
779             case CONTAINER_POSITION_RIGHT -> R.anim.overlay_task_fragment_close_to_right;
780             case CONTAINER_POSITION_BOTTOM -> R.anim.overlay_task_fragment_close_to_bottom;
781             default -> {
782                 Log.w(TAG, "Unknown position:" + position);
783                 yield Resources.ID_NULL;
784             }
785         };
786     }
787 
788     /**
789      * Returns the expanded bounds if the {@code relBounds} violate minimum dimension or are not
790      * fully covered by the task bounds. Otherwise, returns {@code relBounds}.
791      */
792     @NonNull
793     static Rect sanitizeBounds(@NonNull Rect relBounds, @Nullable Size minDimension,
794                         @NonNull TaskFragmentContainer container) {
795         if (relBounds.isEmpty()) {
796             // Don't need to check if the bounds follows the task bounds.
797             return relBounds;
798         }
799         if (boundsSmallerThanMinDimensions(relBounds, minDimension)) {
800             // Expand the bounds if the bounds are smaller than minimum dimensions.
801             return new Rect();
802         }
803         final TaskContainer taskContainer = container.getTaskContainer();
804         final Rect relTaskBounds = new Rect(taskContainer.getBounds());
805         relTaskBounds.offsetTo(0, 0);
806         if (!relTaskBounds.contains(relBounds)) {
807             // Expand the bounds if the bounds exceed the task bounds.
808             return new Rect();
809         }
810         return relBounds;
811     }
812 
813     @Override
814     void setTaskFragmentDimOnTask(@NonNull WindowContainerTransaction wct,
815             @NonNull IBinder fragmentToken, boolean dimOnTask) {
816         final TaskFragmentContainer container = mController.getContainer(fragmentToken);
817         if (container == null) {
818             throw new IllegalStateException("setTaskFragmentDimOnTask on TaskFragment that is"
819                     + " not registered with controller.");
820         }
821 
822         if (container.isLastDimOnTask() == dimOnTask) {
823             return;
824         }
825 
826         container.setLastDimOnTask(dimOnTask);
827         super.setTaskFragmentDimOnTask(wct, fragmentToken, dimOnTask);
828     }
829 
830     /**
831      * Expands the split container if the current split bounds are smaller than the Activity or
832      * Intent that is added to the container.
833      *
834      * @return the {@link ResultCode} based on
835      * {@link #shouldShowSplit(SplitAttributes)} and if
836      * {@link android.window.TaskFragmentInfo} has reported to the client side.
837      */
838     @ResultCode
839     int expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct,
840             @NonNull SplitContainer splitContainer, @NonNull Activity primaryActivity,
841             @Nullable Activity secondaryActivity, @Nullable Intent secondaryIntent) {
842         if (secondaryActivity == null && secondaryIntent == null) {
843             throw new IllegalArgumentException("Either secondaryActivity or secondaryIntent must be"
844                     + " non-null.");
845         }
846         final Pair<Size, Size> minDimensionsPair;
847         if (secondaryActivity != null) {
848             minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity);
849         } else {
850             minDimensionsPair = getActivityIntentMinDimensionsPair(primaryActivity,
851                     secondaryIntent);
852         }
853         // Expand the splitContainer if minimum dimensions are not satisfied.
854         final TaskContainer taskContainer = splitContainer.getTaskContainer();
855         final SplitAttributes splitAttributes = sanitizeSplitAttributes(
856                 taskContainer.getTaskProperties(), splitContainer.getCurrentSplitAttributes(),
857                 minDimensionsPair);
858         splitContainer.updateCurrentSplitAttributes(splitAttributes);
859         if (!shouldShowSplit(splitAttributes)) {
860             // If the client side hasn't received TaskFragmentInfo yet, we can't change TaskFragment
861             // bounds. Return failure to create a new SplitContainer which fills task bounds.
862             if (splitContainer.getPrimaryContainer().getInfo() == null
863                     || splitContainer.getSecondaryContainer().getInfo() == null) {
864                 return RESULT_EXPAND_FAILED_NO_TF_INFO;
865             }
866             final IBinder primaryToken =
867                     splitContainer.getPrimaryContainer().getTaskFragmentToken();
868             final IBinder secondaryToken =
869                     splitContainer.getSecondaryContainer().getTaskFragmentToken();
870             expandTaskFragment(wct, splitContainer.getPrimaryContainer());
871             expandTaskFragment(wct, splitContainer.getSecondaryContainer());
872             // Set the companion TaskFragment when the two containers stacked.
873             setCompanionTaskFragment(wct, primaryToken, secondaryToken,
874                     splitContainer.getSplitRule(), true /* isStacked */);
875             return RESULT_EXPANDED;
876         }
877         return RESULT_NOT_EXPANDED;
878     }
879 
880     /**
881      * Expands an existing TaskFragment to fill parent.
882      * @param wct WindowContainerTransaction in which the task fragment should be resized.
883      * @param container the {@link TaskFragmentContainer} to be expanded.
884      */
885     void expandTaskFragment(@NonNull WindowContainerTransaction wct,
886             @NonNull TaskFragmentContainer container) {
887         super.expandTaskFragment(wct, container);
888         mController.updateDivider(
889                 wct, container.getTaskContainer(), false /* isTaskFragmentVanished */);
890     }
891 
892     static boolean shouldShowSplit(@NonNull SplitContainer splitContainer) {
893         return shouldShowSplit(splitContainer.getCurrentSplitAttributes());
894     }
895 
896     static boolean shouldShowSplit(@NonNull SplitAttributes splitAttributes) {
897         return !(splitAttributes.getSplitType() instanceof ExpandContainersSplitType);
898     }
899 
900     static boolean shouldShowPlaceholderWhenExpanded(@NonNull SplitAttributes splitAttributes) {
901         // The placeholder should be kept if the expand split type is a result of user dragging
902         // the divider.
903         return SplitAttributesHelper.isDraggableExpandType(splitAttributes);
904     }
905 
906     @NonNull
907     SplitAttributes computeSplitAttributes(@NonNull TaskProperties taskProperties,
908             @NonNull SplitRule rule, @NonNull SplitAttributes defaultSplitAttributes,
909             @Nullable Pair<Size, Size> minDimensionsPair) {
910         final Configuration taskConfiguration = taskProperties.getConfiguration();
911         final WindowMetrics taskWindowMetrics = taskProperties.getTaskMetrics();
912         final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator =
913                 mController.getSplitAttributesCalculator();
914         final boolean areDefaultConstraintsSatisfied = rule.checkParentMetrics(taskWindowMetrics);
915         if (calculator == null) {
916             if (!areDefaultConstraintsSatisfied) {
917                 return EXPAND_CONTAINERS_ATTRIBUTES;
918             }
919             return sanitizeSplitAttributes(taskProperties, defaultSplitAttributes,
920                     minDimensionsPair);
921         }
922         final WindowLayoutInfo windowLayoutInfo = mWindowLayoutComponent
923                 .getCurrentWindowLayoutInfo(taskProperties.getDisplayId(),
924                         taskConfiguration.windowConfiguration);
925         final SplitAttributesCalculatorParams params = new SplitAttributesCalculatorParams(
926                 taskWindowMetrics, taskConfiguration, windowLayoutInfo, defaultSplitAttributes,
927                 areDefaultConstraintsSatisfied, rule.getTag());
928         final SplitAttributes splitAttributes = calculator.apply(params);
929         return sanitizeSplitAttributes(taskProperties, splitAttributes, minDimensionsPair);
930     }
931 
932     /**
933      * Returns {@link #EXPAND_CONTAINERS_ATTRIBUTES} if the passed {@link SplitAttributes} doesn't
934      * meet the minimum dimensions set in {@link ActivityInfo.WindowLayout}. Otherwise, returns
935      * the passed {@link SplitAttributes}.
936      */
937     @NonNull
938     private SplitAttributes sanitizeSplitAttributes(@NonNull TaskProperties taskProperties,
939             @NonNull SplitAttributes splitAttributes,
940             @Nullable Pair<Size, Size> minDimensionsPair) {
941         // Sanitize the DividerAttributes and set default values.
942         if (splitAttributes.getDividerAttributes() != null) {
943             splitAttributes = new SplitAttributes.Builder(splitAttributes)
944                     .setDividerAttributes(
945                             DividerPresenter.sanitizeDividerAttributes(
946                                     splitAttributes.getDividerAttributes())
947                     ).build();
948         }
949 
950         if (minDimensionsPair == null) {
951             return splitAttributes;
952         }
953         final FoldingFeature foldingFeature = getFoldingFeatureForHingeType(
954                 taskProperties, splitAttributes);
955         final Configuration taskConfiguration = taskProperties.getConfiguration();
956         final Rect primaryBounds = getPrimaryBounds(taskConfiguration, splitAttributes,
957                 foldingFeature);
958         final Rect secondaryBounds = getSecondaryBounds(taskConfiguration, splitAttributes,
959                 foldingFeature);
960         if (boundsSmallerThanMinDimensions(primaryBounds, minDimensionsPair.first)
961                 || boundsSmallerThanMinDimensions(secondaryBounds, minDimensionsPair.second)) {
962             return EXPAND_CONTAINERS_ATTRIBUTES;
963         }
964         return splitAttributes;
965     }
966 
967     @NonNull
968     static Pair<Size, Size> getActivitiesMinDimensionsPair(
969             @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) {
970         return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryActivity));
971     }
972 
973     @NonNull
974     static Pair<Size, Size> getActivityIntentMinDimensionsPair(@NonNull Activity primaryActivity,
975             @NonNull Intent secondaryIntent) {
976         return new Pair<>(getMinDimensions(primaryActivity), getMinDimensions(secondaryIntent));
977     }
978 
979     @Nullable
980     static Size getMinDimensions(@Nullable Activity activity) {
981         if (activity == null) {
982             return null;
983         }
984         final ActivityInfo.WindowLayout windowLayout = activity.getActivityInfo().windowLayout;
985         if (windowLayout == null) {
986             return null;
987         }
988         return new Size(windowLayout.minWidth, windowLayout.minHeight);
989     }
990 
991     // TODO(b/232871351): find a light-weight approach for this check.
992     @Nullable
993     static Size getMinDimensions(@Nullable Intent intent) {
994         if (intent == null) {
995             return null;
996         }
997         final PackageManager packageManager = ActivityThread.currentActivityThread()
998                 .getApplication().getPackageManager();
999         final ResolveInfo resolveInfo = packageManager.resolveActivity(intent,
1000                 PackageManager.ResolveInfoFlags.of(MATCH_ALL));
1001         if (resolveInfo == null) {
1002             return null;
1003         }
1004         final ActivityInfo activityInfo = resolveInfo.activityInfo;
1005         if (activityInfo == null) {
1006             return null;
1007         }
1008         final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout;
1009         if (windowLayout == null) {
1010             return null;
1011         }
1012         return new Size(windowLayout.minWidth, windowLayout.minHeight);
1013     }
1014 
1015     static boolean boundsSmallerThanMinDimensions(@NonNull Rect bounds,
1016             @Nullable Size minDimensions) {
1017         if (minDimensions == null) {
1018             return false;
1019         }
1020         // Empty bounds mean the bounds follow the parent host task's bounds. Skip the check.
1021         if (bounds.isEmpty()) {
1022             return false;
1023         }
1024         return bounds.width() < minDimensions.getWidth()
1025                 || bounds.height() < minDimensions.getHeight();
1026     }
1027 
1028     @VisibleForTesting
1029     @NonNull
1030     Rect getRelBoundsForPosition(@Position int position, @NonNull TaskProperties taskProperties,
1031             @NonNull SplitAttributes splitAttributes) {
1032         final Configuration taskConfiguration = taskProperties.getConfiguration();
1033         final FoldingFeature foldingFeature = getFoldingFeatureForHingeType(
1034                 taskProperties, splitAttributes);
1035         if (!shouldShowSplit(splitAttributes)) {
1036             return new Rect();
1037         }
1038         final Rect bounds;
1039         switch (position) {
1040             case POSITION_START:
1041                 bounds = getPrimaryBounds(taskConfiguration, splitAttributes, foldingFeature);
1042                 break;
1043             case POSITION_END:
1044                 bounds = getSecondaryBounds(taskConfiguration, splitAttributes, foldingFeature);
1045                 break;
1046             case POSITION_FILL:
1047             default:
1048                 bounds = new Rect();
1049         }
1050         // Convert to relative bounds in parent coordinate. This is to avoid flicker when the Task
1051         // resized before organizer requests have been applied.
1052         taskProperties.translateAbsoluteBoundsToRelativeBounds(bounds);
1053         return bounds;
1054     }
1055 
1056     @NonNull
1057     private Rect getPrimaryBounds(@NonNull Configuration taskConfiguration,
1058             @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
1059         final SplitAttributes computedSplitAttributes = updateSplitAttributesType(splitAttributes,
1060                 computeSplitType(splitAttributes, taskConfiguration, foldingFeature));
1061         if (!shouldShowSplit(computedSplitAttributes)) {
1062             return new Rect();
1063         }
1064         switch (computedSplitAttributes.getLayoutDirection()) {
1065             case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: {
1066                 return getLeftContainerBounds(taskConfiguration, computedSplitAttributes,
1067                         foldingFeature);
1068             }
1069             case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: {
1070                 return getRightContainerBounds(taskConfiguration, computedSplitAttributes,
1071                         foldingFeature);
1072             }
1073             case SplitAttributes.LayoutDirection.LOCALE: {
1074                 final boolean isLtr = taskConfiguration.getLayoutDirection()
1075                         == View.LAYOUT_DIRECTION_LTR;
1076                 return isLtr
1077                         ? getLeftContainerBounds(taskConfiguration, computedSplitAttributes,
1078                                 foldingFeature)
1079                         : getRightContainerBounds(taskConfiguration, computedSplitAttributes,
1080                                 foldingFeature);
1081             }
1082             case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: {
1083                 return getTopContainerBounds(taskConfiguration, computedSplitAttributes,
1084                         foldingFeature);
1085             }
1086             case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: {
1087                 return getBottomContainerBounds(taskConfiguration, computedSplitAttributes,
1088                         foldingFeature);
1089             }
1090             default:
1091                 throw new IllegalArgumentException("Unknown layout direction:"
1092                         + computedSplitAttributes.getLayoutDirection());
1093         }
1094     }
1095 
1096     @NonNull
1097     private Rect getSecondaryBounds(@NonNull Configuration taskConfiguration,
1098             @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
1099         final SplitAttributes computedSplitAttributes = updateSplitAttributesType(splitAttributes,
1100                 computeSplitType(splitAttributes, taskConfiguration, foldingFeature));
1101         if (!shouldShowSplit(computedSplitAttributes)) {
1102             return new Rect();
1103         }
1104         switch (computedSplitAttributes.getLayoutDirection()) {
1105             case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: {
1106                 return getRightContainerBounds(taskConfiguration, computedSplitAttributes,
1107                         foldingFeature);
1108             }
1109             case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: {
1110                 return getLeftContainerBounds(taskConfiguration, computedSplitAttributes,
1111                         foldingFeature);
1112             }
1113             case SplitAttributes.LayoutDirection.LOCALE: {
1114                 final boolean isLtr = taskConfiguration.getLayoutDirection()
1115                         == View.LAYOUT_DIRECTION_LTR;
1116                 return isLtr
1117                         ? getRightContainerBounds(taskConfiguration, computedSplitAttributes,
1118                                 foldingFeature)
1119                         : getLeftContainerBounds(taskConfiguration, computedSplitAttributes,
1120                                 foldingFeature);
1121             }
1122             case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: {
1123                 return getBottomContainerBounds(taskConfiguration, computedSplitAttributes,
1124                         foldingFeature);
1125             }
1126             case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: {
1127                 return getTopContainerBounds(taskConfiguration, computedSplitAttributes,
1128                         foldingFeature);
1129             }
1130             default:
1131                 throw new IllegalArgumentException("Unknown layout direction:"
1132                         + splitAttributes.getLayoutDirection());
1133         }
1134     }
1135 
1136     /**
1137      * Returns the {@link SplitAttributes} that update the {@link SplitType} to
1138      * {@code splitTypeToUpdate}.
1139      */
1140     private static SplitAttributes updateSplitAttributesType(
1141             @NonNull SplitAttributes splitAttributes, @NonNull SplitType splitTypeToUpdate) {
1142         return new SplitAttributes.Builder(splitAttributes)
1143                 .setSplitType(splitTypeToUpdate)
1144                 .build();
1145     }
1146 
1147     @NonNull
1148     private Rect getLeftContainerBounds(@NonNull Configuration taskConfiguration,
1149             @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
1150         final int dividerOffset = getBoundsOffsetForDivider(
1151                 splitAttributes, CONTAINER_POSITION_LEFT);
1152         final int right = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes,
1153                 CONTAINER_POSITION_LEFT, foldingFeature) + dividerOffset;
1154         final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds();
1155         return new Rect(taskBounds.left, taskBounds.top, right, taskBounds.bottom);
1156     }
1157 
1158     @NonNull
1159     private Rect getRightContainerBounds(@NonNull Configuration taskConfiguration,
1160             @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
1161         final int dividerOffset = getBoundsOffsetForDivider(
1162                 splitAttributes, CONTAINER_POSITION_RIGHT);
1163         final int left = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes,
1164                 CONTAINER_POSITION_RIGHT, foldingFeature) + dividerOffset;
1165         final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds();
1166         return new Rect(left, parentBounds.top, parentBounds.right, parentBounds.bottom);
1167     }
1168 
1169     @NonNull
1170     private Rect getTopContainerBounds(@NonNull Configuration taskConfiguration,
1171             @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
1172         final int dividerOffset = getBoundsOffsetForDivider(
1173                 splitAttributes, CONTAINER_POSITION_TOP);
1174         final int bottom = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes,
1175                 CONTAINER_POSITION_TOP, foldingFeature) + dividerOffset;
1176         final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds();
1177         return new Rect(parentBounds.left, parentBounds.top, parentBounds.right, bottom);
1178     }
1179 
1180     @NonNull
1181     private Rect getBottomContainerBounds(@NonNull Configuration taskConfiguration,
1182             @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) {
1183         final int dividerOffset = getBoundsOffsetForDivider(
1184                 splitAttributes, CONTAINER_POSITION_BOTTOM);
1185         final int top = computeBoundaryBetweenContainers(taskConfiguration, splitAttributes,
1186                 CONTAINER_POSITION_BOTTOM, foldingFeature) + dividerOffset;
1187         final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds();
1188         return new Rect(parentBounds.left, top, parentBounds.right, parentBounds.bottom);
1189     }
1190 
1191     /**
1192      * Computes the boundary position between the primary and the secondary containers for the given
1193      * {@link ContainerPosition} with {@link SplitAttributes}, current window and device states.
1194      * <ol>
1195      *     <li>For {@link #CONTAINER_POSITION_TOP}, it computes the boundary with the bottom
1196      *       container, which is {@link Rect#bottom} of the top container bounds.</li>
1197      *     <li>For {@link #CONTAINER_POSITION_BOTTOM}, it computes the boundary with the top
1198      *       container, which is {@link Rect#top} of the bottom container bounds.</li>
1199      *     <li>For {@link #CONTAINER_POSITION_LEFT}, it computes the boundary with the right
1200      *       container, which is {@link Rect#right} of the left container bounds.</li>
1201      *     <li>For {@link #CONTAINER_POSITION_RIGHT}, it computes the boundary with the bottom
1202      *       container, which is {@link Rect#left} of the right container bounds.</li>
1203      * </ol>
1204      *
1205      * @see #getTopContainerBounds(Configuration, SplitAttributes, FoldingFeature)
1206      * @see #getBottomContainerBounds(Configuration, SplitAttributes, FoldingFeature)
1207      * @see #getLeftContainerBounds(Configuration, SplitAttributes, FoldingFeature)
1208      * @see #getRightContainerBounds(Configuration, SplitAttributes, FoldingFeature)
1209      */
1210     private int computeBoundaryBetweenContainers(@NonNull Configuration taskConfiguration,
1211             @NonNull SplitAttributes splitAttributes, @ContainerPosition int position,
1212             @Nullable FoldingFeature foldingFeature) {
1213         final Rect parentBounds = taskConfiguration.windowConfiguration.getBounds();
1214         final int startPoint = shouldSplitHorizontally(splitAttributes)
1215                 ? parentBounds.top
1216                 : parentBounds.left;
1217         final int dimen = shouldSplitHorizontally(splitAttributes)
1218                 ? parentBounds.height()
1219                 : parentBounds.width();
1220         final SplitType splitType = splitAttributes.getSplitType();
1221         if (splitType instanceof RatioSplitType) {
1222             final RatioSplitType splitRatio = (RatioSplitType) splitType;
1223             return (int) (startPoint + dimen * splitRatio.getRatio());
1224         }
1225         // At this point, SplitType must be a HingeSplitType and foldingFeature must be
1226         // non-null. RatioSplitType and ExpandContainerSplitType have been handled earlier.
1227         Objects.requireNonNull(foldingFeature);
1228         if (!(splitType instanceof HingeSplitType)) {
1229             throw new IllegalArgumentException("Unknown splitType:" + splitType);
1230         }
1231         final Rect hingeArea = foldingFeature.getBounds();
1232         switch (position) {
1233             case CONTAINER_POSITION_LEFT:
1234                 return hingeArea.left;
1235             case CONTAINER_POSITION_TOP:
1236                 return hingeArea.top;
1237             case CONTAINER_POSITION_RIGHT:
1238                 return hingeArea.right;
1239             case CONTAINER_POSITION_BOTTOM:
1240                 return hingeArea.bottom;
1241             default:
1242                 throw new IllegalArgumentException("Unknown position:" + position);
1243         }
1244     }
1245 
1246     @Nullable
1247     private FoldingFeature getFoldingFeatureForHingeType(
1248             @NonNull TaskProperties taskProperties,
1249             @NonNull SplitAttributes splitAttributes) {
1250         SplitType splitType = splitAttributes.getSplitType();
1251         if (!(splitType instanceof HingeSplitType)) {
1252             return null;
1253         }
1254         return getFoldingFeature(taskProperties);
1255     }
1256 
1257     @Nullable
1258     @VisibleForTesting
1259     FoldingFeature getFoldingFeature(@NonNull TaskProperties taskProperties) {
1260         final int displayId = taskProperties.getDisplayId();
1261         final WindowConfiguration windowConfiguration = taskProperties.getConfiguration()
1262                 .windowConfiguration;
1263         final WindowLayoutInfo info = mWindowLayoutComponent
1264                 .getCurrentWindowLayoutInfo(displayId, windowConfiguration);
1265         final List<DisplayFeature> displayFeatures = info.getDisplayFeatures();
1266         if (displayFeatures.isEmpty()) {
1267             return null;
1268         }
1269         final List<FoldingFeature> foldingFeatures = new ArrayList<>();
1270         for (DisplayFeature displayFeature : displayFeatures) {
1271             if (displayFeature instanceof FoldingFeature) {
1272                 foldingFeatures.add((FoldingFeature) displayFeature);
1273             }
1274         }
1275         // TODO(b/240219484): Support device with multiple hinges.
1276         if (foldingFeatures.size() != 1) {
1277             return null;
1278         }
1279         return foldingFeatures.get(0);
1280     }
1281 
1282     /**
1283      * Indicates that this {@link SplitAttributes} splits the task horizontally. Returns
1284      * {@code false} if this {@link SplitAttributes} splits the task vertically.
1285      */
1286     private static boolean shouldSplitHorizontally(SplitAttributes splitAttributes) {
1287         switch (splitAttributes.getLayoutDirection()) {
1288             case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM:
1289             case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP:
1290                 return true;
1291             default:
1292                 return false;
1293         }
1294     }
1295 
1296     /**
1297      * Computes the {@link SplitType} with the {@link SplitAttributes} and the current device and
1298      * window state.
1299      * If passed {@link SplitAttributes#getSplitType} is a {@link RatioSplitType}. It reversed
1300      * the ratio if the computed {@link SplitAttributes#getLayoutDirection} is
1301      * {@link SplitAttributes.LayoutDirection.LEFT_TO_RIGHT} or
1302      * {@link SplitAttributes.LayoutDirection.BOTTOM_TO_TOP} to make the bounds calculation easier.
1303      * If passed {@link SplitAttributes#getSplitType} is a {@link HingeSplitType}, it checks
1304      * the current device and window states to determine whether the split container should split
1305      * by hinge or use {@link HingeSplitType#getFallbackSplitType}.
1306      */
1307     private SplitType computeSplitType(@NonNull SplitAttributes splitAttributes,
1308             @NonNull Configuration taskConfiguration, @Nullable FoldingFeature foldingFeature) {
1309         final SplitType splitType = splitAttributes.getSplitType();
1310         if (splitType instanceof ExpandContainersSplitType) {
1311             return splitType;
1312         } else if (splitType instanceof RatioSplitType) {
1313             final RatioSplitType splitRatio = (RatioSplitType) splitType;
1314             // Reverse the ratio for RIGHT_TO_LEFT and BOTTOM_TO_TOP to make the boundary
1315             // computation have the same direction, which is from (top, left) to (bottom, right).
1316             final SplitType reversedSplitType = new RatioSplitType(1 - splitRatio.getRatio());
1317             return isReversedLayout(splitAttributes, taskConfiguration)
1318                     ? reversedSplitType
1319                     : splitType;
1320         } else if (splitType instanceof HingeSplitType) {
1321             final HingeSplitType hinge = (HingeSplitType) splitType;
1322             @WindowingMode
1323             final int windowingMode = taskConfiguration.windowConfiguration.getWindowingMode();
1324             return shouldSplitByHinge(splitAttributes, foldingFeature, windowingMode)
1325                     ? hinge : hinge.getFallbackSplitType();
1326         }
1327         throw new IllegalArgumentException("Unknown SplitType:" + splitType);
1328     }
1329 
1330     private static boolean shouldSplitByHinge(@NonNull SplitAttributes splitAttributes,
1331             @Nullable FoldingFeature foldingFeature, @WindowingMode int taskWindowingMode) {
1332         // Only HingeSplitType may split the task bounds by hinge.
1333         if (!(splitAttributes.getSplitType() instanceof HingeSplitType)) {
1334             return false;
1335         }
1336         // Device is not foldable, so there's no hinge to match.
1337         if (foldingFeature == null) {
1338             return false;
1339         }
1340         // The task is in multi-window mode. Match hinge doesn't make sense because current task
1341         // bounds may not fit display bounds.
1342         if (WindowConfiguration.inMultiWindowMode(taskWindowingMode)) {
1343             return false;
1344         }
1345         // Return true if how the split attributes split the task bounds matches the orientation of
1346         // folding area orientation.
1347         return shouldSplitHorizontally(splitAttributes) == isFoldingAreaHorizontal(foldingFeature);
1348     }
1349 
1350     private static boolean isFoldingAreaHorizontal(@NonNull FoldingFeature foldingFeature) {
1351         final Rect bounds = foldingFeature.getBounds();
1352         return bounds.width() > bounds.height();
1353     }
1354 
1355     @NonNull
1356     TaskProperties getTaskProperties(@NonNull Activity activity) {
1357         final TaskContainer taskContainer = mController.getTaskContainer(
1358                 mController.getTaskId(activity));
1359         if (taskContainer != null) {
1360             return taskContainer.getTaskProperties();
1361         }
1362         return TaskProperties.getTaskPropertiesFromActivity(activity);
1363     }
1364 
1365     @NonNull
1366     WindowMetrics getTaskWindowMetrics(@NonNull Activity activity) {
1367         return getTaskProperties(activity).getTaskMetrics();
1368     }
1369 
1370     @NonNull
1371     ParentContainerInfo createParentContainerInfoFromTaskProperties(
1372             @NonNull TaskProperties taskProperties) {
1373         final Configuration configuration = taskProperties.getConfiguration();
1374         final WindowLayoutInfo windowLayoutInfo = mWindowLayoutComponent
1375                 .getCurrentWindowLayoutInfo(taskProperties.getDisplayId(),
1376                         configuration.windowConfiguration);
1377         return new ParentContainerInfo(taskProperties.getTaskMetrics(), configuration,
1378                 windowLayoutInfo);
1379     }
1380 
1381     @VisibleForTesting
1382     @NonNull
1383     static String positionToString(@ContainerPosition int position) {
1384         return switch (position) {
1385             case CONTAINER_POSITION_LEFT -> "left";
1386             case CONTAINER_POSITION_TOP -> "top";
1387             case CONTAINER_POSITION_RIGHT -> "right";
1388             case CONTAINER_POSITION_BOTTOM -> "bottom";
1389             default -> "Unknown position:" + position;
1390         };
1391     }
1392 }
1393