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