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