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