• 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 static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
25 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
26 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
27 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
28 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP;
29 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TWOFINGER_DOUBLETAP;
30 
31 import android.accessibilityservice.AccessibilityServiceInfo;
32 import android.content.ComponentName;
33 import android.content.Context;
34 import android.content.res.Resources;
35 import android.graphics.Insets;
36 import android.graphics.Rect;
37 import android.icu.text.CaseMap;
38 import android.os.Build;
39 import android.provider.Settings;
40 import android.text.TextUtils;
41 import android.util.TypedValue;
42 import android.view.WindowManager;
43 import android.view.WindowMetrics;
44 import android.view.accessibility.AccessibilityManager;
45 
46 import androidx.annotation.IntDef;
47 import androidx.annotation.NonNull;
48 import androidx.annotation.StringRes;
49 import androidx.annotation.VisibleForTesting;
50 
51 import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType;
52 import com.android.internal.accessibility.util.ShortcutUtils;
53 import com.android.settings.R;
54 import com.android.settings.utils.LocaleUtils;
55 
56 import java.lang.annotation.Retention;
57 import java.lang.annotation.RetentionPolicy;
58 import java.util.ArrayList;
59 import java.util.List;
60 import java.util.Locale;
61 
62 /** Provides utility methods to accessibility settings only. */
63 public final class AccessibilityUtil {
64     // LINT.IfChange(shortcut_type_ui_order)
65     static final int[] SHORTCUTS_ORDER_IN_UI = {
66             QUICK_SETTINGS,
67             SOFTWARE, // FAB displays before gesture. Navbar displays without gesture.
68             GESTURE,
69             HARDWARE,
70             TWOFINGER_DOUBLETAP,
71             TRIPLETAP
72     };
73     // LINT.ThenChange(/res/xml/accessibility_edit_shortcuts.xml:shortcut_type_ui_order)
74 
AccessibilityUtil()75     private AccessibilityUtil(){}
76 
77     /**
78      * Annotation for different accessibilityService fragment UI type.
79      *
80      * {@code VOLUME_SHORTCUT_TOGGLE} for displaying basic accessibility service fragment but
81      * only hardware shortcut allowed.
82      * {@code INVISIBLE_TOGGLE} for displaying basic accessibility service fragment without
83      * switch bar.
84      * {@code TOGGLE} for displaying basic accessibility service fragment.
85      */
86     @Retention(RetentionPolicy.SOURCE)
87     @IntDef({
88             AccessibilityServiceFragmentType.VOLUME_SHORTCUT_TOGGLE,
89             AccessibilityServiceFragmentType.INVISIBLE_TOGGLE,
90             AccessibilityServiceFragmentType.TOGGLE,
91     })
92 
93     public @interface AccessibilityServiceFragmentType {
94         int VOLUME_SHORTCUT_TOGGLE = 0;
95         int INVISIBLE_TOGGLE = 1;
96         int TOGGLE = 2;
97     }
98 
99     // TODO(b/147021230): Will move common functions and variables to
100     //  android/internal/accessibility folder
101     private static final char COMPONENT_NAME_SEPARATOR = ':';
102     private static final TextUtils.SimpleStringSplitter sStringColonSplitter =
103             new TextUtils.SimpleStringSplitter(COMPONENT_NAME_SEPARATOR);
104 
105     /**
106      * Denotes the quick setting tooltip type.
107      *
108      * {@code GUIDE_TO_EDIT} for QS tiles that need to be added by editing.
109      * {@code GUIDE_TO_DIRECT_USE} for QS tiles that have been auto-added already.
110      */
111     public @interface QuickSettingsTooltipType {
112         int GUIDE_TO_EDIT = 0;
113         int GUIDE_TO_DIRECT_USE = 1;
114     }
115 
116     /** Denotes the accessibility enabled status */
117     @Retention(RetentionPolicy.SOURCE)
118     public @interface State {
119         int OFF = 0;
120         int ON = 1;
121     }
122 
123     /**
124      * Returns On/Off string according to the setting which specifies the integer value 1 or 0. This
125      * setting is defined in the secure system settings {@link android.provider.Settings.Secure}.
126      */
getSummary( Context context, String settingsSecureKey, @StringRes int enabledString, @StringRes int disabledString)127     static CharSequence getSummary(
128             Context context, String settingsSecureKey, @StringRes int enabledString,
129             @StringRes int disabledString) {
130         boolean enabled = Settings.Secure.getInt(context.getContentResolver(),
131                 settingsSecureKey, State.OFF) == State.ON;
132         return context.getResources().getText(enabled ? enabledString : disabledString);
133     }
134 
135     /**
136      * Capitalizes a string by capitalizing the first character and making the remaining characters
137      * lower case.
138      */
capitalize(String stringToCapitalize)139     public static String capitalize(String stringToCapitalize) {
140         if (stringToCapitalize == null) {
141             return null;
142         }
143 
144         StringBuilder capitalizedString = new StringBuilder();
145         if (stringToCapitalize.length() > 0) {
146             capitalizedString.append(stringToCapitalize.substring(0, 1).toUpperCase());
147             if (stringToCapitalize.length() > 1) {
148                 capitalizedString.append(stringToCapitalize.substring(1).toLowerCase());
149             }
150         }
151         return capitalizedString.toString();
152     }
153 
154     /** Determines if a gesture navigation bar is being used. */
isGestureNavigateEnabled(Context context)155     public static boolean isGestureNavigateEnabled(Context context) {
156         return Settings.Secure.getInt(context.getContentResolver(),
157                 Settings.Secure.NAVIGATION_MODE, -1)
158                 == NAV_BAR_MODE_GESTURAL;
159     }
160 
161     /** Determines if a accessibility floating menu is being used. */
isFloatingMenuEnabled(Context context)162     public static boolean isFloatingMenuEnabled(Context context) {
163         return Settings.Secure.getInt(context.getContentResolver(),
164                 Settings.Secure.ACCESSIBILITY_BUTTON_MODE, /* def= */ -1)
165                 == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
166     }
167 
168     /** Determines if a touch explore is being used. */
isTouchExploreEnabled(Context context)169     public static boolean isTouchExploreEnabled(Context context) {
170         final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
171         return am.isTouchExplorationEnabled();
172     }
173 
174     /**
175      * Gets the corresponding fragment type of a given accessibility service.
176      *
177      * @param accessibilityServiceInfo The accessibilityService's info
178      * @return int from {@link AccessibilityServiceFragmentType}
179      */
getAccessibilityServiceFragmentType( AccessibilityServiceInfo accessibilityServiceInfo)180     static @AccessibilityServiceFragmentType int getAccessibilityServiceFragmentType(
181             AccessibilityServiceInfo accessibilityServiceInfo) {
182         final int targetSdk = accessibilityServiceInfo.getResolveInfo()
183                 .serviceInfo.applicationInfo.targetSdkVersion;
184         final boolean requestA11yButton = (accessibilityServiceInfo.flags
185                 & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
186 
187         if (targetSdk <= Build.VERSION_CODES.Q) {
188             return AccessibilityServiceFragmentType.VOLUME_SHORTCUT_TOGGLE;
189         }
190         return requestA11yButton
191                 ? AccessibilityServiceFragmentType.INVISIBLE_TOGGLE
192                 : AccessibilityServiceFragmentType.TOGGLE;
193     }
194 
195     /**
196      * Gets the corresponding user shortcut type of a given accessibility service.
197      *
198      * @param context The current context.
199      * @param componentName The component name that need to be checked existed in Settings.
200      * @return The user shortcut type if component name existed in {@code UserShortcutType} string
201      * Settings.
202      */
getUserShortcutTypesFromSettings(Context context, @NonNull ComponentName componentName)203     static int getUserShortcutTypesFromSettings(Context context,
204             @NonNull ComponentName componentName) {
205         int shortcutTypes = UserShortcutType.DEFAULT;
206         for (int shortcutType : AccessibilityUtil.SHORTCUTS_ORDER_IN_UI) {
207             if (!android.provider.Flags.a11yStandaloneGestureEnabled()) {
208                 if ((shortcutType & GESTURE) == GESTURE) {
209                     continue;
210                 }
211             }
212             if (ShortcutUtils.isShortcutContained(
213                     context, shortcutType, componentName.flattenToString())) {
214                 shortcutTypes |= shortcutType;
215             }
216         }
217 
218         return shortcutTypes;
219     }
220 
221     /**
222      * Gets the width of the screen.
223      *
224      * @param context the current context.
225      * @return the width of the screen in terms of pixels.
226      */
getScreenWidthPixels(Context context)227     public static int getScreenWidthPixels(Context context) {
228         final Resources resources = context.getResources();
229         final int screenWidthDp = resources.getConfiguration().screenWidthDp;
230 
231         return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, screenWidthDp,
232                 resources.getDisplayMetrics()));
233     }
234 
235     /**
236      * Gets the height of the screen.
237      *
238      * @param context the current context.
239      * @return the height of the screen in terms of pixels.
240      */
getScreenHeightPixels(Context context)241     public static int getScreenHeightPixels(Context context) {
242         final Resources resources = context.getResources();
243         final int screenHeightDp = resources.getConfiguration().screenHeightDp;
244 
245         return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, screenHeightDp,
246                 resources.getDisplayMetrics()));
247     }
248 
249     /**
250      * Gets the bounds of the display window excluding the insets of the system bar and display
251      * cut out.
252      *
253      * @param context the current context.
254      * @return the bounds of the display window.
255      */
getDisplayBounds(Context context)256     public static Rect getDisplayBounds(Context context) {
257         final WindowManager windowManager = context.getSystemService(WindowManager.class);
258         final WindowMetrics metrics = windowManager.getCurrentWindowMetrics();
259 
260         final Rect displayBounds = metrics.getBounds();
261         final Insets displayInsets = metrics.getWindowInsets().getInsetsIgnoringVisibility(
262                 systemBars() | displayCutout());
263         displayBounds.inset(displayInsets);
264 
265         return displayBounds;
266     }
267 
268     /**
269      * Indicates if the accessibility service belongs to a system App.
270      * @param info AccessibilityServiceInfo
271      * @return {@code true} if the App is a system App.
272      */
isSystemApp(@onNull AccessibilityServiceInfo info)273     public static boolean isSystemApp(@NonNull AccessibilityServiceInfo info) {
274         return info.getResolveInfo().serviceInfo.applicationInfo.isSystemApp();
275     }
276 
277     /**
278      * Bypasses the timeout restriction if volume key shortcut assigned.
279      *
280      * @param context the current context.
281      */
skipVolumeShortcutDialogTimeoutRestriction(Context context)282     public static void skipVolumeShortcutDialogTimeoutRestriction(Context context) {
283         Settings.Secure.putInt(context.getContentResolver(),
284                 Settings.Secure.SKIP_ACCESSIBILITY_SHORTCUT_DIALOG_TIMEOUT_RESTRICTION, /*
285                     true */ 1);
286     }
287 
288     /**
289      * Assembles a localized string describing the provided shortcut types.
290      */
getShortcutSummaryList(Context context, int shortcutTypes)291     public static CharSequence getShortcutSummaryList(Context context, int shortcutTypes) {
292         final List<CharSequence> list = new ArrayList<>();
293 
294         for (int shortcutType : AccessibilityUtil.SHORTCUTS_ORDER_IN_UI) {
295             if (!android.provider.Flags.a11yStandaloneGestureEnabled()
296                     && (shortcutType & GESTURE) == GESTURE) {
297                 continue;
298             }
299             if (!com.android.server.accessibility.Flags
300                     .enableMagnificationMultipleFingerMultipleTapGesture()
301                     && (shortcutType & TWOFINGER_DOUBLETAP) == TWOFINGER_DOUBLETAP) {
302                 continue;
303             }
304 
305             if ((shortcutTypes & shortcutType) == shortcutType) {
306                 list.add(switch (shortcutType) {
307                     case QUICK_SETTINGS -> context.getText(
308                             R.string.accessibility_feature_shortcut_setting_summary_quick_settings);
309                     case SOFTWARE -> getSoftwareShortcutSummary(context);
310                     case GESTURE -> context.getText(
311                             R.string.accessibility_shortcut_edit_summary_software_gesture);
312                     case HARDWARE -> context.getText(
313                             R.string.accessibility_shortcut_hardware_keyword);
314                     case TWOFINGER_DOUBLETAP -> context.getString(
315                             R.string.accessibility_shortcut_two_finger_double_tap_keyword, 2);
316                     case TRIPLETAP -> context.getText(
317                             R.string.accessibility_shortcut_triple_tap_keyword);
318                     default -> "";
319                 });
320             }
321         }
322 
323         list.sort(CharSequence::compare);
324         return CaseMap.toTitle().wholeString().noLowercase().apply(Locale.getDefault(), /* iter= */
325                 null, LocaleUtils.getConcatenatedString(list));
326     }
327 
328     @VisibleForTesting
getSoftwareShortcutSummary(Context context)329     static CharSequence getSoftwareShortcutSummary(Context context) {
330         if (android.provider.Flags.a11yStandaloneGestureEnabled()) {
331             return context.getText(R.string.accessibility_shortcut_edit_summary_software);
332         }
333         int resId;
334         if (AccessibilityUtil.isFloatingMenuEnabled(context)) {
335             resId = R.string.accessibility_shortcut_edit_summary_software;
336         } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) {
337             resId = R.string.accessibility_shortcut_edit_summary_software_gesture;
338         } else {
339             resId = R.string.accessibility_shortcut_edit_summary_software;
340         }
341         return context.getText(resId);
342     }
343 }
344