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 com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; 19 20 import android.animation.Animator; 21 import android.view.KeyEvent; 22 import android.view.View; 23 24 import androidx.annotation.NonNull; 25 import androidx.annotation.Nullable; 26 27 import com.android.launcher3.anim.AnimationSuccessListener; 28 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; 29 import com.android.launcher3.taskbar.overlay.TaskbarOverlayDragLayer; 30 import com.android.quickstep.util.GroupTask; 31 import com.android.systemui.shared.recents.model.Task; 32 import com.android.systemui.shared.recents.model.ThumbnailData; 33 import com.android.systemui.shared.system.ActivityManagerWrapper; 34 35 import java.io.PrintWriter; 36 import java.util.List; 37 import java.util.function.Consumer; 38 39 /** 40 * Handles initialization of the {@link KeyboardQuickSwitchView} and supplies it with the list of 41 * tasks. 42 */ 43 public class KeyboardQuickSwitchViewController { 44 45 @NonNull private final ViewCallbacks mViewCallbacks = new ViewCallbacks(); 46 @NonNull private final TaskbarControllers mControllers; 47 @NonNull private final TaskbarOverlayContext mOverlayContext; 48 @NonNull private final KeyboardQuickSwitchView mKeyboardQuickSwitchView; 49 @NonNull private final KeyboardQuickSwitchController.ControllerCallbacks mControllerCallbacks; 50 51 @Nullable private Animator mCloseAnimation; 52 53 private int mCurrentFocusIndex = -1; 54 KeyboardQuickSwitchViewController( @onNull TaskbarControllers controllers, @NonNull TaskbarOverlayContext overlayContext, @NonNull KeyboardQuickSwitchView keyboardQuickSwitchView, @NonNull KeyboardQuickSwitchController.ControllerCallbacks controllerCallbacks)55 protected KeyboardQuickSwitchViewController( 56 @NonNull TaskbarControllers controllers, 57 @NonNull TaskbarOverlayContext overlayContext, 58 @NonNull KeyboardQuickSwitchView keyboardQuickSwitchView, 59 @NonNull KeyboardQuickSwitchController.ControllerCallbacks controllerCallbacks) { 60 mControllers = controllers; 61 mOverlayContext = overlayContext; 62 mKeyboardQuickSwitchView = keyboardQuickSwitchView; 63 mControllerCallbacks = controllerCallbacks; 64 } 65 getCurrentFocusedIndex()66 protected int getCurrentFocusedIndex() { 67 return mCurrentFocusIndex; 68 } 69 openQuickSwitchView( @onNull List<GroupTask> tasks, int numHiddenTasks, boolean updateTasks, int currentFocusIndexOverride)70 protected void openQuickSwitchView( 71 @NonNull List<GroupTask> tasks, 72 int numHiddenTasks, 73 boolean updateTasks, 74 int currentFocusIndexOverride) { 75 TaskbarOverlayDragLayer dragLayer = mOverlayContext.getDragLayer(); 76 dragLayer.addView(mKeyboardQuickSwitchView); 77 dragLayer.runOnClickOnce(v -> closeQuickSwitchView(true)); 78 79 mKeyboardQuickSwitchView.applyLoadPlan( 80 mOverlayContext, 81 tasks, 82 numHiddenTasks, 83 updateTasks, 84 currentFocusIndexOverride, 85 mViewCallbacks); 86 } 87 closeQuickSwitchView(boolean animate)88 protected void closeQuickSwitchView(boolean animate) { 89 if (mCloseAnimation != null) { 90 if (animate) { 91 // Let currently-running animation finish. 92 return; 93 } else { 94 mCloseAnimation.cancel(); 95 } 96 } 97 if (!animate) { 98 mCloseAnimation = null; 99 onCloseComplete(); 100 return; 101 } 102 mCloseAnimation = mKeyboardQuickSwitchView.getCloseAnimation(); 103 104 mCloseAnimation.addListener(new AnimationSuccessListener() { 105 @Override 106 public void onAnimationSuccess(Animator animator) { 107 mCloseAnimation = null; 108 onCloseComplete(); 109 } 110 }); 111 mCloseAnimation.start(); 112 } 113 114 /** 115 * Launched the currently-focused task. 116 * 117 * Returns index -1 iff the RecentsView shouldn't be opened. 118 * 119 * If the index is not -1, then the {@link com.android.quickstep.views.TaskView} at the returned 120 * index will be focused. 121 */ launchFocusedTask()122 protected int launchFocusedTask() { 123 // Launch the second-most recent task if the user quick switches too quickly, if possible. 124 return launchTaskAt(mCurrentFocusIndex == -1 125 ? (mControllerCallbacks.getTaskCount() > 1 ? 1 : 0) : mCurrentFocusIndex); 126 } 127 launchTaskAt(int index)128 private int launchTaskAt(int index) { 129 if (mCloseAnimation != null) { 130 // Ignore taps on task views and alt key unpresses while the close animation is running. 131 return -1; 132 } 133 // Even with a valid index, this can be null if the user tries to quick switch before the 134 // views have been added in the KeyboardQuickSwitchView. 135 View taskView = mKeyboardQuickSwitchView.getTaskAt(index); 136 GroupTask task = mControllerCallbacks.getTaskAt(index); 137 if (task == null) { 138 return Math.max(0, index); 139 } else if (task.task2 == null) { 140 UI_HELPER_EXECUTOR.execute(() -> 141 ActivityManagerWrapper.getInstance().startActivityFromRecents( 142 task.task1.key, 143 mControllers.taskbarActivityContext.getActivityLaunchOptions( 144 taskView == null ? mKeyboardQuickSwitchView : taskView, null) 145 .options)); 146 } else { 147 mControllers.uiController.launchSplitTasks( 148 taskView == null ? mKeyboardQuickSwitchView : taskView, task); 149 } 150 return -1; 151 } 152 onCloseComplete()153 private void onCloseComplete() { 154 mOverlayContext.getDragLayer().removeView(mKeyboardQuickSwitchView); 155 mControllerCallbacks.onCloseComplete(); 156 } 157 onDestroy()158 protected void onDestroy() { 159 closeQuickSwitchView(false); 160 } 161 dumpLogs(String prefix, PrintWriter pw)162 public void dumpLogs(String prefix, PrintWriter pw) { 163 pw.println(prefix + "KeyboardQuickSwitchViewController:"); 164 165 pw.println(prefix + "\thasFocus=" + mKeyboardQuickSwitchView.hasFocus()); 166 pw.println(prefix + "\tcloseAnimationRunning=" + (mCloseAnimation != null)); 167 pw.println(prefix + "\tmCurrentFocusIndex=" + mCurrentFocusIndex); 168 } 169 170 class ViewCallbacks { 171 onKeyUp(int keyCode, KeyEvent event, boolean isRTL)172 boolean onKeyUp(int keyCode, KeyEvent event, boolean isRTL) { 173 if (keyCode != KeyEvent.KEYCODE_TAB 174 && keyCode != KeyEvent.KEYCODE_DPAD_RIGHT 175 && keyCode != KeyEvent.KEYCODE_DPAD_LEFT 176 && keyCode != KeyEvent.KEYCODE_GRAVE 177 && keyCode != KeyEvent.KEYCODE_ESCAPE) { 178 return false; 179 } 180 if (keyCode == KeyEvent.KEYCODE_GRAVE || keyCode == KeyEvent.KEYCODE_ESCAPE) { 181 closeQuickSwitchView(true); 182 return true; 183 } 184 boolean traverseBackwards = (keyCode == KeyEvent.KEYCODE_TAB && event.isShiftPressed()) 185 || (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && !isRTL) 186 || (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && isRTL); 187 int taskCount = mControllerCallbacks.getTaskCount(); 188 int toIndex = mCurrentFocusIndex == -1 189 // Focus the second-most recent app if possible 190 ? (taskCount > 1 ? 1 : 0) 191 : (traverseBackwards 192 // focus a more recent task or loop back to the opposite end 193 ? Math.max(0, mCurrentFocusIndex == 0 194 ? taskCount - 1 : mCurrentFocusIndex - 1) 195 // focus a less recent app or loop back to the opposite end 196 : ((mCurrentFocusIndex + 1) % taskCount)); 197 198 mKeyboardQuickSwitchView.animateFocusMove(mCurrentFocusIndex, toIndex); 199 200 return true; 201 } 202 updateCurrentFocusIndex(int index)203 void updateCurrentFocusIndex(int index) { 204 mCurrentFocusIndex = index; 205 } 206 launchTappedTask(int index)207 void launchTappedTask(int index) { 208 KeyboardQuickSwitchViewController.this.launchTaskAt(index); 209 closeQuickSwitchView(true); 210 } 211 updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback)212 void updateThumbnailInBackground(Task task, Consumer<ThumbnailData> callback) { 213 mControllerCallbacks.updateThumbnailInBackground(task, callback); 214 } 215 updateTitleInBackground(Task task, Consumer<Task> callback)216 void updateTitleInBackground(Task task, Consumer<Task> callback) { 217 mControllerCallbacks.updateTitleInBackground(task, callback); 218 } 219 } 220 } 221