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