3ef91e087d
- Even without multiple desktops, apps that are moved to connected display show up in separate desktop task. - Further changes may be required to launch a desktop after multiple desktops is implemented. Bug: 382762871 Flag: com.android.launcher3.enable_alt_tab_kqs_on_connected_displays Test: manually built and run the CUJ Change-Id: If104c67d95a1ec3de404998b06352edfa02c0f09
440 lines
18 KiB
Java
440 lines
18 KiB
Java
/*
|
|
* 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.launcher3.taskbar;
|
|
|
|
import static com.android.launcher3.Flags.enableAltTabKqsOnConnectedDisplays;
|
|
|
|
import android.content.ComponentName;
|
|
import android.content.pm.ActivityInfo;
|
|
import android.view.MotionEvent;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.VisibleForTesting;
|
|
|
|
import com.android.launcher3.Flags;
|
|
import com.android.launcher3.R;
|
|
import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
|
|
import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer;
|
|
import com.android.launcher3.util.TouchController;
|
|
import com.android.quickstep.RecentsFilterState;
|
|
import com.android.quickstep.RecentsModel;
|
|
import com.android.quickstep.util.DesktopTask;
|
|
import com.android.quickstep.util.GroupTask;
|
|
import com.android.quickstep.util.LayoutUtils;
|
|
import com.android.quickstep.util.SingleTask;
|
|
import com.android.systemui.shared.recents.model.Task;
|
|
import com.android.systemui.shared.recents.model.ThumbnailData;
|
|
import com.android.systemui.shared.system.ActivityManagerWrapper;
|
|
|
|
import java.io.PrintWriter;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import java.util.function.Consumer;
|
|
import java.util.stream.Collectors;
|
|
|
|
/**
|
|
* Handles initialization of the {@link KeyboardQuickSwitchViewController}.
|
|
*/
|
|
public final class KeyboardQuickSwitchController implements
|
|
TaskbarControllers.LoggableTaskbarController, TouchController {
|
|
|
|
@VisibleForTesting
|
|
public static final int MAX_TASKS = 6;
|
|
|
|
@NonNull private final ControllerCallbacks mControllerCallbacks = new ControllerCallbacks();
|
|
// Callback used to notify when the KQS view is closed.
|
|
@Nullable private Runnable mOnClosed;
|
|
|
|
// Initialized on init
|
|
@Nullable private RecentsModel mModel;
|
|
|
|
// Used to keep track of the last requested task list id, so that we do not request to load the
|
|
// tasks again if we have already requested it and the task list has not changed
|
|
private int mTaskListChangeId = -1;
|
|
// Only empty before the recent tasks list has been loaded the first time
|
|
@NonNull private List<GroupTask> mTasks = new ArrayList<>();
|
|
// Set of task IDs filtered out of tasks in recents model to generate list of tasks to show in
|
|
// the Keyboard Quick Switch view. Non empty only if the view has been shown in response to
|
|
// toggling taskbar overflow button.
|
|
@NonNull private Set<Integer> mExcludedTaskIds = Collections.emptySet();
|
|
|
|
private int mNumHiddenTasks = 0;
|
|
|
|
// Initialized in init
|
|
private TaskbarControllers mControllers;
|
|
|
|
@Nullable private KeyboardQuickSwitchViewController mQuickSwitchViewController;
|
|
@Nullable private TaskbarOverlayContext mOverlayContext;
|
|
|
|
private boolean mHasDesktopTask = false;
|
|
private boolean mWasDesktopTaskFilteredOut = false;
|
|
|
|
/** Initialize the controller. */
|
|
public void init(@NonNull TaskbarControllers controllers) {
|
|
mControllers = controllers;
|
|
mModel = RecentsModel.INSTANCE.get(controllers.taskbarActivityContext);
|
|
}
|
|
|
|
void onConfigurationChanged(@ActivityInfo.Config int configChanges) {
|
|
if (mQuickSwitchViewController == null) {
|
|
return;
|
|
}
|
|
if ((configChanges & (ActivityInfo.CONFIG_KEYBOARD
|
|
| ActivityInfo.CONFIG_KEYBOARD_HIDDEN)) != 0) {
|
|
mQuickSwitchViewController.closeQuickSwitchView(true);
|
|
return;
|
|
}
|
|
int currentFocusedIndex = mQuickSwitchViewController.getCurrentFocusedIndex();
|
|
boolean wasOpenedFromTaskbar = mQuickSwitchViewController.wasOpenedFromTaskbar();
|
|
onDestroy();
|
|
if (currentFocusedIndex != -1) {
|
|
mControllers.taskbarActivityContext.getMainThreadHandler().post(
|
|
() -> openQuickSwitchView(currentFocusedIndex, mExcludedTaskIds,
|
|
wasOpenedFromTaskbar));
|
|
}
|
|
}
|
|
|
|
void openQuickSwitchView() {
|
|
openQuickSwitchView(-1);
|
|
}
|
|
|
|
/**
|
|
* Opens or closes the view in response to taskbar action. The view shows a filtered list of
|
|
* tasks.
|
|
* @param taskIdsToExclude A list of tasks to exclude in the opened view.
|
|
* @param onClosed A callback used to notify when the KQS view is closed.
|
|
*/
|
|
void toggleQuickSwitchViewForTaskbar(@NonNull Set<Integer> taskIdsToExclude,
|
|
@NonNull Runnable onClosed) {
|
|
mOnClosed = onClosed;
|
|
|
|
// Close the view if its shown, and was opened from the taskbar.
|
|
if (mQuickSwitchViewController != null
|
|
&& !mQuickSwitchViewController.isCloseAnimationRunning()
|
|
&& mQuickSwitchViewController.wasOpenedFromTaskbar()) {
|
|
closeQuickSwitchView(true);
|
|
return;
|
|
}
|
|
|
|
openQuickSwitchView(-1, taskIdsToExclude, true);
|
|
}
|
|
|
|
private void openQuickSwitchView(int currentFocusedIndex) {
|
|
openQuickSwitchView(currentFocusedIndex, Collections.emptySet(), false);
|
|
}
|
|
|
|
private void openQuickSwitchView(int currentFocusedIndex,
|
|
@NonNull Set<Integer> taskIdsToExclude,
|
|
boolean wasOpenedFromTaskbar) {
|
|
if (mQuickSwitchViewController != null) {
|
|
if (!mQuickSwitchViewController.isCloseAnimationRunning()) {
|
|
if (mQuickSwitchViewController.wasOpenedFromTaskbar() == wasOpenedFromTaskbar) {
|
|
return;
|
|
}
|
|
|
|
// Relayout the KQS view instead of recreating a new one if it is the current
|
|
// trigger surface is different than the previous one.
|
|
final int currentFocusIndexOverride =
|
|
currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning()
|
|
? 0 : currentFocusedIndex;
|
|
|
|
// Skip the task reload if the list is not changed.
|
|
if (!mModel.isTaskListValid(mTaskListChangeId) || !taskIdsToExclude.equals(
|
|
mExcludedTaskIds)) {
|
|
final boolean shouldShowDesktopTasks = mControllers.taskbarDesktopModeController
|
|
.shouldShowDesktopTasksInTaskbar();
|
|
mExcludedTaskIds = taskIdsToExclude;
|
|
mTaskListChangeId = mModel.getTasks((tasks) -> {
|
|
processLoadedTasks(tasks, taskIdsToExclude);
|
|
mQuickSwitchViewController.updateQuickSwitchView(
|
|
mTasks,
|
|
mNumHiddenTasks,
|
|
currentFocusIndexOverride,
|
|
mHasDesktopTask,
|
|
mWasDesktopTaskFilteredOut);
|
|
}, shouldShowDesktopTasks ? RecentsFilterState.EMPTY_FILTER
|
|
: RecentsFilterState.getEmptyDesktopTaskFilter());
|
|
}
|
|
|
|
mQuickSwitchViewController.updateLayoutForSurface(wasOpenedFromTaskbar,
|
|
currentFocusIndexOverride);
|
|
return;
|
|
} else {
|
|
// Allow the KQS to be reopened during the close animation to make it more
|
|
// responsive.
|
|
closeQuickSwitchView(false);
|
|
}
|
|
}
|
|
|
|
mOverlayContext = mControllers.taskbarOverlayController.requestWindow();
|
|
if (Flags.taskbarOverflow()) {
|
|
mOverlayContext.getDragLayer().addTouchController(this);
|
|
}
|
|
KeyboardQuickSwitchView keyboardQuickSwitchView =
|
|
(KeyboardQuickSwitchView) mOverlayContext.getLayoutInflater()
|
|
.inflate(
|
|
R.layout.keyboard_quick_switch_view,
|
|
mOverlayContext.getDragLayer(),
|
|
/* attachToRoot= */ false);
|
|
mQuickSwitchViewController = new KeyboardQuickSwitchViewController(
|
|
mControllers, mOverlayContext, keyboardQuickSwitchView, mControllerCallbacks);
|
|
|
|
final boolean shouldShowDesktopTasks = mControllers.taskbarDesktopModeController
|
|
.shouldShowDesktopTasksInTaskbar();
|
|
|
|
if (mModel.isTaskListValid(mTaskListChangeId)
|
|
&& taskIdsToExclude.equals(mExcludedTaskIds)) {
|
|
// When we are opening the KQS with no focus override, check if the first task is
|
|
// running. If not, focus that first task.
|
|
mQuickSwitchViewController.openQuickSwitchView(
|
|
mTasks,
|
|
mNumHiddenTasks,
|
|
/* updateTasks= */ false,
|
|
currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning()
|
|
? 0 : currentFocusedIndex,
|
|
shouldShowDesktopTasks,
|
|
mHasDesktopTask,
|
|
mWasDesktopTaskFilteredOut,
|
|
wasOpenedFromTaskbar);
|
|
return;
|
|
}
|
|
|
|
mExcludedTaskIds = taskIdsToExclude;
|
|
mTaskListChangeId = mModel.getTasks((tasks) -> {
|
|
processLoadedTasks(tasks, taskIdsToExclude);
|
|
// Check if the first task is running after the recents model has updated so that we use
|
|
// the correct index.
|
|
mQuickSwitchViewController.openQuickSwitchView(
|
|
mTasks,
|
|
mNumHiddenTasks,
|
|
/* updateTasks= */ true,
|
|
currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning()
|
|
? 0 : currentFocusedIndex,
|
|
shouldShowDesktopTasks,
|
|
mHasDesktopTask,
|
|
mWasDesktopTaskFilteredOut,
|
|
wasOpenedFromTaskbar);
|
|
}, shouldShowDesktopTasks ? RecentsFilterState.EMPTY_FILTER
|
|
: RecentsFilterState.getEmptyDesktopTaskFilter());
|
|
}
|
|
|
|
private boolean shouldExcludeTask(GroupTask task, Set<Integer> taskIdsToExclude) {
|
|
return Flags.taskbarOverflow() && task.getTasks().stream().anyMatch(
|
|
t -> taskIdsToExclude.contains(t.key.id));
|
|
}
|
|
|
|
private void processLoadedTasks(List<GroupTask> tasks, Set<Integer> taskIdsToExclude) {
|
|
mHasDesktopTask = false;
|
|
mWasDesktopTaskFilteredOut = false;
|
|
if (mControllers.taskbarDesktopModeController.shouldShowDesktopTasksInTaskbar()) {
|
|
processLoadedTasksOnDesktop(tasks, taskIdsToExclude);
|
|
} else {
|
|
processLoadedTasksOutsideDesktop(tasks, taskIdsToExclude);
|
|
}
|
|
}
|
|
|
|
private void processLoadedTasksOutsideDesktop(List<GroupTask> tasks,
|
|
Set<Integer> taskIdsToExclude) {
|
|
// Only store MAX_TASK tasks, from most to least recent
|
|
Collections.reverse(tasks);
|
|
mTasks = tasks.stream()
|
|
.filter(task -> !(task instanceof DesktopTask)
|
|
&& !shouldExcludeTask(task, taskIdsToExclude))
|
|
.limit(MAX_TASKS)
|
|
.collect(Collectors.toList());
|
|
|
|
for (int i = 0; i < tasks.size(); i++) {
|
|
if (tasks.get(i) instanceof DesktopTask) {
|
|
mHasDesktopTask = true;
|
|
if (i < mTasks.size()) {
|
|
mWasDesktopTaskFilteredOut = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
mNumHiddenTasks = Math.max(0,
|
|
tasks.size() - (mWasDesktopTaskFilteredOut ? 1 : 0) - MAX_TASKS);
|
|
}
|
|
|
|
private void processLoadedTasksOnDesktop(List<GroupTask> tasks, Set<Integer> taskIdsToExclude) {
|
|
// Find all desktop tasks.
|
|
List<DesktopTask> desktopTasks = tasks.stream()
|
|
.filter(t -> t instanceof DesktopTask)
|
|
.map(t -> (DesktopTask) t)
|
|
.toList();
|
|
|
|
// Apps on the connected displays seem to be in different Desktop tasks even with the
|
|
// multiple desktops flag disabled. So, until multiple desktops is implemented the following
|
|
// should help with team-fooding Alt+tab on connected displays. Post multiple desktop,
|
|
// further changes maybe required to support launching selected desktops.
|
|
if (enableAltTabKqsOnConnectedDisplays()) {
|
|
mTasks = desktopTasks.stream()
|
|
.flatMap(t -> t.getTasks().stream())
|
|
.map(SingleTask::new)
|
|
.filter(task -> !shouldExcludeTask(task, taskIdsToExclude))
|
|
.collect(Collectors.toList());
|
|
|
|
mNumHiddenTasks = Math.max(0, tasks.size() - desktopTasks.size());
|
|
} else if (!desktopTasks.isEmpty()) {
|
|
mTasks = desktopTasks.get(0).getTasks().stream()
|
|
.map(SingleTask::new)
|
|
.filter(task -> !shouldExcludeTask(task, taskIdsToExclude))
|
|
.collect(Collectors.toList());
|
|
// All other tasks, apart from the grouped desktop task, are hidden
|
|
mNumHiddenTasks = Math.max(0, tasks.size() - 1);
|
|
} else {
|
|
// Desktop tasks were visible, but the recents entry is missing. Fall back to empty list
|
|
mTasks = Collections.emptyList();
|
|
mNumHiddenTasks = tasks.size();
|
|
}
|
|
}
|
|
|
|
void closeQuickSwitchView() {
|
|
closeQuickSwitchView(true);
|
|
}
|
|
|
|
void closeQuickSwitchView(boolean animate) {
|
|
if (mQuickSwitchViewController == null) {
|
|
return;
|
|
}
|
|
mQuickSwitchViewController.closeQuickSwitchView(animate);
|
|
}
|
|
|
|
/**
|
|
* See {@link TaskbarUIController#launchFocusedTask()}
|
|
*/
|
|
int launchFocusedTask() {
|
|
// Return -1 so that the RecentsView is not incorrectly opened when the user closes the
|
|
// quick switch view by tapping the screen or when there are no recent tasks.
|
|
return mQuickSwitchViewController == null || mTasks.isEmpty()
|
|
? -1 : mQuickSwitchViewController.launchFocusedTask();
|
|
}
|
|
|
|
@Override
|
|
public boolean onControllerTouchEvent(MotionEvent ev) {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
|
|
if (mQuickSwitchViewController == null
|
|
|| mOverlayContext == null
|
|
|| !Flags.taskbarOverflow()) {
|
|
return false;
|
|
}
|
|
|
|
TaskbarOverlayDragLayer dragLayer = mOverlayContext.getDragLayer();
|
|
if (ev.getAction() == MotionEvent.ACTION_DOWN
|
|
&& !mQuickSwitchViewController.isEventOverKeyboardQuickSwitch(dragLayer, ev)) {
|
|
closeQuickSwitchView(true);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void onDestroy() {
|
|
if (mQuickSwitchViewController != null) {
|
|
mQuickSwitchViewController.onDestroy();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void dumpLogs(String prefix, PrintWriter pw) {
|
|
pw.println(prefix + "KeyboardQuickSwitchController:");
|
|
|
|
pw.println(prefix + "\tisOpen=" + (mQuickSwitchViewController != null));
|
|
pw.println(prefix + "\tmNumHiddenTasks=" + mNumHiddenTasks);
|
|
pw.println(prefix + "\tmTaskListChangeId=" + mTaskListChangeId);
|
|
pw.println(prefix + "\tmHasDesktopTask=" + mHasDesktopTask);
|
|
pw.println(prefix + "\tmWasDesktopTaskFilteredOut=" + mWasDesktopTaskFilteredOut);
|
|
pw.println(prefix + "\tmTasks=[");
|
|
for (GroupTask task : mTasks) {
|
|
int count = 0;
|
|
for (Task t : task.getTasks()) {
|
|
ComponentName cn = t.getTopComponent();
|
|
pw.println(prefix + "\t\tt" + (++count) + ": (id=" + t.key.id
|
|
+ "; package=" + (cn != null ? cn.getPackageName() + ")" : "no package)"));
|
|
}
|
|
}
|
|
pw.println(prefix + "\t]");
|
|
|
|
if (mQuickSwitchViewController != null) {
|
|
mQuickSwitchViewController.dumpLogs(prefix + '\t', pw);
|
|
}
|
|
}
|
|
|
|
class ControllerCallbacks {
|
|
|
|
@Nullable
|
|
GroupTask getTaskAt(int index) {
|
|
return index < 0 || index >= mTasks.size() ? null : mTasks.get(index);
|
|
}
|
|
|
|
void updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback) {
|
|
mModel.getThumbnailCache().getThumbnailInBackground(task,
|
|
thumbnailData -> {
|
|
task.thumbnail = thumbnailData;
|
|
callback.accept(thumbnailData);
|
|
});
|
|
}
|
|
|
|
void updateIconInBackground(Task task, Consumer<Task> callback) {
|
|
mModel.getIconCache().getIconInBackground(task, (icon, contentDescription, title) -> {
|
|
task.icon = icon;
|
|
task.titleDescription = contentDescription;
|
|
task.title = title;
|
|
callback.accept(task);
|
|
});
|
|
}
|
|
|
|
void onCloseStarted() {
|
|
if (mOnClosed != null) {
|
|
mOnClosed.run();
|
|
mOnClosed = null;
|
|
}
|
|
}
|
|
|
|
void onCloseComplete() {
|
|
if (Flags.taskbarOverflow() && mOverlayContext != null) {
|
|
mOverlayContext.getDragLayer()
|
|
.removeTouchController(KeyboardQuickSwitchController.this);
|
|
}
|
|
mOverlayContext = null;
|
|
mQuickSwitchViewController = null;
|
|
}
|
|
|
|
boolean isTaskRunning(@Nullable GroupTask task) {
|
|
if (task == null) {
|
|
return false;
|
|
}
|
|
int runningTaskId = ActivityManagerWrapper.getInstance().getRunningTask().taskId;
|
|
return task.containsTask(runningTaskId);
|
|
}
|
|
|
|
boolean isFirstTaskRunning() {
|
|
return isTaskRunning(getTaskAt(0));
|
|
}
|
|
|
|
boolean isAspectRatioSquare() {
|
|
return mControllers != null && LayoutUtils.isAspectRatioSquare(
|
|
mControllers.taskbarActivityContext.getDeviceProfile().aspectRatio);
|
|
}
|
|
}
|
|
}
|