1 /* 2 * Copyright 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.quickstep.util; 18 19 import static com.android.launcher3.Utilities.postAsyncCallback; 20 import static com.android.launcher3.config.FeatureFlags.ENABLE_SPLIT_FROM_DESKTOP_TO_WORKSPACE; 21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP; 22 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM; 23 import static com.android.launcher3.testing.shared.TestProtocol.LAUNCH_SPLIT_PAIR; 24 import static com.android.launcher3.testing.shared.TestProtocol.testLogD; 25 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 26 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; 27 import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO; 28 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; 29 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_PENDINGINTENT; 30 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_TASK; 31 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SHORTCUT_TASK; 32 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SINGLE_INTENT_FULLSCREEN; 33 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SINGLE_SHORTCUT_FULLSCREEN; 34 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SINGLE_TASK_FULLSCREEN; 35 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_PENDINGINTENT; 36 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_SHORTCUT; 37 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_TASK; 38 39 import android.animation.Animator; 40 import android.animation.AnimatorListenerAdapter; 41 import android.annotation.NonNull; 42 import android.app.ActivityManager; 43 import android.app.ActivityOptions; 44 import android.app.ActivityThread; 45 import android.app.PendingIntent; 46 import android.content.Context; 47 import android.content.Intent; 48 import android.content.pm.PackageManager; 49 import android.content.pm.ShortcutInfo; 50 import android.graphics.Rect; 51 import android.graphics.RectF; 52 import android.graphics.drawable.Drawable; 53 import android.os.Bundle; 54 import android.os.Handler; 55 import android.os.IBinder; 56 import android.os.RemoteException; 57 import android.os.SystemClock; 58 import android.os.UserHandle; 59 import android.util.Log; 60 import android.util.Pair; 61 import android.view.RemoteAnimationAdapter; 62 import android.view.RemoteAnimationTarget; 63 import android.view.SurfaceControl; 64 import android.window.IRemoteTransition; 65 import android.window.IRemoteTransitionFinishedCallback; 66 import android.window.RemoteTransition; 67 import android.window.TransitionInfo; 68 69 import androidx.annotation.Nullable; 70 71 import com.android.internal.logging.InstanceId; 72 import com.android.launcher3.Launcher; 73 import com.android.launcher3.R; 74 import com.android.launcher3.anim.PendingAnimation; 75 import com.android.launcher3.config.FeatureFlags; 76 import com.android.launcher3.icons.IconProvider; 77 import com.android.launcher3.logging.StatsLogManager; 78 import com.android.launcher3.model.data.ItemInfo; 79 import com.android.launcher3.statehandlers.DepthController; 80 import com.android.launcher3.statemanager.StateManager; 81 import com.android.launcher3.testing.TestLogging; 82 import com.android.launcher3.testing.shared.TestProtocol; 83 import com.android.launcher3.util.ComponentKey; 84 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; 85 import com.android.quickstep.OverviewComponentObserver; 86 import com.android.quickstep.RecentsAnimationCallbacks; 87 import com.android.quickstep.RecentsAnimationController; 88 import com.android.quickstep.RecentsAnimationDeviceState; 89 import com.android.quickstep.RecentsAnimationTargets; 90 import com.android.quickstep.RecentsModel; 91 import com.android.quickstep.SplitSelectionListener; 92 import com.android.quickstep.SystemUiProxy; 93 import com.android.quickstep.TaskAnimationManager; 94 import com.android.quickstep.TaskViewUtils; 95 import com.android.quickstep.views.FloatingTaskView; 96 import com.android.quickstep.views.GroupedTaskView; 97 import com.android.quickstep.views.SplitInstructionsView; 98 import com.android.quickstep.views.RecentsView; 99 import com.android.systemui.shared.recents.model.Task; 100 import com.android.systemui.shared.system.ActivityManagerWrapper; 101 import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; 102 import com.android.wm.shell.splitscreen.ISplitSelectListener; 103 104 import java.io.PrintWriter; 105 import java.util.ArrayList; 106 import java.util.Collections; 107 import java.util.List; 108 import java.util.function.Consumer; 109 110 /** 111 * Represent data needed for the transient state when user has selected one app for split screen 112 * and is in the process of either a) selecting a second app or b) exiting intention to invoke split 113 */ 114 public class SplitSelectStateController { 115 private static final String TAG = "SplitSelectStateCtor"; 116 117 private Context mContext; 118 private final Handler mHandler; 119 private final RecentsModel mRecentTasksModel; 120 private final SplitAnimationController mSplitAnimationController; 121 private final AppPairsController mAppPairsController; 122 private final SplitSelectDataHolder mSplitSelectDataHolder; 123 private final StatsLogManager mStatsLogManager; 124 private final SystemUiProxy mSystemUiProxy; 125 private final StateManager mStateManager; 126 private SplitFromDesktopController mSplitFromDesktopController; 127 @Nullable 128 private DepthController mDepthController; 129 private boolean mRecentsAnimationRunning; 130 /** If {@code true}, animates the existing task view split placeholder view */ 131 private boolean mAnimateCurrentTaskDismissal; 132 /** 133 * Acts as a subset of {@link #mAnimateCurrentTaskDismissal}, we can't be dismissing from a 134 * split pair task view without wanting to animate current task dismissal overall 135 */ 136 private boolean mDismissingFromSplitPair; 137 /** If not null, this is the TaskView we want to launch from */ 138 @Nullable 139 private GroupedTaskView mLaunchingTaskView; 140 141 private FloatingTaskView mFirstFloatingTaskView; 142 private SplitInstructionsView mSplitInstructionsView; 143 144 private final List<SplitSelectionListener> mSplitSelectionListeners = new ArrayList<>(); 145 SplitSelectStateController(Context context, Handler handler, StateManager stateManager, DepthController depthController, StatsLogManager statsLogManager, SystemUiProxy systemUiProxy, RecentsModel recentsModel)146 public SplitSelectStateController(Context context, Handler handler, StateManager stateManager, 147 DepthController depthController, StatsLogManager statsLogManager, 148 SystemUiProxy systemUiProxy, RecentsModel recentsModel) { 149 mContext = context; 150 mHandler = handler; 151 mStatsLogManager = statsLogManager; 152 mSystemUiProxy = systemUiProxy; 153 mStateManager = stateManager; 154 mDepthController = depthController; 155 mRecentTasksModel = recentsModel; 156 mSplitAnimationController = new SplitAnimationController(this); 157 mAppPairsController = new AppPairsController(context, this, statsLogManager); 158 mSplitSelectDataHolder = new SplitSelectDataHolder(mContext); 159 } 160 onDestroy()161 public void onDestroy() { 162 mContext = null; 163 } 164 165 /** 166 * @param alreadyRunningTask if set to {@link android.app.ActivityTaskManager#INVALID_TASK_ID} 167 * then @param intent will be used to launch the initial task 168 * @param intent will be ignored if @param alreadyRunningTask is set 169 */ setInitialTaskSelect(@ullable Intent intent, @StagePosition int stagePosition, @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent, int alreadyRunningTask)170 public void setInitialTaskSelect(@Nullable Intent intent, @StagePosition int stagePosition, 171 @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent, 172 int alreadyRunningTask) { 173 mSplitSelectDataHolder.setInitialTaskSelect(intent, stagePosition, itemInfo, splitEvent, 174 alreadyRunningTask); 175 } 176 177 /** 178 * To be called after first task selected from using a split shortcut from the fullscreen 179 * running app. 180 */ setInitialTaskSelect(ActivityManager.RunningTaskInfo info, @StagePosition int stagePosition, @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent)181 public void setInitialTaskSelect(ActivityManager.RunningTaskInfo info, 182 @StagePosition int stagePosition, @NonNull ItemInfo itemInfo, 183 StatsLogManager.EventEnum splitEvent) { 184 mSplitSelectDataHolder.setInitialTaskSelect(info, stagePosition, itemInfo, splitEvent); 185 } 186 187 /** 188 * Maps a List<ComponentKey> to List<@Nullable Task>, searching through active Tasks in 189 * RecentsModel. If found, the Task will be the most recently-interacted-with instance of that 190 * Task. Then runs the given callback on that List. 191 * <p> 192 * Used in various task-switching or splitscreen operations when we need to check if there is a 193 * currently running Task of a certain type and use the most recent one. 194 */ findLastActiveTasksAndRunCallback( @ullable List<ComponentKey> componentKeys, Consumer<List<Task>> callback)195 public void findLastActiveTasksAndRunCallback( 196 @Nullable List<ComponentKey> componentKeys, Consumer<List<Task>> callback) { 197 mRecentTasksModel.getTasks(taskGroups -> { 198 if (componentKeys == null || componentKeys.isEmpty()) { 199 callback.accept(Collections.emptyList()); 200 return; 201 } 202 203 List<Task> lastActiveTasks = new ArrayList<>(); 204 // For each key we are looking for, add to lastActiveTasks with the corresponding Task 205 // (or null if not found). 206 for (ComponentKey key : componentKeys) { 207 Task lastActiveTask = null; 208 // Loop through tasks in reverse, since they are ordered with most-recent tasks last 209 for (int i = taskGroups.size() - 1; i >= 0; i--) { 210 GroupTask groupTask = taskGroups.get(i); 211 Task task1 = groupTask.task1; 212 // Don't add duplicate Tasks 213 if (isInstanceOfComponent(task1, key) && !lastActiveTasks.contains(task1)) { 214 lastActiveTask = task1; 215 break; 216 } 217 Task task2 = groupTask.task2; 218 if (isInstanceOfComponent(task2, key) && !lastActiveTasks.contains(task2)) { 219 lastActiveTask = task2; 220 break; 221 } 222 } 223 224 lastActiveTasks.add(lastActiveTask); 225 } 226 227 callback.accept(lastActiveTasks); 228 }); 229 } 230 231 /** 232 * Checks if a given Task is the most recently-active Task of type componentName. Used for 233 * selecting already-running Tasks for splitscreen. 234 */ isInstanceOfComponent(@ullable Task task, @NonNull ComponentKey componentKey)235 public boolean isInstanceOfComponent(@Nullable Task task, @NonNull ComponentKey componentKey) { 236 // Exclude the task that is already staged 237 if (task == null || task.key.id == mSplitSelectDataHolder.getInitialTaskId()) { 238 return false; 239 } 240 241 return task.key.baseIntent.getComponent().equals(componentKey.componentName) 242 && task.key.userId == componentKey.user.getIdentifier(); 243 } 244 245 /** 246 * Listener will only get callbacks going forward from the point of registration. No 247 * methods will be fired upon registering. 248 */ registerSplitListener(@onNull SplitSelectionListener listener)249 public void registerSplitListener(@NonNull SplitSelectionListener listener) { 250 if (mSplitSelectionListeners.contains(listener)) { 251 return; 252 } 253 mSplitSelectionListeners.add(listener); 254 } 255 unregisterSplitListener(@onNull SplitSelectionListener listener)256 public void unregisterSplitListener(@NonNull SplitSelectionListener listener) { 257 mSplitSelectionListeners.remove(listener); 258 } 259 dispatchOnSplitSelectionExit()260 private void dispatchOnSplitSelectionExit() { 261 for (SplitSelectionListener listener : mSplitSelectionListeners) { 262 listener.onSplitSelectionExit(false); 263 } 264 } 265 266 /** 267 * To be called when the both split tasks are ready to be launched. Call after launcher side 268 * animations are complete. 269 */ launchSplitTasks(@ullable Consumer<Boolean> callback)270 public void launchSplitTasks(@Nullable Consumer<Boolean> callback) { 271 Pair<InstanceId, com.android.launcher3.logging.InstanceId> instanceIds = 272 LogUtils.getShellShareableInstanceId(); 273 launchTasks(callback, false /* freezeTaskList */, DEFAULT_SPLIT_RATIO, 274 instanceIds.first); 275 276 mStatsLogManager.logger() 277 .withItemInfo(mSplitSelectDataHolder.getItemInfo()) 278 .withInstanceId(instanceIds.second) 279 .log(mSplitSelectDataHolder.getSplitEvent()); 280 } 281 282 /** 283 * A version of {@link #launchTasks(Consumer, boolean, float, InstanceId)} with no success 284 * callback. 285 */ launchSplitTasks()286 public void launchSplitTasks() { 287 launchSplitTasks(null); 288 } 289 290 /** 291 * To be called as soon as user selects the second task (even if animations aren't complete) 292 * @param task The second task that will be launched. 293 */ setSecondTask(Task task)294 public void setSecondTask(Task task) { 295 mSplitSelectDataHolder.setSecondTask(task.key.id); 296 } 297 298 /** 299 * To be called as soon as user selects the second app (even if animations aren't complete) 300 * @param intent The second intent that will be launched. 301 * @param user The user of that intent. 302 */ setSecondTask(Intent intent, UserHandle user)303 public void setSecondTask(Intent intent, UserHandle user) { 304 mSplitSelectDataHolder.setSecondTask(intent, user); 305 } 306 307 /** 308 * To be called as soon as user selects the second app (even if animations aren't complete) 309 * @param pendingIntent The second PendingIntent that will be launched. 310 */ setSecondTask(PendingIntent pendingIntent)311 public void setSecondTask(PendingIntent pendingIntent) { 312 mSplitSelectDataHolder.setSecondTask(pendingIntent); 313 } 314 315 /** 316 * To be called when we want to launch split pairs from Overview. Split can be initiated from 317 * either Overview or home, or all apps. Either both taskIds are set, or a pending intent + a 318 * fill in intent with a taskId2 are set. 319 * @param shellInstanceId loggingId to be used by shell, will be non-null for actions that 320 * create a split instance, null for cases that bring existing instaces to the 321 * foreground (quickswitch, launching previous pairs from overview) 322 */ launchTasks(@ullable Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio, @Nullable InstanceId shellInstanceId)323 public void launchTasks(@Nullable Consumer<Boolean> callback, boolean freezeTaskList, 324 float splitRatio, @Nullable InstanceId shellInstanceId) { 325 TestLogging.recordEvent( 326 TestProtocol.SEQUENCE_MAIN, "launchSplitTasks"); 327 final ActivityOptions options1 = ActivityOptions.makeBasic(); 328 if (freezeTaskList) { 329 options1.setFreezeRecentTasksReordering(); 330 } 331 332 SplitSelectDataHolder.SplitLaunchData launchData = 333 mSplitSelectDataHolder.getSplitLaunchData(); 334 int firstTaskId = launchData.getInitialTaskId(); 335 int secondTaskId = launchData.getSecondTaskId(); 336 ShortcutInfo firstShortcut = launchData.getInitialShortcut(); 337 ShortcutInfo secondShortcut = launchData.getSecondShortcut(); 338 PendingIntent firstPI = launchData.getInitialPendingIntent(); 339 PendingIntent secondPI = launchData.getSecondPendingIntent(); 340 int firstUserId = launchData.getInitialUserId(); 341 int secondUserId = launchData.getSecondUserId(); 342 int initialStagePosition = launchData.getInitialStagePosition(); 343 Bundle optionsBundle = options1.toBundle(); 344 345 if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { 346 final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId, 347 secondTaskId, callback, "LaunchSplitPair"); 348 switch (launchData.getSplitLaunchType()) { 349 case SPLIT_TASK_TASK -> 350 mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, 351 null /* options2 */, initialStagePosition, splitRatio, 352 remoteTransition, shellInstanceId); 353 354 case SPLIT_TASK_PENDINGINTENT -> 355 mSystemUiProxy.startIntentAndTask(secondPI, secondUserId, optionsBundle, 356 firstTaskId, null /*options2*/, initialStagePosition, splitRatio, 357 remoteTransition, shellInstanceId); 358 359 case SPLIT_TASK_SHORTCUT -> 360 mSystemUiProxy.startShortcutAndTask(secondShortcut, optionsBundle, 361 firstTaskId, null /*options2*/, initialStagePosition, splitRatio, 362 remoteTransition, shellInstanceId); 363 364 case SPLIT_PENDINGINTENT_TASK -> 365 mSystemUiProxy.startIntentAndTask(firstPI, firstUserId, optionsBundle, 366 secondTaskId, null /*options2*/, initialStagePosition, splitRatio, 367 remoteTransition, shellInstanceId); 368 369 case SPLIT_PENDINGINTENT_PENDINGINTENT -> 370 mSystemUiProxy.startIntents(firstPI, firstUserId, firstShortcut, 371 optionsBundle, secondPI, secondUserId, secondShortcut, 372 null /*options2*/, initialStagePosition, splitRatio, 373 remoteTransition, shellInstanceId); 374 375 case SPLIT_SHORTCUT_TASK -> 376 mSystemUiProxy.startShortcutAndTask(firstShortcut, optionsBundle, 377 secondTaskId, null /*options2*/, initialStagePosition, splitRatio, 378 remoteTransition, shellInstanceId); 379 } 380 } else { 381 final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId, secondTaskId, 382 callback); 383 switch (launchData.getSplitLaunchType()) { 384 case SPLIT_TASK_TASK -> 385 mSystemUiProxy.startTasksWithLegacyTransition(firstTaskId, optionsBundle, 386 secondTaskId, null /* options2 */, initialStagePosition, 387 splitRatio, adapter, shellInstanceId); 388 389 case SPLIT_TASK_PENDINGINTENT -> 390 mSystemUiProxy.startIntentAndTaskWithLegacyTransition(secondPI, 391 secondUserId, optionsBundle, firstTaskId, null /*options2*/, 392 initialStagePosition, splitRatio, adapter, shellInstanceId); 393 394 case SPLIT_TASK_SHORTCUT -> 395 mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(secondShortcut, 396 optionsBundle, firstTaskId, null /*options2*/, initialStagePosition, 397 splitRatio, adapter, shellInstanceId); 398 399 case SPLIT_PENDINGINTENT_TASK -> 400 mSystemUiProxy.startIntentAndTaskWithLegacyTransition(firstPI, firstUserId, 401 optionsBundle, secondTaskId, null /*options2*/, 402 initialStagePosition, splitRatio, adapter, shellInstanceId); 403 404 case SPLIT_PENDINGINTENT_PENDINGINTENT -> 405 mSystemUiProxy.startIntentsWithLegacyTransition(firstPI, firstUserId, 406 firstShortcut, optionsBundle, secondPI, secondUserId, 407 secondShortcut, null /*options2*/, initialStagePosition, splitRatio, 408 adapter, shellInstanceId); 409 410 case SPLIT_SHORTCUT_TASK -> 411 mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(firstShortcut, 412 optionsBundle, secondTaskId, null /*options2*/, 413 initialStagePosition, splitRatio, adapter, shellInstanceId); 414 } 415 } 416 } 417 418 /** 419 * Used to launch split screen from a split pair that already exists (usually accessible through 420 * Overview). This is different than {@link #launchTasks(Consumer, boolean, float, InstanceId)} 421 * in that this only launches split screen that are existing tasks. This doesn't determine which 422 * API should be used (i.e. launching split with existing tasks vs intents vs shortcuts, etc). 423 * 424 * <p/> 425 * NOTE: This is not to be used to launch AppPairs. 426 */ launchExistingSplitPair(@ullable GroupedTaskView groupedTaskView, int firstTaskId, int secondTaskId, @StagePosition int stagePosition, Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio)427 public void launchExistingSplitPair(@Nullable GroupedTaskView groupedTaskView, 428 int firstTaskId, int secondTaskId, @StagePosition int stagePosition, 429 Consumer<Boolean> callback, boolean freezeTaskList, float splitRatio) { 430 mLaunchingTaskView = groupedTaskView; 431 final ActivityOptions options1 = ActivityOptions.makeBasic(); 432 if (freezeTaskList) { 433 options1.setFreezeRecentTasksReordering(); 434 } 435 Bundle optionsBundle = options1.toBundle(); 436 437 if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { 438 final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId, 439 secondTaskId, callback, "LaunchExistingPair"); 440 mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, 441 null /* options2 */, stagePosition, splitRatio, 442 remoteTransition, null /*shellInstanceId*/); 443 } else { 444 final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId, 445 secondTaskId, callback); 446 mSystemUiProxy.startTasksWithLegacyTransition(firstTaskId, optionsBundle, 447 secondTaskId, null /* options2 */, stagePosition, 448 splitRatio, adapter, null /*shellInstanceId*/); 449 } 450 } 451 452 /** 453 * Launches the initially selected task/intent in fullscreen (note the same SystemUi APIs are 454 * used as {@link #launchSplitTasks(Consumer)} because they are overloaded to launch both 455 * split and fullscreen tasks) 456 */ launchInitialAppFullscreen(Consumer<Boolean> callback)457 public void launchInitialAppFullscreen(Consumer<Boolean> callback) { 458 final ActivityOptions options1 = ActivityOptions.makeBasic(); 459 SplitSelectDataHolder.SplitLaunchData launchData = 460 mSplitSelectDataHolder.getFullscreenLaunchData(); 461 int firstTaskId = launchData.getInitialTaskId(); 462 int secondTaskId = launchData.getSecondTaskId(); 463 PendingIntent firstPI = launchData.getInitialPendingIntent(); 464 int firstUserId = launchData.getInitialUserId(); 465 int initialStagePosition = launchData.getInitialStagePosition(); 466 ShortcutInfo initialShortcut = launchData.getInitialShortcut(); 467 Bundle optionsBundle = options1.toBundle(); 468 469 final RemoteSplitLaunchTransitionRunner animationRunner = 470 new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback); 471 final RemoteTransition remoteTransition = new RemoteTransition(animationRunner, 472 ActivityThread.currentActivityThread().getApplicationThread(), 473 "LaunchAppFullscreen"); 474 InstanceId instanceId = LogUtils.getShellShareableInstanceId().first; 475 if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { 476 switch (launchData.getSplitLaunchType()) { 477 case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId, 478 optionsBundle, secondTaskId, null /* options2 */, initialStagePosition, 479 DEFAULT_SPLIT_RATIO, remoteTransition, instanceId); 480 case SPLIT_SINGLE_INTENT_FULLSCREEN -> mSystemUiProxy.startIntentAndTask(firstPI, 481 firstUserId, optionsBundle, secondTaskId, null /*options2*/, 482 initialStagePosition, DEFAULT_SPLIT_RATIO, remoteTransition, 483 instanceId); 484 case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> mSystemUiProxy.startShortcutAndTask( 485 initialShortcut, optionsBundle, firstTaskId, null /* options2 */, 486 initialStagePosition, DEFAULT_SPLIT_RATIO, remoteTransition, instanceId); 487 } 488 } else { 489 final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId, 490 secondTaskId, callback); 491 switch (launchData.getSplitLaunchType()) { 492 case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasksWithLegacyTransition( 493 firstTaskId, optionsBundle, secondTaskId, null /* options2 */, 494 initialStagePosition, DEFAULT_SPLIT_RATIO, adapter, instanceId); 495 case SPLIT_SINGLE_INTENT_FULLSCREEN -> 496 mSystemUiProxy.startIntentAndTaskWithLegacyTransition(firstPI, firstUserId, 497 optionsBundle, secondTaskId, null /*options2*/, 498 initialStagePosition, DEFAULT_SPLIT_RATIO, adapter, 499 instanceId); 500 case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> 501 mSystemUiProxy.startShortcutAndTaskWithLegacyTransition( 502 initialShortcut, optionsBundle, firstTaskId, null /* options2 */, 503 initialStagePosition, DEFAULT_SPLIT_RATIO, adapter, instanceId); 504 } 505 } 506 } 507 initSplitFromDesktopController(Launcher launcher)508 public void initSplitFromDesktopController(Launcher launcher) { 509 mSplitFromDesktopController = new SplitFromDesktopController(launcher); 510 } 511 getShellRemoteTransition(int firstTaskId, int secondTaskId, @Nullable Consumer<Boolean> callback, String transitionName)512 private RemoteTransition getShellRemoteTransition(int firstTaskId, int secondTaskId, 513 @Nullable Consumer<Boolean> callback, String transitionName) { 514 final RemoteSplitLaunchTransitionRunner animationRunner = 515 new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback); 516 return new RemoteTransition(animationRunner, 517 ActivityThread.currentActivityThread().getApplicationThread(), transitionName); 518 } 519 getLegacyRemoteAdapter(int firstTaskId, int secondTaskId, @Nullable Consumer<Boolean> callback)520 private RemoteAnimationAdapter getLegacyRemoteAdapter(int firstTaskId, int secondTaskId, 521 @Nullable Consumer<Boolean> callback) { 522 final RemoteSplitLaunchAnimationRunner animationRunner = 523 new RemoteSplitLaunchAnimationRunner(firstTaskId, secondTaskId, callback); 524 return new RemoteAnimationAdapter(animationRunner, 300, 150, 525 ActivityThread.currentActivityThread().getApplicationThread()); 526 } 527 getActiveSplitStagePosition()528 public @StagePosition int getActiveSplitStagePosition() { 529 return mSplitSelectDataHolder.getInitialStagePosition(); 530 } 531 getSplitEvent()532 public StatsLogManager.EventEnum getSplitEvent() { 533 return mSplitSelectDataHolder.getSplitEvent(); 534 } 535 setRecentsAnimationRunning(boolean running)536 public void setRecentsAnimationRunning(boolean running) { 537 mRecentsAnimationRunning = running; 538 } 539 isAnimateCurrentTaskDismissal()540 public boolean isAnimateCurrentTaskDismissal() { 541 return mAnimateCurrentTaskDismissal; 542 } 543 setAnimateCurrentTaskDismissal(boolean animateCurrentTaskDismissal)544 public void setAnimateCurrentTaskDismissal(boolean animateCurrentTaskDismissal) { 545 mAnimateCurrentTaskDismissal = animateCurrentTaskDismissal; 546 } 547 isDismissingFromSplitPair()548 public boolean isDismissingFromSplitPair() { 549 return mDismissingFromSplitPair; 550 } 551 setDismissingFromSplitPair(boolean dismissingFromSplitPair)552 public void setDismissingFromSplitPair(boolean dismissingFromSplitPair) { 553 mDismissingFromSplitPair = dismissingFromSplitPair; 554 } 555 getSplitAnimationController()556 public SplitAnimationController getSplitAnimationController() { 557 return mSplitAnimationController; 558 } 559 560 /** 561 * Requires Shell Transitions 562 */ 563 private class RemoteSplitLaunchTransitionRunner extends IRemoteTransition.Stub { 564 565 private final int mInitialTaskId; 566 private final int mSecondTaskId; 567 private final Consumer<Boolean> mSuccessCallback; 568 RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId, @Nullable Consumer<Boolean> callback)569 RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId, 570 @Nullable Consumer<Boolean> callback) { 571 mInitialTaskId = initialTaskId; 572 mSecondTaskId = secondTaskId; 573 mSuccessCallback = callback; 574 } 575 576 @Override startAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishedCallback)577 public void startAnimation(IBinder transition, TransitionInfo info, 578 SurfaceControl.Transaction t, 579 IRemoteTransitionFinishedCallback finishedCallback) { 580 testLogD(LAUNCH_SPLIT_PAIR, "Received split startAnimation"); 581 final Runnable finishAdapter = () -> { 582 try { 583 finishedCallback.onTransitionFinished(null /* wct */, null /* sct */); 584 } catch (RemoteException e) { 585 Log.e(TAG, "Failed to call transition finished callback", e); 586 } 587 }; 588 589 MAIN_EXECUTOR.execute(() -> { 590 TaskViewUtils.composeRecentsSplitLaunchAnimator(mLaunchingTaskView, mStateManager, 591 mDepthController, mInitialTaskId, mSecondTaskId, info, t, () -> { 592 finishAdapter.run(); 593 if (mSuccessCallback != null) { 594 mSuccessCallback.accept(true); 595 } 596 resetState(); 597 }); 598 }); 599 } 600 601 @Override mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, IRemoteTransitionFinishedCallback finishedCallback)602 public void mergeAnimation(IBinder transition, TransitionInfo info, 603 SurfaceControl.Transaction t, IBinder mergeTarget, 604 IRemoteTransitionFinishedCallback finishedCallback) { } 605 } 606 607 /** 608 * LEGACY 609 * Remote animation runner for animation to launch an app. 610 */ 611 private class RemoteSplitLaunchAnimationRunner extends RemoteAnimationRunnerCompat { 612 613 private final int mInitialTaskId; 614 private final int mSecondTaskId; 615 private final Consumer<Boolean> mSuccessCallback; 616 RemoteSplitLaunchAnimationRunner(int initialTaskId, int secondTaskId, @Nullable Consumer<Boolean> successCallback)617 RemoteSplitLaunchAnimationRunner(int initialTaskId, int secondTaskId, 618 @Nullable Consumer<Boolean> successCallback) { 619 mInitialTaskId = initialTaskId; 620 mSecondTaskId = secondTaskId; 621 mSuccessCallback = successCallback; 622 } 623 624 @Override onAnimationStart(int transit, RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, Runnable finishedCallback)625 public void onAnimationStart(int transit, RemoteAnimationTarget[] apps, 626 RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, 627 Runnable finishedCallback) { 628 postAsyncCallback(mHandler, 629 () -> TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy( 630 mLaunchingTaskView, mInitialTaskId, mSecondTaskId, apps, wallpapers, 631 nonApps, mStateManager, mDepthController, () -> { 632 finishedCallback.run(); 633 if (mSuccessCallback != null) { 634 mSuccessCallback.accept(true); 635 } 636 resetState(); 637 })); 638 } 639 640 @Override onAnimationCancelled()641 public void onAnimationCancelled() { 642 postAsyncCallback(mHandler, () -> { 643 if (mSuccessCallback != null) { 644 // Launching legacy tasks while recents animation is running will always cause 645 // onAnimationCancelled to be called (should be fixed w/ shell transitions?) 646 mSuccessCallback.accept(mRecentsAnimationRunning); 647 } 648 resetState(); 649 }); 650 } 651 } 652 653 /** 654 * To be called whenever we exit split selection state. If 655 * {@link FeatureFlags#ENABLE_SPLIT_FROM_WORKSPACE_TO_WORKSPACE} is set, this should be the 656 * central way split is getting reset, which should then go through the callbacks to reset 657 * other state. 658 */ resetState()659 public void resetState() { 660 mSplitSelectDataHolder.resetState(); 661 dispatchOnSplitSelectionExit(); 662 mRecentsAnimationRunning = false; 663 mLaunchingTaskView = null; 664 mAnimateCurrentTaskDismissal = false; 665 mDismissingFromSplitPair = false; 666 mFirstFloatingTaskView = null; 667 mSplitInstructionsView = null; 668 } 669 670 /** 671 * @return {@code true} if first task has been selected and waiting for the second task to be 672 * chosen 673 */ isSplitSelectActive()674 public boolean isSplitSelectActive() { 675 return mSplitSelectDataHolder.isSplitSelectActive(); 676 } 677 678 /** 679 * @return {@code true} if the first and second task have been chosen and split is waiting to 680 * be launched 681 */ isBothSplitAppsConfirmed()682 public boolean isBothSplitAppsConfirmed() { 683 return mSplitSelectDataHolder.isBothSplitAppsConfirmed(); 684 } 685 getInitialTaskId()686 public int getInitialTaskId() { 687 return mSplitSelectDataHolder.getInitialTaskId(); 688 } 689 getSecondTaskId()690 public int getSecondTaskId() { 691 return mSplitSelectDataHolder.getSecondTaskId(); 692 } 693 setFirstFloatingTaskView(FloatingTaskView floatingTaskView)694 public void setFirstFloatingTaskView(FloatingTaskView floatingTaskView) { 695 mFirstFloatingTaskView = floatingTaskView; 696 } 697 setSplitInstructionsView(SplitInstructionsView splitInstructionsView)698 public void setSplitInstructionsView(SplitInstructionsView splitInstructionsView) { 699 mSplitInstructionsView = splitInstructionsView; 700 } 701 702 @Nullable getFirstFloatingTaskView()703 public FloatingTaskView getFirstFloatingTaskView() { 704 return mFirstFloatingTaskView; 705 } 706 707 @Nullable getSplitInstructionsView()708 public SplitInstructionsView getSplitInstructionsView() { 709 return mSplitInstructionsView; 710 } 711 getAppPairsController()712 public AppPairsController getAppPairsController() { 713 return mAppPairsController; 714 } 715 dump(String prefix, PrintWriter writer)716 public void dump(String prefix, PrintWriter writer) { 717 if (mSplitSelectDataHolder != null) { 718 mSplitSelectDataHolder.dump(prefix, writer); 719 } 720 } 721 722 public class SplitFromDesktopController { 723 private static final String TAG = "SplitFromDesktopController"; 724 725 private final Launcher mLauncher; 726 private final OverviewComponentObserver mOverviewComponentObserver; 727 private final int mSplitPlaceholderSize; 728 private final int mSplitPlaceholderInset; 729 private ActivityManager.RunningTaskInfo mTaskInfo; 730 private ISplitSelectListener mSplitSelectListener; 731 private Drawable mAppIcon; 732 SplitFromDesktopController(Launcher launcher)733 public SplitFromDesktopController(Launcher launcher) { 734 mLauncher = launcher; 735 RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState( 736 launcher.getApplicationContext()); 737 mOverviewComponentObserver = 738 new OverviewComponentObserver(launcher.getApplicationContext(), deviceState); 739 mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize( 740 R.dimen.split_placeholder_size); 741 mSplitPlaceholderInset = mLauncher.getResources().getDimensionPixelSize( 742 R.dimen.split_placeholder_inset); 743 mSplitSelectListener = new ISplitSelectListener.Stub() { 744 @Override 745 public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo, 746 int splitPosition, Rect taskBounds) { 747 if (!ENABLE_SPLIT_FROM_DESKTOP_TO_WORKSPACE.get()) return false; 748 MAIN_EXECUTOR.execute(() -> enterSplitSelect(taskInfo, splitPosition, 749 taskBounds)); 750 return true; 751 } 752 }; 753 SystemUiProxy.INSTANCE.get(mLauncher).registerSplitSelectListener(mSplitSelectListener); 754 } 755 756 /** 757 * Enter split select from desktop mode. 758 * @param taskInfo the desktop task to move to split stage 759 * @param splitPosition the stage position used for this transition 760 * @param taskBounds the bounds of the task, used for {@link FloatingTaskView} animation 761 */ enterSplitSelect(ActivityManager.RunningTaskInfo taskInfo, int splitPosition, Rect taskBounds)762 public void enterSplitSelect(ActivityManager.RunningTaskInfo taskInfo, 763 int splitPosition, Rect taskBounds) { 764 mTaskInfo = taskInfo; 765 String packageName = mTaskInfo.realActivity.getPackageName(); 766 PackageManager pm = mLauncher.getApplicationContext().getPackageManager(); 767 IconProvider provider = new IconProvider(mLauncher.getApplicationContext()); 768 try { 769 mAppIcon = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity, 770 PackageManager.ComponentInfoFlags.of(0))); 771 } catch (PackageManager.NameNotFoundException e) { 772 Log.w(TAG, "Package not found: " + packageName, e); 773 } 774 RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks( 775 SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()), 776 false /* allowMinimizeSplitScreen */); 777 778 DesktopSplitRecentsAnimationListener listener = 779 new DesktopSplitRecentsAnimationListener(splitPosition, taskBounds); 780 781 MAIN_EXECUTOR.execute(() -> { 782 callbacks.addListener(listener); 783 UI_HELPER_EXECUTOR.execute( 784 // Transition from app to enter stage split in launcher with 785 // recents animation. 786 () -> ActivityManagerWrapper.getInstance().startRecentsActivity( 787 mOverviewComponentObserver.getOverviewIntent(), 788 SystemClock.uptimeMillis(), callbacks, null, null)); 789 }); 790 } 791 792 private class DesktopSplitRecentsAnimationListener implements 793 RecentsAnimationCallbacks.RecentsAnimationListener { 794 private final Rect mTempRect = new Rect(); 795 private final RectF mTaskBounds = new RectF(); 796 private final int mSplitPosition; 797 DesktopSplitRecentsAnimationListener(int splitPosition, Rect taskBounds)798 DesktopSplitRecentsAnimationListener(int splitPosition, Rect taskBounds) { 799 mSplitPosition = splitPosition; 800 mTaskBounds.set(taskBounds); 801 } 802 803 @Override onRecentsAnimationStart(RecentsAnimationController controller, RecentsAnimationTargets targets)804 public void onRecentsAnimationStart(RecentsAnimationController controller, 805 RecentsAnimationTargets targets) { 806 StatsLogManager.LauncherEvent launcherDesktopSplitEvent = 807 mSplitPosition == STAGE_POSITION_BOTTOM_OR_RIGHT ? 808 LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM : 809 LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP; 810 setInitialTaskSelect(mTaskInfo, mSplitPosition, 811 null, launcherDesktopSplitEvent); 812 813 RecentsView recentsView = mLauncher.getOverviewPanel(); 814 recentsView.getPagedOrientationHandler().getInitialSplitPlaceholderBounds( 815 mSplitPlaceholderSize, mSplitPlaceholderInset, 816 mLauncher.getDeviceProfile(), getActiveSplitStagePosition(), mTempRect); 817 818 PendingAnimation anim = new PendingAnimation( 819 SplitAnimationTimings.TABLET_HOME_TO_SPLIT.getDuration()); 820 final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView( 821 mLauncher, mLauncher.getDragLayer(), 822 null /* thumbnail */, 823 mAppIcon, new RectF()); 824 floatingTaskView.setAlpha(1); 825 floatingTaskView.addStagingAnimation(anim, mTaskBounds, mTempRect, 826 false /* fadeWithThumbnail */, true /* isStagedTask */); 827 setFirstFloatingTaskView(floatingTaskView); 828 829 anim.addListener(new AnimatorListenerAdapter() { 830 @Override 831 public void onAnimationStart(Animator animation) { 832 controller.finish(true /* toRecents */, null /* onFinishComplete */, 833 false /* sendUserLeaveHint */); 834 } 835 @Override 836 public void onAnimationEnd(Animator animation) { 837 SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()) 838 .onDesktopSplitSelectAnimComplete(mTaskInfo); 839 } 840 }); 841 anim.buildAnim().start(); 842 } 843 } 844 } 845 } 846