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