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 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 /** Denotes the accessibility enabled status */ 109 @Retention(RetentionPolicy.SOURCE) 110 public @interface State { 111 int OFF = 0; 112 int ON = 1; 113 } 114 115 /** 116 * Return On/Off string according to the setting which specifies the integer value 1 or 0. This 117 * setting is defined in the secure system settings {@link android.provider.Settings.Secure}. 118 */ getSummary(Context context, String settingsSecureKey)119 static CharSequence getSummary(Context context, String settingsSecureKey) { 120 final boolean enabled = Settings.Secure.getInt(context.getContentResolver(), 121 settingsSecureKey, State.OFF) == State.ON; 122 final int resId = enabled ? R.string.accessibility_feature_state_on 123 : R.string.accessibility_feature_state_off; 124 return context.getResources().getText(resId); 125 } 126 127 /** 128 * Capitalizes a string by capitalizing the first character and making the remaining characters 129 * lower case. 130 */ capitalize(String stringToCapitalize)131 public static String capitalize(String stringToCapitalize) { 132 if (stringToCapitalize == null) { 133 return null; 134 } 135 136 StringBuilder capitalizedString = new StringBuilder(); 137 if (stringToCapitalize.length() > 0) { 138 capitalizedString.append(stringToCapitalize.substring(0, 1).toUpperCase()); 139 if (stringToCapitalize.length() > 1) { 140 capitalizedString.append(stringToCapitalize.substring(1).toLowerCase()); 141 } 142 } 143 return capitalizedString.toString(); 144 } 145 146 /** Determines if a gesture navigation bar is being used. */ isGestureNavigateEnabled(Context context)147 public static boolean isGestureNavigateEnabled(Context context) { 148 return context.getResources().getInteger( 149 com.android.internal.R.integer.config_navBarInteractionMode) 150 == NAV_BAR_MODE_GESTURAL; 151 } 152 153 /** Determines if a accessibility floating menu is being used. */ isFloatingMenuEnabled(Context context)154 public static boolean isFloatingMenuEnabled(Context context) { 155 return Settings.Secure.getInt(context.getContentResolver(), 156 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, /* def= */ -1) 157 == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; 158 } 159 160 /** Determines if a touch explore is being used. */ isTouchExploreEnabled(Context context)161 public static boolean isTouchExploreEnabled(Context context) { 162 final AccessibilityManager am = context.getSystemService(AccessibilityManager.class); 163 return am.isTouchExplorationEnabled(); 164 } 165 166 /** 167 * Gets the corresponding fragment type of a given accessibility service. 168 * 169 * @param accessibilityServiceInfo The accessibilityService's info 170 * @return int from {@link AccessibilityServiceFragmentType} 171 */ getAccessibilityServiceFragmentType( AccessibilityServiceInfo accessibilityServiceInfo)172 static @AccessibilityServiceFragmentType int getAccessibilityServiceFragmentType( 173 AccessibilityServiceInfo accessibilityServiceInfo) { 174 final int targetSdk = accessibilityServiceInfo.getResolveInfo() 175 .serviceInfo.applicationInfo.targetSdkVersion; 176 final boolean requestA11yButton = (accessibilityServiceInfo.flags 177 & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0; 178 179 if (targetSdk <= Build.VERSION_CODES.Q) { 180 return AccessibilityServiceFragmentType.VOLUME_SHORTCUT_TOGGLE; 181 } 182 return requestA11yButton 183 ? AccessibilityServiceFragmentType.INVISIBLE_TOGGLE 184 : AccessibilityServiceFragmentType.TOGGLE; 185 } 186 187 /** 188 * Opts in component name into multiple {@code shortcutTypes} colon-separated string in 189 * Settings. 190 * 191 * @param context The current context. 192 * @param shortcutTypes A combination of {@link UserShortcutType}. 193 * @param componentName The component name that need to be opted in Settings. 194 */ optInAllValuesToSettings(Context context, int shortcutTypes, @NonNull ComponentName componentName)195 static void optInAllValuesToSettings(Context context, int shortcutTypes, 196 @NonNull ComponentName componentName) { 197 if ((shortcutTypes & UserShortcutType.SOFTWARE) == UserShortcutType.SOFTWARE) { 198 optInValueToSettings(context, UserShortcutType.SOFTWARE, componentName); 199 } 200 if (((shortcutTypes & UserShortcutType.HARDWARE) == UserShortcutType.HARDWARE)) { 201 optInValueToSettings(context, UserShortcutType.HARDWARE, componentName); 202 } 203 } 204 205 /** 206 * Opts in component name into {@code shortcutType} colon-separated string in Settings. 207 * 208 * @param context The current context. 209 * @param shortcutType The preferred shortcut type user selected. 210 * @param componentName The component name that need to be opted in Settings. 211 */ 212 @VisibleForTesting optInValueToSettings(Context context, @UserShortcutType int shortcutType, @NonNull ComponentName componentName)213 static void optInValueToSettings(Context context, @UserShortcutType int shortcutType, 214 @NonNull ComponentName componentName) { 215 final String targetKey = convertKeyFromSettings(shortcutType); 216 final String targetString = Settings.Secure.getString(context.getContentResolver(), 217 targetKey); 218 219 if (hasValueInSettings(context, shortcutType, componentName)) { 220 return; 221 } 222 223 final StringJoiner joiner = new StringJoiner(String.valueOf(COMPONENT_NAME_SEPARATOR)); 224 if (!TextUtils.isEmpty(targetString)) { 225 joiner.add(targetString); 226 } 227 joiner.add(componentName.flattenToString()); 228 229 Settings.Secure.putString(context.getContentResolver(), targetKey, joiner.toString()); 230 } 231 232 /** 233 * Opts out component name into multiple {@code shortcutTypes} colon-separated string in 234 * Settings. 235 * 236 * @param context The current context. 237 * @param shortcutTypes A combination of {@link UserShortcutType}. 238 * @param componentName The component name that need to be opted out from Settings. 239 */ optOutAllValuesFromSettings(Context context, int shortcutTypes, @NonNull ComponentName componentName)240 static void optOutAllValuesFromSettings(Context context, int shortcutTypes, 241 @NonNull ComponentName componentName) { 242 if ((shortcutTypes & UserShortcutType.SOFTWARE) == UserShortcutType.SOFTWARE) { 243 optOutValueFromSettings(context, UserShortcutType.SOFTWARE, componentName); 244 } 245 if (((shortcutTypes & UserShortcutType.HARDWARE) == UserShortcutType.HARDWARE)) { 246 optOutValueFromSettings(context, UserShortcutType.HARDWARE, componentName); 247 } 248 } 249 250 /** 251 * Opts out component name into {@code shortcutType} colon-separated string in Settings. 252 * 253 * @param context The current context. 254 * @param shortcutType The preferred shortcut type user selected. 255 * @param componentName The component name that need to be opted out from Settings. 256 */ 257 @VisibleForTesting optOutValueFromSettings(Context context, @UserShortcutType int shortcutType, @NonNull ComponentName componentName)258 static void optOutValueFromSettings(Context context, @UserShortcutType int shortcutType, 259 @NonNull ComponentName componentName) { 260 final StringJoiner joiner = new StringJoiner(String.valueOf(COMPONENT_NAME_SEPARATOR)); 261 final String targetKey = convertKeyFromSettings(shortcutType); 262 final String targetString = Settings.Secure.getString(context.getContentResolver(), 263 targetKey); 264 265 if (TextUtils.isEmpty(targetString)) { 266 return; 267 } 268 269 sStringColonSplitter.setString(targetString); 270 while (sStringColonSplitter.hasNext()) { 271 final String name = sStringColonSplitter.next(); 272 if (TextUtils.isEmpty(name) || (componentName.flattenToString()).equals(name)) { 273 continue; 274 } 275 joiner.add(name); 276 } 277 278 Settings.Secure.putString(context.getContentResolver(), targetKey, joiner.toString()); 279 } 280 281 /** 282 * Returns if component name existed in one of {@code shortcutTypes} string in Settings. 283 * 284 * @param context The current context. 285 * @param shortcutTypes A combination of {@link UserShortcutType}. 286 * @param componentName The component name that need to be checked existed in Settings. 287 * @return {@code true} if componentName existed in Settings. 288 */ hasValuesInSettings(Context context, int shortcutTypes, @NonNull ComponentName componentName)289 static boolean hasValuesInSettings(Context context, int shortcutTypes, 290 @NonNull ComponentName componentName) { 291 boolean exist = false; 292 if ((shortcutTypes & UserShortcutType.SOFTWARE) == UserShortcutType.SOFTWARE) { 293 exist = hasValueInSettings(context, UserShortcutType.SOFTWARE, componentName); 294 } 295 if (((shortcutTypes & UserShortcutType.HARDWARE) == UserShortcutType.HARDWARE)) { 296 exist |= hasValueInSettings(context, UserShortcutType.HARDWARE, componentName); 297 } 298 return exist; 299 } 300 301 /** 302 * Returns if component name existed in {@code shortcutType} string Settings. 303 * 304 * @param context The current context. 305 * @param shortcutType The preferred shortcut type user selected. 306 * @param componentName The component name that need to be checked existed in Settings. 307 * @return {@code true} if componentName existed in Settings. 308 */ 309 @VisibleForTesting hasValueInSettings(Context context, @UserShortcutType int shortcutType, @NonNull ComponentName componentName)310 static boolean hasValueInSettings(Context context, @UserShortcutType int shortcutType, 311 @NonNull ComponentName componentName) { 312 final String targetKey = convertKeyFromSettings(shortcutType); 313 final String targetString = Settings.Secure.getString(context.getContentResolver(), 314 targetKey); 315 316 if (TextUtils.isEmpty(targetString)) { 317 return false; 318 } 319 320 sStringColonSplitter.setString(targetString); 321 322 while (sStringColonSplitter.hasNext()) { 323 final String name = sStringColonSplitter.next(); 324 if ((componentName.flattenToString()).equals(name)) { 325 return true; 326 } 327 } 328 return false; 329 } 330 331 /** 332 * Gets the corresponding user shortcut type of a given accessibility service. 333 * 334 * @param context The current context. 335 * @param componentName The component name that need to be checked existed in Settings. 336 * @return The user shortcut type if component name existed in {@code UserShortcutType} string 337 * Settings. 338 */ getUserShortcutTypesFromSettings(Context context, @NonNull ComponentName componentName)339 static int getUserShortcutTypesFromSettings(Context context, 340 @NonNull ComponentName componentName) { 341 int shortcutTypes = UserShortcutType.EMPTY; 342 if (hasValuesInSettings(context, UserShortcutType.SOFTWARE, componentName)) { 343 shortcutTypes |= UserShortcutType.SOFTWARE; 344 } 345 if (hasValuesInSettings(context, UserShortcutType.HARDWARE, componentName)) { 346 shortcutTypes |= UserShortcutType.HARDWARE; 347 } 348 return shortcutTypes; 349 } 350 351 /** 352 * Converts {@link UserShortcutType} to key in Settings. 353 * 354 * @param shortcutType The shortcut type. 355 * @return Mapping key in Settings. 356 */ convertKeyFromSettings(@serShortcutType int shortcutType)357 static String convertKeyFromSettings(@UserShortcutType int shortcutType) { 358 switch (shortcutType) { 359 case UserShortcutType.SOFTWARE: 360 return Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS; 361 case UserShortcutType.HARDWARE: 362 return Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE; 363 case UserShortcutType.TRIPLETAP: 364 return Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED; 365 default: 366 throw new IllegalArgumentException( 367 "Unsupported userShortcutType " + shortcutType); 368 } 369 } 370 371 /** 372 * Gets the width of the screen. 373 * 374 * @param context the current context. 375 * @return the width of the screen in terms of pixels. 376 */ getScreenWidthPixels(Context context)377 public static int getScreenWidthPixels(Context context) { 378 final Resources resources = context.getResources(); 379 final int screenWidthDp = resources.getConfiguration().screenWidthDp; 380 381 return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, screenWidthDp, 382 resources.getDisplayMetrics())); 383 } 384 385 /** 386 * Gets the height of the screen. 387 * 388 * @param context the current context. 389 * @return the height of the screen in terms of pixels. 390 */ getScreenHeightPixels(Context context)391 public static int getScreenHeightPixels(Context context) { 392 final Resources resources = context.getResources(); 393 final int screenHeightDp = resources.getConfiguration().screenHeightDp; 394 395 return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, screenHeightDp, 396 resources.getDisplayMetrics())); 397 } 398 399 /** 400 * Gets the bounds of the display window excluding the insets of the system bar and display 401 * cut out. 402 * 403 * @param context the current context. 404 * @return the bounds of the display window. 405 */ getDisplayBounds(Context context)406 public static Rect getDisplayBounds(Context context) { 407 final WindowManager windowManager = context.getSystemService(WindowManager.class); 408 final WindowMetrics metrics = windowManager.getCurrentWindowMetrics(); 409 410 final Rect displayBounds = metrics.getBounds(); 411 final Insets displayInsets = metrics.getWindowInsets().getInsetsIgnoringVisibility( 412 systemBars() | displayCutout()); 413 displayBounds.inset(displayInsets); 414 415 return displayBounds; 416 } 417 418 /** 419 * Indicates if the accessibility service belongs to a system App. 420 * @param info AccessibilityServiceInfo 421 * @return {@code true} if the App is a system App. 422 */ isSystemApp(@onNull AccessibilityServiceInfo info)423 public static boolean isSystemApp(@NonNull AccessibilityServiceInfo info) { 424 return info.getResolveInfo().serviceInfo.applicationInfo.isSystemApp(); 425 } 426 } 427