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