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