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