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