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