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