1 package com.android.systemui.assist;
2 
3 import static android.view.Display.DEFAULT_DISPLAY;
4 
5 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED;
6 
7 import android.annotation.NonNull;
8 import android.annotation.Nullable;
9 import android.app.ActivityManager;
10 import android.app.ActivityOptions;
11 import android.app.SearchManager;
12 import android.content.ActivityNotFoundException;
13 import android.content.ComponentName;
14 import android.content.Context;
15 import android.content.Intent;
16 import android.content.pm.ActivityInfo;
17 import android.content.pm.PackageManager;
18 import android.content.res.Configuration;
19 import android.content.res.Resources;
20 import android.graphics.PixelFormat;
21 import android.metrics.LogMaker;
22 import android.os.AsyncTask;
23 import android.os.Binder;
24 import android.os.Bundle;
25 import android.os.Handler;
26 import android.os.RemoteException;
27 import android.os.SystemClock;
28 import android.os.UserHandle;
29 import android.provider.Settings;
30 import android.service.voice.VoiceInteractionSession;
31 import android.util.Log;
32 import android.view.Gravity;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.view.WindowManager;
37 import android.widget.ImageView;
38 
39 import com.android.internal.app.AssistUtils;
40 import com.android.internal.app.IVoiceInteractionSessionListener;
41 import com.android.internal.app.IVoiceInteractionSessionShowCallback;
42 import com.android.internal.logging.MetricsLogger;
43 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
44 import com.android.keyguard.KeyguardUpdateMonitor;
45 import com.android.settingslib.applications.InterestingConfigChanges;
46 import com.android.systemui.R;
47 import com.android.systemui.SysUiServiceProvider;
48 import com.android.systemui.assist.ui.DefaultUiController;
49 import com.android.systemui.recents.OverviewProxyService;
50 import com.android.systemui.statusbar.CommandQueue;
51 import com.android.systemui.statusbar.policy.ConfigurationController;
52 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
53 
54 import javax.inject.Inject;
55 import javax.inject.Singleton;
56 
57 /**
58  * Class to manage everything related to assist in SystemUI.
59  */
60 @Singleton
61 public class AssistManager {
62 
63     /**
64      * Controls the UI for showing Assistant invocation progress.
65      */
66     public interface UiController {
67         /**
68          * Updates the invocation progress.
69          *
70          * @param type     one of INVOCATION_TYPE_GESTURE, INVOCATION_TYPE_ACTIVE_EDGE,
71          *                 INVOCATION_TYPE_VOICE, INVOCATION_TYPE_QUICK_SEARCH_BAR,
72          *                 INVOCATION_HOME_BUTTON_LONG_PRESS
73          * @param progress a float between 0 and 1 inclusive. 0 represents the beginning of the
74          *                 gesture; 1 represents the end.
75          */
onInvocationProgress(int type, float progress)76         void onInvocationProgress(int type, float progress);
77 
78         /**
79          * Called when an invocation gesture completes.
80          *
81          * @param velocity the speed of the invocation gesture, in pixels per millisecond. For
82          *                 drags, this is 0.
83          */
onGestureCompletion(float velocity)84         void onGestureCompletion(float velocity);
85 
86         /**
87          * Called with the Bundle from VoiceInteractionSessionListener.onSetUiHints.
88          */
processBundle(Bundle hints)89         void processBundle(Bundle hints);
90 
91         /**
92          * Hides any SysUI for the assistant, but _does not_ close the assistant itself.
93          */
hide()94         void hide();
95     }
96 
97     private static final String TAG = "AssistManager";
98 
99     // Note that VERBOSE logging may leak PII (e.g. transcription contents).
100     private static final boolean VERBOSE = false;
101 
102     private static final String ASSIST_ICON_METADATA_NAME =
103             "com.android.systemui.action_assist_icon";
104     private static final String INVOCATION_TIME_MS_KEY = "invocation_time_ms";
105     private static final String INVOCATION_PHONE_STATE_KEY = "invocation_phone_state";
106     public static final String INVOCATION_TYPE_KEY = "invocation_type";
107     protected static final String ACTION_KEY = "action";
108     protected static final String SHOW_ASSIST_HANDLES_ACTION = "show_assist_handles";
109     protected static final String SET_ASSIST_GESTURE_CONSTRAINED_ACTION =
110             "set_assist_gesture_constrained";
111     protected static final String CONSTRAINED_KEY = "should_constrain";
112 
113     public static final int INVOCATION_TYPE_GESTURE = 1;
114     public static final int INVOCATION_TYPE_ACTIVE_EDGE = 2;
115     public static final int INVOCATION_TYPE_VOICE = 3;
116     public static final int INVOCATION_TYPE_QUICK_SEARCH_BAR = 4;
117     public static final int INVOCATION_HOME_BUTTON_LONG_PRESS = 5;
118 
119     public static final int DISMISS_REASON_INVOCATION_CANCELLED = 1;
120     public static final int DISMISS_REASON_TAP = 2;
121     public static final int DISMISS_REASON_BACK = 3;
122     public static final int DISMISS_REASON_TIMEOUT = 4;
123 
124     private static final long TIMEOUT_SERVICE = 2500;
125     private static final long TIMEOUT_ACTIVITY = 1000;
126 
127     protected final Context mContext;
128     private final WindowManager mWindowManager;
129     private final AssistDisclosure mAssistDisclosure;
130     private final InterestingConfigChanges mInterestingConfigChanges;
131     private final PhoneStateMonitor mPhoneStateMonitor;
132     private final AssistHandleBehaviorController mHandleController;
133     private final UiController mUiController;
134     protected final OverviewProxyService mOverviewProxyService;
135 
136     private AssistOrbContainer mView;
137     private final DeviceProvisionedController mDeviceProvisionedController;
138     protected final AssistUtils mAssistUtils;
139     private final boolean mShouldEnableOrb;
140 
141     private IVoiceInteractionSessionShowCallback mShowCallback =
142             new IVoiceInteractionSessionShowCallback.Stub() {
143 
144                 @Override
145                 public void onFailed() throws RemoteException {
146                     mView.post(mHideRunnable);
147                 }
148 
149                 @Override
150                 public void onShown() throws RemoteException {
151                     mView.post(mHideRunnable);
152                 }
153             };
154 
155     private Runnable mHideRunnable = new Runnable() {
156         @Override
157         public void run() {
158             mView.removeCallbacks(this);
159             mView.show(false /* show */, true /* animate */);
160         }
161     };
162 
163     private ConfigurationController.ConfigurationListener mConfigurationListener =
164             new ConfigurationController.ConfigurationListener() {
165                 @Override
166                 public void onConfigChanged(Configuration newConfig) {
167                     if (!mInterestingConfigChanges.applyNewConfig(mContext.getResources())) {
168                         return;
169                     }
170                     boolean visible = false;
171                     if (mView != null) {
172                         visible = mView.isShowing();
173                         mWindowManager.removeView(mView);
174                     }
175 
176                     mView = (AssistOrbContainer) LayoutInflater.from(mContext).inflate(
177                             R.layout.assist_orb, null);
178                     mView.setVisibility(View.GONE);
179                     mView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
180                             | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
181                             | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
182                     WindowManager.LayoutParams lp = getLayoutParams();
183                     mWindowManager.addView(mView, lp);
184                     if (visible) {
185                         mView.show(true /* show */, false /* animate */);
186                     }
187                 }
188             };
189 
190     @Inject
AssistManager( DeviceProvisionedController controller, Context context, AssistUtils assistUtils, AssistHandleBehaviorController handleController, ConfigurationController configurationController, OverviewProxyService overviewProxyService)191     public AssistManager(
192             DeviceProvisionedController controller,
193             Context context,
194             AssistUtils assistUtils,
195             AssistHandleBehaviorController handleController,
196             ConfigurationController configurationController,
197             OverviewProxyService overviewProxyService) {
198         mContext = context;
199         mDeviceProvisionedController = controller;
200         mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
201         mAssistUtils = assistUtils;
202         mAssistDisclosure = new AssistDisclosure(context, new Handler());
203         mPhoneStateMonitor = new PhoneStateMonitor(context);
204         mHandleController = handleController;
205 
206         configurationController.addCallback(mConfigurationListener);
207 
208         registerVoiceInteractionSessionListener();
209         mInterestingConfigChanges = new InterestingConfigChanges(ActivityInfo.CONFIG_ORIENTATION
210                 | ActivityInfo.CONFIG_LOCALE | ActivityInfo.CONFIG_UI_MODE
211                 | ActivityInfo.CONFIG_SCREEN_LAYOUT | ActivityInfo.CONFIG_ASSETS_PATHS);
212         mConfigurationListener.onConfigChanged(context.getResources().getConfiguration());
213         mShouldEnableOrb = !ActivityManager.isLowRamDeviceStatic();
214 
215         mUiController = new DefaultUiController(mContext);
216 
217         mOverviewProxyService = overviewProxyService;
218         mOverviewProxyService.addCallback(new OverviewProxyService.OverviewProxyListener() {
219             @Override
220             public void onAssistantProgress(float progress) {
221                 // Progress goes from 0 to 1 to indicate how close the assist gesture is to
222                 // completion.
223                 onInvocationProgress(INVOCATION_TYPE_GESTURE, progress);
224             }
225 
226             @Override
227             public void onAssistantGestureCompletion(float velocity) {
228                 onGestureCompletion(velocity);
229             }
230         });
231     }
232 
registerVoiceInteractionSessionListener()233     protected void registerVoiceInteractionSessionListener() {
234         mAssistUtils.registerVoiceInteractionSessionListener(
235                 new IVoiceInteractionSessionListener.Stub() {
236                     @Override
237                     public void onVoiceSessionShown() throws RemoteException {
238                         if (VERBOSE) {
239                             Log.v(TAG, "Voice open");
240                         }
241                     }
242 
243                     @Override
244                     public void onVoiceSessionHidden() throws RemoteException {
245                         if (VERBOSE) {
246                             Log.v(TAG, "Voice closed");
247                         }
248                     }
249 
250                     @Override
251                     public void onSetUiHints(Bundle hints) {
252                         if (VERBOSE) {
253                             Log.v(TAG, "UI hints received");
254                         }
255 
256                         String action = hints.getString(ACTION_KEY);
257                         if (SHOW_ASSIST_HANDLES_ACTION.equals(action)) {
258                             requestAssistHandles();
259                         } else if (SET_ASSIST_GESTURE_CONSTRAINED_ACTION.equals(action)) {
260                             mOverviewProxyService.setSystemUiStateFlag(
261                                     SYSUI_STATE_ASSIST_GESTURE_CONSTRAINED,
262                                     hints.getBoolean(CONSTRAINED_KEY, false),
263                                     DEFAULT_DISPLAY);
264                         }
265                     }
266                 });
267     }
268 
shouldShowOrb()269     protected boolean shouldShowOrb() {
270         return false;
271     }
272 
startAssist(Bundle args)273     public void startAssist(Bundle args) {
274         final ComponentName assistComponent = getAssistInfo();
275         if (assistComponent == null) {
276             return;
277         }
278 
279         final boolean isService = assistComponent.equals(getVoiceInteractorComponentName());
280         if (!isService || (!isVoiceSessionRunning() && shouldShowOrb())) {
281             showOrb(assistComponent, isService);
282             mView.postDelayed(mHideRunnable, isService
283                     ? TIMEOUT_SERVICE
284                     : TIMEOUT_ACTIVITY);
285         }
286 
287         if (args == null) {
288             args = new Bundle();
289         }
290         int invocationType = args.getInt(INVOCATION_TYPE_KEY, 0);
291         if (invocationType == INVOCATION_TYPE_GESTURE) {
292             mHandleController.onAssistantGesturePerformed();
293         }
294         int phoneState = mPhoneStateMonitor.getPhoneState();
295         args.putInt(INVOCATION_PHONE_STATE_KEY, phoneState);
296         args.putLong(INVOCATION_TIME_MS_KEY, SystemClock.elapsedRealtime());
297         logStartAssist(invocationType, phoneState);
298         startAssistInternal(args, assistComponent, isService);
299     }
300 
301     /** Called when the user is performing an assistant invocation action (e.g. Active Edge) */
onInvocationProgress(int type, float progress)302     public void onInvocationProgress(int type, float progress) {
303         mUiController.onInvocationProgress(type, progress);
304     }
305 
306     /**
307      * Called when the user has invoked the assistant with the incoming velocity, in pixels per
308      * millisecond. For invocations without a velocity (e.g. slow drag), the velocity is set to
309      * zero.
310      */
onGestureCompletion(float velocity)311     public void onGestureCompletion(float velocity) {
312         mUiController.onGestureCompletion(velocity);
313     }
314 
requestAssistHandles()315     protected void requestAssistHandles() {
316         mHandleController.onAssistHandlesRequested();
317     }
318 
hideAssist()319     public void hideAssist() {
320         mAssistUtils.hideCurrentSession();
321     }
322 
getLayoutParams()323     private WindowManager.LayoutParams getLayoutParams() {
324         WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
325                 ViewGroup.LayoutParams.MATCH_PARENT,
326                 mContext.getResources().getDimensionPixelSize(R.dimen.assist_orb_scrim_height),
327                 WindowManager.LayoutParams.TYPE_VOICE_INTERACTION_STARTING,
328                 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
329                         | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
330                         | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
331                 PixelFormat.TRANSLUCENT);
332         lp.token = new Binder();
333         lp.gravity = Gravity.BOTTOM | Gravity.START;
334         lp.setTitle("AssistPreviewPanel");
335         lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED
336                 | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
337         return lp;
338     }
339 
showOrb(@onNull ComponentName assistComponent, boolean isService)340     private void showOrb(@NonNull ComponentName assistComponent, boolean isService) {
341         maybeSwapSearchIcon(assistComponent, isService);
342         if (mShouldEnableOrb) {
343             mView.show(true /* show */, true /* animate */);
344         }
345     }
346 
startAssistInternal(Bundle args, @NonNull ComponentName assistComponent, boolean isService)347     private void startAssistInternal(Bundle args, @NonNull ComponentName assistComponent,
348             boolean isService) {
349         if (isService) {
350             startVoiceInteractor(args);
351         } else {
352             startAssistActivity(args, assistComponent);
353         }
354     }
355 
startAssistActivity(Bundle args, @NonNull ComponentName assistComponent)356     private void startAssistActivity(Bundle args, @NonNull ComponentName assistComponent) {
357         if (!mDeviceProvisionedController.isDeviceProvisioned()) {
358             return;
359         }
360 
361         // Close Recent Apps if needed
362         SysUiServiceProvider.getComponent(mContext, CommandQueue.class).animateCollapsePanels(
363                 CommandQueue.FLAG_EXCLUDE_SEARCH_PANEL | CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL,
364                 false /* force */);
365 
366         boolean structureEnabled = Settings.Secure.getIntForUser(mContext.getContentResolver(),
367                 Settings.Secure.ASSIST_STRUCTURE_ENABLED, 1, UserHandle.USER_CURRENT) != 0;
368 
369         final SearchManager searchManager =
370                 (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
371         if (searchManager == null) {
372             return;
373         }
374         final Intent intent = searchManager.getAssistIntent(structureEnabled);
375         if (intent == null) {
376             return;
377         }
378         intent.setComponent(assistComponent);
379         intent.putExtras(args);
380 
381         if (structureEnabled && AssistUtils.isDisclosureEnabled(mContext)) {
382             showDisclosure();
383         }
384 
385         try {
386             final ActivityOptions opts = ActivityOptions.makeCustomAnimation(mContext,
387                     R.anim.search_launch_enter, R.anim.search_launch_exit);
388             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
389             AsyncTask.execute(new Runnable() {
390                 @Override
391                 public void run() {
392                     mContext.startActivityAsUser(intent, opts.toBundle(),
393                             new UserHandle(UserHandle.USER_CURRENT));
394                 }
395             });
396         } catch (ActivityNotFoundException e) {
397             Log.w(TAG, "Activity not found for " + intent.getAction());
398         }
399     }
400 
startVoiceInteractor(Bundle args)401     private void startVoiceInteractor(Bundle args) {
402         mAssistUtils.showSessionForActiveService(args,
403                 VoiceInteractionSession.SHOW_SOURCE_ASSIST_GESTURE, mShowCallback, null);
404     }
405 
launchVoiceAssistFromKeyguard()406     public void launchVoiceAssistFromKeyguard() {
407         mAssistUtils.launchVoiceAssistFromKeyguard();
408     }
409 
canVoiceAssistBeLaunchedFromKeyguard()410     public boolean canVoiceAssistBeLaunchedFromKeyguard() {
411         return mAssistUtils.activeServiceSupportsLaunchFromKeyguard();
412     }
413 
getVoiceInteractorComponentName()414     public ComponentName getVoiceInteractorComponentName() {
415         return mAssistUtils.getActiveServiceComponentName();
416     }
417 
isVoiceSessionRunning()418     private boolean isVoiceSessionRunning() {
419         return mAssistUtils.isSessionRunning();
420     }
421 
maybeSwapSearchIcon(@onNull ComponentName assistComponent, boolean isService)422     private void maybeSwapSearchIcon(@NonNull ComponentName assistComponent, boolean isService) {
423         replaceDrawable(mView.getOrb().getLogo(), assistComponent, ASSIST_ICON_METADATA_NAME,
424                 isService);
425     }
426 
replaceDrawable(ImageView v, ComponentName component, String name, boolean isService)427     public void replaceDrawable(ImageView v, ComponentName component, String name,
428             boolean isService) {
429         if (component != null) {
430             try {
431                 PackageManager packageManager = mContext.getPackageManager();
432                 // Look for the search icon specified in the activity meta-data
433                 Bundle metaData = isService
434                         ? packageManager.getServiceInfo(
435                         component, PackageManager.GET_META_DATA).metaData
436                         : packageManager.getActivityInfo(
437                                 component, PackageManager.GET_META_DATA).metaData;
438                 if (metaData != null) {
439                     int iconResId = metaData.getInt(name);
440                     if (iconResId != 0) {
441                         Resources res = packageManager.getResourcesForApplication(
442                                 component.getPackageName());
443                         v.setImageDrawable(res.getDrawable(iconResId));
444                         return;
445                     }
446                 }
447             } catch (PackageManager.NameNotFoundException e) {
448                 if (VERBOSE) {
449                     Log.v(TAG, "Assistant component "
450                             + component.flattenToShortString() + " not found");
451                 }
452             } catch (Resources.NotFoundException nfe) {
453                 Log.w(TAG, "Failed to swap drawable from "
454                         + component.flattenToShortString(), nfe);
455             }
456         }
457         v.setImageDrawable(null);
458     }
459 
getHandleBehaviorController()460     protected AssistHandleBehaviorController getHandleBehaviorController() {
461         return mHandleController;
462     }
463 
464     @Nullable
getAssistInfoForUser(int userId)465     public ComponentName getAssistInfoForUser(int userId) {
466         return mAssistUtils.getAssistComponentForUser(userId);
467     }
468 
469     @Nullable
getAssistInfo()470     private ComponentName getAssistInfo() {
471         return getAssistInfoForUser(KeyguardUpdateMonitor.getCurrentUser());
472     }
473 
showDisclosure()474     public void showDisclosure() {
475         mAssistDisclosure.postShow();
476     }
477 
onLockscreenShown()478     public void onLockscreenShown() {
479         mAssistUtils.onLockscreenShown();
480     }
481 
getAssistHandleShowAndGoRemainingDurationMs()482     public long getAssistHandleShowAndGoRemainingDurationMs() {
483         return mHandleController.getShowAndGoRemainingTimeMs();
484     }
485 
486     /** Returns the logging flags for the given Assistant invocation type. */
toLoggingSubType(int invocationType)487     public int toLoggingSubType(int invocationType) {
488         return toLoggingSubType(invocationType, mPhoneStateMonitor.getPhoneState());
489     }
490 
logStartAssist(int invocationType, int phoneState)491     protected void logStartAssist(int invocationType, int phoneState) {
492         MetricsLogger.action(
493                 new LogMaker(MetricsEvent.ASSISTANT)
494                         .setType(MetricsEvent.TYPE_OPEN)
495                         .setSubtype(toLoggingSubType(invocationType, phoneState)));
496     }
497 
toLoggingSubType(int invocationType, int phoneState)498     protected final int toLoggingSubType(int invocationType, int phoneState) {
499         // Note that this logic will break if the number of Assistant invocation types exceeds 7.
500         // There are currently 5 invocation types, but we will be migrating to the new logging
501         // framework in the next update.
502         int subType = mHandleController.areHandlesShowing() ? 0 : 1;
503         subType |= invocationType << 1;
504         subType |= phoneState << 4;
505         return subType;
506     }
507 }
508