• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 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 com.android.quickstep.util;
18 
19 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP;
20 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM;
21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTED_SECOND_APP;
22 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_COMPLETE;
23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME;
24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_INITIATED;
25 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
26 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
27 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
28 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_PENDINGINTENT;
29 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_TASK;
30 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SHORTCUT_TASK;
31 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SINGLE_INTENT_FULLSCREEN;
32 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SINGLE_SHORTCUT_FULLSCREEN;
33 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SINGLE_TASK_FULLSCREEN;
34 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_PENDINGINTENT;
35 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_SHORTCUT;
36 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_TASK;
37 import static com.android.wm.shell.shared.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT;
38 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50;
39 
40 import android.animation.Animator;
41 import android.animation.AnimatorListenerAdapter;
42 import android.annotation.NonNull;
43 import android.annotation.UiThread;
44 import android.app.ActivityManager;
45 import android.app.ActivityOptions;
46 import android.app.ActivityThread;
47 import android.app.PendingIntent;
48 import android.content.Intent;
49 import android.content.pm.PackageManager;
50 import android.content.pm.ShortcutInfo;
51 import android.graphics.Rect;
52 import android.graphics.RectF;
53 import android.graphics.drawable.Drawable;
54 import android.os.Bundle;
55 import android.os.IBinder;
56 import android.os.RemoteException;
57 import android.os.UserHandle;
58 import android.util.Log;
59 import android.util.Pair;
60 import android.view.SurfaceControl;
61 import android.view.View;
62 import android.window.IRemoteTransitionFinishedCallback;
63 import android.window.RemoteTransition;
64 import android.window.RemoteTransitionStub;
65 import android.window.TransitionInfo;
66 
67 import androidx.annotation.Nullable;
68 import androidx.annotation.VisibleForTesting;
69 
70 import com.android.internal.logging.InstanceId;
71 import com.android.launcher3.R;
72 import com.android.launcher3.anim.PendingAnimation;
73 import com.android.launcher3.apppairs.AppPairIcon;
74 import com.android.launcher3.icons.IconProvider;
75 import com.android.launcher3.logging.StatsLogManager;
76 import com.android.launcher3.model.data.ItemInfo;
77 import com.android.launcher3.statehandlers.DepthController;
78 import com.android.launcher3.statemanager.StateManager;
79 import com.android.launcher3.taskbar.LauncherTaskbarUIController;
80 import com.android.launcher3.testing.TestLogging;
81 import com.android.launcher3.testing.shared.TestProtocol;
82 import com.android.launcher3.uioverrides.QuickstepLauncher;
83 import com.android.launcher3.util.BackPressHandler;
84 import com.android.launcher3.util.ComponentKey;
85 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition;
86 import com.android.quickstep.OverviewComponentObserver;
87 import com.android.quickstep.RecentsAnimationCallbacks;
88 import com.android.quickstep.RecentsAnimationController;
89 import com.android.quickstep.RecentsAnimationTargets;
90 import com.android.quickstep.RecentsModel;
91 import com.android.quickstep.SplitSelectionListener;
92 import com.android.quickstep.SystemUiProxy;
93 import com.android.quickstep.views.FloatingTaskView;
94 import com.android.quickstep.views.GroupedTaskView;
95 import com.android.quickstep.views.RecentsView;
96 import com.android.quickstep.views.RecentsViewContainer;
97 import com.android.quickstep.views.SplitInstructionsView;
98 import com.android.systemui.shared.recents.model.Task;
99 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
100 import com.android.systemui.shared.system.QuickStepContract;
101 import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition;
102 import com.android.wm.shell.splitscreen.ISplitSelectListener;
103 
104 import java.io.PrintWriter;
105 import java.util.ArrayList;
106 import java.util.Arrays;
107 import java.util.List;
108 import java.util.Optional;
109 import java.util.function.Consumer;
110 
111 /**
112  * Represent data needed for the transient state when user has selected one app for split screen
113  * and is in the process of either a) selecting a second app or b) exiting intention to invoke split
114  */
115 public class SplitSelectStateController {
116     private static final String TAG = "SplitSelectStateCtor";
117 
118     private RecentsViewContainer mContainer;
119     private final RecentsModel mRecentTasksModel;
120     @Nullable
121     private Runnable mActivityBackCallback;
122     private final SplitAnimationController mSplitAnimationController;
123     private final AppPairsController mAppPairsController;
124     private final SplitSelectDataHolder mSplitSelectDataHolder;
125     private final StatsLogManager mStatsLogManager;
126     private final SystemUiProxy mSystemUiProxy;
127     private final StateManager mStateManager;
128     @Nullable
129     private SplitFromDesktopController mSplitFromDesktopController;
130     @Nullable
131     private DepthController mDepthController;
132     private boolean mRecentsAnimationRunning;
133     /** If {@code true}, animates the existing task view split placeholder view */
134     private boolean mAnimateCurrentTaskDismissal;
135     /**
136      * Acts as a subset of {@link #mAnimateCurrentTaskDismissal}, we can't be dismissing from a
137      * split pair task view without wanting to animate current task dismissal overall
138      */
139     private boolean mDismissingFromSplitPair;
140     /** If not null, this is the TaskView we want to launch from */
141     @Nullable
142     private GroupedTaskView mLaunchingTaskView;
143     /** If not null, this is the icon we want to launch from */
144     private AppPairIcon mLaunchingIconView;
145 
146     /** True when the first selected split app is being launched in fullscreen. */
147     private boolean mLaunchingFirstAppFullscreen;
148 
149     /**
150      * Should be a constant from {@link com.android.internal.jank.Cuj} or -1, does not need to be
151      * set for all launches. Used in conjunction with {@link #mLaunchingViewCuj} below.
152      */
153     private int mLaunchCuj = -1;
154     private View mLaunchingViewCuj;
155 
156     private FloatingTaskView mFirstFloatingTaskView;
157     private SplitInstructionsView mSplitInstructionsView;
158 
159     private final List<SplitSelectionListener> mSplitSelectionListeners = new ArrayList<>();
160     /**
161      * Tracks metrics from when first app is selected to split launch or cancellation. This also
162      * gets passed over to shell when attempting to invoke split.
163      */
164     private Pair<InstanceId, com.android.launcher3.logging.InstanceId> mSessionInstanceIds;
165 
166     private boolean mIsDestroyed = false;
167 
168     private final BackPressHandler mSplitBackHandler = new BackPressHandler() {
169         @Override
170         public boolean canHandleBack() {
171             return isSplitSelectActive();
172         }
173 
174         @Override
175         public void onBackInvoked() {
176             // When exiting from split selection, leave current context to go to
177             // homescreen as well
178             getSplitAnimationController().playPlaceholderDismissAnim(mContainer,
179                     LAUNCHER_SPLIT_SELECTION_EXIT_HOME);
180             if (mActivityBackCallback != null) {
181                 mActivityBackCallback.run();
182             }
183         }
184     };
185 
SplitSelectStateController(RecentsViewContainer container, StateManager stateManager, DepthController depthController, StatsLogManager statsLogManager, SystemUiProxy systemUiProxy, RecentsModel recentsModel, Runnable activityBackCallback)186     public SplitSelectStateController(RecentsViewContainer container,
187             StateManager stateManager, DepthController depthController,
188             StatsLogManager statsLogManager, SystemUiProxy systemUiProxy, RecentsModel recentsModel,
189             Runnable activityBackCallback) {
190         mContainer = container;
191         mStatsLogManager = statsLogManager;
192         mSystemUiProxy = systemUiProxy;
193         mStateManager = stateManager;
194         mDepthController = depthController;
195         mRecentTasksModel = recentsModel;
196         mActivityBackCallback = activityBackCallback;
197         mSplitAnimationController = new SplitAnimationController(this);
198         mAppPairsController = new AppPairsController(mContainer, this, statsLogManager);
199         mSplitSelectDataHolder = new SplitSelectDataHolder(mContainer.asContext());
200     }
201 
onDestroy()202     public void onDestroy() {
203         mContainer = null;
204         mIsDestroyed = true;
205         mActivityBackCallback = null;
206         mAppPairsController.onDestroy();
207         mSplitSelectDataHolder.onDestroy();
208         if (mSplitFromDesktopController != null) {
209             mSplitFromDesktopController.onDestroy();
210         }
211     }
212 
213     /**
214      * @param alreadyRunningTask if set to {@link android.app.ActivityTaskManager#INVALID_TASK_ID}
215      *                           then @param intent will be used to launch the initial task
216      * @param intent will be ignored if @param alreadyRunningTask is set
217      */
setInitialTaskSelect(@ullable Intent intent, @StagePosition int stagePosition, @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent, int alreadyRunningTask)218     public void setInitialTaskSelect(@Nullable Intent intent, @StagePosition int stagePosition,
219             @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent,
220             int alreadyRunningTask) {
221         mSplitSelectDataHolder.setInitialTaskSelect(intent, stagePosition, itemInfo, splitEvent,
222                 alreadyRunningTask);
223         createAndLogInstanceIdsForSession();
224     }
225 
226     /**
227      * To be called after first task selected from using a split shortcut from the fullscreen
228      * running app.
229      */
setInitialTaskSelect(ActivityManager.RunningTaskInfo info, @StagePosition int stagePosition, @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent)230     public void setInitialTaskSelect(ActivityManager.RunningTaskInfo info,
231             @StagePosition int stagePosition, @NonNull ItemInfo itemInfo,
232             StatsLogManager.EventEnum splitEvent) {
233         mSplitSelectDataHolder.setInitialTaskSelect(info, stagePosition, itemInfo, splitEvent);
234         createAndLogInstanceIdsForSession();
235     }
236 
237     /**
238      * Given a list of task keys, searches through active Tasks in RecentsModel to find the last
239      * active instances of these tasks. Returns an empty array if there is no such running task.
240      *
241      * @param componentKeys The list of ComponentKeys to search for.
242      * @param callback The callback that will be executed on the list of found tasks.
243      * @param findExactPairMatch If {@code true}, only finds tasks that contain BOTH of the wanted
244      *                           tasks (i.e. searching for a running pair of tasks.)
245      */
findLastActiveTasksAndRunCallback(@ullable List<ComponentKey> componentKeys, boolean findExactPairMatch, Consumer<Task[]> callback)246     public void findLastActiveTasksAndRunCallback(@Nullable List<ComponentKey> componentKeys,
247             boolean findExactPairMatch, Consumer<Task[]> callback) {
248         mRecentTasksModel.getTasks(taskGroups -> {
249             if (componentKeys == null || componentKeys.isEmpty()) {
250                 callback.accept(new Task[]{});
251                 return;
252             }
253 
254             Task[] lastActiveTasks = new Task[componentKeys.size()];
255 
256             if (findExactPairMatch) {
257                 // Loop through tasks in reverse, since they are ordered with most-recent tasks last
258                 for (int i = taskGroups.size() - 1; i >= 0; i--) {
259                     GroupTask groupTask = taskGroups.get(i);
260                     if (isInstanceOfAppPair(
261                             groupTask, componentKeys.get(0), componentKeys.get(1))) {
262                         lastActiveTasks[0] = ((SplitTask) groupTask).getTopLeftTask();
263                         break;
264                     }
265                 }
266             } else {
267                 // For each key we are looking for, add to lastActiveTasks with the corresponding
268                 // Task (or do nothing if not found).
269                 for (int i = 0; i < componentKeys.size(); i++) {
270                     ComponentKey key = componentKeys.get(i);
271                     Task lastActiveTask = null;
272                     // Loop through tasks in reverse, since they are ordered with recent tasks last
273                     for (int j = taskGroups.size() - 1; j >= 0; j--) {
274                         GroupTask groupTask = taskGroups.get(j);
275                         // Account for desktop cases where there can be N tasks in the group
276                         for (Task task : groupTask.getTasks()) {
277                             if (isInstanceOfComponent(task, key)
278                                     && !Arrays.asList(lastActiveTasks).contains(task)) {
279                                 lastActiveTask = task;
280                                 break;
281                             }
282                         }
283                         if (lastActiveTask != null) {
284                             break;
285                         }
286                     }
287 
288                     lastActiveTasks[i] = lastActiveTask;
289                 }
290             }
291 
292             callback.accept(lastActiveTasks);
293         });
294     }
295 
296     /**
297      * Checks if a given Task is the most recently-active Task of type componentName. Used for
298      * selecting already-running Tasks for splitscreen.
299      */
isInstanceOfComponent(@ullable Task task, @NonNull ComponentKey componentKey)300     public boolean isInstanceOfComponent(@Nullable Task task, @NonNull ComponentKey componentKey) {
301         // Exclude the task that is already staged
302         if (task == null || task.key.id == mSplitSelectDataHolder.getInitialTaskId()) {
303             return false;
304         }
305 
306         return task.key.baseIntent.getComponent().equals(componentKey.componentName)
307                 && task.key.userId == componentKey.user.getIdentifier();
308     }
309 
310     /**
311      * Checks if a given GroupTask is a pair of apps that matches two given ComponentKeys. We check
312      * both permutations because task order is not guaranteed in GroupTasks.
313      */
isInstanceOfAppPair(GroupTask groupTask, @NonNull ComponentKey componentKey1, @NonNull ComponentKey componentKey2)314     public boolean isInstanceOfAppPair(GroupTask groupTask, @NonNull ComponentKey componentKey1,
315             @NonNull ComponentKey componentKey2) {
316         if (groupTask instanceof SplitTask splitTask) {
317             return ((isInstanceOfComponent(splitTask.getTopLeftTask(), componentKey1)
318                     && isInstanceOfComponent(splitTask.getBottomRightTask(), componentKey2))
319                     ||
320                     (isInstanceOfComponent(splitTask.getTopLeftTask(), componentKey2)
321                             && isInstanceOfComponent(splitTask.getBottomRightTask(),
322                             componentKey1)));
323         }
324         return false;
325     }
326 
327     /**
328      * Listener will only get callbacks going forward from the point of registration. No
329      * methods will be fired upon registering.
330      */
registerSplitListener(@onNull SplitSelectionListener listener)331     public void registerSplitListener(@NonNull SplitSelectionListener listener) {
332         if (mSplitSelectionListeners.contains(listener)) {
333             return;
334         }
335         mSplitSelectionListeners.add(listener);
336     }
337 
unregisterSplitListener(@onNull SplitSelectionListener listener)338     public void unregisterSplitListener(@NonNull SplitSelectionListener listener) {
339         mSplitSelectionListeners.remove(listener);
340     }
341 
dispatchOnSplitSelectionExit()342     private void dispatchOnSplitSelectionExit() {
343         for (SplitSelectionListener listener : mSplitSelectionListeners) {
344             listener.onSplitSelectionExit(false);
345         }
346     }
347 
348     /**
349      * To be called when the both split tasks are ready to be launched. Call after launcher side
350      * animations are complete.
351      */
launchSplitTasks(@ersistentSnapPosition int snapPosition, @Nullable Consumer<Boolean> callback)352     public void launchSplitTasks(@PersistentSnapPosition int snapPosition,
353             @Nullable Consumer<Boolean> callback) {
354         launchTasks(callback, false /* freezeTaskList */, snapPosition, mSessionInstanceIds.first);
355 
356         mStatsLogManager.logger()
357                 .withItemInfo(mSplitSelectDataHolder.getSecondItemInfo())
358                 .withInstanceId(mSessionInstanceIds.second)
359                 .log(LAUNCHER_SPLIT_SELECTED_SECOND_APP);
360     }
361 
362     /**
363      * A version of {@link #launchTasks(Consumer, boolean, int, InstanceId)} with no success
364      * callback.
365      */
launchSplitTasks(@ersistentSnapPosition int snapPosition)366     public void launchSplitTasks(@PersistentSnapPosition int snapPosition) {
367         launchSplitTasks(snapPosition, /* callback */ null);
368     }
369 
370     /**
371      * A version of {@link #launchSplitTasks(int, Consumer)} that launches with default split ratio.
372      */
launchSplitTasks(@ullable Consumer<Boolean> callback)373     public void launchSplitTasks(@Nullable Consumer<Boolean> callback) {
374         launchSplitTasks(SNAP_TO_2_50_50, callback);
375     }
376 
377     /**
378      * A version of {@link #launchSplitTasks(int, Consumer)} that launches with a default split
379      * ratio and no callback.
380      */
launchSplitTasks()381     public void launchSplitTasks() {
382         launchSplitTasks(SNAP_TO_2_50_50, null);
383     }
384 
385     /**
386      * Use to log an event when user exists split selection when the second app **IS NOT** selected.
387      * This must be called before playing any exit animations since most animations will call
388      * {@link #resetState()} which removes {@link #mSessionInstanceIds}.
389      */
logExitReason(StatsLogManager.EventEnum splitExitEvent)390     public void logExitReason(StatsLogManager.EventEnum splitExitEvent) {
391         StatsLogManager.StatsLogger logger = mStatsLogManager.logger();
392         if (mSessionInstanceIds != null) {
393             logger.withInstanceId(mSessionInstanceIds.second);
394         } else {
395             Log.w(TAG, "Missing session instanceIds");
396         }
397         logger.log(splitExitEvent);
398     }
399 
400     /**
401      * To be called as soon as user selects the second task (even if animations aren't complete)
402      * @param task The second task that will be launched.
403      */
setSecondTask(Task task, ItemInfo itemInfo)404     public void setSecondTask(Task task, ItemInfo itemInfo) {
405         mSplitSelectDataHolder.setSecondTask(task.key.id, itemInfo);
406     }
407 
408     /**
409      * To be called as soon as user selects the second app (even if animations aren't complete)
410      * @param intent The second intent that will be launched.
411      * @param user The user of that intent.
412      */
setSecondTask(Intent intent, UserHandle user, ItemInfo itemInfo)413     public void setSecondTask(Intent intent, UserHandle user, ItemInfo itemInfo) {
414         mSplitSelectDataHolder.setSecondTask(intent, user, itemInfo);
415     }
416 
417     /**
418      * To be called as soon as user selects the second app (even if animations aren't complete)
419      * @param pendingIntent The second PendingIntent that will be launched.
420      */
setSecondTask(PendingIntent pendingIntent, ItemInfo itemInfo)421     public void setSecondTask(PendingIntent pendingIntent, ItemInfo itemInfo) {
422         mSplitSelectDataHolder.setSecondTask(pendingIntent, itemInfo);
423     }
424 
setSecondWidget(PendingIntent pendingIntent, Intent widgetIntent)425     public void setSecondWidget(PendingIntent pendingIntent, Intent widgetIntent) {
426         mSplitSelectDataHolder.setSecondWidget(pendingIntent, widgetIntent, null /*itemInfo*/);
427     }
428 
429     /**
430      * To be called when we want to launch split pairs from Overview. Split can be initiated from
431      * either Overview or home, or all apps. Either both taskIds are set, or a pending intent + a
432      * fill in intent with a taskId2 are set.
433      * @param shellInstanceId loggingId to be used by shell, will be non-null for actions that
434      *                   create a split instance, null for cases that bring existing instaces to the
435      *                   foreground (quickswitch, launching previous pairs from overview)
436      */
launchTasks(@ullable Consumer<Boolean> callback, boolean freezeTaskList, @PersistentSnapPosition int snapPosition, @Nullable InstanceId shellInstanceId)437     public void launchTasks(@Nullable Consumer<Boolean> callback, boolean freezeTaskList,
438             @PersistentSnapPosition int snapPosition, @Nullable InstanceId shellInstanceId) {
439         TestLogging.recordEvent(
440                 TestProtocol.SEQUENCE_MAIN, "launchSplitTasks");
441         final ActivityOptions options1 = ActivityOptions.makeBasic();
442         if (freezeTaskList) {
443             options1.setFreezeRecentTasksReordering();
444         }
445 
446         SplitSelectDataHolder.SplitLaunchData launchData =
447                 mSplitSelectDataHolder.getSplitLaunchData();
448         int firstTaskId = launchData.getInitialTaskId();
449         int secondTaskId = launchData.getSecondTaskId();
450         ShortcutInfo firstShortcut = launchData.getInitialShortcut();
451         ShortcutInfo secondShortcut = launchData.getSecondShortcut();
452         PendingIntent firstPI = launchData.getInitialPendingIntent();
453         PendingIntent secondPI = launchData.getSecondPendingIntent();
454         Intent widgetIntent = launchData.getWidgetSecondIntent();
455         int firstUserId = launchData.getInitialUserId();
456         int secondUserId = launchData.getSecondUserId();
457         int initialStagePosition = launchData.getInitialStagePosition();
458         Bundle optionsBundle = options1.toBundle();
459         Bundle extrasBundle = new Bundle(1);
460         extrasBundle.putParcelable(KEY_EXTRA_WIDGET_INTENT, widgetIntent);
461         final RemoteTransition remoteTransition = getRemoteTransition(firstTaskId,
462                 secondTaskId, callback, "LaunchSplitPair");
463         switch (launchData.getSplitLaunchType()) {
464             case SPLIT_TASK_TASK ->
465                     mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId,
466                             null /* options2 */, initialStagePosition, snapPosition,
467                             remoteTransition, shellInstanceId);
468 
469             case SPLIT_TASK_PENDINGINTENT ->
470                     mSystemUiProxy.startIntentAndTask(secondPI, secondUserId, optionsBundle,
471                             firstTaskId, extrasBundle, initialStagePosition, snapPosition,
472                             remoteTransition, shellInstanceId);
473 
474             case SPLIT_TASK_SHORTCUT ->
475                     mSystemUiProxy.startShortcutAndTask(secondShortcut, optionsBundle,
476                             firstTaskId, null /*options2*/, initialStagePosition, snapPosition,
477                             remoteTransition, shellInstanceId);
478 
479             case SPLIT_PENDINGINTENT_TASK ->
480                     mSystemUiProxy.startIntentAndTask(firstPI, firstUserId, optionsBundle,
481                             secondTaskId, null /*options2*/, initialStagePosition, snapPosition,
482                             remoteTransition, shellInstanceId);
483 
484             case SPLIT_PENDINGINTENT_PENDINGINTENT ->
485                     mSystemUiProxy.startIntents(firstPI, firstUserId, firstShortcut,
486                             optionsBundle, secondPI, secondUserId, secondShortcut, extrasBundle,
487                             initialStagePosition, snapPosition, remoteTransition,
488                             shellInstanceId);
489 
490             case SPLIT_SHORTCUT_TASK ->
491                     mSystemUiProxy.startShortcutAndTask(firstShortcut, optionsBundle,
492                             secondTaskId, null /*options2*/, initialStagePosition, snapPosition,
493                             remoteTransition, shellInstanceId);
494         }
495 
496     }
497 
498     /**
499      * Used to launch split screen from a split pair that already exists, optionally with a custom
500      * remote transition.
501      * <p>
502      * See {@link SplitSelectStateController#launchExistingSplitPair(
503      * GroupedTaskView, int, int, int, Consumer, boolean, int, RemoteTransition)}
504      */
launchExistingSplitPair(@ullable GroupedTaskView groupedTaskView, int firstTaskId, int secondTaskId, @StagePosition int stagePosition, Consumer<Boolean> callback, boolean freezeTaskList, @PersistentSnapPosition int snapPosition)505     public void launchExistingSplitPair(@Nullable GroupedTaskView groupedTaskView,
506             int firstTaskId, int secondTaskId, @StagePosition int stagePosition,
507             Consumer<Boolean> callback, boolean freezeTaskList,
508             @PersistentSnapPosition int snapPosition) {
509         launchExistingSplitPair(
510                 groupedTaskView,
511                 firstTaskId,
512                 secondTaskId,
513                 stagePosition,
514                 callback,
515                 freezeTaskList,
516                 snapPosition,
517                 /* remoteTransition= */ null);
518     }
519 
520 
521     /**
522      * Used to launch split screen from a split pair that already exists (usually accessible through
523      * Overview). This is different than {@link #launchTasks(Consumer, boolean, int, InstanceId)}
524      * in that this only launches split screen that are existing tasks. This doesn't determine which
525      * API should be used (i.e. launching split with existing tasks vs intents vs shortcuts, etc).
526      *
527      * <p/>
528      * NOTE: This is not to be used to launch AppPairs.
529      */
launchExistingSplitPair(@ullable GroupedTaskView groupedTaskView, int firstTaskId, int secondTaskId, @StagePosition int stagePosition, Consumer<Boolean> callback, boolean freezeTaskList, @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition)530     public void launchExistingSplitPair(@Nullable GroupedTaskView groupedTaskView,
531             int firstTaskId, int secondTaskId, @StagePosition int stagePosition,
532             Consumer<Boolean> callback, boolean freezeTaskList,
533             @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition) {
534         mLaunchingTaskView = groupedTaskView;
535         final ActivityOptions options1 = ActivityOptions.makeBasic();
536         if (freezeTaskList) {
537             options1.setFreezeRecentTasksReordering();
538         }
539         Bundle optionsBundle = options1.toBundle();
540 
541         final RemoteTransition transition = remoteTransition == null
542                 ? getRemoteTransition(
543                 firstTaskId, secondTaskId, callback, "LaunchExistingPair")
544                 : remoteTransition;
545         mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, null /* options2 */,
546                 stagePosition, snapPosition, transition, null /*shellInstanceId*/);
547 
548     }
549 
550     /**
551      * Launches the initially selected task/intent in fullscreen (note the same SystemUi APIs are
552      * used as {@link #launchSplitTasks(int, Consumer)} because they are overloaded to launch both
553      * split and fullscreen tasks)
554      */
launchInitialAppFullscreen(Consumer<Boolean> callback)555     public void launchInitialAppFullscreen(Consumer<Boolean> callback) {
556         final ActivityOptions options1 = ActivityOptions.makeBasic();
557         SplitSelectDataHolder.SplitLaunchData launchData =
558                 mSplitSelectDataHolder.getFullscreenLaunchData();
559         int firstTaskId = launchData.getInitialTaskId();
560         int secondTaskId = launchData.getSecondTaskId();
561         PendingIntent firstPI = launchData.getInitialPendingIntent();
562         int firstUserId = launchData.getInitialUserId();
563         int initialStagePosition = launchData.getInitialStagePosition();
564         ShortcutInfo initialShortcut = launchData.getInitialShortcut();
565         Bundle optionsBundle = options1.toBundle();
566 
567         final RemoteSplitLaunchTransitionRunner animationRunner =
568                 new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback);
569         final RemoteTransition remoteTransition = new RemoteTransition(animationRunner,
570                 ActivityThread.currentActivityThread().getApplicationThread(),
571                 "LaunchAppFullscreen");
572         InstanceId instanceId = mSessionInstanceIds.first;
573         switch (launchData.getSplitLaunchType()) {
574             case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId,
575                     optionsBundle, secondTaskId, null /* options2 */, initialStagePosition,
576                     SNAP_TO_2_50_50, remoteTransition, instanceId);
577             case SPLIT_SINGLE_INTENT_FULLSCREEN -> mSystemUiProxy.startIntentAndTask(firstPI,
578                     firstUserId, optionsBundle, secondTaskId, null /*options2*/,
579                     initialStagePosition, SNAP_TO_2_50_50, remoteTransition, instanceId);
580             case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> mSystemUiProxy.startShortcutAndTask(
581                     initialShortcut, optionsBundle, firstTaskId, null /* options2 */,
582                     initialStagePosition, SNAP_TO_2_50_50, remoteTransition, instanceId);
583         }
584     }
585 
586     /**
587      * Init {@code SplitFromDesktopController}
588      */
initSplitFromDesktopController(QuickstepLauncher launcher)589     public void initSplitFromDesktopController(QuickstepLauncher launcher) {
590         initSplitFromDesktopController(new SplitFromDesktopController(launcher));
591     }
592 
593     @VisibleForTesting
initSplitFromDesktopController(SplitFromDesktopController controller)594     void initSplitFromDesktopController(SplitFromDesktopController controller) {
595         mSplitFromDesktopController = controller;
596     }
597 
getRemoteTransition(int firstTaskId, int secondTaskId, @Nullable Consumer<Boolean> callback, String transitionName)598     private RemoteTransition getRemoteTransition(int firstTaskId, int secondTaskId,
599             @Nullable Consumer<Boolean> callback, String transitionName) {
600         final RemoteSplitLaunchTransitionRunner animationRunner =
601                 new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback);
602         return new RemoteTransition(animationRunner,
603                 ActivityThread.currentActivityThread().getApplicationThread(), transitionName);
604     }
605 
606     /**
607      * Will initialize {@link #mSessionInstanceIds} if null and log the first split event from
608      * {@link #mSplitSelectDataHolder}
609      */
createAndLogInstanceIdsForSession()610     private void createAndLogInstanceIdsForSession() {
611         if (mSessionInstanceIds != null) {
612             Log.w(TAG, "SessionIds should be null");
613         }
614         // Log separately the start of the session and then the first app selected
615         mSessionInstanceIds = LogUtils.getShellShareableInstanceId();
616         mStatsLogManager.logger()
617                 .withInstanceId(mSessionInstanceIds.second)
618                 .log(LAUNCHER_SPLIT_SELECTION_INITIATED);
619 
620         mStatsLogManager.logger()
621                 .withItemInfo(mSplitSelectDataHolder.getItemInfo())
622                 .withInstanceId(mSessionInstanceIds.second)
623                 .log(mSplitSelectDataHolder.getSplitEvent());
624     }
625 
getActiveSplitStagePosition()626     public @StagePosition int getActiveSplitStagePosition() {
627         return mSplitSelectDataHolder.getInitialStagePosition();
628     }
629 
getSplitEvent()630     public StatsLogManager.EventEnum getSplitEvent() {
631         return mSplitSelectDataHolder.getSplitEvent();
632     }
633 
setRecentsAnimationRunning(boolean running)634     public void setRecentsAnimationRunning(boolean running) {
635         mRecentsAnimationRunning = running;
636     }
637 
isAnimateCurrentTaskDismissal()638     public boolean isAnimateCurrentTaskDismissal() {
639         return mAnimateCurrentTaskDismissal;
640     }
641 
setAnimateCurrentTaskDismissal(boolean animateCurrentTaskDismissal)642     public void setAnimateCurrentTaskDismissal(boolean animateCurrentTaskDismissal) {
643         mAnimateCurrentTaskDismissal = animateCurrentTaskDismissal;
644     }
645 
isDismissingFromSplitPair()646     public boolean isDismissingFromSplitPair() {
647         return mDismissingFromSplitPair;
648     }
649 
setDismissingFromSplitPair(boolean dismissingFromSplitPair)650     public void setDismissingFromSplitPair(boolean dismissingFromSplitPair) {
651         mDismissingFromSplitPair = dismissingFromSplitPair;
652     }
653 
getSplitAnimationController()654     public SplitAnimationController getSplitAnimationController() {
655         return mSplitAnimationController;
656     }
657 
658     /**
659      * Set params to invoke a trace session for the given view and CUJ when we begin animating the
660      * split launch AFTER we get a response from Shell.
661      */
setLaunchingCuj(View launchingView, int launchCuj)662     public void setLaunchingCuj(View launchingView, int launchCuj) {
663         mLaunchingViewCuj = launchingView;
664         mLaunchCuj = launchCuj;
665     }
666 
667     /**
668      * Requires Shell Transitions
669      */
670     private class RemoteSplitLaunchTransitionRunner extends RemoteTransitionStub {
671 
672         private final int mInitialTaskId;
673         private final int mSecondTaskId;
674         private Consumer<Boolean> mFinishCallback;
675 
RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId, @Nullable Consumer<Boolean> callback)676         RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId,
677                 @Nullable Consumer<Boolean> callback) {
678             mInitialTaskId = initialTaskId;
679             mSecondTaskId = secondTaskId;
680             mFinishCallback = callback;
681         }
682 
683         @Override
startAnimation(IBinder transition, TransitionInfo transitionInfo, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishedCallback)684         public void startAnimation(IBinder transition, TransitionInfo transitionInfo,
685                 SurfaceControl.Transaction t,
686                 IRemoteTransitionFinishedCallback finishedCallback) {
687             final Runnable finishAdapter = () ->  {
688                 try {
689                     finishedCallback.onTransitionFinished(null /* wct */, null /* sct */);
690                 } catch (RemoteException e) {
691                     Log.e(TAG, "Failed to call transition finished callback", e);
692                 }
693             };
694 
695             MAIN_EXECUTOR.execute(() -> {
696                 // Only animate from taskView if it's already visible
697                 boolean shouldLaunchFromTaskView = mLaunchingTaskView != null
698                         && mLaunchingTaskView.getRecentsView() != null
699                         && mLaunchingTaskView.getRecentsView().isTaskViewVisible(
700                         mLaunchingTaskView);
701                 if (mLaunchingViewCuj != null && mLaunchCuj != -1) {
702                     InteractionJankMonitorWrapper.begin(mLaunchingViewCuj, mLaunchCuj);
703                 }
704                 mSplitAnimationController.playSplitLaunchAnimation(
705                         shouldLaunchFromTaskView ? mLaunchingTaskView : null,
706                         mLaunchingIconView,
707                         mInitialTaskId,
708                         mSecondTaskId,
709                         null /* apps */,
710                         null /* wallpapers */,
711                         null /* nonApps */,
712                         mStateManager,
713                         mDepthController,
714                         transitionInfo, t, () -> {
715                             finishAdapter.run();
716                             cleanup(true /*success*/);
717                         },
718                         QuickStepContract.getWindowCornerRadius(mContainer.asContext()));
719             });
720         }
721 
722         @Override
onTransitionConsumed(IBinder transition, boolean aborted)723         public void onTransitionConsumed(IBinder transition, boolean aborted)
724                 throws RemoteException {
725             MAIN_EXECUTOR.execute(() -> {
726                 cleanup(false /*success*/);
727             });
728         }
729 
730         /**
731          * Must be called on UI thread.
732          * @param success if launching the split apps occurred successfully or not
733          */
734         @UiThread
cleanup(boolean success)735         private void cleanup(boolean success) {
736             if (mFinishCallback != null) {
737                 mFinishCallback.accept(success);
738                 mFinishCallback = null;
739             }
740             resetState();
741         }
742     }
743 
744     /**
745      * To be called whenever we exit split selection state. This should be the
746      * central way split is getting reset, which should then go through the callbacks to reset
747      * other state.
748      */
resetState()749     public void resetState() {
750         mSplitSelectDataHolder.resetState();
751         if (!mIsDestroyed) {
752             mContainer.<RecentsView>getOverviewPanel().resetDesktopTaskFromSplitSelectState();
753         }
754         dispatchOnSplitSelectionExit();
755         mRecentsAnimationRunning = false;
756         mLaunchingTaskView = null;
757         mLaunchingIconView = null;
758         mAnimateCurrentTaskDismissal = false;
759         mDismissingFromSplitPair = false;
760         mFirstFloatingTaskView = null;
761         mSplitInstructionsView = null;
762         mLaunchingFirstAppFullscreen = false;
763 
764         if (mLaunchCuj != -1) {
765             InteractionJankMonitorWrapper.end(mLaunchCuj);
766         }
767         mLaunchCuj = -1;
768         mLaunchingViewCuj = null;
769 
770         if (mSessionInstanceIds != null) {
771             mStatsLogManager.logger()
772                     .withInstanceId(mSessionInstanceIds.second)
773                     .log(LAUNCHER_SPLIT_SELECTION_COMPLETE);
774         }
775         mSessionInstanceIds = null;
776     }
777 
778     /**
779      * @return {@code true} if first task has been selected and waiting for the second task to be
780      *         chosen
781      */
isSplitSelectActive()782     public boolean isSplitSelectActive() {
783         return mSplitSelectDataHolder.isSplitSelectActive();
784     }
785 
786     /**
787      * @return {@code true} if the first and second task have been chosen and split is waiting to
788      *          be launched
789      */
isBothSplitAppsConfirmed()790     public boolean isBothSplitAppsConfirmed() {
791         return mSplitSelectDataHolder.isBothSplitAppsConfirmed();
792     }
793 
isLaunchingFirstAppFullscreen()794     public boolean isLaunchingFirstAppFullscreen() {
795         return mLaunchingFirstAppFullscreen;
796     }
797 
getInitialTaskId()798     public int getInitialTaskId() {
799         return mSplitSelectDataHolder.getInitialTaskId();
800     }
801 
getSecondTaskId()802     public int getSecondTaskId() {
803         return mSplitSelectDataHolder.getSecondTaskId();
804     }
805 
setLaunchingFirstAppFullscreen()806     public void setLaunchingFirstAppFullscreen() {
807         mLaunchingFirstAppFullscreen = true;
808     }
setFirstFloatingTaskView(FloatingTaskView floatingTaskView)809     public void setFirstFloatingTaskView(FloatingTaskView floatingTaskView) {
810         mFirstFloatingTaskView = floatingTaskView;
811     }
812 
setSplitInstructionsView(SplitInstructionsView splitInstructionsView)813     public void setSplitInstructionsView(SplitInstructionsView splitInstructionsView) {
814         mSplitInstructionsView = splitInstructionsView;
815     }
816 
817     @Nullable
getFirstFloatingTaskView()818     public FloatingTaskView getFirstFloatingTaskView() {
819         return mFirstFloatingTaskView;
820     }
821 
822     @Nullable
getSplitInstructionsView()823     public SplitInstructionsView getSplitInstructionsView() {
824         return mSplitInstructionsView;
825     }
826 
getAppPairsController()827     public AppPairsController getAppPairsController() {
828         return mAppPairsController;
829     }
830 
setLaunchingIconView(AppPairIcon launchingIconView)831     public void setLaunchingIconView(AppPairIcon launchingIconView) {
832         mLaunchingIconView = launchingIconView;
833     }
834 
getSplitBackHandler()835     public BackPressHandler getSplitBackHandler() {
836         return mSplitBackHandler;
837     }
838 
dump(String prefix, PrintWriter writer)839     public void dump(String prefix, PrintWriter writer) {
840         if (mSplitSelectDataHolder != null) {
841             mSplitSelectDataHolder.dump(prefix, writer);
842         }
843     }
844 
845     public class SplitFromDesktopController {
846         private static final String TAG = "SplitFromDesktopController";
847 
848         private final QuickstepLauncher mLauncher;
849         private final OverviewComponentObserver mOverviewComponentObserver;
850         private final int mSplitPlaceholderSize;
851         private final int mSplitPlaceholderInset;
852         private ActivityManager.RunningTaskInfo mTaskInfo;
853         private DesktopSplitSelectListenerImpl mSplitSelectListener;
854         private Drawable mAppIcon;
855 
SplitFromDesktopController(QuickstepLauncher launcher)856         public SplitFromDesktopController(QuickstepLauncher launcher) {
857             mLauncher = launcher;
858             mOverviewComponentObserver = OverviewComponentObserver.INSTANCE.get(launcher);
859             mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize(
860                     R.dimen.split_placeholder_size);
861             mSplitPlaceholderInset = mLauncher.getResources().getDimensionPixelSize(
862                     R.dimen.split_placeholder_inset);
863             mSplitSelectListener = new DesktopSplitSelectListenerImpl(this);
864             SystemUiProxy.INSTANCE.get(mLauncher).registerSplitSelectListener(mSplitSelectListener);
865         }
866 
onDestroy()867         void onDestroy() {
868             SystemUiProxy.INSTANCE.get(mLauncher).unregisterSplitSelectListener(
869                     mSplitSelectListener);
870             mSplitSelectListener.release();
871             mSplitSelectListener = null;
872         }
873 
874         /**
875          * Enter split select from desktop mode.
876          * @param taskInfo the desktop task to move to split stage
877          * @param splitPosition the stage position used for this transition
878          * @param taskBounds the bounds of the task, used for {@link FloatingTaskView} animation
879          */
enterSplitSelect(ActivityManager.RunningTaskInfo taskInfo, int splitPosition, Rect taskBounds)880         public void enterSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
881                 int splitPosition, Rect taskBounds) {
882             mTaskInfo = taskInfo;
883             String packageName = mTaskInfo.realActivity.getPackageName();
884             PackageManager pm = mLauncher.getApplicationContext().getPackageManager();
885             IconProvider provider = new IconProvider(mLauncher.getApplicationContext());
886             try {
887                 mAppIcon = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity,
888                      PackageManager.ComponentInfoFlags.of(0)));
889             } catch (PackageManager.NameNotFoundException e) {
890                 Log.w(TAG, "Package not found: " + packageName, e);
891             }
892             RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks(
893                     SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()));
894 
895             DesktopSplitRecentsAnimationListener listener =
896                     new DesktopSplitRecentsAnimationListener(splitPosition, taskBounds);
897 
898             callbacks.addListener(listener);
899             UI_HELPER_EXECUTOR.execute(() -> {
900                 // Transition from app to enter stage split in launcher with recents animation
901                 final ActivityOptions options = ActivityOptions.makeBasic();
902                 options.setPendingIntentBackgroundActivityStartMode(
903                         ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS);
904                 options.setTransientLaunch();
905                 SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext())
906                         .startRecentsActivity(
907                                 mOverviewComponentObserver.getOverviewIntent(), options,
908                                 callbacks, false /* useSyntheticRecentsTransition */);
909             });
910         }
911 
912         private class DesktopSplitRecentsAnimationListener implements
913                 RecentsAnimationCallbacks.RecentsAnimationListener {
914             private final Rect mTempRect = new Rect();
915             private final RectF mTaskBounds = new RectF();
916             private final int mSplitPosition;
917 
DesktopSplitRecentsAnimationListener(int splitPosition, Rect taskBounds)918             DesktopSplitRecentsAnimationListener(int splitPosition, Rect taskBounds) {
919                 mSplitPosition = splitPosition;
920                 mTaskBounds.set(taskBounds);
921             }
922 
923             @Override
onRecentsAnimationStart(RecentsAnimationController controller, RecentsAnimationTargets targets, TransitionInfo transitionInfo)924             public void onRecentsAnimationStart(RecentsAnimationController controller,
925                     RecentsAnimationTargets targets, TransitionInfo transitionInfo) {
926                 StatsLogManager.LauncherEvent launcherDesktopSplitEvent =
927                         mSplitPosition == STAGE_POSITION_BOTTOM_OR_RIGHT ?
928                         LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM :
929                         LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP;
930                 setInitialTaskSelect(mTaskInfo, mSplitPosition,
931                         null, launcherDesktopSplitEvent);
932 
933                 RecentsView recentsView = mLauncher.getOverviewPanel();
934                 recentsView.getPagedOrientationHandler().getInitialSplitPlaceholderBounds(
935                         mSplitPlaceholderSize, mSplitPlaceholderInset,
936                         mLauncher.getDeviceProfile(), getActiveSplitStagePosition(), mTempRect);
937 
938                 PendingAnimation anim = new PendingAnimation(
939                         SplitAnimationTimings.TABLET_HOME_TO_SPLIT.getDuration());
940                 final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView(
941                         mLauncher, mLauncher.getDragLayer(),
942                         null /* thumbnail */,
943                         mAppIcon, new RectF());
944                 floatingTaskView.setOnClickListener(view ->
945                         getSplitAnimationController()
946                                 .playAnimPlaceholderToFullscreen(mContainer, view,
947                                         Optional.of(() -> resetState())));
948                 floatingTaskView.setAlpha(1);
949                 floatingTaskView.addStagingAnimation(anim, mTaskBounds, mTempRect,
950                         false /* fadeWithThumbnail */, true /* isStagedTask */);
951                 setFirstFloatingTaskView(floatingTaskView);
952 
953                 anim.addListener(new AnimatorListenerAdapter() {
954                     @Override
955                     public void onAnimationStart(Animator animation) {
956                         controller.finish(
957                                 true /* toRecents */,
958                                 () -> {
959                                     LauncherTaskbarUIController controller =
960                                             mLauncher.getTaskbarUIController();
961                                     if (controller != null) {
962                                         controller.updateTaskbarLauncherStateGoingHome();
963                                     }
964 
965                                 },
966                                 false /* sendUserLeaveHint */);
967                     }
968                     @Override
969                     public void onAnimationEnd(Animator animation) {
970                         SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext())
971                                 .onDesktopSplitSelectAnimComplete(mTaskInfo);
972                     }
973                     @Override
974                     public void onAnimationCancel(Animator animation) {
975                         mLauncher.getDragLayer().removeView(floatingTaskView);
976                         getSplitAnimationController()
977                                 .removeSplitInstructionsView(mLauncher);
978                         resetState();
979                     }
980                 });
981                 anim.add(getSplitAnimationController()
982                         .getShowSplitInstructionsAnim(mLauncher).buildAnim());
983                 anim.buildAnim().start();
984             }
985         }
986     }
987 
988     /**
989      * Wrapper for the ISplitSelectListener stub to prevent lingering references to the launcher
990      * activity via the controller.
991      */
992     private static class DesktopSplitSelectListenerImpl extends ISplitSelectListener.Stub {
993 
994         private SplitFromDesktopController mController;
995 
DesktopSplitSelectListenerImpl(@onNull SplitFromDesktopController controller)996         DesktopSplitSelectListenerImpl(@NonNull SplitFromDesktopController controller) {
997             mController = controller;
998         }
999 
1000         /**
1001          * Clears any references to the controller.
1002          */
release()1003         void release() {
1004             mController = null;
1005         }
1006 
1007         @Override
onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo, int splitPosition, Rect taskBounds)1008         public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
1009                 int splitPosition, Rect taskBounds) {
1010             MAIN_EXECUTOR.execute(() -> {
1011                 if (mController != null) {
1012                     mController.enterSplitSelect(taskInfo, splitPosition, taskBounds);
1013                 }
1014             });
1015             return true;
1016         }
1017     }
1018 }
1019