Merge "Convert OverviewCommandHelper to Kotlin" into main
This commit is contained in:
committed by
Android (Google) Code Review
commit
565f7e73b5
@@ -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<CommandInfo> 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 <T extends StatefulActivity<?> & RecentsViewContainer> boolean executeCommand(
|
||||
CommandInfo cmd) {
|
||||
if (mWaitForToggleCommandComplete && cmd.type == TYPE_TOGGLE) {
|
||||
Log.d(TAG, "executeCommand: " + cmd
|
||||
+ " - waiting for toggle command complete");
|
||||
return true;
|
||||
}
|
||||
BaseActivityInterface<?, T> 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<Integer, ThumbnailData> 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 <T extends StatefulActivity<?> & RecentsViewContainer>
|
||||
void logShowOverviewFrom(int cmdType) {
|
||||
BaseActivityInterface<?, T> 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
|
||||
+ ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<CommandInfo>()
|
||||
|
||||
/**
|
||||
* 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<RecentsView<*, *>>()
|
||||
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<Int, ThumbnailData>
|
||||
) {
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user