• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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