1 /* 2 * Copyright 2021 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 17 package com.android.launcher3.taskbar; 18 19 import static android.view.KeyEvent.ACTION_DOWN; 20 import static android.view.KeyEvent.ACTION_UP; 21 22 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS; 23 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_KEY; 24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_A11Y_BUTTON_LONGPRESS; 25 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_A11Y_BUTTON_TAP; 26 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS; 27 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_BACK_BUTTON_TAP; 28 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS; 29 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_TAP; 30 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS; 31 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP; 32 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS; 33 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP; 34 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY; 35 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; 36 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; 37 import static com.android.window.flags.Flags.predictiveBackThreeButtonNav; 38 39 import android.content.Context; 40 import android.os.Bundle; 41 import android.os.Handler; 42 import android.os.SystemClock; 43 import android.util.Log; 44 import android.view.HapticFeedbackConstants; 45 import android.view.KeyEvent; 46 import android.view.View; 47 import android.view.inputmethod.Flags; 48 49 import androidx.annotation.IntDef; 50 import androidx.annotation.Nullable; 51 import androidx.annotation.StringRes; 52 53 import com.android.launcher3.R; 54 import com.android.launcher3.logging.StatsLogManager; 55 import com.android.launcher3.testing.TestLogging; 56 import com.android.launcher3.testing.shared.TestProtocol; 57 import com.android.quickstep.SystemUiProxy; 58 import com.android.quickstep.TaskUtils; 59 import com.android.quickstep.util.ContextualSearchInvoker; 60 import com.android.systemui.contextualeducation.GestureType; 61 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags; 62 63 import java.io.PrintWriter; 64 import java.lang.annotation.Retention; 65 import java.lang.annotation.RetentionPolicy; 66 67 /** 68 * Controller for 3 button mode in the taskbar. 69 * Handles all the functionality of the various buttons, making/routing the right calls into 70 * launcher or sysui/system. 71 */ 72 public class TaskbarNavButtonController implements TaskbarControllers.LoggableTaskbarController { 73 74 /** Allow some time in between the long press for back and recents. */ 75 static final int SCREEN_PIN_LONG_PRESS_THRESHOLD = 200; 76 static final int SCREEN_PIN_LONG_PRESS_RESET = SCREEN_PIN_LONG_PRESS_THRESHOLD + 100; 77 private static final String TAG = "TaskbarNavButtonController"; 78 79 private long mLastScreenPinLongPress; 80 private boolean mScreenPinned; 81 private boolean mAssistantLongPressEnabled; 82 private int mLastSentBackAction = ACTION_UP; 83 84 @Override dumpLogs(String prefix, PrintWriter pw)85 public void dumpLogs(String prefix, PrintWriter pw) { 86 pw.println(prefix + "TaskbarNavButtonController:"); 87 88 pw.println(prefix + "\tmLastScreenPinLongPress=" + mLastScreenPinLongPress); 89 pw.println(prefix + "\tmScreenPinned=" + mScreenPinned); 90 pw.println(prefix + "\tmLastSentBackAction=" 91 + KeyEvent.actionToString(mLastSentBackAction)); 92 } 93 94 @Retention(RetentionPolicy.SOURCE) 95 @IntDef(value = { 96 BUTTON_BACK, 97 BUTTON_HOME, 98 BUTTON_RECENTS, 99 BUTTON_IME_SWITCH, 100 BUTTON_A11Y, 101 BUTTON_QUICK_SETTINGS, 102 BUTTON_NOTIFICATIONS, 103 }) 104 105 public @interface TaskbarButton {} 106 107 static final int BUTTON_BACK = 1; 108 static final int BUTTON_HOME = BUTTON_BACK << 1; 109 static final int BUTTON_RECENTS = BUTTON_HOME << 1; 110 static final int BUTTON_IME_SWITCH = BUTTON_RECENTS << 1; 111 static final int BUTTON_A11Y = BUTTON_IME_SWITCH << 1; 112 static final int BUTTON_QUICK_SETTINGS = BUTTON_A11Y << 1; 113 static final int BUTTON_NOTIFICATIONS = BUTTON_QUICK_SETTINGS << 1; 114 static final int BUTTON_SPACE = BUTTON_NOTIFICATIONS << 1; 115 116 private static final int SCREEN_UNPIN_COMBO = BUTTON_BACK | BUTTON_RECENTS; 117 private int mLongPressedButtons = 0; 118 119 private final Context mContext; 120 private final TaskbarNavButtonCallbacks mCallbacks; 121 private final SystemUiProxy mSystemUiProxy; 122 private final Handler mHandler; 123 private final ContextualSearchInvoker mContextualSearchInvoker; 124 @Nullable private StatsLogManager mStatsLogManager; 125 126 private final Runnable mResetLongPress = this::resetScreenUnpin; 127 TaskbarNavButtonController( Context context, TaskbarNavButtonCallbacks callbacks, SystemUiProxy systemUiProxy, Handler handler, ContextualSearchInvoker contextualSearchInvoker)128 public TaskbarNavButtonController( 129 Context context, 130 TaskbarNavButtonCallbacks callbacks, 131 SystemUiProxy systemUiProxy, 132 Handler handler, 133 ContextualSearchInvoker contextualSearchInvoker) { 134 mContext = context; 135 mCallbacks = callbacks; 136 mSystemUiProxy = systemUiProxy; 137 mHandler = handler; 138 mContextualSearchInvoker = contextualSearchInvoker; 139 } 140 onButtonClick(@askbarButton int buttonType, View view)141 public void onButtonClick(@TaskbarButton int buttonType, View view) { 142 if (buttonType == BUTTON_SPACE) { 143 return; 144 } 145 if (predictiveBackThreeButtonNav() && mLastSentBackAction == ACTION_DOWN) { 146 Log.i(TAG, "Button click ignored while back button is pressed"); 147 // prevent interactions with other buttons while back button is pressed 148 return; 149 } 150 switch (buttonType) { 151 case BUTTON_BACK: 152 executeBack(/* keyEvent */ null); 153 break; 154 case BUTTON_HOME: 155 logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_TAP); 156 mSystemUiProxy.updateContextualEduStats(/* isTrackpadGesture= */ false, 157 GestureType.HOME); 158 navigateHome(); 159 break; 160 case BUTTON_RECENTS: 161 logEvent(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP); 162 mSystemUiProxy.updateContextualEduStats(/* isTrackpadGesture= */ false, 163 GestureType.OVERVIEW); 164 navigateToOverview(); 165 break; 166 case BUTTON_IME_SWITCH: 167 logEvent(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP); 168 onImeSwitcherPress(); 169 break; 170 case BUTTON_A11Y: 171 logEvent(LAUNCHER_TASKBAR_A11Y_BUTTON_TAP); 172 notifyA11yClick(false /* longClick */); 173 break; 174 case BUTTON_QUICK_SETTINGS: 175 showQuickSettings(); 176 break; 177 case BUTTON_NOTIFICATIONS: 178 showNotifications(); 179 break; 180 } 181 } 182 onButtonLongClick(@askbarButton int buttonType, View view)183 public boolean onButtonLongClick(@TaskbarButton int buttonType, View view) { 184 if (buttonType == BUTTON_SPACE) { 185 return false; 186 } 187 if (predictiveBackThreeButtonNav() && mLastSentBackAction == ACTION_DOWN 188 && buttonType != BUTTON_BACK && buttonType != BUTTON_RECENTS) { 189 // prevent interactions with other buttons while back button is pressed (except back 190 // and recents button for screen-unpin action). 191 Log.i(TAG, "Button long click ignored while back button is pressed"); 192 return false; 193 } 194 195 // Provide the same haptic feedback that the system offers for long press. 196 // The haptic feedback from long pressing on the home button is handled by circle to search. 197 // There are no haptics for long pressing the back button if predictive back is enabled 198 if (buttonType != BUTTON_HOME 199 && (!predictiveBackThreeButtonNav() || buttonType != BUTTON_BACK)) { 200 view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 201 } 202 switch (buttonType) { 203 case BUTTON_HOME: 204 logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS); 205 onLongPressHome(); 206 return true; 207 case BUTTON_A11Y: 208 logEvent(LAUNCHER_TASKBAR_A11Y_BUTTON_LONGPRESS); 209 notifyA11yClick(true /* longClick */); 210 return true; 211 case BUTTON_BACK: 212 logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS); 213 backRecentsLongpress(buttonType); 214 return true; 215 case BUTTON_RECENTS: 216 logEvent(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS); 217 backRecentsLongpress(buttonType); 218 return true; 219 case BUTTON_IME_SWITCH: 220 if (Flags.imeSwitcherRevamp()) { 221 logEvent(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_LONGPRESS); 222 onImeSwitcherLongPress(); 223 return true; 224 } 225 return false; 226 default: 227 return false; 228 } 229 } 230 getButtonContentDescription(@askbarButton int buttonType)231 public @StringRes int getButtonContentDescription(@TaskbarButton int buttonType) { 232 switch (buttonType) { 233 case BUTTON_HOME: 234 return R.string.taskbar_button_home; 235 case BUTTON_A11Y: 236 return R.string.taskbar_button_a11y; 237 case BUTTON_BACK: 238 return R.string.taskbar_button_back; 239 case BUTTON_IME_SWITCH: 240 return R.string.taskbar_button_ime_switcher; 241 case BUTTON_RECENTS: 242 return R.string.taskbar_button_recents; 243 case BUTTON_NOTIFICATIONS: 244 return R.string.taskbar_button_notifications; 245 case BUTTON_QUICK_SETTINGS: 246 return R.string.taskbar_button_quick_settings; 247 default: 248 return 0; 249 } 250 } 251 backRecentsLongpress(@askbarButton int buttonType)252 private boolean backRecentsLongpress(@TaskbarButton int buttonType) { 253 mLongPressedButtons |= buttonType; 254 return determineScreenUnpin(); 255 } 256 257 /** 258 * Checks if the user has long pressed back and recents buttons 259 * "together" (within {@link #SCREEN_PIN_LONG_PRESS_THRESHOLD})ms 260 * If so, then requests the system to turn off screen pinning. 261 * 262 * @return true if the long press is a valid user action in attempting to unpin an app 263 * Will always return {@code false} when screen pinning is not active. 264 * NOTE: Returning true does not mean that screen pinning has stopped 265 */ determineScreenUnpin()266 private boolean determineScreenUnpin() { 267 long timeNow = System.currentTimeMillis(); 268 if (!mScreenPinned) { 269 return false; 270 } 271 272 if (mLastScreenPinLongPress == 0) { 273 // First button long press registered, just mark time and wait for second button press 274 mLastScreenPinLongPress = System.currentTimeMillis(); 275 mHandler.postDelayed(mResetLongPress, SCREEN_PIN_LONG_PRESS_RESET); 276 return true; 277 } 278 279 if ((timeNow - mLastScreenPinLongPress) > SCREEN_PIN_LONG_PRESS_THRESHOLD) { 280 // Too long in-between presses, reset the clock 281 resetScreenUnpin(); 282 return false; 283 } 284 285 if ((mLongPressedButtons & SCREEN_UNPIN_COMBO) == SCREEN_UNPIN_COMBO) { 286 // Hooray! They did it (finally...) 287 mSystemUiProxy.stopScreenPinning(); 288 mHandler.removeCallbacks(mResetLongPress); 289 resetScreenUnpin(); 290 } 291 return true; 292 } 293 resetScreenUnpin()294 private void resetScreenUnpin() { 295 // if only back button was long pressed, navigate back like a single click back behavior. 296 if (mLongPressedButtons == BUTTON_BACK) { 297 executeBack(null); 298 } 299 mLongPressedButtons = 0; 300 mLastScreenPinLongPress = 0; 301 } 302 updateSysuiFlags(@ystemUiStateFlags long sysuiFlags)303 public void updateSysuiFlags(@SystemUiStateFlags long sysuiFlags) { 304 mScreenPinned = (sysuiFlags & SYSUI_STATE_SCREEN_PINNING) != 0; 305 } 306 init(TaskbarControllers taskbarControllers)307 public void init(TaskbarControllers taskbarControllers) { 308 mStatsLogManager = taskbarControllers.getTaskbarActivityContext().getStatsLogManager(); 309 } 310 onDestroy()311 public void onDestroy() { 312 mStatsLogManager = null; 313 } 314 setAssistantLongPressEnabled(boolean assistantLongPressEnabled)315 public void setAssistantLongPressEnabled(boolean assistantLongPressEnabled) { 316 mAssistantLongPressEnabled = assistantLongPressEnabled; 317 } 318 logEvent(StatsLogManager.LauncherEvent event)319 private void logEvent(StatsLogManager.LauncherEvent event) { 320 if (mStatsLogManager == null) { 321 Log.w(TAG, "No stats log manager to log taskbar button event"); 322 return; 323 } 324 mStatsLogManager.logger().log(event); 325 } 326 navigateHome()327 private void navigateHome() { 328 TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY); 329 mCallbacks.onNavigateHome(); 330 } 331 navigateToOverview()332 private void navigateToOverview() { 333 if (mScreenPinned) { 334 return; 335 } 336 TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle"); 337 TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); 338 mCallbacks.onToggleOverview(); 339 } 340 hideOverview()341 public void hideOverview() { 342 mCallbacks.onHideOverview(); 343 } 344 sendBackKeyEvent(int action, boolean cancelled)345 void sendBackKeyEvent(int action, boolean cancelled) { 346 if (action == mLastSentBackAction) { 347 // There must always be an alternating sequence of ACTION_DOWN and ACTION_UP events 348 return; 349 } 350 long time = SystemClock.uptimeMillis(); 351 KeyEvent keyEvent = new KeyEvent(time, time, action, KeyEvent.KEYCODE_BACK, 0); 352 if (cancelled) { 353 keyEvent.cancel(); 354 } 355 executeBack(keyEvent); 356 } 357 executeBack(@ullable KeyEvent keyEvent)358 private void executeBack(@Nullable KeyEvent keyEvent) { 359 if (keyEvent == null || (keyEvent.getAction() == ACTION_UP && !keyEvent.isCanceled())) { 360 logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_TAP); 361 mSystemUiProxy.updateContextualEduStats(/* isTrackpadGesture= */ false, 362 GestureType.BACK); 363 } 364 mSystemUiProxy.onBackEvent(keyEvent); 365 mLastSentBackAction = keyEvent != null ? keyEvent.getAction() : ACTION_UP; 366 } 367 onImeSwitcherPress()368 private void onImeSwitcherPress() { 369 mSystemUiProxy.onImeSwitcherPressed(); 370 } 371 onImeSwitcherLongPress()372 private void onImeSwitcherLongPress() { 373 mSystemUiProxy.onImeSwitcherLongPress(); 374 } 375 notifyA11yClick(boolean longClick)376 private void notifyA11yClick(boolean longClick) { 377 if (longClick) { 378 mSystemUiProxy.notifyAccessibilityButtonLongClicked(); 379 } else { 380 mSystemUiProxy.notifyAccessibilityButtonClicked(mContext.getDisplayId()); 381 } 382 } 383 onLongPressHome()384 private void onLongPressHome() { 385 if (mScreenPinned || !mAssistantLongPressEnabled) { 386 return; 387 } 388 // Attempt to start Contextual Search, otherwise fall back to SysUi's implementation. 389 if (!mContextualSearchInvoker.tryStartAssistOverride( 390 INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) { 391 Bundle args = new Bundle(); 392 args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS); 393 mSystemUiProxy.startAssistant(args); 394 } 395 } 396 showQuickSettings()397 private void showQuickSettings() { 398 mSystemUiProxy.toggleNotificationPanel(); 399 } 400 showNotifications()401 private void showNotifications() { 402 mSystemUiProxy.toggleNotificationPanel(); 403 } 404 405 /** Callbacks for navigation buttons on Taskbar. */ 406 public interface TaskbarNavButtonCallbacks { 407 /** Callback invoked when the home button is pressed. */ onNavigateHome()408 default void onNavigateHome() {} 409 410 /** Callback invoked when the overview button is pressed. */ onToggleOverview()411 default void onToggleOverview() {} 412 413 /** Callback invoken when a visible overview needs to be hidden. */ onHideOverview()414 default void onHideOverview() { } 415 } 416 } 417