/* * 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 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.RecentsModel; import com.android.quickstep.util.DesktopTask; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.LayoutUtils; 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(); // 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 mTasks = new ArrayList<>(); 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(); onDestroy(); if (currentFocusedIndex != -1) { mControllers.taskbarActivityContext.getMainThreadHandler().post( () -> openQuickSwitchView(currentFocusedIndex)); } } void openQuickSwitchView() { openQuickSwitchView(-1); } /** * Opens the view with a filtered list of tasks. * @param taskIdsToExclude A list of tasks to exclude in the opened view. */ void openQuickSwitchView(@NonNull Set taskIdsToExclude) { openQuickSwitchView(-1, taskIdsToExclude, true); } private void openQuickSwitchView(int currentFocusedIndex) { openQuickSwitchView(currentFocusedIndex, Collections.emptySet(), false); } private void openQuickSwitchView(int currentFocusedIndex, @NonNull Set taskIdsToExclude, boolean wasOpenedFromTaskbar) { if (mQuickSwitchViewController != null) { if (!mQuickSwitchViewController.isCloseAnimationRunning()) { return; } // 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 onDesktop = mControllers.taskbarDesktopModeController.getAreDesktopTasksVisible(); // TODO(b/368119679) For now we will re-process the task list every time, but this can be // optimized if we have the same set of task ids to exclude. if (mModel.isTaskListValid(mTaskListChangeId) && !Flags.taskbarOverflow()) { // 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, onDesktop, mHasDesktopTask, mWasDesktopTaskFilteredOut, wasOpenedFromTaskbar); return; } mTaskListChangeId = mModel.getTasks((tasks) -> { mHasDesktopTask = false; mWasDesktopTaskFilteredOut = false; if (onDesktop) { processLoadedTasksOnDesktop(tasks, taskIdsToExclude); } else { 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, onDesktop, mHasDesktopTask, mWasDesktopTaskFilteredOut, wasOpenedFromTaskbar); }); } private boolean shouldExcludeTask(GroupTask task, Set taskIdsToExclude) { return Flags.taskbarOverflow() && taskIdsToExclude.contains(task.task1.key.id); } private void processLoadedTasks(List tasks, Set 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 tasks, Set taskIdsToExclude) { // Find the single desktop task that contains a grouping of desktop tasks DesktopTask desktopTask = findDesktopTask(tasks); if (desktopTask != null) { mTasks = desktopTask.tasks.stream() .map(GroupTask::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(); } } @Nullable private DesktopTask findDesktopTask(List tasks) { return (DesktopTask) tasks.stream() .filter(t -> t instanceof DesktopTask) .findFirst() .orElse(null); } 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) { Task task1 = task.task1; Task task2 = task.task2; ComponentName cn1 = task1.getTopComponent(); ComponentName cn2 = task2 != null ? task2.getTopComponent() : null; pw.println(prefix + "\t\tt1: (id=" + task1.key.id + "; package=" + (cn1 != null ? cn1.getPackageName() + ")" : "no package)") + " t2: (id=" + (task2 != null ? task2.key.id : "-1") + "; package=" + (cn2 != null ? cn2.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 callback) { mModel.getThumbnailCache().getThumbnailInBackground(task, thumbnailData -> { task.thumbnail = thumbnailData; callback.accept(thumbnailData); }); } void updateIconInBackground(Task task, Consumer callback) { mModel.getIconCache().getIconInBackground(task, (icon, contentDescription, title) -> { task.icon = icon; task.titleDescription = contentDescription; task.title = title; callback.accept(task); }); } 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; Task task2 = task.task2; return runningTaskId == task.task1.key.id || (task2 != null && runningTaskId == task2.key.id); } boolean isFirstTaskRunning() { return isTaskRunning(getTaskAt(0)); } boolean isAspectRatioSquare() { return mControllers != null && LayoutUtils.isAspectRatioSquare( mControllers.taskbarActivityContext.getDeviceProfile().aspectRatio); } } }