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 24 import com.android.launcher3.R; 25 import com.android.launcher3.statehandlers.DesktopVisibilityController; 26 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; 27 import com.android.quickstep.LauncherActivityInterface; 28 import com.android.quickstep.RecentsModel; 29 import com.android.quickstep.util.DesktopTask; 30 import com.android.quickstep.util.GroupTask; 31 import com.android.quickstep.views.DesktopTaskView; 32 import com.android.systemui.shared.recents.model.Task; 33 import com.android.systemui.shared.recents.model.ThumbnailData; 34 35 import java.io.PrintWriter; 36 import java.util.ArrayList; 37 import java.util.Collections; 38 import java.util.List; 39 import java.util.function.Consumer; 40 import java.util.stream.Collectors; 41 42 /** 43 * Handles initialization of the {@link KeyboardQuickSwitchViewController}. 44 */ 45 public final class KeyboardQuickSwitchController implements 46 TaskbarControllers.LoggableTaskbarController { 47 48 static final int MAX_TASKS = 6; 49 50 @NonNull private final ControllerCallbacks mControllerCallbacks = new ControllerCallbacks(); 51 52 // Initialized on init 53 @Nullable private RecentsModel mModel; 54 55 // Used to keep track of the last requested task list id, so that we do not request to load the 56 // tasks again if we have already requested it and the task list has not changed 57 private int mTaskListChangeId = -1; 58 // Only empty before the recent tasks list has been loaded the first time 59 @NonNull private List<GroupTask> mTasks = new ArrayList<>(); 60 private int mNumHiddenTasks = 0; 61 62 // Initialized in init 63 private TaskbarControllers mControllers; 64 65 @Nullable private KeyboardQuickSwitchViewController mQuickSwitchViewController; 66 67 /** Initialize the controller. */ init(@onNull TaskbarControllers controllers)68 public void init(@NonNull TaskbarControllers controllers) { 69 mControllers = controllers; 70 mModel = RecentsModel.INSTANCE.get(controllers.taskbarActivityContext); 71 } 72 onConfigurationChanged(@ctivityInfo.Config int configChanges)73 void onConfigurationChanged(@ActivityInfo.Config int configChanges) { 74 if (mQuickSwitchViewController == null) { 75 return; 76 } 77 if ((configChanges & (ActivityInfo.CONFIG_KEYBOARD 78 | ActivityInfo.CONFIG_KEYBOARD_HIDDEN)) != 0) { 79 mQuickSwitchViewController.closeQuickSwitchView(true); 80 return; 81 } 82 int currentFocusedIndex = mQuickSwitchViewController.getCurrentFocusedIndex(); 83 onDestroy(); 84 if (currentFocusedIndex != -1) { 85 mControllers.taskbarActivityContext.getMainThreadHandler().post( 86 () -> openQuickSwitchView(currentFocusedIndex)); 87 } 88 } 89 openQuickSwitchView()90 void openQuickSwitchView() { 91 openQuickSwitchView(-1); 92 } 93 openQuickSwitchView(int currentFocusedIndex)94 private void openQuickSwitchView(int currentFocusedIndex) { 95 if (mQuickSwitchViewController != null) { 96 return; 97 } 98 TaskbarOverlayContext overlayContext = 99 mControllers.taskbarOverlayController.requestWindow(); 100 KeyboardQuickSwitchView keyboardQuickSwitchView = 101 (KeyboardQuickSwitchView) overlayContext.getLayoutInflater() 102 .inflate( 103 R.layout.keyboard_quick_switch_view, 104 overlayContext.getDragLayer(), 105 /* attachToRoot= */ false); 106 mQuickSwitchViewController = new KeyboardQuickSwitchViewController( 107 mControllers, overlayContext, keyboardQuickSwitchView, mControllerCallbacks); 108 109 DesktopVisibilityController desktopController = 110 LauncherActivityInterface.INSTANCE.getDesktopVisibilityController(); 111 final boolean onDesktop = 112 DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED 113 && desktopController != null 114 && desktopController.areFreeformTasksVisible(); 115 116 if (mModel.isTaskListValid(mTaskListChangeId)) { 117 mQuickSwitchViewController.openQuickSwitchView(mTasks, 118 mNumHiddenTasks, /* updateTasks= */ false, currentFocusedIndex, onDesktop); 119 return; 120 } 121 122 mTaskListChangeId = mModel.getTasks((tasks) -> { 123 if (onDesktop) { 124 processLoadedTasksOnDesktop(tasks); 125 } else { 126 processLoadedTasks(tasks); 127 } 128 mQuickSwitchViewController.openQuickSwitchView(mTasks, 129 mNumHiddenTasks, /* updateTasks= */ true, currentFocusedIndex, onDesktop); 130 }); 131 } 132 processLoadedTasks(ArrayList<GroupTask> tasks)133 private void processLoadedTasks(ArrayList<GroupTask> tasks) { 134 // Only store MAX_TASK tasks, from most to least recent 135 Collections.reverse(tasks); 136 137 // Hide all desktop tasks and show them on the hidden tile 138 int hiddenDesktopTasks = 0; 139 if (DesktopTaskView.DESKTOP_IS_PROTO2_ENABLED) { 140 DesktopTask desktopTask = findDesktopTask(tasks); 141 if (desktopTask != null) { 142 hiddenDesktopTasks = desktopTask.tasks.size(); 143 tasks = tasks.stream() 144 .filter(t -> !(t instanceof DesktopTask)) 145 .collect(Collectors.toCollection(ArrayList<GroupTask>::new)); 146 } 147 } 148 mTasks = tasks.stream() 149 .limit(MAX_TASKS) 150 .collect(Collectors.toList()); 151 mNumHiddenTasks = Math.max(0, tasks.size() - MAX_TASKS) + hiddenDesktopTasks; 152 } 153 processLoadedTasksOnDesktop(ArrayList<GroupTask> tasks)154 private void processLoadedTasksOnDesktop(ArrayList<GroupTask> tasks) { 155 // Find the single desktop task that contains a grouping of desktop tasks 156 DesktopTask desktopTask = findDesktopTask(tasks); 157 158 if (desktopTask != null) { 159 mTasks = desktopTask.tasks.stream().map(GroupTask::new).collect(Collectors.toList()); 160 // All other tasks, apart from the grouped desktop task, are hidden 161 mNumHiddenTasks = Math.max(0, tasks.size() - 1); 162 } else { 163 // Desktop tasks were visible, but the recents entry is missing. Fall back to empty list 164 mTasks = Collections.emptyList(); 165 mNumHiddenTasks = tasks.size(); 166 } 167 } 168 169 @Nullable findDesktopTask(ArrayList<GroupTask> tasks)170 private DesktopTask findDesktopTask(ArrayList<GroupTask> tasks) { 171 return (DesktopTask) tasks.stream() 172 .filter(t -> t instanceof DesktopTask) 173 .findFirst() 174 .orElse(null); 175 } 176 closeQuickSwitchView()177 void closeQuickSwitchView() { 178 if (mQuickSwitchViewController == null) { 179 return; 180 } 181 mQuickSwitchViewController.closeQuickSwitchView(true); 182 } 183 184 /** 185 * See {@link TaskbarUIController#launchFocusedTask()} 186 */ launchFocusedTask()187 int launchFocusedTask() { 188 // Return -1 so that the RecentsView is not incorrectly opened when the user closes the 189 // quick switch view by tapping the screen or when there are no recent tasks. 190 return mQuickSwitchViewController == null || mTasks.isEmpty() 191 ? -1 : mQuickSwitchViewController.launchFocusedTask(); 192 } 193 onDestroy()194 void onDestroy() { 195 if (mQuickSwitchViewController != null) { 196 mQuickSwitchViewController.onDestroy(); 197 } 198 } 199 200 @Override dumpLogs(String prefix, PrintWriter pw)201 public void dumpLogs(String prefix, PrintWriter pw) { 202 pw.println(prefix + "KeyboardQuickSwitchController:"); 203 204 pw.println(prefix + "\tisOpen=" + (mQuickSwitchViewController != null)); 205 pw.println(prefix + "\tmNumHiddenTasks=" + mNumHiddenTasks); 206 pw.println(prefix + "\tmTaskListChangeId=" + mTaskListChangeId); 207 pw.println(prefix + "\tmTasks=["); 208 for (GroupTask task : mTasks) { 209 Task task1 = task.task1; 210 Task task2 = task.task2; 211 ComponentName cn1 = task1.getTopComponent(); 212 ComponentName cn2 = task2 != null ? task2.getTopComponent() : null; 213 pw.println(prefix + "\t\tt1: (id=" + task1.key.id 214 + "; package=" + (cn1 != null ? cn1.getPackageName() + ")" : "no package)") 215 + " t2: (id=" + (task2 != null ? task2.key.id : "-1") 216 + "; package=" + (cn2 != null ? cn2.getPackageName() + ")" 217 : "no package)")); 218 } 219 pw.println(prefix + "\t]"); 220 221 if (mQuickSwitchViewController != null) { 222 mQuickSwitchViewController.dumpLogs(prefix + '\t', pw); 223 } 224 } 225 226 class ControllerCallbacks { 227 getTaskCount()228 int getTaskCount() { 229 return mTasks.size() + (mNumHiddenTasks == 0 ? 0 : 1); 230 } 231 232 @Nullable getTaskAt(int index)233 GroupTask getTaskAt(int index) { 234 return index < 0 || index >= mTasks.size() ? null : mTasks.get(index); 235 } 236 updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback)237 void updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback) { 238 mModel.getThumbnailCache().updateThumbnailInBackground(task, callback); 239 } 240 updateIconInBackground(Task task, Consumer<Task> callback)241 void updateIconInBackground(Task task, Consumer<Task> callback) { 242 mModel.getIconCache().updateIconInBackground(task, callback); 243 } 244 onCloseComplete()245 void onCloseComplete() { 246 mQuickSwitchViewController = null; 247 } 248 } 249 } 250