diff options
10 files changed, 663 insertions, 245 deletions
diff --git a/api/test-current.txt b/api/test-current.txt index acea9aada1a6..efb85387a743 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -4438,7 +4438,7 @@ package android.view.accessibility { public final class AccessibilityManager { method public void addAccessibilityServicesStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener, @Nullable android.os.Handler); - method @Nullable @RequiresPermission("android.permission.MANAGE_ACCESSIBILITY") public String getAccessibilityShortcutService(); + method @NonNull @RequiresPermission("android.permission.MANAGE_ACCESSIBILITY") public java.util.List<java.lang.String> getAccessibilityShortcutTargets(int); method @RequiresPermission("android.permission.MANAGE_ACCESSIBILITY") public void performAccessibilityShortcut(); method public void removeAccessibilityServicesStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener); } diff --git a/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java b/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java index f4cadfd8bbc1..d79740b49b3d 100644 --- a/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java +++ b/core/java/android/accessibilityservice/AccessibilityShortcutInfo.java @@ -141,6 +141,16 @@ public final class AccessibilityShortcutInfo { } /** + * The {@link ComponentName} of the accessibility shortcut target. + * + * @return The component name + */ + @NonNull + public ComponentName getComponentName() { + return mComponentName; + } + + /** * The localized summary of the accessibility shortcut target. * * @return The localized summary if available, and {@code null} if a summary diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index cc2884075c39..843f8e33c66a 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -117,7 +117,7 @@ public final class AccessibilityManager { * Activity action: Launch UI to manage which accessibility service or feature is assigned * to the navigation bar Accessibility button. * <p> - * Input: Nothing. + * Input: {@link #EXTRA_SHORTCUT_TYPE} is the shortcut type. * </p> * <p> * Output: Nothing. @@ -130,6 +130,42 @@ public final class AccessibilityManager { "com.android.internal.intent.action.CHOOSE_ACCESSIBILITY_BUTTON"; /** + * Used as an int extra field in {@link #ACTION_CHOOSE_ACCESSIBILITY_BUTTON} intent to specify + * the shortcut type. + * + * @hide + */ + public static final String EXTRA_SHORTCUT_TYPE = + "com.android.internal.intent.extra.SHORTCUT_TYPE"; + + /** + * Used as an int value for {@link #EXTRA_SHORTCUT_TYPE} to represent the accessibility button + * shortcut type. + * + * @hide + */ + public static final int ACCESSIBILITY_BUTTON = 0; + + /** + * Used as an int value for {@link #EXTRA_SHORTCUT_TYPE} to represent hardware key shortcut, + * such as volume key button. + * + * @hide + */ + public static final int ACCESSIBILITY_SHORTCUT_KEY = 1; + + /** + * Annotations for the shortcut type. + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + ACCESSIBILITY_BUTTON, + ACCESSIBILITY_SHORTCUT_KEY + }) + public @interface ShortcutType {} + + /** * Annotations for content flag of UI. * @hide */ @@ -1242,27 +1278,28 @@ public final class AccessibilityManager { } /** - * Get the component name of the service currently assigned to the accessibility shortcut. + * Returns the list of shortcut target names currently assigned to the given shortcut. * - * @return The flattened component name + * @param shortcutType The shortcut type. + * @return The list of shortcut target names. * @hide */ @TestApi @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY) - @Nullable - public String getAccessibilityShortcutService() { + @NonNull + public List<String> getAccessibilityShortcutTargets(@ShortcutType int shortcutType) { final IAccessibilityManager service; synchronized (mLock) { service = getServiceLocked(); } if (service != null) { try { - return service.getAccessibilityShortcutService(); + return service.getAccessibilityShortcutTargets(shortcutType); } catch (RemoteException re) { re.rethrowFromSystemServer(); } } - return null; + return Collections.emptyList(); } /** diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 023fda52a1b1..36515b3ba094 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -73,7 +73,7 @@ interface IAccessibilityManager { void performAccessibilityShortcut(); // Requires Manifest.permission.MANAGE_ACCESSIBILITY - String getAccessibilityShortcutService(); + List<String> getAccessibilityShortcutTargets(int shortcutType); // System process only boolean sendFingerprintGesture(int gestureKeyCode); diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java index 0b15cd06a7ea..3fdedc88fe53 100644 --- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java +++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java @@ -17,6 +17,7 @@ package com.android.internal.accessibility; import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG; +import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY; import static com.android.internal.util.ArrayUtils.convertToLongArray; @@ -34,6 +35,7 @@ import android.media.AudioAttributes; import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; +import android.os.Build; import android.os.Handler; import android.os.UserHandle; import android.os.Vibrator; @@ -52,11 +54,12 @@ import com.android.internal.R; import com.android.internal.util.function.pooled.PooledLambda; import java.util.Collections; +import java.util.List; import java.util.Locale; import java.util.Map; /** - * Class to help manage the accessibility shortcut + * Class to help manage the accessibility shortcut key */ public class AccessibilityShortcutController { private static final String TAG = "AccessibilityShortcutController"; @@ -66,6 +69,8 @@ public class AccessibilityShortcutController { new ComponentName("com.android.server.accessibility", "ColorInversion"); public static final ComponentName DALTONIZER_COMPONENT_NAME = new ComponentName("com.android.server.accessibility", "Daltonizer"); + public static final String MAGNIFICATION_CONTROLLER_NAME = + "com.android.server.accessibility.MagnificationController"; private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) @@ -84,26 +89,6 @@ public class AccessibilityShortcutController { public FrameworkObjectProvider mFrameworkObjectProvider = new FrameworkObjectProvider(); /** - * Get the component name string for the service or feature currently assigned to the - * accessiblity shortcut - * - * @param context A valid context - * @param userId The user ID of interest - * @return The flattened component name string of the service selected by the user, or the - * string for the default service if the user has not made a selection - */ - public static String getTargetServiceComponentNameString( - Context context, int userId) { - final String currentShortcutServiceId = Settings.Secure.getStringForUser( - context.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, - userId); - if (currentShortcutServiceId != null) { - return currentShortcutServiceId; - } - return context.getString(R.string.config_defaultAccessibilityService); - } - - /** * @return An immutable map from dummy component names to feature info for toggling a framework * feature */ @@ -163,7 +148,7 @@ public class AccessibilityShortcutController { /** * Check if the shortcut is available. * - * @param onLockScreen Whether or not the phone is currently locked. + * @param phoneLocked Whether or not the phone is currently locked. * * @return {@code true} if the shortcut is available */ @@ -172,8 +157,7 @@ public class AccessibilityShortcutController { } public void onSettingsChanged() { - final boolean haveValidService = - !TextUtils.isEmpty(getTargetServiceComponentNameString(mContext, mUserId)); + final boolean hasShortcutTarget = hasShortcutTarget(); final ContentResolver cr = mContext.getContentResolver(); final boolean enabled = Settings.Secure.getIntForUser( cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED, 1, mUserId) == 1; @@ -183,7 +167,7 @@ public class AccessibilityShortcutController { mEnabledOnLockScreen = Settings.Secure.getIntForUser( cr, Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN, dialogAlreadyShown, mUserId) == 1; - mIsShortcutEnabled = enabled && haveValidService; + mIsShortcutEnabled = enabled && hasShortcutTarget; } /** @@ -205,7 +189,6 @@ public class AccessibilityShortcutController { vibrator.vibrate(vibePattern, -1, VIBRATION_ATTRIBUTES); } - if (dialogAlreadyShown == 0) { // The first time, we show a warning rather than toggle the service to give the user a // chance to turn off this feature before stuff gets enabled. @@ -229,32 +212,44 @@ public class AccessibilityShortcutController { mAlertDialog.dismiss(); mAlertDialog = null; } - - // Show a toast alerting the user to what's happening - final String serviceName = getShortcutFeatureDescription(false /* no summary */); - if (serviceName == null) { - Slog.e(TAG, "Accessibility shortcut set to invalid service"); - return; - } - // For accessibility services, show a toast explaining what we're doing. - final AccessibilityServiceInfo serviceInfo = getInfoForTargetService(); - if (serviceInfo != null) { - String toastMessageFormatString = mContext.getString(isServiceEnabled(serviceInfo) - ? R.string.accessibility_shortcut_disabling_service - : R.string.accessibility_shortcut_enabling_service); - String toastMessage = String.format(toastMessageFormatString, serviceName); - Toast warningToast = mFrameworkObjectProvider.makeToastFromText( - mContext, toastMessage, Toast.LENGTH_LONG); - warningToast.getWindowParams().privateFlags |= - WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; - warningToast.show(); - } - + showToast(); mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext) .performAccessibilityShortcut(); } } + /** + * Show toast if current assigned shortcut target is an accessibility service and its target + * sdk version is less than or equal to Q, or greater than Q and does not request + * accessibility button. + */ + private void showToast() { + final AccessibilityServiceInfo serviceInfo = getInfoForTargetService(); + if (serviceInfo == null) { + return; + } + final String serviceName = getShortcutFeatureDescription(/* no summary */ false); + if (serviceName == null) { + return; + } + final boolean requestA11yButton = (serviceInfo.flags + & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0; + if (serviceInfo.getResolveInfo().serviceInfo.applicationInfo + .targetSdkVersion > Build.VERSION_CODES.Q && requestA11yButton) { + return; + } + // For accessibility services, show a toast explaining what we're doing. + String toastMessageFormatString = mContext.getString(isServiceEnabled(serviceInfo) + ? R.string.accessibility_shortcut_disabling_service + : R.string.accessibility_shortcut_enabling_service); + String toastMessage = String.format(toastMessageFormatString, serviceName); + Toast warningToast = mFrameworkObjectProvider.makeToastFromText( + mContext, toastMessage, Toast.LENGTH_LONG); + warningToast.getWindowParams().privateFlags |= + WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; + warningToast.show(); + } + private AlertDialog createShortcutWarningDialog(int userId) { final String serviceDescription = getShortcutFeatureDescription(true /* Include summary */); @@ -288,25 +283,21 @@ public class AccessibilityShortcutController { } private AccessibilityServiceInfo getInfoForTargetService() { - final String currentShortcutServiceString = getTargetServiceComponentNameString( - mContext, UserHandle.USER_CURRENT); - if (currentShortcutServiceString == null) { + final ComponentName targetComponentName = getShortcutTargetComponentName(); + if (targetComponentName == null) { return null; } AccessibilityManager accessibilityManager = mFrameworkObjectProvider.getAccessibilityManagerInstance(mContext); return accessibilityManager.getInstalledServiceInfoWithComponentName( - ComponentName.unflattenFromString(currentShortcutServiceString)); + targetComponentName); } private String getShortcutFeatureDescription(boolean includeSummary) { - final String currentShortcutServiceString = getTargetServiceComponentNameString( - mContext, UserHandle.USER_CURRENT); - if (currentShortcutServiceString == null) { + final ComponentName targetComponentName = getShortcutTargetComponentName(); + if (targetComponentName == null) { return null; } - final ComponentName targetComponentName = - ComponentName.unflattenFromString(currentShortcutServiceString); final ToggleableFrameworkFeatureInfo frameworkFeatureInfo = getFrameworkShortcutFeaturesMap().get(targetComponentName); if (frameworkFeatureInfo != null) { @@ -372,6 +363,36 @@ public class AccessibilityShortcutController { } /** + * Returns {@code true} if any shortcut targets were assigned to accessibility shortcut key. + */ + private boolean hasShortcutTarget() { + // AccessibilityShortcutController is initialized earlier than AccessibilityManagerService. + // AccessibilityManager#getAccessibilityShortcutTargets may not return correct shortcut + // targets during boot. Needs to read settings directly here. + String shortcutTargets = Settings.Secure.getStringForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, mUserId); + if (TextUtils.isEmpty(shortcutTargets)) { + shortcutTargets = mContext.getString(R.string.config_defaultAccessibilityService); + } + return !TextUtils.isEmpty(shortcutTargets); + } + + /** + * Gets the component name of the shortcut target. + * + * @return The component name, or null if it's assigned by multiple targets. + */ + private ComponentName getShortcutTargetComponentName() { + final List<String> shortcutTargets = mFrameworkObjectProvider + .getAccessibilityManagerInstance(mContext) + .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY); + if (shortcutTargets.size() != 1) { + return null; + } + return ComponentName.unflattenFromString(shortcutTargets.get(0)); + } + + /** * Class to wrap TextToSpeech for shortcut dialog spoken feedback. */ private class TtsPrompt implements TextToSpeech.OnInitListener { diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java index 7b4054348642..82854e5b8a9d 100644 --- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java +++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java @@ -20,6 +20,7 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_DIALOG_SHO import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED; import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN; import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE; +import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; @@ -49,8 +50,10 @@ import android.content.DialogInterface; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; import android.content.res.Resources; import android.media.Ringtone; +import android.os.Build; import android.os.Handler; import android.os.Message; import android.os.Vibrator; @@ -157,9 +160,12 @@ public class AccessibilityShortcutControllerTest { when(mFrameworkObjectProvider.getRingtone(eq(mContext), any())).thenReturn(mRingtone); when(mResources.getString(anyInt())).thenReturn("Howdy %s"); + when(mResources.getString(R.string.config_defaultAccessibilityService)).thenReturn(null); when(mResources.getIntArray(anyInt())).thenReturn(VIBRATOR_PATTERN_INT); ResolveInfo resolveInfo = mock(ResolveInfo.class); + resolveInfo.serviceInfo = mock(ServiceInfo.class); + resolveInfo.serviceInfo.applicationInfo = mApplicationInfo; when(resolveInfo.loadLabel(anyObject())).thenReturn("Service name"); when(mServiceInfo.getResolveInfo()).thenReturn(resolveInfo); when(mServiceInfo.getComponentName()) @@ -200,42 +206,47 @@ public class AccessibilityShortcutControllerTest { } @Test - public void testShortcutAvailable_enabledButNoServiceWhenCreated_shouldReturnFalse() { + public void testShortcutAvailable_enabledButNoServiceWhenCreated_shouldReturnFalse() + throws Exception { configureNoShortcutService(); configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); assertFalse(getController().isAccessibilityShortcutAvailable(false)); } @Test - public void testShortcutAvailable_enabledWithValidServiceWhenCreated_shouldReturnTrue() { + public void testShortcutAvailable_enabledWithValidServiceWhenCreated_shouldReturnTrue() + throws Exception { configureValidShortcutService(); configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); assertTrue(getController().isAccessibilityShortcutAvailable(false)); } @Test - public void testShortcutAvailable_disabledWithValidServiceWhenCreated_shouldReturnFalse() { + public void testShortcutAvailable_disabledWithValidServiceWhenCreated_shouldReturnFalse() + throws Exception { configureValidShortcutService(); configureShortcutEnabled(DISABLED_BUT_LOCK_SCREEN_ON); assertFalse(getController().isAccessibilityShortcutAvailable(false)); } @Test - public void testShortcutAvailable_onLockScreenButDisabledThere_shouldReturnFalse() { + public void testShortcutAvailable_onLockScreenButDisabledThere_shouldReturnFalse() + throws Exception { configureValidShortcutService(); configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); assertFalse(getController().isAccessibilityShortcutAvailable(true)); } @Test - public void testShortcutAvailable_onLockScreenAndEnabledThere_shouldReturnTrue() { + public void testShortcutAvailable_onLockScreenAndEnabledThere_shouldReturnTrue() + throws Exception { configureValidShortcutService(); configureShortcutEnabled(ENABLED_INCLUDING_LOCK_SCREEN); assertTrue(getController().isAccessibilityShortcutAvailable(true)); } @Test - public void testShortcutAvailable_onLockScreenAndLockScreenPreferenceUnset() { + public void testShortcutAvailable_onLockScreenAndLockScreenPreferenceUnset() throws Exception { // When the user hasn't specified a lock screen preference, we allow from the lock screen // as long as the user has agreed to enable the shortcut configureValidShortcutService(); @@ -249,17 +260,19 @@ public class AccessibilityShortcutControllerTest { } @Test - public void testShortcutAvailable_whenServiceIdBecomesNull_shouldReturnFalse() { + public void testShortcutAvailable_whenServiceIdBecomesNull_shouldReturnFalse() + throws Exception { configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); configureValidShortcutService(); AccessibilityShortcutController accessibilityShortcutController = getController(); - Settings.Secure.putString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, ""); + configureNoShortcutService(); accessibilityShortcutController.onSettingsChanged(); assertFalse(accessibilityShortcutController.isAccessibilityShortcutAvailable(false)); } @Test - public void testShortcutAvailable_whenServiceIdBecomesNonNull_shouldReturnTrue() { + public void testShortcutAvailable_whenServiceIdBecomesNonNull_shouldReturnTrue() + throws Exception { configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); configureNoShortcutService(); AccessibilityShortcutController accessibilityShortcutController = getController(); @@ -269,7 +282,8 @@ public class AccessibilityShortcutControllerTest { } @Test - public void testShortcutAvailable_whenShortcutBecomesDisabled_shouldReturnFalse() { + public void testShortcutAvailable_whenShortcutBecomesDisabled_shouldReturnFalse() + throws Exception { configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); configureValidShortcutService(); AccessibilityShortcutController accessibilityShortcutController = getController(); @@ -279,7 +293,8 @@ public class AccessibilityShortcutControllerTest { } @Test - public void testShortcutAvailable_whenShortcutBecomesEnabled_shouldReturnTrue() { + public void testShortcutAvailable_whenShortcutBecomesEnabled_shouldReturnTrue() + throws Exception { configureShortcutEnabled(DISABLED); configureValidShortcutService(); AccessibilityShortcutController accessibilityShortcutController = getController(); @@ -289,7 +304,8 @@ public class AccessibilityShortcutControllerTest { } @Test - public void testShortcutAvailable_whenLockscreenBecomesDisabled_shouldReturnFalse() { + public void testShortcutAvailable_whenLockscreenBecomesDisabled_shouldReturnFalse() + throws Exception { configureShortcutEnabled(ENABLED_INCLUDING_LOCK_SCREEN); configureValidShortcutService(); AccessibilityShortcutController accessibilityShortcutController = getController(); @@ -299,7 +315,8 @@ public class AccessibilityShortcutControllerTest { } @Test - public void testShortcutAvailable_whenLockscreenBecomesEnabled_shouldReturnTrue() { + public void testShortcutAvailable_whenLockscreenBecomesEnabled_shouldReturnTrue() + throws Exception { configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); configureValidShortcutService(); AccessibilityShortcutController accessibilityShortcutController = getController(); @@ -370,7 +387,7 @@ public class AccessibilityShortcutControllerTest { } @Test - public void testClickingDisableButtonInDialog_shouldClearShortcutId() { + public void testClickingDisableButtonInDialog_shouldClearShortcutId() throws Exception { configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); configureValidShortcutService(); Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 0); @@ -458,7 +475,22 @@ public class AccessibilityShortcutControllerTest { } @Test - public void testOnAccessibilityShortcut_showsWarningDialog_shouldTtsSpokenPrompt() { + public void testOnAccessibilityShortcut_sdkGreaterThanQ_reqA11yButton_callsServiceWithNoToast() + throws Exception { + configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); + configureValidShortcutService(); + configureApplicationTargetSdkVersion(Build.VERSION_CODES.R); + configureRequestAccessibilityButton(); + Settings.Secure.putInt(mContentResolver, ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN, 1); + getController().performAccessibilityShortcut(); + + verifyZeroInteractions(mToast); + verify(mAccessibilityManagerService).performAccessibilityShortcut(); + } + + @Test + public void testOnAccessibilityShortcut_showsWarningDialog_shouldTtsSpokenPrompt() + throws Exception { configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); configureValidShortcutService(); configureTtsSpokenPromptEnabled(); @@ -482,7 +514,8 @@ public class AccessibilityShortcutControllerTest { } @Test - public void testOnAccessibilityShortcut_showsWarningDialog_ttsInitFail_noSpokenPrompt() { + public void testOnAccessibilityShortcut_showsWarningDialog_ttsInitFail_noSpokenPrompt() + throws Exception { configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN); configureValidShortcutService(); configureTtsSpokenPromptEnabled(); @@ -500,19 +533,28 @@ public class AccessibilityShortcutControllerTest { verify(mRingtone).play(); } - private void configureNoShortcutService() { + private void configureNoShortcutService() throws Exception { + when(mAccessibilityManagerService + .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY)) + .thenReturn(Collections.emptyList()); Settings.Secure.putString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, ""); } - private void configureValidShortcutService() { + private void configureValidShortcutService() throws Exception { + when(mAccessibilityManagerService + .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY)) + .thenReturn(Collections.singletonList(SERVICE_NAME_STRING)); Settings.Secure.putString( mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, SERVICE_NAME_STRING); } - private void configureFirstFrameworkFeature() { + private void configureFirstFrameworkFeature() throws Exception { ComponentName featureComponentName = (ComponentName) AccessibilityShortcutController.getFrameworkShortcutFeaturesMap() .keySet().toArray()[0]; + when(mAccessibilityManagerService + .getAccessibilityShortcutTargets(ACCESSIBILITY_SHORTCUT_KEY)) + .thenReturn(Collections.singletonList(featureComponentName.flattenToString())); Settings.Secure.putString(mContentResolver, ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, featureComponentName.flattenToString()); } @@ -552,6 +594,15 @@ public class AccessibilityShortcutControllerTest { .FLAG_REQUEST_SHORTCUT_WARNING_DIALOG_SPOKEN_FEEDBACK; } + private void configureRequestAccessibilityButton() { + mServiceInfo.flags |= AccessibilityServiceInfo + .FLAG_REQUEST_ACCESSIBILITY_BUTTON; + } + + private void configureApplicationTargetSdkVersion(int versionCode) { + mApplicationInfo.targetSdkVersion = versionCode; + } + private void configureHandlerCallbackInvocation() { doAnswer((InvocationOnMock invocation) -> { Message m = (Message) invocation.getArguments()[0]; diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 7fdd83bebbcd..6a6e2b2f3467 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -16,6 +16,11 @@ package com.android.server.accessibility; +import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON; +import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY; +import static android.view.accessibility.AccessibilityManager.ShortcutType; + +import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; import static com.android.internal.util.FunctionalUtils.ignoreRemoteException; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; @@ -31,6 +36,7 @@ import android.app.ActivityOptions; import android.app.AlertDialog; import android.app.PendingIntent; import android.appwidget.AppWidgetManagerInternal; +import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; @@ -70,6 +76,7 @@ import android.provider.Settings; import android.provider.SettingsStringUtil.SettingStringHelper; import android.text.TextUtils; import android.text.TextUtils.SimpleStringSplitter; +import android.util.ArraySet; import android.util.IntArray; import android.util.Slog; import android.util.SparseArray; @@ -113,9 +120,9 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.function.Consumer; +import java.util.function.Function; /** * This class is instantiated by the system as a system level service and can be @@ -754,6 +761,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub userState.setTouchExplorationEnabledLocked(touchExplorationEnabled); userState.setDisplayMagnificationEnabledLocked(false); userState.setNavBarMagnificationEnabledLocked(false); + userState.disableShortcutMagnificationLocked(); + userState.setAutoclickEnabledLocked(false); userState.mEnabledServices.clear(); userState.mEnabledServices.add(service); @@ -1072,6 +1081,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } + // TODO(a11y shortcut): Remove this function and Use #performAccessibilityShortcutInternal( + // ACCESSIBILITY_BUTTON) instead, after the new Settings shortcut Ui merged. private void notifyAccessibilityButtonClickedLocked(int displayId) { final AccessibilityUserState state = getCurrentUserStateLocked(); @@ -1105,8 +1116,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (state.getServiceAssignedToAccessibilityButtonLocked() == null && !state.isNavBarMagnificationAssignedToAccessibilityButtonLocked()) { mMainHandler.sendMessage(obtainMessage( - AccessibilityManagerService::showAccessibilityButtonTargetSelection, this, - displayId)); + AccessibilityManagerService::showAccessibilityTargetsSelection, this, + displayId, ACCESSIBILITY_BUTTON)); } else if (state.isNavBarMagnificationEnabledLocked() && state.isNavBarMagnificationAssignedToAccessibilityButtonLocked()) { mMainHandler.sendMessage(obtainMessage( @@ -1125,8 +1136,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } // The user may have turned off the assigned service or feature mMainHandler.sendMessage(obtainMessage( - AccessibilityManagerService::showAccessibilityButtonTargetSelection, this, - displayId)); + AccessibilityManagerService::showAccessibilityTargetsSelection, this, + displayId, ACCESSIBILITY_BUTTON)); } } @@ -1138,13 +1149,27 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } - private void showAccessibilityButtonTargetSelection(int displayId) { + private void showAccessibilityTargetsSelection(int displayId, + @ShortcutType int shortcutType) { Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); final Bundle bundle = ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle(); + bundle.putInt(AccessibilityManager.EXTRA_SHORTCUT_TYPE, shortcutType); mContext.startActivityAsUser(intent, bundle, UserHandle.of(mCurrentUserId)); } + private void launchShortcutTargetActivity(int displayId, ComponentName name) { + final Intent intent = new Intent(); + final Bundle bundle = ActivityOptions.makeBasic().setLaunchDisplayId(displayId).toBundle(); + intent.setComponent(name); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + try { + mContext.startActivityAsUser(intent, bundle, UserHandle.of(mCurrentUserId)); + } catch (ActivityNotFoundException ignore) { + // ignore the exception + } + } + private void notifyAccessibilityButtonVisibilityChangedLocked(boolean available) { final AccessibilityUserState state = getCurrentUserStateLocked(); mIsAccessibilityButtonShown = available; @@ -1353,9 +1378,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub */ private void readComponentNamesFromSettingLocked(String settingName, int userId, Set<ComponentName> outComponentNames) { - String settingValue = Settings.Secure.getStringForUser(mContext.getContentResolver(), - settingName, userId); - readComponentNamesFromStringLocked(settingValue, outComponentNames, false); + readColonDelimitedSettingToSet(settingName, userId, outComponentNames, + str -> ComponentName.unflattenFromString(str)); } /** @@ -1370,34 +1394,57 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private void readComponentNamesFromStringLocked(String names, Set<ComponentName> outComponentNames, boolean doMerge) { + readColonDelimitedStringToSet(names, outComponentNames, doMerge, + str -> ComponentName.unflattenFromString(str)); + } + + @Override + public void persistComponentNamesToSettingLocked(String settingName, + Set<ComponentName> componentNames, int userId) { + persistColonDelimitedSetToSettingLocked(settingName, userId, componentNames, + componentName -> componentName.flattenToShortString()); + } + + private <T> void readColonDelimitedSettingToSet(String settingName, int userId, Set<T> outSet, + Function<String, T> toItem) { + final String settingValue = Settings.Secure.getStringForUser(mContext.getContentResolver(), + settingName, userId); + readColonDelimitedStringToSet(settingValue, outSet, false, toItem); + } + + private <T> void readColonDelimitedStringToSet(String names, Set<T> outSet, boolean doMerge, + Function<String, T> toItem) { if (!doMerge) { - outComponentNames.clear(); + outSet.clear(); } - if (names != null) { - TextUtils.SimpleStringSplitter splitter = mStringColonSplitter; + if (!TextUtils.isEmpty(names)) { + final TextUtils.SimpleStringSplitter splitter = mStringColonSplitter; splitter.setString(names); while (splitter.hasNext()) { - String str = splitter.next(); - if (str == null || str.length() <= 0) { + final String str = splitter.next(); + if (TextUtils.isEmpty(str)) { continue; } - ComponentName enabledService = ComponentName.unflattenFromString(str); - if (enabledService != null) { - outComponentNames.add(enabledService); + final T item = toItem.apply(str); + if (item != null) { + outSet.add(item); } } } } - @Override - public void persistComponentNamesToSettingLocked(String settingName, - Set<ComponentName> componentNames, int userId) { - StringBuilder builder = new StringBuilder(); - for (ComponentName componentName : componentNames) { + private <T> void persistColonDelimitedSetToSettingLocked(String settingName, int userId, + Set<T> set, Function<T, String> toString) { + final StringBuilder builder = new StringBuilder(); + for (T item : set) { + final String str = (item != null ? toString.apply(item) : null); + if (TextUtils.isEmpty(str)) { + continue; + } if (builder.length() > 0) { builder.append(COMPONENT_NAME_SEPARATOR); } - builder.append(componentName.flattenToShortString()); + builder.append(str); } final long identity = Binder.clearCallingIdentity(); try { @@ -1537,7 +1584,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (userState.isDisplayMagnificationEnabledLocked()) { flags |= AccessibilityInputFilter.FLAG_FEATURE_SCREEN_MAGNIFIER; } - if (userState.isNavBarMagnificationEnabledLocked()) { + if (userState.isNavBarMagnificationEnabledLocked() + || userState.isShortcutKeyMagnificationEnabledLocked()) { flags |= AccessibilityInputFilter.FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER; } if (userHasMagnificationServicesLocked(userState)) { @@ -1647,7 +1695,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mInitialized = true; updateLegacyCapabilitiesLocked(userState); updateServicesLocked(userState); - updateAccessibilityShortcutLocked(userState); updateWindowsForAccessibilityCallbackLocked(userState); updateFilterKeyEventsLocked(userState); updateTouchExplorationLocked(userState); @@ -1657,6 +1704,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub scheduleUpdateInputFilter(userState); updateRelevantEventsLocked(userState); scheduleUpdateClientsIfNeededLocked(userState); + updateAccessibilityShortcutKeyTargetsLocked(userState); updateAccessibilityButtonTargetsLocked(userState); } @@ -1751,7 +1799,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub somethingChanged |= readHighTextContrastEnabledSettingLocked(userState); somethingChanged |= readMagnificationEnabledSettingsLocked(userState); somethingChanged |= readAutoclickEnabledSettingLocked(userState); - somethingChanged |= readAccessibilityShortcutSettingLocked(userState); + somethingChanged |= readAccessibilityShortcutKeySettingLocked(userState); somethingChanged |= readAccessibilityButtonSettingsLocked(userState); somethingChanged |= readUserRecommendedUiTimeoutSettingsLocked(userState); return somethingChanged; @@ -1847,57 +1895,43 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } - private boolean readAccessibilityShortcutSettingLocked(AccessibilityUserState userState) { - String componentNameToEnableString = AccessibilityShortcutController - .getTargetServiceComponentNameString(mContext, userState.mUserId); - if ((componentNameToEnableString == null) || componentNameToEnableString.isEmpty()) { - if (userState.getServiceToEnableWithShortcutLocked() == null) { - return false; + private boolean readAccessibilityShortcutKeySettingLocked(AccessibilityUserState userState) { + final Set<String> targetsFromSetting = new ArraySet<>(); + readColonDelimitedSettingToSet(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, + userState.mUserId, targetsFromSetting, str -> str); + if (targetsFromSetting.isEmpty()) { + // Fall back to device's default a11y service. + final String defaultService = mContext.getString( + R.string.config_defaultAccessibilityService); + if (!TextUtils.isEmpty(defaultService)) { + targetsFromSetting.add(defaultService); } - userState.setServiceToEnableWithShortcutLocked(null); - return true; } - ComponentName componentNameToEnable = - ComponentName.unflattenFromString(componentNameToEnableString); - if ((componentNameToEnable != null) - && componentNameToEnable.equals(userState.getServiceToEnableWithShortcutLocked())) { + + final Set<String> currentTargets = + userState.getShortcutTargetsLocked(ACCESSIBILITY_SHORTCUT_KEY); + if (targetsFromSetting.equals(currentTargets)) { return false; } - - userState.setServiceToEnableWithShortcutLocked(componentNameToEnable); + currentTargets.clear(); + currentTargets.addAll(targetsFromSetting); scheduleNotifyClientsOfServicesStateChangeLocked(userState); return true; } private boolean readAccessibilityButtonSettingsLocked(AccessibilityUserState userState) { - String componentId = Settings.Secure.getStringForUser(mContext.getContentResolver(), - Settings.Secure.ACCESSIBILITY_BUTTON_TARGET_COMPONENT, userState.mUserId); - if (TextUtils.isEmpty(componentId)) { - if ((userState.getServiceAssignedToAccessibilityButtonLocked() == null) - && !userState.isNavBarMagnificationAssignedToAccessibilityButtonLocked()) { - return false; - } - userState.setServiceAssignedToAccessibilityButtonLocked(null); - userState.setNavBarMagnificationAssignedToAccessibilityButtonLocked(false); - return true; - } - - if (componentId.equals(MagnificationController.class.getName())) { - if (userState.isNavBarMagnificationAssignedToAccessibilityButtonLocked()) { - return false; - } - userState.setServiceAssignedToAccessibilityButtonLocked(null); - userState.setNavBarMagnificationAssignedToAccessibilityButtonLocked(true); - return true; - } + final Set<String> targetsFromSetting = new ArraySet<>(); + readColonDelimitedSettingToSet(Settings.Secure.ACCESSIBILITY_BUTTON_TARGET_COMPONENT, + userState.mUserId, targetsFromSetting, str -> str); - ComponentName componentName = ComponentName.unflattenFromString(componentId); - if (Objects.equals(componentName, - userState.getServiceAssignedToAccessibilityButtonLocked())) { + final Set<String> currentTargets = + userState.getShortcutTargetsLocked(ACCESSIBILITY_BUTTON); + if (targetsFromSetting.equals(currentTargets)) { return false; } - userState.setServiceAssignedToAccessibilityButtonLocked(componentName); - userState.setNavBarMagnificationAssignedToAccessibilityButtonLocked(false); + currentTargets.clear(); + currentTargets.addAll(targetsFromSetting); + scheduleNotifyClientsOfServicesStateChangeLocked(userState); return true; } @@ -1921,34 +1955,33 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } /** - * Check if the service that will be enabled by the shortcut is installed. If it isn't, - * clear the value and the associated setting so a sideloaded service can't spoof the - * package name of the default service. - * - * @param userState + * Check if the targets that will be enabled by the accessibility shortcut key is installed. + * If it isn't, remove it from the list and associated setting so a side loaded service can't + * spoof the package name of the default service. */ - private void updateAccessibilityShortcutLocked(AccessibilityUserState userState) { - if (userState.getServiceToEnableWithShortcutLocked() == null) { + private void updateAccessibilityShortcutKeyTargetsLocked(AccessibilityUserState userState) { + final Set<String> currentTargets = + userState.getShortcutTargetsLocked(ACCESSIBILITY_SHORTCUT_KEY); + final int lastSize = currentTargets.size(); + if (lastSize == 0) { return; } - boolean shortcutServiceIsInstalled = - AccessibilityShortcutController.getFrameworkShortcutFeaturesMap() - .containsKey(userState.getServiceToEnableWithShortcutLocked()); - for (int i = 0; !shortcutServiceIsInstalled && (i < userState.mInstalledServices.size()); - i++) { - if (userState.mInstalledServices.get(i).getComponentName() - .equals(userState.getServiceToEnableWithShortcutLocked())) { - shortcutServiceIsInstalled = true; - } + currentTargets.removeIf( + name -> !userState.isShortcutTargetInstalledLocked(name)); + if (lastSize == currentTargets.size()) { + return; } - if (!shortcutServiceIsInstalled) { - userState.setServiceToEnableWithShortcutLocked(null); + + // Update setting key with new value. + persistColonDelimitedSetToSettingLocked( + Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, + userState.mUserId, currentTargets, str -> str); + scheduleNotifyClientsOfServicesStateChangeLocked(userState); + + // Disable accessibility shortcut key if there's no shortcut installed. + if (currentTargets.isEmpty()) { final long identity = Binder.clearCallingIdentity(); try { - Settings.Secure.putStringForUser(mContext.getContentResolver(), - Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, null, - userState.mUserId); - Settings.Secure.putIntForUser(mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_SHORTCUT_ENABLED, 0, userState.mUserId); } finally { @@ -2004,7 +2037,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub // displays in one display. It's not a real display and there's no input events for it. final ArrayList<Display> displays = getValidDisplayList(); if (userState.isDisplayMagnificationEnabledLocked() - || userState.isNavBarMagnificationEnabledLocked()) { + || userState.isNavBarMagnificationEnabledLocked() + || userState.isShortcutKeyMagnificationEnabledLocked()) { for (int i = 0; i < displays.size(); i++) { final Display display = displays.get(i); getMagnificationController().register(display.getDisplayId()); @@ -2088,7 +2122,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } + /** + * 1) Update accessibility button availability to accessibility services. + * 2) Check if the targets that will be enabled by the accessibility button is installed. + * If it isn't, remove it from the list and associated setting so a side loaded service can't + * spoof the package name of the default service. + */ private void updateAccessibilityButtonTargetsLocked(AccessibilityUserState userState) { + // Update accessibility button availability. for (int i = userState.mBoundServices.size() - 1; i >= 0; i--) { final AccessibilityServiceConnection service = userState.mBoundServices.get(i); if (service.mRequestAccessibilityButton) { @@ -2096,6 +2137,24 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub service.isAccessibilityButtonAvailableLocked(userState)); } } + + final Set<String> currentTargets = + userState.getShortcutTargetsLocked(ACCESSIBILITY_BUTTON); + final int lastSize = currentTargets.size(); + if (lastSize == 0) { + return; + } + currentTargets.removeIf( + name -> !userState.isShortcutTargetInstalledLocked(name)); + if (lastSize == currentTargets.size()) { + return; + } + + // Update setting key with new value. + persistColonDelimitedSetToSettingLocked( + Settings.Secure.ACCESSIBILITY_BUTTON_TARGET_COMPONENT, + userState.mUserId, currentTargets, str -> str); + scheduleNotifyClientsOfServicesStateChangeLocked(userState); } private void updateRecommendedUiTimeoutLocked(AccessibilityUserState userState) { @@ -2156,7 +2215,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } /** - * AIDL-exposed method to be called when the accessibility shortcut is enabled. Requires + * AIDL-exposed method to be called when the accessibility shortcut key is enabled. Requires * permission to write secure settings, since someone with that permission can enable * accessibility services themselves. */ @@ -2168,52 +2227,177 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub throw new SecurityException( "performAccessibilityShortcut requires the MANAGE_ACCESSIBILITY permission"); } + mMainHandler.sendMessage(obtainMessage( + AccessibilityManagerService::performAccessibilityShortcutInternal, this, + Display.DEFAULT_DISPLAY, ACCESSIBILITY_SHORTCUT_KEY)); + } + + /** + * Perform the accessibility shortcut action. + * + * @param shortcutType The shortcut type. + * @param displayId The display id of the accessibility button. + */ + private void performAccessibilityShortcutInternal(int displayId, + @ShortcutType int shortcutType) { + final List<String> shortcutTargets = getAccessibilityShortcutTargetsInternal(shortcutType); + if (shortcutTargets.isEmpty()) { + Slog.d(LOG_TAG, "No target to perform shortcut, shortcutType=" + shortcutType); + return; + } + // In case there are many targets assigned to the given shortcut. + if (shortcutTargets.size() > 1) { + showAccessibilityTargetsSelection(displayId, shortcutType); + return; + } + final String targetName = shortcutTargets.get(0); + // In case user assigned magnification to the given shortcut. + if (targetName.equals(MAGNIFICATION_CONTROLLER_NAME)) { + sendAccessibilityButtonToInputFilter(displayId); + return; + } + final ComponentName targetComponentName = ComponentName.unflattenFromString(targetName); + if (targetComponentName == null) { + Slog.d(LOG_TAG, "Perform shortcut failed, invalid target name:" + targetName); + return; + } + // In case user assigned an accessibility framework feature to the given shortcut. + if (performAccessibilityFrameworkFeature(targetComponentName)) { + return; + } + // In case user assigned an accessibility shortcut target to the given shortcut. + if (performAccessibilityShortcutTargetActivity(displayId, targetComponentName)) { + return; + } + // in case user assigned an accessibility service to the given shortcut. + if (performAccessibilityShortcutTargetService( + displayId, shortcutType, targetComponentName)) { + return; + } + } + + private boolean performAccessibilityFrameworkFeature(ComponentName assignedTarget) { final Map<ComponentName, ToggleableFrameworkFeatureInfo> frameworkFeatureMap = AccessibilityShortcutController.getFrameworkShortcutFeaturesMap(); - synchronized(mLock) { - final AccessibilityUserState userState = getUserStateLocked(mCurrentUserId); - final ComponentName serviceName = userState.getServiceToEnableWithShortcutLocked(); - if (serviceName == null) { - return; - } - if (frameworkFeatureMap.containsKey(serviceName)) { - // Toggle the requested framework feature - ToggleableFrameworkFeatureInfo featureInfo = frameworkFeatureMap.get(serviceName); - SettingStringHelper setting = new SettingStringHelper(mContext.getContentResolver(), - featureInfo.getSettingKey(), mCurrentUserId); - // Assuming that the default state will be to have the feature off - if (!TextUtils.equals(featureInfo.getSettingOnValue(), setting.read())) { - setting.write(featureInfo.getSettingOnValue()); - } else { - setting.write(featureInfo.getSettingOffValue()); + if (!frameworkFeatureMap.containsKey(assignedTarget)) { + return false; + } + // Toggle the requested framework feature + final ToggleableFrameworkFeatureInfo featureInfo = frameworkFeatureMap.get(assignedTarget); + final SettingStringHelper setting = new SettingStringHelper(mContext.getContentResolver(), + featureInfo.getSettingKey(), mCurrentUserId); + // Assuming that the default state will be to have the feature off + if (!TextUtils.equals(featureInfo.getSettingOnValue(), setting.read())) { + setting.write(featureInfo.getSettingOnValue()); + } else { + setting.write(featureInfo.getSettingOffValue()); + } + return true; + } + + private boolean performAccessibilityShortcutTargetActivity(int displayId, + ComponentName assignedTarget) { + synchronized (mLock) { + final AccessibilityUserState userState = getCurrentUserStateLocked(); + for (int i = 0; i < userState.mInstalledShortcuts.size(); i++) { + final AccessibilityShortcutInfo shortcutInfo = userState.mInstalledShortcuts.get(i); + if (!shortcutInfo.getComponentName().equals(assignedTarget)) { + continue; } + launchShortcutTargetActivity(displayId, assignedTarget); + return true; } - final long identity = Binder.clearCallingIdentity(); - try { - if (userState.mComponentNameToServiceMap.get(serviceName) == null) { - enableAccessibilityServiceLocked(serviceName, mCurrentUserId); + } + return false; + } + + /** + * Perform accessibility service shortcut action. + * + * 1) For {@link AccessibilityManager#ACCESSIBILITY_BUTTON} type and services targeting sdk + * version <= Q: callbacks to accessibility service if service is bounded and requests + * accessibility button. + * 2) For {@link AccessibilityManager#ACCESSIBILITY_SHORTCUT_KEY} type and service targeting sdk + * version <= Q: turns on / off the accessibility service. + * 3) For services targeting sdk version > Q: + * a) Turns on / off the accessibility service, if service does not request accessibility + * button. + * b) Callbacks to accessibility service if service is bounded and requests accessibility + * button. + */ + private boolean performAccessibilityShortcutTargetService(int displayId, + @ShortcutType int shortcutType, ComponentName assignedTarget) { + synchronized (mLock) { + final AccessibilityUserState userState = getCurrentUserStateLocked(); + final AccessibilityServiceInfo installedServiceInfo = + userState.getInstalledServiceInfoLocked(assignedTarget); + if (installedServiceInfo == null) { + Slog.d(LOG_TAG, "Perform shortcut failed, invalid component name:" + + assignedTarget); + return false; + } + + final AccessibilityServiceConnection serviceConnection = + userState.getServiceConnectionLocked(assignedTarget); + final int targetSdk = installedServiceInfo.getResolveInfo() + .serviceInfo.applicationInfo.targetSdkVersion; + final boolean requestA11yButton = (installedServiceInfo.flags + & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0; + // Turns on / off the accessibility service + if ((targetSdk <= Build.VERSION_CODES.Q && shortcutType == ACCESSIBILITY_SHORTCUT_KEY) + || (targetSdk > Build.VERSION_CODES.Q && !requestA11yButton)) { + if (serviceConnection == null) { + enableAccessibilityServiceLocked(assignedTarget, mCurrentUserId); } else { - disableAccessibilityServiceLocked(serviceName, mCurrentUserId); + disableAccessibilityServiceLocked(assignedTarget, mCurrentUserId); } - } finally { - Binder.restoreCallingIdentity(identity); + return true; + } + // Callbacks to a11y service if it's bounded and requests a11y button. + if (serviceConnection == null + || !userState.mBoundServices.contains(serviceConnection) + || !serviceConnection.mRequestAccessibilityButton) { + Slog.d(LOG_TAG, "Perform shortcut failed, service is not ready:" + + assignedTarget); + return false; } + serviceConnection.notifyAccessibilityButtonClickedLocked(displayId); + return true; } - }; + } @Override - public String getAccessibilityShortcutService() { - if (mContext.checkCallingPermission(Manifest.permission.MANAGE_ACCESSIBILITY) + public List<String> getAccessibilityShortcutTargets(@ShortcutType int shortcutType) { + if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_ACCESSIBILITY) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException( "getAccessibilityShortcutService requires the MANAGE_ACCESSIBILITY permission"); } - synchronized(mLock) { - final AccessibilityUserState userState = getUserStateLocked(mCurrentUserId); - if (userState.getServiceToEnableWithShortcutLocked() == null) { - return null; + return getAccessibilityShortcutTargetsInternal(shortcutType); + } + + private List<String> getAccessibilityShortcutTargetsInternal(@ShortcutType int shortcutType) { + synchronized (mLock) { + final AccessibilityUserState userState = getCurrentUserStateLocked(); + final ArrayList<String> shortcutTargets = new ArrayList<>( + userState.getShortcutTargetsLocked(shortcutType)); + if (shortcutType != ACCESSIBILITY_BUTTON) { + return shortcutTargets; + } + // Adds legacy a11y services requesting a11y button into the list. + for (int i = userState.mBoundServices.size() - 1; i >= 0; i--) { + final AccessibilityServiceConnection service = userState.mBoundServices.get(i); + if (!service.mRequestAccessibilityButton + || service.getServiceInfo().getResolveInfo().serviceInfo.applicationInfo + .targetSdkVersion > Build.VERSION_CODES.Q) { + continue; + } + final String serviceName = service.getComponentName().flattenToString(); + if (!TextUtils.isEmpty(serviceName)) { + shortcutTargets.add(serviceName); + } } - return userState.getServiceToEnableWithShortcutLocked().flattenToString(); + return shortcutTargets; } } @@ -2609,6 +2793,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private final Uri mDisplayMagnificationEnabledUri = Settings.Secure.getUriFor( Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED); + // TODO(a11y shortcut): Remove this setting key, and have a migrate function in + // Setting provider after new shortcut UI merged. private final Uri mNavBarMagnificationEnabledUri = Settings.Secure.getUriFor( Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED); @@ -2713,7 +2899,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub || mShowImeWithHardKeyboardUri.equals(uri)) { userState.reconcileSoftKeyboardModeWithSettingsLocked(); } else if (mAccessibilityShortcutServiceIdUri.equals(uri)) { - if (readAccessibilityShortcutSettingLocked(userState)) { + if (readAccessibilityShortcutKeySettingLocked(userState)) { onUserStateChangedLocked(userState); } } else if (mAccessibilityButtonComponentIdUri.equals(uri)) { @@ -2724,8 +2910,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub || mUserInteractiveUiTimeoutUri.equals(uri)) { readUserRecommendedUiTimeoutSettingsLocked(userState); } - // TODO(a11y shortcut): Monitor new setting keys, when user adds shortcut, and - // remove from the list of enabled targets anything that's been uninstalled. } } } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java index 6cadb6d0f31f..cbff6bdcec77 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java @@ -294,6 +294,7 @@ class AccessibilityServiceConnection extends AbstractAccessibilityServiceConnect } } + // TODO(a11y shortcut): Refactoring the logic here, after the new Settings shortcut Ui merged. public boolean isAccessibilityButtonAvailableLocked(AccessibilityUserState userState) { // If the service does not request the accessibility button, it isn't available if (!mRequestAccessibilityButton) { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java index a0b9866e24d2..a163f7434e1f 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java @@ -22,6 +22,11 @@ import static android.accessibilityservice.AccessibilityService.SHOW_MODE_HARD_K import static android.accessibilityservice.AccessibilityService.SHOW_MODE_HIDDEN; import static android.accessibilityservice.AccessibilityService.SHOW_MODE_IGNORE_HARD_KEYBOARD; import static android.accessibilityservice.AccessibilityService.SHOW_MODE_MASK; +import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON; +import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY; +import static android.view.accessibility.AccessibilityManager.ShortcutType; + +import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; import android.accessibilityservice.AccessibilityService.SoftKeyboardShowMode; import android.accessibilityservice.AccessibilityServiceInfo; @@ -33,10 +38,14 @@ import android.content.Context; import android.os.Binder; import android.os.RemoteCallbackList; import android.provider.Settings; +import android.text.TextUtils; +import android.util.ArraySet; import android.util.Slog; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.IAccessibilityManagerClient; +import com.android.internal.accessibility.AccessibilityShortcutController; + import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; @@ -79,19 +88,18 @@ class AccessibilityUserState { final Set<ComponentName> mTouchExplorationGrantedServices = new HashSet<>(); - private final ServiceInfoChangeListener mServiceInfoChangeListener; + final ArraySet<String> mAccessibilityShortcutKeyTargets = new ArraySet<>(); - private ComponentName mServiceAssignedToAccessibilityButton; + final ArraySet<String> mAccessibilityButtonTargets = new ArraySet<>(); - private ComponentName mServiceChangingSoftKeyboardMode; + private final ServiceInfoChangeListener mServiceInfoChangeListener; - private ComponentName mServiceToEnableWithShortcut; + private ComponentName mServiceChangingSoftKeyboardMode; private boolean mBindInstantServiceAllowed; private boolean mIsAutoclickEnabled; private boolean mIsDisplayMagnificationEnabled; private boolean mIsFilterKeyEventsEnabled; - private boolean mIsNavBarMagnificationAssignedToAccessibilityButton; private boolean mIsNavBarMagnificationEnabled; private boolean mIsPerformGesturesEnabled; private boolean mIsTextHighContrastEnabled; @@ -141,11 +149,11 @@ class AccessibilityUserState { // Clear state persisted in settings. mEnabledServices.clear(); mTouchExplorationGrantedServices.clear(); + mAccessibilityShortcutKeyTargets.clear(); + mAccessibilityButtonTargets.clear(); mIsTouchExplorationEnabled = false; mIsDisplayMagnificationEnabled = false; mIsNavBarMagnificationEnabled = false; - mServiceAssignedToAccessibilityButton = null; - mIsNavBarMagnificationAssignedToAccessibilityButton = false; mIsAutoclickEnabled = false; mUserNonInteractiveUiTimeout = 0; mUserInteractiveUiTimeout = 0; @@ -435,6 +443,26 @@ class AccessibilityUserState { pw.append(", installedServiceCount=").append(String.valueOf(mInstalledServices.size())); pw.append("}"); pw.println(); + pw.append(" shortcut key:{"); + int size = mAccessibilityShortcutKeyTargets.size(); + for (int i = 0; i < size; i++) { + final String componentId = mAccessibilityShortcutKeyTargets.valueAt(i); + pw.append(componentId); + if (i + 1 < size) { + pw.append(", "); + } + } + pw.println("}"); + pw.append(" button:{"); + size = mAccessibilityButtonTargets.size(); + for (int i = 0; i < size; i++) { + final String componentId = mAccessibilityButtonTargets.valueAt(i); + pw.append(componentId); + if (i + 1 < size) { + pw.append(", "); + } + } + pw.println("}"); pw.append(" Bound services:{"); final int serviceCount = mBoundServices.size(); for (int j = 0; j < serviceCount; j++) { @@ -525,20 +553,85 @@ class AccessibilityUserState { mLastSentClientState = state; } - public boolean isNavBarMagnificationAssignedToAccessibilityButtonLocked() { - return mIsNavBarMagnificationAssignedToAccessibilityButton; + public boolean isShortcutKeyMagnificationEnabledLocked() { + return mAccessibilityShortcutKeyTargets.contains(MAGNIFICATION_CONTROLLER_NAME); } - public void setNavBarMagnificationAssignedToAccessibilityButtonLocked(boolean assigned) { - mIsNavBarMagnificationAssignedToAccessibilityButton = assigned; + /** + * Disable both shortcuts' magnification function. + */ + public void disableShortcutMagnificationLocked() { + mAccessibilityShortcutKeyTargets.remove(MAGNIFICATION_CONTROLLER_NAME); + mAccessibilityButtonTargets.remove(MAGNIFICATION_CONTROLLER_NAME); } - public boolean isNavBarMagnificationEnabledLocked() { - return mIsNavBarMagnificationEnabled; + /** + * Returns a set which contains the flattened component names and the system class names + * assigned to the given shortcut. + * + * @param shortcutType The shortcut type. + * @return The array set of the strings + */ + public ArraySet<String> getShortcutTargetsLocked(@ShortcutType int shortcutType) { + if (shortcutType == ACCESSIBILITY_SHORTCUT_KEY) { + return mAccessibilityShortcutKeyTargets; + } else if (shortcutType == ACCESSIBILITY_BUTTON) { + return mAccessibilityButtonTargets; + } + return null; } - public void setNavBarMagnificationEnabledLocked(boolean enabled) { - mIsNavBarMagnificationEnabled = enabled; + /** + * Whether or not the given shortcut target is installed in device. + * + * @param name The shortcut target name + * @return true if the shortcut target is installed. + */ + public boolean isShortcutTargetInstalledLocked(String name) { + if (TextUtils.isEmpty(name)) { + return false; + } + if (MAGNIFICATION_CONTROLLER_NAME.equals(name)) { + return true; + } + + final ComponentName componentName = ComponentName.unflattenFromString(name); + if (componentName == null) { + return false; + } + if (AccessibilityShortcutController.getFrameworkShortcutFeaturesMap() + .containsKey(componentName)) { + return true; + } + if (getInstalledServiceInfoLocked(componentName) != null) { + return true; + } + for (int i = 0; i < mInstalledShortcuts.size(); i++) { + if (mInstalledShortcuts.get(i).getComponentName().equals(componentName)) { + return true; + } + } + return false; + } + + /** + * Returns installed accessibility service info by the given service component name. + */ + public AccessibilityServiceInfo getInstalledServiceInfoLocked(ComponentName componentName) { + for (int i = 0; i < mInstalledServices.size(); i++) { + final AccessibilityServiceInfo serviceInfo = mInstalledServices.get(i); + if (serviceInfo.getComponentName().equals(componentName)) { + return serviceInfo; + } + } + return null; + } + + /** + * Returns accessibility service connection by the given service component name. + */ + public AccessibilityServiceConnection getServiceConnectionLocked(ComponentName componentName) { + return mComponentNameToServiceMap.get(componentName); } public int getNonInteractiveUiTimeoutLocked() { @@ -557,14 +650,6 @@ class AccessibilityUserState { mIsPerformGesturesEnabled = enabled; } - public ComponentName getServiceAssignedToAccessibilityButtonLocked() { - return mServiceAssignedToAccessibilityButton; - } - - public void setServiceAssignedToAccessibilityButtonLocked(ComponentName componentName) { - mServiceAssignedToAccessibilityButton = componentName; - } - public ComponentName getServiceChangingSoftKeyboardModeLocked() { return mServiceChangingSoftKeyboardMode; } @@ -574,14 +659,6 @@ class AccessibilityUserState { mServiceChangingSoftKeyboardMode = serviceChangingSoftKeyboardMode; } - public ComponentName getServiceToEnableWithShortcutLocked() { - return mServiceToEnableWithShortcut; - } - - public void setServiceToEnableWithShortcutLocked(ComponentName componentName) { - mServiceToEnableWithShortcut = componentName; - } - public boolean isTextHighContrastEnabledLocked() { return mIsTextHighContrastEnabled; } @@ -613,4 +690,28 @@ class AccessibilityUserState { public void setUserNonInteractiveUiTimeoutLocked(int timeout) { mUserNonInteractiveUiTimeout = timeout; } + + // TODO(a11y shortcut): These functions aren't necessary, after the new Settings shortcut Ui + // is merged. + boolean isNavBarMagnificationEnabledLocked() { + return mIsNavBarMagnificationEnabled; + } + + void setNavBarMagnificationEnabledLocked(boolean enabled) { + mIsNavBarMagnificationEnabled = enabled; + } + + boolean isNavBarMagnificationAssignedToAccessibilityButtonLocked() { + return mAccessibilityButtonTargets.contains(MAGNIFICATION_CONTROLLER_NAME); + } + + ComponentName getServiceAssignedToAccessibilityButtonLocked() { + final String targetName = mAccessibilityButtonTargets.isEmpty() ? null + : mAccessibilityButtonTargets.valueAt(0); + if (targetName == null) { + return null; + } + return ComponentName.unflattenFromString(targetName); + } + // TODO(a11y shortcut): End } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java index d70e1648f719..96d9c476bcde 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java @@ -73,7 +73,7 @@ public class AccessibilityUserStateTest { @Mock private AccessibilityUserState.ServiceInfoChangeListener mMockListener; - @Mock private Context mContext; + @Mock private Context mMockContext; private MockContentResolver mMockResolver; @@ -85,11 +85,11 @@ public class AccessibilityUserStateTest { FakeSettingsProvider.clearSettingsProvider(); mMockResolver = new MockContentResolver(); mMockResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); - when(mContext.getContentResolver()).thenReturn(mMockResolver); + when(mMockContext.getContentResolver()).thenReturn(mMockResolver); when(mMockServiceInfo.getComponentName()).thenReturn(COMPONENT_NAME); when(mMockConnection.getServiceInfo()).thenReturn(mMockServiceInfo); - mUserState = new AccessibilityUserState(USER_ID, mContext, mMockListener); + mUserState = new AccessibilityUserState(USER_ID, mMockContext, mMockListener); } @After @@ -109,11 +109,11 @@ public class AccessibilityUserStateTest { mUserState.setInteractiveUiTimeoutLocked(30); mUserState.mEnabledServices.add(COMPONENT_NAME); mUserState.mTouchExplorationGrantedServices.add(COMPONENT_NAME); + mUserState.mAccessibilityShortcutKeyTargets.add(COMPONENT_NAME.flattenToString()); + mUserState.mAccessibilityButtonTargets.add(COMPONENT_NAME.flattenToString()); mUserState.setTouchExplorationEnabledLocked(true); mUserState.setDisplayMagnificationEnabledLocked(true); mUserState.setNavBarMagnificationEnabledLocked(true); - mUserState.setServiceAssignedToAccessibilityButtonLocked(COMPONENT_NAME); - mUserState.setNavBarMagnificationAssignedToAccessibilityButtonLocked(true); mUserState.setAutoclickEnabledLocked(true); mUserState.setUserNonInteractiveUiTimeoutLocked(30); mUserState.setUserInteractiveUiTimeoutLocked(30); @@ -128,11 +128,11 @@ public class AccessibilityUserStateTest { assertEquals(0, mUserState.getInteractiveUiTimeoutLocked()); assertTrue(mUserState.mEnabledServices.isEmpty()); assertTrue(mUserState.mTouchExplorationGrantedServices.isEmpty()); + assertTrue(mUserState.mAccessibilityShortcutKeyTargets.isEmpty()); + assertTrue(mUserState.mAccessibilityButtonTargets.isEmpty()); assertFalse(mUserState.isTouchExplorationEnabledLocked()); assertFalse(mUserState.isDisplayMagnificationEnabledLocked()); assertFalse(mUserState.isNavBarMagnificationEnabledLocked()); - assertNull(mUserState.getServiceAssignedToAccessibilityButtonLocked()); - assertFalse(mUserState.isNavBarMagnificationAssignedToAccessibilityButtonLocked()); assertFalse(mUserState.isAutoclickEnabledLocked()); assertEquals(0, mUserState.getUserNonInteractiveUiTimeoutLocked()); assertEquals(0, mUserState.getUserInteractiveUiTimeoutLocked()); @@ -287,6 +287,19 @@ public class AccessibilityUserStateTest { verify(mMockConnection).notifySoftKeyboardShowModeChangedLocked(eq(SHOW_MODE_HIDDEN)); } + @Test + public void isShortcutTargetInstalledLocked_returnTrue() { + mUserState.mInstalledServices.add(mMockServiceInfo); + assertTrue(mUserState.isShortcutTargetInstalledLocked(COMPONENT_NAME.flattenToString())); + } + + @Test + public void isShortcutTargetInstalledLocked_invalidTarget_returnFalse() { + final ComponentName invalidTarget = + new ComponentName("com.android.server.accessibility", "InvalidTarget"); + assertFalse(mUserState.isShortcutTargetInstalledLocked(invalidTarget.flattenToString())); + } + private int getSecureIntForUser(String key, int userId) { return Settings.Secure.getIntForUser(mMockResolver, key, -1, userId); } |