1 /*
2  * Copyright (C) 2019 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.systemui.assist;
18 
19 import static com.android.systemui.assist.AssistModule.ASSIST_HANDLE_THREAD_NAME;
20 
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.os.Handler;
24 import android.os.SystemClock;
25 import android.util.Log;
26 
27 import androidx.annotation.Nullable;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.internal.app.AssistUtils;
31 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
32 import com.android.keyguard.KeyguardUpdateMonitor;
33 import com.android.systemui.DumpController;
34 import com.android.systemui.Dumpable;
35 import com.android.systemui.ScreenDecorations;
36 import com.android.systemui.shared.system.QuickStepContract;
37 import com.android.systemui.statusbar.phone.NavigationModeController;
38 
39 import java.io.FileDescriptor;
40 import java.io.PrintWriter;
41 import java.util.Map;
42 import java.util.concurrent.TimeUnit;
43 
44 import javax.inject.Inject;
45 import javax.inject.Named;
46 import javax.inject.Provider;
47 import javax.inject.Singleton;
48 
49 /**
50  * A class for managing Assistant handle logic.
51  *
52  * Controls when visual handles for Assistant gesture affordance should be shown or hidden using an
53  * {@link AssistHandleBehavior}.
54  */
55 @Singleton
56 public final class AssistHandleBehaviorController implements AssistHandleCallbacks, Dumpable {
57 
58     private static final String TAG = "AssistHandleBehavior";
59 
60     private static final long DEFAULT_SHOWN_FREQUENCY_THRESHOLD_MS = 0;
61     private static final long DEFAULT_SHOW_AND_GO_DURATION_MS = TimeUnit.SECONDS.toMillis(3);
62 
63     /**
64      * This is the default behavior that will be used once the system is up. It will be set once the
65      * behavior dependencies are available. This ensures proper behavior lifecycle.
66      */
67     private static final AssistHandleBehavior DEFAULT_BEHAVIOR = AssistHandleBehavior.REMINDER_EXP;
68 
69     private final Context mContext;
70     private final AssistUtils mAssistUtils;
71     private final Handler mHandler;
72     private final Runnable mHideHandles = this::hideHandles;
73     private final Runnable mShowAndGo = this::showAndGoInternal;
74     private final Provider<ScreenDecorations> mScreenDecorations;
75     private final DeviceConfigHelper mDeviceConfigHelper;
76     private final Map<AssistHandleBehavior, BehaviorController> mBehaviorMap;
77 
78     private boolean mHandlesShowing = false;
79     private long mHandlesLastHiddenAt;
80     private long mShowAndGoEndsAt;
81     /**
82      * This should always be initialized as {@link AssistHandleBehavior#OFF} to ensure proper
83      * behavior lifecycle.
84      */
85     private AssistHandleBehavior mCurrentBehavior = AssistHandleBehavior.OFF;
86     private boolean mInGesturalMode;
87 
88     @Inject
AssistHandleBehaviorController( Context context, AssistUtils assistUtils, @Named(ASSIST_HANDLE_THREAD_NAME) Handler handler, Provider<ScreenDecorations> screenDecorations, DeviceConfigHelper deviceConfigHelper, Map<AssistHandleBehavior, BehaviorController> behaviorMap, NavigationModeController navigationModeController, DumpController dumpController)89     AssistHandleBehaviorController(
90             Context context,
91             AssistUtils assistUtils,
92             @Named(ASSIST_HANDLE_THREAD_NAME) Handler handler,
93             Provider<ScreenDecorations> screenDecorations,
94             DeviceConfigHelper deviceConfigHelper,
95             Map<AssistHandleBehavior, BehaviorController> behaviorMap,
96             NavigationModeController navigationModeController,
97             DumpController dumpController) {
98         mContext = context;
99         mAssistUtils = assistUtils;
100         mHandler = handler;
101         mScreenDecorations = screenDecorations;
102         mDeviceConfigHelper = deviceConfigHelper;
103         mBehaviorMap = behaviorMap;
104 
105         mInGesturalMode = QuickStepContract.isGesturalMode(
106                 navigationModeController.addListener(this::handleNavigationModeChange));
107 
108         setBehavior(getBehaviorMode());
109         mDeviceConfigHelper.addOnPropertiesChangedListener(
110                 mHandler::post,
111                 (properties) -> {
112                     if (properties.getKeyset().contains(
113                             SystemUiDeviceConfigFlags.ASSIST_HANDLES_BEHAVIOR_MODE)) {
114                         setBehavior(properties.getString(
115                                 SystemUiDeviceConfigFlags.ASSIST_HANDLES_BEHAVIOR_MODE, null));
116                     }
117                 });
118 
119         dumpController.addListener(this);
120     }
121 
122     @Override // AssistHandleCallbacks
hide()123     public void hide() {
124         clearPendingCommands();
125         mHandler.post(mHideHandles);
126     }
127 
128     @Override // AssistHandleCallbacks
showAndGo()129     public void showAndGo() {
130         clearPendingCommands();
131         mHandler.post(mShowAndGo);
132     }
133 
showAndGoInternal()134     private void showAndGoInternal() {
135         maybeShowHandles(/* ignoreThreshold = */ false);
136         long showAndGoDuration = getShowAndGoDuration();
137         mShowAndGoEndsAt = SystemClock.elapsedRealtime() + showAndGoDuration;
138         mHandler.postDelayed(mHideHandles, showAndGoDuration);
139     }
140 
141     @Override // AssistHandleCallbacks
showAndGoDelayed(long delayMs, boolean hideIfShowing)142     public void showAndGoDelayed(long delayMs, boolean hideIfShowing) {
143         clearPendingCommands();
144         if (hideIfShowing) {
145             mHandler.post(mHideHandles);
146         }
147         mHandler.postDelayed(mShowAndGo, delayMs);
148     }
149 
150     @Override // AssistHandleCallbacks
showAndStay()151     public void showAndStay() {
152         clearPendingCommands();
153         mHandler.post(() -> maybeShowHandles(/* ignoreThreshold = */ true));
154     }
155 
getShowAndGoRemainingTimeMs()156     public long getShowAndGoRemainingTimeMs() {
157         return Long.max(mShowAndGoEndsAt - SystemClock.elapsedRealtime(), 0);
158     }
159 
areHandlesShowing()160     boolean areHandlesShowing() {
161         return mHandlesShowing;
162     }
163 
onAssistantGesturePerformed()164     void onAssistantGesturePerformed() {
165         mBehaviorMap.get(mCurrentBehavior).onAssistantGesturePerformed();
166     }
167 
onAssistHandlesRequested()168     void onAssistHandlesRequested() {
169         if (mInGesturalMode) {
170             mBehaviorMap.get(mCurrentBehavior).onAssistHandlesRequested();
171         }
172     }
173 
setBehavior(AssistHandleBehavior behavior)174     void setBehavior(AssistHandleBehavior behavior) {
175         if (mCurrentBehavior == behavior) {
176             return;
177         }
178 
179         if (!mBehaviorMap.containsKey(behavior)) {
180             Log.e(TAG, "Unsupported behavior requested: " + behavior.toString());
181             return;
182         }
183 
184         if (mInGesturalMode) {
185             mBehaviorMap.get(mCurrentBehavior).onModeDeactivated();
186             mBehaviorMap.get(behavior).onModeActivated(mContext, /* callbacks = */ this);
187         }
188 
189         mCurrentBehavior = behavior;
190     }
191 
setBehavior(@ullable String behavior)192     private void setBehavior(@Nullable String behavior) {
193         try {
194             setBehavior(AssistHandleBehavior.valueOf(behavior));
195         } catch (IllegalArgumentException | NullPointerException e) {
196             Log.e(TAG, "Invalid behavior: " + behavior, e);
197         }
198     }
199 
handlesUnblocked(boolean ignoreThreshold)200     private boolean handlesUnblocked(boolean ignoreThreshold) {
201         long timeSinceHidden = SystemClock.elapsedRealtime() - mHandlesLastHiddenAt;
202         boolean notThrottled = ignoreThreshold || timeSinceHidden >= getShownFrequencyThreshold();
203         ComponentName assistantComponent =
204                 mAssistUtils.getAssistComponentForUser(KeyguardUpdateMonitor.getCurrentUser());
205         return notThrottled && assistantComponent != null;
206     }
207 
getShownFrequencyThreshold()208     private long getShownFrequencyThreshold() {
209         return mDeviceConfigHelper.getLong(
210                 SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOWN_FREQUENCY_THRESHOLD_MS,
211                 DEFAULT_SHOWN_FREQUENCY_THRESHOLD_MS);
212     }
213 
getShowAndGoDuration()214     private long getShowAndGoDuration() {
215         return mDeviceConfigHelper.getLong(
216                 SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOW_AND_GO_DURATION_MS,
217                 DEFAULT_SHOW_AND_GO_DURATION_MS);
218     }
219 
getBehaviorMode()220     private String getBehaviorMode() {
221         return mDeviceConfigHelper.getString(
222                 SystemUiDeviceConfigFlags.ASSIST_HANDLES_BEHAVIOR_MODE,
223                 DEFAULT_BEHAVIOR.toString());
224     }
225 
maybeShowHandles(boolean ignoreThreshold)226     private void maybeShowHandles(boolean ignoreThreshold) {
227         if (mHandlesShowing) {
228             return;
229         }
230 
231         if (handlesUnblocked(ignoreThreshold)) {
232             ScreenDecorations screenDecorations = mScreenDecorations.get();
233             if (screenDecorations == null) {
234                 Log.w(TAG, "Couldn't show handles, ScreenDecorations unavailable");
235             } else {
236                 mHandlesShowing = true;
237                 screenDecorations.setAssistHintVisible(true);
238             }
239         }
240     }
241 
hideHandles()242     private void hideHandles() {
243         if (!mHandlesShowing) {
244             return;
245         }
246 
247         ScreenDecorations screenDecorations = mScreenDecorations.get();
248         if (screenDecorations == null) {
249             Log.w(TAG, "Couldn't hide handles, ScreenDecorations unavailable");
250         } else {
251             mHandlesShowing = false;
252             mHandlesLastHiddenAt = SystemClock.elapsedRealtime();
253             screenDecorations.setAssistHintVisible(false);
254         }
255     }
256 
handleNavigationModeChange(int navigationMode)257     private void handleNavigationModeChange(int navigationMode) {
258         boolean inGesturalMode = QuickStepContract.isGesturalMode(navigationMode);
259         if (mInGesturalMode == inGesturalMode) {
260             return;
261         }
262 
263         mInGesturalMode = inGesturalMode;
264         if (mInGesturalMode) {
265             mBehaviorMap.get(mCurrentBehavior).onModeActivated(mContext, /* callbacks = */ this);
266         } else {
267             mBehaviorMap.get(mCurrentBehavior).onModeDeactivated();
268             hide();
269         }
270     }
271 
clearPendingCommands()272     private void clearPendingCommands() {
273         mHandler.removeCallbacks(mHideHandles);
274         mHandler.removeCallbacks(mShowAndGo);
275         mShowAndGoEndsAt = 0;
276     }
277 
278     @VisibleForTesting
setInGesturalModeForTest(boolean inGesturalMode)279     void setInGesturalModeForTest(boolean inGesturalMode) {
280         mInGesturalMode = inGesturalMode;
281     }
282 
283     @Override // Dumpable
dump(FileDescriptor fd, PrintWriter pw, String[] args)284     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
285         pw.println("Current AssistHandleBehaviorController State:");
286 
287         pw.println("   mHandlesShowing=" + mHandlesShowing);
288         pw.println("   mHandlesLastHiddenAt=" + mHandlesLastHiddenAt);
289         pw.println("   mInGesturalMode=" + mInGesturalMode);
290 
291         pw.println("   Phenotype Flags:");
292         pw.println("      "
293                 + SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOW_AND_GO_DURATION_MS
294                 + "="
295                 + getShowAndGoDuration());
296         pw.println("      "
297                 + SystemUiDeviceConfigFlags.ASSIST_HANDLES_SHOWN_FREQUENCY_THRESHOLD_MS
298                 + "="
299                 + getShownFrequencyThreshold());
300         pw.println("      "
301                 + SystemUiDeviceConfigFlags.ASSIST_HANDLES_BEHAVIOR_MODE
302                 + "="
303                 + getBehaviorMode());
304 
305         pw.println("   mCurrentBehavior=" + mCurrentBehavior.toString());
306         mBehaviorMap.get(mCurrentBehavior).dump(pw, "   ");
307     }
308 
309     interface BehaviorController {
onModeActivated(Context context, AssistHandleCallbacks callbacks)310         void onModeActivated(Context context, AssistHandleCallbacks callbacks);
onModeDeactivated()311         default void onModeDeactivated() {}
onAssistantGesturePerformed()312         default void onAssistantGesturePerformed() {}
onAssistHandlesRequested()313         default void onAssistHandlesRequested() {}
dump(PrintWriter pw, String prefix)314         default void dump(PrintWriter pw, String prefix) {}
315     }
316 }
317