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