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 
20 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS;
21 import static com.android.internal.app.AssistUtils.INVOCATION_TYPE_KEY;
22 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
23 
24 import android.os.Bundle;
25 import android.os.Handler;
26 
27 import androidx.annotation.IntDef;
28 
29 import com.android.launcher3.testing.TestLogging;
30 import com.android.launcher3.testing.TestProtocol;
31 import com.android.quickstep.OverviewCommandHelper;
32 import com.android.quickstep.SystemUiProxy;
33 import com.android.quickstep.TouchInteractionService;
34 
35 import java.lang.annotation.Retention;
36 import java.lang.annotation.RetentionPolicy;
37 
38 /**
39  * Controller for 3 button mode in the taskbar.
40  * Handles all the functionality of the various buttons, making/routing the right calls into
41  * launcher or sysui/system.
42  */
43 public class TaskbarNavButtonController {
44 
45     /** Allow some time in between the long press for back and recents. */
46     static final int SCREEN_PIN_LONG_PRESS_THRESHOLD = 200;
47     static final int SCREEN_PIN_LONG_PRESS_RESET = SCREEN_PIN_LONG_PRESS_THRESHOLD + 100;
48 
49     private long mLastScreenPinLongPress;
50     private boolean mScreenPinned;
51 
52     @Retention(RetentionPolicy.SOURCE)
53     @IntDef(value = {
54             BUTTON_BACK,
55             BUTTON_HOME,
56             BUTTON_RECENTS,
57             BUTTON_IME_SWITCH,
58             BUTTON_A11Y,
59     })
60 
61     public @interface TaskbarButton {}
62 
63     static final int BUTTON_BACK = 1;
64     static final int BUTTON_HOME = BUTTON_BACK << 1;
65     static final int BUTTON_RECENTS = BUTTON_HOME << 1;
66     static final int BUTTON_IME_SWITCH = BUTTON_RECENTS << 1;
67     static final int BUTTON_A11Y = BUTTON_IME_SWITCH << 1;
68 
69     private static final int SCREEN_UNPIN_COMBO = BUTTON_BACK | BUTTON_RECENTS;
70     private int mLongPressedButtons = 0;
71 
72     private final TouchInteractionService mService;
73     private final SystemUiProxy mSystemUiProxy;
74     private final Handler mHandler;
75 
76     private final Runnable mResetLongPress = this::resetScreenUnpin;
77 
TaskbarNavButtonController(TouchInteractionService service, SystemUiProxy systemUiProxy, Handler handler)78     public TaskbarNavButtonController(TouchInteractionService service,
79             SystemUiProxy systemUiProxy, Handler handler) {
80         mService = service;
81         mSystemUiProxy = systemUiProxy;
82         mHandler = handler;
83     }
84 
onButtonClick(@askbarButton int buttonType)85     public void onButtonClick(@TaskbarButton int buttonType) {
86         switch (buttonType) {
87             case BUTTON_BACK:
88                 executeBack();
89                 break;
90             case BUTTON_HOME:
91                 navigateHome();
92                 break;
93             case BUTTON_RECENTS:
94                 navigateToOverview();
95                 break;
96             case BUTTON_IME_SWITCH:
97                 showIMESwitcher();
98                 break;
99             case BUTTON_A11Y:
100                 notifyA11yClick(false /* longClick */);
101                 break;
102         }
103     }
104 
onButtonLongClick(@askbarButton int buttonType)105     public boolean onButtonLongClick(@TaskbarButton int buttonType) {
106         switch (buttonType) {
107             case BUTTON_HOME:
108                 startAssistant();
109                 return true;
110             case BUTTON_A11Y:
111                 notifyA11yClick(true /* longClick */);
112                 return true;
113             case BUTTON_BACK:
114             case BUTTON_RECENTS:
115                 mLongPressedButtons |= buttonType;
116                 return determineScreenUnpin();
117             case BUTTON_IME_SWITCH:
118             default:
119                 return false;
120         }
121     }
122 
123     /**
124      * Checks if the user has long pressed back and recents buttons
125      * "together" (within {@link #SCREEN_PIN_LONG_PRESS_THRESHOLD})ms
126      * If so, then requests the system to turn off screen pinning.
127      *
128      * @return true if the long press is a valid user action in attempting to unpin an app
129      *         Will always return {@code false} when screen pinning is not active.
130      *         NOTE: Returning true does not mean that screen pinning has stopped
131      */
determineScreenUnpin()132     private boolean determineScreenUnpin() {
133         long timeNow = System.currentTimeMillis();
134         if (!mScreenPinned) {
135             return false;
136         }
137 
138         if (mLastScreenPinLongPress == 0) {
139             // First button long press registered, just mark time and wait for second button press
140             mLastScreenPinLongPress = System.currentTimeMillis();
141             mHandler.postDelayed(mResetLongPress, SCREEN_PIN_LONG_PRESS_RESET);
142             return true;
143         }
144 
145         if ((timeNow - mLastScreenPinLongPress) > SCREEN_PIN_LONG_PRESS_THRESHOLD) {
146             // Too long in-between presses, reset the clock
147             resetScreenUnpin();
148             return false;
149         }
150 
151         if ((mLongPressedButtons & SCREEN_UNPIN_COMBO) == SCREEN_UNPIN_COMBO) {
152             // Hooray! They did it (finally...)
153             mSystemUiProxy.stopScreenPinning();
154             mHandler.removeCallbacks(mResetLongPress);
155             resetScreenUnpin();
156         }
157         return true;
158     }
159 
resetScreenUnpin()160     private void resetScreenUnpin() {
161         mLongPressedButtons = 0;
162         mLastScreenPinLongPress = 0;
163     }
164 
updateSysuiFlags(int sysuiFlags)165     public void updateSysuiFlags(int sysuiFlags) {
166         mScreenPinned = (sysuiFlags & SYSUI_STATE_SCREEN_PINNING) != 0;
167     }
168 
navigateHome()169     private void navigateHome() {
170         mService.getOverviewCommandHelper().addCommand(OverviewCommandHelper.TYPE_HOME);
171     }
172 
navigateToOverview()173     private void navigateToOverview() {
174         if (mScreenPinned) {
175             return;
176         }
177         TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle");
178         mService.getOverviewCommandHelper().addCommand(OverviewCommandHelper.TYPE_TOGGLE);
179     }
180 
executeBack()181     private void executeBack() {
182         mSystemUiProxy.onBackPressed();
183     }
184 
showIMESwitcher()185     private void showIMESwitcher() {
186         mSystemUiProxy.onImeSwitcherPressed();
187     }
188 
notifyA11yClick(boolean longClick)189     private void notifyA11yClick(boolean longClick) {
190         if (longClick) {
191             mSystemUiProxy.notifyAccessibilityButtonLongClicked();
192         } else {
193             mSystemUiProxy.notifyAccessibilityButtonClicked(mService.getDisplayId());
194         }
195     }
196 
startAssistant()197     private void startAssistant() {
198         if (mScreenPinned) {
199             return;
200         }
201         Bundle args = new Bundle();
202         args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS);
203         mSystemUiProxy.startAssistant(args);
204     }
205 }
206