• 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.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 public 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     /**
109      * Denotes the quick setting tooltip type.
110      *
111      * {@code GUIDE_TO_EDIT} for QS tiles that need to be added by editing.
112      * {@code GUIDE_TO_DIRECT_USE} for QS tiles that have been auto-added already.
113      */
114     public @interface QuickSettingsTooltipType {
115         int GUIDE_TO_EDIT = 0;
116         int GUIDE_TO_DIRECT_USE = 1;
117     }
118 
119     /** Denotes the accessibility enabled status */
120     @Retention(RetentionPolicy.SOURCE)
121     public @interface State {
122         int OFF = 0;
123         int ON = 1;
124     }
125 
126     /**
127      * Return On/Off string according to the setting which specifies the integer value 1 or 0. This
128      * setting is defined in the secure system settings {@link android.provider.Settings.Secure}.
129      */
getSummary(Context context, String settingsSecureKey)130     static CharSequence getSummary(Context context, String settingsSecureKey) {
131         final boolean enabled = Settings.Secure.getInt(context.getContentResolver(),
132                 settingsSecureKey, State.OFF) == State.ON;
133         final int resId = enabled ? R.string.accessibility_feature_state_on
134                 : R.string.accessibility_feature_state_off;
135         return context.getResources().getText(resId);
136     }
137 
138     /**
139      * Capitalizes a string by capitalizing the first character and making the remaining characters
140      * lower case.
141      */
capitalize(String stringToCapitalize)142     public static String capitalize(String stringToCapitalize) {
143         if (stringToCapitalize == null) {
144             return null;
145         }
146 
147         StringBuilder capitalizedString = new StringBuilder();
148         if (stringToCapitalize.length() > 0) {
149             capitalizedString.append(stringToCapitalize.substring(0, 1).toUpperCase());
150             if (stringToCapitalize.length() > 1) {
151                 capitalizedString.append(stringToCapitalize.substring(1).toLowerCase());
152             }
153         }
154         return capitalizedString.toString();
155     }
156 
157     /** Determines if a gesture navigation bar is being used. */
isGestureNavigateEnabled(Context context)158     public static boolean isGestureNavigateEnabled(Context context) {
159         return context.getResources().getInteger(
160                 com.android.internal.R.integer.config_navBarInteractionMode)
161                 == NAV_BAR_MODE_GESTURAL;
162     }
163 
164     /** Determines if a accessibility floating menu is being used. */
isFloatingMenuEnabled(Context context)165     public static boolean isFloatingMenuEnabled(Context context) {
166         return Settings.Secure.getInt(context.getContentResolver(),
167                 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, /* def= */ -1)
168                 == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
169     }
170 
171     /** Determines if a touch explore is being used. */
isTouchExploreEnabled(Context context)172     public static boolean isTouchExploreEnabled(Context context) {
173         final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
174         return am.isTouchExplorationEnabled();
175     }
176 
177     /**
178      * Gets the corresponding fragment type of a given accessibility service.
179      *
180      * @param accessibilityServiceInfo The accessibilityService's info
181      * @return int from {@link AccessibilityServiceFragmentType}
182      */
getAccessibilityServiceFragmentType( AccessibilityServiceInfo accessibilityServiceInfo)183     static @AccessibilityServiceFragmentType int getAccessibilityServiceFragmentType(
184             AccessibilityServiceInfo accessibilityServiceInfo) {
185         final int targetSdk = accessibilityServiceInfo.getResolveInfo()
186                 .serviceInfo.applicationInfo.targetSdkVersion;
187         final boolean requestA11yButton = (accessibilityServiceInfo.flags
188                 & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
189 
190         if (targetSdk <= Build.VERSION_CODES.Q) {
191             return AccessibilityServiceFragmentType.VOLUME_SHORTCUT_TOGGLE;
192         }
193         return requestA11yButton
194                 ? AccessibilityServiceFragmentType.INVISIBLE_TOGGLE
195                 : AccessibilityServiceFragmentType.TOGGLE;
196     }
197 
198     /**
199      * Opts in component name into multiple {@code shortcutTypes} colon-separated string in
200      * Settings.
201      *
202      * @param context The current context.
203      * @param shortcutTypes  A combination of {@link UserShortcutType}.
204      * @param componentName The component name that need to be opted in Settings.
205      */
optInAllValuesToSettings(Context context, int shortcutTypes, @NonNull ComponentName componentName)206     static void optInAllValuesToSettings(Context context, int shortcutTypes,
207             @NonNull ComponentName componentName) {
208         if ((shortcutTypes & UserShortcutType.SOFTWARE) == UserShortcutType.SOFTWARE) {
209             optInValueToSettings(context, UserShortcutType.SOFTWARE, componentName);
210         }
211         if (((shortcutTypes & UserShortcutType.HARDWARE) == UserShortcutType.HARDWARE)) {
212             optInValueToSettings(context, UserShortcutType.HARDWARE, componentName);
213         }
214     }
215 
216     /**
217      * Opts in component name into {@code shortcutType} colon-separated string in Settings.
218      *
219      * @param context The current context.
220      * @param shortcutType The preferred shortcut type user selected.
221      * @param componentName The component name that need to be opted in Settings.
222      */
223     @VisibleForTesting
optInValueToSettings(Context context, @UserShortcutType int shortcutType, @NonNull ComponentName componentName)224     static void optInValueToSettings(Context context, @UserShortcutType int shortcutType,
225             @NonNull ComponentName componentName) {
226         final String targetKey = convertKeyFromSettings(shortcutType);
227         final String targetString = Settings.Secure.getString(context.getContentResolver(),
228                 targetKey);
229 
230         if (hasValueInSettings(context, shortcutType, componentName)) {
231             return;
232         }
233 
234         final StringJoiner joiner = new StringJoiner(String.valueOf(COMPONENT_NAME_SEPARATOR));
235         if (!TextUtils.isEmpty(targetString)) {
236             joiner.add(targetString);
237         }
238         joiner.add(componentName.flattenToString());
239 
240         Settings.Secure.putString(context.getContentResolver(), targetKey, joiner.toString());
241     }
242 
243     /**
244      * Opts out component name into multiple {@code shortcutTypes} colon-separated string in
245      * Settings.
246      *
247      * @param context The current context.
248      * @param shortcutTypes A combination of {@link UserShortcutType}.
249      * @param componentName The component name that need to be opted out from Settings.
250      */
optOutAllValuesFromSettings(Context context, int shortcutTypes, @NonNull ComponentName componentName)251     static void optOutAllValuesFromSettings(Context context, int shortcutTypes,
252             @NonNull ComponentName componentName) {
253         if ((shortcutTypes & UserShortcutType.SOFTWARE) == UserShortcutType.SOFTWARE) {
254             optOutValueFromSettings(context, UserShortcutType.SOFTWARE, componentName);
255         }
256         if (((shortcutTypes & UserShortcutType.HARDWARE) == UserShortcutType.HARDWARE)) {
257             optOutValueFromSettings(context, UserShortcutType.HARDWARE, componentName);
258         }
259     }
260 
261     /**
262      * Opts out component name into {@code shortcutType} colon-separated string in Settings.
263      *
264      * @param context The current context.
265      * @param shortcutType The preferred shortcut type user selected.
266      * @param componentName The component name that need to be opted out from Settings.
267      */
268     @VisibleForTesting
optOutValueFromSettings(Context context, @UserShortcutType int shortcutType, @NonNull ComponentName componentName)269     static void optOutValueFromSettings(Context context, @UserShortcutType int shortcutType,
270             @NonNull ComponentName componentName) {
271         final StringJoiner joiner = new StringJoiner(String.valueOf(COMPONENT_NAME_SEPARATOR));
272         final String targetKey = convertKeyFromSettings(shortcutType);
273         final String targetString = Settings.Secure.getString(context.getContentResolver(),
274                 targetKey);
275 
276         if (TextUtils.isEmpty(targetString)) {
277             return;
278         }
279 
280         sStringColonSplitter.setString(targetString);
281         while (sStringColonSplitter.hasNext()) {
282             final String name = sStringColonSplitter.next();
283             if (TextUtils.isEmpty(name) || (componentName.flattenToString()).equals(name)) {
284                 continue;
285             }
286             joiner.add(name);
287         }
288 
289         Settings.Secure.putString(context.getContentResolver(), targetKey, joiner.toString());
290     }
291 
292     /**
293      * Returns if component name existed in one of {@code shortcutTypes} string in Settings.
294      *
295      * @param context The current context.
296      * @param shortcutTypes A combination of {@link UserShortcutType}.
297      * @param componentName The component name that need to be checked existed in Settings.
298      * @return {@code true} if componentName existed in Settings.
299      */
hasValuesInSettings(Context context, int shortcutTypes, @NonNull ComponentName componentName)300     static boolean hasValuesInSettings(Context context, int shortcutTypes,
301             @NonNull ComponentName componentName) {
302         boolean exist = false;
303         if ((shortcutTypes & UserShortcutType.SOFTWARE) == UserShortcutType.SOFTWARE) {
304             exist = hasValueInSettings(context, UserShortcutType.SOFTWARE, componentName);
305         }
306         if (((shortcutTypes & UserShortcutType.HARDWARE) == UserShortcutType.HARDWARE)) {
307             exist |= hasValueInSettings(context, UserShortcutType.HARDWARE, componentName);
308         }
309         return exist;
310     }
311 
312     /**
313      * Returns if component name existed in {@code shortcutType} string 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 checked existed in Settings.
318      * @return {@code true} if componentName existed in Settings.
319      */
320     @VisibleForTesting
hasValueInSettings(Context context, @UserShortcutType int shortcutType, @NonNull ComponentName componentName)321     static boolean hasValueInSettings(Context context, @UserShortcutType int shortcutType,
322             @NonNull ComponentName componentName) {
323         final String targetKey = convertKeyFromSettings(shortcutType);
324         final String targetString = Settings.Secure.getString(context.getContentResolver(),
325                 targetKey);
326 
327         if (TextUtils.isEmpty(targetString)) {
328             return false;
329         }
330 
331         sStringColonSplitter.setString(targetString);
332 
333         while (sStringColonSplitter.hasNext()) {
334             final String name = sStringColonSplitter.next();
335             if ((componentName.flattenToString()).equals(name)) {
336                 return true;
337             }
338         }
339         return false;
340     }
341 
342     /**
343      * Gets the corresponding user shortcut type of a given accessibility service.
344      *
345      * @param context The current context.
346      * @param componentName The component name that need to be checked existed in Settings.
347      * @return The user shortcut type if component name existed in {@code UserShortcutType} string
348      * Settings.
349      */
getUserShortcutTypesFromSettings(Context context, @NonNull ComponentName componentName)350     static int getUserShortcutTypesFromSettings(Context context,
351             @NonNull ComponentName componentName) {
352         int shortcutTypes = UserShortcutType.EMPTY;
353         if (hasValuesInSettings(context, UserShortcutType.SOFTWARE, componentName)) {
354             shortcutTypes |= UserShortcutType.SOFTWARE;
355         }
356         if (hasValuesInSettings(context, UserShortcutType.HARDWARE, componentName)) {
357             shortcutTypes |= UserShortcutType.HARDWARE;
358         }
359         return shortcutTypes;
360     }
361 
362     /**
363      * Converts {@link UserShortcutType} to key in Settings.
364      *
365      * @param shortcutType The shortcut type.
366      * @return Mapping key in Settings.
367      */
convertKeyFromSettings(@serShortcutType int shortcutType)368     static String convertKeyFromSettings(@UserShortcutType int shortcutType) {
369         switch (shortcutType) {
370             case UserShortcutType.SOFTWARE:
371                 return Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS;
372             case UserShortcutType.HARDWARE:
373                 return Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE;
374             case UserShortcutType.TRIPLETAP:
375                 return Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED;
376             default:
377                 throw new IllegalArgumentException(
378                         "Unsupported userShortcutType " + shortcutType);
379         }
380     }
381 
382     /**
383      * Gets the width of the screen.
384      *
385      * @param context the current context.
386      * @return the width of the screen in terms of pixels.
387      */
getScreenWidthPixels(Context context)388     public static int getScreenWidthPixels(Context context) {
389         final Resources resources = context.getResources();
390         final int screenWidthDp = resources.getConfiguration().screenWidthDp;
391 
392         return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, screenWidthDp,
393                 resources.getDisplayMetrics()));
394     }
395 
396     /**
397      * Gets the height of the screen.
398      *
399      * @param context the current context.
400      * @return the height of the screen in terms of pixels.
401      */
getScreenHeightPixels(Context context)402     public static int getScreenHeightPixels(Context context) {
403         final Resources resources = context.getResources();
404         final int screenHeightDp = resources.getConfiguration().screenHeightDp;
405 
406         return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, screenHeightDp,
407                 resources.getDisplayMetrics()));
408     }
409 
410     /**
411      * Gets the bounds of the display window excluding the insets of the system bar and display
412      * cut out.
413      *
414      * @param context the current context.
415      * @return the bounds of the display window.
416      */
getDisplayBounds(Context context)417     public static Rect getDisplayBounds(Context context) {
418         final WindowManager windowManager = context.getSystemService(WindowManager.class);
419         final WindowMetrics metrics = windowManager.getCurrentWindowMetrics();
420 
421         final Rect displayBounds = metrics.getBounds();
422         final Insets displayInsets = metrics.getWindowInsets().getInsetsIgnoringVisibility(
423                 systemBars() | displayCutout());
424         displayBounds.inset(displayInsets);
425 
426         return displayBounds;
427     }
428 
429     /**
430      * Indicates if the accessibility service belongs to a system App.
431      * @param info AccessibilityServiceInfo
432      * @return {@code true} if the App is a system App.
433      */
isSystemApp(@onNull AccessibilityServiceInfo info)434     public static boolean isSystemApp(@NonNull AccessibilityServiceInfo info) {
435         return info.getResolveInfo().serviceInfo.applicationInfo.isSystemApp();
436     }
437 
438     /**
439      * Bypasses the timeout restriction if volume key shortcut assigned.
440      *
441      * @param context the current context.
442      */
skipVolumeShortcutDialogTimeoutRestriction(Context context)443     public static void skipVolumeShortcutDialogTimeoutRestriction(Context context) {
444         Settings.Secure.putInt(context.getContentResolver(),
445                 Settings.Secure.SKIP_ACCESSIBILITY_SHORTCUT_DIALOG_TIMEOUT_RESTRICTION, /*
446                     true */ 1);
447     }
448 }
449