Home | History | Annotate | Download | only in accessibility
      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 android.accessibilityservice.AccessibilityServiceInfo;
     20 import android.app.ActivityManager;
     21 import android.app.ActivityThread;
     22 import android.app.AlertDialog;
     23 import android.content.ComponentName;
     24 import android.content.ContentResolver;
     25 import android.content.Context;
     26 import android.content.DialogInterface;
     27 import android.content.pm.PackageManager;
     28 import android.database.ContentObserver;
     29 import android.media.AudioAttributes;
     30 import android.media.Ringtone;
     31 import android.media.RingtoneManager;
     32 import android.net.Uri;
     33 import android.os.Handler;
     34 import android.os.UserHandle;
     35 import android.os.Vibrator;
     36 import android.provider.Settings;
     37 import android.text.TextUtils;
     38 import android.util.ArrayMap;
     39 import android.util.Slog;
     40 import android.view.Window;
     41 import android.view.WindowManager;
     42 import android.view.accessibility.AccessibilityManager;
     43 
     44 import android.widget.Toast;
     45 import com.android.internal.R;
     46 
     47 import java.util.Collections;
     48 import java.util.Map;
     49 
     50 import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
     51 
     52 import static com.android.internal.util.ArrayUtils.convertToLongArray;
     53 
     54 /**
     55  * Class to help manage the accessibility shortcut
     56  */
     57 public class AccessibilityShortcutController {
     58     private static final String TAG = "AccessibilityShortcutController";
     59 
     60     // Dummy component names for framework features
     61     public static final ComponentName COLOR_INVERSION_COMPONENT_NAME =
     62             new ComponentName("com.android.server.accessibility", "ColorInversion");
     63     public static final ComponentName DALTONIZER_COMPONENT_NAME =
     64             new ComponentName("com.android.server.accessibility", "Daltonizer");
     65 
     66     private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
     67             .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
     68             .setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY)
     69             .build();
     70     private static Map<ComponentName, ToggleableFrameworkFeatureInfo> sFrameworkShortcutFeaturesMap;
     71 
     72     private final Context mContext;
     73     private AlertDialog mAlertDialog;
     74     private boolean mIsShortcutEnabled;
     75     private boolean mEnabledOnLockScreen;
     76     private int mUserId;
     77 
     78     // Visible for testing
     79     public FrameworkObjectProvider mFrameworkObjectProvider = new FrameworkObjectProvider();
     80 
     81     /**
     82      * Get the component name string for the service or feature currently assigned to the
     83      * accessiblity shortcut
     84      *
     85      * @param context A valid context
     86      * @param userId The user ID of interest
     87      * @return The flattened component name string of the service selected by the user, or the
     88      *         string for the default service if the user has not made a selection
     89      */
     90     public static String getTargetServiceComponentNameString(
     91             Context context, int userId) {
     92         final String currentShortcutServiceId = Settings.Secure.getStringForUser(
     93                 context.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
     94                 userId);
     95         if (currentShortcutServiceId != null) {
     96             return currentShortcutServiceId;
     97         }
     98         return context.getString(R.string.config_defaultAccessibilityService);
     99     }
    100 
    101     /**
    102      * @return An immutable map from dummy component names to feature info for toggling a framework
    103      *         feature
    104      */
    105     public static Map<ComponentName, ToggleableFrameworkFeatureInfo>
    106         getFrameworkShortcutFeaturesMap() {
    107         if (sFrameworkShortcutFeaturesMap == null) {
    108             Map<ComponentName, ToggleableFrameworkFeatureInfo> featuresMap = new ArrayMap<>(2);
    109             featuresMap.put(COLOR_INVERSION_COMPONENT_NAME,
    110                     new ToggleableFrameworkFeatureInfo(
    111                             Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED,
    112                             "1" /* Value to enable */, "0" /* Value to disable */,
    113                             R.string.color_inversion_feature_name));
    114             featuresMap.put(DALTONIZER_COMPONENT_NAME,
    115                     new ToggleableFrameworkFeatureInfo(
    116                             Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
    117                             "1" /* Value to enable */, "0" /* Value to disable */,
    118                             R.string.color_correction_feature_name));
    119             sFrameworkShortcutFeaturesMap = Collections.unmodifiableMap(featuresMap);
    120         }
    121         return sFrameworkShortcutFeaturesMap;
    122     }
    123 
    124     public AccessibilityShortcutController(Context context, Handler handler, int initialUserId) {
    125         mContext = context;
    126         mUserId = initialUserId;
    127 
    128         // Keep track of state of shortcut settings
    129         final ContentObserver co = new ContentObserver(handler) {
    130             @Override
    131             public void onChange(boolean selfChange, Uri uri, int userId) {
    132                 if (userId == mUserId) {
    133                     onSettingsChanged();
    134                 }
    135             }
    136         };
    137         mContext.getContentResolver().registerContentObserver(
    138                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE),
    139                 false, co, UserHandle.USER_ALL);
    140         mContext.getContentResolver().registerContentObserver(
    141                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED),
    142                 false, co, UserHandle.USER_ALL);
    143         mContext.getContentResolver().registerContentObserver(
    144                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN),
    145                 false, co, UserHandle.USER_ALL);
    146         mContext.getContentResolver().registerContentObserver(
    147                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN),
    148                 false, co, UserHandle.USER_ALL);
    149         setCurrentUser(mUserId);
    150     }
    151 
    152     public void setCurrentUser(int currentUserId) {
    153         mUserId = currentUserId;
    154         onSettingsChanged();
    155     }
    156 
    157     /**
    158      * Check if the shortcut is available.
    159      *
    160      * @param onLockScreen Whether or not the phone is currently locked.
    161      *
    162      * @return {@code true} if the shortcut is available
    163      */
    164     public boolean isAccessibilityShortcutAvailable(boolean phoneLocked) {
    165         return mIsShortcutEnabled && (!phoneLocked || mEnabledOnLockScreen);
    166     }
    167 
    168     public void onSettingsChanged() {
    169         final boolean haveValidService =
    170                 !TextUtils.isEmpty(getTargetServiceComponentNameString(mContext, mUserId));
    171         final ContentResolver cr = mContext.getContentResolver();
    172         final boolean enabled = Settings.Secure.getIntForUser(
    173                 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED, 1, mUserId) == 1;
    174         // Enable the shortcut from the lockscreen by default if the dialog has been shown
    175         final int dialogAlreadyShown = Settings.Secure.getIntForUser(
    176                 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, mUserId);
    177         mEnabledOnLockScreen = Settings.Secure.getIntForUser(
    178                 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN,
    179                 dialogAlreadyShown, mUserId) == 1;
    180         mIsShortcutEnabled = enabled && haveValidService;
    181     }
    182 
    183     /**
    184      * Called when the accessibility shortcut is activated
    185      */
    186     public void performAccessibilityShortcut() {
    187         Slog.d(TAG, "Accessibility shortcut activated");
    188         final ContentResolver cr = mContext.getContentResolver();
    189         final int userId = ActivityManager.getCurrentUser();
    190         final int dialogAlreadyShown = Settings.Secure.getIntForUser(
    191                 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId);
    192         // Use USAGE_ASSISTANCE_ACCESSIBILITY for TVs to ensure that TVs play the ringtone as they
    193         // have less ways of providing feedback like vibration.
    194         final int audioAttributesUsage = hasFeatureLeanback()
    195                 ? AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY
    196                 : AudioAttributes.USAGE_NOTIFICATION_EVENT;
    197 
    198         // Play a notification tone
    199         final Ringtone tone =
    200                 RingtoneManager.getRingtone(mContext, Settings.System.DEFAULT_NOTIFICATION_URI);
    201         if (tone != null) {
    202             tone.setAudioAttributes(new AudioAttributes.Builder()
    203                 .setUsage(audioAttributesUsage)
    204                 .build());
    205             tone.play();
    206         }
    207 
    208         // Play a notification vibration
    209         Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
    210         if ((vibrator != null) && vibrator.hasVibrator()) {
    211             // Don't check if haptics are disabled, as we need to alert the user that their
    212             // way of interacting with the phone may change if they activate the shortcut
    213             long[] vibePattern = convertToLongArray(
    214                     mContext.getResources().getIntArray(R.array.config_longPressVibePattern));
    215             vibrator.vibrate(vibePattern, -1, VIBRATION_ATTRIBUTES);
    216         }
    217 
    218 
    219         if (dialogAlreadyShown == 0) {
    220             // The first time, we show a warning rather than toggle the service to give the user a
    221             // chance to turn off this feature before stuff gets enabled.
    222             mAlertDialog = createShortcutWarningDialog(userId);
    223             if (mAlertDialog == null) {
    224                 return;
    225             }
    226             Window w = mAlertDialog.getWindow();
    227             WindowManager.LayoutParams attr = w.getAttributes();
    228             attr.type = TYPE_KEYGUARD_DIALOG;
    229             w.setAttributes(attr);
    230             mAlertDialog.show();
    231             Settings.Secure.putIntForUser(
    232                     cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 1, userId);
    233         } else {
    234             if (mAlertDialog != null) {
    235                 mAlertDialog.dismiss();
    236                 mAlertDialog = null;
    237             }
    238 
    239             // Show a toast alerting the user to what's happening
    240             final String serviceName = getShortcutFeatureDescription(false /* no summary */);
    241             if (serviceName == null) {
    242                 Slog.e(TAG, "Accessibility shortcut set to invalid service");
    243                 return;
    244             }
    245             // For accessibility services, show a toast explaining what we're doing.
    246             final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
    247             if (serviceInfo != null) {
    248                 String toastMessageFormatString = mContext.getString(isServiceEnabled(serviceInfo)
    249                         ? R.string.accessibility_shortcut_disabling_service
    250                         : R.string.accessibility_shortcut_enabling_service);
    251                 String toastMessage = String.format(toastMessageFormatString, serviceName);
    252                 Toast warningToast = mFrameworkObjectProvider.makeToastFromText(
    253                         mContext, toastMessage, Toast.LENGTH_LONG);
    254                 warningToast.getWindowParams().privateFlags |=
    255                         WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
    256                 warningToast.show();
    257             }
    258 
    259             mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext)
    260                     .performAccessibilityShortcut();
    261         }
    262     }
    263 
    264     private AlertDialog createShortcutWarningDialog(int userId) {
    265         final String serviceDescription = getShortcutFeatureDescription(true /* Include summary */);
    266 
    267         if (serviceDescription == null) {
    268             return null;
    269         }
    270 
    271         final String warningMessage = String.format(
    272                 mContext.getString(R.string.accessibility_shortcut_toogle_warning),
    273                 serviceDescription);
    274         final AlertDialog alertDialog = mFrameworkObjectProvider.getAlertDialogBuilder(
    275                 // Use SystemUI context so we pick up any theme set in a vendor overlay
    276                 mFrameworkObjectProvider.getSystemUiContext())
    277                 .setTitle(R.string.accessibility_shortcut_warning_dialog_title)
    278                 .setMessage(warningMessage)
    279                 .setCancelable(false)
    280                 .setPositiveButton(R.string.leave_accessibility_shortcut_on, null)
    281                 .setNegativeButton(R.string.disable_accessibility_shortcut,
    282                         (DialogInterface d, int which) -> {
    283                             Settings.Secure.putStringForUser(mContext.getContentResolver(),
    284                                     Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "",
    285                                     userId);
    286                         })
    287                 .setOnCancelListener((DialogInterface d) -> {
    288                     // If canceled, treat as if the dialog has never been shown
    289                     Settings.Secure.putIntForUser(mContext.getContentResolver(),
    290                         Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId);
    291                 })
    292                 .create();
    293         return alertDialog;
    294     }
    295 
    296     private AccessibilityServiceInfo getInfoForTargetService() {
    297         final String currentShortcutServiceString = getTargetServiceComponentNameString(
    298                 mContext, UserHandle.USER_CURRENT);
    299         if (currentShortcutServiceString == null) {
    300             return null;
    301         }
    302         AccessibilityManager accessibilityManager =
    303                 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
    304         return accessibilityManager.getInstalledServiceInfoWithComponentName(
    305                         ComponentName.unflattenFromString(currentShortcutServiceString));
    306     }
    307 
    308     private String getShortcutFeatureDescription(boolean includeSummary) {
    309         final String currentShortcutServiceString = getTargetServiceComponentNameString(
    310                 mContext, UserHandle.USER_CURRENT);
    311         if (currentShortcutServiceString == null) {
    312             return null;
    313         }
    314         final ComponentName targetComponentName =
    315                 ComponentName.unflattenFromString(currentShortcutServiceString);
    316         final ToggleableFrameworkFeatureInfo frameworkFeatureInfo =
    317                 getFrameworkShortcutFeaturesMap().get(targetComponentName);
    318         if (frameworkFeatureInfo != null) {
    319             return frameworkFeatureInfo.getLabel(mContext);
    320         }
    321         final AccessibilityServiceInfo serviceInfo = mFrameworkObjectProvider
    322                 .getAccessibilityManagerInstance(mContext).getInstalledServiceInfoWithComponentName(
    323                         targetComponentName);
    324         if (serviceInfo == null) {
    325             return null;
    326         }
    327         final PackageManager pm = mContext.getPackageManager();
    328         String label = serviceInfo.getResolveInfo().loadLabel(pm).toString();
    329         CharSequence summary = serviceInfo.loadSummary(pm);
    330         if (!includeSummary || TextUtils.isEmpty(summary)) {
    331             return label;
    332         }
    333         return String.format("%s\n%s", label, summary);
    334     }
    335 
    336     private boolean isServiceEnabled(AccessibilityServiceInfo serviceInfo) {
    337         AccessibilityManager accessibilityManager =
    338                 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
    339         return accessibilityManager.getEnabledAccessibilityServiceList(
    340                 AccessibilityServiceInfo.FEEDBACK_ALL_MASK).contains(serviceInfo);
    341     }
    342 
    343     private boolean hasFeatureLeanback() {
    344         return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
    345     }
    346 
    347     /**
    348      * Immutable class to hold info about framework features that can be controlled by shortcut
    349      */
    350     public static class ToggleableFrameworkFeatureInfo {
    351         private final String mSettingKey;
    352         private final String mSettingOnValue;
    353         private final String mSettingOffValue;
    354         private final int mLabelStringResourceId;
    355         // These go to the settings wrapper
    356         private int mIconDrawableId;
    357 
    358         ToggleableFrameworkFeatureInfo(String settingKey, String settingOnValue,
    359                 String settingOffValue, int labelStringResourceId) {
    360             mSettingKey = settingKey;
    361             mSettingOnValue = settingOnValue;
    362             mSettingOffValue = settingOffValue;
    363             mLabelStringResourceId = labelStringResourceId;
    364         }
    365 
    366         /**
    367          * @return The settings key to toggle between two values
    368          */
    369         public String getSettingKey() {
    370             return mSettingKey;
    371         }
    372 
    373         /**
    374          * @return The value to write to settings to turn the feature on
    375          */
    376         public String getSettingOnValue() {
    377             return mSettingOnValue;
    378         }
    379 
    380         /**
    381          * @return The value to write to settings to turn the feature off
    382          */
    383         public String getSettingOffValue() {
    384             return mSettingOffValue;
    385         }
    386 
    387         public String getLabel(Context context) {
    388             return context.getString(mLabelStringResourceId);
    389         }
    390     }
    391 
    392     // Class to allow mocking of static framework calls
    393     public static class FrameworkObjectProvider {
    394         public AccessibilityManager getAccessibilityManagerInstance(Context context) {
    395             return AccessibilityManager.getInstance(context);
    396         }
    397 
    398         public AlertDialog.Builder getAlertDialogBuilder(Context context) {
    399             return new AlertDialog.Builder(context);
    400         }
    401 
    402         public Toast makeToastFromText(Context context, CharSequence charSequence, int duration) {
    403             return Toast.makeText(context, charSequence, duration);
    404         }
    405 
    406         public Context getSystemUiContext() {
    407             return ActivityThread.currentActivityThread().getSystemUiContext();
    408         }
    409     }
    410 }
    411