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