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