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