1 /* 2 * Copyright (C) 2020 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.internal.accessibility.util; 18 19 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE; 20 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; 21 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE; 22 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR; 23 24 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; 25 import static com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType.INVISIBLE_TOGGLE; 26 import static com.android.internal.accessibility.common.ShortcutConstants.SERVICES_SEPARATOR; 27 import static com.android.internal.accessibility.common.ShortcutConstants.USER_SHORTCUT_TYPES; 28 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.DEFAULT; 29 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; 30 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; 31 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.KEY_GESTURE; 32 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; 33 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; 34 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP; 35 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TWOFINGER_DOUBLETAP; 36 37 import android.accessibilityservice.AccessibilityServiceInfo; 38 import android.annotation.NonNull; 39 import android.annotation.SuppressLint; 40 import android.annotation.UserIdInt; 41 import android.content.ComponentName; 42 import android.content.Context; 43 import android.provider.Settings; 44 import android.text.TextUtils; 45 import android.util.ArraySet; 46 import android.util.Slog; 47 import android.view.accessibility.AccessibilityManager; 48 49 import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType; 50 51 import java.util.Collections; 52 import java.util.List; 53 import java.util.Set; 54 import java.util.StringJoiner; 55 56 /** 57 * Collection of utilities for accessibility shortcut. 58 */ 59 public final class ShortcutUtils { ShortcutUtils()60 private ShortcutUtils() {} 61 62 private static final TextUtils.SimpleStringSplitter sStringColonSplitter = 63 new TextUtils.SimpleStringSplitter(SERVICES_SEPARATOR); 64 private static final String TAG = "AccessibilityShortcutUtils"; 65 66 /** 67 * Opts in component id into colon-separated {@link UserShortcutType} 68 * key's string from Settings. 69 * 70 * @param context The current context. 71 * @param shortcutType The preferred shortcut type user selected. 72 * @param componentId The component id that need to be opted in Settings. 73 * @deprecated Use 74 * {@link AccessibilityManager#enableShortcutsForTargets(boolean, int, Set, int)} 75 */ 76 @Deprecated optInValueToSettings(Context context, @UserShortcutType int shortcutType, @NonNull String componentId)77 public static void optInValueToSettings(Context context, @UserShortcutType int shortcutType, 78 @NonNull String componentId) { 79 final StringJoiner joiner = new StringJoiner(String.valueOf(SERVICES_SEPARATOR)); 80 final String targetKey = convertToKey(shortcutType); 81 final String targetString = Settings.Secure.getString(context.getContentResolver(), 82 targetKey); 83 84 if (isComponentIdExistingInSettings(context, shortcutType, componentId)) { 85 return; 86 } 87 88 if (!TextUtils.isEmpty(targetString)) { 89 joiner.add(targetString); 90 } 91 joiner.add(componentId); 92 93 Settings.Secure.putString(context.getContentResolver(), targetKey, joiner.toString()); 94 } 95 96 /** 97 * Opts out of component id into colon-separated {@link UserShortcutType} key's string from 98 * Settings. 99 * 100 * @param context The current context. 101 * @param shortcutType The preferred shortcut type user selected. 102 * @param componentId The component id that need to be opted out of Settings. 103 * 104 * @deprecated Use 105 * {@link AccessibilityManager#enableShortcutForTargets(boolean, int, Set, int)} 106 */ 107 @Deprecated optOutValueFromSettings( Context context, @UserShortcutType int shortcutType, @NonNull String componentId)108 public static void optOutValueFromSettings( 109 Context context, @UserShortcutType int shortcutType, @NonNull String componentId) { 110 final StringJoiner joiner = new StringJoiner(String.valueOf(SERVICES_SEPARATOR)); 111 final String targetsKey = convertToKey(shortcutType); 112 final String targetsValue = Settings.Secure.getString(context.getContentResolver(), 113 targetsKey); 114 115 if (TextUtils.isEmpty(targetsValue)) { 116 return; 117 } 118 119 sStringColonSplitter.setString(targetsValue); 120 while (sStringColonSplitter.hasNext()) { 121 final String id = sStringColonSplitter.next(); 122 if (TextUtils.isEmpty(id) || componentId.equals(id)) { 123 continue; 124 } 125 joiner.add(id); 126 } 127 128 Settings.Secure.putString(context.getContentResolver(), targetsKey, joiner.toString()); 129 } 130 131 /** 132 * Returns if component id existed in Settings. 133 * 134 * @param context The current context. 135 * @param shortcutType The preferred shortcut type user selected. 136 * @param componentId The component id that need to be checked existed in Settings. 137 * @return {@code true} if component id existed in Settings. 138 */ isComponentIdExistingInSettings(Context context, @UserShortcutType int shortcutType, @NonNull String componentId)139 public static boolean isComponentIdExistingInSettings(Context context, 140 @UserShortcutType int shortcutType, @NonNull String componentId) { 141 final String targetKey = convertToKey(shortcutType); 142 final String targetString = Settings.Secure.getString(context.getContentResolver(), 143 targetKey); 144 145 if (TextUtils.isEmpty(targetString)) { 146 return false; 147 } 148 149 sStringColonSplitter.setString(targetString); 150 while (sStringColonSplitter.hasNext()) { 151 final String id = sStringColonSplitter.next(); 152 if (componentId.equals(id)) { 153 return true; 154 } 155 } 156 157 return false; 158 } 159 160 /** 161 * Returns if a {@code shortcutType} shortcut contains {@code componentName}. 162 * 163 * @param context The current context. 164 * @param shortcutType The preferred shortcut type user selected. 165 * @param componentName The component that need to be checked. 166 * @return {@code true} if the shortcut contains {@code componentName}. 167 */ 168 @SuppressLint("MissingPermission") isShortcutContained( Context context, @UserShortcutType int shortcutType, @NonNull String componentName)169 public static boolean isShortcutContained( 170 Context context, @UserShortcutType int shortcutType, @NonNull String componentName) { 171 AccessibilityManager manager = context.getSystemService(AccessibilityManager.class); 172 if (manager != null) { 173 return manager 174 .getAccessibilityShortcutTargets(shortcutType).contains(componentName); 175 } else { 176 return false; 177 } 178 } 179 180 /** 181 * Returns every shortcut type that currently has the provided componentName as a target. 182 * Types are returned as a singular flag integer. 183 * If none have the componentName, returns {@link UserShortcutType#DEFAULT} 184 */ getEnabledShortcutTypes( Context context, String componentName)185 public static int getEnabledShortcutTypes( 186 Context context, String componentName) { 187 final AccessibilityManager am = context.getSystemService(AccessibilityManager.class); 188 if (am == null) return DEFAULT; 189 190 int shortcutTypes = DEFAULT; 191 for (int shortcutType : USER_SHORTCUT_TYPES) { 192 if (am.getAccessibilityShortcutTargets(shortcutType).contains(componentName)) { 193 shortcutTypes |= shortcutType; 194 } 195 } 196 return shortcutTypes; 197 } 198 199 /** 200 * Converts {@link UserShortcutType} to {@link Settings.Secure} key. 201 * 202 * @param type The shortcut type. 203 * @return Mapping key in Settings. 204 */ 205 @SuppressLint("SwitchIntDef") convertToKey(@serShortcutType int type)206 public static String convertToKey(@UserShortcutType int type) { 207 return switch (type) { 208 case SOFTWARE -> Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS; 209 case GESTURE -> Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS; 210 case HARDWARE -> Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE; 211 case TRIPLETAP -> Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED; 212 case TWOFINGER_DOUBLETAP -> 213 Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED; 214 case QUICK_SETTINGS -> Settings.Secure.ACCESSIBILITY_QS_TARGETS; 215 case KEY_GESTURE -> Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS; 216 default -> throw new IllegalArgumentException( 217 "Unsupported user shortcut type: " + type); 218 }; 219 } 220 221 /** 222 * Converts {@link Settings.Secure} key to {@link UserShortcutType}. 223 * 224 * @param key The shortcut key in Settings. 225 * @return The mapped type 226 */ 227 @UserShortcutType convertToType(String key)228 public static int convertToType(String key) { 229 return switch (key) { 230 case Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS -> SOFTWARE; 231 case Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS -> GESTURE; 232 case Settings.Secure.ACCESSIBILITY_QS_TARGETS -> QUICK_SETTINGS; 233 case Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE -> HARDWARE; 234 case Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED -> 235 TRIPLETAP; 236 case Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED -> 237 TWOFINGER_DOUBLETAP; 238 case Settings.Secure.ACCESSIBILITY_KEY_GESTURE_TARGETS -> KEY_GESTURE; 239 default -> throw new IllegalArgumentException( 240 "Unsupported user shortcut key: " + key); 241 }; 242 } 243 244 /** 245 * Updates an accessibility state if the accessibility service is a Always-On a11y service, 246 * a.k.a. AccessibilityServices that has FLAG_REQUEST_ACCESSIBILITY_BUTTON 247 * <p> 248 * Turn on the accessibility service when there is any shortcut associated to it. 249 * <p> 250 * Turn off the accessibility service when there is no shortcut associated to it. 251 * 252 * @param componentNames the a11y shortcut target's component names 253 */ 254 public static void updateInvisibleToggleAccessibilityServiceEnableState( 255 Context context, Set<String> componentNames, int userId) { 256 final AccessibilityManager am = context.getSystemService(AccessibilityManager.class); 257 if (am == null) return; 258 259 final List<AccessibilityServiceInfo> installedServices = 260 am.getInstalledAccessibilityServiceList(); 261 262 final Set<String> invisibleToggleServices = new ArraySet<>(); 263 for (AccessibilityServiceInfo serviceInfo : installedServices) { 264 if (AccessibilityUtils.getAccessibilityServiceFragmentType(serviceInfo) 265 == INVISIBLE_TOGGLE) { 266 invisibleToggleServices.add(serviceInfo.getComponentName().flattenToString()); 267 } 268 } 269 270 final Set<String> servicesWithShortcuts = new ArraySet<>(); 271 for (int shortcutType: USER_SHORTCUT_TYPES) { 272 // The call to update always-on service might modify the shortcut setting right before 273 // calling #updateAccessibilityServiceStateIfNeeded in the same call. 274 // To avoid getting the shortcut target from out-dated value, use values from Settings 275 // instead. 276 servicesWithShortcuts.addAll( 277 getShortcutTargetsFromSettings(context, shortcutType, userId)); 278 } 279 280 for (String componentName : componentNames) { 281 // Only needs to update the Always-On A11yService's state when the shortcut changes. 282 if (invisibleToggleServices.contains(componentName)) { 283 284 boolean enableA11yService = servicesWithShortcuts.contains(componentName); 285 AccessibilityUtils.setAccessibilityServiceState( 286 context, 287 ComponentName.unflattenFromString(componentName), 288 enableA11yService, 289 userId); 290 } 291 } 292 } 293 294 /** 295 * Returns the target component names of a given user shortcut type from Settings. 296 * 297 * <p> 298 * Note: grab shortcut targets from Settings is only needed 299 * if you depends on a value being set in the same call. 300 * For example, you disable a single shortcut, 301 * and you're checking if there is any shortcut remaining. 302 * 303 * <p> 304 * If you just want to know the current state, you can use 305 * {@link AccessibilityManager#getAccessibilityShortcutTargets(int)} 306 */ 307 @NonNull 308 public static Set<String> getShortcutTargetsFromSettings( 309 Context context, @UserShortcutType int shortcutType, int userId) { 310 final String targetKey = convertToKey(shortcutType); 311 if (Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED.equals(targetKey) 312 || Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED 313 .equals(targetKey)) { 314 boolean magnificationEnabled = Settings.Secure.getIntForUser( 315 context.getContentResolver(), targetKey, /* def= */ 0, userId) == 1; 316 return magnificationEnabled ? Set.of(MAGNIFICATION_CONTROLLER_NAME) 317 : Collections.emptySet(); 318 319 } else { 320 final String targetString = Settings.Secure.getStringForUser( 321 context.getContentResolver(), targetKey, userId); 322 323 if (TextUtils.isEmpty(targetString)) { 324 return Collections.emptySet(); 325 } 326 327 Set<String> targets = new ArraySet<>(); 328 sStringColonSplitter.setString(targetString); 329 while (sStringColonSplitter.hasNext()) { 330 targets.add(sStringColonSplitter.next()); 331 } 332 return Collections.unmodifiableSet(targets); 333 } 334 } 335 336 /** 337 * Retrieves the button mode of the provided context. 338 * Returns -1 if the button mode is undefined. 339 * Valid button modes: 340 * {@link Settings.Secure#ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR}, 341 * {@link Settings.Secure#ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU}, 342 * {@link Settings.Secure#ACCESSIBILITY_BUTTON_MODE_GESTURE} 343 */ 344 public static int getButtonMode(Context context, @UserIdInt int userId) { 345 return Settings.Secure.getIntForUser(context.getContentResolver(), 346 ACCESSIBILITY_BUTTON_MODE, /* default value = */ -1, userId); 347 } 348 349 /** 350 * Sets the button mode of the provided context. 351 * Must be a valid button mode, or it will return false. 352 * Returns true if the setting was changed, false otherwise. 353 * Valid button modes: 354 * {@link Settings.Secure#ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR}, 355 * {@link Settings.Secure#ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU}, 356 * {@link Settings.Secure#ACCESSIBILITY_BUTTON_MODE_GESTURE} 357 */ 358 public static boolean setButtonMode(Context context, int mode, @UserIdInt int userId) { 359 // Input validation 360 if (getButtonMode(context, userId) == mode) { 361 return false; 362 } 363 if ((mode 364 & (ACCESSIBILITY_BUTTON_MODE_NAVIGATION_BAR 365 | ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU 366 | ACCESSIBILITY_BUTTON_MODE_GESTURE)) != mode) { 367 Slog.w(TAG, "Tried to set button mode to unexpected value " + mode); 368 return false; 369 } 370 return Settings.Secure.putIntForUser( 371 context.getContentResolver(), ACCESSIBILITY_BUTTON_MODE, mode, userId); 372 } 373 } 374