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