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 static android.window.SplashScreen.SPLASH_SCREEN_STYLE_UNDEFINED;
19 
20 import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
21 
22 import android.animation.Animator;
23 import android.app.ActivityOptions;
24 import android.view.KeyEvent;
25 import android.view.animation.AnimationUtils;
26 import android.window.RemoteTransition;
27 
28 import androidx.annotation.NonNull;
29 import androidx.annotation.Nullable;
30 
31 import com.android.launcher3.Utilities;
32 import com.android.launcher3.anim.AnimatorListeners;
33 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext;
34 import com.android.quickstep.SystemUiProxy;
35 import com.android.quickstep.util.DesktopTask;
36 import com.android.quickstep.util.GroupTask;
37 import com.android.quickstep.util.SlideInRemoteTransition;
38 import com.android.systemui.shared.recents.model.Task;
39 import com.android.systemui.shared.recents.model.ThumbnailData;
40 import com.android.systemui.shared.system.ActivityManagerWrapper;
41 import com.android.systemui.shared.system.QuickStepContract;
42 
43 import java.io.PrintWriter;
44 import java.util.List;
45 import java.util.function.Consumer;
46 
47 /**
48  * Handles initialization of the {@link KeyboardQuickSwitchView} and supplies it with the list of
49  * tasks.
50  */
51 public class KeyboardQuickSwitchViewController {
52 
53     @NonNull private final ViewCallbacks mViewCallbacks = new ViewCallbacks();
54     @NonNull private final TaskbarControllers mControllers;
55     @NonNull private final TaskbarOverlayContext mOverlayContext;
56     @NonNull private final KeyboardQuickSwitchView mKeyboardQuickSwitchView;
57     @NonNull private final KeyboardQuickSwitchController.ControllerCallbacks mControllerCallbacks;
58 
59     @Nullable private Animator mCloseAnimation;
60 
61     private int mCurrentFocusIndex = -1;
62 
63     private boolean mOnDesktop;
64 
KeyboardQuickSwitchViewController( @onNull TaskbarControllers controllers, @NonNull TaskbarOverlayContext overlayContext, @NonNull KeyboardQuickSwitchView keyboardQuickSwitchView, @NonNull KeyboardQuickSwitchController.ControllerCallbacks controllerCallbacks)65     protected KeyboardQuickSwitchViewController(
66             @NonNull TaskbarControllers controllers,
67             @NonNull TaskbarOverlayContext overlayContext,
68             @NonNull KeyboardQuickSwitchView keyboardQuickSwitchView,
69             @NonNull KeyboardQuickSwitchController.ControllerCallbacks controllerCallbacks) {
70         mControllers = controllers;
71         mOverlayContext = overlayContext;
72         mKeyboardQuickSwitchView = keyboardQuickSwitchView;
73         mControllerCallbacks = controllerCallbacks;
74     }
75 
getCurrentFocusedIndex()76     protected int getCurrentFocusedIndex() {
77         return mCurrentFocusIndex;
78     }
79 
openQuickSwitchView( @onNull List<GroupTask> tasks, int numHiddenTasks, boolean updateTasks, int currentFocusIndexOverride, boolean onDesktop)80     protected void openQuickSwitchView(
81             @NonNull List<GroupTask> tasks,
82             int numHiddenTasks,
83             boolean updateTasks,
84             int currentFocusIndexOverride,
85             boolean onDesktop) {
86         mOverlayContext.getDragLayer().addView(mKeyboardQuickSwitchView);
87         mOnDesktop = onDesktop;
88 
89         mKeyboardQuickSwitchView.applyLoadPlan(
90                 mOverlayContext,
91                 tasks,
92                 numHiddenTasks,
93                 updateTasks,
94                 currentFocusIndexOverride,
95                 mViewCallbacks);
96     }
97 
isCloseAnimationRunning()98     boolean isCloseAnimationRunning() {
99         return mCloseAnimation != null;
100     }
101 
closeQuickSwitchView(boolean animate)102     protected void closeQuickSwitchView(boolean animate) {
103         if (isCloseAnimationRunning()) {
104             // Let currently-running animation finish.
105             if (!animate) {
106                 mCloseAnimation.end();
107             }
108             return;
109         }
110         if (!animate) {
111             onCloseComplete();
112             return;
113         }
114         mCloseAnimation = mKeyboardQuickSwitchView.getCloseAnimation();
115 
116         mCloseAnimation.addListener(AnimatorListeners.forEndCallback(this::onCloseComplete));
117         mCloseAnimation.start();
118     }
119 
120     /**
121      * Launched the currently-focused task.
122      *
123      * Returns index -1 iff the RecentsView shouldn't be opened.
124      *
125      * If the index is not -1, then the {@link com.android.quickstep.views.TaskView} at the returned
126      * index will be focused.
127      */
launchFocusedTask()128     protected int launchFocusedTask() {
129         if (mCurrentFocusIndex != -1) {
130             return launchTaskAt(mCurrentFocusIndex);
131         }
132         // If the user quick switches too quickly, updateCurrentFocusIndex might not have run.
133         return launchTaskAt(mControllerCallbacks.isFirstTaskRunning()
134                 && mControllerCallbacks.getTaskCount() > 1 ? 1 : 0);
135     }
136 
launchTaskAt(int index)137     private int launchTaskAt(int index) {
138         if (isCloseAnimationRunning()) {
139             // Ignore taps on task views and alt key unpresses while the close animation is running.
140             return -1;
141         }
142         // Even with a valid index, this can be null if the user tries to quick switch before the
143         // views have been added in the KeyboardQuickSwitchView.
144         GroupTask task = mControllerCallbacks.getTaskAt(index);
145         if (task == null) {
146             return mOnDesktop ? 1 : Math.max(0, index);
147         }
148         if (mControllerCallbacks.isTaskRunning(task)) {
149             // Ignore attempts to run the selected task if it is already running.
150             return -1;
151         }
152 
153         TaskbarActivityContext context = mControllers.taskbarActivityContext;
154         RemoteTransition remoteTransition = new RemoteTransition(new SlideInRemoteTransition(
155                 Utilities.isRtl(mControllers.taskbarActivityContext.getResources()),
156                 context.getDeviceProfile().overviewPageSpacing,
157                 QuickStepContract.getWindowCornerRadius(context),
158                 AnimationUtils.loadInterpolator(
159                         context, android.R.interpolator.fast_out_extra_slow_in)),
160                 "SlideInTransition");
161         if (task instanceof DesktopTask) {
162             UI_HELPER_EXECUTOR.execute(() ->
163                     SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
164                             .showDesktopApps(
165                                     mKeyboardQuickSwitchView.getDisplay().getDisplayId(),
166                                     remoteTransition));
167         } else if (mOnDesktop) {
168             UI_HELPER_EXECUTOR.execute(() ->
169                     SystemUiProxy.INSTANCE.get(mKeyboardQuickSwitchView.getContext())
170                             .showDesktopApp(task.task1.key.id));
171         } else if (task.task2 == null) {
172             UI_HELPER_EXECUTOR.execute(() -> {
173                 ActivityOptions activityOptions = mControllers.taskbarActivityContext
174                         .makeDefaultActivityOptions(SPLASH_SCREEN_STYLE_UNDEFINED).options;
175                 activityOptions.setRemoteTransition(remoteTransition);
176 
177                 ActivityManagerWrapper.getInstance().startActivityFromRecents(
178                         task.task1.key, activityOptions);
179             });
180         } else {
181             mControllers.uiController.launchSplitTasks(task, remoteTransition);
182         }
183         return -1;
184     }
185 
onCloseComplete()186     private void onCloseComplete() {
187         mCloseAnimation = null;
188         mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView);
189         mControllerCallbacks.onCloseComplete();
190     }
191 
onDestroy()192     protected void onDestroy() {
193         closeQuickSwitchView(false);
194     }
195 
dumpLogs(String prefix, PrintWriter pw)196     public void dumpLogs(String prefix, PrintWriter pw) {
197         pw.println(prefix + "KeyboardQuickSwitchViewController:");
198 
199         pw.println(prefix + "\thasFocus=" + mKeyboardQuickSwitchView.hasFocus());
200         pw.println(prefix + "\tisCloseAnimationRunning=" + isCloseAnimationRunning());
201         pw.println(prefix + "\tmCurrentFocusIndex=" + mCurrentFocusIndex);
202     }
203 
204     class ViewCallbacks {
205 
onKeyUp(int keyCode, KeyEvent event, boolean isRTL, boolean allowTraversal)206         boolean onKeyUp(int keyCode, KeyEvent event, boolean isRTL, boolean allowTraversal) {
207             if (keyCode != KeyEvent.KEYCODE_TAB
208                     && keyCode != KeyEvent.KEYCODE_DPAD_RIGHT
209                     && keyCode != KeyEvent.KEYCODE_DPAD_LEFT
210                     && keyCode != KeyEvent.KEYCODE_GRAVE
211                     && keyCode != KeyEvent.KEYCODE_ESCAPE
212                     && keyCode != KeyEvent.KEYCODE_ENTER) {
213                 return false;
214             }
215             if (keyCode == KeyEvent.KEYCODE_GRAVE || keyCode == KeyEvent.KEYCODE_ESCAPE) {
216                 closeQuickSwitchView(true);
217                 return true;
218             }
219             if (keyCode == KeyEvent.KEYCODE_ENTER) {
220                 launchTaskAt(mCurrentFocusIndex);
221                 return true;
222             }
223             if (!allowTraversal) {
224                 return false;
225             }
226             boolean traverseBackwards = (keyCode == KeyEvent.KEYCODE_TAB && event.isShiftPressed())
227                     || (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && isRTL)
228                     || (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && !isRTL);
229             int taskCount = mControllerCallbacks.getTaskCount();
230             int toIndex = mCurrentFocusIndex == -1
231                     // Focus the second-most recent app if possible
232                     ? (taskCount > 1 ? 1 : 0)
233                     : (traverseBackwards
234                             // focus a more recent task or loop back to the opposite end
235                             ? Math.max(0, mCurrentFocusIndex == 0
236                                     ? taskCount - 1 : mCurrentFocusIndex - 1)
237                             // focus a less recent app or loop back to the opposite end
238                             : ((mCurrentFocusIndex + 1) % taskCount));
239 
240             if (mCurrentFocusIndex == toIndex) {
241                 return true;
242             }
243             mKeyboardQuickSwitchView.animateFocusMove(mCurrentFocusIndex, toIndex);
244 
245             return true;
246         }
247 
updateCurrentFocusIndex(int index)248         void updateCurrentFocusIndex(int index) {
249             mCurrentFocusIndex = index;
250         }
251 
launchTaskAt(int index)252         void launchTaskAt(int index) {
253             mCurrentFocusIndex = index;
254             mControllers.taskbarActivityContext.launchKeyboardFocusedTask();
255         }
256 
updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback)257         void updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback) {
258             mControllerCallbacks.updateThumbnailInBackground(task, callback);
259         }
260 
updateIconInBackground(Task task, Consumer<Task> callback)261         void updateIconInBackground(Task task, Consumer<Task> callback) {
262             mControllerCallbacks.updateIconInBackground(task, callback);
263         }
264     }
265 }
266