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.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP; 20 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM; 21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTED_SECOND_APP; 22 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_COMPLETE; 23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_EXIT_HOME; 24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SPLIT_SELECTION_INITIATED; 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.STAGE_POSITION_BOTTOM_OR_RIGHT; 28 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_PENDINGINTENT; 29 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_TASK; 30 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SHORTCUT_TASK; 31 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SINGLE_INTENT_FULLSCREEN; 32 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SINGLE_SHORTCUT_FULLSCREEN; 33 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SINGLE_TASK_FULLSCREEN; 34 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_PENDINGINTENT; 35 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_SHORTCUT; 36 import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_TASK; 37 import static com.android.wm.shell.shared.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT; 38 import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; 39 40 import android.animation.Animator; 41 import android.animation.AnimatorListenerAdapter; 42 import android.annotation.NonNull; 43 import android.annotation.UiThread; 44 import android.app.ActivityManager; 45 import android.app.ActivityOptions; 46 import android.app.ActivityThread; 47 import android.app.PendingIntent; 48 import android.content.Intent; 49 import android.content.pm.PackageManager; 50 import android.content.pm.ShortcutInfo; 51 import android.graphics.Rect; 52 import android.graphics.RectF; 53 import android.graphics.drawable.Drawable; 54 import android.os.Bundle; 55 import android.os.IBinder; 56 import android.os.RemoteException; 57 import android.os.UserHandle; 58 import android.util.Log; 59 import android.util.Pair; 60 import android.view.SurfaceControl; 61 import android.view.View; 62 import android.window.IRemoteTransitionFinishedCallback; 63 import android.window.RemoteTransition; 64 import android.window.RemoteTransitionStub; 65 import android.window.TransitionInfo; 66 67 import androidx.annotation.Nullable; 68 import androidx.annotation.VisibleForTesting; 69 70 import com.android.internal.logging.InstanceId; 71 import com.android.launcher3.R; 72 import com.android.launcher3.anim.PendingAnimation; 73 import com.android.launcher3.apppairs.AppPairIcon; 74 import com.android.launcher3.icons.IconProvider; 75 import com.android.launcher3.logging.StatsLogManager; 76 import com.android.launcher3.model.data.ItemInfo; 77 import com.android.launcher3.statehandlers.DepthController; 78 import com.android.launcher3.statemanager.StateManager; 79 import com.android.launcher3.taskbar.LauncherTaskbarUIController; 80 import com.android.launcher3.testing.TestLogging; 81 import com.android.launcher3.testing.shared.TestProtocol; 82 import com.android.launcher3.uioverrides.QuickstepLauncher; 83 import com.android.launcher3.util.BackPressHandler; 84 import com.android.launcher3.util.ComponentKey; 85 import com.android.launcher3.util.SplitConfigurationOptions.StagePosition; 86 import com.android.quickstep.OverviewComponentObserver; 87 import com.android.quickstep.RecentsAnimationCallbacks; 88 import com.android.quickstep.RecentsAnimationController; 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.views.FloatingTaskView; 94 import com.android.quickstep.views.GroupedTaskView; 95 import com.android.quickstep.views.RecentsView; 96 import com.android.quickstep.views.RecentsViewContainer; 97 import com.android.quickstep.views.SplitInstructionsView; 98 import com.android.systemui.shared.recents.model.Task; 99 import com.android.systemui.shared.system.InteractionJankMonitorWrapper; 100 import com.android.systemui.shared.system.QuickStepContract; 101 import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; 102 import com.android.wm.shell.splitscreen.ISplitSelectListener; 103 104 import java.io.PrintWriter; 105 import java.util.ArrayList; 106 import java.util.Arrays; 107 import java.util.List; 108 import java.util.Optional; 109 import java.util.function.Consumer; 110 111 /** 112 * Represent data needed for the transient state when user has selected one app for split screen 113 * and is in the process of either a) selecting a second app or b) exiting intention to invoke split 114 */ 115 public class SplitSelectStateController { 116 private static final String TAG = "SplitSelectStateCtor"; 117 118 private RecentsViewContainer mContainer; 119 private final RecentsModel mRecentTasksModel; 120 @Nullable 121 private Runnable mActivityBackCallback; 122 private final SplitAnimationController mSplitAnimationController; 123 private final AppPairsController mAppPairsController; 124 private final SplitSelectDataHolder mSplitSelectDataHolder; 125 private final StatsLogManager mStatsLogManager; 126 private final SystemUiProxy mSystemUiProxy; 127 private final StateManager mStateManager; 128 @Nullable 129 private SplitFromDesktopController mSplitFromDesktopController; 130 @Nullable 131 private DepthController mDepthController; 132 private boolean mRecentsAnimationRunning; 133 /** If {@code true}, animates the existing task view split placeholder view */ 134 private boolean mAnimateCurrentTaskDismissal; 135 /** 136 * Acts as a subset of {@link #mAnimateCurrentTaskDismissal}, we can't be dismissing from a 137 * split pair task view without wanting to animate current task dismissal overall 138 */ 139 private boolean mDismissingFromSplitPair; 140 /** If not null, this is the TaskView we want to launch from */ 141 @Nullable 142 private GroupedTaskView mLaunchingTaskView; 143 /** If not null, this is the icon we want to launch from */ 144 private AppPairIcon mLaunchingIconView; 145 146 /** True when the first selected split app is being launched in fullscreen. */ 147 private boolean mLaunchingFirstAppFullscreen; 148 149 /** 150 * Should be a constant from {@link com.android.internal.jank.Cuj} or -1, does not need to be 151 * set for all launches. Used in conjunction with {@link #mLaunchingViewCuj} below. 152 */ 153 private int mLaunchCuj = -1; 154 private View mLaunchingViewCuj; 155 156 private FloatingTaskView mFirstFloatingTaskView; 157 private SplitInstructionsView mSplitInstructionsView; 158 159 private final List<SplitSelectionListener> mSplitSelectionListeners = new ArrayList<>(); 160 /** 161 * Tracks metrics from when first app is selected to split launch or cancellation. This also 162 * gets passed over to shell when attempting to invoke split. 163 */ 164 private Pair<InstanceId, com.android.launcher3.logging.InstanceId> mSessionInstanceIds; 165 166 private boolean mIsDestroyed = false; 167 168 private final BackPressHandler mSplitBackHandler = new BackPressHandler() { 169 @Override 170 public boolean canHandleBack() { 171 return isSplitSelectActive(); 172 } 173 174 @Override 175 public void onBackInvoked() { 176 // When exiting from split selection, leave current context to go to 177 // homescreen as well 178 getSplitAnimationController().playPlaceholderDismissAnim(mContainer, 179 LAUNCHER_SPLIT_SELECTION_EXIT_HOME); 180 if (mActivityBackCallback != null) { 181 mActivityBackCallback.run(); 182 } 183 } 184 }; 185 SplitSelectStateController(RecentsViewContainer container, StateManager stateManager, DepthController depthController, StatsLogManager statsLogManager, SystemUiProxy systemUiProxy, RecentsModel recentsModel, Runnable activityBackCallback)186 public SplitSelectStateController(RecentsViewContainer container, 187 StateManager stateManager, DepthController depthController, 188 StatsLogManager statsLogManager, SystemUiProxy systemUiProxy, RecentsModel recentsModel, 189 Runnable activityBackCallback) { 190 mContainer = container; 191 mStatsLogManager = statsLogManager; 192 mSystemUiProxy = systemUiProxy; 193 mStateManager = stateManager; 194 mDepthController = depthController; 195 mRecentTasksModel = recentsModel; 196 mActivityBackCallback = activityBackCallback; 197 mSplitAnimationController = new SplitAnimationController(this); 198 mAppPairsController = new AppPairsController(mContainer, this, statsLogManager); 199 mSplitSelectDataHolder = new SplitSelectDataHolder(mContainer.asContext()); 200 } 201 onDestroy()202 public void onDestroy() { 203 mContainer = null; 204 mIsDestroyed = true; 205 mActivityBackCallback = null; 206 mAppPairsController.onDestroy(); 207 mSplitSelectDataHolder.onDestroy(); 208 if (mSplitFromDesktopController != null) { 209 mSplitFromDesktopController.onDestroy(); 210 } 211 } 212 213 /** 214 * @param alreadyRunningTask if set to {@link android.app.ActivityTaskManager#INVALID_TASK_ID} 215 * then @param intent will be used to launch the initial task 216 * @param intent will be ignored if @param alreadyRunningTask is set 217 */ setInitialTaskSelect(@ullable Intent intent, @StagePosition int stagePosition, @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent, int alreadyRunningTask)218 public void setInitialTaskSelect(@Nullable Intent intent, @StagePosition int stagePosition, 219 @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent, 220 int alreadyRunningTask) { 221 mSplitSelectDataHolder.setInitialTaskSelect(intent, stagePosition, itemInfo, splitEvent, 222 alreadyRunningTask); 223 createAndLogInstanceIdsForSession(); 224 } 225 226 /** 227 * To be called after first task selected from using a split shortcut from the fullscreen 228 * running app. 229 */ setInitialTaskSelect(ActivityManager.RunningTaskInfo info, @StagePosition int stagePosition, @NonNull ItemInfo itemInfo, StatsLogManager.EventEnum splitEvent)230 public void setInitialTaskSelect(ActivityManager.RunningTaskInfo info, 231 @StagePosition int stagePosition, @NonNull ItemInfo itemInfo, 232 StatsLogManager.EventEnum splitEvent) { 233 mSplitSelectDataHolder.setInitialTaskSelect(info, stagePosition, itemInfo, splitEvent); 234 createAndLogInstanceIdsForSession(); 235 } 236 237 /** 238 * Given a list of task keys, searches through active Tasks in RecentsModel to find the last 239 * active instances of these tasks. Returns an empty array if there is no such running task. 240 * 241 * @param componentKeys The list of ComponentKeys to search for. 242 * @param callback The callback that will be executed on the list of found tasks. 243 * @param findExactPairMatch If {@code true}, only finds tasks that contain BOTH of the wanted 244 * tasks (i.e. searching for a running pair of tasks.) 245 */ findLastActiveTasksAndRunCallback(@ullable List<ComponentKey> componentKeys, boolean findExactPairMatch, Consumer<Task[]> callback)246 public void findLastActiveTasksAndRunCallback(@Nullable List<ComponentKey> componentKeys, 247 boolean findExactPairMatch, Consumer<Task[]> callback) { 248 mRecentTasksModel.getTasks(taskGroups -> { 249 if (componentKeys == null || componentKeys.isEmpty()) { 250 callback.accept(new Task[]{}); 251 return; 252 } 253 254 Task[] lastActiveTasks = new Task[componentKeys.size()]; 255 256 if (findExactPairMatch) { 257 // Loop through tasks in reverse, since they are ordered with most-recent tasks last 258 for (int i = taskGroups.size() - 1; i >= 0; i--) { 259 GroupTask groupTask = taskGroups.get(i); 260 if (isInstanceOfAppPair( 261 groupTask, componentKeys.get(0), componentKeys.get(1))) { 262 lastActiveTasks[0] = ((SplitTask) groupTask).getTopLeftTask(); 263 break; 264 } 265 } 266 } else { 267 // For each key we are looking for, add to lastActiveTasks with the corresponding 268 // Task (or do nothing if not found). 269 for (int i = 0; i < componentKeys.size(); i++) { 270 ComponentKey key = componentKeys.get(i); 271 Task lastActiveTask = null; 272 // Loop through tasks in reverse, since they are ordered with recent tasks last 273 for (int j = taskGroups.size() - 1; j >= 0; j--) { 274 GroupTask groupTask = taskGroups.get(j); 275 // Account for desktop cases where there can be N tasks in the group 276 for (Task task : groupTask.getTasks()) { 277 if (isInstanceOfComponent(task, key) 278 && !Arrays.asList(lastActiveTasks).contains(task)) { 279 lastActiveTask = task; 280 break; 281 } 282 } 283 if (lastActiveTask != null) { 284 break; 285 } 286 } 287 288 lastActiveTasks[i] = lastActiveTask; 289 } 290 } 291 292 callback.accept(lastActiveTasks); 293 }); 294 } 295 296 /** 297 * Checks if a given Task is the most recently-active Task of type componentName. Used for 298 * selecting already-running Tasks for splitscreen. 299 */ isInstanceOfComponent(@ullable Task task, @NonNull ComponentKey componentKey)300 public boolean isInstanceOfComponent(@Nullable Task task, @NonNull ComponentKey componentKey) { 301 // Exclude the task that is already staged 302 if (task == null || task.key.id == mSplitSelectDataHolder.getInitialTaskId()) { 303 return false; 304 } 305 306 return task.key.baseIntent.getComponent().equals(componentKey.componentName) 307 && task.key.userId == componentKey.user.getIdentifier(); 308 } 309 310 /** 311 * Checks if a given GroupTask is a pair of apps that matches two given ComponentKeys. We check 312 * both permutations because task order is not guaranteed in GroupTasks. 313 */ isInstanceOfAppPair(GroupTask groupTask, @NonNull ComponentKey componentKey1, @NonNull ComponentKey componentKey2)314 public boolean isInstanceOfAppPair(GroupTask groupTask, @NonNull ComponentKey componentKey1, 315 @NonNull ComponentKey componentKey2) { 316 if (groupTask instanceof SplitTask splitTask) { 317 return ((isInstanceOfComponent(splitTask.getTopLeftTask(), componentKey1) 318 && isInstanceOfComponent(splitTask.getBottomRightTask(), componentKey2)) 319 || 320 (isInstanceOfComponent(splitTask.getTopLeftTask(), componentKey2) 321 && isInstanceOfComponent(splitTask.getBottomRightTask(), 322 componentKey1))); 323 } 324 return false; 325 } 326 327 /** 328 * Listener will only get callbacks going forward from the point of registration. No 329 * methods will be fired upon registering. 330 */ registerSplitListener(@onNull SplitSelectionListener listener)331 public void registerSplitListener(@NonNull SplitSelectionListener listener) { 332 if (mSplitSelectionListeners.contains(listener)) { 333 return; 334 } 335 mSplitSelectionListeners.add(listener); 336 } 337 unregisterSplitListener(@onNull SplitSelectionListener listener)338 public void unregisterSplitListener(@NonNull SplitSelectionListener listener) { 339 mSplitSelectionListeners.remove(listener); 340 } 341 dispatchOnSplitSelectionExit()342 private void dispatchOnSplitSelectionExit() { 343 for (SplitSelectionListener listener : mSplitSelectionListeners) { 344 listener.onSplitSelectionExit(false); 345 } 346 } 347 348 /** 349 * To be called when the both split tasks are ready to be launched. Call after launcher side 350 * animations are complete. 351 */ launchSplitTasks(@ersistentSnapPosition int snapPosition, @Nullable Consumer<Boolean> callback)352 public void launchSplitTasks(@PersistentSnapPosition int snapPosition, 353 @Nullable Consumer<Boolean> callback) { 354 launchTasks(callback, false /* freezeTaskList */, snapPosition, mSessionInstanceIds.first); 355 356 mStatsLogManager.logger() 357 .withItemInfo(mSplitSelectDataHolder.getSecondItemInfo()) 358 .withInstanceId(mSessionInstanceIds.second) 359 .log(LAUNCHER_SPLIT_SELECTED_SECOND_APP); 360 } 361 362 /** 363 * A version of {@link #launchTasks(Consumer, boolean, int, InstanceId)} with no success 364 * callback. 365 */ launchSplitTasks(@ersistentSnapPosition int snapPosition)366 public void launchSplitTasks(@PersistentSnapPosition int snapPosition) { 367 launchSplitTasks(snapPosition, /* callback */ null); 368 } 369 370 /** 371 * A version of {@link #launchSplitTasks(int, Consumer)} that launches with default split ratio. 372 */ launchSplitTasks(@ullable Consumer<Boolean> callback)373 public void launchSplitTasks(@Nullable Consumer<Boolean> callback) { 374 launchSplitTasks(SNAP_TO_2_50_50, callback); 375 } 376 377 /** 378 * A version of {@link #launchSplitTasks(int, Consumer)} that launches with a default split 379 * ratio and no callback. 380 */ launchSplitTasks()381 public void launchSplitTasks() { 382 launchSplitTasks(SNAP_TO_2_50_50, null); 383 } 384 385 /** 386 * Use to log an event when user exists split selection when the second app **IS NOT** selected. 387 * This must be called before playing any exit animations since most animations will call 388 * {@link #resetState()} which removes {@link #mSessionInstanceIds}. 389 */ logExitReason(StatsLogManager.EventEnum splitExitEvent)390 public void logExitReason(StatsLogManager.EventEnum splitExitEvent) { 391 StatsLogManager.StatsLogger logger = mStatsLogManager.logger(); 392 if (mSessionInstanceIds != null) { 393 logger.withInstanceId(mSessionInstanceIds.second); 394 } else { 395 Log.w(TAG, "Missing session instanceIds"); 396 } 397 logger.log(splitExitEvent); 398 } 399 400 /** 401 * To be called as soon as user selects the second task (even if animations aren't complete) 402 * @param task The second task that will be launched. 403 */ setSecondTask(Task task, ItemInfo itemInfo)404 public void setSecondTask(Task task, ItemInfo itemInfo) { 405 mSplitSelectDataHolder.setSecondTask(task.key.id, itemInfo); 406 } 407 408 /** 409 * To be called as soon as user selects the second app (even if animations aren't complete) 410 * @param intent The second intent that will be launched. 411 * @param user The user of that intent. 412 */ setSecondTask(Intent intent, UserHandle user, ItemInfo itemInfo)413 public void setSecondTask(Intent intent, UserHandle user, ItemInfo itemInfo) { 414 mSplitSelectDataHolder.setSecondTask(intent, user, itemInfo); 415 } 416 417 /** 418 * To be called as soon as user selects the second app (even if animations aren't complete) 419 * @param pendingIntent The second PendingIntent that will be launched. 420 */ setSecondTask(PendingIntent pendingIntent, ItemInfo itemInfo)421 public void setSecondTask(PendingIntent pendingIntent, ItemInfo itemInfo) { 422 mSplitSelectDataHolder.setSecondTask(pendingIntent, itemInfo); 423 } 424 setSecondWidget(PendingIntent pendingIntent, Intent widgetIntent)425 public void setSecondWidget(PendingIntent pendingIntent, Intent widgetIntent) { 426 mSplitSelectDataHolder.setSecondWidget(pendingIntent, widgetIntent, null /*itemInfo*/); 427 } 428 429 /** 430 * To be called when we want to launch split pairs from Overview. Split can be initiated from 431 * either Overview or home, or all apps. Either both taskIds are set, or a pending intent + a 432 * fill in intent with a taskId2 are set. 433 * @param shellInstanceId loggingId to be used by shell, will be non-null for actions that 434 * create a split instance, null for cases that bring existing instaces to the 435 * foreground (quickswitch, launching previous pairs from overview) 436 */ launchTasks(@ullable Consumer<Boolean> callback, boolean freezeTaskList, @PersistentSnapPosition int snapPosition, @Nullable InstanceId shellInstanceId)437 public void launchTasks(@Nullable Consumer<Boolean> callback, boolean freezeTaskList, 438 @PersistentSnapPosition int snapPosition, @Nullable InstanceId shellInstanceId) { 439 TestLogging.recordEvent( 440 TestProtocol.SEQUENCE_MAIN, "launchSplitTasks"); 441 final ActivityOptions options1 = ActivityOptions.makeBasic(); 442 if (freezeTaskList) { 443 options1.setFreezeRecentTasksReordering(); 444 } 445 446 SplitSelectDataHolder.SplitLaunchData launchData = 447 mSplitSelectDataHolder.getSplitLaunchData(); 448 int firstTaskId = launchData.getInitialTaskId(); 449 int secondTaskId = launchData.getSecondTaskId(); 450 ShortcutInfo firstShortcut = launchData.getInitialShortcut(); 451 ShortcutInfo secondShortcut = launchData.getSecondShortcut(); 452 PendingIntent firstPI = launchData.getInitialPendingIntent(); 453 PendingIntent secondPI = launchData.getSecondPendingIntent(); 454 Intent widgetIntent = launchData.getWidgetSecondIntent(); 455 int firstUserId = launchData.getInitialUserId(); 456 int secondUserId = launchData.getSecondUserId(); 457 int initialStagePosition = launchData.getInitialStagePosition(); 458 Bundle optionsBundle = options1.toBundle(); 459 Bundle extrasBundle = new Bundle(1); 460 extrasBundle.putParcelable(KEY_EXTRA_WIDGET_INTENT, widgetIntent); 461 final RemoteTransition remoteTransition = getRemoteTransition(firstTaskId, 462 secondTaskId, callback, "LaunchSplitPair"); 463 switch (launchData.getSplitLaunchType()) { 464 case SPLIT_TASK_TASK -> 465 mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, 466 null /* options2 */, initialStagePosition, snapPosition, 467 remoteTransition, shellInstanceId); 468 469 case SPLIT_TASK_PENDINGINTENT -> 470 mSystemUiProxy.startIntentAndTask(secondPI, secondUserId, optionsBundle, 471 firstTaskId, extrasBundle, initialStagePosition, snapPosition, 472 remoteTransition, shellInstanceId); 473 474 case SPLIT_TASK_SHORTCUT -> 475 mSystemUiProxy.startShortcutAndTask(secondShortcut, optionsBundle, 476 firstTaskId, null /*options2*/, initialStagePosition, snapPosition, 477 remoteTransition, shellInstanceId); 478 479 case SPLIT_PENDINGINTENT_TASK -> 480 mSystemUiProxy.startIntentAndTask(firstPI, firstUserId, optionsBundle, 481 secondTaskId, null /*options2*/, initialStagePosition, snapPosition, 482 remoteTransition, shellInstanceId); 483 484 case SPLIT_PENDINGINTENT_PENDINGINTENT -> 485 mSystemUiProxy.startIntents(firstPI, firstUserId, firstShortcut, 486 optionsBundle, secondPI, secondUserId, secondShortcut, extrasBundle, 487 initialStagePosition, snapPosition, remoteTransition, 488 shellInstanceId); 489 490 case SPLIT_SHORTCUT_TASK -> 491 mSystemUiProxy.startShortcutAndTask(firstShortcut, optionsBundle, 492 secondTaskId, null /*options2*/, initialStagePosition, snapPosition, 493 remoteTransition, shellInstanceId); 494 } 495 496 } 497 498 /** 499 * Used to launch split screen from a split pair that already exists, optionally with a custom 500 * remote transition. 501 * <p> 502 * See {@link SplitSelectStateController#launchExistingSplitPair( 503 * GroupedTaskView, int, int, int, Consumer, boolean, int, RemoteTransition)} 504 */ launchExistingSplitPair(@ullable GroupedTaskView groupedTaskView, int firstTaskId, int secondTaskId, @StagePosition int stagePosition, Consumer<Boolean> callback, boolean freezeTaskList, @PersistentSnapPosition int snapPosition)505 public void launchExistingSplitPair(@Nullable GroupedTaskView groupedTaskView, 506 int firstTaskId, int secondTaskId, @StagePosition int stagePosition, 507 Consumer<Boolean> callback, boolean freezeTaskList, 508 @PersistentSnapPosition int snapPosition) { 509 launchExistingSplitPair( 510 groupedTaskView, 511 firstTaskId, 512 secondTaskId, 513 stagePosition, 514 callback, 515 freezeTaskList, 516 snapPosition, 517 /* remoteTransition= */ null); 518 } 519 520 521 /** 522 * Used to launch split screen from a split pair that already exists (usually accessible through 523 * Overview). This is different than {@link #launchTasks(Consumer, boolean, int, InstanceId)} 524 * in that this only launches split screen that are existing tasks. This doesn't determine which 525 * API should be used (i.e. launching split with existing tasks vs intents vs shortcuts, etc). 526 * 527 * <p/> 528 * NOTE: This is not to be used to launch AppPairs. 529 */ launchExistingSplitPair(@ullable GroupedTaskView groupedTaskView, int firstTaskId, int secondTaskId, @StagePosition int stagePosition, Consumer<Boolean> callback, boolean freezeTaskList, @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition)530 public void launchExistingSplitPair(@Nullable GroupedTaskView groupedTaskView, 531 int firstTaskId, int secondTaskId, @StagePosition int stagePosition, 532 Consumer<Boolean> callback, boolean freezeTaskList, 533 @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition) { 534 mLaunchingTaskView = groupedTaskView; 535 final ActivityOptions options1 = ActivityOptions.makeBasic(); 536 if (freezeTaskList) { 537 options1.setFreezeRecentTasksReordering(); 538 } 539 Bundle optionsBundle = options1.toBundle(); 540 541 final RemoteTransition transition = remoteTransition == null 542 ? getRemoteTransition( 543 firstTaskId, secondTaskId, callback, "LaunchExistingPair") 544 : remoteTransition; 545 mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, null /* options2 */, 546 stagePosition, snapPosition, transition, null /*shellInstanceId*/); 547 548 } 549 550 /** 551 * Launches the initially selected task/intent in fullscreen (note the same SystemUi APIs are 552 * used as {@link #launchSplitTasks(int, Consumer)} because they are overloaded to launch both 553 * split and fullscreen tasks) 554 */ launchInitialAppFullscreen(Consumer<Boolean> callback)555 public void launchInitialAppFullscreen(Consumer<Boolean> callback) { 556 final ActivityOptions options1 = ActivityOptions.makeBasic(); 557 SplitSelectDataHolder.SplitLaunchData launchData = 558 mSplitSelectDataHolder.getFullscreenLaunchData(); 559 int firstTaskId = launchData.getInitialTaskId(); 560 int secondTaskId = launchData.getSecondTaskId(); 561 PendingIntent firstPI = launchData.getInitialPendingIntent(); 562 int firstUserId = launchData.getInitialUserId(); 563 int initialStagePosition = launchData.getInitialStagePosition(); 564 ShortcutInfo initialShortcut = launchData.getInitialShortcut(); 565 Bundle optionsBundle = options1.toBundle(); 566 567 final RemoteSplitLaunchTransitionRunner animationRunner = 568 new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback); 569 final RemoteTransition remoteTransition = new RemoteTransition(animationRunner, 570 ActivityThread.currentActivityThread().getApplicationThread(), 571 "LaunchAppFullscreen"); 572 InstanceId instanceId = mSessionInstanceIds.first; 573 switch (launchData.getSplitLaunchType()) { 574 case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId, 575 optionsBundle, secondTaskId, null /* options2 */, initialStagePosition, 576 SNAP_TO_2_50_50, remoteTransition, instanceId); 577 case SPLIT_SINGLE_INTENT_FULLSCREEN -> mSystemUiProxy.startIntentAndTask(firstPI, 578 firstUserId, optionsBundle, secondTaskId, null /*options2*/, 579 initialStagePosition, SNAP_TO_2_50_50, remoteTransition, instanceId); 580 case SPLIT_SINGLE_SHORTCUT_FULLSCREEN -> mSystemUiProxy.startShortcutAndTask( 581 initialShortcut, optionsBundle, firstTaskId, null /* options2 */, 582 initialStagePosition, SNAP_TO_2_50_50, remoteTransition, instanceId); 583 } 584 } 585 586 /** 587 * Init {@code SplitFromDesktopController} 588 */ initSplitFromDesktopController(QuickstepLauncher launcher)589 public void initSplitFromDesktopController(QuickstepLauncher launcher) { 590 initSplitFromDesktopController(new SplitFromDesktopController(launcher)); 591 } 592 593 @VisibleForTesting initSplitFromDesktopController(SplitFromDesktopController controller)594 void initSplitFromDesktopController(SplitFromDesktopController controller) { 595 mSplitFromDesktopController = controller; 596 } 597 getRemoteTransition(int firstTaskId, int secondTaskId, @Nullable Consumer<Boolean> callback, String transitionName)598 private RemoteTransition getRemoteTransition(int firstTaskId, int secondTaskId, 599 @Nullable Consumer<Boolean> callback, String transitionName) { 600 final RemoteSplitLaunchTransitionRunner animationRunner = 601 new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback); 602 return new RemoteTransition(animationRunner, 603 ActivityThread.currentActivityThread().getApplicationThread(), transitionName); 604 } 605 606 /** 607 * Will initialize {@link #mSessionInstanceIds} if null and log the first split event from 608 * {@link #mSplitSelectDataHolder} 609 */ createAndLogInstanceIdsForSession()610 private void createAndLogInstanceIdsForSession() { 611 if (mSessionInstanceIds != null) { 612 Log.w(TAG, "SessionIds should be null"); 613 } 614 // Log separately the start of the session and then the first app selected 615 mSessionInstanceIds = LogUtils.getShellShareableInstanceId(); 616 mStatsLogManager.logger() 617 .withInstanceId(mSessionInstanceIds.second) 618 .log(LAUNCHER_SPLIT_SELECTION_INITIATED); 619 620 mStatsLogManager.logger() 621 .withItemInfo(mSplitSelectDataHolder.getItemInfo()) 622 .withInstanceId(mSessionInstanceIds.second) 623 .log(mSplitSelectDataHolder.getSplitEvent()); 624 } 625 getActiveSplitStagePosition()626 public @StagePosition int getActiveSplitStagePosition() { 627 return mSplitSelectDataHolder.getInitialStagePosition(); 628 } 629 getSplitEvent()630 public StatsLogManager.EventEnum getSplitEvent() { 631 return mSplitSelectDataHolder.getSplitEvent(); 632 } 633 setRecentsAnimationRunning(boolean running)634 public void setRecentsAnimationRunning(boolean running) { 635 mRecentsAnimationRunning = running; 636 } 637 isAnimateCurrentTaskDismissal()638 public boolean isAnimateCurrentTaskDismissal() { 639 return mAnimateCurrentTaskDismissal; 640 } 641 setAnimateCurrentTaskDismissal(boolean animateCurrentTaskDismissal)642 public void setAnimateCurrentTaskDismissal(boolean animateCurrentTaskDismissal) { 643 mAnimateCurrentTaskDismissal = animateCurrentTaskDismissal; 644 } 645 isDismissingFromSplitPair()646 public boolean isDismissingFromSplitPair() { 647 return mDismissingFromSplitPair; 648 } 649 setDismissingFromSplitPair(boolean dismissingFromSplitPair)650 public void setDismissingFromSplitPair(boolean dismissingFromSplitPair) { 651 mDismissingFromSplitPair = dismissingFromSplitPair; 652 } 653 getSplitAnimationController()654 public SplitAnimationController getSplitAnimationController() { 655 return mSplitAnimationController; 656 } 657 658 /** 659 * Set params to invoke a trace session for the given view and CUJ when we begin animating the 660 * split launch AFTER we get a response from Shell. 661 */ setLaunchingCuj(View launchingView, int launchCuj)662 public void setLaunchingCuj(View launchingView, int launchCuj) { 663 mLaunchingViewCuj = launchingView; 664 mLaunchCuj = launchCuj; 665 } 666 667 /** 668 * Requires Shell Transitions 669 */ 670 private class RemoteSplitLaunchTransitionRunner extends RemoteTransitionStub { 671 672 private final int mInitialTaskId; 673 private final int mSecondTaskId; 674 private Consumer<Boolean> mFinishCallback; 675 RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId, @Nullable Consumer<Boolean> callback)676 RemoteSplitLaunchTransitionRunner(int initialTaskId, int secondTaskId, 677 @Nullable Consumer<Boolean> callback) { 678 mInitialTaskId = initialTaskId; 679 mSecondTaskId = secondTaskId; 680 mFinishCallback = callback; 681 } 682 683 @Override startAnimation(IBinder transition, TransitionInfo transitionInfo, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishedCallback)684 public void startAnimation(IBinder transition, TransitionInfo transitionInfo, 685 SurfaceControl.Transaction t, 686 IRemoteTransitionFinishedCallback finishedCallback) { 687 final Runnable finishAdapter = () -> { 688 try { 689 finishedCallback.onTransitionFinished(null /* wct */, null /* sct */); 690 } catch (RemoteException e) { 691 Log.e(TAG, "Failed to call transition finished callback", e); 692 } 693 }; 694 695 MAIN_EXECUTOR.execute(() -> { 696 // Only animate from taskView if it's already visible 697 boolean shouldLaunchFromTaskView = mLaunchingTaskView != null 698 && mLaunchingTaskView.getRecentsView() != null 699 && mLaunchingTaskView.getRecentsView().isTaskViewVisible( 700 mLaunchingTaskView); 701 if (mLaunchingViewCuj != null && mLaunchCuj != -1) { 702 InteractionJankMonitorWrapper.begin(mLaunchingViewCuj, mLaunchCuj); 703 } 704 mSplitAnimationController.playSplitLaunchAnimation( 705 shouldLaunchFromTaskView ? mLaunchingTaskView : null, 706 mLaunchingIconView, 707 mInitialTaskId, 708 mSecondTaskId, 709 null /* apps */, 710 null /* wallpapers */, 711 null /* nonApps */, 712 mStateManager, 713 mDepthController, 714 transitionInfo, t, () -> { 715 finishAdapter.run(); 716 cleanup(true /*success*/); 717 }, 718 QuickStepContract.getWindowCornerRadius(mContainer.asContext())); 719 }); 720 } 721 722 @Override onTransitionConsumed(IBinder transition, boolean aborted)723 public void onTransitionConsumed(IBinder transition, boolean aborted) 724 throws RemoteException { 725 MAIN_EXECUTOR.execute(() -> { 726 cleanup(false /*success*/); 727 }); 728 } 729 730 /** 731 * Must be called on UI thread. 732 * @param success if launching the split apps occurred successfully or not 733 */ 734 @UiThread cleanup(boolean success)735 private void cleanup(boolean success) { 736 if (mFinishCallback != null) { 737 mFinishCallback.accept(success); 738 mFinishCallback = null; 739 } 740 resetState(); 741 } 742 } 743 744 /** 745 * To be called whenever we exit split selection state. This should be the 746 * central way split is getting reset, which should then go through the callbacks to reset 747 * other state. 748 */ resetState()749 public void resetState() { 750 mSplitSelectDataHolder.resetState(); 751 if (!mIsDestroyed) { 752 mContainer.<RecentsView>getOverviewPanel().resetDesktopTaskFromSplitSelectState(); 753 } 754 dispatchOnSplitSelectionExit(); 755 mRecentsAnimationRunning = false; 756 mLaunchingTaskView = null; 757 mLaunchingIconView = null; 758 mAnimateCurrentTaskDismissal = false; 759 mDismissingFromSplitPair = false; 760 mFirstFloatingTaskView = null; 761 mSplitInstructionsView = null; 762 mLaunchingFirstAppFullscreen = false; 763 764 if (mLaunchCuj != -1) { 765 InteractionJankMonitorWrapper.end(mLaunchCuj); 766 } 767 mLaunchCuj = -1; 768 mLaunchingViewCuj = null; 769 770 if (mSessionInstanceIds != null) { 771 mStatsLogManager.logger() 772 .withInstanceId(mSessionInstanceIds.second) 773 .log(LAUNCHER_SPLIT_SELECTION_COMPLETE); 774 } 775 mSessionInstanceIds = null; 776 } 777 778 /** 779 * @return {@code true} if first task has been selected and waiting for the second task to be 780 * chosen 781 */ isSplitSelectActive()782 public boolean isSplitSelectActive() { 783 return mSplitSelectDataHolder.isSplitSelectActive(); 784 } 785 786 /** 787 * @return {@code true} if the first and second task have been chosen and split is waiting to 788 * be launched 789 */ isBothSplitAppsConfirmed()790 public boolean isBothSplitAppsConfirmed() { 791 return mSplitSelectDataHolder.isBothSplitAppsConfirmed(); 792 } 793 isLaunchingFirstAppFullscreen()794 public boolean isLaunchingFirstAppFullscreen() { 795 return mLaunchingFirstAppFullscreen; 796 } 797 getInitialTaskId()798 public int getInitialTaskId() { 799 return mSplitSelectDataHolder.getInitialTaskId(); 800 } 801 getSecondTaskId()802 public int getSecondTaskId() { 803 return mSplitSelectDataHolder.getSecondTaskId(); 804 } 805 setLaunchingFirstAppFullscreen()806 public void setLaunchingFirstAppFullscreen() { 807 mLaunchingFirstAppFullscreen = true; 808 } setFirstFloatingTaskView(FloatingTaskView floatingTaskView)809 public void setFirstFloatingTaskView(FloatingTaskView floatingTaskView) { 810 mFirstFloatingTaskView = floatingTaskView; 811 } 812 setSplitInstructionsView(SplitInstructionsView splitInstructionsView)813 public void setSplitInstructionsView(SplitInstructionsView splitInstructionsView) { 814 mSplitInstructionsView = splitInstructionsView; 815 } 816 817 @Nullable getFirstFloatingTaskView()818 public FloatingTaskView getFirstFloatingTaskView() { 819 return mFirstFloatingTaskView; 820 } 821 822 @Nullable getSplitInstructionsView()823 public SplitInstructionsView getSplitInstructionsView() { 824 return mSplitInstructionsView; 825 } 826 getAppPairsController()827 public AppPairsController getAppPairsController() { 828 return mAppPairsController; 829 } 830 setLaunchingIconView(AppPairIcon launchingIconView)831 public void setLaunchingIconView(AppPairIcon launchingIconView) { 832 mLaunchingIconView = launchingIconView; 833 } 834 getSplitBackHandler()835 public BackPressHandler getSplitBackHandler() { 836 return mSplitBackHandler; 837 } 838 dump(String prefix, PrintWriter writer)839 public void dump(String prefix, PrintWriter writer) { 840 if (mSplitSelectDataHolder != null) { 841 mSplitSelectDataHolder.dump(prefix, writer); 842 } 843 } 844 845 public class SplitFromDesktopController { 846 private static final String TAG = "SplitFromDesktopController"; 847 848 private final QuickstepLauncher mLauncher; 849 private final OverviewComponentObserver mOverviewComponentObserver; 850 private final int mSplitPlaceholderSize; 851 private final int mSplitPlaceholderInset; 852 private ActivityManager.RunningTaskInfo mTaskInfo; 853 private DesktopSplitSelectListenerImpl mSplitSelectListener; 854 private Drawable mAppIcon; 855 SplitFromDesktopController(QuickstepLauncher launcher)856 public SplitFromDesktopController(QuickstepLauncher launcher) { 857 mLauncher = launcher; 858 mOverviewComponentObserver = OverviewComponentObserver.INSTANCE.get(launcher); 859 mSplitPlaceholderSize = mLauncher.getResources().getDimensionPixelSize( 860 R.dimen.split_placeholder_size); 861 mSplitPlaceholderInset = mLauncher.getResources().getDimensionPixelSize( 862 R.dimen.split_placeholder_inset); 863 mSplitSelectListener = new DesktopSplitSelectListenerImpl(this); 864 SystemUiProxy.INSTANCE.get(mLauncher).registerSplitSelectListener(mSplitSelectListener); 865 } 866 onDestroy()867 void onDestroy() { 868 SystemUiProxy.INSTANCE.get(mLauncher).unregisterSplitSelectListener( 869 mSplitSelectListener); 870 mSplitSelectListener.release(); 871 mSplitSelectListener = null; 872 } 873 874 /** 875 * Enter split select from desktop mode. 876 * @param taskInfo the desktop task to move to split stage 877 * @param splitPosition the stage position used for this transition 878 * @param taskBounds the bounds of the task, used for {@link FloatingTaskView} animation 879 */ enterSplitSelect(ActivityManager.RunningTaskInfo taskInfo, int splitPosition, Rect taskBounds)880 public void enterSplitSelect(ActivityManager.RunningTaskInfo taskInfo, 881 int splitPosition, Rect taskBounds) { 882 mTaskInfo = taskInfo; 883 String packageName = mTaskInfo.realActivity.getPackageName(); 884 PackageManager pm = mLauncher.getApplicationContext().getPackageManager(); 885 IconProvider provider = new IconProvider(mLauncher.getApplicationContext()); 886 try { 887 mAppIcon = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity, 888 PackageManager.ComponentInfoFlags.of(0))); 889 } catch (PackageManager.NameNotFoundException e) { 890 Log.w(TAG, "Package not found: " + packageName, e); 891 } 892 RecentsAnimationCallbacks callbacks = new RecentsAnimationCallbacks( 893 SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext())); 894 895 DesktopSplitRecentsAnimationListener listener = 896 new DesktopSplitRecentsAnimationListener(splitPosition, taskBounds); 897 898 callbacks.addListener(listener); 899 UI_HELPER_EXECUTOR.execute(() -> { 900 // Transition from app to enter stage split in launcher with recents animation 901 final ActivityOptions options = ActivityOptions.makeBasic(); 902 options.setPendingIntentBackgroundActivityStartMode( 903 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); 904 options.setTransientLaunch(); 905 SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()) 906 .startRecentsActivity( 907 mOverviewComponentObserver.getOverviewIntent(), options, 908 callbacks, false /* useSyntheticRecentsTransition */); 909 }); 910 } 911 912 private class DesktopSplitRecentsAnimationListener implements 913 RecentsAnimationCallbacks.RecentsAnimationListener { 914 private final Rect mTempRect = new Rect(); 915 private final RectF mTaskBounds = new RectF(); 916 private final int mSplitPosition; 917 DesktopSplitRecentsAnimationListener(int splitPosition, Rect taskBounds)918 DesktopSplitRecentsAnimationListener(int splitPosition, Rect taskBounds) { 919 mSplitPosition = splitPosition; 920 mTaskBounds.set(taskBounds); 921 } 922 923 @Override onRecentsAnimationStart(RecentsAnimationController controller, RecentsAnimationTargets targets, TransitionInfo transitionInfo)924 public void onRecentsAnimationStart(RecentsAnimationController controller, 925 RecentsAnimationTargets targets, TransitionInfo transitionInfo) { 926 StatsLogManager.LauncherEvent launcherDesktopSplitEvent = 927 mSplitPosition == STAGE_POSITION_BOTTOM_OR_RIGHT ? 928 LAUNCHER_DESKTOP_MODE_SPLIT_RIGHT_BOTTOM : 929 LAUNCHER_DESKTOP_MODE_SPLIT_LEFT_TOP; 930 setInitialTaskSelect(mTaskInfo, mSplitPosition, 931 null, launcherDesktopSplitEvent); 932 933 RecentsView recentsView = mLauncher.getOverviewPanel(); 934 recentsView.getPagedOrientationHandler().getInitialSplitPlaceholderBounds( 935 mSplitPlaceholderSize, mSplitPlaceholderInset, 936 mLauncher.getDeviceProfile(), getActiveSplitStagePosition(), mTempRect); 937 938 PendingAnimation anim = new PendingAnimation( 939 SplitAnimationTimings.TABLET_HOME_TO_SPLIT.getDuration()); 940 final FloatingTaskView floatingTaskView = FloatingTaskView.getFloatingTaskView( 941 mLauncher, mLauncher.getDragLayer(), 942 null /* thumbnail */, 943 mAppIcon, new RectF()); 944 floatingTaskView.setOnClickListener(view -> 945 getSplitAnimationController() 946 .playAnimPlaceholderToFullscreen(mContainer, view, 947 Optional.of(() -> resetState()))); 948 floatingTaskView.setAlpha(1); 949 floatingTaskView.addStagingAnimation(anim, mTaskBounds, mTempRect, 950 false /* fadeWithThumbnail */, true /* isStagedTask */); 951 setFirstFloatingTaskView(floatingTaskView); 952 953 anim.addListener(new AnimatorListenerAdapter() { 954 @Override 955 public void onAnimationStart(Animator animation) { 956 controller.finish( 957 true /* toRecents */, 958 () -> { 959 LauncherTaskbarUIController controller = 960 mLauncher.getTaskbarUIController(); 961 if (controller != null) { 962 controller.updateTaskbarLauncherStateGoingHome(); 963 } 964 965 }, 966 false /* sendUserLeaveHint */); 967 } 968 @Override 969 public void onAnimationEnd(Animator animation) { 970 SystemUiProxy.INSTANCE.get(mLauncher.getApplicationContext()) 971 .onDesktopSplitSelectAnimComplete(mTaskInfo); 972 } 973 @Override 974 public void onAnimationCancel(Animator animation) { 975 mLauncher.getDragLayer().removeView(floatingTaskView); 976 getSplitAnimationController() 977 .removeSplitInstructionsView(mLauncher); 978 resetState(); 979 } 980 }); 981 anim.add(getSplitAnimationController() 982 .getShowSplitInstructionsAnim(mLauncher).buildAnim()); 983 anim.buildAnim().start(); 984 } 985 } 986 } 987 988 /** 989 * Wrapper for the ISplitSelectListener stub to prevent lingering references to the launcher 990 * activity via the controller. 991 */ 992 private static class DesktopSplitSelectListenerImpl extends ISplitSelectListener.Stub { 993 994 private SplitFromDesktopController mController; 995 DesktopSplitSelectListenerImpl(@onNull SplitFromDesktopController controller)996 DesktopSplitSelectListenerImpl(@NonNull SplitFromDesktopController controller) { 997 mController = controller; 998 } 999 1000 /** 1001 * Clears any references to the controller. 1002 */ release()1003 void release() { 1004 mController = null; 1005 } 1006 1007 @Override onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo, int splitPosition, Rect taskBounds)1008 public boolean onRequestSplitSelect(ActivityManager.RunningTaskInfo taskInfo, 1009 int splitPosition, Rect taskBounds) { 1010 MAIN_EXECUTOR.execute(() -> { 1011 if (mController != null) { 1012 mController.enterSplitSelect(taskInfo, splitPosition, taskBounds); 1013 } 1014 }); 1015 return true; 1016 } 1017 } 1018 } 1019