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