1 /*
2  * Copyright 2017 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * 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, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.android.internal.accessibility;
18 
19 import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK;
20 import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
21 import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
22 
23 import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets;
24 import static com.android.internal.os.RoSystemProperties.SUPPORT_ONE_HANDED_MODE;
25 import static com.android.internal.util.ArrayUtils.convertToLongArray;
26 
27 import android.accessibilityservice.AccessibilityServiceInfo;
28 import android.annotation.IntDef;
29 import android.app.ActivityManager;
30 import android.app.ActivityThread;
31 import android.app.AlertDialog;
32 import android.content.ComponentName;
33 import android.content.ContentResolver;
34 import android.content.Context;
35 import android.content.DialogInterface;
36 import android.content.pm.PackageManager;
37 import android.content.res.Configuration;
38 import android.database.ContentObserver;
39 import android.media.AudioAttributes;
40 import android.media.Ringtone;
41 import android.media.RingtoneManager;
42 import android.net.Uri;
43 import android.os.Build;
44 import android.os.Handler;
45 import android.os.UserHandle;
46 import android.os.Vibrator;
47 import android.provider.Settings;
48 import android.speech.tts.TextToSpeech;
49 import android.speech.tts.Voice;
50 import android.text.TextUtils;
51 import android.util.ArrayMap;
52 import android.util.Slog;
53 import android.view.Window;
54 import android.view.WindowManager;
55 import android.view.accessibility.AccessibilityManager;
56 import android.widget.Toast;
57 
58 import com.android.internal.R;
59 import com.android.internal.accessibility.dialog.AccessibilityTarget;
60 import com.android.internal.util.function.pooled.PooledLambda;
61 
62 import java.lang.annotation.Retention;
63 import java.lang.annotation.RetentionPolicy;
64 import java.util.Collection;
65 import java.util.Collections;
66 import java.util.List;
67 import java.util.Locale;
68 import java.util.Map;
69 
70 /**
71  * Class to help manage the accessibility shortcut key
72  */
73 public class AccessibilityShortcutController {
74     private static final String TAG = "AccessibilityShortcutController";
75 
76     // Placeholder component names for framework features
77     public static final ComponentName COLOR_INVERSION_COMPONENT_NAME =
78             new ComponentName("com.android.server.accessibility", "ColorInversion");
79     public static final ComponentName DALTONIZER_COMPONENT_NAME =
80             new ComponentName("com.android.server.accessibility", "Daltonizer");
81     // TODO(b/147990389): Use MAGNIFICATION_COMPONENT_NAME to replace.
82     public static final String MAGNIFICATION_CONTROLLER_NAME =
83             "com.android.server.accessibility.MagnificationController";
84     public static final ComponentName MAGNIFICATION_COMPONENT_NAME =
85             new ComponentName("com.android.server.accessibility", "Magnification");
86     public static final ComponentName ONE_HANDED_COMPONENT_NAME =
87             new ComponentName("com.android.server.accessibility", "OneHandedMode");
88     public static final ComponentName REDUCE_BRIGHT_COLORS_COMPONENT_NAME =
89             new ComponentName("com.android.server.accessibility", "ReduceBrightColors");
90     public static final ComponentName FONT_SIZE_COMPONENT_NAME =
91             new ComponentName("com.android.server.accessibility", "FontSize");
92 
93     // The component name for the sub setting of Accessibility button in Accessibility settings
94     public static final ComponentName ACCESSIBILITY_BUTTON_COMPONENT_NAME =
95             new ComponentName("com.android.server.accessibility", "AccessibilityButton");
96 
97     // The component name for the sub setting of Hearing aids in Accessibility settings
98     public static final ComponentName ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME =
99             new ComponentName("com.android.server.accessibility", "HearingAids");
100 
101     public static final ComponentName COLOR_INVERSION_TILE_COMPONENT_NAME =
102             new ComponentName("com.android.server.accessibility", "ColorInversionTile");
103     public static final ComponentName DALTONIZER_TILE_COMPONENT_NAME =
104             new ComponentName("com.android.server.accessibility", "ColorCorrectionTile");
105     public static final ComponentName ONE_HANDED_TILE_COMPONENT_NAME =
106             new ComponentName("com.android.server.accessibility", "OneHandedModeTile");
107     public static final ComponentName REDUCE_BRIGHT_COLORS_TILE_SERVICE_COMPONENT_NAME =
108             new ComponentName("com.android.server.accessibility", "ReduceBrightColorsTile");
109 
110     private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
111             .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
112             .setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY)
113             .build();
114     private static Map<ComponentName, FrameworkFeatureInfo> sFrameworkShortcutFeaturesMap;
115 
116     private final Context mContext;
117     private final Handler mHandler;
118     private final UserSetupCompleteObserver  mUserSetupCompleteObserver;
119 
120     private AlertDialog mAlertDialog;
121     private boolean mIsShortcutEnabled;
122     private boolean mEnabledOnLockScreen;
123     private int mUserId;
124 
125     @Retention(RetentionPolicy.SOURCE)
126     @IntDef({
127             DialogStatus.NOT_SHOWN,
128             DialogStatus.SHOWN,
129     })
130     /** Denotes the user shortcut type. */
131     private @interface DialogStatus {
132         int NOT_SHOWN = 0;
133         int SHOWN  = 1;
134     }
135 
136     // Visible for testing
137     public FrameworkObjectProvider mFrameworkObjectProvider = new FrameworkObjectProvider();
138 
139     /**
140      * @return An immutable map from placeholder component names to feature
141      *         info for toggling a framework feature
142      */
143     public static Map<ComponentName, FrameworkFeatureInfo>
getFrameworkShortcutFeaturesMap()144         getFrameworkShortcutFeaturesMap() {
145         if (sFrameworkShortcutFeaturesMap == null) {
146             Map<ComponentName, FrameworkFeatureInfo> featuresMap = new ArrayMap<>(4);
147             featuresMap.put(COLOR_INVERSION_COMPONENT_NAME,
148                     new ToggleableFrameworkFeatureInfo(
149                             Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED,
150                             "1" /* Value to enable */, "0" /* Value to disable */,
151                             R.string.color_inversion_feature_name));
152             featuresMap.put(DALTONIZER_COMPONENT_NAME,
153                     new ToggleableFrameworkFeatureInfo(
154                             Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
155                             "1" /* Value to enable */, "0" /* Value to disable */,
156                             R.string.color_correction_feature_name));
157             if (SUPPORT_ONE_HANDED_MODE) {
158                 featuresMap.put(ONE_HANDED_COMPONENT_NAME,
159                         new ToggleableFrameworkFeatureInfo(
160                                 Settings.Secure.ONE_HANDED_MODE_ACTIVATED,
161                                 "1" /* Value to enable */, "0" /* Value to disable */,
162                                 R.string.one_handed_mode_feature_name));
163             }
164             featuresMap.put(REDUCE_BRIGHT_COLORS_COMPONENT_NAME,
165                     new ToggleableFrameworkFeatureInfo(
166                             Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED,
167                             "1" /* Value to enable */, "0" /* Value to disable */,
168                             R.string.reduce_bright_colors_feature_name));
169             featuresMap.put(ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME,
170                     new LaunchableFrameworkFeatureInfo(R.string.hearing_aids_feature_name));
171             sFrameworkShortcutFeaturesMap = Collections.unmodifiableMap(featuresMap);
172         }
173         return sFrameworkShortcutFeaturesMap;
174     }
175 
AccessibilityShortcutController(Context context, Handler handler, int initialUserId)176     public AccessibilityShortcutController(Context context, Handler handler, int initialUserId) {
177         mContext = context;
178         mHandler = handler;
179         mUserId = initialUserId;
180         mUserSetupCompleteObserver = new UserSetupCompleteObserver(handler, initialUserId);
181 
182         // Keep track of state of shortcut settings
183         final ContentObserver co = new ContentObserver(handler) {
184             @Override
185             public void onChange(boolean selfChange, Collection<Uri> uris, int flags, int userId) {
186                 if (userId == mUserId) {
187                     onSettingsChanged();
188                 }
189             }
190         };
191         mContext.getContentResolver().registerContentObserver(
192                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE),
193                 false, co, UserHandle.USER_ALL);
194         mContext.getContentResolver().registerContentObserver(
195                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN),
196                 false, co, UserHandle.USER_ALL);
197         mContext.getContentResolver().registerContentObserver(
198                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN),
199                 false, co, UserHandle.USER_ALL);
200         setCurrentUser(mUserId);
201     }
202 
setCurrentUser(int currentUserId)203     public void setCurrentUser(int currentUserId) {
204         mUserId = currentUserId;
205         onSettingsChanged();
206         mUserSetupCompleteObserver.onUserSwitched(currentUserId);
207     }
208 
209     /**
210      * Check if the shortcut is available.
211      *
212      * @param phoneLocked Whether or not the phone is currently locked.
213      *
214      * @return {@code true} if the shortcut is available
215      */
isAccessibilityShortcutAvailable(boolean phoneLocked)216     public boolean isAccessibilityShortcutAvailable(boolean phoneLocked) {
217         return mIsShortcutEnabled && (!phoneLocked || mEnabledOnLockScreen);
218     }
219 
onSettingsChanged()220     public void onSettingsChanged() {
221         final boolean hasShortcutTarget = hasShortcutTarget();
222         final ContentResolver cr = mContext.getContentResolver();
223         // Enable the shortcut from the lockscreen by default if the dialog has been shown
224         final int dialogAlreadyShown = Settings.Secure.getIntForUser(
225                 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, DialogStatus.NOT_SHOWN,
226                 mUserId);
227         mEnabledOnLockScreen = Settings.Secure.getIntForUser(
228                 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN,
229                 dialogAlreadyShown, mUserId) == 1;
230         mIsShortcutEnabled = hasShortcutTarget;
231     }
232 
233     /**
234      * Called when the accessibility shortcut is activated
235      */
performAccessibilityShortcut()236     public void performAccessibilityShortcut() {
237         Slog.d(TAG, "Accessibility shortcut activated");
238         final ContentResolver cr = mContext.getContentResolver();
239         final int userId = ActivityManager.getCurrentUser();
240 
241         // Play a notification vibration
242         Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
243         if ((vibrator != null) && vibrator.hasVibrator()) {
244             // Don't check if haptics are disabled, as we need to alert the user that their
245             // way of interacting with the phone may change if they activate the shortcut
246             long[] vibePattern = convertToLongArray(
247                     mContext.getResources().getIntArray(R.array.config_longPressVibePattern));
248             vibrator.vibrate(vibePattern, -1, VIBRATION_ATTRIBUTES);
249         }
250 
251         if (shouldShowDialog()) {
252             // The first time, we show a warning rather than toggle the service to give the user a
253             // chance to turn off this feature before stuff gets enabled.
254             mAlertDialog = createShortcutWarningDialog(userId);
255             if (mAlertDialog == null) {
256                 return;
257             }
258             if (!performTtsPrompt(mAlertDialog)) {
259                 playNotificationTone();
260             }
261             Window w = mAlertDialog.getWindow();
262             WindowManager.LayoutParams attr = w.getAttributes();
263             attr.type = TYPE_KEYGUARD_DIALOG;
264             w.setAttributes(attr);
265             mAlertDialog.show();
266             Settings.Secure.putIntForUser(
267                     cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, DialogStatus.SHOWN,
268                     userId);
269         } else {
270             playNotificationTone();
271             if (mAlertDialog != null) {
272                 mAlertDialog.dismiss();
273                 mAlertDialog = null;
274             }
275             showToast();
276             mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext)
277                     .performAccessibilityShortcut();
278         }
279     }
280 
281     /** Whether the warning dialog should be shown instead of performing the shortcut. */
shouldShowDialog()282     private boolean shouldShowDialog() {
283         if (hasFeatureLeanback()) {
284             // Never show the dialog on TV, instead always perform the shortcut directly.
285             return false;
286         }
287         final ContentResolver cr = mContext.getContentResolver();
288         final int userId = ActivityManager.getCurrentUser();
289         final int dialogAlreadyShown = Settings.Secure.getIntForUser(cr,
290                 Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, DialogStatus.NOT_SHOWN,
291                 userId);
292         return dialogAlreadyShown == DialogStatus.NOT_SHOWN;
293     }
294 
295     /**
296      * Show toast to alert the user that the accessibility shortcut turned on or off an
297      * accessibility service.
298      */
showToast()299     private void showToast() {
300         final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
301         if (serviceInfo == null) {
302             return;
303         }
304         final String serviceName = getShortcutFeatureDescription(/* no summary */ false);
305         if (serviceName == null) {
306             return;
307         }
308         final boolean requestA11yButton = (serviceInfo.flags
309                 & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
310         final boolean isServiceEnabled = isServiceEnabled(serviceInfo);
311         if (serviceInfo.getResolveInfo().serviceInfo.applicationInfo.targetSdkVersion
312                 > Build.VERSION_CODES.Q && requestA11yButton && isServiceEnabled) {
313             // An accessibility button callback is sent to the target accessibility service.
314             // No need to show up a toast in this case.
315             return;
316         }
317         // For accessibility services, show a toast explaining what we're doing.
318         String toastMessageFormatString = mContext.getString(isServiceEnabled
319                 ? R.string.accessibility_shortcut_disabling_service
320                 : R.string.accessibility_shortcut_enabling_service);
321         String toastMessage = String.format(toastMessageFormatString, serviceName);
322         Toast warningToast = mFrameworkObjectProvider.makeToastFromText(
323                 mContext, toastMessage, Toast.LENGTH_LONG);
324         warningToast.show();
325     }
326 
createShortcutWarningDialog(int userId)327     private AlertDialog createShortcutWarningDialog(int userId) {
328         List<AccessibilityTarget> targets = getTargets(mContext, ACCESSIBILITY_SHORTCUT_KEY);
329         if (targets.size() == 0) {
330             return null;
331         }
332 
333         // Avoid non-a11y users accidentally turning shortcut on without reading this carefully.
334         // Put "don't turn on" as the primary action.
335         final AlertDialog alertDialog = mFrameworkObjectProvider.getAlertDialogBuilder(
336                 // Use SystemUI context so we pick up any theme set in a vendor overlay
337                 mFrameworkObjectProvider.getSystemUiContext())
338                 .setTitle(getShortcutWarningTitle(targets))
339                 .setMessage(getShortcutWarningMessage(targets))
340                 .setCancelable(false)
341                 .setNegativeButton(R.string.accessibility_shortcut_on, null)
342                 .setPositiveButton(R.string.accessibility_shortcut_off,
343                         (DialogInterface d, int which) -> {
344                             Settings.Secure.putStringForUser(mContext.getContentResolver(),
345                                     Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "",
346                                     userId);
347 
348                             // If canceled, treat as if the dialog has never been shown
349                             Settings.Secure.putIntForUser(mContext.getContentResolver(),
350                                     Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN,
351                                     DialogStatus.NOT_SHOWN, userId);
352                         })
353                 .setOnCancelListener((DialogInterface d) -> {
354                     // If canceled, treat as if the dialog has never been shown
355                     Settings.Secure.putIntForUser(mContext.getContentResolver(),
356                             Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN,
357                             DialogStatus.NOT_SHOWN, userId);
358                 })
359                 .create();
360         return alertDialog;
361     }
362 
getShortcutWarningTitle(List<AccessibilityTarget> targets)363     private String getShortcutWarningTitle(List<AccessibilityTarget> targets) {
364         if (targets.size() == 1) {
365             return mContext.getString(
366                     R.string.accessibility_shortcut_single_service_warning_title,
367                     targets.get(0).getLabel());
368         }
369         return mContext.getString(
370                 R.string.accessibility_shortcut_multiple_service_warning_title);
371     }
372 
getShortcutWarningMessage(List<AccessibilityTarget> targets)373     private String getShortcutWarningMessage(List<AccessibilityTarget> targets) {
374         if (targets.size() == 1) {
375             return mContext.getString(
376                     R.string.accessibility_shortcut_single_service_warning,
377                     targets.get(0).getLabel());
378         }
379 
380         final StringBuilder sb = new StringBuilder();
381         for (AccessibilityTarget target : targets) {
382             sb.append(mContext.getString(R.string.accessibility_shortcut_multiple_service_list,
383                     target.getLabel()));
384         }
385         return mContext.getString(R.string.accessibility_shortcut_multiple_service_warning,
386                 sb.toString());
387     }
388 
getInfoForTargetService()389     private AccessibilityServiceInfo getInfoForTargetService() {
390         final ComponentName targetComponentName = getShortcutTargetComponentName();
391         if (targetComponentName == null) {
392             return null;
393         }
394         AccessibilityManager accessibilityManager =
395                 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
396         return accessibilityManager.getInstalledServiceInfoWithComponentName(
397                 targetComponentName);
398     }
399 
getShortcutFeatureDescription(boolean includeSummary)400     private String getShortcutFeatureDescription(boolean includeSummary) {
401         final ComponentName targetComponentName = getShortcutTargetComponentName();
402         if (targetComponentName == null) {
403             return null;
404         }
405         final FrameworkFeatureInfo frameworkFeatureInfo =
406                 getFrameworkShortcutFeaturesMap().get(targetComponentName);
407         if (frameworkFeatureInfo != null) {
408             return frameworkFeatureInfo.getLabel(mContext);
409         }
410         final AccessibilityServiceInfo serviceInfo = mFrameworkObjectProvider
411                 .getAccessibilityManagerInstance(mContext).getInstalledServiceInfoWithComponentName(
412                         targetComponentName);
413         if (serviceInfo == null) {
414             return null;
415         }
416         final PackageManager pm = mContext.getPackageManager();
417         String label = serviceInfo.getResolveInfo().loadLabel(pm).toString();
418         CharSequence summary = serviceInfo.loadSummary(pm);
419         if (!includeSummary || TextUtils.isEmpty(summary)) {
420             return label;
421         }
422         return String.format("%s\n%s", label, summary);
423     }
424 
isServiceEnabled(AccessibilityServiceInfo serviceInfo)425     private boolean isServiceEnabled(AccessibilityServiceInfo serviceInfo) {
426         AccessibilityManager accessibilityManager =
427                 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
428         return accessibilityManager.getEnabledAccessibilityServiceList(
429                 FEEDBACK_ALL_MASK).contains(serviceInfo);
430     }
431 
hasFeatureLeanback()432     private boolean hasFeatureLeanback() {
433         return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
434     }
435 
playNotificationTone()436     private void playNotificationTone() {
437         // Use USAGE_ASSISTANCE_ACCESSIBILITY for TVs to ensure that TVs play the ringtone as they
438         // have less ways of providing feedback like vibration.
439         final int audioAttributesUsage = hasFeatureLeanback()
440                 ? AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY
441                 : AudioAttributes.USAGE_NOTIFICATION_EVENT;
442 
443         // Use the default accessibility notification sound instead to avoid users confusing the new
444         // notification received. Point to the default notification sound if the sound does not
445         // exist.
446         final Uri ringtoneUri = Uri.parse("file://"
447                 + mContext.getString(R.string.config_defaultAccessibilityNotificationSound));
448         Ringtone tone = mFrameworkObjectProvider.getRingtone(mContext, ringtoneUri);
449         if (tone == null) {
450             tone = mFrameworkObjectProvider.getRingtone(mContext,
451                     Settings.System.DEFAULT_NOTIFICATION_URI);
452         }
453 
454         // Play a notification tone
455         if (tone != null) {
456             tone.setAudioAttributes(new AudioAttributes.Builder()
457                     .setUsage(audioAttributesUsage)
458                     .build());
459             tone.play();
460         }
461     }
462 
performTtsPrompt(AlertDialog alertDialog)463     private boolean performTtsPrompt(AlertDialog alertDialog) {
464         final String serviceName = getShortcutFeatureDescription(false /* no summary */);
465         final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
466         if (TextUtils.isEmpty(serviceName) || serviceInfo == null) {
467             return false;
468         }
469         if ((serviceInfo.flags & AccessibilityServiceInfo
470                 .FLAG_REQUEST_SHORTCUT_WARNING_DIALOG_SPOKEN_FEEDBACK) == 0) {
471             return false;
472         }
473         final TtsPrompt tts = new TtsPrompt(serviceName);
474         alertDialog.setOnDismissListener(dialog -> tts.dismiss());
475         return true;
476     }
477 
478     /**
479      * Returns {@code true} if any shortcut targets were assigned to accessibility shortcut key.
480      */
hasShortcutTarget()481     private boolean hasShortcutTarget() {
482         // AccessibilityShortcutController is initialized earlier than AccessibilityManagerService.
483         // AccessibilityManager#getAccessibilityShortcutTargets may not return correct shortcut
484         // targets during boot. Needs to read settings directly here.
485         String shortcutTargets = Settings.Secure.getStringForUser(mContext.getContentResolver(),
486                 Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, mUserId);
487         // A11y warning dialog updates settings to empty string, when user disables a11y shortcut.
488         // Only fallback to default a11y service, when setting is never updated.
489         if (shortcutTargets == null) {
490             shortcutTargets = mContext.getString(R.string.config_defaultAccessibilityService);
491         }
492         return !TextUtils.isEmpty(shortcutTargets);
493     }
494 
495     /**
496      * Gets the component name of the shortcut target.
497      *
498      * @return The component name, or null if it's assigned by multiple targets.
499      */
getShortcutTargetComponentName()500     private ComponentName getShortcutTargetComponentName() {
501         final List<String> shortcutTargets = mFrameworkObjectProvider
502                 .getAccessibilityManagerInstance(mContext)
503                 .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY);
504         if (shortcutTargets.size() != 1) {
505             return null;
506         }
507         return ComponentName.unflattenFromString(shortcutTargets.get(0));
508     }
509 
510     /**
511      * Class to wrap TextToSpeech for shortcut dialog spoken feedback.
512      */
513     private class TtsPrompt implements TextToSpeech.OnInitListener {
514         private static final int RETRY_MILLIS = 1000;
515 
516         private final CharSequence mText;
517 
518         private int mRetryCount = 3;
519         private boolean mDismiss;
520         private boolean mLanguageReady = false;
521         private TextToSpeech mTts;
522 
TtsPrompt(String serviceName)523         TtsPrompt(String serviceName) {
524             mText = mContext.getString(R.string.accessibility_shortcut_spoken_feedback,
525                     serviceName);
526             mTts = mFrameworkObjectProvider.getTextToSpeech(mContext, this);
527         }
528 
529         /**
530          * Releases the resources used by the TextToSpeech, when dialog dismiss.
531          */
dismiss()532         public void dismiss() {
533             mDismiss = true;
534             mHandler.sendMessage(PooledLambda.obtainMessage(TextToSpeech::shutdown, mTts));
535         }
536 
537         @Override
onInit(int status)538         public void onInit(int status) {
539             if (status != TextToSpeech.SUCCESS) {
540                 Slog.d(TAG, "Tts init fail, status=" + Integer.toString(status));
541                 playNotificationTone();
542                 return;
543             }
544             mHandler.sendMessage(PooledLambda.obtainMessage(
545                     TtsPrompt::waitForTtsReady, this));
546         }
547 
play()548         private void play() {
549             if (mDismiss) {
550                 return;
551             }
552             final int status = mTts.speak(mText, TextToSpeech.QUEUE_FLUSH, null, null);
553             if (status != TextToSpeech.SUCCESS) {
554                 Slog.d(TAG, "Tts play fail");
555                 playNotificationTone();
556             }
557         }
558 
559         /**
560          * Waiting for tts is ready to speak. Trying again if tts language pack is not available
561          * or tts voice data is not installed yet.
562          */
waitForTtsReady()563         private void waitForTtsReady() {
564             if (mDismiss) {
565                 return;
566             }
567             if (!mLanguageReady) {
568                 final int status = mTts.setLanguage(Locale.getDefault());
569                 // True if language is available and TTS#loadVoice has called once
570                 // that trigger TTS service to start initialization.
571                 mLanguageReady = status != TextToSpeech.LANG_MISSING_DATA
572                     && status != TextToSpeech.LANG_NOT_SUPPORTED;
573             }
574             if (mLanguageReady) {
575                 final Voice voice = mTts.getVoice();
576                 final boolean voiceDataInstalled = voice != null
577                         && voice.getFeatures() != null
578                         && !voice.getFeatures().contains(
579                                 TextToSpeech.Engine.KEY_FEATURE_NOT_INSTALLED);
580                 if (voiceDataInstalled) {
581                     mHandler.sendMessage(PooledLambda.obtainMessage(
582                             TtsPrompt::play, this));
583                     return;
584                 }
585             }
586 
587             if (mRetryCount == 0) {
588                 Slog.d(TAG, "Tts not ready to speak.");
589                 playNotificationTone();
590                 return;
591             }
592             // Retry if TTS service not ready yet.
593             mRetryCount -= 1;
594             mHandler.sendMessageDelayed(PooledLambda.obtainMessage(
595                     TtsPrompt::waitForTtsReady, this), RETRY_MILLIS);
596         }
597     }
598 
599     private class UserSetupCompleteObserver extends ContentObserver {
600 
601         private boolean mIsRegistered = false;
602         private int mUserId;
603 
604         /**
605          * Creates a content observer.
606          *
607          * @param handler The handler to run {@link #onChange} on, or null if none.
608          * @param userId The current user id.
609          */
UserSetupCompleteObserver(Handler handler, int userId)610         UserSetupCompleteObserver(Handler handler, int userId) {
611             super(handler);
612             mUserId = userId;
613             if (!isUserSetupComplete()) {
614                 registerObserver();
615             }
616         }
617 
isUserSetupComplete()618         private boolean isUserSetupComplete() {
619             return Settings.Secure.getIntForUser(mContext.getContentResolver(),
620                     Settings.Secure.USER_SETUP_COMPLETE, 0, mUserId) == 1;
621         }
622 
registerObserver()623         private void registerObserver() {
624             if (mIsRegistered) {
625                 return;
626             }
627             mContext.getContentResolver().registerContentObserver(
628                     Settings.Secure.getUriFor(Settings.Secure.USER_SETUP_COMPLETE),
629                     false, this, mUserId);
630             mIsRegistered = true;
631         }
632 
633         @Override
onChange(boolean selfChange)634         public void onChange(boolean selfChange) {
635             if (isUserSetupComplete()) {
636                 unregisterObserver();
637                 setEmptyShortcutTargetIfNeeded();
638             }
639         }
640 
unregisterObserver()641         private void unregisterObserver() {
642             if (!mIsRegistered) {
643                 return;
644             }
645             mContext.getContentResolver().unregisterContentObserver(this);
646             mIsRegistered = false;
647         }
648 
649         /**
650          * Sets empty shortcut target if shortcut targets is not assigned and there is no any
651          * enabled service matching the default target after the setup wizard completed.
652          *
653          */
setEmptyShortcutTargetIfNeeded()654         private void setEmptyShortcutTargetIfNeeded() {
655             if (hasFeatureLeanback()) {
656                 // Do not disable the default shortcut on TV.
657                 return;
658             }
659 
660             final ContentResolver contentResolver = mContext.getContentResolver();
661 
662             final String shortcutTargets = Settings.Secure.getStringForUser(contentResolver,
663                     Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, mUserId);
664             if (shortcutTargets != null) {
665                 return;
666             }
667 
668             final String defaultShortcutTarget = mContext.getString(
669                     R.string.config_defaultAccessibilityService);
670             final List<AccessibilityServiceInfo> enabledServices =
671                     mFrameworkObjectProvider.getAccessibilityManagerInstance(
672                             mContext).getEnabledAccessibilityServiceList(FEEDBACK_ALL_MASK);
673             for (int i = enabledServices.size() - 1; i >= 0; i--) {
674                 if (TextUtils.equals(defaultShortcutTarget, enabledServices.get(i).getId())) {
675                     return;
676                 }
677             }
678 
679             Settings.Secure.putStringForUser(contentResolver,
680                     Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "", mUserId);
681         }
682 
onUserSwitched(int userId)683         void onUserSwitched(int userId) {
684             if (mUserId == userId) {
685                 return;
686             }
687             unregisterObserver();
688             mUserId = userId;
689             if (!isUserSetupComplete()) {
690                 registerObserver();
691             }
692         }
693     }
694 
695     /**
696      * Immutable class to hold info about framework features that can be controlled by shortcut
697      */
698     public abstract static class FrameworkFeatureInfo {
699         private final String mSettingKey;
700         private final String mSettingOnValue;
701         private final String mSettingOffValue;
702         private final int mLabelStringResourceId;
703 
FrameworkFeatureInfo(String settingKey, String settingOnValue, String settingOffValue, int labelStringResourceId)704         FrameworkFeatureInfo(String settingKey, String settingOnValue,
705                 String settingOffValue, int labelStringResourceId) {
706             mSettingKey = settingKey;
707             mSettingOnValue = settingOnValue;
708             mSettingOffValue = settingOffValue;
709             mLabelStringResourceId = labelStringResourceId;
710         }
711 
712         /**
713          * @return The settings key to toggle between two values
714          */
getSettingKey()715         public String getSettingKey() {
716             return mSettingKey;
717         }
718 
719         /**
720          * @return The value to write to settings to turn the feature on
721          */
getSettingOnValue()722         public String getSettingOnValue()  {
723             return mSettingOnValue;
724         }
725 
726         /**
727          * @return The value to write to settings to turn the feature off
728          */
getSettingOffValue()729         public String getSettingOffValue() {
730             return mSettingOffValue;
731         }
732 
getLabel(Context context)733         public String getLabel(Context context) {
734             return context.getString(mLabelStringResourceId);
735         }
736     }
737     /**
738      * Immutable class to hold framework features that have on/off state settings key and can be
739      * controlled by shortcut.
740      */
741     public static class ToggleableFrameworkFeatureInfo extends FrameworkFeatureInfo {
742 
ToggleableFrameworkFeatureInfo(String settingKey, String settingOnValue, String settingOffValue, int labelStringResourceId)743         ToggleableFrameworkFeatureInfo(String settingKey, String settingOnValue,
744                 String settingOffValue, int labelStringResourceId) {
745             super(settingKey, settingOnValue, settingOffValue, labelStringResourceId);
746         }
747     }
748 
749     /**
750      * Immutable class to hold framework features that don't have settings key and can be controlled
751      * by shortcut.
752      */
753     public static class LaunchableFrameworkFeatureInfo extends FrameworkFeatureInfo {
754 
LaunchableFrameworkFeatureInfo(int labelStringResourceId)755         LaunchableFrameworkFeatureInfo(int labelStringResourceId) {
756             super(/* settingKey= */ null, /* settingOnValue= */ null, /* settingOffValue= */ null,
757                     labelStringResourceId);
758         }
759     }
760 
761     // Class to allow mocking of static framework calls
762     public static class FrameworkObjectProvider {
getAccessibilityManagerInstance(Context context)763         public AccessibilityManager getAccessibilityManagerInstance(Context context) {
764             return AccessibilityManager.getInstance(context);
765         }
766 
getAlertDialogBuilder(Context context)767         public AlertDialog.Builder getAlertDialogBuilder(Context context) {
768             final boolean inNightMode = (context.getResources().getConfiguration().uiMode
769                     & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
770             final int themeId = inNightMode ? R.style.Theme_DeviceDefault_Dialog_Alert :
771                     R.style.Theme_DeviceDefault_Light_Dialog_Alert;
772             return new AlertDialog.Builder(context, themeId);
773         }
774 
makeToastFromText(Context context, CharSequence charSequence, int duration)775         public Toast makeToastFromText(Context context, CharSequence charSequence, int duration) {
776             return Toast.makeText(context, charSequence, duration);
777         }
778 
getSystemUiContext()779         public Context getSystemUiContext() {
780             return ActivityThread.currentActivityThread().getSystemUiContext();
781         }
782 
783         /**
784          * @param ctx A context for TextToSpeech
785          * @param listener TextToSpeech initialization callback
786          * @return TextToSpeech instance
787          */
getTextToSpeech(Context ctx, TextToSpeech.OnInitListener listener)788         public TextToSpeech getTextToSpeech(Context ctx, TextToSpeech.OnInitListener listener) {
789             return new TextToSpeech(ctx, listener);
790         }
791 
792         /**
793          * @param ctx context for ringtone
794          * @param uri ringtone uri
795          * @return Ringtone instance
796          */
getRingtone(Context ctx, Uri uri)797         public Ringtone getRingtone(Context ctx, Uri uri) {
798             return RingtoneManager.getRingtone(ctx, uri);
799         }
800     }
801 }
802