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