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
merged-in: Id0696bff522cb7e19c5a16a47bbb76a794472c52
(cherry picked from commit 1a9081a982970d812676a3ee3e3aa7fe27f3b449)
diff --git a/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java b/core/java/com/android/internal/accessibility/AccessibilityShortcutController.java
index 43be031..1b901f5 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.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;
@@ -147,11 +148,13 @@
                             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 fc2c8cc..2d87745 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
@@ -25,6 +25,7 @@
 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;
@@ -209,6 +210,7 @@
                         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,
@@ -219,6 +221,7 @@
                         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,
@@ -229,16 +232,20 @@
                         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,
@@ -249,11 +256,6 @@
                         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(magnification);
-        targets.add(daltonizer);
-        targets.add(colorInversion);
-        targets.add(oneHandedMode);
         targets.add(reduceBrightColors);
 
         return targets;
diff --git a/core/java/com/android/internal/os/RoSystemProperties.java b/core/java/com/android/internal/os/RoSystemProperties.java
index 98d81c9..cccd80e 100644
--- a/core/java/com/android/internal/os/RoSystemProperties.java
+++ b/core/java/com/android/internal/os/RoSystemProperties.java
@@ -31,6 +31,8 @@
             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/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index 3e4b1cc..e96c642 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -1414,6 +1414,7 @@
         <activity android:name="com.android.internal.app.ChooserWrapperActivity"/>
         <activity android:name="com.android.internal.app.ResolverWrapperActivity"/>
         <activity android:name="com.android.internal.app.IntentForwarderActivityTest$IntentForwarderWrapperActivity"/>
+        <activity android:name="com.android.internal.accessibility.AccessibilityShortcutChooserActivityTest$TestAccessibilityShortcutChooserActivity"/>
 
         <receiver android:name="android.app.activity.AbortReceiver"
             android:exported="true">
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
new file mode 100644
index 0000000..973b904
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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 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.when;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Handler;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.IAccessibilityManager;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.R;
+import com.android.internal.accessibility.dialog.AccessibilityShortcutChooserActivity;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.Collections;
+
+/**
+ * Tests for {@link AccessibilityShortcutChooserActivity}.
+ */
+@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 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;
+    @Mock
+    private IAccessibilityManager mAccessibilityManagerService;
+
+    @Test
+    public void doubleClickTestServiceAndClickDenyButton_permissionDialogDoesNotExist()
+            throws Exception {
+        configureTestService();
+        final ActivityScenario<TestAccessibilityShortcutChooserActivity> 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());
+        scenario.moveToState(Lifecycle.State.DESTROYED);
+    }
+
+    @Test
+    public void popEditShortcutMenuList_oneHandedModeEnabled_shouldBeInListView() {
+        TestUtils.setOneHandedModeEnabled(this, /* enabled= */ true);
+        final ActivityScenario<TestAccessibilityShortcutChooserActivity> 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<TestAccessibilityShortcutChooserActivity> 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);
+    }
+
+    /**
+     * Used for testing.
+     */
+    public static class TestAccessibilityShortcutChooserActivity extends
+            AccessibilityShortcutChooserActivity {
+        private static IAccessibilityManager sAccessibilityManagerService;
+
+        public static void setupForTesting(IAccessibilityManager accessibilityManagerService) {
+            sAccessibilityManagerService = accessibilityManagerService;
+        }
+
+        @Override
+        public Object getSystemService(String name) {
+            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 6baf305..dc04220 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutControllerTest.java
@@ -21,6 +21,11 @@
 import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE;
 import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY;
 
+import static com.android.internal.accessibility.AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME;
+import static com.android.internal.accessibility.AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME;
+import static com.android.internal.accessibility.AccessibilityShortcutController.ONE_HANDED_COMPONENT_NAME;
+import static com.android.internal.accessibility.AccessibilityShortcutController.REDUCE_BRIGHT_COLORS_COMPONENT_NAME;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
@@ -203,6 +208,17 @@
         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
@@ -428,11 +444,10 @@
     }
 
     @Test
-    public void getFrameworkFeatureMap_shouldBeNonNullAndUnmodifiable() {
-        Map<ComponentName, AccessibilityShortcutController.ToggleableFrameworkFeatureInfo>
+    public void getFrameworkFeatureMap_shouldBeUnmodifiable() {
+        final Map<ComponentName, AccessibilityShortcutController.ToggleableFrameworkFeatureInfo>
                 frameworkFeatureMap =
                 AccessibilityShortcutController.getFrameworkShortcutFeaturesMap();
-        assertTrue("Framework features not supported", frameworkFeatureMap.size() > 0);
 
         try {
             frameworkFeatureMap.clear();
@@ -442,7 +457,38 @@
         }
     }
 
+    public void getFrameworkFeatureMap_containsExpectedDefaultKeys() {
+        final Map<ComponentName, AccessibilityShortcutController.ToggleableFrameworkFeatureInfo>
+                frameworkFeatureMap =
+                AccessibilityShortcutController.getFrameworkShortcutFeaturesMap();
+
+        assertTrue(frameworkFeatureMap.containsKey(COLOR_INVERSION_COMPONENT_NAME));
+        assertTrue(frameworkFeatureMap.containsKey(DALTONIZER_COMPONENT_NAME));
+        assertTrue(frameworkFeatureMap.containsKey(REDUCE_BRIGHT_COLORS_COMPONENT_NAME));
+    }
+
     @Test
+    public void getFrameworkFeatureMap_oneHandedModeEnabled_containsExpectedKey() {
+        TestUtils.setOneHandedModeEnabled(this, /* enabled= */ true);
+
+        final Map<ComponentName, AccessibilityShortcutController.ToggleableFrameworkFeatureInfo>
+                frameworkFeatureMap =
+                AccessibilityShortcutController.getFrameworkShortcutFeaturesMap();
+
+        assertTrue(frameworkFeatureMap.containsKey(ONE_HANDED_COMPONENT_NAME));
+    }
+
+    @Test
+    public void getFrameworkFeatureMap_oneHandedModeDisabled_containsExpectedKey() {
+        TestUtils.setOneHandedModeEnabled(this, /* enabled= */ false);
+
+        final Map<ComponentName, AccessibilityShortcutController.ToggleableFrameworkFeatureInfo>
+                frameworkFeatureMap =
+                AccessibilityShortcutController.getFrameworkShortcutFeaturesMap();
+
+        assertFalse(frameworkFeatureMap.containsKey(ONE_HANDED_COMPONENT_NAME));
+    }
+
     public void testOnAccessibilityShortcut_forServiceWithNoSummary_doesNotCrash()
             throws Exception {
         configureShortcutEnabled(ENABLED_EXCEPT_LOCK_SCREEN);
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 0000000..ff014ad
--- /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);
+        }
+    }
+}