Home | History | Annotate | Download | only in policy
      1 /*
      2  * Copyright (C) 2012 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.server.policy;
     18 
     19 import android.accessibilityservice.AccessibilityServiceInfo;
     20 import android.app.ActivityManager;
     21 import android.app.AlertDialog;
     22 import android.content.ComponentName;
     23 import android.content.ContentResolver;
     24 import android.content.Context;
     25 import android.content.DialogInterface;
     26 import android.database.ContentObserver;
     27 import android.media.AudioAttributes;
     28 import android.media.Ringtone;
     29 import android.media.RingtoneManager;
     30 import android.net.Uri;
     31 import android.os.Handler;
     32 import android.os.UserHandle;
     33 import android.os.Vibrator;
     34 import android.provider.Settings;
     35 import android.text.TextUtils;
     36 import android.util.Slog;
     37 import android.view.Window;
     38 import android.view.WindowManager;
     39 import android.view.accessibility.AccessibilityManager;
     40 
     41 import android.widget.Toast;
     42 import com.android.internal.R;
     43 
     44 import java.util.List;
     45 
     46 import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG;
     47 
     48 /**
     49  * Class to help manage the accessibility shortcut
     50  */
     51 public class AccessibilityShortcutController {
     52     private static final String TAG = "AccessibilityShortcutController";
     53     private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
     54             .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
     55             .setUsage(AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY)
     56             .build();
     57 
     58 
     59     private final Context mContext;
     60     private AlertDialog mAlertDialog;
     61     private boolean mIsShortcutEnabled;
     62     private boolean mEnabledOnLockScreen;
     63     private int mUserId;
     64 
     65     // Visible for testing
     66     public FrameworkObjectProvider mFrameworkObjectProvider = new FrameworkObjectProvider();
     67 
     68     public static String getTargetServiceComponentNameString(
     69             Context context, int userId) {
     70         final String currentShortcutServiceId = Settings.Secure.getStringForUser(
     71                 context.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
     72                 userId);
     73         if (currentShortcutServiceId != null) {
     74             return currentShortcutServiceId;
     75         }
     76         return context.getString(R.string.config_defaultAccessibilityService);
     77     }
     78 
     79     public AccessibilityShortcutController(Context context, Handler handler, int initialUserId) {
     80         mContext = context;
     81 
     82         // Keep track of state of shortcut settings
     83         final ContentObserver co = new ContentObserver(handler) {
     84             @Override
     85             public void onChange(boolean selfChange, Uri uri, int userId) {
     86                 if (userId == mUserId) {
     87                     onSettingsChanged();
     88                 }
     89             }
     90         };
     91         mContext.getContentResolver().registerContentObserver(
     92                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE),
     93                 false, co, UserHandle.USER_ALL);
     94         mContext.getContentResolver().registerContentObserver(
     95                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED),
     96                 false, co, UserHandle.USER_ALL);
     97         mContext.getContentResolver().registerContentObserver(
     98                 Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN),
     99                 false, co, UserHandle.USER_ALL);
    100         setCurrentUser(mUserId);
    101     }
    102 
    103     public void setCurrentUser(int currentUserId) {
    104         mUserId = currentUserId;
    105         onSettingsChanged();
    106     }
    107 
    108     /**
    109      * Check if the shortcut is available.
    110      *
    111      * @param onLockScreen Whether or not the phone is currently locked.
    112      *
    113      * @return {@code true} if the shortcut is available
    114      */
    115     public boolean isAccessibilityShortcutAvailable(boolean phoneLocked) {
    116         return mIsShortcutEnabled && (!phoneLocked || mEnabledOnLockScreen);
    117     }
    118 
    119     public void onSettingsChanged() {
    120         final boolean haveValidService =
    121                 !TextUtils.isEmpty(getTargetServiceComponentNameString(mContext, mUserId));
    122         final ContentResolver cr = mContext.getContentResolver();
    123         final boolean enabled = Settings.Secure.getIntForUser(
    124                 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED, 1, mUserId) == 1;
    125         mEnabledOnLockScreen = Settings.Secure.getIntForUser(
    126                 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN, 0, mUserId) == 1;
    127         mIsShortcutEnabled = enabled && haveValidService;
    128     }
    129 
    130     /**
    131      * Called when the accessibility shortcut is activated
    132      */
    133     public void performAccessibilityShortcut() {
    134         Slog.d(TAG, "Accessibility shortcut activated");
    135         final ContentResolver cr = mContext.getContentResolver();
    136         final int userId = ActivityManager.getCurrentUser();
    137         final int dialogAlreadyShown = Settings.Secure.getIntForUser(
    138                 cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId);
    139 
    140         // Play a notification tone
    141         final Ringtone tone =
    142                 RingtoneManager.getRingtone(mContext, Settings.System.DEFAULT_NOTIFICATION_URI);
    143         if (tone != null) {
    144             tone.setAudioAttributes(new AudioAttributes.Builder()
    145                 .setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT)
    146                 .build());
    147             tone.play();
    148         }
    149 
    150         // Play a notification vibration
    151         Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
    152         if ((vibrator != null) && vibrator.hasVibrator()) {
    153             // Don't check if haptics are disabled, as we need to alert the user that their
    154             // way of interacting with the phone may change if they activate the shortcut
    155             long[] vibePattern = PhoneWindowManager.getLongIntArray(mContext.getResources(),
    156                     R.array.config_safeModeDisabledVibePattern);
    157             vibrator.vibrate(vibePattern, -1, VIBRATION_ATTRIBUTES);
    158         }
    159 
    160 
    161         if (dialogAlreadyShown == 0) {
    162             // The first time, we show a warning rather than toggle the service to give the user a
    163             // chance to turn off this feature before stuff gets enabled.
    164             mAlertDialog = createShortcutWarningDialog(userId);
    165             if (mAlertDialog == null) {
    166                 return;
    167             }
    168             Window w = mAlertDialog.getWindow();
    169             WindowManager.LayoutParams attr = w.getAttributes();
    170             attr.type = TYPE_KEYGUARD_DIALOG;
    171             w.setAttributes(attr);
    172             mAlertDialog.show();
    173             Settings.Secure.putIntForUser(
    174                     cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 1, userId);
    175         } else {
    176             if (mAlertDialog != null) {
    177                 mAlertDialog.dismiss();
    178                 mAlertDialog = null;
    179             }
    180 
    181             // Show a toast alerting the user to what's happening
    182             final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
    183             if (serviceInfo == null) {
    184                 Slog.e(TAG, "Accessibility shortcut set to invalid service");
    185                 return;
    186             }
    187             String toastMessageFormatString = mContext.getString(isServiceEnabled(serviceInfo)
    188                     ? R.string.accessibility_shortcut_disabling_service
    189                     : R.string.accessibility_shortcut_enabling_service);
    190             String toastMessage = String.format(toastMessageFormatString,
    191                     serviceInfo.getResolveInfo()
    192                             .loadLabel(mContext.getPackageManager()).toString());
    193             Toast warningToast = mFrameworkObjectProvider.makeToastFromText(
    194                     mContext, toastMessage, Toast.LENGTH_LONG);
    195             warningToast.getWindowParams().privateFlags |=
    196                     WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
    197             warningToast.show();
    198 
    199             mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext)
    200                     .performAccessibilityShortcut();
    201         }
    202     }
    203 
    204     private AlertDialog createShortcutWarningDialog(int userId) {
    205         final AccessibilityServiceInfo serviceInfo = getInfoForTargetService();
    206 
    207         if (serviceInfo == null) {
    208             return null;
    209         }
    210 
    211         final String warningMessage = String.format(
    212                 mContext.getString(R.string.accessibility_shortcut_toogle_warning),
    213                 serviceInfo.getResolveInfo().loadLabel(mContext.getPackageManager()).toString());
    214         final AlertDialog alertDialog = mFrameworkObjectProvider.getAlertDialogBuilder(mContext)
    215                 .setTitle(R.string.accessibility_shortcut_warning_dialog_title)
    216                 .setMessage(warningMessage)
    217                 .setCancelable(false)
    218                 .setPositiveButton(R.string.leave_accessibility_shortcut_on, null)
    219                 .setNegativeButton(R.string.disable_accessibility_shortcut,
    220                         (DialogInterface d, int which) -> {
    221                             Settings.Secure.putStringForUser(mContext.getContentResolver(),
    222                                     Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, "",
    223                                     userId);
    224                         })
    225                 .setOnCancelListener((DialogInterface d) -> {
    226                     // If canceled, treat as if the dialog has never been shown
    227                     Settings.Secure.putIntForUser(mContext.getContentResolver(),
    228                         Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0, userId);
    229                 })
    230                 .create();
    231         return alertDialog;
    232     }
    233 
    234     private AccessibilityServiceInfo getInfoForTargetService() {
    235         final String currentShortcutServiceString = getTargetServiceComponentNameString(
    236                 mContext, UserHandle.USER_CURRENT);
    237         if (currentShortcutServiceString == null) {
    238             return null;
    239         }
    240         AccessibilityManager accessibilityManager =
    241                 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
    242         return accessibilityManager.getInstalledServiceInfoWithComponentName(
    243                         ComponentName.unflattenFromString(currentShortcutServiceString));
    244     }
    245 
    246     private boolean isServiceEnabled(AccessibilityServiceInfo serviceInfo) {
    247         AccessibilityManager accessibilityManager =
    248                 mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext);
    249         return accessibilityManager.getEnabledAccessibilityServiceList(
    250                 AccessibilityServiceInfo.FEEDBACK_ALL_MASK).contains(serviceInfo);
    251     }
    252 
    253     // Class to allow mocking of static framework calls
    254     public static class FrameworkObjectProvider {
    255         public AccessibilityManager getAccessibilityManagerInstance(Context context) {
    256             return AccessibilityManager.getInstance(context);
    257         }
    258 
    259         public AlertDialog.Builder getAlertDialogBuilder(Context context) {
    260             return new AlertDialog.Builder(context);
    261         }
    262 
    263         public Toast makeToastFromText(Context context, CharSequence charSequence, int duration) {
    264             return Toast.makeText(context, charSequence, duration);
    265         }
    266     }
    267 }
    268