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