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