diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java deleted file mode 100644 index 8f533a362a..0000000000 --- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java +++ /dev/null @@ -1,529 +0,0 @@ -/* - * Copyright (C) 2018 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; - -import static com.android.launcher3.PagedView.INVALID_PAGE; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT; -import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; -import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.content.Intent; -import android.graphics.PointF; -import android.os.SystemClock; -import android.os.Trace; -import android.util.Log; -import android.view.View; - -import androidx.annotation.BinderThread; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.UiThread; - -import com.android.internal.jank.Cuj; -import com.android.launcher3.DeviceProfile; -import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.logger.LauncherAtom; -import com.android.launcher3.logging.StatsLogManager; -import com.android.launcher3.statemanager.StatefulActivity; -import com.android.launcher3.taskbar.TaskbarUIController; -import com.android.launcher3.util.RunnableList; -import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener; -import com.android.quickstep.util.ActiveGestureLog; -import com.android.quickstep.views.RecentsView; -import com.android.quickstep.views.RecentsViewContainer; -import com.android.quickstep.views.TaskView; -import com.android.systemui.shared.recents.model.ThumbnailData; -import com.android.systemui.shared.system.InteractionJankMonitorWrapper; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.HashMap; - -/** - * Helper class to handle various atomic commands for switching between Overview. - */ -public class OverviewCommandHelper { - private static final String TAG = "OverviewCommandHelper"; - - public static final int TYPE_SHOW = 1; - public static final int TYPE_KEYBOARD_INPUT = 2; - public static final int TYPE_HIDE = 3; - public static final int TYPE_TOGGLE = 4; - public static final int TYPE_HOME = 5; - - /** - * Use case for needing a queue is double tapping recents button in 3 button nav. - * Size of 2 should be enough. We'll toss in one more because we're kind hearted. - */ - private final static int MAX_QUEUE_SIZE = 3; - - private static final String TRANSITION_NAME = "Transition:toOverview"; - - private final TouchInteractionService mService; - private final OverviewComponentObserver mOverviewComponentObserver; - private final TaskAnimationManager mTaskAnimationManager; - private final ArrayList mPendingCommands = new ArrayList<>(); - - /** - * Index of the TaskView that should be focused when launching Overview. Persisted so that we - * do not lose the focus across multiple calls of - * {@link OverviewCommandHelper#executeCommand(CommandInfo)} for the same command - */ - private int mKeyboardTaskFocusIndex = -1; - - /** - * Whether we should incoming toggle commands while a previous toggle command is still ongoing. - * This serves as a rate-limiter to prevent overlapping animations that can clobber each other - * and prevent clean-up callbacks from running. This thus prevents a recurring set of bugs with - * janky recents animations and unresponsive home and overview buttons. - */ - private boolean mWaitForToggleCommandComplete = false; - - public OverviewCommandHelper(TouchInteractionService service, - OverviewComponentObserver observer, - TaskAnimationManager taskAnimationManager) { - mService = service; - mOverviewComponentObserver = observer; - mTaskAnimationManager = taskAnimationManager; - } - - /** - * Called when the command finishes execution. - */ - private void scheduleNextTask(CommandInfo command) { - if (mPendingCommands.isEmpty()) { - Log.d(TAG, "no pending commands to schedule"); - return; - } - if (mPendingCommands.get(0) != command) { - Log.d(TAG, "next task not scheduled." - + " mPendingCommands[0] type is " + mPendingCommands.get(0) - + " - command type is: " + command); - return; - } - Log.d(TAG, "scheduleNextTask called: " + command); - mPendingCommands.remove(0); - executeNext(); - } - - /** - * Executes the next command from the queue. If the command finishes immediately (returns true), - * it continues to execute the next command, until the queue is empty of a command defer's its - * completion (returns false). - */ - @UiThread - private void executeNext() { - if (mPendingCommands.isEmpty()) { - Log.d(TAG, "executeNext - mPendingCommands is empty"); - return; - } - CommandInfo cmd = mPendingCommands.get(0); - - boolean result = executeCommand(cmd); - Log.d(TAG, "executeNext cmd type: " + cmd + ", result: " + result); - if (result) { - scheduleNextTask(cmd); - } - } - - @UiThread - private void addCommand(CommandInfo cmd) { - boolean wasEmpty = mPendingCommands.isEmpty(); - mPendingCommands.add(cmd); - if (wasEmpty) { - executeNext(); - } - } - - /** - * Adds a command to be executed next, after all pending tasks are completed. - * Max commands that can be queued is {@link #MAX_QUEUE_SIZE}. - * Requests after reaching that limit will be silently dropped. - */ - @BinderThread - public void addCommand(int type) { - if (mPendingCommands.size() >= MAX_QUEUE_SIZE) { - Log.d(TAG, "the pending command queue is full (" + mPendingCommands.size() + "). " - + "command not added: " + type); - return; - } - Log.d(TAG, "adding command type: " + type); - CommandInfo cmd = new CommandInfo(type); - MAIN_EXECUTOR.execute(() -> addCommand(cmd)); - } - - @UiThread - public void clearPendingCommands() { - Log.d(TAG, "clearing pending commands - size: " + mPendingCommands.size()); - mPendingCommands.clear(); - } - - @UiThread - public boolean canStartHomeSafely() { - return mPendingCommands.isEmpty() || mPendingCommands.get(0).type == TYPE_HOME; - } - - @Nullable - private TaskView getNextTask(RecentsView view) { - final TaskView runningTaskView = view.getRunningTaskView(); - - if (runningTaskView == null) { - return view.getTaskViewAt(0); - } else { - final TaskView nextTask = view.getNextTaskView(); - return nextTask != null ? nextTask : runningTaskView; - } - } - - private boolean launchTask(RecentsView recents, @Nullable TaskView taskView, CommandInfo cmd) { - RunnableList callbackList = null; - if (taskView != null) { - mWaitForToggleCommandComplete = true; - taskView.setEndQuickSwitchCuj(true); - callbackList = taskView.launchTasks(); - } - - if (callbackList != null) { - callbackList.add(() -> { - Log.d(TAG, "launching task callback: " + cmd); - scheduleNextTask(cmd); - mWaitForToggleCommandComplete = false; - }); - Log.d(TAG, "launching task - waiting for callback: " + cmd); - return false; - } else { - recents.startHome(); - mWaitForToggleCommandComplete = false; - return true; - } - } - - /** - * Executes the task and returns true if next task can be executed. If false, then the next - * task is deferred until {@link #scheduleNextTask} is called - */ - private & RecentsViewContainer> boolean executeCommand( - CommandInfo cmd) { - if (mWaitForToggleCommandComplete && cmd.type == TYPE_TOGGLE) { - Log.d(TAG, "executeCommand: " + cmd - + " - waiting for toggle command complete"); - return true; - } - BaseActivityInterface activityInterface = - mOverviewComponentObserver.getActivityInterface(); - - RecentsView visibleRecentsView = activityInterface.getVisibleRecentsView(); - RecentsView createdRecentsView; - - Log.d(TAG, "executeCommand: " + cmd - + " - visibleRecentsView: " + visibleRecentsView); - if (visibleRecentsView == null) { - T activity = activityInterface.getCreatedContainer(); - createdRecentsView = activity == null ? null : activity.getOverviewPanel(); - DeviceProfile dp = activity == null ? null : activity.getDeviceProfile(); - TaskbarUIController uiController = activityInterface.getTaskbarController(); - boolean allowQuickSwitch = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get() - && uiController != null - && dp != null - && (dp.isTablet || dp.isTwoPanels); - - switch (cmd.type) { - case TYPE_HIDE: - if (!allowQuickSwitch) { - return true; - } - mKeyboardTaskFocusIndex = uiController.launchFocusedTask(); - if (mKeyboardTaskFocusIndex == -1) { - return true; - } - break; - case TYPE_KEYBOARD_INPUT: - if (allowQuickSwitch) { - uiController.openQuickSwitchView(); - return true; - } else { - mKeyboardTaskFocusIndex = 0; - break; - } - case TYPE_HOME: - ActiveGestureLog.INSTANCE.addLog( - "OverviewCommandHelper.executeCommand(TYPE_HOME)"); - // Although IActivityTaskManager$Stub$Proxy.startActivity is a slow binder call, - // we should still call it on main thread because launcher is waiting for - // ActivityTaskManager to resume it. Also calling startActivity() on bg thread - // could potentially delay resuming launcher. See b/348668521 for more details. - mService.startActivity(mOverviewComponentObserver.getHomeIntent()); - return true; - case TYPE_SHOW: - // When Recents is not currently visible, the command's type is TYPE_SHOW - // when overview is triggered via the keyboard overview button or Action+Tab - // keys (Not Alt+Tab which is KQS). The overview button on-screen in 3-button - // nav is TYPE_TOGGLE. - mKeyboardTaskFocusIndex = 0; - break; - default: - // continue below to handle displaying Recents. - } - } else { - createdRecentsView = visibleRecentsView; - switch (cmd.type) { - case TYPE_SHOW: - // already visible - return true; - case TYPE_KEYBOARD_INPUT: { - if (visibleRecentsView.isHandlingTouch()) { - return true; - } - } - case TYPE_HIDE: { - if (visibleRecentsView.isHandlingTouch()) { - return true; - } - mKeyboardTaskFocusIndex = INVALID_PAGE; - int currentPage = visibleRecentsView.getNextPage(); - TaskView tv = (currentPage >= 0 - && currentPage < visibleRecentsView.getTaskViewCount()) - ? (TaskView) visibleRecentsView.getPageAt(currentPage) - : null; - return launchTask(visibleRecentsView, tv, cmd); - } - case TYPE_TOGGLE: - return launchTask(visibleRecentsView, getNextTask(visibleRecentsView), cmd); - case TYPE_HOME: - visibleRecentsView.startHome(); - return true; - } - } - - if (createdRecentsView != null) { - createdRecentsView.setKeyboardTaskFocusIndex(mKeyboardTaskFocusIndex); - } - // Handle recents view focus when launching from home - Animator.AnimatorListener animatorListener = new AnimatorListenerAdapter() { - - @Override - public void onAnimationStart(Animator animation) { - super.onAnimationStart(animation); - updateRecentsViewFocus(cmd); - logShowOverviewFrom(cmd.type); - } - - @Override - public void onAnimationEnd(Animator animation) { - Log.d(TAG, "switching to Overview state - onAnimationEnd: " + cmd); - super.onAnimationEnd(animation); - onRecentsViewFocusUpdated(cmd); - scheduleNextTask(cmd); - } - }; - if (activityInterface.switchToRecentsIfVisible(animatorListener)) { - Log.d(TAG, "switching to Overview state - waiting: " + cmd); - // If successfully switched, wait until animation finishes - return false; - } - - final T activity = activityInterface.getCreatedContainer(); - if (activity != null) { - InteractionJankMonitorWrapper.begin( - activity.getRootView(), - Cuj.CUJ_LAUNCHER_QUICK_SWITCH); - } - - GestureState gestureState = mService.createGestureState(GestureState.DEFAULT_STATE, - GestureState.TrackpadGestureType.NONE); - gestureState.setHandlingAtomicEvent(true); - AbsSwipeUpHandler interactionHandler = mService.getSwipeUpHandlerFactory() - .newHandler(gestureState, cmd.createTime); - interactionHandler.setGestureEndCallback( - () -> onTransitionComplete(cmd, interactionHandler)); - interactionHandler.initWhenReady("OverviewCommandHelper: cmd.type=" + cmd.type); - - RecentsAnimationListener recentAnimListener = new RecentsAnimationListener() { - @Override - public void onRecentsAnimationStart(RecentsAnimationController controller, - RecentsAnimationTargets targets) { - updateRecentsViewFocus(cmd); - logShowOverviewFrom(cmd.type); - activityInterface.runOnInitBackgroundStateUI(() -> - interactionHandler.onGestureEnded(0, new PointF())); - cmd.removeListener(this); - } - - @Override - public void onRecentsAnimationCanceled(HashMap thumbnailDatas) { - interactionHandler.onGestureCancelled(); - cmd.removeListener(this); - - T createdActivity = activityInterface.getCreatedContainer(); - if (createdActivity == null) { - return; - } - if (createdRecentsView != null) { - createdRecentsView.onRecentsAnimationComplete(); - } - } - }; - - if (visibleRecentsView != null) { - visibleRecentsView.moveRunningTaskToFront(); - } - if (mTaskAnimationManager.isRecentsAnimationRunning()) { - cmd.mActiveCallbacks = mTaskAnimationManager.continueRecentsAnimation(gestureState); - cmd.mActiveCallbacks.addListener(interactionHandler); - mTaskAnimationManager.notifyRecentsAnimationState(interactionHandler); - interactionHandler.onGestureStarted(true /*isLikelyToStartNewTask*/); - - cmd.mActiveCallbacks.addListener(recentAnimListener); - mTaskAnimationManager.notifyRecentsAnimationState(recentAnimListener); - } else { - Intent intent = new Intent(interactionHandler.getLaunchIntent()); - intent.putExtra(INTENT_EXTRA_LOG_TRACE_ID, gestureState.getGestureId()); - cmd.mActiveCallbacks = mTaskAnimationManager.startRecentsAnimation( - gestureState, intent, interactionHandler); - interactionHandler.onGestureStarted(false /*isLikelyToStartNewTask*/); - cmd.mActiveCallbacks.addListener(recentAnimListener); - } - Trace.beginAsyncSection(TRANSITION_NAME, 0); - Log.d(TAG, "switching via recents animation - onGestureStarted: " + cmd); - return false; - } - - private void onTransitionComplete(CommandInfo cmd, AbsSwipeUpHandler handler) { - Log.d(TAG, "switching via recents animation - onTransitionComplete: " + cmd); - cmd.removeListener(handler); - Trace.endAsyncSection(TRANSITION_NAME, 0); - onRecentsViewFocusUpdated(cmd); - scheduleNextTask(cmd); - } - - private void updateRecentsViewFocus(CommandInfo cmd) { - RecentsView recentsView = - mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView(); - if (recentsView == null || (cmd.type != TYPE_KEYBOARD_INPUT && cmd.type != TYPE_HIDE - && cmd.type != TYPE_SHOW)) { - return; - } - // When the overview is launched via alt tab (cmd type is TYPE_KEYBOARD_INPUT), - // the touch mode somehow is not change to false by the Android framework. - // The subsequent tab to go through tasks in overview can only be dispatched to - // focuses views, while focus can only be requested in - // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note, - // here we launch overview with live tile. - recentsView.getViewRootImpl().touchModeChanged(false); - // Ensure that recents view has focus so that it receives the followup key inputs - if (requestFocus(recentsView.getTaskViewAt(mKeyboardTaskFocusIndex))) { - return; - } - if (requestFocus(recentsView.getNextTaskView())) { - return; - } - if (requestFocus(recentsView.getTaskViewAt(0))) { - return; - } - requestFocus(recentsView); - } - - private void onRecentsViewFocusUpdated(CommandInfo cmd) { - RecentsView recentsView = - mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView(); - if (recentsView == null - || cmd.type != TYPE_HIDE - || mKeyboardTaskFocusIndex == INVALID_PAGE) { - return; - } - recentsView.setKeyboardTaskFocusIndex(INVALID_PAGE); - recentsView.setCurrentPage(mKeyboardTaskFocusIndex); - mKeyboardTaskFocusIndex = INVALID_PAGE; - } - - private boolean requestFocus(@Nullable View taskView) { - if (taskView == null) { - return false; - } - taskView.post(() -> { - taskView.requestFocus(); - taskView.requestAccessibilityFocus(); - }); - return true; - } - - private & RecentsViewContainer> - void logShowOverviewFrom(int cmdType) { - BaseActivityInterface activityInterface = - mOverviewComponentObserver.getActivityInterface(); - var container = activityInterface.getCreatedContainer(); - if (container != null) { - StatsLogManager.LauncherEvent event; - switch (cmdType) { - case TYPE_SHOW -> event = LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT; - case TYPE_HIDE -> - event = LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH; - case TYPE_TOGGLE -> event = LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON; - default -> { - return; - } - } - - StatsLogManager.newInstance(container.asContext()) - .logger() - .withContainerInfo(LauncherAtom.ContainerInfo.newBuilder() - .setTaskSwitcherContainer( - LauncherAtom.TaskSwitcherContainer.getDefaultInstance()) - .build()) - .log(event); - } - } - - public void dump(PrintWriter pw) { - pw.println("OverviewCommandHelper:"); - pw.println(" mPendingCommands=" + mPendingCommands.size()); - if (!mPendingCommands.isEmpty()) { - pw.println(" pendingCommandType=" + mPendingCommands.get(0).type); - } - pw.println(" mKeyboardTaskFocusIndex=" + mKeyboardTaskFocusIndex); - pw.println(" mWaitForToggleCommandComplete=" + mWaitForToggleCommandComplete); - } - - private static class CommandInfo { - public final long createTime = SystemClock.elapsedRealtime(); - public final int type; - RecentsAnimationCallbacks mActiveCallbacks; - - CommandInfo(int type) { - this.type = type; - } - - void removeListener(RecentsAnimationListener listener) { - if (mActiveCallbacks != null) { - mActiveCallbacks.removeListener(listener); - } - } - - @NonNull - @Override - public String toString() { - return "CommandInfo(" - + "type=" + type + ", " - + "createTime=" + createTime + ", " - + "mActiveCallbacks=" + mActiveCallbacks - + ")"; - } - } -} diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt new file mode 100644 index 0000000000..f6b9e4e26f --- /dev/null +++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.kt @@ -0,0 +1,484 @@ +/* + * Copyright (C) 2018 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 + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.content.Intent +import android.graphics.PointF +import android.os.SystemClock +import android.os.Trace +import android.util.Log +import android.view.View +import androidx.annotation.BinderThread +import androidx.annotation.UiThread +import com.android.internal.jank.Cuj +import com.android.launcher3.PagedView +import com.android.launcher3.config.FeatureFlags +import com.android.launcher3.logger.LauncherAtom +import com.android.launcher3.logging.StatsLogManager +import com.android.launcher3.logging.StatsLogManager.LauncherEvent.* +import com.android.launcher3.util.Executors +import com.android.launcher3.util.RunnableList +import com.android.quickstep.util.ActiveGestureLog +import com.android.quickstep.views.RecentsView +import com.android.quickstep.views.RecentsViewContainer +import com.android.quickstep.views.TaskView +import com.android.systemui.shared.recents.model.ThumbnailData +import com.android.systemui.shared.system.InteractionJankMonitorWrapper +import java.io.PrintWriter + +/** Helper class to handle various atomic commands for switching between Overview. */ +class OverviewCommandHelper( + private val touchInteractionService: TouchInteractionService, + private val overviewComponentObserver: OverviewComponentObserver, + private val taskAnimationManager: TaskAnimationManager +) { + private val pendingCommands = mutableListOf() + + /** + * Index of the TaskView that should be focused when launching Overview. Persisted so that we do + * not lose the focus across multiple calls of [OverviewCommandHelper.executeCommand] for the + * same command + */ + private var keyboardTaskFocusIndex = -1 + + /** + * Whether we should incoming toggle commands while a previous toggle command is still ongoing. + * This serves as a rate-limiter to prevent overlapping animations that can clobber each other + * and prevent clean-up callbacks from running. This thus prevents a recurring set of bugs with + * janky recents animations and unresponsive home and overview buttons. + */ + private var waitForToggleCommandComplete = false + + /** Called when the command finishes execution. */ + private fun scheduleNextTask(command: CommandInfo) { + if (pendingCommands.isEmpty()) { + Log.d(TAG, "no pending commands to schedule") + return + } + if (pendingCommands.first() !== command) { + Log.d( + TAG, + "next task not scheduled. First pending command type " + + "is ${pendingCommands.first()} - command type is: $command" + ) + return + } + Log.d(TAG, "scheduleNextTask called: $command") + pendingCommands.removeFirst() + executeNext() + } + + /** + * Executes the next command from the queue. If the command finishes immediately (returns true), + * it continues to execute the next command, until the queue is empty of a command defer's its + * completion (returns false). + */ + @UiThread + private fun executeNext() { + if (pendingCommands.isEmpty()) { + Log.d(TAG, "executeNext - pendingCommands is empty") + return + } + val command = pendingCommands.first() + val result = executeCommand(command) + Log.d(TAG, "executeNext command type: $command, result: $result") + if (result) { + scheduleNextTask(command) + } + } + + @UiThread + private fun addCommand(command: CommandInfo) { + val wasEmpty = pendingCommands.isEmpty() + pendingCommands.add(command) + if (wasEmpty) { + executeNext() + } + } + + /** + * Adds a command to be executed next, after all pending tasks are completed. Max commands that + * can be queued is [.MAX_QUEUE_SIZE]. Requests after reaching that limit will be silently + * dropped. + */ + @BinderThread + fun addCommand(type: Int) { + if (pendingCommands.size >= MAX_QUEUE_SIZE) { + Log.d( + TAG, + "the pending command queue is full (${pendingCommands.size}). command not added: $type" + ) + return + } + Log.d(TAG, "adding command type: $type") + val command = CommandInfo(type) + Executors.MAIN_EXECUTOR.execute { addCommand(command) } + } + + @UiThread + fun clearPendingCommands() { + Log.d(TAG, "clearing pending commands - size: ${pendingCommands.size}") + pendingCommands.clear() + } + + @UiThread + fun canStartHomeSafely(): Boolean = + pendingCommands.isEmpty() || pendingCommands.first().type == TYPE_HOME + + private fun getNextTask(view: RecentsView<*, *>): TaskView? { + val runningTaskView = view.runningTaskView + + return if (runningTaskView == null) { + view.getTaskViewAt(0) + } else { + val nextTask = view.nextTaskView + nextTask ?: runningTaskView + } + } + + private fun launchTask( + recents: RecentsView<*, *>, + taskView: TaskView?, + command: CommandInfo + ): Boolean { + var callbackList: RunnableList? = null + if (taskView != null) { + waitForToggleCommandComplete = true + taskView.isEndQuickSwitchCuj = true + callbackList = taskView.launchTasks() + } + + if (callbackList != null) { + callbackList.add { + Log.d(TAG, "launching task callback: $command") + scheduleNextTask(command) + waitForToggleCommandComplete = false + } + Log.d(TAG, "launching task - waiting for callback: $command") + return false + } else { + recents.startHome() + waitForToggleCommandComplete = false + return true + } + } + + /** + * Executes the task and returns true if next task can be executed. If false, then the next task + * is deferred until [.scheduleNextTask] is called + */ + private fun executeCommand(command: CommandInfo): Boolean { + if (waitForToggleCommandComplete && command.type == TYPE_TOGGLE) { + Log.d(TAG, "executeCommand: $command - waiting for toggle command complete") + return true + } + val activityInterface: BaseActivityInterface<*, *> = + overviewComponentObserver.activityInterface + + val visibleRecentsView: RecentsView<*, *>? = + activityInterface.getVisibleRecentsView>() + val createdRecentsView: RecentsView<*, *>? + + Log.d(TAG, "executeCommand: $command - visibleRecentsView: $visibleRecentsView") + if (visibleRecentsView == null) { + val activity = activityInterface.getCreatedContainer() as? RecentsViewContainer + createdRecentsView = activity?.getOverviewPanel() + val deviceProfile = activity?.getDeviceProfile() + val uiController = activityInterface.getTaskbarController() + val allowQuickSwitch = + FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get() && + uiController != null && + deviceProfile != null && + (deviceProfile.isTablet || deviceProfile.isTwoPanels) + + when (command.type) { + TYPE_HIDE -> { + if (!allowQuickSwitch) return true + keyboardTaskFocusIndex = uiController!!.launchFocusedTask() + if (keyboardTaskFocusIndex == -1) return true + } + TYPE_KEYBOARD_INPUT -> + if (allowQuickSwitch) { + uiController!!.openQuickSwitchView() + return true + } else { + keyboardTaskFocusIndex = 0 + } + TYPE_HOME -> { + ActiveGestureLog.INSTANCE.addLog( + "OverviewCommandHelper.executeCommand(TYPE_HOME)" + ) + // Although IActivityTaskManager$Stub$Proxy.startActivity is a slow binder call, + // we should still call it on main thread because launcher is waiting for + // ActivityTaskManager to resume it. Also calling startActivity() on bg thread + // could potentially delay resuming launcher. See b/348668521 for more details. + touchInteractionService.startActivity(overviewComponentObserver.homeIntent) + return true + } + TYPE_SHOW -> + // When Recents is not currently visible, the command's type is + // TYPE_SHOW + // when overview is triggered via the keyboard overview button or Action+Tab + // keys (Not Alt+Tab which is KQS). The overview button on-screen in 3-button + // nav is TYPE_TOGGLE. + keyboardTaskFocusIndex = 0 + else -> {} + } + } else { + createdRecentsView = visibleRecentsView + when (command.type) { + TYPE_SHOW -> return true // already visible + TYPE_KEYBOARD_INPUT, + TYPE_HIDE -> { + if (visibleRecentsView.isHandlingTouch) return true + + keyboardTaskFocusIndex = PagedView.INVALID_PAGE + val currentPage = visibleRecentsView.nextPage + val taskView = visibleRecentsView.getTaskViewAt(currentPage) + return launchTask(visibleRecentsView, taskView, command) + } + TYPE_TOGGLE -> + return launchTask(visibleRecentsView, getNextTask(visibleRecentsView), command) + TYPE_HOME -> { + visibleRecentsView.startHome() + return true + } + } + } + + createdRecentsView?.setKeyboardTaskFocusIndex(keyboardTaskFocusIndex) + // Handle recents view focus when launching from home + val animatorListener: Animator.AnimatorListener = + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + super.onAnimationStart(animation) + updateRecentsViewFocus(command) + logShowOverviewFrom(command.type) + } + + override fun onAnimationEnd(animation: Animator) { + Log.d(TAG, "switching to Overview state - onAnimationEnd: $command") + super.onAnimationEnd(animation) + onRecentsViewFocusUpdated(command) + scheduleNextTask(command) + } + } + if (activityInterface.switchToRecentsIfVisible(animatorListener)) { + Log.d(TAG, "switching to Overview state - waiting: $command") + // If successfully switched, wait until animation finishes + return false + } + + val activity = activityInterface.getCreatedContainer() + if (activity != null) { + InteractionJankMonitorWrapper.begin(activity.rootView, Cuj.CUJ_LAUNCHER_QUICK_SWITCH) + } + + val gestureState = + touchInteractionService.createGestureState( + GestureState.DEFAULT_STATE, + GestureState.TrackpadGestureType.NONE + ) + gestureState.isHandlingAtomicEvent = true + val interactionHandler = + touchInteractionService.swipeUpHandlerFactory.newHandler( + gestureState, + command.createTime + ) + interactionHandler.setGestureEndCallback { + onTransitionComplete(command, interactionHandler) + } + interactionHandler.initWhenReady("OverviewCommandHelper: command.type=${command.type}") + + val recentAnimListener: RecentsAnimationCallbacks.RecentsAnimationListener = + object : RecentsAnimationCallbacks.RecentsAnimationListener { + override fun onRecentsAnimationStart( + controller: RecentsAnimationController, + targets: RecentsAnimationTargets + ) { + updateRecentsViewFocus(command) + logShowOverviewFrom(command.type) + activityInterface.runOnInitBackgroundStateUI { + interactionHandler.onGestureEnded(0f, PointF()) + } + command.removeListener(this) + } + + override fun onRecentsAnimationCanceled( + thumbnailDatas: HashMap + ) { + interactionHandler.onGestureCancelled() + command.removeListener(this) + + activityInterface.getCreatedContainer() ?: return + createdRecentsView?.onRecentsAnimationComplete() + } + } + + // TODO(b/361768912): Dead code. Remove or update after this bug is fixed. + // if (visibleRecentsView != null) { + // visibleRecentsView.moveRunningTaskToFront(); + // } + + if (taskAnimationManager.isRecentsAnimationRunning) { + command.setAnimationCallbacks( + taskAnimationManager.continueRecentsAnimation(gestureState) + ) + command.addListener(interactionHandler) + taskAnimationManager.notifyRecentsAnimationState(interactionHandler) + interactionHandler.onGestureStarted(true /*isLikelyToStartNewTask*/) + + command.addListener(recentAnimListener) + taskAnimationManager.notifyRecentsAnimationState(recentAnimListener) + } else { + val intent = + Intent(interactionHandler.launchIntent) + .putExtra(ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID, gestureState.gestureId) + command.setAnimationCallbacks( + taskAnimationManager.startRecentsAnimation(gestureState, intent, interactionHandler) + ) + interactionHandler.onGestureStarted(false /*isLikelyToStartNewTask*/) + command.addListener(recentAnimListener) + } + Trace.beginAsyncSection(TRANSITION_NAME, 0) + Log.d(TAG, "switching via recents animation - onGestureStarted: $command") + return false + } + + private fun onTransitionComplete(command: CommandInfo, handler: AbsSwipeUpHandler<*, *, *>) { + Log.d(TAG, "switching via recents animation - onTransitionComplete: $command") + command.removeListener(handler) + Trace.endAsyncSection(TRANSITION_NAME, 0) + onRecentsViewFocusUpdated(command) + scheduleNextTask(command) + } + + private fun updateRecentsViewFocus(command: CommandInfo) { + val recentsView: RecentsView<*, *> = + overviewComponentObserver.activityInterface.getVisibleRecentsView() ?: return + if ( + command.type != TYPE_KEYBOARD_INPUT && + command.type != TYPE_HIDE && + command.type != TYPE_SHOW + ) { + return + } + + // When the overview is launched via alt tab (command type is TYPE_KEYBOARD_INPUT), + // the touch mode somehow is not change to false by the Android framework. + // The subsequent tab to go through tasks in overview can only be dispatched to + // focuses views, while focus can only be requested in + // {@link View#requestFocusNoSearch(int, Rect)} when touch mode is false. To note, + // here we launch overview with live tile. + recentsView.viewRootImpl.touchModeChanged(false) + // Ensure that recents view has focus so that it receives the followup key inputs + if (requestFocus(recentsView.getTaskViewAt(keyboardTaskFocusIndex))) return + if (requestFocus(recentsView.nextTaskView)) return + if (requestFocus(recentsView.getTaskViewAt(0))) return + requestFocus(recentsView) + } + + private fun onRecentsViewFocusUpdated(command: CommandInfo) { + val recentsView: RecentsView<*, *> = + overviewComponentObserver.activityInterface.getVisibleRecentsView() ?: return + if (command.type != TYPE_HIDE || keyboardTaskFocusIndex == PagedView.INVALID_PAGE) { + return + } + recentsView.setKeyboardTaskFocusIndex(PagedView.INVALID_PAGE) + recentsView.currentPage = keyboardTaskFocusIndex + keyboardTaskFocusIndex = PagedView.INVALID_PAGE + } + + private fun requestFocus(taskView: View?): Boolean { + if (taskView == null) return false + taskView.post { + taskView.requestFocus() + taskView.requestAccessibilityFocus() + } + return true + } + + private fun logShowOverviewFrom(commandType: Int) { + val activityInterface: BaseActivityInterface<*, *> = + overviewComponentObserver.activityInterface + val container = activityInterface.getCreatedContainer() as? RecentsViewContainer ?: return + val event = + when (commandType) { + TYPE_SHOW -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_SHORTCUT + TYPE_HIDE -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_KEYBOARD_QUICK_SWITCH + TYPE_TOGGLE -> LAUNCHER_OVERVIEW_SHOW_OVERVIEW_FROM_3_BUTTON + else -> return + } + StatsLogManager.newInstance(container.asContext()) + .logger() + .withContainerInfo( + LauncherAtom.ContainerInfo.newBuilder() + .setTaskSwitcherContainer( + LauncherAtom.TaskSwitcherContainer.getDefaultInstance() + ) + .build() + ) + .log(event) + } + + fun dump(pw: PrintWriter) { + pw.println("OverviewCommandHelper:") + pw.println(" pendingCommands=${pendingCommands.size}") + if (pendingCommands.isNotEmpty()) { + pw.println(" pendingCommandType=${pendingCommands.first().type}") + } + pw.println(" mKeyboardTaskFocusIndex=$keyboardTaskFocusIndex") + pw.println(" mWaitForToggleCommandComplete=$waitForToggleCommandComplete") + } + + private data class CommandInfo( + val type: Int, + val createTime: Long = SystemClock.elapsedRealtime(), + private var animationCallbacks: RecentsAnimationCallbacks? = null + ) { + fun setAnimationCallbacks(recentsAnimationCallbacks: RecentsAnimationCallbacks) { + this.animationCallbacks = recentsAnimationCallbacks + } + + fun addListener(listener: RecentsAnimationCallbacks.RecentsAnimationListener) { + animationCallbacks?.addListener(listener) + } + + fun removeListener(listener: RecentsAnimationCallbacks.RecentsAnimationListener?) { + animationCallbacks?.removeListener(listener) + } + } + + companion object { + private const val TAG = "OverviewCommandHelper" + + const val TYPE_SHOW: Int = 1 + const val TYPE_KEYBOARD_INPUT: Int = 2 + const val TYPE_HIDE: Int = 3 + const val TYPE_TOGGLE: Int = 4 + const val TYPE_HOME: Int = 5 + + /** + * Use case for needing a queue is double tapping recents button in 3 button nav. Size of 2 + * should be enough. We'll toss in one more because we're kind hearted. + */ + private const val MAX_QUEUE_SIZE = 3 + + private const val TRANSITION_NAME = "Transition:toOverview" + } +}