diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index 9db03f53a6..7e0530bcc0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -673,18 +673,6 @@ public class TaskbarActivityContext extends BaseTaskbarContext { return mIsFullscreen; } - /** - * Notify system to inset the rounded corner frame based on the task bar insets. - */ - public void updateInsetRoundedCornerFrame(boolean shouldInsetsRoundedCorner) { - if (!mDragLayer.isAttachedToWindow() - || mWindowLayoutParams.insetsRoundedCornerFrame == shouldInsetsRoundedCorner) { - return; - } - mWindowLayoutParams.insetsRoundedCornerFrame = shouldInsetsRoundedCorner; - mWindowManager.updateViewLayout(mDragLayer, mWindowLayoutParams); - } - /** * Updates the TaskbarContainer height (pass {@link #getDefaultTaskbarWindowHeight()} to reset). */ diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt index c029097b71..19b9a1868e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarInsetsController.kt @@ -71,7 +71,6 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas fun init(controllers: TaskbarControllers) { this.controllers = controllers windowLayoutParams = context.windowLayoutParams - windowLayoutParams.insetsRoundedCornerFrame = true onTaskbarWindowHeightOrInsetsChanged() context.addOnDeviceProfileChangeListener(deviceProfileChangeListener) @@ -86,23 +85,23 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas fun onTaskbarWindowHeightOrInsetsChanged() { if (context.isGestureNav) { windowLayoutParams.providedInsets = - arrayOf( - InsetsFrameProvider(insetsOwner, 0, navigationBars()) - .setFlags(FLAG_SUPPRESS_SCRIM, FLAG_SUPPRESS_SCRIM), - InsetsFrameProvider(insetsOwner, 0, tappableElement()), - InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()), - InsetsFrameProvider(insetsOwner, INDEX_LEFT, systemGestures()) - .setSource(SOURCE_DISPLAY), - InsetsFrameProvider(insetsOwner, INDEX_RIGHT, systemGestures()) - .setSource(SOURCE_DISPLAY) - ) + arrayOf( + InsetsFrameProvider(insetsOwner, 0, navigationBars()) + .setFlags(FLAG_SUPPRESS_SCRIM, FLAG_SUPPRESS_SCRIM), + InsetsFrameProvider(insetsOwner, 0, tappableElement()), + InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()), + InsetsFrameProvider(insetsOwner, INDEX_LEFT, systemGestures()) + .setSource(SOURCE_DISPLAY), + InsetsFrameProvider(insetsOwner, INDEX_RIGHT, systemGestures()) + .setSource(SOURCE_DISPLAY) + ) } else { windowLayoutParams.providedInsets = - arrayOf( - InsetsFrameProvider(insetsOwner, 0, navigationBars()), - InsetsFrameProvider(insetsOwner, 0, tappableElement()), - InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()) - ) + arrayOf( + InsetsFrameProvider(insetsOwner, 0, navigationBars()), + InsetsFrameProvider(insetsOwner, 0, tappableElement()), + InsetsFrameProvider(insetsOwner, 0, mandatorySystemGestures()) + ) } val touchableHeight = controllers.taskbarStashController.touchableHeight @@ -162,6 +161,11 @@ class TaskbarInsetsController(val context: TaskbarActivityContext) : LoggableTas provider.insetsSizeOverrides = insetsSizeOverride } } + + // We only report tappableElement height for unstashed, persistent taskbar, + // which is also when we draw the rounded corners above taskbar. + windowLayoutParams.insetsRoundedCornerFrame = tappableHeight > 0 + context.notifyUpdateLayoutParams() } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java index e334d0569d..b2f93787e0 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java @@ -106,9 +106,6 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba | FLAG_STASHED_IN_APP_IME | FLAG_STASHED_IN_TASKBAR_ALL_APPS | FLAG_STASHED_SMALL_SCREEN | FLAG_STASHED_IN_APP_AUTO; - private static final int FLAGS_STASHED_IN_APP_IGNORING_IME = - FLAGS_STASHED_IN_APP & ~FLAG_STASHED_IN_APP_IME; - // If any of these flags are enabled, inset apps by our stashed height instead of our unstashed // height. This way the reported insets are consistent even during transitions out of the app. // Currently any flag that causes us to stash in an app is included, except for IME or All Apps @@ -413,13 +410,6 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba return hasAnyFlag(FLAGS_STASHED_IN_APP); } - /** - * Returns whether the taskbar should be stashed in apps regardless of the IME visibility. - */ - public boolean isStashedInAppIgnoringIme() { - return hasAnyFlag(FLAGS_STASHED_IN_APP_IGNORING_IME); - } - /** * Returns whether the taskbar should be stashed in the current LauncherState. */ @@ -1059,11 +1049,6 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba private void notifyStashChange(boolean visible, boolean stashed) { mSystemUiProxy.notifyTaskbarStatus(visible, stashed); setUpTaskbarSystemAction(visible); - // If stashing taskbar is caused by IME visibility, we could just skip updating rounded - // corner insets since the rounded corners will be covered by IME during IME is showing and - // taskbar will be restored back to unstashed when IME is hidden. - mControllers.taskbarActivityContext.updateInsetRoundedCornerFrame( - visible && !isStashedInAppIgnoringIme()); mControllers.rotationButtonController.onTaskbarStateChange(visible, stashed); } diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index d67dbaec09..549c50b8f8 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -1317,5 +1317,8 @@ public class QuickstepLauncher extends Launcher { writer.println("\nQuickstepLauncher:"); writer.println(prefix + "\tmOrientationState: " + (recentsView == null ? "recentsNull" : recentsView.getPagedViewOrientedState())); + if (recentsView != null) { + recentsView.getSplitSelectController().dump(prefix, writer); + } } } diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index b7a29e0034..5333cbe919 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -177,7 +177,7 @@ public abstract class AbsSwipeUpHandler, protected @Nullable RecentsAnimationController mDeferredCleanupRecentsAnimationController; protected RecentsAnimationTargets mRecentsAnimationTargets; protected T mActivity; - protected Q mRecentsView; + protected @Nullable Q mRecentsView; protected Runnable mGestureEndCallback; protected MultiStateCallback mStateCallback; protected boolean mCanceled; @@ -1895,7 +1895,9 @@ public abstract class AbsSwipeUpHandler, private void invalidateHandlerWithLauncher() { endLauncherTransitionController(); - mRecentsView.onGestureAnimationEnd(); + if (mRecentsView != null) { + mRecentsView.onGestureAnimationEnd(); + } resetLauncherListeners(); } @@ -1922,7 +1924,9 @@ public abstract class AbsSwipeUpHandler, private void resetLauncherListeners() { mActivity.getRootView().setOnApplyWindowInsetsListener(null); - mRecentsView.removeOnScrollChangedListener(mOnRecentsScrollListener); + if (mRecentsView != null) { + mRecentsView.removeOnScrollChangedListener(mOnRecentsScrollListener); + } } private void resetStateForAnimationCancel() { @@ -1981,8 +1985,10 @@ public abstract class AbsSwipeUpHandler, private boolean updateThumbnail(int runningTaskId, boolean refreshView) { boolean finishTransitionPosted = false; final TaskView taskView; - if (mGestureState.getEndTarget() == HOME || mGestureState.getEndTarget() == NEW_TASK - || mGestureState.getEndTarget() == ALL_APPS) { + if (mGestureState.getEndTarget() == HOME + || mGestureState.getEndTarget() == NEW_TASK + || mGestureState.getEndTarget() == ALL_APPS + || mRecentsView == null) { // Capture the screenshot before finishing the transition to home or quickswitching to // ensure it's taken in the correct orientation, but no need to update the thumbnail. taskView = null; @@ -2135,7 +2141,7 @@ public abstract class AbsSwipeUpHandler, protected void startNewTask(Consumer resultCallback) { // Launch the task user scrolled to (mRecentsView.getNextPage()). if (!mCanceled) { - TaskView nextTask = mRecentsView.getNextPageTaskView(); + TaskView nextTask = mRecentsView == null ? null : mRecentsView.getNextPageTaskView(); if (nextTask != null) { Task.TaskKey nextTaskKey = nextTask.getTask().key; int taskId = nextTaskKey.id; @@ -2237,7 +2243,8 @@ public abstract class AbsSwipeUpHandler, return; } RemoteAnimationTarget taskTarget = taskTargetOptional.get(); - TaskView taskView = mRecentsView.getTaskViewByTaskId(taskTarget.taskId); + TaskView taskView = mRecentsView == null + ? null : mRecentsView.getTaskViewByTaskId(taskTarget.taskId); if (taskView == null || !taskView.getThumbnail().shouldShowSplashView()) { finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */); return; @@ -2296,9 +2303,11 @@ public abstract class AbsSwipeUpHandler, * resume if we finish the controller. */ protected int getLastAppearedTaskIndex() { - return mGestureState.getLastAppearedTaskId() != -1 - ? mRecentsView.getTaskIndexForId(mGestureState.getLastAppearedTaskId()) - : mRecentsView.getRunningTaskIndex(); + return mRecentsView == null + ? -1 + : mGestureState.getLastAppearedTaskId() != -1 + ? mRecentsView.getTaskIndexForId(mGestureState.getLastAppearedTaskId()) + : mRecentsView.getRunningTaskIndex(); } /** diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java index 4e892e2722..ab3ae9f1dc 100644 --- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java +++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java @@ -149,6 +149,11 @@ public class QuickstepTestInformationHandler extends TestInformationHandler { case TestProtocol.REQUEST_DISABLE_TRANSIENT_TASKBAR: enableTransientTaskbar(false); return response; + + case TestProtocol.REQUEST_SHELL_DRAG_READY: + response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD, + SystemUiProxy.INSTANCE.get(mContext).isDragAndDropReady()); + return response; } return super.call(method, arg, extras); diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java index 616ddef748..89f06f6895 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.java +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java @@ -70,6 +70,7 @@ import com.android.systemui.unfold.progress.IUnfoldAnimation; import com.android.systemui.unfold.progress.IUnfoldTransitionListener; import com.android.wm.shell.back.IBackAnimation; import com.android.wm.shell.desktopmode.IDesktopMode; +import com.android.wm.shell.draganddrop.IDragAndDrop; import com.android.wm.shell.onehanded.IOneHanded; import com.android.wm.shell.pip.IPip; import com.android.wm.shell.pip.IPipAnimationListener; @@ -128,6 +129,7 @@ public class SystemUiProxy implements ISystemUiProxy { private IBinder mOriginalTransactionToken = null; private IOnBackInvokedCallback mBackToLauncherCallback; private IRemoteAnimationRunner mBackToLauncherRunner; + private IDragAndDrop mDragAndDrop; // Used to dedupe calls to SystemUI private int mLastShelfHeight; @@ -203,7 +205,7 @@ public class SystemUiProxy implements ISystemUiProxy { IStartingWindow startingWindow, IRecentTasks recentTasks, ISysuiUnlockAnimationController sysuiUnlockAnimationController, IBackAnimation backAnimation, IDesktopMode desktopMode, - IUnfoldAnimation unfoldAnimation) { + IUnfoldAnimation unfoldAnimation, IDragAndDrop dragAndDrop) { unlinkToDeath(); mSystemUiProxy = proxy; mPip = pip; @@ -216,6 +218,7 @@ public class SystemUiProxy implements ISystemUiProxy { mBackAnimation = backAnimation; mDesktopMode = desktopMode; mUnfoldAnimation = unfoldAnimation; + mDragAndDrop = dragAndDrop; linkToDeath(); // re-attach the listeners once missing due to setProxy has not been initialized yet. setPipAnimationListener(mPipAnimationListener); @@ -230,7 +233,7 @@ public class SystemUiProxy implements ISystemUiProxy { } public void clearProxy() { - setProxy(null, null, null, null, null, null, null, null, null, null, null); + setProxy(null, null, null, null, null, null, null, null, null, null, null, null); } // TODO(141886704): Find a way to remove this @@ -1099,6 +1102,11 @@ public class SystemUiProxy implements ISystemUiProxy { Log.e(TAG, "Failed call setUnfoldAnimationListener", e); } } + + // + // Recents + // + /** * Starts the recents activity. The caller should manage the thread on which this is called. */ @@ -1131,10 +1139,30 @@ public class SystemUiProxy implements ISystemUiProxy { try { mRecentTasks.startRecentsTransition(mRecentsPendingIntent, intent, optsBundle, mContext.getIApplicationThread(), runner); + return true; } catch (RemoteException e) { Log.e(TAG, "Error starting recents via shell", e); return false; } - return true; + } + + // + // Drag and drop + // + + /** + * For testing purposes. Returns `true` only if the shell drop target has shown and + * drawn and is ready to handle drag events and the subsequent drop. + */ + public boolean isDragAndDropReady() { + if (mDragAndDrop == null) { + return false; + } + try { + return mDragAndDrop.isReadyToHandleDrag(); + } catch (RemoteException e) { + Log.e(TAG, "Error querying drag state", e); + return false; + } } } diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index 038c6743dd..6ea171e6e2 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -43,6 +43,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_Q import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE; +import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_DRAG_AND_DROP; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_ONE_HANDED; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS; @@ -125,6 +126,7 @@ import com.android.systemui.shared.tracing.ProtoTraceable; import com.android.systemui.unfold.progress.IUnfoldAnimation; import com.android.wm.shell.back.IBackAnimation; import com.android.wm.shell.desktopmode.IDesktopMode; +import com.android.wm.shell.draganddrop.IDragAndDrop; import com.android.wm.shell.onehanded.IOneHanded; import com.android.wm.shell.pip.IPip; import com.android.wm.shell.recents.IRecentTasks; @@ -185,11 +187,13 @@ public class TouchInteractionService extends Service bundle.getBinder(KEY_EXTRA_SHELL_DESKTOP_MODE)); IUnfoldAnimation unfoldTransition = IUnfoldAnimation.Stub.asInterface( bundle.getBinder(KEY_EXTRA_UNFOLD_ANIMATION_FORWARDER)); + IDragAndDrop dragAndDrop = IDragAndDrop.Stub.asInterface( + bundle.getBinder(KEY_EXTRA_SHELL_DRAG_AND_DROP)); MAIN_EXECUTOR.execute(() -> { SystemUiProxy.INSTANCE.get(TouchInteractionService.this).setProxy(proxy, pip, splitscreen, onehanded, shellTransitions, startingWindow, recentTasks, launcherUnlockAnimationController, backAnimation, desktopMode, - unfoldTransition); + unfoldTransition, dragAndDrop); TouchInteractionService.this.initInputMonitor("TISBinder#onInitialize()"); preloadOverview(true /* fromInit */); }); diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt b/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt new file mode 100644 index 0000000000..ebea58cee2 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt @@ -0,0 +1,364 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.quickstep.util + +import android.annotation.IntDef +import android.app.ActivityManager.RunningTaskInfo +import android.app.ActivityTaskManager.INVALID_TASK_ID +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ShortcutInfo +import android.os.UserHandle +import android.util.Log +import com.android.internal.annotations.VisibleForTesting +import com.android.launcher3.logging.StatsLogManager.EventEnum +import com.android.launcher3.model.data.ItemInfo +import com.android.launcher3.shortcuts.ShortcutKey +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED +import com.android.launcher3.util.SplitConfigurationOptions.StagePosition +import com.android.launcher3.util.SplitConfigurationOptions.getOppositeStagePosition +import com.android.quickstep.util.SplitSelectDataHolder.Companion.SplitLaunchType +import java.io.PrintWriter + +/** + * Holds/transforms/signs/seals/delivers information for the transient state of the user + * selecting a first app to start split with and then choosing a second app. + * This class DOES NOT associate itself with drag-and-drop split screen starts because they come + * from the bad part of town. + * + * After setting the correct fields for initial/second.* variables, this converts them into the + * correct [PendingIntent] and [ShortcutInfo] objects where applicable and sends the necessary + * data back via [getSplitLaunchData]. + * [SplitLaunchType] indicates the type of tasks/apps/intents being launched given the provided + * state + */ +class SplitSelectDataHolder( + val context: Context +) { + val TAG = SplitSelectDataHolder::class.simpleName + + /** + * Order of the constant indicates the order of which task/app was selected. + * Ex. SPLIT_TASK_SHORTCUT means primary split app identified by task, secondary is shortcut + * SPLIT_SHORTCUT_TASK means primary split app is determined by shortcut, secondary is task + */ + companion object { + @IntDef(SPLIT_TASK_TASK, SPLIT_TASK_PENDINGINTENT, SPLIT_TASK_SHORTCUT, + SPLIT_PENDINGINTENT_TASK, SPLIT_PENDINGINTENT_PENDINGINTENT, SPLIT_SHORTCUT_TASK) + @Retention(AnnotationRetention.SOURCE) + annotation class SplitLaunchType + + const val SPLIT_TASK_TASK = 0 + const val SPLIT_TASK_PENDINGINTENT = 1 + const val SPLIT_TASK_SHORTCUT = 2 + const val SPLIT_PENDINGINTENT_TASK = 3 + const val SPLIT_SHORTCUT_TASK = 4 + const val SPLIT_PENDINGINTENT_PENDINGINTENT = 5 + } + + + @StagePosition + private var initialStagePosition: Int = STAGE_POSITION_UNDEFINED + private var initialTaskId: Int = INVALID_TASK_ID + private var secondTaskId: Int = INVALID_TASK_ID + private var initialUser: UserHandle? = null + private var secondUser: UserHandle? = null + private var initialIntent: Intent? = null + private var secondIntent: Intent? = null + private var secondPendingIntent: PendingIntent? = null + private var itemInfo: ItemInfo? = null + private var splitEvent: EventEnum? = null + private var initialShortcut: ShortcutInfo? = null + private var secondShortcut: ShortcutInfo? = null + private var initialPendingIntent: PendingIntent? = null + + /** + * @param alreadyRunningTask if set to [android.app.ActivityTaskManager.INVALID_TASK_ID] + * then @param intent will be used to launch the initial task + * @param intent will be ignored if @param alreadyRunningTask is set + */ + fun setInitialTaskSelect(intent: Intent?, @StagePosition stagePosition: Int, + itemInfo: ItemInfo?, splitEvent: EventEnum?, + alreadyRunningTask: Int) { + if (alreadyRunningTask != INVALID_TASK_ID) { + initialTaskId = alreadyRunningTask + } else { + initialIntent = intent!! + initialUser = itemInfo!!.user + } + setInitialData(stagePosition, splitEvent, itemInfo) + } + + /** + * To be called after first task selected from using a split shortcut from the fullscreen + * running app. + */ + fun setInitialTaskSelect(info: RunningTaskInfo, + @StagePosition stagePosition: Int, itemInfo: ItemInfo?, + splitEvent: EventEnum?) { + initialTaskId = info.taskId + setInitialData(stagePosition, splitEvent, itemInfo) + } + + private fun setInitialData(@StagePosition stagePosition: Int, + event: EventEnum?, item: ItemInfo?) { + itemInfo = item + initialStagePosition = stagePosition + splitEvent = event + } + + /** + * To be called as soon as user selects the second task (even if animations aren't complete) + * @param taskId The second task that will be launched. + */ + fun setSecondTask(taskId: Int) { + secondTaskId = taskId + } + + /** + * To be called as soon as user selects the second app (even if animations aren't complete) + * @param intent The second intent that will be launched. + * @param user The user of that intent. + */ + fun setSecondTask(intent: Intent, user: UserHandle) { + secondIntent = intent + secondUser = user + } + + /** + * To be called as soon as user selects the second app (even if animations aren't complete) + * Sets [secondUser] from that of the pendingIntent + * @param pendingIntent The second PendingIntent that will be launched. + */ + fun setSecondTask(pendingIntent: PendingIntent) { + secondPendingIntent = pendingIntent + secondUser = pendingIntent.creatorUserHandle!! + } + + private fun getShortcutInfo(intent: Intent?, user: UserHandle?): ShortcutInfo? { + if (intent?.getPackage() == null) { + return null + } + val shortcutId = intent.getStringExtra(ShortcutKey.EXTRA_SHORTCUT_ID) + ?: return null + try { + val context: Context = context.createPackageContextAsUser( + intent.getPackage(), 0 /* flags */, user) + return ShortcutInfo.Builder(context, shortcutId).build() + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Failed to create a ShortcutInfo for " + intent.getPackage()) + } + return null + } + + /** + * Converts intents to pendingIntents, associating the [user] with the intent if provided + */ + private fun getPendingIntent(intent: Intent?, user: UserHandle?): PendingIntent? { + if (intent != initialIntent && intent != secondIntent) { + throw IllegalStateException("Invalid intent to convert to PendingIntent") + } + + return if (intent == null) { + null + } else if (user != null) { + PendingIntent.getActivityAsUser(context, 0, intent, + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT, + null /* options */, user) + } else { + PendingIntent.getActivity(context, 0, intent, + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT) + } + } + + /** + * @return [SplitLaunchData] with the necessary fields populated as determined by + * [SplitLaunchData.splitLaunchType] + */ + fun getSplitLaunchData() : SplitLaunchData { + // Convert all intents to shortcut infos to see if determine if we launch shortcut or intent + convertIntentsToFinalTypes() + val splitLaunchType = getSplitLaunchType() + if (splitLaunchType == SPLIT_TASK_PENDINGINTENT || splitLaunchType == SPLIT_TASK_SHORTCUT) { + // need to get opposite stage position + initialStagePosition = getOppositeStagePosition(initialStagePosition) + } + + return SplitLaunchData( + splitLaunchType, + initialTaskId, + secondTaskId, + initialPendingIntent, + secondPendingIntent, + initialShortcut, + secondShortcut, + itemInfo, + splitEvent, + initialStagePosition) + } + + /** + * Converts our [initialIntent] and [secondIntent] into shortcuts and pendingIntents, if + * possible. + * + * Note that both [initialIntent] and [secondIntent] will be nullified on method return + * + * One caveat is that if [secondPendingIntent] is set, we will use that and *not* attempt to + * convert [secondIntent] + */ + private fun convertIntentsToFinalTypes() { + initialShortcut = getShortcutInfo(initialIntent, initialUser) + initialPendingIntent = getPendingIntent(initialIntent, initialUser) + initialIntent = null + + // Only one of the two is currently allowed (secondPendingIntent directly set for widgets) + if (secondIntent != null && secondPendingIntent != null) { + throw IllegalStateException("Both secondIntent and secondPendingIntent non-null") + } + // If secondPendingIntent already set, no need to convert. Prioritize using that + if (secondPendingIntent != null) { + secondIntent = null + return + } + + secondShortcut = getShortcutInfo(secondIntent, secondUser) + secondPendingIntent = getPendingIntent(secondIntent, secondUser) + secondIntent = null + } + + /** + * Only valid data fields at this point should be tasks, shortcuts, or pendingIntents + * Intents need to be converted in [convertIntentsToFinalTypes] prior to calling this method + */ + @VisibleForTesting + @SplitLaunchType + fun getSplitLaunchType(): Int { + if (initialIntent != null || secondIntent != null) { + throw IllegalStateException("Intents need to be converted") + } + + // Prioritize task launches first + if (initialTaskId != INVALID_TASK_ID) { + if (secondTaskId != INVALID_TASK_ID) { + return SPLIT_TASK_TASK + } + if (secondShortcut != null) { + return SPLIT_TASK_SHORTCUT + } + if (secondPendingIntent != null) { + return SPLIT_TASK_PENDINGINTENT + } + } + + if (secondTaskId != INVALID_TASK_ID) { + if (initialShortcut != null) { + return SPLIT_SHORTCUT_TASK + } + if (initialPendingIntent != null) { + return SPLIT_PENDINGINTENT_TASK + } + } + + // All task+shortcut combinations are handled above, only launch left is with multiple + // intents (and respective shortcut infos, if necessary) + if (initialPendingIntent != null && secondPendingIntent != null) { + return SPLIT_PENDINGINTENT_PENDINGINTENT + } + throw IllegalStateException("Unidentified split launch type") + } + + data class SplitLaunchData( + @SplitLaunchType + val splitLaunchType: Int, + var initialTaskId: Int = INVALID_TASK_ID, + var secondTaskId: Int = INVALID_TASK_ID, + var initialPendingIntent: PendingIntent? = null, + var secondPendingIntent: PendingIntent? = null, + var initialShortcut: ShortcutInfo? = null, + var secondShortcut: ShortcutInfo? = null, + var itemInfo: ItemInfo? = null, + var splitEvent: EventEnum? = null, + val initialStagePosition: Int = STAGE_POSITION_UNDEFINED + ) + + /** + * @return `true` if first task has been selected and waiting for the second task to be + * chosen + */ + fun isSplitSelectActive(): Boolean { + return isInitialTaskIntentSet() && !isSecondTaskIntentSet() + } + + /** + * @return `true` if the first and second task have been chosen and split is waiting to + * be launched + */ + fun isBothSplitAppsConfirmed(): Boolean { + return isInitialTaskIntentSet() && isSecondTaskIntentSet() + } + + private fun isInitialTaskIntentSet(): Boolean { + return initialTaskId != INVALID_TASK_ID || initialIntent != null + } + + fun getInitialTaskId(): Int { + return initialTaskId + } + + fun getSecondTaskId(): Int { + return secondTaskId + } + + private fun isSecondTaskIntentSet(): Boolean { + return secondTaskId != INVALID_TASK_ID || secondIntent != null + || secondPendingIntent != null + } + + fun resetState() { + initialStagePosition = STAGE_POSITION_UNDEFINED + initialTaskId = INVALID_TASK_ID + secondTaskId = INVALID_TASK_ID + initialUser = null + secondUser = null + initialIntent = null + secondIntent = null + secondPendingIntent = null + itemInfo = null + splitEvent = null + initialShortcut = null + secondShortcut = null + } + + fun dump(prefix: String, writer: PrintWriter) { + writer.println("$prefix ${javaClass.simpleName}") + writer.println("$prefix\tinitialStagePosition= $initialStagePosition") + writer.println("$prefix\tinitialTaskId= $initialTaskId") + writer.println("$prefix\tsecondTaskId= $secondTaskId") + writer.println("$prefix\tinitialUser= $initialUser") + writer.println("$prefix\tsecondUser= $secondUser") + writer.println("$prefix\tinitialIntent= $initialIntent") + writer.println("$prefix\tsecondIntent= $secondIntent") + writer.println("$prefix\tsecondPendingIntent= $secondPendingIntent") + writer.println("$prefix\titemInfo= $itemInfo") + writer.println("$prefix\tsplitEvent= $splitEvent") + writer.println("$prefix\tinitialShortcut= $initialShortcut") + writer.println("$prefix\tsecondShortcut= $secondShortcut") + } +} \ No newline at end of file diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java index 8b21115f0f..acc3ba1681 100644 --- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java +++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java @@ -24,6 +24,12 @@ import static com.android.launcher3.Utilities.postAsyncCallback; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO; import static com.android.launcher3.util.SplitConfigurationOptions.getOppositeStagePosition; +import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_PENDINGINTENT; +import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_TASK; +import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SHORTCUT_TASK; +import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_PENDINGINTENT; +import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_SHORTCUT; +import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_TASK; import android.annotation.NonNull; import android.app.ActivityManager; @@ -34,6 +40,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; @@ -51,6 +58,7 @@ import android.window.TransitionInfo; import androidx.annotation.Nullable; import com.android.internal.logging.InstanceId; +import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.logging.StatsLogManager; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.shortcuts.ShortcutKey; @@ -71,6 +79,7 @@ import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; +import java.io.PrintWriter; import java.util.function.Consumer; /** @@ -85,6 +94,7 @@ public class SplitSelectStateController { private final RecentsModel mRecentTasksModel; private final SplitAnimationController mSplitAnimationController; private final AppPairsController mAppPairsController; + private final SplitSelectDataHolder mSplitSelectDataHolder; private StatsLogManager mStatsLogManager; private final SystemUiProxy mSystemUiProxy; private final StateManager mStateManager; @@ -137,6 +147,7 @@ public class SplitSelectStateController { mRecentTasksModel = recentsModel; mSplitAnimationController = new SplitAnimationController(this); mAppPairsController = new AppPairsController(context, this); + mSplitSelectDataHolder = new SplitSelectDataHolder(mContext); } /** @@ -155,6 +166,11 @@ public class SplitSelectStateController { } setInitialData(stagePosition, splitEvent, itemInfo); + + if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) { + mSplitSelectDataHolder.setInitialTaskSelect(intent, stagePosition, itemInfo, splitEvent, + alreadyRunningTask); + } } /** @@ -166,6 +182,10 @@ public class SplitSelectStateController { StatsLogManager.EventEnum splitEvent) { mInitialTaskId = info.taskId; setInitialData(stagePosition, splitEvent, itemInfo); + + if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) { + mSplitSelectDataHolder.setInitialTaskSelect(info, stagePosition, itemInfo, splitEvent); + } } private void setInitialData(@StagePosition int stagePosition, @@ -243,6 +263,10 @@ public class SplitSelectStateController { */ public void setSecondTask(Task task) { mSecondTaskId = task.key.id; + + if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) { + mSplitSelectDataHolder.setSecondTask(task.key.id); + } } /** @@ -253,6 +277,10 @@ public class SplitSelectStateController { public void setSecondTask(Intent intent, UserHandle user) { mSecondTaskIntent = intent; mSecondUser = user; + + if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) { + mSplitSelectDataHolder.setSecondTask(intent, user); + } } /** @@ -263,6 +291,10 @@ public class SplitSelectStateController { public void setSecondTask(PendingIntent pendingIntent) { mSecondPendingIntent = pendingIntent; mSecondUser = pendingIntent.getCreatorUserHandle(); + + if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) { + mSplitSelectDataHolder.setSecondTask(pendingIntent); + } } /** @@ -285,6 +317,12 @@ public class SplitSelectStateController { */ public void launchTasks(int taskId1, int taskId2, @StagePosition int stagePosition, Consumer callback, boolean freezeTaskList, float splitRatio) { + if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) { + mSplitSelectDataHolder.setInitialTaskSelect(null /*intent*/, + stagePosition, null /*itemInfo*/, null /*splitEvent*/, + taskId1); + mSplitSelectDataHolder.setSecondTask(taskId2); + } launchTasks(taskId1, null /* intent1 */, taskId2, null /* intent2 */, stagePosition, callback, freezeTaskList, splitRatio, null); } @@ -305,6 +343,11 @@ public class SplitSelectStateController { @Nullable InstanceId shellInstanceId) { TestLogging.recordEvent( TestProtocol.SEQUENCE_MAIN, "launchSplitTasks"); + if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) { + launchTasksRefactored(callback, freezeTaskList, splitRatio, shellInstanceId); + return; + } + final ActivityOptions options1 = ActivityOptions.makeBasic(); if (freezeTaskList) { options1.setFreezeRecentTasksReordering(); @@ -367,6 +410,101 @@ public class SplitSelectStateController { } } + private void launchTasksRefactored(Consumer callback, boolean freezeTaskList, + float splitRatio, @Nullable InstanceId shellInstanceId) { + final ActivityOptions options1 = ActivityOptions.makeBasic(); + if (freezeTaskList) { + options1.setFreezeRecentTasksReordering(); + } + + SplitSelectDataHolder.SplitLaunchData launchData = + mSplitSelectDataHolder.getSplitLaunchData(); + int firstTaskId = launchData.getInitialTaskId(); + int secondTaskId = launchData.getSecondTaskId(); + ShortcutInfo firstShortcut = launchData.getInitialShortcut(); + ShortcutInfo secondShortcut = launchData.getSecondShortcut(); + PendingIntent firstPI = launchData.getInitialPendingIntent(); + PendingIntent secondPI = launchData.getSecondPendingIntent(); + int initialStagePosition = launchData.getInitialStagePosition(); + Bundle optionsBundle = options1.toBundle(); + + if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { + final RemoteSplitLaunchTransitionRunner animationRunner = + new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback); + final RemoteTransition remoteTransition = new RemoteTransition(animationRunner, + ActivityThread.currentActivityThread().getApplicationThread(), + "LaunchSplitPair"); + switch (launchData.getSplitLaunchType()) { + case SPLIT_TASK_TASK -> + mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, + null /* options2 */, initialStagePosition, splitRatio, + remoteTransition, shellInstanceId); + + case SPLIT_TASK_PENDINGINTENT -> + mSystemUiProxy.startIntentAndTask(secondPI, optionsBundle, firstTaskId, + null /*options2*/, initialStagePosition, splitRatio, + remoteTransition, shellInstanceId); + + case SPLIT_TASK_SHORTCUT -> + mSystemUiProxy.startShortcutAndTask(secondShortcut, optionsBundle, + firstTaskId, null /*options2*/, initialStagePosition, splitRatio, + remoteTransition, shellInstanceId); + + case SPLIT_PENDINGINTENT_TASK -> + mSystemUiProxy.startIntentAndTask(firstPI, optionsBundle, secondTaskId, + null /*options2*/, initialStagePosition, splitRatio, + remoteTransition, shellInstanceId); + + case SPLIT_PENDINGINTENT_PENDINGINTENT -> + mSystemUiProxy.startIntents(firstPI, firstShortcut, optionsBundle, secondPI, + secondShortcut, null /*options2*/, initialStagePosition, splitRatio, + remoteTransition, shellInstanceId); + + case SPLIT_SHORTCUT_TASK -> + mSystemUiProxy.startShortcutAndTask(firstShortcut, optionsBundle, + secondTaskId, null /*options2*/, initialStagePosition, splitRatio, + remoteTransition, shellInstanceId); + } + } else { + final RemoteSplitLaunchAnimationRunner animationRunner = + new RemoteSplitLaunchAnimationRunner(firstTaskId, secondTaskId, callback); + final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( + animationRunner, 300, 150, + ActivityThread.currentActivityThread().getApplicationThread()); + switch (launchData.getSplitLaunchType()) { + case SPLIT_TASK_TASK -> + mSystemUiProxy.startTasksWithLegacyTransition(firstTaskId, optionsBundle, + secondTaskId, null /* options2 */, initialStagePosition, + splitRatio, adapter, shellInstanceId); + + case SPLIT_TASK_PENDINGINTENT -> + mSystemUiProxy.startIntentAndTaskWithLegacyTransition(secondPI, + optionsBundle, firstTaskId, null /*options2*/, initialStagePosition, + splitRatio, adapter, shellInstanceId); + + case SPLIT_TASK_SHORTCUT -> + mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(secondShortcut, + optionsBundle, firstTaskId, null /*options2*/, initialStagePosition, + splitRatio, adapter, shellInstanceId); + + case SPLIT_PENDINGINTENT_TASK -> + mSystemUiProxy.startIntentAndTaskWithLegacyTransition(firstPI, + optionsBundle, secondTaskId, null /*options2*/, + initialStagePosition, splitRatio, adapter, shellInstanceId); + + case SPLIT_PENDINGINTENT_PENDINGINTENT -> + mSystemUiProxy.startIntentsWithLegacyTransition(firstPI, firstShortcut, + optionsBundle, secondPI, secondShortcut, null /*options2*/, + initialStagePosition, splitRatio, adapter, shellInstanceId); + + case SPLIT_SHORTCUT_TASK -> + mSystemUiProxy.startShortcutAndTaskWithLegacyTransition(firstShortcut, + optionsBundle, secondTaskId, null /*options2*/, + initialStagePosition, splitRatio, adapter, shellInstanceId); + } + } + } + private void launchIntentOrShortcut(Intent intent, UserHandle user, ActivityOptions options1, int taskId, @StagePosition int stagePosition, float splitRatio, RemoteTransition remoteTransition, @Nullable InstanceId shellInstanceId) { @@ -572,6 +710,9 @@ public class SplitSelectStateController { * To be called if split select was cancelled */ public void resetState() { + if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) { + mSplitSelectDataHolder.resetState(); + } mInitialTaskId = INVALID_TASK_ID; mInitialTaskIntent = null; mSecondTaskId = INVALID_TASK_ID; @@ -593,7 +734,11 @@ public class SplitSelectStateController { * chosen */ public boolean isSplitSelectActive() { - return isInitialTaskIntentSet() && !isSecondTaskIntentSet(); + if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) { + return mSplitSelectDataHolder.isSplitSelectActive(); + } else { + return isInitialTaskIntentSet() && !isSecondTaskIntentSet(); + } } /** @@ -601,7 +746,11 @@ public class SplitSelectStateController { * be launched */ public boolean isBothSplitAppsConfirmed() { - return isInitialTaskIntentSet() && isSecondTaskIntentSet(); + if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) { + return mSplitSelectDataHolder.isBothSplitAppsConfirmed(); + } else { + return isInitialTaskIntentSet() && isSecondTaskIntentSet(); + } } private boolean isInitialTaskIntentSet() { @@ -609,11 +758,19 @@ public class SplitSelectStateController { } public int getInitialTaskId() { - return mInitialTaskId; + if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) { + return mSplitSelectDataHolder.getInitialTaskId(); + } else { + return mInitialTaskId; + } } public int getSecondTaskId() { - return mSecondTaskId; + if (FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) { + return mSplitSelectDataHolder.getSecondTaskId(); + } else { + return mSecondTaskId; + } } private boolean isSecondTaskIntentSet() { @@ -632,4 +789,10 @@ public class SplitSelectStateController { public AppPairsController getAppPairsController() { return mAppPairsController; } + + public void dump(String prefix, PrintWriter writer) { + if (mSplitSelectDataHolder != null) { + mSplitSelectDataHolder.dump(prefix, writer); + } + } } diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index 2a3ad9afdd..331ae5da65 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -407,7 +407,12 @@ public final class FeatureFlags { "USE_SEARCH_REQUEST_TIMEOUT_OVERRIDES", DISABLED, "Use local overrides for search request timeout"); - // TODO(Block 31): Empty block + // TODO(Block 31) + public static final BooleanFlag ENABLE_SPLIT_LAUNCH_DATA_REFACTOR = getDebugFlag(279494325, + "ENABLE_SPLIT_LAUNCH_DATA_REFACTOR", DISABLED, + "Use refactored split launching code path"); + + // TODO(Block 32): Empty block public static class BooleanFlag { diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java index 36255b4628..b472cdbea3 100644 --- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java +++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java @@ -111,6 +111,7 @@ public final class TestProtocol { public static final String REQUEST_IS_TABLET = "is-tablet"; public static final String REQUEST_IS_TWO_PANELS = "is-two-panel"; public static final String REQUEST_START_DRAG_THRESHOLD = "start-drag-threshold"; + public static final String REQUEST_SHELL_DRAG_READY = "shell-drag-ready"; public static final String REQUEST_GET_ACTIVITIES_CREATED_COUNT = "get-activities-created-count"; public static final String REQUEST_GET_ACTIVITIES = "get-activities"; diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java index 4a3507ed69..58d5a36d66 100644 --- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java +++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java @@ -16,11 +16,14 @@ package com.android.launcher3.tapl; +import static com.android.launcher3.tapl.LauncherInstrumentation.DEFAULT_POLL_INTERVAL; import static com.android.launcher3.tapl.LauncherInstrumentation.TASKBAR_RES_ID; +import static com.android.launcher3.tapl.LauncherInstrumentation.WAIT_TIME_MS; import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_DISABLE_BLOCK_TIMEOUT; import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_DISABLE_MANUAL_TASKBAR_STASHING; import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_ENABLE_BLOCK_TIMEOUT; import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_ENABLE_MANUAL_TASKBAR_STASHING; +import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_SHELL_DRAG_READY; import static com.android.launcher3.testing.shared.TestProtocol.REQUEST_STASHED_TASKBAR_HEIGHT; import android.graphics.Point; @@ -146,6 +149,12 @@ public final class LaunchedAppState extends Background { try (LauncherInstrumentation.Closable c2 = launcher.addContextLayer( "started item drag")) { + launcher.assertTrue("Shell drag not marked as ready", launcher.waitAndGet(() -> { + LauncherInstrumentation.log("Checking shell drag ready"); + return launcher.getTestInfo(REQUEST_SHELL_DRAG_READY) + .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD, false); + }, WAIT_TIME_MS, DEFAULT_POLL_INTERVAL)); + launcher.movePointer( dragStart, endPoint,