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