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