1 /*
2  * Copyright (C) 2020 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.wm.shell.splitscreen;
18 
19 import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
20 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
21 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
22 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
23 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
24 import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
25 import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED;
26 import static android.view.Display.DEFAULT_DISPLAY;
27 import static android.view.RemoteAnimationTarget.MODE_OPENING;
28 import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
29 import static android.view.WindowManager.TRANSIT_CHANGE;
30 import static android.view.WindowManager.TRANSIT_CLOSE;
31 import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE;
32 import static android.view.WindowManager.TRANSIT_TO_BACK;
33 import static android.view.WindowManager.TRANSIT_TO_FRONT;
34 import static android.view.WindowManager.transitTypeToString;
35 import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
36 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
37 
38 import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER;
39 import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
40 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
41 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
42 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
43 import static com.android.wm.shell.common.split.SplitScreenConstants.splitPositionToString;
44 import static com.android.wm.shell.common.split.SplitScreenUtils.getResizingBackgroundColor;
45 import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition;
46 import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage;
47 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN;
48 import static com.android.wm.shell.shared.TransitionUtil.isClosingType;
49 import static com.android.wm.shell.shared.TransitionUtil.isOpeningType;
50 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN;
51 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE;
52 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
53 import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_LAUNCHER;
54 import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE;
55 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW;
56 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED;
57 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
58 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DESKTOP_MODE;
59 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED;
60 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
61 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_REQUEST;
62 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT;
63 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RECREATE_SPLIT;
64 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME;
65 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_ROOT_TASK_VANISHED;
66 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP;
67 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_UNKNOWN;
68 import static com.android.wm.shell.splitscreen.SplitScreenController.exitReasonToString;
69 import static com.android.wm.shell.transition.MixedTransitionHelper.getPipReplacingChange;
70 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
71 import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE;
72 import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
73 
74 import android.animation.Animator;
75 import android.animation.AnimatorListenerAdapter;
76 import android.animation.ValueAnimator;
77 import android.annotation.CallSuper;
78 import android.annotation.NonNull;
79 import android.annotation.Nullable;
80 import android.app.ActivityManager;
81 import android.app.ActivityOptions;
82 import android.app.IActivityTaskManager;
83 import android.app.PendingIntent;
84 import android.app.TaskInfo;
85 import android.app.WindowConfiguration;
86 import android.content.ActivityNotFoundException;
87 import android.content.Context;
88 import android.content.Intent;
89 import android.content.pm.LauncherApps;
90 import android.content.pm.ShortcutInfo;
91 import android.graphics.Rect;
92 import android.hardware.devicestate.DeviceStateManager;
93 import android.os.Bundle;
94 import android.os.Debug;
95 import android.os.IBinder;
96 import android.os.RemoteException;
97 import android.os.ServiceManager;
98 import android.os.UserHandle;
99 import android.util.ArrayMap;
100 import android.util.ArraySet;
101 import android.util.IntArray;
102 import android.util.Log;
103 import android.util.Slog;
104 import android.view.Choreographer;
105 import android.view.IRemoteAnimationFinishedCallback;
106 import android.view.IRemoteAnimationRunner;
107 import android.view.RemoteAnimationAdapter;
108 import android.view.RemoteAnimationTarget;
109 import android.view.SurfaceControl;
110 import android.view.SurfaceSession;
111 import android.view.WindowManager;
112 import android.widget.Toast;
113 import android.window.DisplayAreaInfo;
114 import android.window.RemoteTransition;
115 import android.window.TransitionInfo;
116 import android.window.TransitionRequestInfo;
117 import android.window.WindowContainerToken;
118 import android.window.WindowContainerTransaction;
119 
120 import com.android.internal.annotations.VisibleForTesting;
121 import com.android.internal.logging.InstanceId;
122 import com.android.internal.protolog.common.ProtoLog;
123 import com.android.internal.util.ArrayUtils;
124 import com.android.launcher3.icons.IconProvider;
125 import com.android.wm.shell.R;
126 import com.android.wm.shell.ShellTaskOrganizer;
127 import com.android.wm.shell.common.DisplayController;
128 import com.android.wm.shell.common.DisplayImeController;
129 import com.android.wm.shell.common.DisplayInsetsController;
130 import com.android.wm.shell.common.LaunchAdjacentController;
131 import com.android.wm.shell.common.ScreenshotUtils;
132 import com.android.wm.shell.common.ShellExecutor;
133 import com.android.wm.shell.common.SyncTransactionQueue;
134 import com.android.wm.shell.common.TransactionPool;
135 import com.android.wm.shell.common.split.SplitLayout;
136 import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
137 import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
138 import com.android.wm.shell.common.split.SplitScreenUtils;
139 import com.android.wm.shell.common.split.SplitWindowManager;
140 import com.android.wm.shell.protolog.ShellProtoLogGroup;
141 import com.android.wm.shell.recents.RecentTasksController;
142 import com.android.wm.shell.shared.TransitionUtil;
143 import com.android.wm.shell.splitscreen.SplitScreen.StageType;
144 import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason;
145 import com.android.wm.shell.splitscreen.SplitScreenController.SplitEnterReason;
146 import com.android.wm.shell.transition.DefaultMixedHandler;
147 import com.android.wm.shell.transition.LegacyTransitions;
148 import com.android.wm.shell.transition.Transitions;
149 import com.android.wm.shell.util.SplitBounds;
150 import com.android.wm.shell.windowdecor.WindowDecorViewModel;
151 
152 import dalvik.annotation.optimization.NeverCompile;
153 
154 import java.io.PrintWriter;
155 import java.util.ArrayList;
156 import java.util.HashSet;
157 import java.util.List;
158 import java.util.Optional;
159 import java.util.Set;
160 import java.util.concurrent.Executor;
161 
162 /**
163  * Coordinates the staging (visibility, sizing, ...) of the split-screen {@link MainStage} and
164  * {@link SideStage} stages.
165  * Some high-level rules:
166  * - The {@link StageCoordinator} is only considered active if the {@link SideStage} contains at
167  * least one child task.
168  * - The {@link MainStage} should only have children if the coordinator is active.
169  * - The {@link SplitLayout} divider is only visible if both the {@link MainStage}
170  * and {@link SideStage} are visible.
171  * - Both stages are put under a single-top root task.
172  * This rules are mostly implemented in {@link #onStageVisibilityChanged(StageListenerImpl)} and
173  * {@link #onStageHasChildrenChanged(StageListenerImpl).}
174  */
175 public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
176         DisplayController.OnDisplaysChangedListener, Transitions.TransitionHandler,
177         ShellTaskOrganizer.TaskListener {
178 
179     private static final String TAG = StageCoordinator.class.getSimpleName();
180 
181     private final SurfaceSession mSurfaceSession = new SurfaceSession();
182 
183     private final MainStage mMainStage;
184     private final StageListenerImpl mMainStageListener = new StageListenerImpl();
185     private final SideStage mSideStage;
186     private final StageListenerImpl mSideStageListener = new StageListenerImpl();
187     @SplitPosition
188     private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT;
189 
190     private final int mDisplayId;
191     private SplitLayout mSplitLayout;
192     private ValueAnimator mDividerFadeInAnimator;
193     private boolean mDividerVisible;
194     private boolean mKeyguardShowing;
195     private boolean mShowDecorImmediately;
196     private final SyncTransactionQueue mSyncQueue;
197     private final ShellTaskOrganizer mTaskOrganizer;
198     private final Context mContext;
199     private final List<SplitScreen.SplitScreenListener> mListeners = new ArrayList<>();
200     private final Set<SplitScreen.SplitSelectListener> mSelectListeners = new HashSet<>();
201     private final DisplayController mDisplayController;
202     private final DisplayImeController mDisplayImeController;
203     private final DisplayInsetsController mDisplayInsetsController;
204     private final TransactionPool mTransactionPool;
205     private SplitScreenTransitions mSplitTransitions;
206     private final SplitscreenEventLogger mLogger;
207     private final ShellExecutor mMainExecutor;
208     // Cache live tile tasks while entering recents, evict them from stages in finish transaction
209     // if user is opening another task(s).
210     private final ArrayList<Integer> mPausingTasks = new ArrayList<>();
211     private final Optional<RecentTasksController> mRecentTasks;
212     private final LaunchAdjacentController mLaunchAdjacentController;
213     private final Optional<WindowDecorViewModel> mWindowDecorViewModel;
214 
215     private final Rect mTempRect1 = new Rect();
216     private final Rect mTempRect2 = new Rect();
217 
218     /**
219      * A single-top root task which the split divider attached to.
220      */
221     @VisibleForTesting
222     ActivityManager.RunningTaskInfo mRootTaskInfo;
223 
224     private SurfaceControl mRootTaskLeash;
225 
226     // Tracks whether we should update the recent tasks.  Only allow this to happen in between enter
227     // and exit, since exit itself can trigger a number of changes that update the stages.
228     private boolean mShouldUpdateRecents;
229     private boolean mExitSplitScreenOnHide;
230     private boolean mIsDividerRemoteAnimating;
231     private boolean mIsDropEntering;
232     private boolean mSkipEvictingMainStageChildren;
233     private boolean mIsExiting;
234     private boolean mIsRootTranslucent;
235     @VisibleForTesting
236     int mTopStageAfterFoldDismiss;
237 
238     private DefaultMixedHandler mMixedHandler;
239     private final Toast mSplitUnsupportedToast;
240     private SplitRequest mSplitRequest;
241     /** Used to notify others of when shell is animating into split screen */
242     private SplitScreen.SplitInvocationListener mSplitInvocationListener;
243     private Executor mSplitInvocationListenerExecutor;
244 
245     /**
246      * Since StageCoordinator only coordinates MainStage and SideStage, it shouldn't support
247      * CompatUI layouts. CompatUI is handled separately by MainStage and SideStage.
248      */
249     @Override
supportCompatUI()250     public boolean supportCompatUI() {
251         return false;
252     }
253 
254     /** NOTE: Will overwrite any previously set {@link #mSplitInvocationListener} */
registerSplitAnimationListener( @onNull SplitScreen.SplitInvocationListener listener, @NonNull Executor executor)255     public void registerSplitAnimationListener(
256             @NonNull SplitScreen.SplitInvocationListener listener, @NonNull Executor executor) {
257         mSplitInvocationListener = listener;
258         mSplitInvocationListenerExecutor = executor;
259         mSplitTransitions.registerSplitAnimListener(listener, executor);
260     }
261 
262     class SplitRequest {
263         @SplitPosition
264         int mActivatePosition;
265         int mActivateTaskId;
266         int mActivateTaskId2;
267         Intent mStartIntent;
268         Intent mStartIntent2;
269 
SplitRequest(int taskId, Intent startIntent, int position)270         SplitRequest(int taskId, Intent startIntent, int position) {
271             mActivateTaskId = taskId;
272             mStartIntent = startIntent;
273             mActivatePosition = position;
274         }
SplitRequest(Intent startIntent, int position)275         SplitRequest(Intent startIntent, int position) {
276             mStartIntent = startIntent;
277             mActivatePosition = position;
278         }
SplitRequest(Intent startIntent, Intent startIntent2, int position)279         SplitRequest(Intent startIntent, Intent startIntent2, int position) {
280             mStartIntent = startIntent;
281             mStartIntent2 = startIntent2;
282             mActivatePosition = position;
283         }
SplitRequest(int taskId1, int position)284         SplitRequest(int taskId1, int position) {
285             mActivateTaskId = taskId1;
286             mActivatePosition = position;
287         }
SplitRequest(int taskId1, int taskId2, int position)288         SplitRequest(int taskId1, int taskId2, int position) {
289             mActivateTaskId = taskId1;
290             mActivateTaskId2 = taskId2;
291             mActivatePosition = position;
292         }
293     }
294 
295     private final SplitWindowManager.ParentContainerCallbacks mParentContainerCallbacks =
296             new SplitWindowManager.ParentContainerCallbacks() {
297                 @Override
298                 public void attachToParentSurface(SurfaceControl.Builder b) {
299                     b.setParent(mRootTaskLeash);
300                 }
301 
302                 @Override
303                 public void onLeashReady(SurfaceControl leash) {
304                     // This is for avoiding divider invisible due to delay of creating so only need
305                     // to do when divider should visible case.
306                     if (mDividerVisible) {
307                         mSyncQueue.runInSync(t -> applyDividerVisibility(t));
308                     }
309                 }
310             };
311 
StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, ShellTaskOrganizer taskOrganizer, DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider, ShellExecutor mainExecutor, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, Optional<WindowDecorViewModel> windowDecorViewModel)312     protected StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
313             ShellTaskOrganizer taskOrganizer, DisplayController displayController,
314             DisplayImeController displayImeController,
315             DisplayInsetsController displayInsetsController, Transitions transitions,
316             TransactionPool transactionPool,
317             IconProvider iconProvider, ShellExecutor mainExecutor,
318             Optional<RecentTasksController> recentTasks,
319             LaunchAdjacentController launchAdjacentController,
320             Optional<WindowDecorViewModel> windowDecorViewModel) {
321         mContext = context;
322         mDisplayId = displayId;
323         mSyncQueue = syncQueue;
324         mTaskOrganizer = taskOrganizer;
325         mLogger = new SplitscreenEventLogger();
326         mMainExecutor = mainExecutor;
327         mRecentTasks = recentTasks;
328         mLaunchAdjacentController = launchAdjacentController;
329         mWindowDecorViewModel = windowDecorViewModel;
330 
331         taskOrganizer.createRootTask(displayId, WINDOWING_MODE_FULLSCREEN, this /* listener */);
332 
333         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Creating main/side root task");
334         mMainStage = new MainStage(
335                 mContext,
336                 mTaskOrganizer,
337                 mDisplayId,
338                 mMainStageListener,
339                 mSyncQueue,
340                 mSurfaceSession,
341                 iconProvider,
342                 mWindowDecorViewModel);
343         mSideStage = new SideStage(
344                 mContext,
345                 mTaskOrganizer,
346                 mDisplayId,
347                 mSideStageListener,
348                 mSyncQueue,
349                 mSurfaceSession,
350                 iconProvider,
351                 mWindowDecorViewModel);
352         mDisplayController = displayController;
353         mDisplayImeController = displayImeController;
354         mDisplayInsetsController = displayInsetsController;
355         mTransactionPool = transactionPool;
356         final DeviceStateManager deviceStateManager =
357                 mContext.getSystemService(DeviceStateManager.class);
358         deviceStateManager.registerCallback(taskOrganizer.getExecutor(),
359                 new DeviceStateManager.FoldStateListener(mContext, this::onFoldedStateChanged));
360         mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
361                 this::onTransitionAnimationComplete, this);
362         mDisplayController.addDisplayWindowListener(this);
363         transitions.addHandler(this);
364         mSplitUnsupportedToast = Toast.makeText(mContext,
365                 R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT);
366         // With shell transition, we should update recents tile each callback so set this to true by
367         // default.
368         mShouldUpdateRecents = ENABLE_SHELL_TRANSITIONS;
369     }
370 
371     @VisibleForTesting
StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue, ShellTaskOrganizer taskOrganizer, MainStage mainStage, SideStage sideStage, DisplayController displayController, DisplayImeController displayImeController, DisplayInsetsController displayInsetsController, SplitLayout splitLayout, Transitions transitions, TransactionPool transactionPool, ShellExecutor mainExecutor, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, Optional<WindowDecorViewModel> windowDecorViewModel)372     StageCoordinator(Context context, int displayId, SyncTransactionQueue syncQueue,
373             ShellTaskOrganizer taskOrganizer, MainStage mainStage, SideStage sideStage,
374             DisplayController displayController, DisplayImeController displayImeController,
375             DisplayInsetsController displayInsetsController, SplitLayout splitLayout,
376             Transitions transitions, TransactionPool transactionPool,
377             ShellExecutor mainExecutor,
378             Optional<RecentTasksController> recentTasks,
379             LaunchAdjacentController launchAdjacentController,
380             Optional<WindowDecorViewModel> windowDecorViewModel) {
381         mContext = context;
382         mDisplayId = displayId;
383         mSyncQueue = syncQueue;
384         mTaskOrganizer = taskOrganizer;
385         mMainStage = mainStage;
386         mSideStage = sideStage;
387         mDisplayController = displayController;
388         mDisplayImeController = displayImeController;
389         mDisplayInsetsController = displayInsetsController;
390         mTransactionPool = transactionPool;
391         mSplitLayout = splitLayout;
392         mSplitTransitions = new SplitScreenTransitions(transactionPool, transitions,
393                 this::onTransitionAnimationComplete, this);
394         mLogger = new SplitscreenEventLogger();
395         mMainExecutor = mainExecutor;
396         mRecentTasks = recentTasks;
397         mLaunchAdjacentController = launchAdjacentController;
398         mWindowDecorViewModel = windowDecorViewModel;
399         mDisplayController.addDisplayWindowListener(this);
400         transitions.addHandler(this);
401         mSplitUnsupportedToast = Toast.makeText(mContext,
402                 R.string.dock_non_resizeble_failed_to_dock_text, Toast.LENGTH_SHORT);
403     }
404 
setMixedHandler(DefaultMixedHandler mixedHandler)405     public void setMixedHandler(DefaultMixedHandler mixedHandler) {
406         mMixedHandler = mixedHandler;
407     }
408 
409     @VisibleForTesting
getSplitTransitions()410     SplitScreenTransitions getSplitTransitions() {
411         return mSplitTransitions;
412     }
413 
414     @VisibleForTesting
setSplitTransitions(SplitScreenTransitions splitScreenTransitions)415     void setSplitTransitions(SplitScreenTransitions splitScreenTransitions) {
416         mSplitTransitions = splitScreenTransitions;
417     }
418 
isSplitScreenVisible()419     public boolean isSplitScreenVisible() {
420         return mSideStageListener.mVisible && mMainStageListener.mVisible;
421     }
422 
isSplitActive()423     public boolean isSplitActive() {
424         return mMainStage.isActive();
425     }
426 
427     /** @return whether this transition-request has the launch-adjacent flag. */
requestHasLaunchAdjacentFlag(TransitionRequestInfo request)428     public boolean requestHasLaunchAdjacentFlag(TransitionRequestInfo request) {
429         final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
430         return triggerTask != null && triggerTask.baseIntent != null
431                 && (triggerTask.baseIntent.getFlags() & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0;
432     }
433 
434     /** @return whether the transition-request implies entering pip from split. */
requestImpliesSplitToPip(TransitionRequestInfo request)435     public boolean requestImpliesSplitToPip(TransitionRequestInfo request) {
436         if (!isSplitActive() || !mMixedHandler.requestHasPipEnter(request)) {
437             return false;
438         }
439 
440         if (request.getTriggerTask() != null && getSplitPosition(
441                 request.getTriggerTask().taskId) != SPLIT_POSITION_UNDEFINED) {
442             return true;
443         }
444 
445         // If one of the splitting tasks support auto-pip, wm-core might reparent the task to TDA
446         // and file a TRANSIT_PIP transition when finishing transitions.
447         // @see com.android.server.wm.RootWindowContainer#moveActivityToPinnedRootTask
448         if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) {
449             return true;
450         }
451 
452         return false;
453     }
454 
455     /** Checks if `transition` is a pending enter-split transition. */
isPendingEnter(IBinder transition)456     public boolean isPendingEnter(IBinder transition) {
457         return mSplitTransitions.isPendingEnter(transition);
458     }
459 
460     @StageType
getStageOfTask(int taskId)461     int getStageOfTask(int taskId) {
462         if (mMainStage.containsTask(taskId)) {
463             return STAGE_TYPE_MAIN;
464         } else if (mSideStage.containsTask(taskId)) {
465             return STAGE_TYPE_SIDE;
466         }
467 
468         return STAGE_TYPE_UNDEFINED;
469     }
470 
isRootOrStageRoot(int taskId)471     boolean isRootOrStageRoot(int taskId) {
472         if (mRootTaskInfo != null && mRootTaskInfo.taskId == taskId) {
473             return true;
474         }
475         return mMainStage.isRootTaskId(taskId) || mSideStage.isRootTaskId(taskId);
476     }
477 
moveToStage(ActivityManager.RunningTaskInfo task, @SplitPosition int stagePosition, WindowContainerTransaction wct)478     boolean moveToStage(ActivityManager.RunningTaskInfo task, @SplitPosition int stagePosition,
479             WindowContainerTransaction wct) {
480         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "moveToStage: task=%d position=%d", task.taskId,
481                 stagePosition);
482         prepareEnterSplitScreen(wct, task, stagePosition, false /* resizeAnim */);
483         if (ENABLE_SHELL_TRANSITIONS) {
484             mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct,
485                     null, this,
486                     isSplitScreenVisible()
487                             ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN,
488                     !mIsDropEntering);
489         } else {
490             mSyncQueue.queue(wct);
491             mSyncQueue.runInSync(t -> {
492                 updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
493             });
494         }
495         // Due to drag already pip task entering split by this method so need to reset flag here.
496         mIsDropEntering = false;
497         mSkipEvictingMainStageChildren = false;
498         return true;
499     }
500 
removeFromSideStage(int taskId)501     boolean removeFromSideStage(int taskId) {
502         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "removeFromSideStage: task=%d", taskId);
503         final WindowContainerTransaction wct = new WindowContainerTransaction();
504 
505         /**
506          * {@link MainStage} will be deactivated in {@link #onStageHasChildrenChanged} if the
507          * {@link SideStage} no longer has children.
508          */
509         final boolean result = mSideStage.removeTask(taskId,
510                 mMainStage.isActive() ? mMainStage.mRootTaskInfo.token : null,
511                 wct);
512         mTaskOrganizer.applyTransaction(wct);
513         return result;
514     }
515 
getLogger()516     SplitscreenEventLogger getLogger() {
517         return mLogger;
518     }
519 
requestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo, WindowContainerTransaction wct, int splitPosition, Rect taskBounds)520     void requestEnterSplitSelect(ActivityManager.RunningTaskInfo taskInfo,
521             WindowContainerTransaction wct, int splitPosition, Rect taskBounds) {
522         boolean enteredSplitSelect = false;
523         for (SplitScreen.SplitSelectListener listener : mSelectListeners) {
524             enteredSplitSelect |= listener.onRequestEnterSplitSelect(taskInfo, splitPosition,
525                     taskBounds);
526         }
527         if (enteredSplitSelect) {
528             mTaskOrganizer.applyTransaction(wct);
529         }
530     }
531 
startShortcut(String packageName, String shortcutId, @SplitPosition int position, Bundle options, UserHandle user)532     void startShortcut(String packageName, String shortcutId, @SplitPosition int position,
533             Bundle options, UserHandle user) {
534         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startShortcut: pkg=%s id=%s position=%d user=%d",
535                 packageName, shortcutId, position, user.getIdentifier());
536         final boolean isEnteringSplit = !isSplitActive();
537 
538         IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
539             @Override
540             public void onAnimationStart(@WindowManager.TransitionOldType int transit,
541                     RemoteAnimationTarget[] apps,
542                     RemoteAnimationTarget[] wallpapers,
543                     RemoteAnimationTarget[] nonApps,
544                     final IRemoteAnimationFinishedCallback finishedCallback) {
545                 if (isEnteringSplit && mSideStage.getChildCount() == 0) {
546                     mMainExecutor.execute(() -> exitSplitScreen(
547                             null /* childrenToTop */, EXIT_REASON_UNKNOWN));
548                     Log.w(TAG, splitFailureMessage("startShortcut",
549                             "side stage was not populated"));
550                     handleUnsupportedSplitStart();
551                 }
552 
553                 if (finishedCallback != null) {
554                     try {
555                         finishedCallback.onAnimationFinished();
556                     } catch (RemoteException e) {
557                         Slog.e(TAG, "Error finishing legacy transition: ", e);
558                     }
559                 }
560 
561                 if (!isEnteringSplit && apps != null) {
562                     final WindowContainerTransaction evictWct = new WindowContainerTransaction();
563                     prepareEvictNonOpeningChildTasks(position, apps, evictWct);
564                     mSyncQueue.queue(evictWct);
565                 }
566             }
567             @Override
568             public void onAnimationCancelled() {
569                 if (isEnteringSplit) {
570                     mMainExecutor.execute(() -> exitSplitScreen(
571                             mSideStage.getChildCount() == 0 ? mMainStage : mSideStage,
572                             EXIT_REASON_UNKNOWN));
573                 }
574             }
575         };
576         options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
577                 null /* wct */);
578         RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(wrapper,
579                 0 /* duration */, 0 /* statusBarTransitionDelay */);
580         ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
581         // Flag this as a no-user-action launch to prevent sending user leaving event to the current
582         // top activity since it's going to be put into another side of the split. This prevents the
583         // current top activity from going into pip mode due to user leaving event.
584         activityOptions.setApplyNoUserActionFlagForShortcut(true);
585         activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
586         try {
587             LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
588             launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */,
589                     activityOptions.toBundle(), user);
590         } catch (ActivityNotFoundException e) {
591             Slog.e(TAG, "Failed to launch shortcut", e);
592         }
593     }
594 
595     /** Use this method to launch an existing Task via a taskId */
startTask(int taskId, @SplitPosition int position, @Nullable Bundle options)596     void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options) {
597         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startTask: task=%d position=%d", taskId, position);
598         mSplitRequest = new SplitRequest(taskId, position);
599         final WindowContainerTransaction wct = new WindowContainerTransaction();
600         options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */);
601         wct.startTask(taskId, options);
602         // If this should be mixed, send the task to avoid split handle transition directly.
603         if (mMixedHandler != null && mMixedHandler.isTaskInPip(taskId, mTaskOrganizer)) {
604             mTaskOrganizer.applyTransaction(wct);
605             return;
606         }
607 
608         // Don't evict the main stage children as this can race and happen after the activity is
609         // started into that stage
610         if (!isSplitScreenVisible()) {
611             mSkipEvictingMainStageChildren = true;
612             // Starting the split task without evicting children will bring the single root task
613             // container forward, so ensure that we hide the divider before we start animate it
614             setDividerVisibility(false, null);
615         }
616 
617         // If split screen is not activated, we're expecting to open a pair of apps to split.
618         final int extraTransitType = mMainStage.isActive()
619                 ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
620         prepareEnterSplitScreen(wct, null /* taskInfo */, position, !mIsDropEntering);
621 
622         mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, null, this,
623                 extraTransitType, !mIsDropEntering);
624     }
625 
626     /** Launches an activity into split. */
startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options)627     void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
628             @Nullable Bundle options) {
629         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startIntent: intent=%s position=%d", intent.getIntent(),
630                 position);
631         mSplitRequest = new SplitRequest(intent.getIntent(), position);
632         if (!ENABLE_SHELL_TRANSITIONS) {
633             startIntentLegacy(intent, fillInIntent, position, options);
634             return;
635         }
636 
637         final WindowContainerTransaction wct = new WindowContainerTransaction();
638         options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */);
639         wct.sendPendingIntent(intent, fillInIntent, options);
640 
641         // If this should be mixed, just send the intent to avoid split handle transition directly.
642         if (mMixedHandler != null && mMixedHandler.isIntentInPip(intent)) {
643             mTaskOrganizer.applyTransaction(wct);
644             return;
645         }
646 
647         // Don't evict the main stage children as this can race and happen after the activity is
648         // started into that stage
649         if (!isSplitScreenVisible()) {
650             mSkipEvictingMainStageChildren = true;
651             // Starting the split task without evicting children will bring the single root task
652             // container forward, so ensure that we hide the divider before we start animate it
653             setDividerVisibility(false, null);
654         }
655 
656         // If split screen is not activated, we're expecting to open a pair of apps to split.
657         final int extraTransitType = mMainStage.isActive()
658                 ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN;
659         prepareEnterSplitScreen(wct, null /* taskInfo */, position, !mIsDropEntering);
660 
661         mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, null, this,
662                 extraTransitType, !mIsDropEntering);
663     }
664 
665     /** Launches an activity into split by legacy transition. */
startIntentLegacy(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options)666     void startIntentLegacy(PendingIntent intent, Intent fillInIntent, @SplitPosition int position,
667             @Nullable Bundle options) {
668         final boolean isEnteringSplit = !isSplitActive();
669 
670         LegacyTransitions.ILegacyTransition transition = new LegacyTransitions.ILegacyTransition() {
671             @Override
672             public void onAnimationStart(int transit, RemoteAnimationTarget[] apps,
673                     RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
674                     IRemoteAnimationFinishedCallback finishedCallback,
675                     SurfaceControl.Transaction t) {
676                 if (isEnteringSplit && mSideStage.getChildCount() == 0) {
677                     mMainExecutor.execute(() -> exitSplitScreen(
678                             null /* childrenToTop */, EXIT_REASON_UNKNOWN));
679                     Log.w(TAG, splitFailureMessage("startIntentLegacy",
680                             "side stage was not populated"));
681                     handleUnsupportedSplitStart();
682                 }
683 
684                 if (apps != null) {
685                     for (int i = 0; i < apps.length; ++i) {
686                         if (apps[i].mode == MODE_OPENING) {
687                             t.show(apps[i].leash);
688                         }
689                     }
690                 }
691                 t.apply();
692 
693                 if (finishedCallback != null) {
694                     try {
695                         finishedCallback.onAnimationFinished();
696                     } catch (RemoteException e) {
697                         Slog.e(TAG, "Error finishing legacy transition: ", e);
698                     }
699                 }
700 
701 
702                 if (!isEnteringSplit && apps != null) {
703                     final WindowContainerTransaction evictWct = new WindowContainerTransaction();
704                     prepareEvictNonOpeningChildTasks(position, apps, evictWct);
705                     mSyncQueue.queue(evictWct);
706                 }
707             }
708         };
709 
710         final WindowContainerTransaction wct = new WindowContainerTransaction();
711         options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, wct);
712 
713         // If split still not active, apply windows bounds first to avoid surface reset to
714         // wrong pos by SurfaceAnimator from wms.
715         if (isEnteringSplit && mLogger.isEnterRequestedByDrag()) {
716             updateWindowBounds(mSplitLayout, wct);
717         }
718         wct.sendPendingIntent(intent, fillInIntent, options);
719         mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct);
720     }
721 
722     /** Starts 2 tasks in one transition. */
startTasks(int taskId1, @Nullable Bundle options1, int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition, InstanceId instanceId)723     void startTasks(int taskId1, @Nullable Bundle options1, int taskId2, @Nullable Bundle options2,
724             @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition,
725             @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
726         ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
727                 "startTasks: task1=%d task2=%d position=%d snapPosition=%d",
728                 taskId1, taskId2, splitPosition, snapPosition);
729         final WindowContainerTransaction wct = new WindowContainerTransaction();
730         if (taskId2 == INVALID_TASK_ID) {
731             startSingleTask(taskId1, options1, wct, remoteTransition);
732             return;
733         }
734 
735         setSideStagePosition(splitPosition, wct);
736         options1 = options1 != null ? options1 : new Bundle();
737         addActivityOptions(options1, mSideStage);
738         wct.startTask(taskId1, options1);
739 
740         startWithTask(wct, taskId2, options2, snapPosition, remoteTransition, instanceId);
741     }
742 
743     /** Start an intent and a task to a split pair in one transition. */
startIntentAndTask(PendingIntent pendingIntent, Intent fillInIntent, @Nullable Bundle options1, int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition, InstanceId instanceId)744     void startIntentAndTask(PendingIntent pendingIntent, Intent fillInIntent,
745             @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
746             @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition,
747             @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
748         ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
749                 "startIntentAndTask: intent=%s task1=%d position=%d snapPosition=%d",
750                 pendingIntent.getIntent(), taskId, splitPosition, snapPosition);
751         final WindowContainerTransaction wct = new WindowContainerTransaction();
752         boolean firstIntentPipped = mMixedHandler.isIntentInPip(pendingIntent);
753         boolean secondTaskPipped = mMixedHandler.isTaskInPip(taskId, mTaskOrganizer);
754         if (taskId == INVALID_TASK_ID || secondTaskPipped) {
755             startSingleIntent(pendingIntent, fillInIntent, options1, wct, remoteTransition);
756             return;
757         }
758 
759         if (firstIntentPipped) {
760             startSingleTask(taskId, options2, wct, remoteTransition);
761             return;
762         }
763 
764         setSideStagePosition(splitPosition, wct);
765         options1 = options1 != null ? options1 : new Bundle();
766         addActivityOptions(options1, mSideStage);
767         wct.sendPendingIntent(pendingIntent, fillInIntent, options1);
768 
769         startWithTask(wct, taskId, options2, snapPosition, remoteTransition, instanceId);
770     }
771 
772     /**
773      * @param taskId Starts this task in fullscreen, removing it from existing pairs if it was part
774      *               of one.
775      */
startSingleTask(int taskId, Bundle options, WindowContainerTransaction wct, RemoteTransition remoteTransition)776     private void startSingleTask(int taskId, Bundle options, WindowContainerTransaction wct,
777             RemoteTransition remoteTransition) {
778         if (mMainStage.containsTask(taskId) || mSideStage.containsTask(taskId)) {
779             prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, wct);
780         }
781         if (mRecentTasks.isPresent()) {
782             mRecentTasks.get().removeSplitPair(taskId);
783         }
784         options = options != null ? options : new Bundle();
785         addActivityOptions(options, null);
786         wct.startTask(taskId, options);
787         mSplitTransitions.startFullscreenTransition(wct, remoteTransition);
788     }
789 
790     /** Starts a shortcut and a task to a split pair in one transition. */
startShortcutAndTask(ShortcutInfo shortcutInfo, @Nullable Bundle options1, int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition, InstanceId instanceId)791     void startShortcutAndTask(ShortcutInfo shortcutInfo, @Nullable Bundle options1,
792             int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition,
793             @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition,
794             InstanceId instanceId) {
795         ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
796                 "startShortcutAndTask: shortcut=%s task1=%d position=%d snapPosition=%d",
797                 shortcutInfo, taskId, splitPosition, snapPosition);
798         final WindowContainerTransaction wct = new WindowContainerTransaction();
799         if (taskId == INVALID_TASK_ID) {
800             options1 = options1 != null ? options1 : new Bundle();
801             addActivityOptions(options1, null);
802             wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1);
803             mSplitTransitions.startFullscreenTransition(wct, remoteTransition);
804             return;
805         }
806 
807         setSideStagePosition(splitPosition, wct);
808         options1 = options1 != null ? options1 : new Bundle();
809         addActivityOptions(options1, mSideStage);
810         wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1);
811 
812         startWithTask(wct, taskId, options2, snapPosition, remoteTransition, instanceId);
813     }
814 
815     /**
816      * Starts with the second task to a split pair in one transition.
817      *
818      * @param wct        transaction to start the first task
819      * @param instanceId if {@code null}, will not log. Otherwise it will be used in
820      *                   {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)}
821      */
startWithTask(WindowContainerTransaction wct, int mainTaskId, @Nullable Bundle mainOptions, @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition, InstanceId instanceId)822     private void startWithTask(WindowContainerTransaction wct, int mainTaskId,
823             @Nullable Bundle mainOptions, @PersistentSnapPosition int snapPosition,
824             @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
825         if (!mMainStage.isActive()) {
826             // Build a request WCT that will launch both apps such that task 0 is on the main stage
827             // while task 1 is on the side stage.
828             mMainStage.activate(wct, false /* reparent */);
829         }
830         mSplitLayout.setDivideRatio(snapPosition);
831         updateWindowBounds(mSplitLayout, wct);
832         wct.reorder(mRootTaskInfo.token, true);
833         wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
834                 false /* reparentLeafTaskIfRelaunch */);
835         setRootForceTranslucent(false, wct);
836 
837         // Make sure the launch options will put tasks in the corresponding split roots
838         mainOptions = mainOptions != null ? mainOptions : new Bundle();
839         addActivityOptions(mainOptions, mMainStage);
840 
841         // Add task launch requests
842         wct.startTask(mainTaskId, mainOptions);
843 
844         // leave recents animation by re-start pausing tasks
845         if (mPausingTasks.contains(mainTaskId)) {
846             mPausingTasks.clear();
847         }
848         mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, remoteTransition, this,
849                 TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false);
850         setEnterInstanceId(instanceId);
851     }
852 
startIntents(PendingIntent pendingIntent1, Intent fillInIntent1, @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1, @Nullable PendingIntent pendingIntent2, Intent fillInIntent2, @Nullable ShortcutInfo shortcutInfo2, @Nullable Bundle options2, @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition, InstanceId instanceId)853     void startIntents(PendingIntent pendingIntent1, Intent fillInIntent1,
854             @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1,
855             @Nullable PendingIntent pendingIntent2, Intent fillInIntent2,
856             @Nullable ShortcutInfo shortcutInfo2, @Nullable Bundle options2,
857             @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition,
858             @Nullable RemoteTransition remoteTransition, InstanceId instanceId) {
859         ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
860                 "startIntents: intent1=%s intent2=%s position=%d snapPosition=%d",
861                 pendingIntent1.getIntent(),
862                 (pendingIntent2 != null ? pendingIntent2.getIntent() : "null"),
863                 splitPosition, snapPosition);
864         final WindowContainerTransaction wct = new WindowContainerTransaction();
865         if (pendingIntent2 == null) {
866             options1 = options1 != null ? options1 : new Bundle();
867             addActivityOptions(options1, null);
868             if (shortcutInfo1 != null) {
869                 wct.startShortcut(mContext.getPackageName(), shortcutInfo1, options1);
870             } else {
871                 wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1);
872             }
873             mSplitTransitions.startFullscreenTransition(wct, remoteTransition);
874             return;
875         }
876 
877         boolean handledForPipSplitLaunch = handlePippedSplitIntentsLaunch(
878                 pendingIntent1,
879                 pendingIntent2,
880                 options1,
881                 options2,
882                 shortcutInfo1,
883                 shortcutInfo2,
884                 wct,
885                 fillInIntent1,
886                 fillInIntent2,
887                 remoteTransition);
888         if (handledForPipSplitLaunch) {
889             return;
890         }
891 
892         if (!mMainStage.isActive()) {
893             // Build a request WCT that will launch both apps such that task 0 is on the main stage
894             // while task 1 is on the side stage.
895             mMainStage.activate(wct, false /* reparent */);
896         }
897 
898         setSideStagePosition(splitPosition, wct);
899         mSplitLayout.setDivideRatio(snapPosition);
900         updateWindowBounds(mSplitLayout, wct);
901         wct.reorder(mRootTaskInfo.token, true);
902         wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
903                 false /* reparentLeafTaskIfRelaunch */);
904         setRootForceTranslucent(false, wct);
905 
906         options1 = options1 != null ? options1 : new Bundle();
907         addActivityOptions(options1, mSideStage);
908         if (shortcutInfo1 != null) {
909             wct.startShortcut(mContext.getPackageName(), shortcutInfo1, options1);
910         } else {
911             wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1);
912         }
913         options2 = options2 != null ? options2 : new Bundle();
914         addActivityOptions(options2, mMainStage);
915         if (shortcutInfo2 != null) {
916             wct.startShortcut(mContext.getPackageName(), shortcutInfo2, options2);
917         } else {
918             wct.sendPendingIntent(pendingIntent2, fillInIntent2, options2);
919         }
920 
921         mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, remoteTransition, this,
922                 TRANSIT_SPLIT_SCREEN_PAIR_OPEN, false);
923         setEnterInstanceId(instanceId);
924     }
925 
926     /**
927      * Checks if either of the apps in the desired split launch is currently in Pip. If so, it will
928      * launch the non-pipped app as a fullscreen app, otherwise no-op.
929      */
handlePippedSplitIntentsLaunch(PendingIntent pendingIntent1, PendingIntent pendingIntent2, Bundle options1, Bundle options2, ShortcutInfo shortcutInfo1, ShortcutInfo shortcutInfo2, WindowContainerTransaction wct, Intent fillInIntent1, Intent fillInIntent2, RemoteTransition remoteTransition)930     private boolean handlePippedSplitIntentsLaunch(PendingIntent pendingIntent1,
931             PendingIntent pendingIntent2, Bundle options1, Bundle options2,
932             ShortcutInfo shortcutInfo1, ShortcutInfo shortcutInfo2, WindowContainerTransaction wct,
933             Intent fillInIntent1, Intent fillInIntent2, RemoteTransition remoteTransition) {
934         // If one of the split apps to start is in Pip, only launch the non-pip app in fullscreen
935         boolean firstIntentPipped = mMixedHandler.isIntentInPip(pendingIntent1);
936         boolean secondIntentPipped = mMixedHandler.isIntentInPip(pendingIntent2);
937         if (firstIntentPipped || secondIntentPipped) {
938             Bundle options = secondIntentPipped ? options1 : options2;
939             options = options == null ? new Bundle() : options;
940             addActivityOptions(options, null);
941             if (shortcutInfo1 != null || shortcutInfo2 != null) {
942                 ShortcutInfo infoToLaunch = secondIntentPipped ? shortcutInfo1 : shortcutInfo2;
943                 wct.startShortcut(mContext.getPackageName(), infoToLaunch, options);
944                 mSplitTransitions.startFullscreenTransition(wct, remoteTransition);
945             } else {
946                 PendingIntent intentToLaunch = secondIntentPipped ? pendingIntent1 : pendingIntent2;
947                 Intent fillInIntentToLaunch = secondIntentPipped ? fillInIntent1 : fillInIntent2;
948                 startSingleIntent(intentToLaunch, fillInIntentToLaunch, options, wct,
949                         remoteTransition);
950             }
951             return true;
952         }
953         return false;
954     }
955 
956     /** @param pendingIntent Starts this intent in fullscreen */
startSingleIntent(PendingIntent pendingIntent, Intent fillInIntent, Bundle options, WindowContainerTransaction wct, RemoteTransition remoteTransition)957     private void startSingleIntent(PendingIntent pendingIntent, Intent fillInIntent, Bundle options,
958             WindowContainerTransaction wct,
959             RemoteTransition remoteTransition) {
960         Bundle optionsToLaunch = options != null ? options : new Bundle();
961         addActivityOptions(optionsToLaunch, null);
962         wct.sendPendingIntent(pendingIntent, fillInIntent, optionsToLaunch);
963         mSplitTransitions.startFullscreenTransition(wct, remoteTransition);
964     }
965 
966     /** Starts a pair of tasks using legacy transition. */
startTasksWithLegacyTransition(int taskId1, @Nullable Bundle options1, int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter, InstanceId instanceId)967     void startTasksWithLegacyTransition(int taskId1, @Nullable Bundle options1,
968             int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition,
969             @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter,
970             InstanceId instanceId) {
971         final WindowContainerTransaction wct = new WindowContainerTransaction();
972         if (options1 == null) options1 = new Bundle();
973         if (taskId2 == INVALID_TASK_ID) {
974             // Launching a solo task.
975             // Exit split first if this task under split roots.
976             if (mMainStage.containsTask(taskId1) || mSideStage.containsTask(taskId1)) {
977                 exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT);
978             }
979             ActivityOptions activityOptions = ActivityOptions.fromBundle(options1);
980             activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter));
981             options1 = activityOptions.toBundle();
982             addActivityOptions(options1, null /* launchTarget */);
983             wct.startTask(taskId1, options1);
984             mSyncQueue.queue(wct);
985             return;
986         }
987 
988         addActivityOptions(options1, mSideStage);
989         wct.startTask(taskId1, options1);
990         mSplitRequest = new SplitRequest(taskId1, taskId2, splitPosition);
991         startWithLegacyTransition(wct, taskId2, options2, splitPosition, snapPosition, adapter,
992                 instanceId);
993     }
994 
995     /** Starts a pair of intents using legacy transition. */
startIntentsWithLegacyTransition(PendingIntent pendingIntent1, Intent fillInIntent1, @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1, @Nullable PendingIntent pendingIntent2, Intent fillInIntent2, @Nullable ShortcutInfo shortcutInfo2, @Nullable Bundle options2, @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter, InstanceId instanceId)996     void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, Intent fillInIntent1,
997             @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1,
998             @Nullable PendingIntent pendingIntent2, Intent fillInIntent2,
999             @Nullable ShortcutInfo shortcutInfo2, @Nullable Bundle options2,
1000             @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition,
1001             RemoteAnimationAdapter adapter, InstanceId instanceId) {
1002         final WindowContainerTransaction wct = new WindowContainerTransaction();
1003         if (options1 == null) options1 = new Bundle();
1004         if (pendingIntent2 == null) {
1005             // Launching a solo intent or shortcut as fullscreen.
1006             launchAsFullscreenWithRemoteAnimation(pendingIntent1, fillInIntent1, shortcutInfo1,
1007                     options1, adapter, wct);
1008             return;
1009         }
1010 
1011         addActivityOptions(options1, mSideStage);
1012         if (shortcutInfo1 != null) {
1013             wct.startShortcut(mContext.getPackageName(), shortcutInfo1, options1);
1014         } else {
1015             wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1);
1016             mSplitRequest = new SplitRequest(pendingIntent1.getIntent(),
1017                     pendingIntent2 != null ? pendingIntent2.getIntent() : null, splitPosition);
1018         }
1019         startWithLegacyTransition(wct, pendingIntent2, fillInIntent2, shortcutInfo2, options2,
1020                 splitPosition, snapPosition, adapter, instanceId);
1021     }
1022 
startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, Intent fillInIntent, @Nullable Bundle options1, int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter, InstanceId instanceId)1023     void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, Intent fillInIntent,
1024             @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
1025             @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition,
1026             RemoteAnimationAdapter adapter, InstanceId instanceId) {
1027         final WindowContainerTransaction wct = new WindowContainerTransaction();
1028         if (options1 == null) options1 = new Bundle();
1029         if (taskId == INVALID_TASK_ID) {
1030             // Launching a solo intent as fullscreen.
1031             launchAsFullscreenWithRemoteAnimation(pendingIntent, fillInIntent, null, options1,
1032                     adapter, wct);
1033             return;
1034         }
1035 
1036         addActivityOptions(options1, mSideStage);
1037         wct.sendPendingIntent(pendingIntent, fillInIntent, options1);
1038         mSplitRequest = new SplitRequest(taskId, pendingIntent.getIntent(), splitPosition);
1039         startWithLegacyTransition(wct, taskId, options2, splitPosition, snapPosition, adapter,
1040                 instanceId);
1041     }
1042 
1043     /** Starts a pair of shortcut and task using legacy transition. */
startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo, @Nullable Bundle options1, int taskId, @Nullable Bundle options2, @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter, InstanceId instanceId)1044     void startShortcutAndTaskWithLegacyTransition(ShortcutInfo shortcutInfo,
1045             @Nullable Bundle options1, int taskId, @Nullable Bundle options2,
1046             @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition,
1047             RemoteAnimationAdapter adapter, InstanceId instanceId) {
1048         final WindowContainerTransaction wct = new WindowContainerTransaction();
1049         if (options1 == null) options1 = new Bundle();
1050         if (taskId == INVALID_TASK_ID) {
1051             // Launching a solo shortcut as fullscreen.
1052             launchAsFullscreenWithRemoteAnimation(null, null, shortcutInfo, options1, adapter, wct);
1053             return;
1054         }
1055 
1056         addActivityOptions(options1, mSideStage);
1057         wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1);
1058         startWithLegacyTransition(wct, taskId, options2, splitPosition, snapPosition, adapter,
1059                 instanceId);
1060     }
1061 
launchAsFullscreenWithRemoteAnimation(@ullable PendingIntent pendingIntent, @Nullable Intent fillInIntent, @Nullable ShortcutInfo shortcutInfo, @Nullable Bundle options, RemoteAnimationAdapter adapter, WindowContainerTransaction wct)1062     private void launchAsFullscreenWithRemoteAnimation(@Nullable PendingIntent pendingIntent,
1063             @Nullable Intent fillInIntent, @Nullable ShortcutInfo shortcutInfo,
1064             @Nullable Bundle options, RemoteAnimationAdapter adapter,
1065             WindowContainerTransaction wct) {
1066         LegacyTransitions.ILegacyTransition transition =
1067                 (transit, apps, wallpapers, nonApps, finishedCallback, t) -> {
1068                     if (apps == null || apps.length == 0) {
1069                         onRemoteAnimationFinished(apps);
1070                         t.apply();
1071                         try {
1072                             adapter.getRunner().onAnimationCancelled();
1073                         } catch (RemoteException e) {
1074                             Slog.e(TAG, "Error starting remote animation", e);
1075                         }
1076                         return;
1077                     }
1078 
1079                     for (int i = 0; i < apps.length; ++i) {
1080                         if (apps[i].mode == MODE_OPENING) {
1081                             t.show(apps[i].leash);
1082                         }
1083                     }
1084                     t.apply();
1085 
1086                     try {
1087                         adapter.getRunner().onAnimationStart(
1088                                 transit, apps, wallpapers, nonApps, finishedCallback);
1089                     } catch (RemoteException e) {
1090                         Slog.e(TAG, "Error starting remote animation", e);
1091                     }
1092                 };
1093 
1094         addActivityOptions(options, null /* launchTarget */);
1095         if (shortcutInfo != null) {
1096             wct.startShortcut(mContext.getPackageName(), shortcutInfo, options);
1097         } else if (pendingIntent != null) {
1098             wct.sendPendingIntent(pendingIntent, fillInIntent, options);
1099         } else {
1100             Slog.e(TAG, "Pending intent and shortcut are null is invalid case.");
1101         }
1102         mSyncQueue.queue(transition, WindowManager.TRANSIT_OPEN, wct);
1103     }
1104 
startWithLegacyTransition(WindowContainerTransaction wct, @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent, @Nullable ShortcutInfo mainShortcutInfo, @Nullable Bundle mainOptions, @SplitPosition int sidePosition, @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter, InstanceId instanceId)1105     private void startWithLegacyTransition(WindowContainerTransaction wct,
1106             @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent,
1107             @Nullable ShortcutInfo mainShortcutInfo, @Nullable Bundle mainOptions,
1108             @SplitPosition int sidePosition, @PersistentSnapPosition int snapPosition,
1109             RemoteAnimationAdapter adapter, InstanceId instanceId) {
1110         startWithLegacyTransition(wct, INVALID_TASK_ID, mainPendingIntent, mainFillInIntent,
1111                 mainShortcutInfo, mainOptions, sidePosition, snapPosition, adapter, instanceId);
1112     }
1113 
startWithLegacyTransition(WindowContainerTransaction wct, int mainTaskId, @Nullable Bundle mainOptions, @SplitPosition int sidePosition, @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter, InstanceId instanceId)1114     private void startWithLegacyTransition(WindowContainerTransaction wct, int mainTaskId,
1115             @Nullable Bundle mainOptions, @SplitPosition int sidePosition,
1116             @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter,
1117             InstanceId instanceId) {
1118         startWithLegacyTransition(wct, mainTaskId, null /* mainPendingIntent */,
1119                 null /* mainFillInIntent */, null /* mainShortcutInfo */, mainOptions, sidePosition,
1120                 snapPosition, adapter, instanceId);
1121     }
1122 
1123     /**
1124      * @param wct        transaction to start the first task
1125      * @param instanceId if {@code null}, will not log. Otherwise it will be used in
1126      *                   {@link SplitscreenEventLogger#logEnter(float, int, int, int, int, boolean)}
1127      */
startWithLegacyTransition(WindowContainerTransaction wct, int mainTaskId, @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent, @Nullable ShortcutInfo mainShortcutInfo, @Nullable Bundle options, @SplitPosition int sidePosition, @PersistentSnapPosition int snapPosition, RemoteAnimationAdapter adapter, InstanceId instanceId)1128     private void startWithLegacyTransition(WindowContainerTransaction wct, int mainTaskId,
1129             @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent,
1130             @Nullable ShortcutInfo mainShortcutInfo, @Nullable Bundle options,
1131             @SplitPosition int sidePosition, @PersistentSnapPosition int snapPosition,
1132             RemoteAnimationAdapter adapter, InstanceId instanceId) {
1133         if (!isSplitScreenVisible()) {
1134             exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT);
1135         }
1136 
1137         // Init divider first to make divider leash for remote animation target.
1138         mSplitLayout.init();
1139         mSplitLayout.setDivideRatio(snapPosition);
1140 
1141         // Apply surface bounds before animation start.
1142         SurfaceControl.Transaction startT = mTransactionPool.acquire();
1143         updateSurfaceBounds(mSplitLayout, startT, false /* applyResizingOffset */);
1144         startT.apply();
1145         mTransactionPool.release(startT);
1146 
1147         // Set false to avoid record new bounds with old task still on top;
1148         mShouldUpdateRecents = false;
1149         mIsDividerRemoteAnimating = true;
1150         if (mSplitRequest == null) {
1151             mSplitRequest = new SplitRequest(mainTaskId,
1152                     mainPendingIntent != null ? mainPendingIntent.getIntent() : null,
1153                     sidePosition);
1154         }
1155         setSideStagePosition(sidePosition, wct);
1156         if (!mMainStage.isActive()) {
1157             mMainStage.activate(wct, false /* reparent */);
1158         }
1159 
1160         if (options == null) options = new Bundle();
1161         addActivityOptions(options, mMainStage);
1162 
1163         updateWindowBounds(mSplitLayout, wct);
1164         wct.reorder(mRootTaskInfo.token, true);
1165         setRootForceTranslucent(false, wct);
1166 
1167         // TODO(b/268008375): Merge APIs to start a split pair into one.
1168         if (mainTaskId != INVALID_TASK_ID) {
1169             options = wrapAsSplitRemoteAnimation(adapter, options);
1170             wct.startTask(mainTaskId, options);
1171             mSyncQueue.queue(wct);
1172         } else {
1173             if (mainShortcutInfo != null) {
1174                 wct.startShortcut(mContext.getPackageName(), mainShortcutInfo, options);
1175             } else {
1176                 wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, options);
1177             }
1178             mSyncQueue.queue(wrapAsSplitRemoteAnimation(adapter), WindowManager.TRANSIT_OPEN, wct);
1179         }
1180 
1181         setEnterInstanceId(instanceId);
1182     }
1183 
wrapAsSplitRemoteAnimation(RemoteAnimationAdapter adapter, Bundle options)1184     private Bundle wrapAsSplitRemoteAnimation(RemoteAnimationAdapter adapter, Bundle options) {
1185         final WindowContainerTransaction evictWct = new WindowContainerTransaction();
1186         if (isSplitScreenVisible()) {
1187             mMainStage.evictAllChildren(evictWct);
1188             mSideStage.evictAllChildren(evictWct);
1189         }
1190 
1191         IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
1192             @Override
1193             public void onAnimationStart(@WindowManager.TransitionOldType int transit,
1194                     RemoteAnimationTarget[] apps,
1195                     RemoteAnimationTarget[] wallpapers,
1196                     RemoteAnimationTarget[] nonApps,
1197                     final IRemoteAnimationFinishedCallback finishedCallback) {
1198                 IRemoteAnimationFinishedCallback wrapCallback =
1199                         new IRemoteAnimationFinishedCallback.Stub() {
1200                             @Override
1201                             public void onAnimationFinished() throws RemoteException {
1202                                 onRemoteAnimationFinishedOrCancelled(evictWct);
1203                                 finishedCallback.onAnimationFinished();
1204                             }
1205                         };
1206                 Transitions.setRunningRemoteTransitionDelegate(adapter.getCallingApplication());
1207                 try {
1208                     adapter.getRunner().onAnimationStart(transit, apps, wallpapers,
1209                             ArrayUtils.appendElement(RemoteAnimationTarget.class, nonApps,
1210                                     getDividerBarLegacyTarget()), wrapCallback);
1211                 } catch (RemoteException e) {
1212                     Slog.e(TAG, "Error starting remote animation", e);
1213                 }
1214             }
1215 
1216             @Override
1217             public void onAnimationCancelled() {
1218                 onRemoteAnimationFinishedOrCancelled(evictWct);
1219                 setDividerVisibility(true, null);
1220                 try {
1221                     adapter.getRunner().onAnimationCancelled();
1222                 } catch (RemoteException e) {
1223                     Slog.e(TAG, "Error starting remote animation", e);
1224                 }
1225             }
1226         };
1227         RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(
1228                 wrapper, adapter.getDuration(), adapter.getStatusBarTransitionDelay());
1229         ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
1230         activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
1231         return activityOptions.toBundle();
1232     }
1233 
wrapAsSplitRemoteAnimation( RemoteAnimationAdapter adapter)1234     private LegacyTransitions.ILegacyTransition wrapAsSplitRemoteAnimation(
1235             RemoteAnimationAdapter adapter) {
1236         LegacyTransitions.ILegacyTransition transition =
1237                 (transit, apps, wallpapers, nonApps, finishedCallback, t) -> {
1238                     if (apps == null || apps.length == 0) {
1239                         onRemoteAnimationFinished(apps);
1240                         t.apply();
1241                         try {
1242                             adapter.getRunner().onAnimationCancelled();
1243                         } catch (RemoteException e) {
1244                             Slog.e(TAG, "Error starting remote animation", e);
1245                         }
1246                         return;
1247                     }
1248 
1249                     // Wrap the divider bar into non-apps target to animate together.
1250                     nonApps = ArrayUtils.appendElement(RemoteAnimationTarget.class, nonApps,
1251                             getDividerBarLegacyTarget());
1252 
1253                     for (int i = 0; i < apps.length; ++i) {
1254                         if (apps[i].mode == MODE_OPENING) {
1255                             t.show(apps[i].leash);
1256                             // Reset the surface position of the opening app to prevent offset.
1257                             t.setPosition(apps[i].leash, 0, 0);
1258                         }
1259                     }
1260                     setDividerVisibility(true, t);
1261                     t.apply();
1262 
1263                     IRemoteAnimationFinishedCallback wrapCallback =
1264                             new IRemoteAnimationFinishedCallback.Stub() {
1265                                 @Override
1266                                 public void onAnimationFinished() throws RemoteException {
1267                                     onRemoteAnimationFinished(apps);
1268                                     finishedCallback.onAnimationFinished();
1269                                 }
1270                             };
1271                     Transitions.setRunningRemoteTransitionDelegate(adapter.getCallingApplication());
1272                     try {
1273                         adapter.getRunner().onAnimationStart(
1274                                 transit, apps, wallpapers, nonApps, wrapCallback);
1275                     } catch (RemoteException e) {
1276                         Slog.e(TAG, "Error starting remote animation", e);
1277                     }
1278                 };
1279 
1280         return transition;
1281     }
1282 
setEnterInstanceId(InstanceId instanceId)1283     private void setEnterInstanceId(InstanceId instanceId) {
1284         if (instanceId != null) {
1285             mLogger.enterRequested(instanceId, ENTER_REASON_LAUNCHER);
1286         }
1287     }
1288 
onRemoteAnimationFinishedOrCancelled(WindowContainerTransaction evictWct)1289     private void onRemoteAnimationFinishedOrCancelled(WindowContainerTransaction evictWct) {
1290         mIsDividerRemoteAnimating = false;
1291         mShouldUpdateRecents = true;
1292         clearRequestIfPresented();
1293         // If any stage has no child after animation finished, it means that split will display
1294         // nothing, such status will happen if task and intent is same app but not support
1295         // multi-instance, we should exit split and expand that app as full screen.
1296         if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) {
1297             mMainExecutor.execute(() ->
1298                     exitSplitScreen(mMainStage.getChildCount() == 0
1299                             ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
1300             Log.w(TAG, splitFailureMessage("onRemoteAnimationFinishedOrCancelled",
1301                     "main or side stage was not populated."));
1302             handleUnsupportedSplitStart();
1303         } else {
1304             mSyncQueue.queue(evictWct);
1305             mSyncQueue.runInSync(t -> {
1306                 updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
1307             });
1308         }
1309     }
1310 
onRemoteAnimationFinished(RemoteAnimationTarget[] apps)1311     private void onRemoteAnimationFinished(RemoteAnimationTarget[] apps) {
1312         mIsDividerRemoteAnimating = false;
1313         mShouldUpdateRecents = true;
1314         clearRequestIfPresented();
1315         // If any stage has no child after finished animation, that side of the split will display
1316         // nothing. This might happen if starting the same app on the both sides while not
1317         // supporting multi-instance. Exit the split screen and expand that app to full screen.
1318         if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) {
1319             mMainExecutor.execute(() -> exitSplitScreen(mMainStage.getChildCount() == 0
1320                     ? mSideStage : mMainStage, EXIT_REASON_UNKNOWN));
1321             Log.w(TAG, splitFailureMessage("onRemoteAnimationFinished",
1322                     "main or side stage was not populated"));
1323             handleUnsupportedSplitStart();
1324             return;
1325         }
1326 
1327         final WindowContainerTransaction evictWct = new WindowContainerTransaction();
1328         mMainStage.evictNonOpeningChildren(apps, evictWct);
1329         mSideStage.evictNonOpeningChildren(apps, evictWct);
1330         mSyncQueue.queue(evictWct);
1331     }
1332 
prepareEvictNonOpeningChildTasks(@plitPosition int position, RemoteAnimationTarget[] apps, WindowContainerTransaction wct)1333     void prepareEvictNonOpeningChildTasks(@SplitPosition int position, RemoteAnimationTarget[] apps,
1334             WindowContainerTransaction wct) {
1335         if (position == mSideStagePosition) {
1336             mSideStage.evictNonOpeningChildren(apps, wct);
1337         } else {
1338             mMainStage.evictNonOpeningChildren(apps, wct);
1339         }
1340     }
1341 
prepareEvictInvisibleChildTasks(WindowContainerTransaction wct)1342     void prepareEvictInvisibleChildTasks(WindowContainerTransaction wct) {
1343         mMainStage.evictInvisibleChildren(wct);
1344         mSideStage.evictInvisibleChildren(wct);
1345     }
1346 
resolveStartStage(@tageType int stage, @SplitPosition int position, @Nullable Bundle options, @Nullable WindowContainerTransaction wct)1347     Bundle resolveStartStage(@StageType int stage, @SplitPosition int position,
1348             @Nullable Bundle options, @Nullable WindowContainerTransaction wct) {
1349         switch (stage) {
1350             case STAGE_TYPE_UNDEFINED: {
1351                 if (position != SPLIT_POSITION_UNDEFINED) {
1352                     if (isSplitScreenVisible()) {
1353                         // Use the stage of the specified position
1354                         options = resolveStartStage(
1355                                 position == mSideStagePosition ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN,
1356                                 position, options, wct);
1357                     } else {
1358                         // Use the side stage as default to active split screen
1359                         options = resolveStartStage(STAGE_TYPE_SIDE, position, options, wct);
1360                     }
1361                 } else {
1362                     Slog.w(TAG,
1363                             "No stage type nor split position specified to resolve start stage");
1364                 }
1365                 break;
1366             }
1367             case STAGE_TYPE_SIDE: {
1368                 if (position != SPLIT_POSITION_UNDEFINED) {
1369                     setSideStagePosition(position, wct);
1370                 } else {
1371                     position = getSideStagePosition();
1372                 }
1373                 if (options == null) {
1374                     options = new Bundle();
1375                 }
1376                 updateActivityOptions(options, position);
1377                 break;
1378             }
1379             case STAGE_TYPE_MAIN: {
1380                 if (position != SPLIT_POSITION_UNDEFINED) {
1381                     // Set the side stage opposite of what we want to the main stage.
1382                     setSideStagePosition(reverseSplitPosition(position), wct);
1383                 } else {
1384                     position = getMainStagePosition();
1385                 }
1386                 if (options == null) {
1387                     options = new Bundle();
1388                 }
1389                 updateActivityOptions(options, position);
1390                 break;
1391             }
1392             default:
1393                 throw new IllegalArgumentException("Unknown stage=" + stage);
1394         }
1395 
1396         return options;
1397     }
1398 
1399     @SplitPosition
getSideStagePosition()1400     int getSideStagePosition() {
1401         return mSideStagePosition;
1402     }
1403 
1404     @SplitPosition
getMainStagePosition()1405     int getMainStagePosition() {
1406         return reverseSplitPosition(mSideStagePosition);
1407     }
1408 
getTaskId(@plitPosition int splitPosition)1409     int getTaskId(@SplitPosition int splitPosition) {
1410         if (splitPosition == SPLIT_POSITION_UNDEFINED) {
1411             return INVALID_TASK_ID;
1412         }
1413 
1414         return mSideStagePosition == splitPosition
1415                 ? mSideStage.getTopVisibleChildTaskId()
1416                 : mMainStage.getTopVisibleChildTaskId();
1417     }
1418 
switchSplitPosition(String reason)1419     void switchSplitPosition(String reason) {
1420         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "switchSplitPosition");
1421         final SurfaceControl.Transaction t = mTransactionPool.acquire();
1422         mTempRect1.setEmpty();
1423         final StageTaskListener topLeftStage =
1424                 mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
1425         final SurfaceControl topLeftScreenshot = ScreenshotUtils.takeScreenshot(t,
1426                 topLeftStage.mRootLeash, mTempRect1, Integer.MAX_VALUE - 1);
1427         final StageTaskListener bottomRightStage =
1428                 mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
1429         final SurfaceControl bottomRightScreenshot = ScreenshotUtils.takeScreenshot(t,
1430                 bottomRightStage.mRootLeash, mTempRect1, Integer.MAX_VALUE - 1);
1431         mSplitLayout.splitSwitching(t, topLeftStage.mRootLeash, bottomRightStage.mRootLeash,
1432                 insets -> {
1433                     WindowContainerTransaction wct = new WindowContainerTransaction();
1434                     setSideStagePosition(reverseSplitPosition(mSideStagePosition), wct);
1435                     mSyncQueue.queue(wct);
1436                     mSyncQueue.runInSync(st -> {
1437                         updateSurfaceBounds(mSplitLayout, st, false /* applyResizingOffset */);
1438                         st.setPosition(topLeftScreenshot, -insets.left, -insets.top);
1439                         st.setPosition(bottomRightScreenshot, insets.left, insets.top);
1440 
1441                         final ValueAnimator va = ValueAnimator.ofFloat(1, 0);
1442                         va.addUpdateListener(valueAnimator-> {
1443                             final float progress = (float) valueAnimator.getAnimatedValue();
1444                             t.setAlpha(topLeftScreenshot, progress);
1445                             t.setAlpha(bottomRightScreenshot, progress);
1446                             t.apply();
1447                         });
1448                         va.addListener(new AnimatorListenerAdapter() {
1449                             @Override
1450                             public void onAnimationEnd(
1451                                     @androidx.annotation.NonNull Animator animation) {
1452                                 t.remove(topLeftScreenshot);
1453                                 t.remove(bottomRightScreenshot);
1454                                 t.apply();
1455                                 mTransactionPool.release(t);
1456                             }
1457                         });
1458                         va.start();
1459                     });
1460                 });
1461 
1462         ProtoLog.v(WM_SHELL_SPLIT_SCREEN, "Switch split position: %s", reason);
1463         mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
1464                 getSideStagePosition(), mSideStage.getTopChildTaskUid(),
1465                 mSplitLayout.isLeftRightSplit());
1466     }
1467 
setSideStagePosition(@plitPosition int sideStagePosition, @Nullable WindowContainerTransaction wct)1468     void setSideStagePosition(@SplitPosition int sideStagePosition,
1469             @Nullable WindowContainerTransaction wct) {
1470         setSideStagePosition(sideStagePosition, true /* updateBounds */, wct);
1471     }
1472 
setSideStagePosition(@plitPosition int sideStagePosition, boolean updateBounds, @Nullable WindowContainerTransaction wct)1473     private void setSideStagePosition(@SplitPosition int sideStagePosition, boolean updateBounds,
1474             @Nullable WindowContainerTransaction wct) {
1475         if (mSideStagePosition == sideStagePosition) return;
1476         mSideStagePosition = sideStagePosition;
1477         sendOnStagePositionChanged();
1478 
1479         if (mSideStageListener.mVisible && updateBounds) {
1480             if (wct == null) {
1481                 // onLayoutChanged builds/applies a wct with the contents of updateWindowBounds.
1482                 onLayoutSizeChanged(mSplitLayout);
1483             } else {
1484                 updateWindowBounds(mSplitLayout, wct);
1485                 sendOnBoundsChanged();
1486             }
1487         }
1488     }
1489 
onKeyguardVisibilityChanged(boolean showing)1490     void onKeyguardVisibilityChanged(boolean showing) {
1491         mKeyguardShowing = showing;
1492         if (!mMainStage.isActive()) {
1493             return;
1494         }
1495         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onKeyguardVisibilityChanged: showing=%b", showing);
1496         setDividerVisibility(!mKeyguardShowing, null);
1497     }
1498 
onFinishedWakingUp()1499     void onFinishedWakingUp() {
1500         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onFinishedWakingUp");
1501         if (!mMainStage.isActive()) {
1502             return;
1503         }
1504 
1505         // Check if there's only one stage visible while keyguard occluded.
1506         final boolean mainStageVisible = mMainStage.mRootTaskInfo.isVisible;
1507         final boolean oneStageVisible =
1508                 mMainStage.mRootTaskInfo.isVisible != mSideStage.mRootTaskInfo.isVisible;
1509         if (oneStageVisible && !ENABLE_SHELL_TRANSITIONS) {
1510             // Dismiss split because there's show-when-locked activity showing on top of keyguard.
1511             // Also make sure the task contains show-when-locked activity remains on top after split
1512             // dismissed.
1513             final StageTaskListener toTop = mainStageVisible ? mMainStage : mSideStage;
1514             exitSplitScreen(toTop, EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
1515         }
1516 
1517         // Dismiss split if the flag record any side of stages.
1518         if (mTopStageAfterFoldDismiss != STAGE_TYPE_UNDEFINED) {
1519             if (ENABLE_SHELL_TRANSITIONS) {
1520                 // Need manually clear here due to this transition might be aborted due to keyguard
1521                 // on top and lead to no visible change.
1522                 clearSplitPairedInRecents(EXIT_REASON_DEVICE_FOLDED);
1523                 final WindowContainerTransaction wct = new WindowContainerTransaction();
1524                 prepareExitSplitScreen(mTopStageAfterFoldDismiss, wct);
1525                 mSplitTransitions.startDismissTransition(wct, this,
1526                         mTopStageAfterFoldDismiss, EXIT_REASON_DEVICE_FOLDED);
1527                 setSplitsVisible(false);
1528             } else {
1529                 exitSplitScreen(
1530                         mTopStageAfterFoldDismiss == STAGE_TYPE_MAIN ? mMainStage : mSideStage,
1531                         EXIT_REASON_DEVICE_FOLDED);
1532             }
1533             mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
1534         }
1535     }
1536 
exitSplitScreenOnHide(boolean exitSplitScreenOnHide)1537     void exitSplitScreenOnHide(boolean exitSplitScreenOnHide) {
1538         mExitSplitScreenOnHide = exitSplitScreenOnHide;
1539     }
1540 
1541     /** Exits split screen with legacy transition */
exitSplitScreen(int toTopTaskId, @ExitReason int exitReason)1542     void exitSplitScreen(int toTopTaskId, @ExitReason int exitReason) {
1543         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "exitSplitScreen: topTaskId=%d reason=%s active=%b",
1544                 toTopTaskId, exitReasonToString(exitReason), mMainStage.isActive());
1545         if (!mMainStage.isActive()) return;
1546 
1547         StageTaskListener childrenToTop = null;
1548         if (mMainStage.containsTask(toTopTaskId)) {
1549             childrenToTop = mMainStage;
1550         } else if (mSideStage.containsTask(toTopTaskId)) {
1551             childrenToTop = mSideStage;
1552         }
1553 
1554         final WindowContainerTransaction wct = new WindowContainerTransaction();
1555         if (childrenToTop != null) {
1556             childrenToTop.reorderChild(toTopTaskId, true /* onTop */, wct);
1557         }
1558         applyExitSplitScreen(childrenToTop, wct, exitReason);
1559     }
1560 
1561     /** Exits split screen with legacy transition */
exitSplitScreen(@ullable StageTaskListener childrenToTop, @ExitReason int exitReason)1562     private void exitSplitScreen(@Nullable StageTaskListener childrenToTop,
1563             @ExitReason int exitReason) {
1564         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "exitSplitScreen: mainStageToTop=%b reason=%s active=%b",
1565                 childrenToTop == mMainStage, exitReasonToString(exitReason), mMainStage.isActive());
1566         if (!mMainStage.isActive()) return;
1567 
1568         final WindowContainerTransaction wct = new WindowContainerTransaction();
1569         applyExitSplitScreen(childrenToTop, wct, exitReason);
1570     }
1571 
applyExitSplitScreen(@ullable StageTaskListener childrenToTop, WindowContainerTransaction wct, @ExitReason int exitReason)1572     private void applyExitSplitScreen(@Nullable StageTaskListener childrenToTop,
1573             WindowContainerTransaction wct, @ExitReason int exitReason) {
1574         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "applyExitSplitScreen: reason=%s",
1575                 exitReasonToString(exitReason));
1576         if (!mMainStage.isActive() || mIsExiting) return;
1577 
1578         onSplitScreenExit();
1579         clearSplitPairedInRecents(exitReason);
1580 
1581         mShouldUpdateRecents = false;
1582         mIsDividerRemoteAnimating = false;
1583         mSplitRequest = null;
1584 
1585         mSplitLayout.getInvisibleBounds(mTempRect1);
1586         if (childrenToTop == null || childrenToTop.getTopVisibleChildTaskId() == INVALID_TASK_ID) {
1587             mSideStage.removeAllTasks(wct, false /* toTop */);
1588             mMainStage.deactivate(wct, false /* toTop */);
1589             wct.reorder(mRootTaskInfo.token, false /* onTop */);
1590             setRootForceTranslucent(true, wct);
1591             wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
1592             onTransitionAnimationComplete();
1593         } else {
1594             // Expand to top side split as full screen for fading out decor animation and dismiss
1595             // another side split(Moving its children to bottom).
1596             mIsExiting = true;
1597             childrenToTop.resetBounds(wct);
1598             wct.reorder(childrenToTop.mRootTaskInfo.token, true);
1599         }
1600         wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
1601                 false /* reparentLeafTaskIfRelaunch */);
1602         mSyncQueue.queue(wct);
1603         mSyncQueue.runInSync(t -> {
1604             t.setWindowCrop(mMainStage.mRootLeash, null)
1605                     .setWindowCrop(mSideStage.mRootLeash, null);
1606             t.hide(mMainStage.mDimLayer).hide(mSideStage.mDimLayer);
1607             setDividerVisibility(false, t);
1608 
1609             if (childrenToTop == null) {
1610                 t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.right);
1611             } else {
1612                 // In this case, exit still under progress, fade out the split decor after first WCT
1613                 // done and do remaining WCT after animation finished.
1614                 childrenToTop.fadeOutDecor(() -> {
1615                     WindowContainerTransaction finishedWCT = new WindowContainerTransaction();
1616                     mIsExiting = false;
1617                     mMainStage.deactivate(finishedWCT, childrenToTop == mMainStage /* toTop */);
1618                     mSideStage.removeAllTasks(finishedWCT, childrenToTop == mSideStage /* toTop */);
1619                     finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */);
1620                     setRootForceTranslucent(true, finishedWCT);
1621                     finishedWCT.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
1622                     mSyncQueue.queue(finishedWCT);
1623                     mSyncQueue.runInSync(at -> {
1624                         at.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.right);
1625                     });
1626                     onTransitionAnimationComplete();
1627                 });
1628             }
1629         });
1630 
1631         // Log the exit
1632         if (childrenToTop != null) {
1633             logExitToStage(exitReason, childrenToTop == mMainStage);
1634         } else {
1635             logExit(exitReason);
1636         }
1637     }
1638 
dismissSplitScreen(int toTopTaskId, @ExitReason int exitReason)1639     void dismissSplitScreen(int toTopTaskId, @ExitReason int exitReason) {
1640         if (!mMainStage.isActive()) return;
1641         final int stage = getStageOfTask(toTopTaskId);
1642         final WindowContainerTransaction wct = new WindowContainerTransaction();
1643         prepareExitSplitScreen(stage, wct);
1644         mSplitTransitions.startDismissTransition(wct, this, stage, exitReason);
1645     }
1646 
1647     /**
1648      * Overridden by child classes.
1649      */
onSplitScreenEnter()1650     protected void onSplitScreenEnter() {
1651     }
1652 
1653     /**
1654      * Overridden by child classes.
1655      */
onSplitScreenExit()1656     protected void onSplitScreenExit() {
1657     }
1658 
1659     /**
1660      * Exits the split screen by finishing one of the tasks.
1661      */
exitStage(@plitPosition int stageToClose)1662     protected void exitStage(@SplitPosition int stageToClose) {
1663         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "exitStage: stageToClose=%d", stageToClose);
1664         mSplitLayout.flingDividerToDismiss(stageToClose == SPLIT_POSITION_BOTTOM_OR_RIGHT,
1665                 EXIT_REASON_APP_FINISHED);
1666     }
1667 
1668     /**
1669      * Grants focus to the main or the side stages.
1670      */
grantFocusToStage(@plitPosition int stageToFocus)1671     protected void grantFocusToStage(@SplitPosition int stageToFocus) {
1672         IActivityTaskManager activityTaskManagerService = IActivityTaskManager.Stub.asInterface(
1673                 ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE));
1674         try {
1675             activityTaskManagerService.setFocusedTask(getTaskId(stageToFocus));
1676         } catch (RemoteException | NullPointerException e) {
1677             ProtoLog.e(WM_SHELL_SPLIT_SCREEN,
1678                     "Unable to update focus on the chosen stage: %s", e.getMessage());
1679         }
1680     }
1681 
grantFocusToPosition(boolean leftOrTop)1682     protected void grantFocusToPosition(boolean leftOrTop) {
1683         int stageToFocus;
1684         if (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT) {
1685             stageToFocus = leftOrTop ? getMainStagePosition() : getSideStagePosition();
1686         } else {
1687             stageToFocus = leftOrTop ? getSideStagePosition() : getMainStagePosition();
1688         }
1689         grantFocusToStage(stageToFocus);
1690     }
1691 
clearRequestIfPresented()1692     private void clearRequestIfPresented() {
1693         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "clearRequestIfPresented");
1694         if (mSideStageListener.mVisible && mSideStageListener.mHasChildren
1695                 && mMainStageListener.mVisible && mSideStageListener.mHasChildren) {
1696             mSplitRequest = null;
1697         }
1698     }
1699 
1700     /**
1701      * Returns whether the split pair in the recent tasks list should be broken.
1702      */
shouldBreakPairedTaskInRecents(@xitReason int exitReason)1703     private boolean shouldBreakPairedTaskInRecents(@ExitReason int exitReason) {
1704         switch (exitReason) {
1705             // One of the apps doesn't support MW
1706             case EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW:
1707                 // User has explicitly dragged the divider to dismiss split
1708             case EXIT_REASON_DRAG_DIVIDER:
1709                 // Either of the split apps have finished
1710             case EXIT_REASON_APP_FINISHED:
1711                 // One of the children enters PiP
1712             case EXIT_REASON_CHILD_TASK_ENTER_PIP:
1713                 // One of the apps occludes lock screen.
1714             case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP:
1715                 // User has unlocked the device after folded
1716             case EXIT_REASON_DEVICE_FOLDED:
1717                 // The device is folded
1718             case EXIT_REASON_FULLSCREEN_SHORTCUT:
1719                 // User has used a keyboard shortcut to go back to fullscreen from split
1720             case EXIT_REASON_DESKTOP_MODE:
1721                 // One of the children enters desktop mode
1722             case EXIT_REASON_UNKNOWN:
1723                 // Unknown reason
1724                 return true;
1725             default:
1726                 return false;
1727         }
1728     }
1729 
clearSplitPairedInRecents(@xitReason int exitReason)1730     void clearSplitPairedInRecents(@ExitReason int exitReason) {
1731         if (!shouldBreakPairedTaskInRecents(exitReason) || !mShouldUpdateRecents) return;
1732 
1733         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "clearSplitPairedInRecents: reason=%s",
1734                 exitReasonToString(exitReason));
1735         mRecentTasks.ifPresent(recentTasks -> {
1736             // Notify recents if we are exiting in a way that breaks the pair, and disable further
1737             // updates to splits in the recents until we enter split again
1738             mMainStage.doForAllChildTasks(taskId -> recentTasks.removeSplitPair(taskId));
1739             mSideStage.doForAllChildTasks(taskId -> recentTasks.removeSplitPair(taskId));
1740         });
1741     }
1742 
1743     /**
1744      * Unlike exitSplitScreen, this takes a stagetype vs an actual stage-reference and populates
1745      * an existing WindowContainerTransaction (rather than applying immediately). This is intended
1746      * to be used when exiting split might be bundled with other window operations.
1747      */
prepareExitSplitScreen(@tageType int stageToTop, @NonNull WindowContainerTransaction wct)1748     void prepareExitSplitScreen(@StageType int stageToTop,
1749             @NonNull WindowContainerTransaction wct) {
1750         if (!mMainStage.isActive()) return;
1751         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%d", stageToTop);
1752         mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE);
1753         mMainStage.deactivate(wct, stageToTop == STAGE_TYPE_MAIN);
1754     }
1755 
prepareEnterSplitScreen(WindowContainerTransaction wct)1756     private void prepareEnterSplitScreen(WindowContainerTransaction wct) {
1757         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareEnterSplitScreen");
1758         prepareEnterSplitScreen(wct, null /* taskInfo */, SPLIT_POSITION_UNDEFINED,
1759                 !mIsDropEntering);
1760     }
1761 
1762     /**
1763      * Prepare transaction to active split screen. If there's a task indicated, the task will be put
1764      * into side stage.
1765      */
prepareEnterSplitScreen(WindowContainerTransaction wct, @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition, boolean resizeAnim)1766     void prepareEnterSplitScreen(WindowContainerTransaction wct,
1767             @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition,
1768             boolean resizeAnim) {
1769         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareEnterSplitScreen: position=%d resize=%b",
1770                 startPosition, resizeAnim);
1771         onSplitScreenEnter();
1772         // Preemptively reset the reparenting behavior if we know that we are entering, as starting
1773         // split tasks with activity trampolines can inadvertently trigger the task to be
1774         // reparented out of the split root mid-launch
1775         wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
1776                 false /* setReparentLeafTaskIfRelaunch */);
1777         if (isSplitActive()) {
1778             prepareBringSplit(wct, taskInfo, startPosition, resizeAnim);
1779         } else {
1780             prepareActiveSplit(wct, taskInfo, startPosition, resizeAnim);
1781         }
1782     }
1783 
prepareBringSplit(WindowContainerTransaction wct, @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition, boolean resizeAnim)1784     private void prepareBringSplit(WindowContainerTransaction wct,
1785             @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition,
1786             boolean resizeAnim) {
1787         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareBringSplit: task=%d isSplitVisible=%b",
1788                 taskInfo != null ? taskInfo.taskId : -1, isSplitScreenVisible());
1789         if (taskInfo != null) {
1790             wct.startTask(taskInfo.taskId,
1791                     resolveStartStage(STAGE_TYPE_UNDEFINED, startPosition, null, wct));
1792         }
1793         // If running background, we need to reparent current top visible task to main stage.
1794         if (!isSplitScreenVisible()) {
1795             // Ensure to evict old splitting tasks because the new split pair might be composed by
1796             // one of the splitting tasks, evicting the task when finishing entering transition
1797             // won't guarantee to put the task to the indicated new position.
1798             if (!mSkipEvictingMainStageChildren) {
1799                 mMainStage.evictAllChildren(wct);
1800             }
1801             mMainStage.reparentTopTask(wct);
1802             prepareSplitLayout(wct, resizeAnim);
1803         }
1804     }
1805 
prepareActiveSplit(WindowContainerTransaction wct, @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition, boolean resizeAnim)1806     private void prepareActiveSplit(WindowContainerTransaction wct,
1807             @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition,
1808             boolean resizeAnim) {
1809         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareActiveSplit: task=%d isSplitVisible=%b",
1810                 taskInfo != null ? taskInfo.taskId : -1, isSplitScreenVisible());
1811         if (!ENABLE_SHELL_TRANSITIONS) {
1812             // Legacy transition we need to create divider here, shell transition case we will
1813             // create it on #finishEnterSplitScreen
1814             mSplitLayout.init();
1815         } else {
1816             // We handle split visibility itself on shell transition, but sometimes we didn't
1817             // reset it correctly after dismiss by some reason, so just set invisible before active.
1818             setSplitsVisible(false);
1819         }
1820         if (taskInfo != null) {
1821             setSideStagePosition(startPosition, wct);
1822             mSideStage.addTask(taskInfo, wct);
1823         }
1824         mMainStage.activate(wct, true /* includingTopTask */);
1825         prepareSplitLayout(wct, resizeAnim);
1826     }
1827 
prepareSplitLayout(WindowContainerTransaction wct, boolean resizeAnim)1828     private void prepareSplitLayout(WindowContainerTransaction wct, boolean resizeAnim) {
1829         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareSplitLayout: resize=%b", resizeAnim);
1830         if (resizeAnim) {
1831             mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
1832         } else {
1833             mSplitLayout.resetDividerPosition();
1834         }
1835         updateWindowBounds(mSplitLayout, wct);
1836         if (resizeAnim) {
1837             // Reset its smallest width dp to avoid is change layout before it actually resized to
1838             // split bounds.
1839             wct.setSmallestScreenWidthDp(mMainStage.mRootTaskInfo.token,
1840                     SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);
1841             mSplitLayout.getInvisibleBounds(mTempRect1);
1842             mSplitLayout.setTaskBounds(wct, mSideStage.mRootTaskInfo, mTempRect1);
1843         }
1844         wct.reorder(mRootTaskInfo.token, true);
1845         setRootForceTranslucent(false, wct);
1846     }
1847 
finishEnterSplitScreen(SurfaceControl.Transaction finishT)1848     void finishEnterSplitScreen(SurfaceControl.Transaction finishT) {
1849         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "finishEnterSplitScreen");
1850         mSplitLayout.update(null, true /* resetImePosition */);
1851         mMainStage.getSplitDecorManager().inflate(mContext, mMainStage.mRootLeash);
1852         mSideStage.getSplitDecorManager().inflate(mContext, mSideStage.mRootLeash);
1853         setDividerVisibility(true, finishT);
1854         // Ensure divider surface are re-parented back into the hierarchy at the end of the
1855         // transition. See Transition#buildFinishTransaction for more detail.
1856         finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash);
1857 
1858         updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */);
1859         finishT.show(mRootTaskLeash);
1860         setSplitsVisible(true);
1861         mIsDropEntering = false;
1862         mSkipEvictingMainStageChildren = false;
1863         mSplitRequest = null;
1864         updateRecentTasksSplitPair();
1865         if (!mLogger.hasStartedSession()) {
1866             mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(),
1867                     getMainStagePosition(), mMainStage.getTopChildTaskUid(),
1868                     getSideStagePosition(), mSideStage.getTopChildTaskUid(),
1869                     mSplitLayout.isLeftRightSplit());
1870         }
1871     }
1872 
getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds)1873     void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) {
1874         outTopOrLeftBounds.set(mSplitLayout.getBounds1());
1875         outBottomOrRightBounds.set(mSplitLayout.getBounds2());
1876     }
1877 
1878     @SplitPosition
getSplitPosition(int taskId)1879     int getSplitPosition(int taskId) {
1880         if (mSideStage.getTopVisibleChildTaskId() == taskId) {
1881             return getSideStagePosition();
1882         } else if (mMainStage.getTopVisibleChildTaskId() == taskId) {
1883             return getMainStagePosition();
1884         }
1885         return SPLIT_POSITION_UNDEFINED;
1886     }
1887 
addActivityOptions(Bundle opts, @Nullable StageTaskListener launchTarget)1888     private void addActivityOptions(Bundle opts, @Nullable StageTaskListener launchTarget) {
1889         ActivityOptions options = ActivityOptions.fromBundle(opts);
1890         if (launchTarget != null) {
1891             options.setLaunchRootTask(launchTarget.mRootTaskInfo.token);
1892         }
1893         // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split
1894         // will be canceled.
1895         options.setPendingIntentBackgroundActivityStartMode(MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
1896         options.setPendingIntentBackgroundActivityLaunchAllowedByPermission(true);
1897 
1898         // TODO (b/336477473): Disallow enter PiP when launching a task in split by default;
1899         //                     this might have to be changed as more split-to-pip cujs are defined.
1900         options.setDisallowEnterPictureInPictureWhileLaunching(true);
1901         opts.putAll(options.toBundle());
1902     }
1903 
updateActivityOptions(Bundle opts, @SplitPosition int position)1904     void updateActivityOptions(Bundle opts, @SplitPosition int position) {
1905         addActivityOptions(opts, position == mSideStagePosition ? mSideStage : mMainStage);
1906     }
1907 
registerSplitScreenListener(SplitScreen.SplitScreenListener listener)1908     void registerSplitScreenListener(SplitScreen.SplitScreenListener listener) {
1909         if (mListeners.contains(listener)) return;
1910         mListeners.add(listener);
1911         sendStatusToListener(listener);
1912     }
1913 
unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener)1914     void unregisterSplitScreenListener(SplitScreen.SplitScreenListener listener) {
1915         mListeners.remove(listener);
1916     }
1917 
registerSplitSelectListener(SplitScreen.SplitSelectListener listener)1918     void registerSplitSelectListener(SplitScreen.SplitSelectListener listener) {
1919         mSelectListeners.add(listener);
1920     }
1921 
unregisterSplitSelectListener(SplitScreen.SplitSelectListener listener)1922     void unregisterSplitSelectListener(SplitScreen.SplitSelectListener listener) {
1923         mSelectListeners.remove(listener);
1924     }
1925 
sendStatusToListener(SplitScreen.SplitScreenListener listener)1926     void sendStatusToListener(SplitScreen.SplitScreenListener listener) {
1927         listener.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition());
1928         listener.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition());
1929         listener.onSplitVisibilityChanged(isSplitScreenVisible());
1930         if (mSplitLayout != null) {
1931             listener.onSplitBoundsChanged(mSplitLayout.getRootBounds(), getMainStageBounds(),
1932                     getSideStageBounds());
1933         }
1934         mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE);
1935         mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN);
1936     }
1937 
sendOnStagePositionChanged()1938     private void sendOnStagePositionChanged() {
1939         for (int i = mListeners.size() - 1; i >= 0; --i) {
1940             final SplitScreen.SplitScreenListener l = mListeners.get(i);
1941             l.onStagePositionChanged(STAGE_TYPE_MAIN, getMainStagePosition());
1942             l.onStagePositionChanged(STAGE_TYPE_SIDE, getSideStagePosition());
1943         }
1944     }
1945 
sendOnBoundsChanged()1946     private void sendOnBoundsChanged() {
1947         if (mSplitLayout == null) return;
1948         for (int i = mListeners.size() - 1; i >= 0; --i) {
1949             mListeners.get(i).onSplitBoundsChanged(mSplitLayout.getRootBounds(),
1950                     getMainStageBounds(), getSideStageBounds());
1951         }
1952     }
1953 
onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId, boolean present, boolean visible)1954     private void onStageChildTaskStatusChanged(StageListenerImpl stageListener, int taskId,
1955             boolean present, boolean visible) {
1956         int stage;
1957         if (present) {
1958             stage = stageListener == mSideStageListener ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
1959         } else {
1960             // No longer on any stage
1961             stage = STAGE_TYPE_UNDEFINED;
1962         }
1963         if (stage == STAGE_TYPE_MAIN) {
1964             mLogger.logMainStageAppChange(getMainStagePosition(), mMainStage.getTopChildTaskUid(),
1965                     mSplitLayout.isLeftRightSplit());
1966         } else {
1967             mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(),
1968                     mSplitLayout.isLeftRightSplit());
1969         }
1970         if (present) {
1971             updateRecentTasksSplitPair();
1972         }
1973 
1974         for (int i = mListeners.size() - 1; i >= 0; --i) {
1975             mListeners.get(i).onTaskStageChanged(taskId, stage, visible);
1976         }
1977     }
1978 
updateRecentTasksSplitPair()1979     private void updateRecentTasksSplitPair() {
1980         // Preventing from single task update while processing recents.
1981         if (!mShouldUpdateRecents || !mPausingTasks.isEmpty()) {
1982             return;
1983         }
1984         mRecentTasks.ifPresent(recentTasks -> {
1985             Rect topLeftBounds = mSplitLayout.getBounds1();
1986             Rect bottomRightBounds = mSplitLayout.getBounds2();
1987             int mainStageTopTaskId = mMainStage.getTopVisibleChildTaskId();
1988             int sideStageTopTaskId = mSideStage.getTopVisibleChildTaskId();
1989             boolean sideStageTopLeft = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT;
1990             int leftTopTaskId;
1991             int rightBottomTaskId;
1992             if (sideStageTopLeft) {
1993                 leftTopTaskId = sideStageTopTaskId;
1994                 rightBottomTaskId = mainStageTopTaskId;
1995             } else {
1996                 leftTopTaskId = mainStageTopTaskId;
1997                 rightBottomTaskId = sideStageTopTaskId;
1998             }
1999             SplitBounds splitBounds = new SplitBounds(topLeftBounds, bottomRightBounds,
2000                     leftTopTaskId, rightBottomTaskId, mSplitLayout.calculateCurrentSnapPosition());
2001             if (mainStageTopTaskId != INVALID_TASK_ID && sideStageTopTaskId != INVALID_TASK_ID) {
2002                 // Update the pair for the top tasks
2003                 boolean added = recentTasks.addSplitPair(mainStageTopTaskId, sideStageTopTaskId,
2004                         splitBounds);
2005                 if (added) {
2006                     ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
2007                             "updateRecentTasksSplitPair: adding split pair ltTask=%d rbTask=%d",
2008                             leftTopTaskId, rightBottomTaskId);
2009                 }
2010             }
2011         });
2012     }
2013 
sendSplitVisibilityChanged()2014     private void sendSplitVisibilityChanged() {
2015         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "sendSplitVisibilityChanged: dividerVisible=%b",
2016                 mDividerVisible);
2017         for (int i = mListeners.size() - 1; i >= 0; --i) {
2018             final SplitScreen.SplitScreenListener l = mListeners.get(i);
2019             l.onSplitVisibilityChanged(mDividerVisible);
2020         }
2021         sendOnBoundsChanged();
2022     }
2023 
2024     @Override
2025     @CallSuper
onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash)2026     public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) {
2027         if (mRootTaskInfo != null || taskInfo.hasParentTask()) {
2028             throw new IllegalArgumentException(this + "\n Unknown task appeared: " + taskInfo);
2029         }
2030 
2031         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskAppeared: task=%s", taskInfo);
2032         mRootTaskInfo = taskInfo;
2033         mRootTaskLeash = leash;
2034 
2035         if (mSplitLayout == null) {
2036             mSplitLayout = new SplitLayout(TAG + "SplitDivider", mContext,
2037                     mRootTaskInfo.configuration, this, mParentContainerCallbacks,
2038                     mDisplayController, mDisplayImeController, mTaskOrganizer,
2039                     PARALLAX_ALIGN_CENTER /* parallaxType */);
2040             mDisplayInsetsController.addInsetsChangedListener(mDisplayId, mSplitLayout);
2041         }
2042 
2043         onRootTaskAppeared();
2044     }
2045 
2046     @Override
2047     @CallSuper
onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo)2048     public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) {
2049         if (mRootTaskInfo == null || mRootTaskInfo.taskId != taskInfo.taskId) {
2050             throw new IllegalArgumentException(this + "\n Unknown task info changed: " + taskInfo);
2051         }
2052         mWindowDecorViewModel.ifPresent(viewModel -> viewModel.onTaskInfoChanged(taskInfo));
2053         mRootTaskInfo = taskInfo;
2054         if (mSplitLayout != null
2055                 && mSplitLayout.updateConfiguration(mRootTaskInfo.configuration)
2056                 && mMainStage.isActive()) {
2057             ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskInfoChanged: task=%d updating",
2058                     taskInfo.taskId);
2059             // Clear the divider remote animating flag as the divider will be re-rendered to apply
2060             // the new rotation config.  Don't reset the IME state since those updates are not in
2061             // sync with task info changes.
2062             mIsDividerRemoteAnimating = false;
2063             mSplitLayout.update(null /* t */, false /* resetImePosition */);
2064             onLayoutSizeChanged(mSplitLayout);
2065         }
2066     }
2067 
2068     @Override
2069     @CallSuper
onTaskVanished(ActivityManager.RunningTaskInfo taskInfo)2070     public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) {
2071         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskVanished: task=%s", taskInfo);
2072         if (mRootTaskInfo == null) {
2073             throw new IllegalArgumentException(this + "\n Unknown task vanished: " + taskInfo);
2074         }
2075 
2076         onRootTaskVanished();
2077 
2078         if (mSplitLayout != null) {
2079             mSplitLayout.release();
2080             mSplitLayout = null;
2081         }
2082 
2083         mRootTaskInfo = null;
2084         mRootTaskLeash = null;
2085         mIsRootTranslucent = false;
2086     }
2087 
2088 
2089     @VisibleForTesting
onRootTaskAppeared()2090     void onRootTaskAppeared() {
2091         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRootTaskAppeared: rootTask=%s mainRoot=%b sideRoot=%b",
2092                 mRootTaskInfo, mMainStageListener.mHasRootTask, mSideStageListener.mHasRootTask);
2093         // Wait unit all root tasks appeared.
2094         if (mRootTaskInfo == null
2095                 || !mMainStageListener.mHasRootTask
2096                 || !mSideStageListener.mHasRootTask) {
2097             return;
2098         }
2099 
2100         final WindowContainerTransaction wct = new WindowContainerTransaction();
2101         wct.reparent(mMainStage.mRootTaskInfo.token, mRootTaskInfo.token, true);
2102         wct.reparent(mSideStage.mRootTaskInfo.token, mRootTaskInfo.token, true);
2103         // Make the stages adjacent to each other so they occlude what's behind them.
2104         wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token);
2105         setRootForceTranslucent(true, wct);
2106         mSplitLayout.getInvisibleBounds(mTempRect1);
2107         wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1);
2108         mSyncQueue.queue(wct);
2109         mSyncQueue.runInSync(t -> {
2110             t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.top);
2111         });
2112         mLaunchAdjacentController.setLaunchAdjacentRoot(mSideStage.mRootTaskInfo.token);
2113     }
2114 
2115     /** Callback when split roots have child task appeared under it, this is a little different from
2116      * #onStageHasChildrenChanged because this would be called every time child task appeared.
2117      * NOTICE: This only be called on legacy transition. */
onChildTaskAppeared(StageListenerImpl stageListener, int taskId)2118     private void onChildTaskAppeared(StageListenerImpl stageListener, int taskId) {
2119         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onChildTaskAppeared: isMainStage=%b task=%d",
2120                 stageListener == mMainStageListener, taskId);
2121         // Handle entering split screen while there is a split pair running in the background.
2122         if (stageListener == mSideStageListener && !isSplitScreenVisible() && isSplitActive()
2123                 && mSplitRequest == null) {
2124             final WindowContainerTransaction wct = new WindowContainerTransaction();
2125             prepareEnterSplitScreen(wct);
2126             mMainStage.evictAllChildren(wct);
2127             mSideStage.evictOtherChildren(wct, taskId);
2128 
2129             mSyncQueue.queue(wct);
2130             mSyncQueue.runInSync(t -> {
2131                 if (mIsDropEntering) {
2132                     updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
2133                     mIsDropEntering = false;
2134                     mSkipEvictingMainStageChildren = false;
2135                 } else {
2136                     mShowDecorImmediately = true;
2137                     mSplitLayout.flingDividerToCenter(/*finishCallback*/ null);
2138                 }
2139             });
2140         }
2141     }
2142 
onRootTaskVanished()2143     private void onRootTaskVanished() {
2144         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRootTaskVanished");
2145         final WindowContainerTransaction wct = new WindowContainerTransaction();
2146         mLaunchAdjacentController.clearLaunchAdjacentRoot();
2147         applyExitSplitScreen(null /* childrenToTop */, wct, EXIT_REASON_ROOT_TASK_VANISHED);
2148         mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, mSplitLayout);
2149     }
2150 
setRootForceTranslucent(boolean translucent, WindowContainerTransaction wct)2151     private void setRootForceTranslucent(boolean translucent, WindowContainerTransaction wct) {
2152         if (mIsRootTranslucent == translucent) return;
2153 
2154         mIsRootTranslucent = translucent;
2155         wct.setForceTranslucent(mRootTaskInfo.token, translucent);
2156     }
2157 
2158     /** Callback when split roots visiblility changed.
2159      * NOTICE: This only be called on legacy transition. */
onStageVisibilityChanged(StageListenerImpl stageListener)2160     private void onStageVisibilityChanged(StageListenerImpl stageListener) {
2161         // If split didn't active, just ignore this callback because we should already did these
2162         // on #applyExitSplitScreen.
2163         if (!isSplitActive()) {
2164             return;
2165         }
2166 
2167         final boolean sideStageVisible = mSideStageListener.mVisible;
2168         final boolean mainStageVisible = mMainStageListener.mVisible;
2169 
2170         // Wait for both stages having the same visibility to prevent causing flicker.
2171         if (mainStageVisible != sideStageVisible) {
2172             return;
2173         }
2174 
2175         // TODO Protolog
2176 
2177         // Check if it needs to dismiss split screen when both stage invisible.
2178         if (!mainStageVisible && mExitSplitScreenOnHide) {
2179             exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RETURN_HOME);
2180             return;
2181         }
2182 
2183         final WindowContainerTransaction wct = new WindowContainerTransaction();
2184         if (!mainStageVisible) {
2185             // Split entering background.
2186             wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
2187                     true /* setReparentLeafTaskIfRelaunch */);
2188             setRootForceTranslucent(true, wct);
2189         } else {
2190             clearRequestIfPresented();
2191             wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
2192                     false /* setReparentLeafTaskIfRelaunch */);
2193             setRootForceTranslucent(false, wct);
2194         }
2195 
2196         mSyncQueue.queue(wct);
2197         setDividerVisibility(mainStageVisible, null);
2198     }
2199 
2200     // Set divider visibility flag and try to apply it, the param transaction is used to apply.
2201     // See applyDividerVisibility for more detail.
setDividerVisibility(boolean visible, @Nullable SurfaceControl.Transaction t)2202     private void setDividerVisibility(boolean visible, @Nullable SurfaceControl.Transaction t) {
2203         if (visible == mDividerVisible) {
2204             return;
2205         }
2206 
2207         ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
2208                 "setDividerVisibility: visible=%b keyguardShowing=%b dividerAnimating=%b caller=%s",
2209                 visible, mKeyguardShowing, mIsDividerRemoteAnimating, Debug.getCaller());
2210 
2211         // Defer showing divider bar after keyguard dismissed, so it won't interfere with keyguard
2212         // dismissing animation.
2213         if (visible && mKeyguardShowing) {
2214             ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
2215                     "   Defer showing divider bar due to keyguard showing.");
2216             return;
2217         }
2218 
2219         mDividerVisible = visible;
2220         sendSplitVisibilityChanged();
2221 
2222         if (mIsDividerRemoteAnimating) {
2223             ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
2224                     "   Skip animating divider bar due to it's remote animating.");
2225             return;
2226         }
2227 
2228         applyDividerVisibility(t);
2229     }
2230 
2231     // Apply divider visibility by current visibility flag. If param transaction is non-null, it
2232     // will apply by that transaction, if it is null and visible, it will run a fade-in animation,
2233     // otherwise hide immediately.
applyDividerVisibility(@ullable SurfaceControl.Transaction t)2234     private void applyDividerVisibility(@Nullable SurfaceControl.Transaction t) {
2235         final SurfaceControl dividerLeash = mSplitLayout.getDividerLeash();
2236         if (dividerLeash == null) {
2237             ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
2238                     "   Skip animating divider bar due to divider leash not ready.");
2239             return;
2240         }
2241         if (mIsDividerRemoteAnimating) {
2242             ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
2243                     "   Skip animating divider bar due to it's remote animating.");
2244             return;
2245         }
2246 
2247         if (mDividerFadeInAnimator != null && mDividerFadeInAnimator.isRunning()) {
2248             mDividerFadeInAnimator.cancel();
2249         }
2250 
2251         mSplitLayout.getRefDividerBounds(mTempRect1);
2252         if (t != null) {
2253             t.setVisibility(dividerLeash, mDividerVisible);
2254             t.setLayer(dividerLeash, Integer.MAX_VALUE);
2255             t.setPosition(dividerLeash, mTempRect1.left, mTempRect1.top);
2256         } else if (mDividerVisible) {
2257             final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
2258             mDividerFadeInAnimator = ValueAnimator.ofFloat(0f, 1f);
2259             mDividerFadeInAnimator.addUpdateListener(animation -> {
2260                 if (dividerLeash == null || !dividerLeash.isValid()) {
2261                     mDividerFadeInAnimator.cancel();
2262                     return;
2263                 }
2264                 transaction.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
2265                 transaction.setAlpha(dividerLeash, (float) animation.getAnimatedValue());
2266                 transaction.apply();
2267             });
2268             mDividerFadeInAnimator.addListener(new AnimatorListenerAdapter() {
2269                 @Override
2270                 public void onAnimationStart(Animator animation) {
2271                     if (dividerLeash == null || !dividerLeash.isValid()) {
2272                         mDividerFadeInAnimator.cancel();
2273                         return;
2274                     }
2275                     mSplitLayout.getRefDividerBounds(mTempRect1);
2276                     transaction.show(dividerLeash);
2277                     transaction.setAlpha(dividerLeash, 0);
2278                     transaction.setLayer(dividerLeash, Integer.MAX_VALUE);
2279                     transaction.setPosition(dividerLeash, mTempRect1.left, mTempRect1.top);
2280                     transaction.apply();
2281                 }
2282 
2283                 @Override
2284                 public void onAnimationEnd(Animator animation) {
2285                     if (dividerLeash != null && dividerLeash.isValid()) {
2286                         transaction.setAlpha(dividerLeash, 1);
2287                         transaction.apply();
2288                     }
2289                     mTransactionPool.release(transaction);
2290                     mDividerFadeInAnimator = null;
2291                 }
2292             });
2293 
2294             mDividerFadeInAnimator.start();
2295         } else {
2296             final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
2297             transaction.hide(dividerLeash);
2298             transaction.apply();
2299             mTransactionPool.release(transaction);
2300         }
2301     }
2302 
2303     /** Callback when split roots have child or haven't under it.
2304      * NOTICE: This only be called on legacy transition. */
onStageHasChildrenChanged(StageListenerImpl stageListener)2305     private void onStageHasChildrenChanged(StageListenerImpl stageListener) {
2306         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onStageHasChildrenChanged: isMainStage=%b",
2307                 stageListener == mMainStageListener);
2308         final boolean hasChildren = stageListener.mHasChildren;
2309         final boolean isSideStage = stageListener == mSideStageListener;
2310         if (!hasChildren && !mIsExiting && mMainStage.isActive()) {
2311             if (isSideStage && mMainStageListener.mVisible) {
2312                 // Exit to main stage if side stage no longer has children.
2313                 mSplitLayout.flingDividerToDismiss(
2314                         mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT,
2315                         EXIT_REASON_APP_FINISHED);
2316             } else if (!isSideStage && mSideStageListener.mVisible) {
2317                 // Exit to side stage if main stage no longer has children.
2318                 mSplitLayout.flingDividerToDismiss(
2319                         mSideStagePosition != SPLIT_POSITION_BOTTOM_OR_RIGHT,
2320                         EXIT_REASON_APP_FINISHED);
2321             } else if (!isSplitScreenVisible() && mSplitRequest == null) {
2322                 // Dismiss split screen in the background once any sides of the split become empty.
2323                 exitSplitScreen(null /* childrenToTop */, EXIT_REASON_APP_FINISHED);
2324             }
2325         } else if (isSideStage && hasChildren && !mMainStage.isActive()) {
2326             final WindowContainerTransaction wct = new WindowContainerTransaction();
2327             prepareEnterSplitScreen(wct);
2328 
2329             mSyncQueue.queue(wct);
2330             mSyncQueue.runInSync(t -> {
2331                 if (mIsDropEntering) {
2332                     updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
2333                     mIsDropEntering = false;
2334                     mSkipEvictingMainStageChildren = false;
2335                 } else {
2336                     mShowDecorImmediately = true;
2337                     mSplitLayout.flingDividerToCenter(/*finishCallback*/ null);
2338                 }
2339             });
2340         }
2341         if (mMainStageListener.mHasChildren && mSideStageListener.mHasChildren) {
2342             mShouldUpdateRecents = true;
2343             clearRequestIfPresented();
2344             updateRecentTasksSplitPair();
2345 
2346             if (!mLogger.hasStartedSession()) {
2347                 if (!mLogger.hasValidEnterSessionId()) {
2348                     mLogger.enterRequested(null /*enterSessionId*/, ENTER_REASON_MULTI_INSTANCE);
2349                 }
2350                 mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(),
2351                         getMainStagePosition(), mMainStage.getTopChildTaskUid(),
2352                         getSideStagePosition(), mSideStage.getTopChildTaskUid(),
2353                         mSplitLayout.isLeftRightSplit());
2354             }
2355         }
2356     }
2357 
2358     @Override
onSnappedToDismiss(boolean bottomOrRight, @ExitReason int exitReason)2359     public void onSnappedToDismiss(boolean bottomOrRight, @ExitReason int exitReason) {
2360         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onSnappedToDismiss: bottomOrRight=%b reason=%s",
2361                 bottomOrRight, exitReasonToString(exitReason));
2362         final boolean mainStageToTop =
2363                 bottomOrRight ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT
2364                         : mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT;
2365         final StageTaskListener toTopStage = mainStageToTop ? mMainStage : mSideStage;
2366         if (!ENABLE_SHELL_TRANSITIONS) {
2367             exitSplitScreen(toTopStage, exitReason);
2368             return;
2369         }
2370 
2371         final int dismissTop = mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
2372         final WindowContainerTransaction wct = new WindowContainerTransaction();
2373         toTopStage.resetBounds(wct);
2374         prepareExitSplitScreen(dismissTop, wct);
2375         if (mRootTaskInfo != null) {
2376             wct.setDoNotPip(mRootTaskInfo.token);
2377         }
2378         mSplitTransitions.startDismissTransition(wct, this, dismissTop, EXIT_REASON_DRAG_DIVIDER);
2379     }
2380 
2381     @Override
onDoubleTappedDivider()2382     public void onDoubleTappedDivider() {
2383         switchSplitPosition("double tap");
2384     }
2385 
2386     @Override
onLayoutPositionChanging(SplitLayout layout)2387     public void onLayoutPositionChanging(SplitLayout layout) {
2388         final SurfaceControl.Transaction t = mTransactionPool.acquire();
2389         t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
2390         updateSurfaceBounds(layout, t, false /* applyResizingOffset */);
2391         t.apply();
2392         mTransactionPool.release(t);
2393     }
2394 
2395     @Override
onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY, boolean shouldUseParallaxEffect)2396     public void onLayoutSizeChanging(SplitLayout layout, int offsetX, int offsetY,
2397             boolean shouldUseParallaxEffect) {
2398         final SurfaceControl.Transaction t = mTransactionPool.acquire();
2399         t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
2400         updateSurfaceBounds(layout, t, shouldUseParallaxEffect);
2401         getMainStageBounds(mTempRect1);
2402         getSideStageBounds(mTempRect2);
2403         // TODO (b/307490004): "commonColor" below is a temporary fix to ensure the colors on both
2404         //  sides match. When b/307490004 is fixed, this code can be reverted.
2405         float[] commonColor = getResizingBackgroundColor(mSideStage.mRootTaskInfo).getComponents();
2406         mMainStage.onResizing(
2407                 mTempRect1, mTempRect2, t, offsetX, offsetY, mShowDecorImmediately, commonColor);
2408         mSideStage.onResizing(
2409                 mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately, commonColor);
2410         t.apply();
2411         mTransactionPool.release(t);
2412     }
2413 
2414     @Override
onLayoutSizeChanged(SplitLayout layout)2415     public void onLayoutSizeChanged(SplitLayout layout) {
2416         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onLayoutSizeChanged");
2417         // Reset this flag every time onLayoutSizeChanged.
2418         mShowDecorImmediately = false;
2419 
2420         final WindowContainerTransaction wct = new WindowContainerTransaction();
2421         boolean sizeChanged = updateWindowBounds(layout, wct);
2422         if (!sizeChanged) {
2423             // We still need to resize on decor for ensure all current status clear.
2424             final SurfaceControl.Transaction t = mTransactionPool.acquire();
2425             mMainStage.onResized(t);
2426             mSideStage.onResized(t);
2427             mTransactionPool.release(t);
2428             return;
2429         }
2430 
2431         sendOnBoundsChanged();
2432         if (ENABLE_SHELL_TRANSITIONS) {
2433             mSplitLayout.setDividerInteractive(false, false, "onSplitResizeStart");
2434             mSplitTransitions.startResizeTransition(wct, this, (aborted) -> {
2435                         mSplitLayout.setDividerInteractive(true, false, "onSplitResizeConsumed");
2436                     }, (finishWct, t) -> {
2437                         mSplitLayout.setDividerInteractive(true, false, "onSplitResizeFinish");
2438                     }, mMainStage.getSplitDecorManager(), mSideStage.getSplitDecorManager());
2439         } else {
2440             // Only need screenshot for legacy case because shell transition should screenshot
2441             // itself during transition.
2442             final SurfaceControl.Transaction startT = mTransactionPool.acquire();
2443             mMainStage.screenshotIfNeeded(startT);
2444             mSideStage.screenshotIfNeeded(startT);
2445             mTransactionPool.release(startT);
2446 
2447             mSyncQueue.queue(wct);
2448             mSyncQueue.runInSync(t -> {
2449                 updateSurfaceBounds(layout, t, false /* applyResizingOffset */);
2450                 mMainStage.onResized(t);
2451                 mSideStage.onResized(t);
2452             });
2453         }
2454         mLogger.logResize(mSplitLayout.getDividerPositionAsFraction());
2455     }
2456 
2457     /**
2458      * @return {@code true} if we should create a left-right split, {@code false} if we should
2459      * create a top-bottom split.
2460      */
isLeftRightSplit()2461     boolean isLeftRightSplit() {
2462         return mSplitLayout != null && mSplitLayout.isLeftRightSplit();
2463     }
2464 
2465     /**
2466      * Populates `wct` with operations that match the split windows to the current layout.
2467      * To match relevant surfaces, make sure to call updateSurfaceBounds after `wct` is applied
2468      *
2469      * @return true if stage bounds actually .
2470      */
updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct)2471     private boolean updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) {
2472         final StageTaskListener topLeftStage =
2473                 mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
2474         final StageTaskListener bottomRightStage =
2475                 mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
2476         boolean updated = layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo,
2477                 bottomRightStage.mRootTaskInfo);
2478         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "updateWindowBounds: topLeftStage=%s bottomRightStage=%s",
2479                 layout.getBounds1(), layout.getBounds2());
2480         return updated;
2481     }
2482 
updateSurfaceBounds(@ullable SplitLayout layout, @NonNull SurfaceControl.Transaction t, boolean applyResizingOffset)2483     void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t,
2484             boolean applyResizingOffset) {
2485         final StageTaskListener topLeftStage =
2486                 mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
2487         final StageTaskListener bottomRightStage =
2488                 mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
2489         (layout != null ? layout : mSplitLayout).applySurfaceChanges(t, topLeftStage.mRootLeash,
2490                 bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer,
2491                 applyResizingOffset);
2492         ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
2493                 "updateSurfaceBounds: topLeftStage=%s bottomRightStage=%s",
2494                 layout.getBounds1(), layout.getBounds2());
2495     }
2496 
2497     @Override
getSplitItemPosition(WindowContainerToken token)2498     public int getSplitItemPosition(WindowContainerToken token) {
2499         if (token == null) {
2500             return SPLIT_POSITION_UNDEFINED;
2501         }
2502 
2503         if (mMainStage.containsToken(token)) {
2504             return getMainStagePosition();
2505         } else if (mSideStage.containsToken(token)) {
2506             return getSideStagePosition();
2507         }
2508 
2509         return SPLIT_POSITION_UNDEFINED;
2510     }
2511 
2512     /**
2513      * Returns the {@link StageType} where {@param token} is being used
2514      * {@link SplitScreen#STAGE_TYPE_UNDEFINED} otherwise
2515      */
2516     @StageType
getSplitItemStage(@ullable WindowContainerToken token)2517     public int getSplitItemStage(@Nullable WindowContainerToken token) {
2518         if (token == null) {
2519             return STAGE_TYPE_UNDEFINED;
2520         }
2521 
2522         if (mMainStage.containsToken(token)) {
2523             return STAGE_TYPE_MAIN;
2524         } else if (mSideStage.containsToken(token)) {
2525             return STAGE_TYPE_SIDE;
2526         }
2527 
2528         return STAGE_TYPE_UNDEFINED;
2529     }
2530 
2531     @Override
setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout)2532     public void setLayoutOffsetTarget(int offsetX, int offsetY, SplitLayout layout) {
2533         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setLayoutOffsetTarget: x=%d y=%d",
2534                 offsetX, offsetY);
2535         final StageTaskListener topLeftStage =
2536                 mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage;
2537         final StageTaskListener bottomRightStage =
2538                 mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage;
2539         final WindowContainerTransaction wct = new WindowContainerTransaction();
2540         layout.applyLayoutOffsetTarget(wct, offsetX, offsetY, topLeftStage.mRootTaskInfo,
2541                 bottomRightStage.mRootTaskInfo);
2542         mTaskOrganizer.applyTransaction(wct);
2543     }
2544 
onDisplayAdded(int displayId)2545     public void onDisplayAdded(int displayId) {
2546         if (displayId != DEFAULT_DISPLAY) {
2547             return;
2548         }
2549         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onDisplayAdded: display=%d", displayId);
2550         mDisplayController.addDisplayChangingController(this::onDisplayChange);
2551     }
2552 
2553     /**
2554      * Update surfaces of the split screen layout based on the current state
2555      * @param transaction to write the updates to
2556      */
updateSurfaces(SurfaceControl.Transaction transaction)2557     public void updateSurfaces(SurfaceControl.Transaction transaction) {
2558         updateSurfaceBounds(mSplitLayout, transaction, /* applyResizingOffset */ false);
2559         mSplitLayout.update(transaction, true /* resetImePosition */);
2560     }
2561 
onDisplayChange(int displayId, int fromRotation, int toRotation, @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct)2562     private void onDisplayChange(int displayId, int fromRotation, int toRotation,
2563             @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction wct) {
2564         if (displayId != DEFAULT_DISPLAY || !mMainStage.isActive()) {
2565             return;
2566         }
2567 
2568         ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
2569                 "onDisplayChange: display=%d fromRot=%d toRot=%d config=%s",
2570                 displayId, fromRotation, toRotation,
2571                 newDisplayAreaInfo != null ? newDisplayAreaInfo.configuration : null);
2572         mSplitLayout.rotateTo(toRotation);
2573         if (newDisplayAreaInfo != null) {
2574             mSplitLayout.updateConfiguration(newDisplayAreaInfo.configuration);
2575         }
2576         updateWindowBounds(mSplitLayout, wct);
2577         sendOnBoundsChanged();
2578     }
2579 
2580     @VisibleForTesting
onFoldedStateChanged(boolean folded)2581     void onFoldedStateChanged(boolean folded) {
2582         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onFoldedStateChanged: folded=%b", folded);
2583         mTopStageAfterFoldDismiss = STAGE_TYPE_UNDEFINED;
2584         if (!folded) return;
2585 
2586         if (!isSplitActive() || !isSplitScreenVisible()) return;
2587 
2588         // To avoid split dismiss when user fold the device and unfold to use later, we only
2589         // record the flag here and try to dismiss on wakeUp callback to ensure split dismiss
2590         // when user interact on phone folded.
2591         if (mMainStage.isFocused()) {
2592             mTopStageAfterFoldDismiss = STAGE_TYPE_MAIN;
2593         } else if (mSideStage.isFocused()) {
2594             mTopStageAfterFoldDismiss = STAGE_TYPE_SIDE;
2595         }
2596     }
2597 
getSideStageBounds()2598     private Rect getSideStageBounds() {
2599         return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
2600                 ? mSplitLayout.getBounds1() : mSplitLayout.getBounds2();
2601     }
2602 
getMainStageBounds()2603     private Rect getMainStageBounds() {
2604         return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT
2605                 ? mSplitLayout.getBounds2() : mSplitLayout.getBounds1();
2606     }
2607 
getSideStageBounds(Rect rect)2608     private void getSideStageBounds(Rect rect) {
2609         if (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT) {
2610             mSplitLayout.getBounds1(rect);
2611         } else {
2612             mSplitLayout.getBounds2(rect);
2613         }
2614     }
2615 
getMainStageBounds(Rect rect)2616     private void getMainStageBounds(Rect rect) {
2617         if (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT) {
2618             mSplitLayout.getBounds2(rect);
2619         } else {
2620             mSplitLayout.getBounds1(rect);
2621         }
2622     }
2623 
2624     /**
2625      * Get the stage that should contain this `taskInfo`. The stage doesn't necessarily contain
2626      * this task (yet) so this can also be used to identify which stage to put a task into.
2627      */
getStageOfTask(ActivityManager.RunningTaskInfo taskInfo)2628     private StageTaskListener getStageOfTask(ActivityManager.RunningTaskInfo taskInfo) {
2629         // TODO(b/184679596): Find a way to either include task-org information in the transition,
2630         //                    or synchronize task-org callbacks so we can use stage.containsTask
2631         if (mMainStage.mRootTaskInfo != null
2632                 && taskInfo.parentTaskId == mMainStage.mRootTaskInfo.taskId) {
2633             return mMainStage;
2634         } else if (mSideStage.mRootTaskInfo != null
2635                 && taskInfo.parentTaskId == mSideStage.mRootTaskInfo.taskId) {
2636             return mSideStage;
2637         }
2638         return null;
2639     }
2640 
2641     @StageType
getStageType(StageTaskListener stage)2642     private int getStageType(StageTaskListener stage) {
2643         if (stage == null) return STAGE_TYPE_UNDEFINED;
2644         return stage == mMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
2645     }
2646 
2647     @Override
handleRequest(@onNull IBinder transition, @Nullable TransitionRequestInfo request)2648     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
2649             @Nullable TransitionRequestInfo request) {
2650         final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
2651         if (triggerTask == null) {
2652             if (isSplitActive()) {
2653                 ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d display rotation",
2654                         request.getDebugId());
2655                 // Check if the display is rotating.
2656                 final TransitionRequestInfo.DisplayChange displayChange =
2657                         request.getDisplayChange();
2658                 if (request.getType() == TRANSIT_CHANGE && displayChange != null
2659                         && displayChange.getStartRotation() != displayChange.getEndRotation()) {
2660                     mSplitLayout.setFreezeDividerWindow(true);
2661                 }
2662                 if (request.getRemoteTransition() != null) {
2663                     mSplitTransitions.setRemotePassThroughTransition(transition,
2664                             request.getRemoteTransition());
2665                 }
2666                 // Still want to monitor everything while in split-screen, so return non-null.
2667                 return new WindowContainerTransaction();
2668             } else {
2669                 return null;
2670             }
2671         } else if (triggerTask.displayId != mDisplayId) {
2672             // Skip handling task on the other display.
2673             return null;
2674         }
2675 
2676         WindowContainerTransaction out = null;
2677         final @WindowManager.TransitionType int type = request.getType();
2678         final boolean isOpening = isOpeningType(type);
2679         final boolean inFullscreen = triggerTask.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
2680 
2681         if (isOpening && inFullscreen) {
2682             // One task is opening into fullscreen mode, remove the corresponding split record.
2683             mRecentTasks.ifPresent(recentTasks -> recentTasks.removeSplitPair(triggerTask.taskId));
2684         }
2685 
2686         if (isSplitActive()) {
2687             ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d split active",
2688                     request.getDebugId());
2689             // Try to handle everything while in split-screen, so return a WCT even if it's empty.
2690             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  split is active so using split"
2691                             + "Transition to handle request. triggerTask=%d type=%s mainChildren=%d"
2692                             + " sideChildren=%d", triggerTask.taskId, transitTypeToString(type),
2693                     mMainStage.getChildCount(), mSideStage.getChildCount());
2694             out = new WindowContainerTransaction();
2695             final StageTaskListener stage = getStageOfTask(triggerTask);
2696             if (stage != null) {
2697                 if (isClosingType(type) && stage.getChildCount() == 1) {
2698                     // Dismiss split if the last task in one of the stages is going away
2699                     // The top should be the opposite side that is closing:
2700                     int dismissTop = getStageType(stage) == STAGE_TYPE_MAIN
2701                             ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
2702                     prepareExitSplitScreen(dismissTop, out);
2703                     mSplitTransitions.setDismissTransition(transition, dismissTop,
2704                             EXIT_REASON_APP_FINISHED);
2705                 } else if (isOpening && !mPausingTasks.isEmpty()) {
2706                     // One of the splitting task is opening while animating the split pair in
2707                     // recents, which means to dismiss the split pair to this task.
2708                     int dismissTop = getStageType(stage) == STAGE_TYPE_MAIN
2709                             ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
2710                     prepareExitSplitScreen(dismissTop, out);
2711                     mSplitTransitions.setDismissTransition(transition, dismissTop,
2712                             EXIT_REASON_APP_FINISHED);
2713                 } else if (!isSplitScreenVisible() && isOpening) {
2714                     // If split is running in the background and the trigger task is appearing into
2715                     // split, prepare to enter split screen.
2716                     prepareEnterSplitScreen(out);
2717                     mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(),
2718                             TRANSIT_SPLIT_SCREEN_PAIR_OPEN, !mIsDropEntering);
2719                 } else if (inFullscreen && isSplitScreenVisible()) {
2720                     // If the trigger task is in fullscreen and in split, exit split and place
2721                     // task on top
2722                     final int stageType = getStageOfTask(triggerTask.taskId);
2723                     prepareExitSplitScreen(stageType, out);
2724                     mSplitTransitions.setDismissTransition(transition, stageType,
2725                             EXIT_REASON_FULLSCREEN_REQUEST);
2726                 }
2727             } else if (isOpening && inFullscreen) {
2728                 final int activityType = triggerTask.getActivityType();
2729                 if (activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS) {
2730                     // starting recents/home, so don't handle this and let it fall-through to
2731                     // the remote handler.
2732                     return null;
2733                 }
2734 
2735                 if ((mMainStage.containsTask(triggerTask.taskId)
2736                             && mMainStage.getChildCount() == 1)
2737                         || (mSideStage.containsTask(triggerTask.taskId)
2738                             && mSideStage.getChildCount() == 1)) {
2739                     // A splitting task is opening to fullscreen causes one side of the split empty,
2740                     // so appends operations to exit split.
2741                     prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, out);
2742                 }
2743             } else if (type == TRANSIT_KEYGUARD_OCCLUDE && triggerTask.topActivity != null
2744                     && isSplitScreenVisible()) {
2745                 // Split include show when lock activity case, check the top activity under which
2746                 // stage and move it to the top.
2747                 int top = triggerTask.topActivity.equals(mMainStage.mRootTaskInfo.topActivity)
2748                         ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
2749                 prepareExitSplitScreen(top, out);
2750                 mSplitTransitions.setDismissTransition(transition, top,
2751                         EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP);
2752             }
2753 
2754             if (!out.isEmpty()) {
2755                 // One of the cases above handled it
2756                 return out;
2757             } else if (isSplitScreenVisible()) {
2758                 // If split is visible, only defer handling this transition if it's launching
2759                 // adjacent while there is already a split pair -- this may trigger PIP and
2760                 // that should be handled by the mixed handler.
2761                 final boolean deferTransition = requestHasLaunchAdjacentFlag(request)
2762                     && mMainStage.getChildCount() != 0 && mSideStage.getChildCount() != 0;
2763                 return !deferTransition ? out : null;
2764             }
2765             // Don't intercept the transition if we are not handling it as a part of one of the
2766             // cases above and it is not already visible
2767             return null;
2768         } else {
2769             if (triggerTask.parentTaskId == mMainStage.mRootTaskInfo.taskId
2770                     || triggerTask.parentTaskId == mSideStage.mRootTaskInfo.taskId) {
2771                 ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d "
2772                                 + "restoring to split", request.getDebugId());
2773                 out = new WindowContainerTransaction();
2774                 mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(),
2775                         TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, false /* resizeAnim */);
2776             }
2777             if (isOpening && getStageOfTask(triggerTask) != null) {
2778                 ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d enter split",
2779                         request.getDebugId());
2780                 // One task is appearing into split, prepare to enter split screen.
2781                 out = new WindowContainerTransaction();
2782                 prepareEnterSplitScreen(out);
2783                 mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(),
2784                         TRANSIT_SPLIT_SCREEN_PAIR_OPEN, !mIsDropEntering);
2785             }
2786             return out;
2787         }
2788     }
2789 
2790     /**
2791      * This is used for mixed scenarios. For such scenarios, just make sure to include exiting
2792      * split or entering split when appropriate.
2793      */
addEnterOrExitIfNeeded(@ullable TransitionRequestInfo request, @NonNull WindowContainerTransaction outWCT)2794     public void addEnterOrExitIfNeeded(@Nullable TransitionRequestInfo request,
2795             @NonNull WindowContainerTransaction outWCT) {
2796         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "addEnterOrExitIfNeeded: transition=%d",
2797                 request.getDebugId());
2798         final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
2799         if (triggerTask != null && triggerTask.displayId != mDisplayId) {
2800             // Skip handling task on the other display.
2801             return;
2802         }
2803         final @WindowManager.TransitionType int type = request.getType();
2804         if (isSplitActive() && !isOpeningType(type)
2805                 && (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0)) {
2806             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  One of the splits became "
2807                             + "empty during a mixed transition (one not handled by split),"
2808                             + " so make sure split-screen state is cleaned-up. "
2809                             + "mainStageCount=%d sideStageCount=%d", mMainStage.getChildCount(),
2810                     mSideStage.getChildCount());
2811             if (triggerTask != null) {
2812                 mRecentTasks.ifPresent(
2813                         recentTasks -> recentTasks.removeSplitPair(triggerTask.taskId));
2814             }
2815             @StageType int topStage = STAGE_TYPE_UNDEFINED;
2816             if (isSplitScreenVisible()) {
2817                 // Get the stage where a child exists to keep that stage onTop
2818                 if (mMainStage.getChildCount() != 0 && mSideStage.getChildCount() == 0) {
2819                     topStage = STAGE_TYPE_MAIN;
2820                 } else if (mSideStage.getChildCount() != 0 && mMainStage.getChildCount() == 0) {
2821                     topStage = STAGE_TYPE_SIDE;
2822                 }
2823             }
2824             prepareExitSplitScreen(topStage, outWCT);
2825         }
2826     }
2827 
2828     @Override
mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback)2829     public void mergeAnimation(IBinder transition, TransitionInfo info,
2830             SurfaceControl.Transaction t, IBinder mergeTarget,
2831             Transitions.TransitionFinishCallback finishCallback) {
2832         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "mergeAnimation: transition=%d", info.getDebugId());
2833         mSplitTransitions.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
2834     }
2835 
2836     /** Jump the current transition animation to the end. */
end()2837     public boolean end() {
2838         return mSplitTransitions.end();
2839     }
2840 
2841     @Override
onTransitionConsumed(@onNull IBinder transition, boolean aborted, @Nullable SurfaceControl.Transaction finishT)2842     public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
2843             @Nullable SurfaceControl.Transaction finishT) {
2844         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTransitionConsumed");
2845         mSplitTransitions.onTransitionConsumed(transition, aborted, finishT);
2846     }
2847 
2848     @Override
startAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback)2849     public boolean startAnimation(@NonNull IBinder transition,
2850             @NonNull TransitionInfo info,
2851             @NonNull SurfaceControl.Transaction startTransaction,
2852             @NonNull SurfaceControl.Transaction finishTransaction,
2853             @NonNull Transitions.TransitionFinishCallback finishCallback) {
2854         if (!mSplitTransitions.isPendingTransition(transition)) {
2855             // Not entering or exiting, so just do some house-keeping and validation.
2856 
2857             // If we're not in split-mode, just abort so something else can handle it.
2858             if (!mMainStage.isActive()) return false;
2859 
2860             ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startAnimation: transition=%d", info.getDebugId());
2861             mSplitLayout.setFreezeDividerWindow(false);
2862             final StageChangeRecord record = new StageChangeRecord();
2863             final int transitType = info.getType();
2864             TransitionInfo.Change pipChange = null;
2865             for (int iC = 0; iC < info.getChanges().size(); ++iC) {
2866                 final TransitionInfo.Change change = info.getChanges().get(iC);
2867                 if (change.getMode() == TRANSIT_CHANGE
2868                         && (change.getFlags() & FLAG_IS_DISPLAY) != 0) {
2869                     // Don't reset the IME state since those updates are not in sync with the
2870                     // display change transition
2871                     mSplitLayout.update(startTransaction, false /* resetImePosition */);
2872                 }
2873 
2874                 if (mMixedHandler.isEnteringPip(change, transitType)) {
2875                     pipChange = change;
2876                 }
2877 
2878                 final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
2879                 if (taskInfo == null) continue;
2880                 if (taskInfo.token.equals(mRootTaskInfo.token)) {
2881                     if (isOpeningType(change.getMode())) {
2882                         // Split is opened by someone so set it as visible.
2883                         setSplitsVisible(true);
2884                         // TODO(b/275664132): Find a way to integrate this with finishWct.
2885                         //  This is setting the flag to a task and not interfering with the
2886                         //  transition.
2887                         final WindowContainerTransaction wct = new WindowContainerTransaction();
2888                         wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
2889                                 false /* reparentLeafTaskIfRelaunch */);
2890                         mTaskOrganizer.applyTransaction(wct);
2891                     } else if (isClosingType(change.getMode())) {
2892                         // Split is closed by someone so set it as invisible.
2893                         setSplitsVisible(false);
2894                         // TODO(b/275664132): Find a way to integrate this with finishWct.
2895                         //  This is setting the flag to a task and not interfering with the
2896                         //  transition.
2897                         final WindowContainerTransaction wct = new WindowContainerTransaction();
2898                         wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
2899                                 true /* reparentLeafTaskIfRelaunch */);
2900                         mTaskOrganizer.applyTransaction(wct);
2901                     }
2902                     continue;
2903                 }
2904                 final StageTaskListener stage = getStageOfTask(taskInfo);
2905                 if (stage == null) {
2906                     if (change.getParent() == null && !isClosingType(change.getMode())
2907                             && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
2908                         record.mContainShowFullscreenChange = true;
2909                     }
2910                     continue;
2911                 }
2912                 if (isOpeningType(change.getMode())) {
2913                     if (!stage.containsTask(taskInfo.taskId)) {
2914                         Log.w(TAG, "Expected onTaskAppeared on " + stage + " to have been called"
2915                                 + " with " + taskInfo.taskId + " before startAnimation().");
2916                         record.addRecord(stage, true, taskInfo.taskId);
2917                     }
2918                 } else if (change.getMode() == TRANSIT_CLOSE) {
2919                     if (stage.containsTask(taskInfo.taskId)) {
2920                         record.addRecord(stage, false, taskInfo.taskId);
2921                         Log.w(TAG, "Expected onTaskVanished on " + stage + " to have been called"
2922                                 + " with " + taskInfo.taskId + " before startAnimation().");
2923                     }
2924                 }
2925             }
2926 
2927             if (pipChange != null) {
2928                 TransitionInfo.Change pipReplacingChange = getPipReplacingChange(info, pipChange,
2929                         mMainStage.mRootTaskInfo.taskId, mSideStage.mRootTaskInfo.taskId,
2930                         getSplitItemStage(pipChange.getLastParent()));
2931                 if (pipReplacingChange != null) {
2932                     // Set an enter transition for when startAnimation gets called again
2933                     mSplitTransitions.setEnterTransition(transition, /*remoteTransition*/ null,
2934                             TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, /*resizeAnim*/ false);
2935                 }
2936 
2937                 mMixedHandler.animatePendingEnterPipFromSplit(transition, info,
2938                         startTransaction, finishTransaction, finishCallback,
2939                         pipReplacingChange != null);
2940                 notifySplitAnimationFinished();
2941                 return true;
2942             }
2943 
2944             final ArraySet<StageTaskListener> dismissStages = record.getShouldDismissedStage();
2945             if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0
2946                     || dismissStages.size() == 1) {
2947                 // If the size of dismissStages == 1, one of the task is closed without prepare
2948                 // pending transition, which could happen if all activities were finished after
2949                 // finish top activity in a task, so the trigger task is null when handleRequest.
2950                 // Note if the size of dismissStages == 2, it's starting a new task,
2951                 // so don't handle it.
2952                 Log.e(TAG, "Somehow removed the last task in a stage outside of a proper "
2953                         + "transition.");
2954                 // This new transition would be merged to current one so we need to clear
2955                 // tile manually here.
2956                 clearSplitPairedInRecents(EXIT_REASON_APP_FINISHED);
2957                 final WindowContainerTransaction wct = new WindowContainerTransaction();
2958                 final int dismissTop = (dismissStages.size() == 1
2959                         && getStageType(dismissStages.valueAt(0)) == STAGE_TYPE_MAIN)
2960                         || mMainStage.getChildCount() == 0 ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN;
2961                 // If there is a fullscreen opening change, we should not bring stage to top.
2962                 prepareExitSplitScreen(
2963                         !record.mContainShowFullscreenChange && isSplitScreenVisible()
2964                         ? dismissTop : STAGE_TYPE_UNDEFINED, wct);
2965                 mSplitTransitions.startDismissTransition(wct, this, dismissTop,
2966                         EXIT_REASON_APP_FINISHED);
2967                 // This can happen in some pathological cases. For example:
2968                 // 1. main has 2 tasks [Task A (Single-task), Task B], side has one task [Task C]
2969                 // 2. Task B closes itself and starts Task A in LAUNCH_ADJACENT at the same time
2970                 // In this case, the result *should* be that we leave split.
2971                 // TODO(b/184679596): Find a way to either include task-org information in
2972                 //                    the transition, or synchronize task-org callbacks.
2973             }
2974             // Use normal animations.
2975             notifySplitAnimationFinished();
2976             return false;
2977         } else if (mMixedHandler != null && TransitionUtil.hasDisplayChange(info)) {
2978             // A display-change has been un-expectedly inserted into the transition. Redirect
2979             // handling to the mixed-handler to deal with splitting it up.
2980             if (mMixedHandler.animatePendingSplitWithDisplayChange(transition, info,
2981                     startTransaction, finishTransaction, finishCallback)) {
2982                 if (mSplitTransitions.isPendingResize(transition)) {
2983                     ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
2984                             "startAnimation: transition=%d display change", info.getDebugId());
2985                     // Only need to update in resize because divider exist before transition.
2986                     mSplitLayout.update(startTransaction, true /* resetImePosition */);
2987                     startTransaction.apply();
2988                 }
2989                 notifySplitAnimationFinished();
2990                 return true;
2991             }
2992         } else if (mSplitTransitions.isPendingPassThrough(transition)) {
2993             ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
2994                     "startAnimation: passThrough transition=%d", info.getDebugId());
2995             mSplitTransitions.mPendingRemotePassthrough.mRemoteHandler.startAnimation(transition,
2996                     info, startTransaction, finishTransaction, finishCallback);
2997             notifySplitAnimationFinished();
2998             return true;
2999         }
3000 
3001         return startPendingAnimation(transition, info, startTransaction, finishTransaction,
3002                 finishCallback);
3003     }
3004 
3005     static class StageChangeRecord {
3006         boolean mContainShowFullscreenChange = false;
3007         static class StageChange {
3008             final StageTaskListener mStageTaskListener;
3009             final IntArray mAddedTaskId = new IntArray();
3010             final IntArray mRemovedTaskId = new IntArray();
StageChange(StageTaskListener stage)3011             StageChange(StageTaskListener stage) {
3012                 mStageTaskListener = stage;
3013             }
3014 
shouldDismissStage()3015             boolean shouldDismissStage() {
3016                 if (mAddedTaskId.size() > 0 || mRemovedTaskId.size() == 0) {
3017                     return false;
3018                 }
3019                 int removeChildTaskCount = 0;
3020                 for (int i = mRemovedTaskId.size() - 1; i >= 0; --i) {
3021                     if (mStageTaskListener.containsTask(mRemovedTaskId.get(i))) {
3022                         ++removeChildTaskCount;
3023                     }
3024                 }
3025                 return removeChildTaskCount == mStageTaskListener.getChildCount();
3026             }
3027         }
3028         private final ArrayMap<StageTaskListener, StageChange> mChanges = new ArrayMap<>();
3029 
addRecord(StageTaskListener stage, boolean open, int taskId)3030         void addRecord(StageTaskListener stage, boolean open, int taskId) {
3031             final StageChange next;
3032             if (!mChanges.containsKey(stage)) {
3033                 next = new StageChange(stage);
3034                 mChanges.put(stage, next);
3035             } else {
3036                 next = mChanges.get(stage);
3037             }
3038             if (open) {
3039                 next.mAddedTaskId.add(taskId);
3040             } else {
3041                 next.mRemovedTaskId.add(taskId);
3042             }
3043         }
3044 
getShouldDismissedStage()3045         ArraySet<StageTaskListener> getShouldDismissedStage() {
3046             final ArraySet<StageTaskListener> dismissTarget = new ArraySet<>();
3047             for (int i = mChanges.size() - 1; i >= 0; --i) {
3048                 final StageChange change = mChanges.valueAt(i);
3049                 if (change.shouldDismissStage()) {
3050                     dismissTarget.add(change.mStageTaskListener);
3051                 }
3052             }
3053             return dismissTarget;
3054         }
3055     }
3056 
3057     /** Starts the pending transition animation. */
startPendingAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback)3058     public boolean startPendingAnimation(@NonNull IBinder transition,
3059             @NonNull TransitionInfo info,
3060             @NonNull SurfaceControl.Transaction startTransaction,
3061             @NonNull SurfaceControl.Transaction finishTransaction,
3062             @NonNull Transitions.TransitionFinishCallback finishCallback) {
3063         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startPendingAnimation: transition=%d",
3064                 info.getDebugId());
3065         boolean shouldAnimate = true;
3066         if (mSplitTransitions.isPendingEnter(transition)) {
3067             shouldAnimate = startPendingEnterAnimation(transition,
3068                     mSplitTransitions.mPendingEnter, info, startTransaction, finishTransaction);
3069         } else if (mSplitTransitions.isPendingDismiss(transition)) {
3070             final SplitScreenTransitions.DismissSession dismiss = mSplitTransitions.mPendingDismiss;
3071             shouldAnimate = startPendingDismissAnimation(
3072                     dismiss, info, startTransaction, finishTransaction);
3073             if (shouldAnimate && dismiss.mReason == EXIT_REASON_DRAG_DIVIDER) {
3074                 final StageTaskListener toTopStage =
3075                         dismiss.mDismissTop == STAGE_TYPE_MAIN ? mMainStage : mSideStage;
3076                 mSplitTransitions.playDragDismissAnimation(transition, info, startTransaction,
3077                         finishTransaction, finishCallback, toTopStage.mRootTaskInfo.token,
3078                         toTopStage.getSplitDecorManager(), mRootTaskInfo.token);
3079                 return true;
3080             }
3081         } else if (mSplitTransitions.isPendingResize(transition)) {
3082             mSplitTransitions.playResizeAnimation(transition, info, startTransaction,
3083                     finishTransaction, finishCallback, mMainStage.mRootTaskInfo.token,
3084                     mSideStage.mRootTaskInfo.token, mMainStage.getSplitDecorManager(),
3085                     mSideStage.getSplitDecorManager());
3086             return true;
3087         }
3088         if (!shouldAnimate) return false;
3089 
3090         mSplitTransitions.playAnimation(transition, info, startTransaction, finishTransaction,
3091                 finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token,
3092                 mRootTaskInfo.token);
3093         return true;
3094     }
3095 
3096     /** Called to clean-up state and do house-keeping after the animation is done. */
onTransitionAnimationComplete()3097     public void onTransitionAnimationComplete() {
3098         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTransitionAnimationComplete");
3099         // If still playing, let it finish.
3100         if (!mMainStage.isActive() && !mIsExiting) {
3101             // Update divider state after animation so that it is still around and positioned
3102             // properly for the animation itself.
3103             mSplitLayout.release();
3104         }
3105     }
3106 
startPendingEnterAnimation(@onNull IBinder transition, @NonNull SplitScreenTransitions.EnterSession enterTransition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT)3107     private boolean startPendingEnterAnimation(@NonNull IBinder transition,
3108             @NonNull SplitScreenTransitions.EnterSession enterTransition,
3109             @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t,
3110             @NonNull SurfaceControl.Transaction finishT) {
3111         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startPendingEnterAnimation: enterTransition=%s",
3112                 enterTransition);
3113         // First, verify that we actually have opened apps in both splits.
3114         TransitionInfo.Change mainChild = null;
3115         TransitionInfo.Change sideChild = null;
3116         final WindowContainerTransaction evictWct = new WindowContainerTransaction();
3117         for (int iC = 0; iC < info.getChanges().size(); ++iC) {
3118             final TransitionInfo.Change change = info.getChanges().get(iC);
3119             final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
3120             if (taskInfo == null || !taskInfo.hasParentTask()) continue;
3121             if (mPausingTasks.contains(taskInfo.taskId)) {
3122                 continue;
3123             }
3124             final @StageType int stageType = getStageType(getStageOfTask(taskInfo));
3125             if (mainChild == null && stageType == STAGE_TYPE_MAIN
3126                     && (isOpeningType(change.getMode()) || change.getMode() == TRANSIT_CHANGE)) {
3127                 // Includes TRANSIT_CHANGE to cover reparenting top-most task to split.
3128                 mainChild = change;
3129             } else if (sideChild == null && stageType == STAGE_TYPE_SIDE
3130                     && (isOpeningType(change.getMode()) || change.getMode() == TRANSIT_CHANGE)) {
3131                 sideChild = change;
3132             } else if (stageType != STAGE_TYPE_UNDEFINED && change.getMode() == TRANSIT_TO_BACK) {
3133                 // Collect all to back task's and evict them when transition finished.
3134                 evictWct.reparent(taskInfo.token, null /* parent */, false /* onTop */);
3135             }
3136         }
3137 
3138         SplitScreenTransitions.EnterSession pendingEnter = mSplitTransitions.mPendingEnter;
3139         if (pendingEnter.mExtraTransitType
3140                 == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) {
3141             // Open to side should only be used when split already active and foregorund or when
3142             // app is restoring to split from fullscreen.
3143             if (mainChild == null && sideChild == null) {
3144                 Log.w(TAG, splitFailureMessage("startPendingEnterAnimation",
3145                         "Launched a task in split, but didn't receive any task in transition."));
3146                 // This should happen when the target app is already on front, so just cancel.
3147                 pendingEnter.cancel(null);
3148                 return true;
3149             }
3150         } else {
3151             if (mainChild == null || sideChild == null) {
3152                 final int dismissTop = mainChild != null ? STAGE_TYPE_MAIN :
3153                         (sideChild != null ? STAGE_TYPE_SIDE : STAGE_TYPE_UNDEFINED);
3154                 pendingEnter.cancel(
3155                         (cancelWct, cancelT) -> prepareExitSplitScreen(dismissTop, cancelWct));
3156                 Log.w(TAG, splitFailureMessage("startPendingEnterAnimation",
3157                         "launched 2 tasks in split, but didn't receive "
3158                         + "2 tasks in transition. Possibly one of them failed to launch"));
3159                 if (mRecentTasks.isPresent() && mainChild != null) {
3160                     mRecentTasks.get().removeSplitPair(mainChild.getTaskInfo().taskId);
3161                 }
3162                 if (mRecentTasks.isPresent() && sideChild != null) {
3163                     mRecentTasks.get().removeSplitPair(sideChild.getTaskInfo().taskId);
3164                 }
3165                 if (pendingEnter.mRemoteHandler != null) {
3166                     // Pass false for aborted since WM didn't abort, business logic chose to
3167                     // terminate/exit early
3168                     pendingEnter.mRemoteHandler.onTransitionConsumed(transition,
3169                             false /*aborted*/, finishT);
3170                 }
3171                 handleUnsupportedSplitStart();
3172                 return true;
3173             }
3174         }
3175 
3176         // Make some noise if things aren't totally expected. These states shouldn't effect
3177         // transitions locally, but remotes (like Launcher) may get confused if they were
3178         // depending on listener callbacks. This can happen because task-organizer callbacks
3179         // aren't serialized with transition callbacks.
3180         // This usually occurred on app use trampoline launch new task and finish itself.
3181         // TODO(b/184679596): Find a way to either include task-org information in
3182         //                    the transition, or synchronize task-org callbacks.
3183         final boolean mainNotContainOpenTask =
3184                 mainChild != null && !mMainStage.containsTask(mainChild.getTaskInfo().taskId);
3185         final boolean sideNotContainOpenTask =
3186                 sideChild != null && !mSideStage.containsTask(sideChild.getTaskInfo().taskId);
3187         if (mainNotContainOpenTask) {
3188             Log.w(TAG, "Expected onTaskAppeared on " + mMainStage
3189                     + " to have been called with " + mainChild.getTaskInfo().taskId
3190                     + " before startAnimation().");
3191         }
3192         if (sideNotContainOpenTask) {
3193             Log.w(TAG, "Expected onTaskAppeared on " + mSideStage
3194                     + " to have been called with " + sideChild.getTaskInfo().taskId
3195                     + " before startAnimation().");
3196         }
3197         final TransitionInfo.Change finalMainChild = mainChild;
3198         final TransitionInfo.Change finalSideChild = sideChild;
3199         enterTransition.setFinishedCallback((callbackWct, callbackT) -> {
3200             if (!enterTransition.mResizeAnim) {
3201                 // If resizing, we'll call notify at the end of the resizing animation (below)
3202                 notifySplitAnimationFinished();
3203             }
3204             if (finalMainChild != null) {
3205                 if (!mainNotContainOpenTask) {
3206                     mMainStage.evictOtherChildren(callbackWct, finalMainChild.getTaskInfo().taskId);
3207                 } else {
3208                     mMainStage.evictInvisibleChildren(callbackWct);
3209                 }
3210             }
3211             if (finalSideChild != null) {
3212                 if (!sideNotContainOpenTask) {
3213                     mSideStage.evictOtherChildren(callbackWct, finalSideChild.getTaskInfo().taskId);
3214                 } else {
3215                     mSideStage.evictInvisibleChildren(callbackWct);
3216                 }
3217             }
3218             if (!evictWct.isEmpty()) {
3219                 callbackWct.merge(evictWct, true);
3220             }
3221             if (enterTransition.mResizeAnim) {
3222                 mShowDecorImmediately = true;
3223                 mSplitLayout.flingDividerToCenter(this::notifySplitAnimationFinished);
3224             }
3225             callbackWct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false);
3226             mPausingTasks.clear();
3227         });
3228 
3229         if (info.getType() == TRANSIT_CHANGE && !isSplitActive()
3230                 && pendingEnter.mExtraTransitType == TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE) {
3231             if (finalMainChild != null && finalSideChild == null) {
3232                 requestEnterSplitSelect(finalMainChild.getTaskInfo(),
3233                         new WindowContainerTransaction(),
3234                         getMainStagePosition(), finalMainChild.getStartAbsBounds());
3235             } else if (finalSideChild != null && finalMainChild == null) {
3236                 requestEnterSplitSelect(finalSideChild.getTaskInfo(),
3237                         new WindowContainerTransaction(),
3238                         getSideStagePosition(), finalSideChild.getStartAbsBounds());
3239             } else {
3240                 throw new IllegalStateException(
3241                         "Attempting to restore to split but reparenting change not found");
3242             }
3243         }
3244 
3245         finishEnterSplitScreen(finishT);
3246         addDividerBarToTransition(info, true /* show */);
3247         return true;
3248     }
3249 
goToFullscreenFromSplit()3250     public void goToFullscreenFromSplit() {
3251         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "goToFullscreenFromSplit");
3252         // If main stage is focused, toEnd = true if
3253         // mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT. Otherwise toEnd = false
3254         // If side stage is focused, toEnd = true if
3255         // mSideStagePosition = SPLIT_POSITION_TOP_OR_LEFT. Otherwise toEnd = false
3256         final boolean toEnd;
3257         if (mMainStage.isFocused()) {
3258             toEnd = (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT);
3259         } else {
3260             toEnd = (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
3261         }
3262         mSplitLayout.flingDividerToDismiss(toEnd, EXIT_REASON_FULLSCREEN_SHORTCUT);
3263     }
3264 
3265     /** Move the specified task to fullscreen, regardless of focus state. */
moveTaskToFullscreen(int taskId, int exitReason)3266     public void moveTaskToFullscreen(int taskId, int exitReason) {
3267         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "moveTaskToFullscreen");
3268         boolean leftOrTop;
3269         if (mMainStage.containsTask(taskId)) {
3270             leftOrTop = (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
3271         } else if (mSideStage.containsTask(taskId)) {
3272             leftOrTop = (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT);
3273         } else {
3274             return;
3275         }
3276         mSplitLayout.flingDividerToDismiss(!leftOrTop, exitReason);
3277 
3278     }
3279 
3280     /**
3281      * Performs previous child eviction and such to prepare for the pip task expending into one of
3282      * the split stages
3283      *
3284      * @param taskInfo TaskInfo of the pip task
3285      */
onPipExpandToSplit(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo taskInfo)3286     public void onPipExpandToSplit(WindowContainerTransaction wct,
3287             ActivityManager.RunningTaskInfo taskInfo) {
3288         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onPipExpandToSplit: task=%s", taskInfo);
3289         prepareEnterSplitScreen(wct, taskInfo, getActivateSplitPosition(taskInfo),
3290                 false /*resizeAnim*/);
3291 
3292         if (!isSplitScreenVisible() || mSplitRequest == null) {
3293             return;
3294         }
3295 
3296         boolean replacingMainStage = getMainStagePosition() == mSplitRequest.mActivatePosition;
3297         (replacingMainStage ? mMainStage : mSideStage).evictOtherChildren(wct, taskInfo.taskId);
3298     }
3299 
isLaunchToSplit(TaskInfo taskInfo)3300     boolean isLaunchToSplit(TaskInfo taskInfo) {
3301         return getActivateSplitPosition(taskInfo) != SPLIT_POSITION_UNDEFINED;
3302     }
3303 
getActivateSplitPosition(TaskInfo taskInfo)3304     int getActivateSplitPosition(TaskInfo taskInfo) {
3305         if (mSplitRequest == null || taskInfo == null) {
3306             return SPLIT_POSITION_UNDEFINED;
3307         }
3308         if (mSplitRequest.mActivateTaskId != 0
3309                 && mSplitRequest.mActivateTaskId2 == taskInfo.taskId) {
3310             return mSplitRequest.mActivatePosition;
3311         }
3312         if (mSplitRequest.mActivateTaskId == taskInfo.taskId) {
3313             return mSplitRequest.mActivatePosition;
3314         }
3315         final String packageName1 = SplitScreenUtils.getPackageName(mSplitRequest.mStartIntent);
3316         final String basePackageName = SplitScreenUtils.getPackageName(taskInfo.baseIntent);
3317         if (packageName1 != null && packageName1.equals(basePackageName)) {
3318             return mSplitRequest.mActivatePosition;
3319         }
3320         final String packageName2 = SplitScreenUtils.getPackageName(mSplitRequest.mStartIntent2);
3321         if (packageName2 != null && packageName2.equals(basePackageName)) {
3322             return mSplitRequest.mActivatePosition;
3323         }
3324         return SPLIT_POSITION_UNDEFINED;
3325     }
3326 
3327     /**
3328      * Synchronize split-screen state with transition and make appropriate preparations.
3329      * @param toStage The stage that will not be dismissed. If set to
3330      *        {@link SplitScreen#STAGE_TYPE_UNDEFINED} then both stages will be dismissed
3331      */
prepareDismissAnimation(@tageType int toStage, @ExitReason int dismissReason, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT)3332     public void prepareDismissAnimation(@StageType int toStage, @ExitReason int dismissReason,
3333             @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t,
3334             @NonNull SurfaceControl.Transaction finishT) {
3335         ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
3336                 "prepareDismissAnimation: transition=%d toStage=%d reason=%s",
3337                 info.getDebugId(), toStage, exitReasonToString(dismissReason));
3338         // Make some noise if things aren't totally expected. These states shouldn't effect
3339         // transitions locally, but remotes (like Launcher) may get confused if they were
3340         // depending on listener callbacks. This can happen because task-organizer callbacks
3341         // aren't serialized with transition callbacks.
3342         // TODO(b/184679596): Find a way to either include task-org information in
3343         //                    the transition, or synchronize task-org callbacks.
3344         if (toStage == STAGE_TYPE_UNDEFINED) {
3345             if (mMainStage.getChildCount() != 0) {
3346                 final StringBuilder tasksLeft = new StringBuilder();
3347                 for (int i = 0; i < mMainStage.getChildCount(); ++i) {
3348                     tasksLeft.append(i != 0 ? ", " : "");
3349                     tasksLeft.append(mMainStage.mChildrenTaskInfo.keyAt(i));
3350                 }
3351                 Log.w(TAG, "Expected onTaskVanished on " + mMainStage
3352                         + " to have been called with [" + tasksLeft.toString()
3353                         + "] before startAnimation().");
3354             }
3355             if (mSideStage.getChildCount() != 0) {
3356                 final StringBuilder tasksLeft = new StringBuilder();
3357                 for (int i = 0; i < mSideStage.getChildCount(); ++i) {
3358                     tasksLeft.append(i != 0 ? ", " : "");
3359                     tasksLeft.append(mSideStage.mChildrenTaskInfo.keyAt(i));
3360                 }
3361                 Log.w(TAG, "Expected onTaskVanished on " + mSideStage
3362                         + " to have been called with [" + tasksLeft.toString()
3363                         + "] before startAnimation().");
3364             }
3365         }
3366 
3367         final ArrayMap<Integer, SurfaceControl> dismissingTasks = new ArrayMap<>();
3368         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
3369             final TransitionInfo.Change change = info.getChanges().get(i);
3370             final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
3371             if (taskInfo == null) continue;
3372             if (getStageOfTask(taskInfo) != null
3373                     || getSplitItemPosition(change.getLastParent()) != SPLIT_POSITION_UNDEFINED) {
3374                 dismissingTasks.put(taskInfo.taskId, change.getLeash());
3375             }
3376         }
3377 
3378 
3379         if (shouldBreakPairedTaskInRecents(dismissReason)) {
3380             // Notify recents if we are exiting in a way that breaks the pair, and disable further
3381             // updates to splits in the recents until we enter split again
3382             mRecentTasks.ifPresent(recentTasks -> {
3383                 for (int i = dismissingTasks.keySet().size() - 1; i >= 0; --i) {
3384                     recentTasks.removeSplitPair(dismissingTasks.keyAt(i));
3385                 }
3386             });
3387         }
3388         mSplitRequest = null;
3389 
3390         // Update local states.
3391         setSplitsVisible(false);
3392         // Wait until after animation to update divider
3393 
3394         // Reset crops so they don't interfere with subsequent launches
3395         t.setCrop(mMainStage.mRootLeash, null);
3396         t.setCrop(mSideStage.mRootLeash, null);
3397         // Hide the non-top stage and set the top one to the fullscreen position.
3398         if (toStage != STAGE_TYPE_UNDEFINED) {
3399             t.hide(toStage == STAGE_TYPE_MAIN ? mSideStage.mRootLeash : mMainStage.mRootLeash);
3400             t.setPosition(toStage == STAGE_TYPE_MAIN
3401                     ? mMainStage.mRootLeash : mSideStage.mRootLeash, 0, 0);
3402         } else {
3403             for (int i = dismissingTasks.keySet().size() - 1; i >= 0; --i) {
3404                 finishT.hide(dismissingTasks.valueAt(i));
3405             }
3406         }
3407 
3408         if (toStage == STAGE_TYPE_UNDEFINED) {
3409             logExit(dismissReason);
3410         } else {
3411             logExitToStage(dismissReason, toStage == STAGE_TYPE_MAIN);
3412         }
3413 
3414         // Hide divider and dim layer on transition finished.
3415         setDividerVisibility(false, t);
3416         finishT.hide(mMainStage.mDimLayer);
3417         finishT.hide(mSideStage.mDimLayer);
3418     }
3419 
startPendingDismissAnimation( @onNull SplitScreenTransitions.DismissSession dismissTransition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT)3420     private boolean startPendingDismissAnimation(
3421             @NonNull SplitScreenTransitions.DismissSession dismissTransition,
3422             @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t,
3423             @NonNull SurfaceControl.Transaction finishT) {
3424         ProtoLog.d(WM_SHELL_SPLIT_SCREEN,
3425                 "startPendingDismissAnimation: transition=%d dismissTransition=%s",
3426                 info.getDebugId(), dismissTransition);
3427         prepareDismissAnimation(dismissTransition.mDismissTop, dismissTransition.mReason, info,
3428                 t, finishT);
3429         if (dismissTransition.mDismissTop == STAGE_TYPE_UNDEFINED) {
3430             // TODO: Have a proper remote for this. Until then, though, reset state and use the
3431             //       normal animation stuff (which falls back to the normal launcher remote).
3432             setDividerVisibility(false, t);
3433             mSplitLayout.release(t);
3434             mSplitTransitions.mPendingDismiss = null;
3435             return false;
3436         }
3437         dismissTransition.setFinishedCallback((callbackWct, callbackT) -> {
3438             mMainStage.getSplitDecorManager().release(callbackT);
3439             mSideStage.getSplitDecorManager().release(callbackT);
3440             callbackWct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false);
3441         });
3442         return true;
3443     }
3444 
3445     /** Call this when starting the open-recents animation while split-screen is active. */
onRecentsInSplitAnimationStart(TransitionInfo info)3446     public void onRecentsInSplitAnimationStart(TransitionInfo info) {
3447         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationStart: transition=%d",
3448                 info.getDebugId());
3449         if (isSplitScreenVisible()) {
3450             // Cache tasks on live tile.
3451             for (int i = 0; i < info.getChanges().size(); ++i) {
3452                 final TransitionInfo.Change change = info.getChanges().get(i);
3453                 if (TransitionUtil.isClosingType(change.getMode())
3454                         && change.getTaskInfo() != null) {
3455                     final int taskId = change.getTaskInfo().taskId;
3456                     if (mMainStage.getTopVisibleChildTaskId() == taskId
3457                             || mSideStage.getTopVisibleChildTaskId() == taskId) {
3458                         mPausingTasks.add(taskId);
3459                     }
3460                 }
3461             }
3462         }
3463 
3464         addDividerBarToTransition(info, false /* show */);
3465     }
3466 
3467     /** Call this when the recents animation canceled during split-screen. */
onRecentsInSplitAnimationCanceled()3468     public void onRecentsInSplitAnimationCanceled() {
3469         mPausingTasks.clear();
3470         setSplitsVisible(false);
3471 
3472         final WindowContainerTransaction wct = new WindowContainerTransaction();
3473         wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
3474                 true /* reparentLeafTaskIfRelaunch */);
3475         mTaskOrganizer.applyTransaction(wct);
3476     }
3477 
3478     /** Call this when the recents animation during split-screen finishes. */
onRecentsInSplitAnimationFinish(WindowContainerTransaction finishWct, SurfaceControl.Transaction finishT)3479     public void onRecentsInSplitAnimationFinish(WindowContainerTransaction finishWct,
3480             SurfaceControl.Transaction finishT) {
3481         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationFinish");
3482         mPausingTasks.clear();
3483         // Check if the recent transition is finished by returning to the current
3484         // split, so we can restore the divider bar.
3485         for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) {
3486             final WindowContainerTransaction.HierarchyOp op =
3487                     finishWct.getHierarchyOps().get(i);
3488             final IBinder container = op.getContainer();
3489             if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop()
3490                     && (mMainStage.containsContainer(container)
3491                     || mSideStage.containsContainer(container))) {
3492                 updateSurfaceBounds(mSplitLayout, finishT,
3493                         false /* applyResizingOffset */);
3494                 finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash);
3495                 setDividerVisibility(true, finishT);
3496                 return;
3497             }
3498         }
3499 
3500         setSplitsVisible(false);
3501         finishWct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
3502                 true /* reparentLeafTaskIfRelaunch */);
3503     }
3504 
3505     /** Call this when the animation from split screen to desktop is started. */
onSplitToDesktop()3506     public void onSplitToDesktop() {
3507         setSplitsVisible(false);
3508     }
3509 
3510     /** Call this when the recents animation finishes by doing pair-to-pair switch. */
onRecentsPairToPairAnimationFinish(WindowContainerTransaction finishWct)3511     public void onRecentsPairToPairAnimationFinish(WindowContainerTransaction finishWct) {
3512         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsPairToPairAnimationFinish");
3513         // Pair-to-pair switch happened so here should evict the live tile from its stage.
3514         // Otherwise, the task will remain in stage, and occluding the new task when next time
3515         // user entering recents.
3516         for (int i = mPausingTasks.size() - 1; i >= 0; --i) {
3517             final int taskId = mPausingTasks.get(i);
3518             if (mMainStage.containsTask(taskId)) {
3519                 mMainStage.evictChildren(finishWct, taskId);
3520             } else if (mSideStage.containsTask(taskId)) {
3521                 mSideStage.evictChildren(finishWct, taskId);
3522             }
3523         }
3524         // If pending enter hasn't consumed, the mix handler will invoke start pending
3525         // animation within following transition.
3526         if (mSplitTransitions.mPendingEnter == null) {
3527             mPausingTasks.clear();
3528             updateRecentTasksSplitPair();
3529         }
3530     }
3531 
addDividerBarToTransition(@onNull TransitionInfo info, boolean show)3532     private void addDividerBarToTransition(@NonNull TransitionInfo info, boolean show) {
3533         final SurfaceControl leash = mSplitLayout.getDividerLeash();
3534         if (leash == null || !leash.isValid()) {
3535             Slog.w(TAG, "addDividerBarToTransition but leash was released or not be created");
3536             return;
3537         }
3538 
3539         final TransitionInfo.Change barChange = new TransitionInfo.Change(null /* token */, leash);
3540         mSplitLayout.getRefDividerBounds(mTempRect1);
3541         barChange.setParent(mRootTaskInfo.token);
3542         barChange.setStartAbsBounds(mTempRect1);
3543         barChange.setEndAbsBounds(mTempRect1);
3544         barChange.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK);
3545         barChange.setFlags(FLAG_IS_DIVIDER_BAR);
3546         // Technically this should be order-0, but this is running after layer assignment
3547         // and it's a special case, so just add to end.
3548         info.addChange(barChange);
3549     }
3550 
getDividerBarLegacyTarget()3551     RemoteAnimationTarget getDividerBarLegacyTarget() {
3552         final Rect bounds = mSplitLayout.getDividerBounds();
3553         return new RemoteAnimationTarget(-1 /* taskId */, -1 /* mode */,
3554                 mSplitLayout.getDividerLeash(), false /* isTranslucent */, null /* clipRect */,
3555                 null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */,
3556                 new android.graphics.Point(0, 0) /* position */, bounds, bounds,
3557                 new WindowConfiguration(), true, null /* startLeash */, null /* startBounds */,
3558                 null /* taskInfo */, false /* allowEnterPip */, TYPE_DOCK_DIVIDER);
3559     }
3560 
3561     @NeverCompile
3562     @Override
dump(@onNull PrintWriter pw, String prefix)3563     public void dump(@NonNull PrintWriter pw, String prefix) {
3564         final String innerPrefix = prefix + "  ";
3565         final String childPrefix = innerPrefix + "  ";
3566         pw.println(prefix + TAG + " mDisplayId=" + mDisplayId);
3567         pw.println(innerPrefix + "mDividerVisible=" + mDividerVisible);
3568         pw.println(innerPrefix + "isSplitActive=" + isSplitActive());
3569         pw.println(innerPrefix + "isSplitVisible=" + isSplitScreenVisible());
3570         pw.println(innerPrefix + "isLeftRightSplit=" + mSplitLayout.isLeftRightSplit());
3571         pw.println(innerPrefix + "MainStage");
3572         pw.println(childPrefix + "stagePosition=" + splitPositionToString(getMainStagePosition()));
3573         pw.println(childPrefix + "isActive=" + mMainStage.isActive());
3574         mMainStage.dump(pw, childPrefix);
3575         pw.println(innerPrefix + "MainStageListener");
3576         mMainStageListener.dump(pw, childPrefix);
3577         pw.println(innerPrefix + "SideStage");
3578         pw.println(childPrefix + "stagePosition=" + splitPositionToString(getSideStagePosition()));
3579         mSideStage.dump(pw, childPrefix);
3580         pw.println(innerPrefix + "SideStageListener");
3581         mSideStageListener.dump(pw, childPrefix);
3582         mSplitLayout.dump(pw, childPrefix);
3583         if (!mPausingTasks.isEmpty()) {
3584             pw.println(childPrefix + "mPausingTasks=" + mPausingTasks);
3585         }
3586     }
3587 
3588     /**
3589      * Directly set the visibility of both splits. This assumes hasChildren matches visibility.
3590      * This is intended for batch use, so it assumes other state management logic is already
3591      * handled.
3592      */
setSplitsVisible(boolean visible)3593     private void setSplitsVisible(boolean visible) {
3594         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setSplitsVisible: visible=%b", visible);
3595         mMainStageListener.mVisible = mSideStageListener.mVisible = visible;
3596         mMainStageListener.mHasChildren = mSideStageListener.mHasChildren = visible;
3597     }
3598 
3599     /**
3600      * Sets drag info to be logged when splitscreen is next entered.
3601      */
onDroppedToSplit(@plitPosition int position, InstanceId dragSessionId)3602     public void onDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
3603         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onDroppedToSplit: position=%d", position);
3604         if (!isSplitScreenVisible()) {
3605             mIsDropEntering = true;
3606             mSkipEvictingMainStageChildren = true;
3607         }
3608         if (!isSplitScreenVisible() && !ENABLE_SHELL_TRANSITIONS) {
3609             // If split running background, exit split first.
3610             // Skip this on shell transition due to we could evict existing tasks on transition
3611             // finished.
3612             exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT);
3613         }
3614         mLogger.enterRequestedByDrag(position, dragSessionId);
3615     }
3616 
3617     /**
3618      * Sets info to be logged when splitscreen is next entered.
3619      */
onRequestToSplit(InstanceId sessionId, @SplitEnterReason int enterReason)3620     public void onRequestToSplit(InstanceId sessionId, @SplitEnterReason int enterReason) {
3621         ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRequestToSplit: reason=%d", enterReason);
3622         if (!isSplitScreenVisible() && !ENABLE_SHELL_TRANSITIONS) {
3623             // If split running background, exit split first.
3624             // Skip this on shell transition due to we could evict existing tasks on transition
3625             // finished.
3626             exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT);
3627         }
3628         mLogger.enterRequested(sessionId, enterReason);
3629     }
3630 
3631     /**
3632      * Logs the exit of splitscreen.
3633      */
logExit(@xitReason int exitReason)3634     private void logExit(@ExitReason int exitReason) {
3635         mLogger.logExit(exitReason,
3636                 SPLIT_POSITION_UNDEFINED, 0 /* mainStageUid */,
3637                 SPLIT_POSITION_UNDEFINED, 0 /* sideStageUid */,
3638                 mSplitLayout.isLeftRightSplit());
3639     }
3640 
handleUnsupportedSplitStart()3641     private void handleUnsupportedSplitStart() {
3642         mSplitUnsupportedToast.show();
3643         notifySplitAnimationFinished();
3644     }
3645 
notifySplitAnimationFinished()3646     void notifySplitAnimationFinished() {
3647         if (mSplitInvocationListener == null || mSplitInvocationListenerExecutor == null) {
3648             return;
3649         }
3650         mSplitInvocationListenerExecutor.execute(() ->
3651                 mSplitInvocationListener.onSplitAnimationInvoked(false /*animationRunning*/));
3652     }
3653 
3654     /**
3655      * Logs the exit of splitscreen to a specific stage. This must be called before the exit is
3656      * executed.
3657      */
logExitToStage(@xitReason int exitReason, boolean toMainStage)3658     private void logExitToStage(@ExitReason int exitReason, boolean toMainStage) {
3659         mLogger.logExit(exitReason,
3660                 toMainStage ? getMainStagePosition() : SPLIT_POSITION_UNDEFINED,
3661                 toMainStage ? mMainStage.getTopChildTaskUid() : 0 /* mainStageUid */,
3662                 !toMainStage ? getSideStagePosition() : SPLIT_POSITION_UNDEFINED,
3663                 !toMainStage ? mSideStage.getTopChildTaskUid() : 0 /* sideStageUid */,
3664                 mSplitLayout.isLeftRightSplit());
3665     }
3666 
3667     class StageListenerImpl implements StageTaskListener.StageListenerCallbacks {
3668         boolean mHasRootTask = false;
3669         boolean mVisible = false;
3670         boolean mHasChildren = false;
3671 
3672         @Override
onRootTaskAppeared()3673         public void onRootTaskAppeared() {
3674             mHasRootTask = true;
3675             StageCoordinator.this.onRootTaskAppeared();
3676         }
3677 
3678         @Override
onChildTaskAppeared(int taskId)3679         public void onChildTaskAppeared(int taskId) {
3680             StageCoordinator.this.onChildTaskAppeared(this, taskId);
3681         }
3682 
3683         @Override
onStatusChanged(boolean visible, boolean hasChildren)3684         public void onStatusChanged(boolean visible, boolean hasChildren) {
3685             if (!mHasRootTask) return;
3686 
3687             if (mHasChildren != hasChildren) {
3688                 mHasChildren = hasChildren;
3689                 StageCoordinator.this.onStageHasChildrenChanged(this);
3690             }
3691             if (mVisible != visible) {
3692                 mVisible = visible;
3693                 StageCoordinator.this.onStageVisibilityChanged(this);
3694             }
3695         }
3696 
3697         @Override
onChildTaskStatusChanged(int taskId, boolean present, boolean visible)3698         public void onChildTaskStatusChanged(int taskId, boolean present, boolean visible) {
3699             StageCoordinator.this.onStageChildTaskStatusChanged(this, taskId, present, visible);
3700         }
3701 
3702         @Override
onRootTaskVanished()3703         public void onRootTaskVanished() {
3704             reset();
3705             StageCoordinator.this.onRootTaskVanished();
3706         }
3707 
3708         @Override
onNoLongerSupportMultiWindow(ActivityManager.RunningTaskInfo taskInfo)3709         public void onNoLongerSupportMultiWindow(ActivityManager.RunningTaskInfo taskInfo) {
3710             ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onNoLongerSupportMultiWindow: task=%s", taskInfo);
3711             if (mMainStage.isActive()) {
3712                 final boolean isMainStage = mMainStageListener == this;
3713                 if (!ENABLE_SHELL_TRANSITIONS) {
3714                     StageCoordinator.this.exitSplitScreen(isMainStage ? mMainStage : mSideStage,
3715                             EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW);
3716                     handleUnsupportedSplitStart();
3717                     return;
3718                 }
3719 
3720                 // If visible, we preserve the app and keep it running. If an app becomes
3721                 // unsupported in the bg, break split without putting anything on top
3722                 boolean splitScreenVisible = isSplitScreenVisible();
3723                 int stageType = STAGE_TYPE_UNDEFINED;
3724                 if (splitScreenVisible) {
3725                     stageType = isMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE;
3726                 }
3727                 final WindowContainerTransaction wct = new WindowContainerTransaction();
3728                 prepareExitSplitScreen(stageType, wct);
3729                 clearSplitPairedInRecents(EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW);
3730                 mSplitTransitions.startDismissTransition(wct, StageCoordinator.this, stageType,
3731                         EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW);
3732                 Log.w(TAG, splitFailureMessage("onNoLongerSupportMultiWindow",
3733                         "app package " + taskInfo.baseActivity.getPackageName()
3734                         + " does not support splitscreen, or is a controlled activity type"));
3735                 if (splitScreenVisible) {
3736                     handleUnsupportedSplitStart();
3737                 }
3738             }
3739         }
3740 
reset()3741         private void reset() {
3742             mHasRootTask = false;
3743             mVisible = false;
3744             mHasChildren = false;
3745         }
3746 
dump(@onNull PrintWriter pw, String prefix)3747         public void dump(@NonNull PrintWriter pw, String prefix) {
3748             pw.println(prefix + "mHasRootTask=" + mHasRootTask);
3749             pw.println(prefix + "mVisible=" + mVisible);
3750             pw.println(prefix + "mHasChildren=" + mHasChildren);
3751         }
3752     }
3753 }
3754