Files
lawnchair/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
Ats Jenk 3067874559 Support alt+tab for desktop tasks
Updates keyboard quickswitch logic handle quickswitching while on
desktop. When on desktop, alt+tab moves only between desktop tasks.
Fullscreen tasks are shown in the overflow tile and can be accessed by
going to overview.

TODO:
- when not in desktop and using quickswitch, show the desktop tile as a
  combined tile in the row, similar to what is shown in overview

Flag: persist.wm.debug.desktop_mode_2
Bug: 280468885
Test: open some apps on desktop and have some fullscreen apps opened in
  the background, when on desktop, observe that alt+tab only switches
  between desktop tasks
Test: same setup, but switch to a fullscreen app, observe that alt+tab
  only switches between fullscreen tasks and desktop is accessible from
  overview

Change-Id: Ib19f2eaa24363bbd0669c8c8c3d99ed9d9118a17
2023-05-26 15:38:58 -07:00

375 lines
14 KiB
Java

/*
* 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.util.Executors.MAIN_EXECUTOR;
import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
import android.annotation.TargetApi;
import android.content.Intent;
import android.graphics.PointF;
import android.os.Build;
import android.os.SystemClock;
import android.os.Trace;
import android.view.View;
import androidx.annotation.BinderThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.config.FeatureFlags;
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.views.RecentsView;
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.
*/
@TargetApi(Build.VERSION_CODES.P)
public class 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 mTaskFocusIndexOverride = -1;
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() && mPendingCommands.get(0) == 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()) {
return;
}
CommandInfo cmd = mPendingCommands.get(0);
if (executeCommand(cmd)) {
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) {
return;
}
CommandInfo cmd = new CommandInfo(type);
MAIN_EXECUTOR.execute(() -> addCommand(cmd));
}
@UiThread
public void clearPendingCommands() {
mPendingCommands.clear();
}
@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) {
taskView.setEndQuickswitchCuj(true);
callbackList = taskView.launchTasks();
}
if (callbackList != null) {
callbackList.add(() -> scheduleNextTask(cmd));
return false;
} else {
recents.startHome();
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<?>> boolean executeCommand(CommandInfo cmd) {
BaseActivityInterface<?, T> activityInterface =
mOverviewComponentObserver.getActivityInterface();
RecentsView recents = activityInterface.getVisibleRecentsView();
if (recents == null) {
T activity = activityInterface.getCreatedActivity();
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);
if (cmd.type == TYPE_HIDE) {
if (!allowQuickSwitch) {
return true;
}
mTaskFocusIndexOverride = uiController.launchFocusedTask();
if (mTaskFocusIndexOverride == -1) {
return true;
}
}
if (cmd.type == TYPE_KEYBOARD_INPUT && allowQuickSwitch) {
uiController.openQuickSwitchView();
return true;
}
if (cmd.type == TYPE_HOME) {
mService.startActivity(mOverviewComponentObserver.getHomeIntent());
return true;
}
} else {
switch (cmd.type) {
case TYPE_SHOW:
// already visible
return true;
case TYPE_HIDE: {
mTaskFocusIndexOverride = -1;
int currentPage = recents.getNextPage();
TaskView tv = (currentPage >= 0 && currentPage < recents.getTaskViewCount())
? (TaskView) recents.getPageAt(currentPage)
: null;
return launchTask(recents, tv, cmd);
}
case TYPE_TOGGLE:
return launchTask(recents, getNextTask(recents), cmd);
case TYPE_HOME:
recents.startHome();
return true;
}
}
final Runnable completeCallback = () -> {
RecentsView rv = activityInterface.getVisibleRecentsView();
if (rv != null && (cmd.type == TYPE_KEYBOARD_INPUT || cmd.type == TYPE_HIDE)) {
updateRecentsViewFocus(rv);
}
scheduleNextTask(cmd);
};
if (activityInterface.switchToRecentsIfVisible(completeCallback)) {
// If successfully switched, wait until animation finishes
return false;
}
final T activity = activityInterface.getCreatedActivity();
if (activity != null) {
InteractionJankMonitorWrapper.begin(
activity.getRootView(),
InteractionJankMonitorWrapper.CUJ_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();
RecentsAnimationListener recentAnimListener = new RecentsAnimationListener() {
@Override
public void onRecentsAnimationStart(RecentsAnimationController controller,
RecentsAnimationTargets targets) {
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.getCreatedActivity();
if (createdActivity == null) {
return;
}
RecentsView createdRecents = createdActivity.getOverviewPanel();
if (createdRecents != null) {
createdRecents.onRecentsAnimationComplete();
}
}
};
RecentsView<?, ?> visibleRecentsView = activityInterface.getVisibleRecentsView();
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);
return false;
}
private void onTransitionComplete(CommandInfo cmd, AbsSwipeUpHandler handler) {
cmd.removeListener(handler);
Trace.endAsyncSection(TRANSITION_NAME, 0);
RecentsView rv =
mOverviewComponentObserver.getActivityInterface().getVisibleRecentsView();
if (rv != null && (cmd.type == TYPE_KEYBOARD_INPUT || cmd.type == TYPE_HIDE)) {
updateRecentsViewFocus(rv);
}
scheduleNextTask(cmd);
}
private void updateRecentsViewFocus(@NonNull RecentsView rv) {
// 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.
rv.getViewRootImpl().touchModeChanged(false);
// Ensure that recents view has focus so that it receives the followup key inputs
TaskView taskView = rv.getTaskViewAt(mTaskFocusIndexOverride);
if (taskView != null) {
requestFocus(taskView);
return;
}
taskView = rv.getNextTaskView();
if (taskView != null) {
requestFocus(taskView);
return;
}
taskView = rv.getTaskViewAt(0);
if (taskView != null) {
requestFocus(taskView);
return;
}
requestFocus(rv);
}
private void requestFocus(@NonNull View view) {
view.post(() -> {
view.requestFocus();
view.requestAccessibilityFocus();
});
}
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(" mTaskFocusIndexOverride=" + mTaskFocusIndexOverride);
}
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);
}
}
}
}