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