1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.launcher3.taskbar; 17 18 import android.content.ComponentName; 19 import android.content.pm.ActivityInfo; 20 21 import androidx.annotation.NonNull; 22 import androidx.annotation.Nullable; 23 import androidx.annotation.VisibleForTesting; 24 25 import com.android.launcher3.R; 26 import com.android.launcher3.statehandlers.DesktopVisibilityController; 27 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; 28 import com.android.quickstep.LauncherActivityInterface; 29 import com.android.quickstep.RecentsModel; 30 import com.android.quickstep.util.DesktopTask; 31 import com.android.quickstep.util.GroupTask; 32 import com.android.systemui.shared.recents.model.Task; 33 import com.android.systemui.shared.recents.model.ThumbnailData; 34 import com.android.systemui.shared.system.ActivityManagerWrapper; 35 36 import java.io.PrintWriter; 37 import java.util.ArrayList; 38 import java.util.Collections; 39 import java.util.List; 40 import java.util.function.Consumer; 41 import java.util.stream.Collectors; 42 43 /** 44 * Handles initialization of the {@link KeyboardQuickSwitchViewController}. 45 */ 46 public final class KeyboardQuickSwitchController implements 47 TaskbarControllers.LoggableTaskbarController { 48 49 @VisibleForTesting 50 public static final int MAX_TASKS = 6; 51 52 @NonNull private final ControllerCallbacks mControllerCallbacks = new ControllerCallbacks(); 53 54 // Initialized on init 55 @Nullable private RecentsModel mModel; 56 57 // Used to keep track of the last requested task list id, so that we do not request to load the 58 // tasks again if we have already requested it and the task list has not changed 59 private int mTaskListChangeId = -1; 60 // Only empty before the recent tasks list has been loaded the first time 61 @NonNull private List<GroupTask> mTasks = new ArrayList<>(); 62 private int mNumHiddenTasks = 0; 63 64 // Initialized in init 65 private TaskbarControllers mControllers; 66 67 @Nullable private KeyboardQuickSwitchViewController mQuickSwitchViewController; 68 69 /** Initialize the controller. */ init(@onNull TaskbarControllers controllers)70 public void init(@NonNull TaskbarControllers controllers) { 71 mControllers = controllers; 72 mModel = RecentsModel.INSTANCE.get(controllers.taskbarActivityContext); 73 } 74 onConfigurationChanged(@ctivityInfo.Config int configChanges)75 void onConfigurationChanged(@ActivityInfo.Config int configChanges) { 76 if (mQuickSwitchViewController == null) { 77 return; 78 } 79 if ((configChanges & (ActivityInfo.CONFIG_KEYBOARD 80 | ActivityInfo.CONFIG_KEYBOARD_HIDDEN)) != 0) { 81 mQuickSwitchViewController.closeQuickSwitchView(true); 82 return; 83 } 84 int currentFocusedIndex = mQuickSwitchViewController.getCurrentFocusedIndex(); 85 onDestroy(); 86 if (currentFocusedIndex != -1) { 87 mControllers.taskbarActivityContext.getMainThreadHandler().post( 88 () -> openQuickSwitchView(currentFocusedIndex)); 89 } 90 } 91 openQuickSwitchView()92 void openQuickSwitchView() { 93 openQuickSwitchView(-1); 94 } 95 openQuickSwitchView(int currentFocusedIndex)96 private void openQuickSwitchView(int currentFocusedIndex) { 97 if (mQuickSwitchViewController != null) { 98 if (!mQuickSwitchViewController.isCloseAnimationRunning()) { 99 return; 100 } 101 // Allow the KQS to be reopened during the close animation to make it more responsive 102 closeQuickSwitchView(false); 103 } 104 TaskbarOverlayContext overlayContext = 105 mControllers.taskbarOverlayController.requestWindow(); 106 KeyboardQuickSwitchView keyboardQuickSwitchView = 107 (KeyboardQuickSwitchView) overlayContext.getLayoutInflater() 108 .inflate( 109 R.layout.keyboard_quick_switch_view, 110 overlayContext.getDragLayer(), 111 /* attachToRoot= */ false); 112 mQuickSwitchViewController = new KeyboardQuickSwitchViewController( 113 mControllers, overlayContext, keyboardQuickSwitchView, mControllerCallbacks); 114 115 DesktopVisibilityController desktopController = 116 LauncherActivityInterface.INSTANCE.getDesktopVisibilityController(); 117 final boolean onDesktop = 118 desktopController != null && desktopController.areDesktopTasksVisible(); 119 120 if (mModel.isTaskListValid(mTaskListChangeId)) { 121 // When we are opening the KQS with no focus override, check if the first task is 122 // running. If not, focus that first task. 123 mQuickSwitchViewController.openQuickSwitchView( 124 mTasks, 125 mNumHiddenTasks, 126 /* updateTasks= */ false, 127 currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning() 128 ? 0 : currentFocusedIndex, 129 onDesktop); 130 return; 131 } 132 133 mTaskListChangeId = mModel.getTasks((tasks) -> { 134 if (onDesktop) { 135 processLoadedTasksOnDesktop(tasks); 136 } else { 137 processLoadedTasks(tasks); 138 } 139 // Check if the first task is running after the recents model has updated so that we use 140 // the correct index. 141 mQuickSwitchViewController.openQuickSwitchView( 142 mTasks, 143 mNumHiddenTasks, 144 /* updateTasks= */ true, 145 currentFocusedIndex == -1 && !mControllerCallbacks.isFirstTaskRunning() 146 ? 0 : currentFocusedIndex, 147 onDesktop); 148 }); 149 } 150 processLoadedTasks(List<GroupTask> tasks)151 private void processLoadedTasks(List<GroupTask> tasks) { 152 // Only store MAX_TASK tasks, from most to least recent 153 Collections.reverse(tasks); 154 mTasks = tasks.stream() 155 .limit(MAX_TASKS) 156 .collect(Collectors.toList()); 157 mNumHiddenTasks = Math.max(0, tasks.size() - MAX_TASKS); 158 } 159 processLoadedTasksOnDesktop(List<GroupTask> tasks)160 private void processLoadedTasksOnDesktop(List<GroupTask> tasks) { 161 // Find the single desktop task that contains a grouping of desktop tasks 162 DesktopTask desktopTask = findDesktopTask(tasks); 163 164 if (desktopTask != null) { 165 mTasks = desktopTask.tasks.stream().map(GroupTask::new).collect(Collectors.toList()); 166 // All other tasks, apart from the grouped desktop task, are hidden 167 mNumHiddenTasks = Math.max(0, tasks.size() - 1); 168 } else { 169 // Desktop tasks were visible, but the recents entry is missing. Fall back to empty list 170 mTasks = Collections.emptyList(); 171 mNumHiddenTasks = tasks.size(); 172 } 173 } 174 175 @Nullable findDesktopTask(List<GroupTask> tasks)176 private DesktopTask findDesktopTask(List<GroupTask> tasks) { 177 return (DesktopTask) tasks.stream() 178 .filter(t -> t instanceof DesktopTask) 179 .findFirst() 180 .orElse(null); 181 } 182 closeQuickSwitchView()183 void closeQuickSwitchView() { 184 closeQuickSwitchView(true); 185 } 186 closeQuickSwitchView(boolean animate)187 void closeQuickSwitchView(boolean animate) { 188 if (mQuickSwitchViewController == null) { 189 return; 190 } 191 mQuickSwitchViewController.closeQuickSwitchView(animate); 192 } 193 194 /** 195 * See {@link TaskbarUIController#launchFocusedTask()} 196 */ launchFocusedTask()197 int launchFocusedTask() { 198 // Return -1 so that the RecentsView is not incorrectly opened when the user closes the 199 // quick switch view by tapping the screen or when there are no recent tasks. 200 return mQuickSwitchViewController == null || mTasks.isEmpty() 201 ? -1 : mQuickSwitchViewController.launchFocusedTask(); 202 } 203 onDestroy()204 void onDestroy() { 205 if (mQuickSwitchViewController != null) { 206 mQuickSwitchViewController.onDestroy(); 207 } 208 } 209 210 @Override dumpLogs(String prefix, PrintWriter pw)211 public void dumpLogs(String prefix, PrintWriter pw) { 212 pw.println(prefix + "KeyboardQuickSwitchController:"); 213 214 pw.println(prefix + "\tisOpen=" + (mQuickSwitchViewController != null)); 215 pw.println(prefix + "\tmNumHiddenTasks=" + mNumHiddenTasks); 216 pw.println(prefix + "\tmTaskListChangeId=" + mTaskListChangeId); 217 pw.println(prefix + "\tmTasks=["); 218 for (GroupTask task : mTasks) { 219 Task task1 = task.task1; 220 Task task2 = task.task2; 221 ComponentName cn1 = task1.getTopComponent(); 222 ComponentName cn2 = task2 != null ? task2.getTopComponent() : null; 223 pw.println(prefix + "\t\tt1: (id=" + task1.key.id 224 + "; package=" + (cn1 != null ? cn1.getPackageName() + ")" : "no package)") 225 + " t2: (id=" + (task2 != null ? task2.key.id : "-1") 226 + "; package=" + (cn2 != null ? cn2.getPackageName() + ")" 227 : "no package)")); 228 } 229 pw.println(prefix + "\t]"); 230 231 if (mQuickSwitchViewController != null) { 232 mQuickSwitchViewController.dumpLogs(prefix + '\t', pw); 233 } 234 } 235 236 class ControllerCallbacks { 237 getTaskCount()238 int getTaskCount() { 239 return mTasks.size() + (mNumHiddenTasks == 0 ? 0 : 1); 240 } 241 242 @Nullable getTaskAt(int index)243 GroupTask getTaskAt(int index) { 244 return index < 0 || index >= mTasks.size() ? null : mTasks.get(index); 245 } 246 updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback)247 void updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback) { 248 mModel.getThumbnailCache().updateThumbnailInBackground(task, callback); 249 } 250 updateIconInBackground(Task task, Consumer<Task> callback)251 void updateIconInBackground(Task task, Consumer<Task> callback) { 252 mModel.getIconCache().updateIconInBackground(task, callback); 253 } 254 onCloseComplete()255 void onCloseComplete() { 256 mQuickSwitchViewController = null; 257 } 258 isTaskRunning(@ullable GroupTask task)259 boolean isTaskRunning(@Nullable GroupTask task) { 260 if (task == null) { 261 return false; 262 } 263 int runningTaskId = ActivityManagerWrapper.getInstance().getRunningTask().taskId; 264 Task task2 = task.task2; 265 266 return runningTaskId == task.task1.key.id 267 || (task2 != null && runningTaskId == task2.key.id); 268 } 269 isFirstTaskRunning()270 boolean isFirstTaskRunning() { 271 return isTaskRunning(getTaskAt(0)); 272 } 273 } 274 } 275