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