| /* | |
| * Copyright (C) 2019 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| package com.android.settings.accessibility; | |
| import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; | |
| import static android.view.WindowInsets.Type.displayCutout; | |
| import static android.view.WindowInsets.Type.systemBars; | |
| import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; | |
| import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; | |
| import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; | |
| import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; | |
| import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE; | |
| import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP; | |
| import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TWOFINGER_DOUBLETAP; | |
| import android.accessibilityservice.AccessibilityServiceInfo; | |
| import android.content.ComponentName; | |
| import android.content.Context; | |
| import android.content.res.Resources; | |
| import android.graphics.Insets; | |
| import android.graphics.Rect; | |
| import android.icu.text.CaseMap; | |
| import android.os.Build; | |
| import android.provider.Settings; | |
| import android.text.TextUtils; | |
| import android.util.TypedValue; | |
| import android.view.WindowManager; | |
| import android.view.WindowMetrics; | |
| import android.view.accessibility.AccessibilityManager; | |
| import androidx.annotation.IntDef; | |
| import androidx.annotation.NonNull; | |
| import androidx.annotation.StringRes; | |
| import androidx.annotation.VisibleForTesting; | |
| import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType; | |
| import com.android.internal.accessibility.util.ShortcutUtils; | |
| import com.android.settings.R; | |
| import com.android.settings.utils.LocaleUtils; | |
| import java.lang.annotation.Retention; | |
| import java.lang.annotation.RetentionPolicy; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.Locale; | |
| /** Provides utility methods to accessibility settings only. */ | |
| public final class AccessibilityUtil { | |
| // LINT.IfChange(shortcut_type_ui_order) | |
| static final int[] SHORTCUTS_ORDER_IN_UI = { | |
| QUICK_SETTINGS, | |
| SOFTWARE, // FAB displays before gesture. Navbar displays without gesture. | |
| GESTURE, | |
| HARDWARE, | |
| TWOFINGER_DOUBLETAP, | |
| TRIPLETAP | |
| }; | |
| // LINT.ThenChange(/res/xml/accessibility_edit_shortcuts.xml:shortcut_type_ui_order) | |
| private AccessibilityUtil(){} | |
| /** | |
| * Annotation for different accessibilityService fragment UI type. | |
| * | |
| * {@code VOLUME_SHORTCUT_TOGGLE} for displaying basic accessibility service fragment but | |
| * only hardware shortcut allowed. | |
| * {@code INVISIBLE_TOGGLE} for displaying basic accessibility service fragment without | |
| * switch bar. | |
| * {@code TOGGLE} for displaying basic accessibility service fragment. | |
| */ | |
| @Retention(RetentionPolicy.SOURCE) | |
| @IntDef({ | |
| AccessibilityServiceFragmentType.VOLUME_SHORTCUT_TOGGLE, | |
| AccessibilityServiceFragmentType.INVISIBLE_TOGGLE, | |
| AccessibilityServiceFragmentType.TOGGLE, | |
| }) | |
| public @interface AccessibilityServiceFragmentType { | |
| int VOLUME_SHORTCUT_TOGGLE = 0; | |
| int INVISIBLE_TOGGLE = 1; | |
| int TOGGLE = 2; | |
| } | |
| // TODO(b/147021230): Will move common functions and variables to | |
| // android/internal/accessibility folder | |
| private static final char COMPONENT_NAME_SEPARATOR = ':'; | |
| private static final TextUtils.SimpleStringSplitter sStringColonSplitter = | |
| new TextUtils.SimpleStringSplitter(COMPONENT_NAME_SEPARATOR); | |
| /** | |
| * Denotes the quick setting tooltip type. | |
| * | |
| * {@code GUIDE_TO_EDIT} for QS tiles that need to be added by editing. | |
| * {@code GUIDE_TO_DIRECT_USE} for QS tiles that have been auto-added already. | |
| */ | |
| public @interface QuickSettingsTooltipType { | |
| int GUIDE_TO_EDIT = 0; | |
| int GUIDE_TO_DIRECT_USE = 1; | |
| } | |
| /** Denotes the accessibility enabled status */ | |
| @Retention(RetentionPolicy.SOURCE) | |
| public @interface State { | |
| int OFF = 0; | |
| int ON = 1; | |
| } | |
| /** | |
| * Returns On/Off string according to the setting which specifies the integer value 1 or 0. This | |
| * setting is defined in the secure system settings {@link android.provider.Settings.Secure}. | |
| */ | |
| static CharSequence getSummary( | |
| Context context, String settingsSecureKey, @StringRes int enabledString, | |
| @StringRes int disabledString) { | |
| boolean enabled = Settings.Secure.getInt(context.getContentResolver(), | |
| settingsSecureKey, State.OFF) == State.ON; | |
| return context.getResources().getText(enabled ? enabledString : disabledString); | |
| } | |
| /** | |
| * Capitalizes a string by capitalizing the first character and making the remaining characters | |
| * lower case. | |
| */ | |
| public static String capitalize(String stringToCapitalize) { | |
| if (stringToCapitalize == null) { | |
| return null; | |
| } | |
| StringBuilder capitalizedString = new StringBuilder(); | |
| if (stringToCapitalize.length() > 0) { | |
| capitalizedString.append(stringToCapitalize.substring(0, 1).toUpperCase()); | |
| if (stringToCapitalize.length() > 1) { | |
| capitalizedString.append(stringToCapitalize.substring(1).toLowerCase()); | |
| } | |
| } | |
| return capitalizedString.toString(); | |
| } | |
| /** Determines if a gesture navigation bar is being used. */ | |
| public static boolean isGestureNavigateEnabled(Context context) { | |
| return Settings.Secure.getInt(context.getContentResolver(), | |
| Settings.Secure.NAVIGATION_MODE, -1) | |
| == NAV_BAR_MODE_GESTURAL; | |
| } | |
| /** Determines if a accessibility floating menu is being used. */ | |
| public static boolean isFloatingMenuEnabled(Context context) { | |
| return Settings.Secure.getInt(context.getContentResolver(), | |
| Settings.Secure.ACCESSIBILITY_BUTTON_MODE, /* def= */ -1) | |
| == ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU; | |
| } | |
| /** Determines if a touch explore is being used. */ | |
| public static boolean isTouchExploreEnabled(Context context) { | |
| final AccessibilityManager am = context.getSystemService(AccessibilityManager.class); | |
| return am.isTouchExplorationEnabled(); | |
| } | |
| /** | |
| * Gets the corresponding fragment type of a given accessibility service. | |
| * | |
| * @param accessibilityServiceInfo The accessibilityService's info | |
| * @return int from {@link AccessibilityServiceFragmentType} | |
| */ | |
| static @AccessibilityServiceFragmentType int getAccessibilityServiceFragmentType( | |
| AccessibilityServiceInfo accessibilityServiceInfo) { | |
| final int targetSdk = accessibilityServiceInfo.getResolveInfo() | |
| .serviceInfo.applicationInfo.targetSdkVersion; | |
| final boolean requestA11yButton = (accessibilityServiceInfo.flags | |
| & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0; | |
| if (targetSdk <= Build.VERSION_CODES.Q) { | |
| return AccessibilityServiceFragmentType.VOLUME_SHORTCUT_TOGGLE; | |
| } | |
| return requestA11yButton | |
| ? AccessibilityServiceFragmentType.INVISIBLE_TOGGLE | |
| : AccessibilityServiceFragmentType.TOGGLE; | |
| } | |
| /** | |
| * Gets the corresponding user shortcut type of a given accessibility service. | |
| * | |
| * @param context The current context. | |
| * @param componentName The component name that need to be checked existed in Settings. | |
| * @return The user shortcut type if component name existed in {@code UserShortcutType} string | |
| * Settings. | |
| */ | |
| static int getUserShortcutTypesFromSettings(Context context, | |
| @NonNull ComponentName componentName) { | |
| int shortcutTypes = UserShortcutType.DEFAULT; | |
| for (int shortcutType : AccessibilityUtil.SHORTCUTS_ORDER_IN_UI) { | |
| if (!android.provider.Flags.a11yStandaloneGestureEnabled()) { | |
| if ((shortcutType & GESTURE) == GESTURE) { | |
| continue; | |
| } | |
| } | |
| if (ShortcutUtils.isShortcutContained( | |
| context, shortcutType, componentName.flattenToString())) { | |
| shortcutTypes |= shortcutType; | |
| } | |
| } | |
| return shortcutTypes; | |
| } | |
| /** | |
| * Gets the width of the screen. | |
| * | |
| * @param context the current context. | |
| * @return the width of the screen in terms of pixels. | |
| */ | |
| public static int getScreenWidthPixels(Context context) { | |
| final Resources resources = context.getResources(); | |
| final int screenWidthDp = resources.getConfiguration().screenWidthDp; | |
| return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, screenWidthDp, | |
| resources.getDisplayMetrics())); | |
| } | |
| /** | |
| * Gets the height of the screen. | |
| * | |
| * @param context the current context. | |
| * @return the height of the screen in terms of pixels. | |
| */ | |
| public static int getScreenHeightPixels(Context context) { | |
| final Resources resources = context.getResources(); | |
| final int screenHeightDp = resources.getConfiguration().screenHeightDp; | |
| return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, screenHeightDp, | |
| resources.getDisplayMetrics())); | |
| } | |
| /** | |
| * Gets the bounds of the display window excluding the insets of the system bar and display | |
| * cut out. | |
| * | |
| * @param context the current context. | |
| * @return the bounds of the display window. | |
| */ | |
| public static Rect getDisplayBounds(Context context) { | |
| final WindowManager windowManager = context.getSystemService(WindowManager.class); | |
| final WindowMetrics metrics = windowManager.getCurrentWindowMetrics(); | |
| final Rect displayBounds = metrics.getBounds(); | |
| final Insets displayInsets = metrics.getWindowInsets().getInsetsIgnoringVisibility( | |
| systemBars() | displayCutout()); | |
| displayBounds.inset(displayInsets); | |
| return displayBounds; | |
| } | |
| /** | |
| * Indicates if the accessibility service belongs to a system App. | |
| * @param info AccessibilityServiceInfo | |
| * @return {@code true} if the App is a system App. | |
| */ | |
| public static boolean isSystemApp(@NonNull AccessibilityServiceInfo info) { | |
| return info.getResolveInfo().serviceInfo.applicationInfo.isSystemApp(); | |
| } | |
| /** | |
| * Bypasses the timeout restriction if volume key shortcut assigned. | |
| * | |
| * @param context the current context. | |
| */ | |
| public static void skipVolumeShortcutDialogTimeoutRestriction(Context context) { | |
| Settings.Secure.putInt(context.getContentResolver(), | |
| Settings.Secure.SKIP_ACCESSIBILITY_SHORTCUT_DIALOG_TIMEOUT_RESTRICTION, /* | |
| true */ 1); | |
| } | |
| /** | |
| * Assembles a localized string describing the provided shortcut types. | |
| */ | |
| public static CharSequence getShortcutSummaryList(Context context, int shortcutTypes) { | |
| final List<CharSequence> list = new ArrayList<>(); | |
| for (int shortcutType : AccessibilityUtil.SHORTCUTS_ORDER_IN_UI) { | |
| if (!android.provider.Flags.a11yStandaloneGestureEnabled() | |
| && (shortcutType & GESTURE) == GESTURE) { | |
| continue; | |
| } | |
| if (!com.android.server.accessibility.Flags | |
| .enableMagnificationMultipleFingerMultipleTapGesture() | |
| && (shortcutType & TWOFINGER_DOUBLETAP) == TWOFINGER_DOUBLETAP) { | |
| continue; | |
| } | |
| if ((shortcutTypes & shortcutType) == shortcutType) { | |
| list.add(switch (shortcutType) { | |
| case QUICK_SETTINGS -> context.getText( | |
| R.string.accessibility_feature_shortcut_setting_summary_quick_settings); | |
| case SOFTWARE -> getSoftwareShortcutSummary(context); | |
| case GESTURE -> context.getText( | |
| R.string.accessibility_shortcut_edit_summary_software_gesture); | |
| case HARDWARE -> context.getText( | |
| R.string.accessibility_shortcut_hardware_keyword); | |
| case TWOFINGER_DOUBLETAP -> context.getString( | |
| R.string.accessibility_shortcut_two_finger_double_tap_keyword, 2); | |
| case TRIPLETAP -> context.getText( | |
| R.string.accessibility_shortcut_triple_tap_keyword); | |
| default -> ""; | |
| }); | |
| } | |
| } | |
| list.sort(CharSequence::compare); | |
| return CaseMap.toTitle().wholeString().noLowercase().apply(Locale.getDefault(), /* iter= */ | |
| null, LocaleUtils.getConcatenatedString(list)); | |
| } | |
| @VisibleForTesting | |
| static CharSequence getSoftwareShortcutSummary(Context context) { | |
| if (android.provider.Flags.a11yStandaloneGestureEnabled()) { | |
| return context.getText(R.string.accessibility_shortcut_edit_summary_software); | |
| } | |
| int resId; | |
| if (AccessibilityUtil.isFloatingMenuEnabled(context)) { | |
| resId = R.string.accessibility_shortcut_edit_summary_software; | |
| } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) { | |
| resId = R.string.accessibility_shortcut_edit_summary_software_gesture; | |
| } else { | |
| resId = R.string.accessibility_shortcut_edit_summary_software; | |
| } | |
| return context.getText(resId); | |
| } | |
| } |