From 1a9081a982970d812676a3ee3e3aa7fe27f3b449 Mon Sep 17 00:00:00 2001 From: menghanli Date: Tue, 20 Dec 2022 15:30:53 +0800 Subject: [PATCH] Avoid users add one-handed mode by edit shortcut dialog in unsupported devices Root cause: We show preinstalled framework features in edit shortcut dialog Solution: Only show one-handed mode if the device supports Bug: 260182478 Test: atest AccessibilityTargetHelperTest Change-Id: Id0696bff522cb7e19c5a16a47bbb76a794472c52 --- .../AccessibilityShortcutController.java | 13 +- .../dialog/AccessibilityTargetHelper.java | 31 +++-- .../internal/os/RoSystemProperties.java | 2 + ...essibilityShortcutChooserActivityTest.java | 122 ++++++++++++------ .../AccessibilityShortcutControllerTest.java | 43 +++++- .../internal/accessibility/TestUtils.java | 41 ++++++ 6 files changed, 185 insertions(+), 67 deletions(-) create mode 100644 core/tests/coretests/src/com/android/internal/accessibility/TestUtils.java diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java index 4f97d210a1546..bf55255a7f25a 100644 --- a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java +++ b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java @@ -21,6 +21,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG; import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY; import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets; +import static com.android.internal.os.RoSystemProperties.SUPPORT_ONE_HANDED_MODE; import static com.android.internal.util.ArrayUtils.convertToLongArray; import android.accessibilityservice.AccessibilityServiceInfo; @@ -151,11 +152,13 @@ public class AccessibilityShortcutController { Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED, "1" /* Value to enable */, "0" /* Value to disable */, R.string.color_correction_feature_name)); - featuresMap.put(ONE_HANDED_COMPONENT_NAME, - new ToggleableFrameworkFeatureInfo( - Settings.Secure.ONE_HANDED_MODE_ACTIVATED, - "1" /* Value to enable */, "0" /* Value to disable */, - R.string.one_handed_mode_feature_name)); + if (SUPPORT_ONE_HANDED_MODE) { + featuresMap.put(ONE_HANDED_COMPONENT_NAME, + new ToggleableFrameworkFeatureInfo( + Settings.Secure.ONE_HANDED_MODE_ACTIVATED, + "1" /* Value to enable */, "0" /* Value to disable */, + R.string.one_handed_mode_feature_name)); + } featuresMap.put(REDUCE_BRIGHT_COLORS_COMPONENT_NAME, new ToggleableFrameworkFeatureInfo( Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED, diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java index b5455f25ef280..a47a97c02bfbc 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java @@ -26,6 +26,7 @@ import static com.android.internal.accessibility.AccessibilityShortcutController import static com.android.internal.accessibility.AccessibilityShortcutController.REDUCE_BRIGHT_COLORS_COMPONENT_NAME; import static com.android.internal.accessibility.util.AccessibilityUtils.getAccessibilityServiceFragmentType; import static com.android.internal.accessibility.util.ShortcutUtils.isShortcutContained; +import static com.android.internal.os.RoSystemProperties.SUPPORT_ONE_HANDED_MODE; import android.accessibilityservice.AccessibilityServiceInfo; import android.accessibilityservice.AccessibilityShortcutInfo; @@ -210,6 +211,7 @@ public final class AccessibilityTargetHelper { context.getString(R.string.accessibility_magnification_chooser_text), context.getDrawable(R.drawable.ic_accessibility_magnification), Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED); + targets.add(magnification); final ToggleAllowListingFeatureTarget daltonizer = new ToggleAllowListingFeatureTarget(context, @@ -220,6 +222,7 @@ public final class AccessibilityTargetHelper { context.getString(R.string.color_correction_feature_name), context.getDrawable(R.drawable.ic_accessibility_color_correction), Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED); + targets.add(daltonizer); final ToggleAllowListingFeatureTarget colorInversion = new ToggleAllowListingFeatureTarget(context, @@ -230,16 +233,20 @@ public final class AccessibilityTargetHelper { context.getString(R.string.color_inversion_feature_name), context.getDrawable(R.drawable.ic_accessibility_color_inversion), Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED); + targets.add(colorInversion); - final ToggleAllowListingFeatureTarget oneHandedMode = - new ToggleAllowListingFeatureTarget(context, - shortcutType, - isShortcutContained(context, shortcutType, - ONE_HANDED_COMPONENT_NAME.flattenToString()), - ONE_HANDED_COMPONENT_NAME.flattenToString(), - context.getString(R.string.one_handed_mode_feature_name), - context.getDrawable(R.drawable.ic_accessibility_one_handed), - Settings.Secure.ONE_HANDED_MODE_ACTIVATED); + if (SUPPORT_ONE_HANDED_MODE) { + final ToggleAllowListingFeatureTarget oneHandedMode = + new ToggleAllowListingFeatureTarget(context, + shortcutType, + isShortcutContained(context, shortcutType, + ONE_HANDED_COMPONENT_NAME.flattenToString()), + ONE_HANDED_COMPONENT_NAME.flattenToString(), + context.getString(R.string.one_handed_mode_feature_name), + context.getDrawable(R.drawable.ic_accessibility_one_handed), + Settings.Secure.ONE_HANDED_MODE_ACTIVATED); + targets.add(oneHandedMode); + } final ToggleAllowListingFeatureTarget reduceBrightColors = new ToggleAllowListingFeatureTarget(context, @@ -250,6 +257,7 @@ public final class AccessibilityTargetHelper { context.getString(R.string.reduce_bright_colors_feature_name), context.getDrawable(R.drawable.ic_accessibility_reduce_bright_colors), Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED); + targets.add(reduceBrightColors); final InvisibleToggleAllowListingFeatureTarget hearingAids = new InvisibleToggleAllowListingFeatureTarget(context, @@ -260,11 +268,6 @@ public final class AccessibilityTargetHelper { context.getString(R.string.hearing_aids_feature_name), context.getDrawable(R.drawable.ic_accessibility_hearing_aid), /* key= */ null); - targets.add(magnification); - targets.add(daltonizer); - targets.add(colorInversion); - targets.add(oneHandedMode); - targets.add(reduceBrightColors); targets.add(hearingAids); return targets; diff --git a/core/java/com/android/internal/os/RoSystemProperties.java b/core/java/com/android/internal/os/RoSystemProperties.java index 6870d09c8a7f5..af205d2a7e0be 100644 --- a/core/java/com/android/internal/os/RoSystemProperties.java +++ b/core/java/com/android/internal/os/RoSystemProperties.java @@ -31,6 +31,8 @@ public class RoSystemProperties { SystemProperties.getInt("ro.factorytest", 0); public static final String CONTROL_PRIVAPP_PERMISSIONS = SystemProperties.get("ro.control_privapp_permissions"); + public static final boolean SUPPORT_ONE_HANDED_MODE = + SystemProperties.getBoolean("ro.support_one_handed_mode", /* def= */ false); // ------ ro.hdmi.* -------- // /** diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java index 728757929cfd9..973b904c9344f 100644 --- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java @@ -20,16 +20,19 @@ import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.doubleClick; import static androidx.test.espresso.action.ViewActions.scrollTo; +import static androidx.test.espresso.action.ViewActions.swipeUp; import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.RootMatchers.isDialog; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withClassName; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.endsWith; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.accessibilityservice.AccessibilityServiceInfo; @@ -39,7 +42,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; -import android.os.Bundle; import android.os.Handler; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.IAccessibilityManager; @@ -52,11 +54,12 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.R; import com.android.internal.accessibility.dialog.AccessibilityShortcutChooserActivity; -import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.Collections; @@ -65,57 +68,93 @@ import java.util.Collections; */ @RunWith(AndroidJUnit4.class) public class AccessibilityShortcutChooserActivityTest { + private static final String ONE_HANDED_MODE = "One-Handed mode"; private static final String TEST_LABEL = "TEST_LABEL"; - private static final Context sContext = - InstrumentationRegistry.getInstrumentation().getContext(); - private ActivityScenario mScenario; - private static IAccessibilityManager sAccessibilityManagerService; + private static final ComponentName TEST_COMPONENT_NAME = new ComponentName("package", "class"); + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @Mock private AccessibilityServiceInfo mAccessibilityServiceInfo; - @Mock private ResolveInfo mResolveInfo; - @Mock private ServiceInfo mServiceInfo; - @Mock private ApplicationInfo mApplicationInfo; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - sAccessibilityManagerService = mock(IAccessibilityManager.class); - - when(mAccessibilityServiceInfo.getResolveInfo()).thenReturn(mResolveInfo); - mResolveInfo.serviceInfo = mServiceInfo; - mServiceInfo.applicationInfo = mApplicationInfo; - when(mResolveInfo.loadLabel(any(PackageManager.class))).thenReturn(TEST_LABEL); - when(mAccessibilityServiceInfo.getComponentName()).thenReturn( - new ComponentName("package", "class")); - when(sAccessibilityManagerService.getInstalledAccessibilityServiceList( - anyInt())).thenReturn(Collections.singletonList(mAccessibilityServiceInfo)); - - mScenario = ActivityScenario.launch(TestAccessibilityShortcutChooserActivity.class); - } + @Mock + private IAccessibilityManager mAccessibilityManagerService; @Test - public void doubleClickServiceTargetAndClickDenyButton_permissionDialogDoesNotExist() { - mScenario.moveToState(Lifecycle.State.CREATED); - mScenario.moveToState(Lifecycle.State.STARTED); - mScenario.moveToState(Lifecycle.State.RESUMED); + public void doubleClickTestServiceAndClickDenyButton_permissionDialogDoesNotExist() + throws Exception { + configureTestService(); + final ActivityScenario scenario = + ActivityScenario.launch(TestAccessibilityShortcutChooserActivity.class); + scenario.moveToState(Lifecycle.State.CREATED); + scenario.moveToState(Lifecycle.State.STARTED); + scenario.moveToState(Lifecycle.State.RESUMED); + onView(withText(R.string.accessibility_select_shortcut_menu_title)).inRoot( isDialog()).check(matches(isDisplayed())); onView(withText(R.string.edit_accessibility_shortcut_menu_button)).perform(click()); - onView(withText(TEST_LABEL)).perform(scrollTo(), doubleClick()); onView(withId(R.id.accessibility_permission_enable_deny_button)).perform(scrollTo(), click()); onView(withId(R.id.accessibility_permissionDialog_title)).inRoot(isDialog()).check( doesNotExist()); - mScenario.moveToState(Lifecycle.State.DESTROYED); + scenario.moveToState(Lifecycle.State.DESTROYED); + } + + @Test + public void popEditShortcutMenuList_oneHandedModeEnabled_shouldBeInListView() { + TestUtils.setOneHandedModeEnabled(this, /* enabled= */ true); + final ActivityScenario scenario = + ActivityScenario.launch(TestAccessibilityShortcutChooserActivity.class); + scenario.moveToState(Lifecycle.State.CREATED); + scenario.moveToState(Lifecycle.State.STARTED); + scenario.moveToState(Lifecycle.State.RESUMED); + + onView(withText(R.string.accessibility_select_shortcut_menu_title)).inRoot( + isDialog()).check(matches(isDisplayed())); + onView(withText(R.string.edit_accessibility_shortcut_menu_button)).perform(click()); + onView(allOf(withClassName(endsWith("ListView")), isDisplayed())).perform(swipeUp()); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + onView(withText(ONE_HANDED_MODE)).inRoot(isDialog()).check(matches(isDisplayed())); + scenario.moveToState(Lifecycle.State.DESTROYED); + } + + @Test + public void popEditShortcutMenuList_oneHandedModeDisabled_shouldNotBeInListView() { + TestUtils.setOneHandedModeEnabled(this, /* enabled= */ false); + final ActivityScenario scenario = + ActivityScenario.launch(TestAccessibilityShortcutChooserActivity.class); + scenario.moveToState(Lifecycle.State.CREATED); + scenario.moveToState(Lifecycle.State.STARTED); + scenario.moveToState(Lifecycle.State.RESUMED); + + onView(withText(R.string.accessibility_select_shortcut_menu_title)).inRoot( + isDialog()).check(matches(isDisplayed())); + onView(withText(R.string.edit_accessibility_shortcut_menu_button)).perform(click()); + onView(allOf(withClassName(endsWith("ListView")), isDisplayed())).perform(swipeUp()); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + + onView(withText(ONE_HANDED_MODE)).inRoot(isDialog()).check(doesNotExist()); + scenario.moveToState(Lifecycle.State.DESTROYED); + } + + private void configureTestService() throws Exception { + when(mAccessibilityServiceInfo.getResolveInfo()).thenReturn(mResolveInfo); + mResolveInfo.serviceInfo = mServiceInfo; + mServiceInfo.applicationInfo = mApplicationInfo; + when(mResolveInfo.loadLabel(any(PackageManager.class))).thenReturn(TEST_LABEL); + when(mAccessibilityServiceInfo.getComponentName()).thenReturn(TEST_COMPONENT_NAME); + when(mAccessibilityManagerService.getInstalledAccessibilityServiceList( + anyInt())).thenReturn(Collections.singletonList(mAccessibilityServiceInfo)); + + TestAccessibilityShortcutChooserActivity.setupForTesting(mAccessibilityManagerService); } /** @@ -123,19 +162,18 @@ public class AccessibilityShortcutChooserActivityTest { */ public static class TestAccessibilityShortcutChooserActivity extends AccessibilityShortcutChooserActivity { - private AccessibilityManager mAccessibilityManager; + private static IAccessibilityManager sAccessibilityManagerService; - @Override - protected void onCreate(Bundle savedInstanceState) { - mAccessibilityManager = new AccessibilityManager(sContext, new Handler(getMainLooper()), - sAccessibilityManagerService, /* userId= */ 0, /* serviceConnect= */ true); - super.onCreate(savedInstanceState); + public static void setupForTesting(IAccessibilityManager accessibilityManagerService) { + sAccessibilityManagerService = accessibilityManagerService; } @Override public Object getSystemService(String name) { - if (Context.ACCESSIBILITY_SERVICE.equals(name)) { - return mAccessibilityManager; + if (Context.ACCESSIBILITY_SERVICE.equals(name) + && sAccessibilityManagerService != null) { + return new AccessibilityManager(this, new Handler(getMainLooper()), + sAccessibilityManagerService, /* userId= */ 0, /* serviceConnect= */ true); } return super.getSystemService(name); 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 a68c39225ce37..9763679c7ad98 100644 --- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java +++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java @@ -208,6 +208,17 @@ public class AccessibilityShortcutControllerTest { when(mAlertDialog.getWindow()).thenReturn(window); when(mTextToSpeech.getVoice()).thenReturn(mVoice); + + // Clears the sFrameworkShortcutFeaturesMap field which was not properly initialized + // during testing. + try { + Field field = AccessibilityShortcutController.class.getDeclaredField( + "sFrameworkShortcutFeaturesMap"); + field.setAccessible(true); + field.set(window, null); + } catch (Exception e) { + throw new RuntimeException("Unable to set sFrameworkShortcutFeaturesMap", e); + } } @AfterClass @@ -433,11 +444,10 @@ public class AccessibilityShortcutControllerTest { } @Test - public void getFrameworkFeatureMap_shouldBeNonNullAndUnmodifiable() { - Map + public void getFrameworkFeatureMap_shouldBeUnmodifiable() { + final Map frameworkFeatureMap = AccessibilityShortcutController.getFrameworkShortcutFeaturesMap(); - assertTrue("Framework features not supported", frameworkFeatureMap.size() > 0); try { frameworkFeatureMap.clear(); @@ -448,18 +458,39 @@ public class AccessibilityShortcutControllerTest { } @Test - public void getFrameworkFeatureMap_containsExpectedKey() { - Map + public void getFrameworkFeatureMap_containsExpectedDefaultKeys() { + final Map frameworkFeatureMap = AccessibilityShortcutController.getFrameworkShortcutFeaturesMap(); assertTrue(frameworkFeatureMap.containsKey(COLOR_INVERSION_COMPONENT_NAME)); assertTrue(frameworkFeatureMap.containsKey(DALTONIZER_COMPONENT_NAME)); - assertTrue(frameworkFeatureMap.containsKey(ONE_HANDED_COMPONENT_NAME)); assertTrue(frameworkFeatureMap.containsKey(REDUCE_BRIGHT_COLORS_COMPONENT_NAME)); assertTrue(frameworkFeatureMap.containsKey(ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME)); } + @Test + public void getFrameworkFeatureMap_oneHandedModeEnabled_containsExpectedKey() { + TestUtils.setOneHandedModeEnabled(this, /* enabled= */ true); + + final Map + frameworkFeatureMap = + AccessibilityShortcutController.getFrameworkShortcutFeaturesMap(); + + assertTrue(frameworkFeatureMap.containsKey(ONE_HANDED_COMPONENT_NAME)); + } + + @Test + public void getFrameworkFeatureMap_oneHandedModeDisabled_containsExpectedKey() { + TestUtils.setOneHandedModeEnabled(this, /* enabled= */ false); + + final Map + frameworkFeatureMap = + AccessibilityShortcutController.getFrameworkShortcutFeaturesMap(); + + assertFalse(frameworkFeatureMap.containsKey(ONE_HANDED_COMPONENT_NAME)); + } + @Test public void testOnAccessibilityShortcut_forServiceWithNoSummary_doesNotCrash() throws Exception { diff --git a/core/tests/coretests/src/com/android/internal/accessibility/TestUtils.java b/core/tests/coretests/src/com/android/internal/accessibility/TestUtils.java new file mode 100644 index 0000000000000..ff014add793a3 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/accessibility/TestUtils.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 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.internal.accessibility; + +import com.android.internal.os.RoSystemProperties; + +import java.lang.reflect.Field; + +/** + * Test utility methods. + */ +public class TestUtils { + + /** + * Sets the {@code enabled} of the given OneHandedMode flags to simulate device behavior. + */ + public static void setOneHandedModeEnabled(Object obj, boolean enabled) { + try { + final Field field = RoSystemProperties.class.getDeclaredField( + "SUPPORT_ONE_HANDED_MODE"); + field.setAccessible(true); + field.setBoolean(obj, enabled); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } +}