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 com.android.internal.app.AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS;
20 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_KEY;
21 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_A11Y_BUTTON_LONGPRESS;
22 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_A11Y_BUTTON_TAP;
23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS;
24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_BACK_BUTTON_TAP;
25 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS;
26 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_HOME_BUTTON_TAP;
27 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP;
28 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS;
29 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP;
30 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
31 import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;
32 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
33 
34 import android.content.Context;
35 import android.os.Bundle;
36 import android.os.Handler;
37 import android.util.Log;
38 import android.view.HapticFeedbackConstants;
39 import android.view.View;
40 
41 import androidx.annotation.IntDef;
42 import androidx.annotation.Nullable;
43 import androidx.annotation.StringRes;
44 
45 import com.android.launcher3.R;
46 import com.android.launcher3.logging.StatsLogManager;
47 import com.android.launcher3.testing.TestLogging;
48 import com.android.launcher3.testing.shared.TestProtocol;
49 import com.android.quickstep.SystemUiProxy;
50 import com.android.quickstep.TaskUtils;
51 import com.android.quickstep.util.AssistUtils;
52 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
53 
54 import java.io.PrintWriter;
55 import java.lang.annotation.Retention;
56 import java.lang.annotation.RetentionPolicy;
57 
58 /**
59  * Controller for 3 button mode in the taskbar.
60  * Handles all the functionality of the various buttons, making/routing the right calls into
61  * launcher or sysui/system.
62  */
63 public class TaskbarNavButtonController implements TaskbarControllers.LoggableTaskbarController {
64 
65     /** Allow some time in between the long press for back and recents. */
66     static final int SCREEN_PIN_LONG_PRESS_THRESHOLD = 200;
67     static final int SCREEN_PIN_LONG_PRESS_RESET = SCREEN_PIN_LONG_PRESS_THRESHOLD + 100;
68     private static final String TAG = "TaskbarNavButtonController";
69 
70     private long mLastScreenPinLongPress;
71     private boolean mScreenPinned;
72     private boolean mAssistantLongPressEnabled;
73 
74     @Override
dumpLogs(String prefix, PrintWriter pw)75     public void dumpLogs(String prefix, PrintWriter pw) {
76         pw.println(prefix + "TaskbarNavButtonController:");
77 
78         pw.println(prefix + "\tmLastScreenPinLongPress=" + mLastScreenPinLongPress);
79         pw.println(prefix + "\tmScreenPinned=" + mScreenPinned);
80     }
81 
82     @Retention(RetentionPolicy.SOURCE)
83     @IntDef(value = {
84             BUTTON_BACK,
85             BUTTON_HOME,
86             BUTTON_RECENTS,
87             BUTTON_IME_SWITCH,
88             BUTTON_A11Y,
89             BUTTON_QUICK_SETTINGS,
90             BUTTON_NOTIFICATIONS,
91     })
92 
93     public @interface TaskbarButton {}
94 
95     static final int BUTTON_BACK = 1;
96     static final int BUTTON_HOME = BUTTON_BACK << 1;
97     static final int BUTTON_RECENTS = BUTTON_HOME << 1;
98     static final int BUTTON_IME_SWITCH = BUTTON_RECENTS << 1;
99     static final int BUTTON_A11Y = BUTTON_IME_SWITCH << 1;
100     static final int BUTTON_QUICK_SETTINGS = BUTTON_A11Y << 1;
101     static final int BUTTON_NOTIFICATIONS = BUTTON_QUICK_SETTINGS << 1;
102     static final int BUTTON_SPACE = BUTTON_NOTIFICATIONS << 1;
103 
104     private static final int SCREEN_UNPIN_COMBO = BUTTON_BACK | BUTTON_RECENTS;
105     private int mLongPressedButtons = 0;
106 
107     private final Context mContext;
108     private final TaskbarNavButtonCallbacks mCallbacks;
109     private final SystemUiProxy mSystemUiProxy;
110     private final Handler mHandler;
111     private final AssistUtils mAssistUtils;
112     @Nullable private StatsLogManager mStatsLogManager;
113 
114     private final Runnable mResetLongPress = this::resetScreenUnpin;
115 
TaskbarNavButtonController( Context context, TaskbarNavButtonCallbacks callbacks, SystemUiProxy systemUiProxy, Handler handler, AssistUtils assistUtils)116     public TaskbarNavButtonController(
117             Context context,
118             TaskbarNavButtonCallbacks callbacks,
119             SystemUiProxy systemUiProxy,
120             Handler handler,
121             AssistUtils assistUtils) {
122         mContext = context;
123         mCallbacks = callbacks;
124         mSystemUiProxy = systemUiProxy;
125         mHandler = handler;
126         mAssistUtils = assistUtils;
127     }
128 
onButtonClick(@askbarButton int buttonType, View view)129     public void onButtonClick(@TaskbarButton int buttonType, View view) {
130         if (buttonType == BUTTON_SPACE) {
131             return;
132         }
133         // Provide the same haptic feedback that the system offers for virtual keys.
134         view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
135         switch (buttonType) {
136             case BUTTON_BACK:
137                 logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_TAP);
138                 executeBack();
139                 break;
140             case BUTTON_HOME:
141                 logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_TAP);
142                 navigateHome();
143                 break;
144             case BUTTON_RECENTS:
145                 logEvent(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_TAP);
146                 navigateToOverview();
147                 break;
148             case BUTTON_IME_SWITCH:
149                 logEvent(LAUNCHER_TASKBAR_IME_SWITCHER_BUTTON_TAP);
150                 showIMESwitcher();
151                 break;
152             case BUTTON_A11Y:
153                 logEvent(LAUNCHER_TASKBAR_A11Y_BUTTON_TAP);
154                 notifyA11yClick(false /* longClick */);
155                 break;
156             case BUTTON_QUICK_SETTINGS:
157                 showQuickSettings();
158                 break;
159             case BUTTON_NOTIFICATIONS:
160                 showNotifications();
161                 break;
162         }
163     }
164 
onButtonLongClick(@askbarButton int buttonType, View view)165     public boolean onButtonLongClick(@TaskbarButton int buttonType, View view) {
166         if (buttonType == BUTTON_SPACE) {
167             return false;
168         }
169         // Provide the same haptic feedback that the system offers for virtual keys.
170         view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
171         switch (buttonType) {
172             case BUTTON_HOME:
173                 logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS);
174                 onLongPressHome();
175                 return true;
176             case BUTTON_A11Y:
177                 logEvent(LAUNCHER_TASKBAR_A11Y_BUTTON_LONGPRESS);
178                 notifyA11yClick(true /* longClick */);
179                 return true;
180             case BUTTON_BACK:
181                 logEvent(LAUNCHER_TASKBAR_BACK_BUTTON_LONGPRESS);
182                 return backRecentsLongpress(buttonType);
183             case BUTTON_RECENTS:
184                 logEvent(LAUNCHER_TASKBAR_OVERVIEW_BUTTON_LONGPRESS);
185                 return backRecentsLongpress(buttonType);
186             case BUTTON_IME_SWITCH:
187             default:
188                 return false;
189         }
190     }
191 
getButtonContentDescription(@askbarButton int buttonType)192     public @StringRes int getButtonContentDescription(@TaskbarButton int buttonType) {
193         switch (buttonType) {
194             case BUTTON_HOME:
195                 return R.string.taskbar_button_home;
196             case BUTTON_A11Y:
197                 return R.string.taskbar_button_a11y;
198             case BUTTON_BACK:
199                 return R.string.taskbar_button_back;
200             case BUTTON_IME_SWITCH:
201                 return R.string.taskbar_button_ime_switcher;
202             case BUTTON_RECENTS:
203                 return R.string.taskbar_button_recents;
204             case BUTTON_NOTIFICATIONS:
205                 return R.string.taskbar_button_notifications;
206             case BUTTON_QUICK_SETTINGS:
207                 return R.string.taskbar_button_quick_settings;
208             default:
209                 return 0;
210         }
211     }
212 
backRecentsLongpress(@askbarButton int buttonType)213     private boolean backRecentsLongpress(@TaskbarButton int buttonType) {
214         mLongPressedButtons |= buttonType;
215         return determineScreenUnpin();
216     }
217 
218     /**
219      * Checks if the user has long pressed back and recents buttons
220      * "together" (within {@link #SCREEN_PIN_LONG_PRESS_THRESHOLD})ms
221      * If so, then requests the system to turn off screen pinning.
222      *
223      * @return true if the long press is a valid user action in attempting to unpin an app
224      *         Will always return {@code false} when screen pinning is not active.
225      *         NOTE: Returning true does not mean that screen pinning has stopped
226      */
determineScreenUnpin()227     private boolean determineScreenUnpin() {
228         long timeNow = System.currentTimeMillis();
229         if (!mScreenPinned) {
230             return false;
231         }
232 
233         if (mLastScreenPinLongPress == 0) {
234             // First button long press registered, just mark time and wait for second button press
235             mLastScreenPinLongPress = System.currentTimeMillis();
236             mHandler.postDelayed(mResetLongPress, SCREEN_PIN_LONG_PRESS_RESET);
237             return true;
238         }
239 
240         if ((timeNow - mLastScreenPinLongPress) > SCREEN_PIN_LONG_PRESS_THRESHOLD) {
241             // Too long in-between presses, reset the clock
242             resetScreenUnpin();
243             return false;
244         }
245 
246         if ((mLongPressedButtons & SCREEN_UNPIN_COMBO) == SCREEN_UNPIN_COMBO) {
247             // Hooray! They did it (finally...)
248             mSystemUiProxy.stopScreenPinning();
249             mHandler.removeCallbacks(mResetLongPress);
250             resetScreenUnpin();
251         }
252         return true;
253     }
254 
resetScreenUnpin()255     private void resetScreenUnpin() {
256         mLongPressedButtons = 0;
257         mLastScreenPinLongPress = 0;
258     }
259 
updateSysuiFlags(@ystemUiStateFlags long sysuiFlags)260     public void updateSysuiFlags(@SystemUiStateFlags long sysuiFlags) {
261         mScreenPinned = (sysuiFlags & SYSUI_STATE_SCREEN_PINNING) != 0;
262     }
263 
init(TaskbarControllers taskbarControllers)264     public void init(TaskbarControllers taskbarControllers) {
265         mStatsLogManager = taskbarControllers.getTaskbarActivityContext().getStatsLogManager();
266     }
267 
onDestroy()268     public void onDestroy() {
269         mStatsLogManager = null;
270     }
271 
setAssistantLongPressEnabled(boolean assistantLongPressEnabled)272     public void setAssistantLongPressEnabled(boolean assistantLongPressEnabled) {
273         mAssistantLongPressEnabled = assistantLongPressEnabled;
274     }
275 
logEvent(StatsLogManager.LauncherEvent event)276     private void logEvent(StatsLogManager.LauncherEvent event) {
277         if (mStatsLogManager == null) {
278             Log.w(TAG, "No stats log manager to log taskbar button event");
279             return;
280         }
281         mStatsLogManager.logger().log(event);
282     }
283 
navigateHome()284     private void navigateHome() {
285         TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY);
286         mCallbacks.onNavigateHome();
287     }
288 
navigateToOverview()289     private void navigateToOverview() {
290         if (mScreenPinned) {
291             return;
292         }
293         TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle");
294         TaskUtils.closeSystemWindowsAsync(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS);
295         mCallbacks.onToggleOverview();
296     }
297 
executeBack()298     private void executeBack() {
299         mSystemUiProxy.onBackPressed();
300     }
301 
showIMESwitcher()302     private void showIMESwitcher() {
303         mSystemUiProxy.onImeSwitcherPressed();
304     }
305 
notifyA11yClick(boolean longClick)306     private void notifyA11yClick(boolean longClick) {
307         if (longClick) {
308             mSystemUiProxy.notifyAccessibilityButtonLongClicked();
309         } else {
310             mSystemUiProxy.notifyAccessibilityButtonClicked(mContext.getDisplayId());
311         }
312     }
313 
onLongPressHome()314     private void onLongPressHome() {
315         if (mScreenPinned || !mAssistantLongPressEnabled) {
316             return;
317         }
318         // Attempt to start Assist with AssistUtils, otherwise fall back to SysUi's implementation.
319         if (!mAssistUtils.tryStartAssistOverride(INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) {
320             Bundle args = new Bundle();
321             args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
322             mSystemUiProxy.startAssistant(args);
323         }
324     }
325 
showQuickSettings()326     private void showQuickSettings() {
327         mSystemUiProxy.toggleNotificationPanel();
328     }
329 
showNotifications()330     private void showNotifications() {
331         mSystemUiProxy.toggleNotificationPanel();
332     }
333 
334     /** Callbacks for navigation buttons on Taskbar. */
335     public interface TaskbarNavButtonCallbacks {
336         /** Callback invoked when the home button is pressed. */
onNavigateHome()337         default void onNavigateHome() {}
338 
339         /** Callback invoked when the overview button is pressed. */
onToggleOverview()340         default void onToggleOverview() {}
341     }
342 }
343