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