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 
21 import static com.android.internal.util.ArrayUtils.convertToLongArray;
22 
23 import android.accessibilityservice.AccessibilityServiceInfo;
24 import android.app.ActivityManager;
25 import android.app.ActivityThread;
26 import android.app.AlertDialog;
27 import android.content.ComponentName;
28 import android.content.ContentResolver;
29 import android.content.Context;
30 import android.content.DialogInterface;
31 import android.content.pm.PackageManager;
32 import android.database.ContentObserver;
33 import android.media.AudioAttributes;
34 import android.media.Ringtone;
35 import android.media.RingtoneManager;
36 import android.net.Uri;
37 import android.os.Handler;
38 import android.os.UserHandle;
39 import android.os.Vibrator;
40 import android.provider.Settings;
41 import android.speech.tts.TextToSpeech;
42 import android.speech.tts.Voice;
43 import android.text.TextUtils;
44 import android.util.ArrayMap;
45 import android.util.Slog;
46 import android.view.Window;
47 import android.view.WindowManager;
48 import android.view.accessibility.AccessibilityManager;
49 import android.widget.Toast;
50 
51 import com.android.internal.R;
52 import com.android.internal.util.function.pooled.PooledLambda;
53 
54 import java.util.Collections;
55 import java.util.Locale;
56 import java.util.Map;
57 
58 /**
59  * Class to help manage the accessibility shortcut
60  */
61 public class AccessibilityShortcutController {
62     private static final String TAG = "AccessibilityShortcutController";
63 
64     // Dummy component names for framework features
65     public static final ComponentName COLOR_INVERSION_COMPONENT_NAME =
66             new ComponentName("com.android.server.accessibility", "ColorInversion");
67     public static final ComponentName DALTONIZER_COMPONENT_NAME =
68             new ComponentName("com.android.server.accessibility", "Daltonizer");
69 
70     private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
71             .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
72             .setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY)
73             .build();
74     private static Map<ComponentName, ToggleableFrameworkFeatureInfo> sFrameworkShortcutFeaturesMap;
75 
76     private final Context mContext;
77     private final Handler mHandler;
78     private AlertDialog mAlertDialog;
79     private boolean mIsShortcutEnabled;
80     private boolean mEnabledOnLockScreen;
81     private int mUserId;
82 
83     // Visible for testing
84     public FrameworkObjectProvider mFrameworkObjectProvider = new FrameworkObjectProvider();
85 
86     /**
87      * Get the component name string for the service or feature currently assigned to the
88      * accessiblity shortcut
89      *
90      * @param context A valid context
91      * @param userId The user ID of interest
92      * @return The flattened component name string of the service selected by the user, or the
93      *         string for the default service if the user has not made a selection
94      */
getTargetServiceComponentNameString( Context context, int userId)95     public static String getTargetServiceComponentNameString(
96             Context context, int userId) {
97         final String currentShortcutServiceId = Settings.Secure.getStringForUser(
98                 context.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
99                 userId);
100         if (currentShortcutServiceId != null) {
101             return currentShortcutServiceId;
102         }
103         return context.getString(R.string.config_defaultAccessibilityService);
104     }
105 
106     /**
107      * @return An immutable map from dummy component names to feature info for toggling a framework
108      *         feature
109      */
110     public static Map<ComponentName, ToggleableFrameworkFeatureInfo>
getFrameworkShortcutFeaturesMap()111         getFrameworkShortcutFeaturesMap() {
112         if (sFrameworkShortcutFeaturesMap == null) {
113             Map<ComponentName, ToggleableFrameworkFeatureInfo> featuresMap = new ArrayMap<>(2);
114             featuresMap.put(COLOR_INVERSION_COMPONENT_NAME,
115                     new ToggleableFrameworkFeatureInfo(
116                             Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED,
117                             "1" /* Value to enable */, "0" /* Value to disable */,
118                             R.string.color_inversion_feature_name));
119             featuresMap.put(DALTONIZER_COMPONENT_NAME,
120                     new ToggleableFrameworkFeatureInfo(
121                             Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
122                             "1" /* Value to enable */, "0" /* Value to disable */,
123                             R.string.color_correction_feature_name));
124             sFrameworkShortcutFeaturesMap = Collections.unmodifiableMap(featuresMap);
125         }
126         return sFrameworkShortcutFeaturesMap;
127     }
128 
AccessibilityShortcutController(Context context, Handler handler, int initialUserId)129     public AccessibilityShortcutController(Context context, Handler handler, int initialUserId) {
130         mContext = context;
131         mHandler = handler;
132         mUserId = initialUserId;
133 
134         // Keep track of state of shortcut settings
135         final ContentObserver co = new ContentObserver(handler) {
136             @Override
137             public void onChange(boolean selfChange, Uri uri, int userId) {
138                 if (userId == mUserId) {
139                     onSettingsChanged();
140                 }
141             }
142         };
143         mContext.getContentResolver().registerContentObserver(
144                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE),
145                 false, co, UserHandle.USER_ALL);
146         mContext.getContentResolver().registerContentObserver(
147                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED),
148                 false, co, UserHandle.USER_ALL);
149         mContext.getContentResolver().registerContentObserver(
150                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN),
151                 false, co, UserHandle.USER_ALL);
152         mContext.getContentResolver().registerContentObserver(
153                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN),
154                 false, co, UserHandle.USER_ALL);
155         setCurrentUser(mUserId);
156     }
157 
setCurrentUser(int currentUserId)158     public void setCurrentUser(int currentUserId) {
159         mUserId = currentUserId;
160         onSettingsChanged();
161     }
162 
163     /**
164      * Check if the shortcut is available.
165      *
166      * @param onLockScreen Whether or not the phone is currently locked.
167      *
168      * @return {@code true} if the shortcut is available
169      */
isAccessibilityShortcutAvailable(boolean phoneLocked)170     public boolean isAccessibilityShortcutAvailable(boolean phoneLocked) {
171         return mIsShortcutEnabled && (!phoneLocked || mEnabledOnLockScreen);
172     }
173 
onSettingsChanged()174     public void onSettingsChanged() {
175         final boolean haveValidService =
176                 !TextUtils.isEmpty(getTargetServiceComponentNameString(mContext, mUserId));
177         final ContentResolver cr = mContext.getContentResolver();
178         final boolean enabled = Settings.Secure.getIntForUser(
179                 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED, 1, mUserId) == 1;
180         // Enable the shortcut from the lockscreen by default if the dialog has been shown
181         final int dialogAlreadyShown = Settings.Secure.getIntForUser(
182                 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, mUserId);
183         mEnabledOnLockScreen = Settings.Secure.getIntForUser(
184                 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN,
185                 dialogAlreadyShown, mUserId) == 1;
186         mIsShortcutEnabled = enabled && haveValidService;
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, 0, userId);
198         // Play a notification vibration
199         Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
200         if ((vibrator != null) && vibrator.hasVibrator()) {
201             // Don't check if haptics are disabled, as we need to alert the user that their
202             // way of interacting with the phone may change if they activate the shortcut
203             long[] vibePattern = convertToLongArray(
204                     mContext.getResources().getIntArray(R.array.config_longPressVibePattern));
205             vibrator.vibrate(vibePattern, -1, VIBRATION_ATTRIBUTES);
206         }
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, 1, userId);
226         } else {
227             playNotificationTone();
228             if (mAlertDialog != null) {
229                 mAlertDialog.dismiss();
230                 mAlertDialog = null;
231             }
232 
233             // Show a toast alerting the user to what's happening
234             final String serviceName = getShortcutFeatureDescription(false /* no summary */);
235             if (serviceName == null) {
236                 Slog.e(TAG, "Accessibility shortcut set to invalid service");
237                 return;
238             }
239             // For accessibility services, show a toast explaining what we're doing.
240             final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
241             if (serviceInfo != null) {
242                 String toastMessageFormatString = mContext.getString(isServiceEnabled(serviceInfo)
243                         ? R.string.accessibility_shortcut_disabling_service
244                         : R.string.accessibility_shortcut_enabling_service);
245                 String toastMessage = String.format(toastMessageFormatString, serviceName);
246                 Toast warningToast = mFrameworkObjectProvider.makeToastFromText(
247                         mContext, toastMessage, Toast.LENGTH_LONG);
248                 warningToast.getWindowParams().privateFlags |=
249                         WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
250                 warningToast.show();
251             }
252 
253             mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext)
254                     .performAccessibilityShortcut();
255         }
256     }
257 
createShortcutWarningDialog(int userId)258     private AlertDialog createShortcutWarningDialog(int userId) {
259         final String serviceDescription = getShortcutFeatureDescription(true /* Include summary */);
260 
261         if (serviceDescription == null) {
262             return null;
263         }
264 
265         final String warningMessage = String.format(
266                 mContext.getString(R.string.accessibility_shortcut_toogle_warning),
267                 serviceDescription);
268         final AlertDialog alertDialog = mFrameworkObjectProvider.getAlertDialogBuilder(
269                 // Use SystemUI context so we pick up any theme set in a vendor overlay
270                 mFrameworkObjectProvider.getSystemUiContext())
271                 .setTitle(R.string.accessibility_shortcut_warning_dialog_title)
272                 .setMessage(warningMessage)
273                 .setCancelable(false)
274                 .setPositiveButton(R.string.leave_accessibility_shortcut_on, null)
275                 .setNegativeButton(R.string.disable_accessibility_shortcut,
276                         (DialogInterface d, int which) -> {
277                             Settings.Secure.putStringForUser(mContext.getContentResolver(),
278                                     Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "",
279                                     userId);
280                         })
281                 .setOnCancelListener((DialogInterface d) -> {
282                     // If canceled, treat as if the dialog has never been shown
283                     Settings.Secure.putIntForUser(mContext.getContentResolver(),
284                         Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId);
285                 })
286                 .create();
287         return alertDialog;
288     }
289 
getInfoForTargetService()290     private AccessibilityServiceInfo getInfoForTargetService() {
291         final String currentShortcutServiceString = getTargetServiceComponentNameString(
292                 mContext, UserHandle.USER_CURRENT);
293         if (currentShortcutServiceString == null) {
294             return null;
295         }
296         AccessibilityManager accessibilityManager =
297                 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
298         return accessibilityManager.getInstalledServiceInfoWithComponentName(
299                         ComponentName.unflattenFromString(currentShortcutServiceString));
300     }
301 
getShortcutFeatureDescription(boolean includeSummary)302     private String getShortcutFeatureDescription(boolean includeSummary) {
303         final String currentShortcutServiceString = getTargetServiceComponentNameString(
304                 mContext, UserHandle.USER_CURRENT);
305         if (currentShortcutServiceString == null) {
306             return null;
307         }
308         final ComponentName targetComponentName =
309                 ComponentName.unflattenFromString(currentShortcutServiceString);
310         final ToggleableFrameworkFeatureInfo frameworkFeatureInfo =
311                 getFrameworkShortcutFeaturesMap().get(targetComponentName);
312         if (frameworkFeatureInfo != null) {
313             return frameworkFeatureInfo.getLabel(mContext);
314         }
315         final AccessibilityServiceInfo serviceInfo = mFrameworkObjectProvider
316                 .getAccessibilityManagerInstance(mContext).getInstalledServiceInfoWithComponentName(
317                         targetComponentName);
318         if (serviceInfo == null) {
319             return null;
320         }
321         final PackageManager pm = mContext.getPackageManager();
322         String label = serviceInfo.getResolveInfo().loadLabel(pm).toString();
323         CharSequence summary = serviceInfo.loadSummary(pm);
324         if (!includeSummary || TextUtils.isEmpty(summary)) {
325             return label;
326         }
327         return String.format("%s\n%s", label, summary);
328     }
329 
isServiceEnabled(AccessibilityServiceInfo serviceInfo)330     private boolean isServiceEnabled(AccessibilityServiceInfo serviceInfo) {
331         AccessibilityManager accessibilityManager =
332                 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
333         return accessibilityManager.getEnabledAccessibilityServiceList(
334                 AccessibilityServiceInfo.FEEDBACK_ALL_MASK).contains(serviceInfo);
335     }
336 
hasFeatureLeanback()337     private boolean hasFeatureLeanback() {
338         return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
339     }
340 
playNotificationTone()341     private void playNotificationTone() {
342         // Use USAGE_ASSISTANCE_ACCESSIBILITY for TVs to ensure that TVs play the ringtone as they
343         // have less ways of providing feedback like vibration.
344         final int audioAttributesUsage = hasFeatureLeanback()
345                 ? AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY
346                 : AudioAttributes.USAGE_NOTIFICATION_EVENT;
347 
348         // Play a notification tone
349         final Ringtone tone = mFrameworkObjectProvider.getRingtone(mContext,
350                 Settings.System.DEFAULT_NOTIFICATION_URI);
351         if (tone != null) {
352             tone.setAudioAttributes(new AudioAttributes.Builder()
353                     .setUsage(audioAttributesUsage)
354                     .build());
355             tone.play();
356         }
357     }
358 
performTtsPrompt(AlertDialog alertDialog)359     private boolean performTtsPrompt(AlertDialog alertDialog) {
360         final String serviceName = getShortcutFeatureDescription(false /* no summary */);
361         final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
362         if (TextUtils.isEmpty(serviceName) || serviceInfo == null) {
363             return false;
364         }
365         if ((serviceInfo.flags & AccessibilityServiceInfo
366                 .FLAG_REQUEST_SHORTCUT_WARNING_DIALOG_SPOKEN_FEEDBACK) == 0) {
367             return false;
368         }
369         final TtsPrompt tts = new TtsPrompt(serviceName);
370         alertDialog.setOnDismissListener(dialog -> tts.dismiss());
371         return true;
372     }
373 
374     /**
375      * Class to wrap TextToSpeech for shortcut dialog spoken feedback.
376      */
377     private class TtsPrompt implements TextToSpeech.OnInitListener {
378         private final CharSequence mText;
379         private boolean mDismiss;
380         private TextToSpeech mTts;
381 
TtsPrompt(String serviceName)382         TtsPrompt(String serviceName) {
383             mText = mContext.getString(R.string.accessibility_shortcut_spoken_feedback,
384                     serviceName);
385             mTts = mFrameworkObjectProvider.getTextToSpeech(mContext, this);
386         }
387 
388         /**
389          * Releases the resources used by the TextToSpeech, when dialog dismiss.
390          */
dismiss()391         public void dismiss() {
392             mDismiss = true;
393             mHandler.sendMessage(PooledLambda.obtainMessage(TextToSpeech::shutdown, mTts));
394         }
395 
396         @Override
onInit(int status)397         public void onInit(int status) {
398             if (status != TextToSpeech.SUCCESS) {
399                 Slog.d(TAG, "Tts init fail, status=" + Integer.toString(status));
400                 playNotificationTone();
401                 return;
402             }
403             mHandler.sendMessage(PooledLambda.obtainMessage(TtsPrompt::play, this));
404         }
405 
play()406         private void play() {
407             if (mDismiss) {
408                 return;
409             }
410             int status = TextToSpeech.ERROR;
411             if (setLanguage(Locale.getDefault())) {
412                 status = mTts.speak(mText, TextToSpeech.QUEUE_FLUSH, null, null);
413             }
414             if (status != TextToSpeech.SUCCESS) {
415                 Slog.d(TAG, "Tts play fail");
416                 playNotificationTone();
417             }
418         }
419 
420         /**
421          * @return false if tts language is not available
422          */
setLanguage(final Locale locale)423         private boolean setLanguage(final Locale locale) {
424             int status = mTts.isLanguageAvailable(locale);
425             if (status == TextToSpeech.LANG_MISSING_DATA
426                     || status == TextToSpeech.LANG_NOT_SUPPORTED) {
427                 return false;
428             }
429             mTts.setLanguage(locale);
430             Voice voice = mTts.getVoice();
431             if (voice == null || (voice.getFeatures() != null && voice.getFeatures()
432                     .contains(TextToSpeech.Engine.KEY_FEATURE_NOT_INSTALLED))) {
433                 return false;
434             }
435             return true;
436         }
437     }
438 
439     /**
440      * Immutable class to hold info about framework features that can be controlled by shortcut
441      */
442     public static class ToggleableFrameworkFeatureInfo {
443         private final String mSettingKey;
444         private final String mSettingOnValue;
445         private final String mSettingOffValue;
446         private final int mLabelStringResourceId;
447         // These go to the settings wrapper
448         private int mIconDrawableId;
449 
ToggleableFrameworkFeatureInfo(String settingKey, String settingOnValue, String settingOffValue, int labelStringResourceId)450         ToggleableFrameworkFeatureInfo(String settingKey, String settingOnValue,
451                 String settingOffValue, int labelStringResourceId) {
452             mSettingKey = settingKey;
453             mSettingOnValue = settingOnValue;
454             mSettingOffValue = settingOffValue;
455             mLabelStringResourceId = labelStringResourceId;
456         }
457 
458         /**
459          * @return The settings key to toggle between two values
460          */
getSettingKey()461         public String getSettingKey() {
462             return mSettingKey;
463         }
464 
465         /**
466          * @return The value to write to settings to turn the feature on
467          */
getSettingOnValue()468         public String getSettingOnValue() {
469             return mSettingOnValue;
470         }
471 
472         /**
473          * @return The value to write to settings to turn the feature off
474          */
getSettingOffValue()475         public String getSettingOffValue() {
476             return mSettingOffValue;
477         }
478 
getLabel(Context context)479         public String getLabel(Context context) {
480             return context.getString(mLabelStringResourceId);
481         }
482     }
483 
484     // Class to allow mocking of static framework calls
485     public static class FrameworkObjectProvider {
getAccessibilityManagerInstance(Context context)486         public AccessibilityManager getAccessibilityManagerInstance(Context context) {
487             return AccessibilityManager.getInstance(context);
488         }
489 
getAlertDialogBuilder(Context context)490         public AlertDialog.Builder getAlertDialogBuilder(Context context) {
491             return new AlertDialog.Builder(context);
492         }
493 
makeToastFromText(Context context, CharSequence charSequence, int duration)494         public Toast makeToastFromText(Context context, CharSequence charSequence, int duration) {
495             return Toast.makeText(context, charSequence, duration);
496         }
497 
getSystemUiContext()498         public Context getSystemUiContext() {
499             return ActivityThread.currentActivityThread().getSystemUiContext();
500         }
501 
502         /**
503          * @param ctx A context for TextToSpeech
504          * @param listener TextToSpeech initialization callback
505          * @return TextToSpeech instance
506          */
getTextToSpeech(Context ctx, TextToSpeech.OnInitListener listener)507         public TextToSpeech getTextToSpeech(Context ctx, TextToSpeech.OnInitListener listener) {
508             return new TextToSpeech(ctx, listener);
509         }
510 
511         /**
512          * @param ctx context for ringtone
513          * @param uri ringtone uri
514          * @return Ringtone instance
515          */
getRingtone(Context ctx, Uri uri)516         public Ringtone getRingtone(Context ctx, Uri uri) {
517             return RingtoneManager.getRingtone(ctx, uri);
518         }
519     }
520 }
521