• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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