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