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