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 androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior;
20 import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior;
21 import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceholderRule;
22 import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent;
23 import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked;
24 
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.app.Activity;
28 import android.app.ActivityClient;
29 import android.app.ActivityOptions;
30 import android.app.ActivityThread;
31 import android.app.Application.ActivityLifecycleCallbacks;
32 import android.app.Instrumentation;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.content.res.Configuration;
36 import android.graphics.Rect;
37 import android.os.Bundle;
38 import android.os.Handler;
39 import android.os.IBinder;
40 import android.os.Looper;
41 import android.window.TaskFragmentInfo;
42 import android.window.WindowContainerTransaction;
43 
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.Set;
47 import java.util.concurrent.Executor;
48 import java.util.function.Consumer;
49 
50 /**
51  * Main controller class that manages split states and presentation.
52  */
53 public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback,
54         ActivityEmbeddingComponent {
55 
56     private final SplitPresenter mPresenter;
57 
58     // Currently applied split configuration.
59     private final List<EmbeddingRule> mSplitRules = new ArrayList<>();
60     private final List<TaskFragmentContainer> mContainers = new ArrayList<>();
61     private final List<SplitContainer> mSplitContainers = new ArrayList<>();
62 
63     // Callback to Jetpack to notify about changes to split states.
64     private @NonNull Consumer<List<SplitInfo>> mEmbeddingCallback;
65     private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>();
66 
67     // We currently only support split activity embedding within the one root Task.
68     private final Rect mParentBounds = new Rect();
69 
SplitController()70     public SplitController() {
71         mPresenter = new SplitPresenter(new MainThreadExecutor(), this);
72         ActivityThread activityThread = ActivityThread.currentActivityThread();
73         // Register a callback to be notified about activities being created.
74         activityThread.getApplication().registerActivityLifecycleCallbacks(
75                 new LifecycleCallbacks());
76         // Intercept activity starts to route activities to new containers if necessary.
77         Instrumentation instrumentation = activityThread.getInstrumentation();
78         instrumentation.addMonitor(new ActivityStartMonitor());
79     }
80 
81     /** Updates the embedding rules applied to future activity launches. */
82     @Override
setEmbeddingRules(@onNull Set<EmbeddingRule> rules)83     public void setEmbeddingRules(@NonNull Set<EmbeddingRule> rules) {
84         mSplitRules.clear();
85         mSplitRules.addAll(rules);
86         updateAnimationOverride();
87     }
88 
89     @NonNull
getSplitRules()90     public List<EmbeddingRule> getSplitRules() {
91         return mSplitRules;
92     }
93 
94     /**
95      * Starts an activity to side of the launchingActivity with the provided split config.
96      */
startActivityToSide(@onNull Activity launchingActivity, @NonNull Intent intent, @Nullable Bundle options, @NonNull SplitRule sideRule, @Nullable Consumer<Exception> failureCallback)97     public void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent,
98             @Nullable Bundle options, @NonNull SplitRule sideRule,
99             @Nullable Consumer<Exception> failureCallback) {
100         try {
101             mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule);
102         } catch (Exception e) {
103             if (failureCallback != null) {
104                 failureCallback.accept(e);
105             }
106         }
107     }
108 
109     /**
110      * Registers the split organizer callback to notify about changes to active splits.
111      */
112     @Override
setSplitInfoCallback(@onNull Consumer<List<SplitInfo>> callback)113     public void setSplitInfoCallback(@NonNull Consumer<List<SplitInfo>> callback) {
114         mEmbeddingCallback = callback;
115         updateCallbackIfNecessary();
116     }
117 
118     @Override
onTaskFragmentAppeared(@onNull TaskFragmentInfo taskFragmentInfo)119     public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) {
120         TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
121         if (container == null) {
122             return;
123         }
124 
125         container.setInfo(taskFragmentInfo);
126         if (container.isFinished()) {
127             mPresenter.cleanupContainer(container, false /* shouldFinishDependent */);
128         }
129         updateCallbackIfNecessary();
130     }
131 
132     @Override
onTaskFragmentInfoChanged(@onNull TaskFragmentInfo taskFragmentInfo)133     public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
134         TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
135         if (container == null) {
136             return;
137         }
138 
139         container.setInfo(taskFragmentInfo);
140         // Check if there are no running activities - consider the container empty if there are no
141         // non-finishing activities left.
142         if (!taskFragmentInfo.hasRunningActivity()) {
143             // Do not finish the dependents if this TaskFragment was cleared due to launching
144             // activity in the Task.
145             final boolean shouldFinishDependent =
146                     !taskFragmentInfo.isTaskClearedForReuse();
147             mPresenter.cleanupContainer(container, shouldFinishDependent);
148         }
149         updateCallbackIfNecessary();
150     }
151 
152     @Override
onTaskFragmentVanished(@onNull TaskFragmentInfo taskFragmentInfo)153     public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
154         TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
155         if (container == null) {
156             return;
157         }
158 
159         mPresenter.cleanupContainer(container, true /* shouldFinishDependent */);
160         updateCallbackIfNecessary();
161     }
162 
163     @Override
onTaskFragmentParentInfoChanged(@onNull IBinder fragmentToken, @NonNull Configuration parentConfig)164     public void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken,
165             @NonNull Configuration parentConfig) {
166         onParentBoundsMayChange(parentConfig.windowConfiguration.getBounds());
167         TaskFragmentContainer container = getContainer(fragmentToken);
168         if (container != null) {
169             mPresenter.updateContainer(container);
170             updateCallbackIfNecessary();
171         }
172     }
173 
onParentBoundsMayChange(Activity activity)174     private void onParentBoundsMayChange(Activity activity) {
175         if (activity.isFinishing()) {
176             return;
177         }
178 
179         onParentBoundsMayChange(mPresenter.getParentContainerBounds(activity));
180     }
181 
onParentBoundsMayChange(Rect parentBounds)182     private void onParentBoundsMayChange(Rect parentBounds) {
183         if (!parentBounds.isEmpty() && !mParentBounds.equals(parentBounds)) {
184             mParentBounds.set(parentBounds);
185             updateAnimationOverride();
186         }
187     }
188 
189     /**
190      * Updates if we should override transition animation. We only want to override if the Task
191      * bounds is large enough for at least one split rule.
192      */
updateAnimationOverride()193     private void updateAnimationOverride() {
194         if (mParentBounds.isEmpty()) {
195             // We don't know about the parent bounds yet.
196             return;
197         }
198 
199         // Check if the parent container bounds can support any split rule.
200         boolean supportSplit = false;
201         for (EmbeddingRule rule : mSplitRules) {
202             if (!(rule instanceof SplitRule)) {
203                 continue;
204             }
205             if (mPresenter.shouldShowSideBySide(mParentBounds, (SplitRule) rule)) {
206                 supportSplit = true;
207                 break;
208             }
209         }
210 
211         // We only want to override if it supports split.
212         if (supportSplit) {
213             mPresenter.startOverrideSplitAnimation();
214         } else {
215             mPresenter.stopOverrideSplitAnimation();
216         }
217     }
218 
onActivityCreated(@onNull Activity launchedActivity)219     void onActivityCreated(@NonNull Activity launchedActivity) {
220         handleActivityCreated(launchedActivity);
221         updateCallbackIfNecessary();
222     }
223 
224     /**
225      * Checks if the activity start should be routed to a particular container. It can create a new
226      * container for the activity and a new split container if necessary.
227      */
228     // TODO(b/190433398): Break down into smaller functions.
handleActivityCreated(@onNull Activity launchedActivity)229     void handleActivityCreated(@NonNull Activity launchedActivity) {
230         final List<EmbeddingRule> splitRules = getSplitRules();
231         final TaskFragmentContainer currentContainer = getContainerWithActivity(
232                 launchedActivity.getActivityToken());
233 
234         if (currentContainer == null) {
235             // Initial check before any TaskFragment is created.
236             onParentBoundsMayChange(launchedActivity);
237         }
238 
239         // Check if the activity is configured to always be expanded.
240         if (shouldExpand(launchedActivity, null, splitRules)) {
241             if (shouldContainerBeExpanded(currentContainer)) {
242                 // Make sure that the existing container is expanded
243                 mPresenter.expandTaskFragment(currentContainer.getTaskFragmentToken());
244             } else {
245                 // Put activity into a new expanded container
246                 final TaskFragmentContainer newContainer = newContainer(launchedActivity);
247                 mPresenter.expandActivity(newContainer.getTaskFragmentToken(),
248                         launchedActivity);
249             }
250             return;
251         }
252 
253         // Check if activity requires a placeholder
254         if (launchPlaceholderIfNecessary(launchedActivity)) {
255             return;
256         }
257 
258         // TODO(b/190433398): Check if it is a placeholder and there is already another split
259         // created by the primary activity. This is necessary for the case when the primary activity
260         // launched another secondary in the split, but the placeholder was still launched by the
261         // logic above. We didn't prevent the placeholder launcher because we didn't know that
262         // another secondary activity is coming up.
263 
264         // Check if the activity should form a split with the activity below in the same task
265         // fragment.
266         Activity activityBelow = null;
267         if (currentContainer != null) {
268             final List<Activity> containerActivities = currentContainer.collectActivities();
269             final int index = containerActivities.indexOf(launchedActivity);
270             if (index > 0) {
271                 activityBelow = containerActivities.get(index - 1);
272             }
273         }
274         if (activityBelow == null) {
275             IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow(
276                     launchedActivity.getActivityToken());
277             if (belowToken != null) {
278                 activityBelow = ActivityThread.currentActivityThread().getActivity(belowToken);
279             }
280         }
281         if (activityBelow == null) {
282             return;
283         }
284 
285         // Check if the split is already set.
286         final TaskFragmentContainer activityBelowContainer = getContainerWithActivity(
287                 activityBelow.getActivityToken());
288         if (currentContainer != null && activityBelowContainer != null) {
289             final SplitContainer existingSplit = getActiveSplitForContainers(currentContainer,
290                     activityBelowContainer);
291             if (existingSplit != null) {
292                 // There is already an active split with the activity below.
293                 return;
294             }
295         }
296 
297         final SplitPairRule splitPairRule = getSplitRule(activityBelow, launchedActivity,
298                 splitRules);
299         if (splitPairRule == null) {
300             return;
301         }
302 
303         mPresenter.createNewSplitContainer(activityBelow, launchedActivity,
304                 splitPairRule);
305     }
306 
onActivityConfigurationChanged(@onNull Activity activity)307     private void onActivityConfigurationChanged(@NonNull Activity activity) {
308         final TaskFragmentContainer currentContainer = getContainerWithActivity(
309                 activity.getActivityToken());
310 
311         if (currentContainer != null) {
312             // Changes to activities in controllers are handled in
313             // onTaskFragmentParentInfoChanged
314             return;
315         }
316         // The bounds of the container may have been changed.
317         onParentBoundsMayChange(activity);
318 
319         // Check if activity requires a placeholder
320         launchPlaceholderIfNecessary(activity);
321     }
322 
323     /**
324      * Returns a container that this activity is registered with. An activity can only belong to one
325      * container, or no container at all.
326      */
327     @Nullable
getContainerWithActivity(@onNull IBinder activityToken)328     TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) {
329         for (TaskFragmentContainer container : mContainers) {
330             if (container.hasActivity(activityToken)) {
331                 return container;
332             }
333         }
334 
335         return null;
336     }
337 
338     /**
339      * Creates and registers a new organized container with an optional activity that will be
340      * re-parented to it in a WCT.
341      */
newContainer(@ullable Activity activity)342     TaskFragmentContainer newContainer(@Nullable Activity activity) {
343         TaskFragmentContainer container = new TaskFragmentContainer(activity);
344         mContainers.add(container);
345         return container;
346     }
347 
348     /**
349      * Creates and registers a new split with the provided containers and configuration. Finishes
350      * existing secondary containers if found for the given primary container.
351      */
registerSplit(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule)352     void registerSplit(@NonNull WindowContainerTransaction wct,
353             @NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity,
354             @NonNull TaskFragmentContainer secondaryContainer,
355             @NonNull SplitRule splitRule) {
356         SplitContainer splitContainer = new SplitContainer(primaryContainer, primaryActivity,
357                 secondaryContainer, splitRule);
358         // Remove container later to prevent pinning escaping toast showing in lock task mode.
359         if (splitRule instanceof SplitPairRule && ((SplitPairRule) splitRule).shouldClearTop()) {
360             removeExistingSecondaryContainers(wct, primaryContainer);
361         }
362         mSplitContainers.add(splitContainer);
363     }
364 
365     /**
366      * Removes the container from bookkeeping records.
367      */
removeContainer(@onNull TaskFragmentContainer container)368     void removeContainer(@NonNull TaskFragmentContainer container) {
369         // Remove all split containers that included this one
370         mContainers.remove(container);
371         List<SplitContainer> containersToRemove = new ArrayList<>();
372         for (SplitContainer splitContainer : mSplitContainers) {
373             if (container.equals(splitContainer.getSecondaryContainer())
374                     || container.equals(splitContainer.getPrimaryContainer())) {
375                 containersToRemove.add(splitContainer);
376             }
377         }
378         mSplitContainers.removeAll(containersToRemove);
379     }
380 
381     /**
382      * Removes a secondary container for the given primary container if an existing split is
383      * already registered.
384      */
removeExistingSecondaryContainers(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer primaryContainer)385     void removeExistingSecondaryContainers(@NonNull WindowContainerTransaction wct,
386             @NonNull TaskFragmentContainer primaryContainer) {
387         // If the primary container was already in a split - remove the secondary container that
388         // is now covered by the new one that replaced it.
389         final SplitContainer existingSplitContainer = getActiveSplitForContainer(
390                 primaryContainer);
391         if (existingSplitContainer == null
392                 || primaryContainer == existingSplitContainer.getSecondaryContainer()) {
393             return;
394         }
395 
396         existingSplitContainer.getSecondaryContainer().finish(
397                 false /* shouldFinishDependent */, mPresenter, wct, this);
398     }
399 
400     /**
401      * Returns the topmost not finished container.
402      */
403     @Nullable
getTopActiveContainer()404     TaskFragmentContainer getTopActiveContainer() {
405         for (int i = mContainers.size() - 1; i >= 0; i--) {
406             TaskFragmentContainer container = mContainers.get(i);
407             if (!container.isFinished() && container.getTopNonFinishingActivity() != null) {
408                 return container;
409             }
410         }
411         return null;
412     }
413 
414     /**
415      * Updates the presentation of the container. If the container is part of the split or should
416      * have a placeholder, it will also update the other part of the split.
417      */
updateContainer(@onNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container)418     void updateContainer(@NonNull WindowContainerTransaction wct,
419             @NonNull TaskFragmentContainer container) {
420         if (launchPlaceholderIfNecessary(container)) {
421             // Placeholder was launched, the positions will be updated when the activity is added
422             // to the secondary container.
423             return;
424         }
425         if (shouldContainerBeExpanded(container)) {
426             if (container.getInfo() != null) {
427                 mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken());
428             }
429             // If the info is not available yet the task fragment will be expanded when it's ready
430             return;
431         }
432         SplitContainer splitContainer = getActiveSplitForContainer(container);
433         if (splitContainer == null) {
434             return;
435         }
436         if (splitContainer != mSplitContainers.get(mSplitContainers.size() - 1)) {
437             // Skip position update - it isn't the topmost split.
438             return;
439         }
440         if (splitContainer.getPrimaryContainer().isEmpty()
441                 || splitContainer.getSecondaryContainer().isEmpty()) {
442             // Skip position update - one or both containers are empty.
443             return;
444         }
445         if (dismissPlaceholderIfNecessary(splitContainer)) {
446             // Placeholder was finished, the positions will be updated when its container is emptied
447             return;
448         }
449         mPresenter.updateSplitContainer(splitContainer, container, wct);
450     }
451 
452     /**
453      * Returns the top active split container that has the provided container, if available.
454      */
455     @Nullable
getActiveSplitForContainer(@onNull TaskFragmentContainer container)456     private SplitContainer getActiveSplitForContainer(@NonNull TaskFragmentContainer container) {
457         for (int i = mSplitContainers.size() - 1; i >= 0; i--) {
458             SplitContainer splitContainer = mSplitContainers.get(i);
459             if (container.equals(splitContainer.getSecondaryContainer())
460                     || container.equals(splitContainer.getPrimaryContainer())) {
461                 return splitContainer;
462             }
463         }
464         return null;
465     }
466 
467     /**
468      * Returns the active split that has the provided containers as primary and secondary or as
469      * secondary and primary, if available.
470      */
471     @Nullable
getActiveSplitForContainers( @onNull TaskFragmentContainer firstContainer, @NonNull TaskFragmentContainer secondContainer)472     private SplitContainer getActiveSplitForContainers(
473             @NonNull TaskFragmentContainer firstContainer,
474             @NonNull TaskFragmentContainer secondContainer) {
475         for (int i = mSplitContainers.size() - 1; i >= 0; i--) {
476             SplitContainer splitContainer = mSplitContainers.get(i);
477             final TaskFragmentContainer primary = splitContainer.getPrimaryContainer();
478             final TaskFragmentContainer secondary = splitContainer.getSecondaryContainer();
479             if ((firstContainer == secondary && secondContainer == primary)
480                     || (firstContainer == primary && secondContainer == secondary)) {
481                 return splitContainer;
482             }
483         }
484         return null;
485     }
486 
487     /**
488      * Checks if the container requires a placeholder and launches it if necessary.
489      */
launchPlaceholderIfNecessary(@onNull TaskFragmentContainer container)490     private boolean launchPlaceholderIfNecessary(@NonNull TaskFragmentContainer container) {
491         final Activity topActivity = container.getTopNonFinishingActivity();
492         if (topActivity == null) {
493             return false;
494         }
495 
496         return launchPlaceholderIfNecessary(topActivity);
497     }
498 
launchPlaceholderIfNecessary(@onNull Activity activity)499     boolean launchPlaceholderIfNecessary(@NonNull Activity activity) {
500         final  TaskFragmentContainer container = getContainerWithActivity(
501                 activity.getActivityToken());
502 
503         SplitContainer splitContainer = container != null ? getActiveSplitForContainer(container)
504                 : null;
505         if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) {
506             // Don't launch placeholder in primary split container
507             return false;
508         }
509 
510         // Check if there is enough space for launch
511         final SplitPlaceholderRule placeholderRule = getPlaceholderRule(activity);
512         if (placeholderRule == null || !mPresenter.shouldShowSideBySide(
513                 mPresenter.getParentContainerBounds(activity), placeholderRule)) {
514             return false;
515         }
516 
517         // TODO(b/190433398): Handle failed request
518         startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), null,
519                 placeholderRule, null);
520         return true;
521     }
522 
dismissPlaceholderIfNecessary(@onNull SplitContainer splitContainer)523     private boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) {
524         if (!splitContainer.isPlaceholderContainer()) {
525             return false;
526         }
527 
528         if (isStickyPlaceholderRule(splitContainer.getSplitRule())) {
529             // The placeholder should remain after it was first shown.
530             return false;
531         }
532 
533         if (mPresenter.shouldShowSideBySide(splitContainer)) {
534             return false;
535         }
536 
537         mPresenter.cleanupContainer(splitContainer.getSecondaryContainer(),
538                 false /* shouldFinishDependent */);
539         return true;
540     }
541 
542     /**
543      * Returns the rule to launch a placeholder for the activity with the provided component name
544      * if it is configured in the split config.
545      */
getPlaceholderRule(@onNull Activity activity)546     private SplitPlaceholderRule getPlaceholderRule(@NonNull Activity activity) {
547         for (EmbeddingRule rule : mSplitRules) {
548             if (!(rule instanceof SplitPlaceholderRule)) {
549                 continue;
550             }
551             SplitPlaceholderRule placeholderRule = (SplitPlaceholderRule) rule;
552             if (placeholderRule.matchesActivity(activity)) {
553                 return placeholderRule;
554             }
555         }
556         return null;
557     }
558 
559     /**
560      * Notifies listeners about changes to split states if necessary.
561      */
updateCallbackIfNecessary()562     private void updateCallbackIfNecessary() {
563         if (mEmbeddingCallback == null) {
564             return;
565         }
566         if (!allActivitiesCreated()) {
567             return;
568         }
569         List<SplitInfo> currentSplitStates = getActiveSplitStates();
570         if (currentSplitStates == null || mLastReportedSplitStates.equals(currentSplitStates)) {
571             return;
572         }
573         mLastReportedSplitStates.clear();
574         mLastReportedSplitStates.addAll(currentSplitStates);
575         mEmbeddingCallback.accept(currentSplitStates);
576     }
577 
578     /**
579      * @return a list of descriptors for currently active split states. If the value returned is
580      * null, that indicates that the active split states are in an intermediate state and should
581      * not be reported.
582      */
583     @Nullable
getActiveSplitStates()584     private List<SplitInfo> getActiveSplitStates() {
585         List<SplitInfo> splitStates = new ArrayList<>();
586         for (SplitContainer container : mSplitContainers) {
587             if (container.getPrimaryContainer().isEmpty()
588                     || container.getSecondaryContainer().isEmpty()) {
589                 // We are in an intermediate state because either the split container is about to be
590                 // removed or the primary or secondary container are about to receive an activity.
591                 return null;
592             }
593             ActivityStack primaryContainer = container.getPrimaryContainer().toActivityStack();
594             ActivityStack secondaryContainer = container.getSecondaryContainer().toActivityStack();
595             SplitInfo splitState = new SplitInfo(primaryContainer,
596                     secondaryContainer,
597                     // Splits that are not showing side-by-side are reported as having 0 split
598                     // ratio, since by definition in the API the primary container occupies no
599                     // width of the split when covered by the secondary.
600                     mPresenter.shouldShowSideBySide(container)
601                             ? container.getSplitRule().getSplitRatio()
602                             : 0.0f);
603             splitStates.add(splitState);
604         }
605         return splitStates;
606     }
607 
608     /**
609      * Checks if all activities that are registered with the containers have already appeared in
610      * the client.
611      */
allActivitiesCreated()612     private boolean allActivitiesCreated() {
613         for (TaskFragmentContainer container : mContainers) {
614             if (container.getInfo() == null
615                     || container.getInfo().getActivities().size()
616                     != container.collectActivities().size()) {
617                 return false;
618             }
619         }
620         return true;
621     }
622 
623     /**
624      * Returns {@code true} if the container is expanded to occupy full task size.
625      * Returns {@code false} if the container is included in an active split.
626      */
shouldContainerBeExpanded(@ullable TaskFragmentContainer container)627     boolean shouldContainerBeExpanded(@Nullable TaskFragmentContainer container) {
628         if (container == null) {
629             return false;
630         }
631         for (SplitContainer splitContainer : mSplitContainers) {
632             if (container.equals(splitContainer.getPrimaryContainer())
633                     || container.equals(splitContainer.getSecondaryContainer())) {
634                 return false;
635             }
636         }
637         return true;
638     }
639 
640     /**
641      * Returns a split rule for the provided pair of primary activity and secondary activity intent
642      * if available.
643      */
644     @Nullable
getSplitRule(@onNull Activity primaryActivity, @NonNull Intent secondaryActivityIntent, @NonNull List<EmbeddingRule> splitRules)645     private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity,
646             @NonNull Intent secondaryActivityIntent, @NonNull List<EmbeddingRule> splitRules) {
647         for (EmbeddingRule rule : splitRules) {
648             if (!(rule instanceof SplitPairRule)) {
649                 continue;
650             }
651             SplitPairRule pairRule = (SplitPairRule) rule;
652             if (pairRule.matchesActivityIntentPair(primaryActivity, secondaryActivityIntent)) {
653                 return pairRule;
654             }
655         }
656         return null;
657     }
658 
659     /**
660      * Returns a split rule for the provided pair of primary and secondary activities if available.
661      */
662     @Nullable
getSplitRule(@onNull Activity primaryActivity, @NonNull Activity secondaryActivity, @NonNull List<EmbeddingRule> splitRules)663     private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity,
664             @NonNull Activity secondaryActivity, @NonNull List<EmbeddingRule> splitRules) {
665         for (EmbeddingRule rule : splitRules) {
666             if (!(rule instanceof SplitPairRule)) {
667                 continue;
668             }
669             SplitPairRule pairRule = (SplitPairRule) rule;
670             final Intent intent = secondaryActivity.getIntent();
671             if (pairRule.matchesActivityPair(primaryActivity, secondaryActivity)
672                     && (intent == null
673                     || pairRule.matchesActivityIntentPair(primaryActivity, intent))) {
674                 return pairRule;
675             }
676         }
677         return null;
678     }
679 
680     @Nullable
getContainer(@onNull IBinder fragmentToken)681     TaskFragmentContainer getContainer(@NonNull IBinder fragmentToken) {
682         for (TaskFragmentContainer container : mContainers) {
683             if (container.getTaskFragmentToken().equals(fragmentToken)) {
684                 return container;
685             }
686         }
687         return null;
688     }
689 
690     /**
691      * Returns {@code true} if an Activity with the provided component name should always be
692      * expanded to occupy full task bounds. Such activity must not be put in a split.
693      */
shouldExpand(@ullable Activity activity, @Nullable Intent intent, List<EmbeddingRule> splitRules)694     private static boolean shouldExpand(@Nullable Activity activity, @Nullable Intent intent,
695             List<EmbeddingRule> splitRules) {
696         if (splitRules == null) {
697             return false;
698         }
699         for (EmbeddingRule rule : splitRules) {
700             if (!(rule instanceof ActivityRule)) {
701                 continue;
702             }
703             ActivityRule activityRule = (ActivityRule) rule;
704             if (!activityRule.shouldAlwaysExpand()) {
705                 continue;
706             }
707             if (activity != null && activityRule.matchesActivity(activity)) {
708                 return true;
709             } else if (intent != null && activityRule.matchesIntent(intent)) {
710                 return true;
711             }
712         }
713         return false;
714     }
715 
716     /**
717      * Checks whether the associated container should be destroyed together with a finishing
718      * container. There is a case when primary containers for placeholders should be retained
719      * despite the rule configuration to finish primary with secondary - if they are marked as
720      * 'sticky' and the placeholder was finished when fully overlapping the primary container.
721      * @return {@code true} if the associated container should be retained (and not be finished).
722      */
shouldRetainAssociatedContainer(@onNull TaskFragmentContainer finishingContainer, @NonNull TaskFragmentContainer associatedContainer)723     boolean shouldRetainAssociatedContainer(@NonNull TaskFragmentContainer finishingContainer,
724             @NonNull TaskFragmentContainer associatedContainer) {
725         SplitContainer splitContainer = getActiveSplitForContainers(associatedContainer,
726                 finishingContainer);
727         if (splitContainer == null) {
728             // Containers are not in the same split, no need to retain.
729             return false;
730         }
731         // Find the finish behavior for the associated container
732         int finishBehavior;
733         SplitRule splitRule = splitContainer.getSplitRule();
734         if (finishingContainer == splitContainer.getPrimaryContainer()) {
735             finishBehavior = getFinishSecondaryWithPrimaryBehavior(splitRule);
736         } else {
737             finishBehavior = getFinishPrimaryWithSecondaryBehavior(splitRule);
738         }
739         // Decide whether the associated container should be retained based on the current
740         // presentation mode.
741         if (mPresenter.shouldShowSideBySide(splitContainer)) {
742             return !shouldFinishAssociatedContainerWhenAdjacent(finishBehavior);
743         } else {
744             return !shouldFinishAssociatedContainerWhenStacked(finishBehavior);
745         }
746     }
747 
748     /**
749      * @see #shouldRetainAssociatedContainer(TaskFragmentContainer, TaskFragmentContainer)
750      */
shouldRetainAssociatedActivity(@onNull TaskFragmentContainer finishingContainer, @NonNull Activity associatedActivity)751     boolean shouldRetainAssociatedActivity(@NonNull TaskFragmentContainer finishingContainer,
752             @NonNull Activity associatedActivity) {
753         TaskFragmentContainer associatedContainer = getContainerWithActivity(
754                 associatedActivity.getActivityToken());
755         if (associatedContainer == null) {
756             return false;
757         }
758 
759         return shouldRetainAssociatedContainer(finishingContainer, associatedContainer);
760     }
761 
762     private final class LifecycleCallbacks implements ActivityLifecycleCallbacks {
763 
764         @Override
onActivityCreated(Activity activity, Bundle savedInstanceState)765         public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
766         }
767 
768         @Override
onActivityPostCreated(Activity activity, Bundle savedInstanceState)769         public void onActivityPostCreated(Activity activity, Bundle savedInstanceState) {
770             // Calling after Activity#onCreate is complete to allow the app launch something
771             // first. In case of a configured placeholder activity we want to make sure
772             // that we don't launch it if an activity itself already requested something to be
773             // launched to side.
774             SplitController.this.onActivityCreated(activity);
775         }
776 
777         @Override
onActivityStarted(Activity activity)778         public void onActivityStarted(Activity activity) {
779         }
780 
781         @Override
onActivityResumed(Activity activity)782         public void onActivityResumed(Activity activity) {
783         }
784 
785         @Override
onActivityPaused(Activity activity)786         public void onActivityPaused(Activity activity) {
787         }
788 
789         @Override
onActivityStopped(Activity activity)790         public void onActivityStopped(Activity activity) {
791         }
792 
793         @Override
onActivitySaveInstanceState(Activity activity, Bundle outState)794         public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
795         }
796 
797         @Override
onActivityDestroyed(Activity activity)798         public void onActivityDestroyed(Activity activity) {
799         }
800 
801         @Override
onActivityConfigurationChanged(Activity activity)802         public void onActivityConfigurationChanged(Activity activity) {
803             SplitController.this.onActivityConfigurationChanged(activity);
804         }
805     }
806 
807     /** Executor that posts on the main application thread. */
808     private static class MainThreadExecutor implements Executor {
809         private final Handler mHandler = new Handler(Looper.getMainLooper());
810 
811         @Override
execute(Runnable r)812         public void execute(Runnable r) {
813             mHandler.post(r);
814         }
815     }
816 
817     /**
818      * A monitor that intercepts all activity start requests originating in the client process and
819      * can amend them to target a specific task fragment to form a split.
820      */
821     private class ActivityStartMonitor extends Instrumentation.ActivityMonitor {
822 
823         @Override
onStartActivity(@onNull Context who, @NonNull Intent intent, @NonNull Bundle options)824         public Instrumentation.ActivityResult onStartActivity(@NonNull Context who,
825                 @NonNull Intent intent, @NonNull Bundle options) {
826             // TODO(b/190433398): Check if the activity is configured to always be expanded.
827 
828             // Check if activity should be put in a split with the activity that launched it.
829             if (!(who instanceof Activity)) {
830                 return super.onStartActivity(who, intent, options);
831             }
832             final Activity launchingActivity = (Activity) who;
833 
834             if (shouldExpand(null, intent, getSplitRules())) {
835                 setLaunchingInExpandedContainer(launchingActivity, options);
836             } else if (!setLaunchingToSideContainer(launchingActivity, intent, options)) {
837                 setLaunchingInSameContainer(launchingActivity, intent, options);
838             }
839 
840             return super.onStartActivity(who, intent, options);
841         }
842 
setLaunchingInExpandedContainer(Activity launchingActivity, Bundle options)843         private void setLaunchingInExpandedContainer(Activity launchingActivity, Bundle options) {
844             TaskFragmentContainer newContainer = mPresenter.createNewExpandedContainer(
845                     launchingActivity);
846 
847             // Amend the request to let the WM know that the activity should be placed in the
848             // dedicated container.
849             options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
850                     newContainer.getTaskFragmentToken());
851         }
852 
853         /**
854          * Returns {@code true} if the activity that is going to be started via the
855          * {@code intent} should be paired with the {@code launchingActivity} and is set to be
856          * launched in an empty side container.
857          */
setLaunchingToSideContainer(Activity launchingActivity, Intent intent, Bundle options)858         private boolean setLaunchingToSideContainer(Activity launchingActivity, Intent intent,
859                 Bundle options) {
860             final SplitPairRule splitPairRule = getSplitRule(launchingActivity, intent,
861                     getSplitRules());
862             if (splitPairRule == null) {
863                 return false;
864             }
865 
866             // Create a new split with an empty side container
867             final TaskFragmentContainer secondaryContainer = mPresenter
868                     .createNewSplitWithEmptySideContainer(launchingActivity, splitPairRule);
869 
870             // Amend the request to let the WM know that the activity should be placed in the
871             // dedicated container.
872             options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
873                     secondaryContainer.getTaskFragmentToken());
874             return true;
875         }
876 
877         /**
878          * Checks if the activity that is going to be started via the {@code intent} should be
879          * paired with the existing top activity which is currently paired with the
880          * {@code launchingActivity}. If so, set the activity to be launched in the same
881          * container of the {@code launchingActivity}.
882          */
setLaunchingInSameContainer(Activity launchingActivity, Intent intent, Bundle options)883         private void setLaunchingInSameContainer(Activity launchingActivity, Intent intent,
884                 Bundle options) {
885             final TaskFragmentContainer launchingContainer = getContainerWithActivity(
886                     launchingActivity.getActivityToken());
887             if (launchingContainer == null) {
888                 return;
889             }
890 
891             final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer);
892             if (splitContainer == null) {
893                 return;
894             }
895 
896             if (splitContainer.getSecondaryContainer() != launchingContainer) {
897                 return;
898             }
899 
900             // The launching activity is on the secondary container. Retrieve the primary
901             // activity from the other container.
902             Activity primaryActivity =
903                     splitContainer.getPrimaryContainer().getTopNonFinishingActivity();
904             if (primaryActivity == null) {
905                 return;
906             }
907 
908             final SplitPairRule splitPairRule = getSplitRule(primaryActivity, intent,
909                     getSplitRules());
910             if (splitPairRule == null) {
911                 return;
912             }
913 
914             // Amend the request to let the WM know that the activity should be placed in the
915             // dedicated container. This is necessary for the case that the activity is started
916             // into a new Task, or new Task will be escaped from the current host Task and be
917             // displayed in fullscreen.
918             options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
919                     launchingContainer.getTaskFragmentToken());
920         }
921     }
922 
923     /**
924      * Checks if an activity is embedded and its presentation is customized by a
925      * {@link android.window.TaskFragmentOrganizer} to only occupy a portion of Task bounds.
926      */
isActivityEmbedded(@onNull Activity activity)927     public boolean isActivityEmbedded(@NonNull Activity activity) {
928         return mPresenter.isActivityEmbedded(activity.getActivityToken());
929     }
930 }
931