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 com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; 20 import static com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType.INVISIBLE_TOGGLE; 21 import static com.android.internal.accessibility.common.ShortcutConstants.SERVICES_SEPARATOR; 22 import static com.android.internal.accessibility.common.ShortcutConstants.USER_SHORTCUT_TYPES; 23 24 import android.accessibilityservice.AccessibilityServiceInfo; 25 import android.annotation.NonNull; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.provider.Settings; 29 import android.text.TextUtils; 30 import android.util.ArraySet; 31 import android.view.accessibility.AccessibilityManager; 32 33 import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType; 34 35 import java.util.Collections; 36 import java.util.List; 37 import java.util.Set; 38 import java.util.StringJoiner; 39 40 /** 41 * Collection of utilities for accessibility shortcut. 42 */ 43 public final class ShortcutUtils { ShortcutUtils()44 private ShortcutUtils() {} 45 46 private static final TextUtils.SimpleStringSplitter sStringColonSplitter = 47 new TextUtils.SimpleStringSplitter(SERVICES_SEPARATOR); 48 49 /** 50 * Opts in component id into colon-separated {@link UserShortcutType} 51 * key's string from Settings. 52 * 53 * @param context The current context. 54 * @param shortcutType The preferred shortcut type user selected. 55 * @param componentId The component id that need to be opted in Settings. 56 * @deprecated Use 57 * {@link AccessibilityManager#enableShortcutsForTargets(boolean, int, Set, int)} 58 */ 59 @Deprecated optInValueToSettings(Context context, @UserShortcutType int shortcutType, @NonNull String componentId)60 public static void optInValueToSettings(Context context, @UserShortcutType int shortcutType, 61 @NonNull String componentId) { 62 final StringJoiner joiner = new StringJoiner(String.valueOf(SERVICES_SEPARATOR)); 63 final String targetKey = convertToKey(shortcutType); 64 final String targetString = Settings.Secure.getString(context.getContentResolver(), 65 targetKey); 66 67 if (isComponentIdExistingInSettings(context, shortcutType, componentId)) { 68 return; 69 } 70 71 if (!TextUtils.isEmpty(targetString)) { 72 joiner.add(targetString); 73 } 74 joiner.add(componentId); 75 76 Settings.Secure.putString(context.getContentResolver(), targetKey, joiner.toString()); 77 } 78 79 /** 80 * Opts out of component id into colon-separated {@link UserShortcutType} key's string from 81 * Settings. 82 * 83 * @param context The current context. 84 * @param shortcutType The preferred shortcut type user selected. 85 * @param componentId The component id that need to be opted out of Settings. 86 * 87 * @deprecated Use 88 * {@link AccessibilityManager#enableShortcutForTargets(boolean, int, Set, int)} 89 */ 90 @Deprecated optOutValueFromSettings( Context context, @UserShortcutType int shortcutType, @NonNull String componentId)91 public static void optOutValueFromSettings( 92 Context context, @UserShortcutType int shortcutType, @NonNull String componentId) { 93 final StringJoiner joiner = new StringJoiner(String.valueOf(SERVICES_SEPARATOR)); 94 final String targetsKey = convertToKey(shortcutType); 95 final String targetsValue = Settings.Secure.getString(context.getContentResolver(), 96 targetsKey); 97 98 if (TextUtils.isEmpty(targetsValue)) { 99 return; 100 } 101 102 sStringColonSplitter.setString(targetsValue); 103 while (sStringColonSplitter.hasNext()) { 104 final String id = sStringColonSplitter.next(); 105 if (TextUtils.isEmpty(id) || componentId.equals(id)) { 106 continue; 107 } 108 joiner.add(id); 109 } 110 111 Settings.Secure.putString(context.getContentResolver(), targetsKey, joiner.toString()); 112 } 113 114 /** 115 * Returns if component id existed in Settings. 116 * 117 * @param context The current context. 118 * @param shortcutType The preferred shortcut type user selected. 119 * @param componentId The component id that need to be checked existed in Settings. 120 * @return {@code true} if component id existed in Settings. 121 */ isComponentIdExistingInSettings(Context context, @UserShortcutType int shortcutType, @NonNull String componentId)122 public static boolean isComponentIdExistingInSettings(Context context, 123 @UserShortcutType int shortcutType, @NonNull String componentId) { 124 final String targetKey = convertToKey(shortcutType); 125 final String targetString = Settings.Secure.getString(context.getContentResolver(), 126 targetKey); 127 128 if (TextUtils.isEmpty(targetString)) { 129 return false; 130 } 131 132 sStringColonSplitter.setString(targetString); 133 while (sStringColonSplitter.hasNext()) { 134 final String id = sStringColonSplitter.next(); 135 if (componentId.equals(id)) { 136 return true; 137 } 138 } 139 140 return false; 141 } 142 143 /** 144 * Returns if a {@code shortcutType} shortcut contains {@code componentId}. 145 * 146 * @param context The current context. 147 * @param shortcutType The preferred shortcut type user selected. 148 * @param componentId The component id that need to be checked. 149 * @return {@code true} if a component id is contained. 150 */ isShortcutContained(Context context, @UserShortcutType int shortcutType, @NonNull String componentId)151 public static boolean isShortcutContained(Context context, @UserShortcutType int shortcutType, 152 @NonNull String componentId) { 153 final AccessibilityManager am = (AccessibilityManager) context.getSystemService( 154 Context.ACCESSIBILITY_SERVICE); 155 final List<String> requiredTargets = am.getAccessibilityShortcutTargets(shortcutType); 156 return requiredTargets.contains(componentId); 157 } 158 159 /** 160 * Converts {@link UserShortcutType} to {@link Settings.Secure} key. 161 * 162 * @param type The shortcut type. 163 * @return Mapping key in Settings. 164 */ convertToKey(@serShortcutType int type)165 public static String convertToKey(@UserShortcutType int type) { 166 switch (type) { 167 case UserShortcutType.SOFTWARE: 168 return Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS; 169 case UserShortcutType.HARDWARE: 170 return Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE; 171 case UserShortcutType.TRIPLETAP: 172 return Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED; 173 case UserShortcutType.TWOFINGER_DOUBLETAP: 174 return Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED; 175 case UserShortcutType.QUICK_SETTINGS: 176 return Settings.Secure.ACCESSIBILITY_QS_TARGETS; 177 default: 178 throw new IllegalArgumentException( 179 "Unsupported user shortcut type: " + type); 180 } 181 } 182 183 /** 184 * Updates an accessibility state if the accessibility service is a Always-On a11y service, 185 * a.k.a. AccessibilityServices that has FLAG_REQUEST_ACCESSIBILITY_BUTTON 186 * <p> 187 * Turn on the accessibility service when there is any shortcut associated to it. 188 * <p> 189 * Turn off the accessibility service when there is no shortcut associated to it. 190 * 191 * @param componentNames the a11y shortcut target's component names 192 */ updateInvisibleToggleAccessibilityServiceEnableState( Context context, Set<String> componentNames, int userId)193 public static void updateInvisibleToggleAccessibilityServiceEnableState( 194 Context context, Set<String> componentNames, int userId) { 195 final AccessibilityManager am = (AccessibilityManager) context.getSystemService( 196 Context.ACCESSIBILITY_SERVICE); 197 if (am == null) return; 198 199 final List<AccessibilityServiceInfo> installedServices = 200 am.getInstalledAccessibilityServiceList(); 201 202 final Set<String> invisibleToggleServices = new ArraySet<>(); 203 for (AccessibilityServiceInfo serviceInfo : installedServices) { 204 if (AccessibilityUtils.getAccessibilityServiceFragmentType(serviceInfo) 205 == INVISIBLE_TOGGLE) { 206 invisibleToggleServices.add(serviceInfo.getComponentName().flattenToString()); 207 } 208 } 209 210 final Set<String> servicesWithShortcuts = new ArraySet<>(); 211 for (int shortcutType: USER_SHORTCUT_TYPES) { 212 // The call to update always-on service might modify the shortcut setting right before 213 // calling #updateAccessibilityServiceStateIfNeeded in the same call. 214 // To avoid getting the shortcut target from out-dated value, use values from Settings 215 // instead. 216 servicesWithShortcuts.addAll( 217 getShortcutTargetsFromSettings(context, shortcutType, userId)); 218 } 219 220 for (String componentName : componentNames) { 221 // Only needs to update the Always-On A11yService's state when the shortcut changes. 222 if (invisibleToggleServices.contains(componentName)) { 223 224 boolean enableA11yService = servicesWithShortcuts.contains(componentName); 225 AccessibilityUtils.setAccessibilityServiceState( 226 context, 227 ComponentName.unflattenFromString(componentName), enableA11yService); 228 } 229 } 230 } 231 232 /** 233 * Returns the target component names of a given user shortcut type from Settings. 234 * 235 * <p> 236 * Note: grab shortcut targets from Settings is only needed 237 * if you depends on a value being set in the same call. 238 * For example, you disable a single shortcut, 239 * and you're checking if there is any shortcut remaining. 240 * 241 * <p> 242 * If you just want to know the current state, you can use 243 * {@link AccessibilityManager#getAccessibilityShortcutTargets(int)} 244 */ 245 @NonNull getShortcutTargetsFromSettings( Context context, @UserShortcutType int shortcutType, int userId)246 public static Set<String> getShortcutTargetsFromSettings( 247 Context context, @UserShortcutType int shortcutType, int userId) { 248 final String targetKey = convertToKey(shortcutType); 249 if (Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED.equals(targetKey) 250 || Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED 251 .equals(targetKey)) { 252 boolean magnificationEnabled = Settings.Secure.getIntForUser( 253 context.getContentResolver(), targetKey, /* def= */ 0, userId) == 1; 254 return magnificationEnabled ? Set.of(MAGNIFICATION_CONTROLLER_NAME) 255 : Collections.emptySet(); 256 257 } else { 258 final String targetString = Settings.Secure.getStringForUser( 259 context.getContentResolver(), targetKey, userId); 260 261 if (TextUtils.isEmpty(targetString)) { 262 return Collections.emptySet(); 263 } 264 265 Set<String> targets = new ArraySet<>(); 266 sStringColonSplitter.setString(targetString); 267 while (sStringColonSplitter.hasNext()) { 268 targets.add(sStringColonSplitter.next()); 269 } 270 return Collections.unmodifiableSet(targets); 271 } 272 } 273 } 274