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