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