1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of 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, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.accessibility; 18 19 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; 20 import static android.view.WindowInsets.Type.displayCutout; 21 import static android.view.WindowInsets.Type.systemBars; 22 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; 23 24 import android.accessibilityservice.AccessibilityServiceInfo; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.res.Resources; 28 import android.graphics.Insets; 29 import android.graphics.Rect; 30 import android.os.Build; 31 import android.os.UserHandle; 32 import android.provider.Settings; 33 import android.text.TextUtils; 34 import android.util.TypedValue; 35 import android.view.WindowManager; 36 import android.view.WindowMetrics; 37 import android.view.accessibility.AccessibilityManager; 38 39 import androidx.annotation.IntDef; 40 import androidx.annotation.NonNull; 41 import androidx.annotation.StringRes; 42 import androidx.annotation.VisibleForTesting; 43 44 import com.android.internal.accessibility.util.ShortcutUtils; 45 46 import java.lang.annotation.Retention; 47 import java.lang.annotation.RetentionPolicy; 48 import java.util.Set; 49 import java.util.StringJoiner; 50 51 /** Provides utility methods to accessibility settings only. */ 52 public final class AccessibilityUtil { 53 AccessibilityUtil()54 private AccessibilityUtil(){} 55 56 /** 57 * Annotation for different accessibilityService fragment UI type. 58 * 59 * {@code VOLUME_SHORTCUT_TOGGLE} for displaying basic accessibility service fragment but 60 * only hardware shortcut allowed. 61 * {@code INVISIBLE_TOGGLE} for displaying basic accessibility service fragment without 62 * switch bar. 63 * {@code TOGGLE} for displaying basic accessibility service fragment. 64 */ 65 @Retention(RetentionPolicy.SOURCE) 66 @IntDef({ 67 AccessibilityServiceFragmentType.VOLUME_SHORTCUT_TOGGLE, 68 AccessibilityServiceFragmentType.INVISIBLE_TOGGLE, 69 AccessibilityServiceFragmentType.TOGGLE, 70 }) 71 72 public @interface AccessibilityServiceFragmentType { 73 int VOLUME_SHORTCUT_TOGGLE = 0; 74 int INVISIBLE_TOGGLE = 1; 75 int TOGGLE = 2; 76 } 77 78 // TODO(b/147021230): Will move common functions and variables to 79 // android/internal/accessibility folder 80 private static final char COMPONENT_NAME_SEPARATOR = ':'; 81 private static final TextUtils.SimpleStringSplitter sStringColonSplitter = 82 new TextUtils.SimpleStringSplitter(COMPONENT_NAME_SEPARATOR); 83 84 /** 85 * Annotation for different user shortcut type UI type. 86 * 87 * {@code EMPTY} for displaying default value. 88 * {@code SOFTWARE} for displaying specifying the accessibility services or features which 89 * choose accessibility button in the navigation bar as preferred shortcut. 90 * {@code HARDWARE} for displaying specifying the accessibility services or features which 91 * choose accessibility shortcut as preferred shortcut. 92 * {@code TRIPLETAP} for displaying specifying magnification to be toggled via quickly 93 * tapping screen 3 times as preferred shortcut. 94 * {@code TWOFINGER_DOUBLETAP} for displaying specifying magnification to be toggled via 95 * quickly tapping screen 2 times with two fingers as preferred shortcut. 96 * {@code QUICK_SETTINGS} for displaying specifying the accessibility services or features which 97 * choose Quick Settings as preferred shortcut. 98 */ 99 @Retention(RetentionPolicy.SOURCE) 100 @IntDef({ 101 UserShortcutType.EMPTY, 102 UserShortcutType.SOFTWARE, 103 UserShortcutType.HARDWARE, 104 UserShortcutType.TRIPLETAP, 105 UserShortcutType.TWOFINGER_DOUBLETAP, 106 UserShortcutType.QUICK_SETTINGS, 107 }) 108 109 /** Denotes the user shortcut type. */ 110 public @interface UserShortcutType { 111 int EMPTY = 0; 112 int SOFTWARE = 1; 113 int HARDWARE = 1 << 1; 114 int TRIPLETAP = 1 << 2; 115 int TWOFINGER_DOUBLETAP = 1 << 3; 116 int QUICK_SETTINGS = 1 << 4; 117 } 118 119 /** 120 * Denotes the quick setting tooltip type. 121 * 122 * {@code GUIDE_TO_EDIT} for QS tiles that need to be added by editing. 123 * {@code GUIDE_TO_DIRECT_USE} for QS tiles that have been auto-added already. 124 */ 125 public @interface QuickSettingsTooltipType { 126 int GUIDE_TO_EDIT = 0; 127 int GUIDE_TO_DIRECT_USE = 1; 128 } 129 130 /** Denotes the accessibility enabled status */ 131 @Retention(RetentionPolicy.SOURCE) 132 public @interface State { 133 int OFF = 0; 134 int ON = 1; 135 } 136 137 /** 138 * Returns On/Off string according to the setting which specifies the integer value 1 or 0. This 139 * setting is defined in the secure system settings {@link android.provider.Settings.Secure}. 140 */ getSummary( Context context, String settingsSecureKey, @StringRes int enabledString, @StringRes int disabledString)141 static CharSequence getSummary( 142 Context context, String settingsSecureKey, @StringRes int enabledString, 143 @StringRes int disabledString) { 144 boolean enabled = Settings.Secure.getInt(context.getContentResolver(), 145 settingsSecureKey, State.OFF) == State.ON; 146 return context.getResources().getText(enabled ? enabledString : disabledString); 147 } 148 149 /** 150 * Capitalizes a string by capitalizing the first character and making the remaining characters 151 * lower case. 152 */ capitalize(String stringToCapitalize)153 public static String capitalize(String stringToCapitalize) { 154 if (stringToCapitalize == null) { 155 return null; 156 } 157 158 StringBuilder capitalizedString = new StringBuilder(); 159 if (stringToCapitalize.length() > 0) { 160 capitalizedString.append(stringToCapitalize.substring(0, 1).toUpperCase()); 161 if (stringToCapitalize.length() > 1) { 162 capitalizedString.append(stringToCapitalize.substring(1).toLowerCase()); 163 } 164 } 165 return capitalizedString.toString(); 166 } 167 168 /** Determines if a gesture navigation bar is being used. */ isGestureNavigateEnabled(Context context)169 public static boolean isGestureNavigateEnabled(Context context) { 170 return context.getResources().getInteger( 171 com.android.internal.R.integer.config_navBarInteractionMode) 172 == NAV_BAR_MODE_GESTURAL; 173 } 174 175 /** Determines if a accessibility floating menu is being used. */ isFloatingMenuEnabled(Context context)176 public static boolean isFloatingMenuEnabled(Context context) { 177 return Settings.Secure.getInt(context.getContentResolver(), 178 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, /* def= */ -1) 179 == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; 180 } 181 182 /** Determines if a touch explore is being used. */ isTouchExploreEnabled(Context context)183 public static boolean isTouchExploreEnabled(Context context) { 184 final AccessibilityManager am = context.getSystemService(AccessibilityManager.class); 185 return am.isTouchExplorationEnabled(); 186 } 187 188 /** 189 * Gets the corresponding fragment type of a given accessibility service. 190 * 191 * @param accessibilityServiceInfo The accessibilityService's info 192 * @return int from {@link AccessibilityServiceFragmentType} 193 */ getAccessibilityServiceFragmentType( AccessibilityServiceInfo accessibilityServiceInfo)194 static @AccessibilityServiceFragmentType int getAccessibilityServiceFragmentType( 195 AccessibilityServiceInfo accessibilityServiceInfo) { 196 final int targetSdk = accessibilityServiceInfo.getResolveInfo() 197 .serviceInfo.applicationInfo.targetSdkVersion; 198 final boolean requestA11yButton = (accessibilityServiceInfo.flags 199 & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0; 200 201 if (targetSdk <= Build.VERSION_CODES.Q) { 202 return AccessibilityServiceFragmentType.VOLUME_SHORTCUT_TOGGLE; 203 } 204 return requestA11yButton 205 ? AccessibilityServiceFragmentType.INVISIBLE_TOGGLE 206 : AccessibilityServiceFragmentType.TOGGLE; 207 } 208 209 /** 210 * Opts in component name into multiple {@code shortcutTypes} colon-separated string in 211 * Settings. 212 * 213 * @param context The current context. 214 * @param shortcutTypes A combination of {@link UserShortcutType}. 215 * @param componentName The component name that need to be opted in Settings. 216 */ optInAllValuesToSettings(Context context, int shortcutTypes, @NonNull ComponentName componentName)217 static void optInAllValuesToSettings(Context context, int shortcutTypes, 218 @NonNull ComponentName componentName) { 219 if (android.view.accessibility.Flags.a11yQsShortcut()) { 220 AccessibilityManager a11yManager = context.getSystemService(AccessibilityManager.class); 221 if (a11yManager != null) { 222 a11yManager.enableShortcutsForTargets( 223 /* enable= */ true, 224 shortcutTypes, 225 Set.of(componentName.flattenToString()), 226 UserHandle.myUserId() 227 ); 228 } 229 230 return; 231 } 232 233 if ((shortcutTypes & UserShortcutType.SOFTWARE) == UserShortcutType.SOFTWARE) { 234 optInValueToSettings(context, UserShortcutType.SOFTWARE, componentName); 235 } 236 if (((shortcutTypes & UserShortcutType.HARDWARE) == UserShortcutType.HARDWARE)) { 237 optInValueToSettings(context, UserShortcutType.HARDWARE, componentName); 238 } 239 } 240 241 /** 242 * Opts in component name into {@code shortcutType} colon-separated string in Settings. 243 * 244 * @param context The current context. 245 * @param shortcutType The preferred shortcut type user selected. 246 * @param componentName The component name that need to be opted in Settings. 247 */ 248 @VisibleForTesting optInValueToSettings(Context context, @UserShortcutType int shortcutType, @NonNull ComponentName componentName)249 static void optInValueToSettings(Context context, @UserShortcutType int shortcutType, 250 @NonNull ComponentName componentName) { 251 if (android.view.accessibility.Flags.a11yQsShortcut()) { 252 AccessibilityManager a11yManager = context.getSystemService(AccessibilityManager.class); 253 if (a11yManager != null) { 254 a11yManager.enableShortcutsForTargets( 255 /* enable= */ true, 256 shortcutType, 257 Set.of(componentName.flattenToString()), 258 UserHandle.myUserId() 259 ); 260 } 261 return; 262 } 263 264 final String targetKey = convertKeyFromSettings(shortcutType); 265 final String targetString = Settings.Secure.getString(context.getContentResolver(), 266 targetKey); 267 268 if (hasValueInSettings(context, shortcutType, componentName)) { 269 return; 270 } 271 272 final StringJoiner joiner = new StringJoiner(String.valueOf(COMPONENT_NAME_SEPARATOR)); 273 if (!TextUtils.isEmpty(targetString)) { 274 joiner.add(targetString); 275 } 276 joiner.add(componentName.flattenToString()); 277 278 Settings.Secure.putString(context.getContentResolver(), targetKey, joiner.toString()); 279 } 280 281 /** 282 * Opts out component name into multiple {@code shortcutTypes} colon-separated string in 283 * Settings. 284 * 285 * @param context The current context. 286 * @param shortcutTypes A combination of {@link UserShortcutType}. 287 * @param componentName The component name that need to be opted out from Settings. 288 */ optOutAllValuesFromSettings(Context context, int shortcutTypes, @NonNull ComponentName componentName)289 static void optOutAllValuesFromSettings(Context context, int shortcutTypes, 290 @NonNull ComponentName componentName) { 291 if (android.view.accessibility.Flags.a11yQsShortcut()) { 292 AccessibilityManager a11yManager = context.getSystemService(AccessibilityManager.class); 293 if (a11yManager != null) { 294 a11yManager.enableShortcutsForTargets( 295 /* enable= */ false, 296 shortcutTypes, 297 Set.of(componentName.flattenToString()), 298 UserHandle.myUserId() 299 ); 300 } 301 return; 302 } 303 304 if ((shortcutTypes & UserShortcutType.SOFTWARE) == UserShortcutType.SOFTWARE) { 305 optOutValueFromSettings(context, UserShortcutType.SOFTWARE, componentName); 306 } 307 if (((shortcutTypes & UserShortcutType.HARDWARE) == UserShortcutType.HARDWARE)) { 308 optOutValueFromSettings(context, UserShortcutType.HARDWARE, componentName); 309 } 310 } 311 312 /** 313 * Opts out component name into {@code shortcutType} colon-separated string in Settings. 314 * 315 * @param context The current context. 316 * @param shortcutType The preferred shortcut type user selected. 317 * @param componentName The component name that need to be opted out from Settings. 318 */ 319 @VisibleForTesting optOutValueFromSettings(Context context, @UserShortcutType int shortcutType, @NonNull ComponentName componentName)320 static void optOutValueFromSettings(Context context, @UserShortcutType int shortcutType, 321 @NonNull ComponentName componentName) { 322 if (android.view.accessibility.Flags.a11yQsShortcut()) { 323 AccessibilityManager a11yManager = context.getSystemService(AccessibilityManager.class); 324 if (a11yManager != null) { 325 a11yManager.enableShortcutsForTargets( 326 /* enable= */ false, 327 shortcutType, 328 Set.of(componentName.flattenToString()), 329 UserHandle.myUserId() 330 ); 331 } 332 return; 333 } 334 335 final StringJoiner joiner = new StringJoiner(String.valueOf(COMPONENT_NAME_SEPARATOR)); 336 final String targetKey = convertKeyFromSettings(shortcutType); 337 final String targetString = Settings.Secure.getString(context.getContentResolver(), 338 targetKey); 339 340 if (TextUtils.isEmpty(targetString)) { 341 return; 342 } 343 344 sStringColonSplitter.setString(targetString); 345 while (sStringColonSplitter.hasNext()) { 346 final String name = sStringColonSplitter.next(); 347 if (TextUtils.isEmpty(name) || (componentName.flattenToString()).equals(name)) { 348 continue; 349 } 350 joiner.add(name); 351 } 352 353 Settings.Secure.putString(context.getContentResolver(), targetKey, joiner.toString()); 354 } 355 356 /** 357 * Returns if component name existed in one of {@code shortcutTypes} string in Settings. 358 * 359 * @param context The current context. 360 * @param shortcutTypes A combination of {@link UserShortcutType}. 361 * @param componentName The component name that need to be checked existed in Settings. 362 * @return {@code true} if componentName existed in Settings. 363 */ hasValuesInSettings(Context context, int shortcutTypes, @NonNull ComponentName componentName)364 static boolean hasValuesInSettings(Context context, int shortcutTypes, 365 @NonNull ComponentName componentName) { 366 boolean exist = false; 367 if ((shortcutTypes & UserShortcutType.SOFTWARE) == UserShortcutType.SOFTWARE) { 368 exist = hasValueInSettings(context, UserShortcutType.SOFTWARE, componentName); 369 } 370 if (((shortcutTypes & UserShortcutType.HARDWARE) == UserShortcutType.HARDWARE)) { 371 exist |= hasValueInSettings(context, UserShortcutType.HARDWARE, componentName); 372 } 373 if (android.view.accessibility.Flags.a11yQsShortcut()) { 374 if ((shortcutTypes & UserShortcutType.QUICK_SETTINGS) 375 == UserShortcutType.QUICK_SETTINGS) { 376 exist |= hasValueInSettings(context, UserShortcutType.QUICK_SETTINGS, 377 componentName); 378 } 379 } 380 381 return exist; 382 } 383 384 /** 385 * Returns if component name existed in {@code shortcutType} string Settings. 386 * 387 * @param context The current context. 388 * @param shortcutType The preferred shortcut type user selected. 389 * @param componentName The component name that need to be checked existed in Settings. 390 * @return {@code true} if componentName existed in Settings. 391 */ 392 @VisibleForTesting hasValueInSettings(Context context, @UserShortcutType int shortcutType, @NonNull ComponentName componentName)393 static boolean hasValueInSettings(Context context, @UserShortcutType int shortcutType, 394 @NonNull ComponentName componentName) { 395 if (android.view.accessibility.Flags.a11yQsShortcut()) { 396 return ShortcutUtils.getShortcutTargetsFromSettings( 397 context, shortcutType, UserHandle.myUserId() 398 ).contains(componentName.flattenToString()); 399 } 400 401 final String targetKey = convertKeyFromSettings(shortcutType); 402 final String targetString = Settings.Secure.getString(context.getContentResolver(), 403 targetKey); 404 405 if (TextUtils.isEmpty(targetString)) { 406 return false; 407 } 408 409 sStringColonSplitter.setString(targetString); 410 411 while (sStringColonSplitter.hasNext()) { 412 final String name = sStringColonSplitter.next(); 413 if ((componentName.flattenToString()).equals(name)) { 414 return true; 415 } 416 } 417 return false; 418 } 419 420 /** 421 * Gets the corresponding user shortcut type of a given accessibility service. 422 * 423 * @param context The current context. 424 * @param componentName The component name that need to be checked existed in Settings. 425 * @return The user shortcut type if component name existed in {@code UserShortcutType} string 426 * Settings. 427 */ getUserShortcutTypesFromSettings(Context context, @NonNull ComponentName componentName)428 static int getUserShortcutTypesFromSettings(Context context, 429 @NonNull ComponentName componentName) { 430 int shortcutTypes = UserShortcutType.EMPTY; 431 if (hasValuesInSettings(context, UserShortcutType.SOFTWARE, componentName)) { 432 shortcutTypes |= UserShortcutType.SOFTWARE; 433 } 434 if (hasValuesInSettings(context, UserShortcutType.HARDWARE, componentName)) { 435 shortcutTypes |= UserShortcutType.HARDWARE; 436 } 437 if (android.view.accessibility.Flags.a11yQsShortcut()) { 438 if (hasValuesInSettings(context, UserShortcutType.QUICK_SETTINGS, componentName)) { 439 shortcutTypes |= UserShortcutType.QUICK_SETTINGS; 440 } 441 } 442 443 return shortcutTypes; 444 } 445 446 /** 447 * Converts {@link UserShortcutType} to key in Settings. 448 * 449 * @param shortcutType The shortcut type. 450 * @return Mapping key in Settings. 451 */ convertKeyFromSettings(@serShortcutType int shortcutType)452 static String convertKeyFromSettings(@UserShortcutType int shortcutType) { 453 if (android.view.accessibility.Flags.a11yQsShortcut()) { 454 return ShortcutUtils.convertToKey(shortcutType); 455 } 456 457 switch (shortcutType) { 458 case UserShortcutType.SOFTWARE: 459 return Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS; 460 case UserShortcutType.HARDWARE: 461 return Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE; 462 case UserShortcutType.TRIPLETAP: 463 return Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED; 464 default: 465 throw new IllegalArgumentException( 466 "Unsupported userShortcutType " + shortcutType); 467 } 468 } 469 470 /** 471 * Gets the width of the screen. 472 * 473 * @param context the current context. 474 * @return the width of the screen in terms of pixels. 475 */ getScreenWidthPixels(Context context)476 public static int getScreenWidthPixels(Context context) { 477 final Resources resources = context.getResources(); 478 final int screenWidthDp = resources.getConfiguration().screenWidthDp; 479 480 return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, screenWidthDp, 481 resources.getDisplayMetrics())); 482 } 483 484 /** 485 * Gets the height of the screen. 486 * 487 * @param context the current context. 488 * @return the height of the screen in terms of pixels. 489 */ getScreenHeightPixels(Context context)490 public static int getScreenHeightPixels(Context context) { 491 final Resources resources = context.getResources(); 492 final int screenHeightDp = resources.getConfiguration().screenHeightDp; 493 494 return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, screenHeightDp, 495 resources.getDisplayMetrics())); 496 } 497 498 /** 499 * Gets the bounds of the display window excluding the insets of the system bar and display 500 * cut out. 501 * 502 * @param context the current context. 503 * @return the bounds of the display window. 504 */ getDisplayBounds(Context context)505 public static Rect getDisplayBounds(Context context) { 506 final WindowManager windowManager = context.getSystemService(WindowManager.class); 507 final WindowMetrics metrics = windowManager.getCurrentWindowMetrics(); 508 509 final Rect displayBounds = metrics.getBounds(); 510 final Insets displayInsets = metrics.getWindowInsets().getInsetsIgnoringVisibility( 511 systemBars() | displayCutout()); 512 displayBounds.inset(displayInsets); 513 514 return displayBounds; 515 } 516 517 /** 518 * Indicates if the accessibility service belongs to a system App. 519 * @param info AccessibilityServiceInfo 520 * @return {@code true} if the App is a system App. 521 */ isSystemApp(@onNull AccessibilityServiceInfo info)522 public static boolean isSystemApp(@NonNull AccessibilityServiceInfo info) { 523 return info.getResolveInfo().serviceInfo.applicationInfo.isSystemApp(); 524 } 525 526 /** 527 * Bypasses the timeout restriction if volume key shortcut assigned. 528 * 529 * @param context the current context. 530 */ skipVolumeShortcutDialogTimeoutRestriction(Context context)531 public static void skipVolumeShortcutDialogTimeoutRestriction(Context context) { 532 Settings.Secure.putInt(context.getContentResolver(), 533 Settings.Secure.SKIP_ACCESSIBILITY_SHORTCUT_DIALOG_TIMEOUT_RESTRICTION, /* 534 true */ 1); 535 } 536 } 537