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