AccessibilityMenuService test class setup & initial test.

Bug: 265173123
Test: atest AccessibilityMenuServiceTests
Change-Id: I388e02b58bf582ae6b481804c5f34883e8692e5f
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
index 140c10d..f358417 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
+++ b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp
@@ -18,6 +18,15 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+// This filegroup is used by menu tests.
+filegroup {
+    name: "AccessibilityMenuSource",
+    srcs: [
+        "src/**/AccessibilityMenuService.java",
+        "src/**/A11yMenuShortcut.java",
+    ],
+}
+
 android_app {
     name: "AccessibilityMenu",
 
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml
index 39e5a8c..a902c5b 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml
+++ b/packages/SystemUI/accessibility/accessibilitymenu/res/layout/grid_item.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/shortcutItem"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:paddingTop="@dimen/grid_item_padding"
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
index c1f2aa8..8ca64d2 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.accessibility.accessibilitymenu;
 
+import android.Manifest;
 import android.accessibilityservice.AccessibilityButtonController;
 import android.accessibilityservice.AccessibilityService;
 import android.content.BroadcastReceiver;
@@ -51,8 +52,12 @@
 /** @hide */
 public class AccessibilityMenuService extends AccessibilityService
         implements View.OnTouchListener {
-    private static final String TAG = "A11yMenuService";
 
+    public static final String PACKAGE_NAME = AccessibilityMenuService.class.getPackageName();
+    public static final String INTENT_TOGGLE_MENU = ".toggle_menu";
+    public static final String INTENT_HIDE_MENU = ".hide_menu";
+
+    private static final String TAG = "A11yMenuService";
     private static final long BUFFER_MILLISECONDS_TO_PREVENT_UPDATE_FAILURE = 100L;
 
     private static final int BRIGHTNESS_UP_INCREMENT_GAMMA =
@@ -74,7 +79,8 @@
 
     // TODO(b/136716947): Support multi-display once a11y framework side is ready.
     private DisplayManager mDisplayManager;
-    final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() {
+    private final DisplayManager.DisplayListener mDisplayListener =
+            new DisplayManager.DisplayListener() {
         int mRotation;
 
         @Override
@@ -95,13 +101,20 @@
         }
     };
 
-    final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+    private final BroadcastReceiver mHideMenuReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
             mA11yMenuLayout.hideMenu();
         }
     };
 
+    private final BroadcastReceiver mToggleMenuReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mA11yMenuLayout.toggleVisibility();
+        }
+    };
+
     /**
      * Update a11y menu layout when large button setting is changed.
      */
@@ -172,7 +185,19 @@
     protected void onServiceConnected() {
         mA11yMenuLayout = new A11yMenuOverlayLayout(this);
 
-        registerReceiver(mBroadcastReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
+        IntentFilter hideMenuFilter = new IntentFilter();
+        hideMenuFilter.addAction(Intent.ACTION_SCREEN_OFF);
+        hideMenuFilter.addAction(PACKAGE_NAME + INTENT_HIDE_MENU);
+
+        // Including WRITE_SECURE_SETTINGS enforces that we only listen to apps
+        // with the restricted WRITE_SECURE_SETTINGS permission who broadcast this intent.
+        registerReceiver(mHideMenuReceiver, hideMenuFilter,
+                Manifest.permission.WRITE_SECURE_SETTINGS, null,
+                Context.RECEIVER_EXPORTED);
+        registerReceiver(mToggleMenuReceiver,
+                new IntentFilter(PACKAGE_NAME + INTENT_TOGGLE_MENU),
+                Manifest.permission.WRITE_SECURE_SETTINGS, null,
+                Context.RECEIVER_EXPORTED);
 
         mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
         mPrefs.registerOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener);
@@ -260,7 +285,8 @@
      * @param increment The increment amount in gamma-space
      */
     private void adjustBrightness(int increment) {
-        BrightnessInfo info = getDisplay().getBrightnessInfo();
+        Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
+        BrightnessInfo info = display.getBrightnessInfo();
         int gamma = BrightnessUtils.convertLinearToGammaFloat(
                 info.brightness,
                 info.brightnessMinimum,
@@ -275,7 +301,7 @@
                 info.brightnessMinimum,
                 info.brightnessMaximum
         );
-        mDisplayManager.setBrightness(getDisplayId(), brightness);
+        mDisplayManager.setBrightness(display.getDisplayId(), brightness);
         mA11yMenuLayout.showSnackbar(
                 getString(R.string.brightness_percentage_label,
                         (gamma / (BrightnessUtils.GAMMA_SPACE_MAX / 100))));
@@ -310,7 +336,8 @@
 
     @Override
     public boolean onUnbind(Intent intent) {
-        unregisterReceiver(mBroadcastReceiver);
+        unregisterReceiver(mHideMenuReceiver);
+        unregisterReceiver(mToggleMenuReceiver);
         mPrefs.unregisterOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener);
         sInitialized = false;
         return super.onUnbind(intent);
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java
index 6f0fe37..6ae65cb 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java
@@ -21,6 +21,7 @@
 import android.view.TouchDelegate;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.BaseAdapter;
 import android.widget.ImageButton;
 import android.widget.TextView;
@@ -146,6 +147,15 @@
 
             shortcutIconButton.setBackground(
                     mShortcutDrawableUtils.createAdaptiveIconDrawable(shortcutItem.imageColor));
+
+            shortcutIconButton.setAccessibilityDelegate(new View.AccessibilityDelegate() {
+                @Override
+                public void onInitializeAccessibilityNodeInfo(
+                        View host, AccessibilityNodeInfo info) {
+                    super.onInitializeAccessibilityNodeInfo(host, info);
+                    info.setUniqueId(host.getTag().toString());
+                }
+            });
         }
     }
 }
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/tests/Android.bp
new file mode 100644
index 0000000..1757dda
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/Android.bp
@@ -0,0 +1,43 @@
+//
+// Copyright (C) 2023 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "AccessibilityMenuServiceTests",
+    certificate: "platform",
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+    ],
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.runner",
+        "androidx.test.ext.junit",
+        "compatibility-device-util-axt",
+        "platform-test-annotations",
+        "truth-prebuilt",
+    ],
+    srcs: [
+        "src/**/*.java",
+        ":AccessibilityMenuSource",
+    ],
+    platform_apis: true,
+    test_suites: ["device-tests"],
+    instrumentation_for: "AccessibilityMenu",
+}
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/AndroidManifest.xml b/packages/SystemUI/accessibility/accessibilitymenu/tests/AndroidManifest.xml
new file mode 100644
index 0000000..7be6ca7
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.android.systemui.accessibility.accessibilitymenu.tests">
+
+    <!-- Needed to write to Settings.Secure to enable and disable the service under test. -->
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS"/>
+
+    <application android:debuggable="true">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.systemui.accessibility.accessibilitymenu.tests"
+        android:label="AccessibilityMenu Test Cases">
+    </instrumentation>
+</manifest>
\ No newline at end of file
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/AndroidTest.xml b/packages/SystemUI/accessibility/accessibilitymenu/tests/AndroidTest.xml
new file mode 100644
index 0000000..39bee53
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/AndroidTest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+<configuration description="Runs AccessibilityMenu Test Cases.">
+    <option name="test-tag" value="AccessibilityMenuServiceTests" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="AccessibilityMenuServiceTests.apk" />
+        <option name="aapt-version" value="AAPT2" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.systemui.accessibility.accessibilitymenu.tests" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/TEST_MAPPING b/packages/SystemUI/accessibility/accessibilitymenu/tests/TEST_MAPPING
new file mode 100644
index 0000000..2bd52b5
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+  "presubmit": [
+    {
+      "name": "AccessibilityMenuServiceTests",
+      "options": [
+        {
+          "include-annotation": "android.platform.test.annotations.Presubmit"
+        },
+        {
+          "exclude-annotation": "android.support.test.filters.FlakyTest"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
new file mode 100644
index 0000000..529a70c
--- /dev/null
+++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2023 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.systemui.accessibility.accessibilitymenu.tests;
+
+import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_HIDE_MENU;
+import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_TOGGLE_MENU;
+import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.PACKAGE_NAME;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.display.BrightnessInfo;
+import android.hardware.display.DisplayManager;
+import android.provider.Settings;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.compatibility.common.util.TestUtils;
+import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut.ShortcutId;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class AccessibilityMenuServiceTest {
+    private static final String TAG = "A11yMenuServiceTest";
+
+    private static final int TIMEOUT_SERVICE_STATUS_CHANGE_S = 5;
+    private static final int TIMEOUT_UI_CHANGE_S = 5;
+
+    private static Instrumentation sInstrumentation;
+    private static UiAutomation sUiAutomation;
+
+    private static AccessibilityManager sAccessibilityManager;
+
+    @BeforeClass
+    public static void classSetup() throws Throwable {
+        final String serviceName = PACKAGE_NAME + "/.AccessibilityMenuService";
+        sInstrumentation = InstrumentationRegistry.getInstrumentation();
+        sUiAutomation = sInstrumentation.getUiAutomation(
+                UiAutomation.FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES);
+        final Context context = sInstrumentation.getContext();
+        sAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+
+        // Disable all a11yServices if any are active.
+        if (!sAccessibilityManager.getEnabledAccessibilityServiceList(
+                AccessibilityServiceInfo.FEEDBACK_ALL_MASK).isEmpty()) {
+            Settings.Secure.putString(context.getContentResolver(),
+                    Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, "");
+            TestUtils.waitUntil("Failed to disable all services",
+                    TIMEOUT_SERVICE_STATUS_CHANGE_S,
+                    () -> sAccessibilityManager.getEnabledAccessibilityServiceList(
+                            AccessibilityServiceInfo.FEEDBACK_ALL_MASK).isEmpty());
+        }
+
+        // Enable a11yMenu service.
+        Settings.Secure.putString(context.getContentResolver(),
+                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, serviceName);
+
+        TestUtils.waitUntil("Failed to enable service",
+                TIMEOUT_SERVICE_STATUS_CHANGE_S,
+                () -> sAccessibilityManager.getEnabledAccessibilityServiceList(
+                        AccessibilityServiceInfo.FEEDBACK_ALL_MASK).stream().filter(
+                                info -> info.getId().contains(serviceName)).count() == 1);
+    }
+
+    @AfterClass
+    public static void classTeardown() throws Throwable {
+        Settings.Secure.putString(sInstrumentation.getTargetContext().getContentResolver(),
+                Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, "");
+    }
+
+    private boolean isMenuVisible() {
+        return sUiAutomation.getRootInActiveWindow() != null
+                && sUiAutomation.getRootInActiveWindow().getPackageName().toString().equals(
+                PACKAGE_NAME);
+    }
+
+    private void openMenu() throws Throwable {
+        if (isMenuVisible()) {
+            return;
+        }
+        Intent intent = new Intent(PACKAGE_NAME + INTENT_TOGGLE_MENU);
+        sInstrumentation.getContext().sendBroadcast(intent);
+        TestUtils.waitUntil("Timed out before menu could appear.",
+                TIMEOUT_UI_CHANGE_S, () -> isMenuVisible());
+    }
+
+    private void closeMenu() throws Throwable {
+        if (!isMenuVisible()) {
+            return;
+        }
+        Intent intent = new Intent(PACKAGE_NAME + INTENT_HIDE_MENU);
+        sInstrumentation.getContext().sendBroadcast(intent);
+        TestUtils.waitUntil("Timed out before menu could close.",
+                TIMEOUT_UI_CHANGE_S, () -> !isMenuVisible());
+    }
+
+    private List<AccessibilityNodeInfo> getGridButtonList() {
+        return sUiAutomation.getRootInActiveWindow()
+                        .findAccessibilityNodeInfosByViewId(PACKAGE_NAME + ":id/shortcutIconBtn");
+    }
+
+    private AccessibilityNodeInfo findGridButtonInfo(
+            List<AccessibilityNodeInfo> buttons, String text) {
+        for (AccessibilityNodeInfo button: buttons) {
+            if (button.getUniqueId().equals(text)) {
+                return button;
+            }
+        }
+        return null;
+    }
+
+    @Test
+    public void testAdjustBrightness() throws Throwable {
+        openMenu();
+
+        Context context = sInstrumentation.getTargetContext();
+        DisplayManager displayManager = context.getSystemService(
+                DisplayManager.class);
+        float resetBrightness = displayManager.getBrightness(context.getDisplayId());
+
+        List<AccessibilityNodeInfo> buttons = getGridButtonList();
+        AccessibilityNodeInfo brightnessUpButton = findGridButtonInfo(buttons,
+                String.valueOf(ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal()));
+        AccessibilityNodeInfo brightnessDownButton = findGridButtonInfo(buttons,
+                String.valueOf(ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal()));
+
+        int clickId = AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.getId();
+        BrightnessInfo brightnessInfo = displayManager.getDisplay(
+                context.getDisplayId()).getBrightnessInfo();
+
+        try {
+            displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMinimum);
+            TestUtils.waitUntil("Could not change to minimum brightness",
+                    TIMEOUT_UI_CHANGE_S,
+                    () -> displayManager.getBrightness(context.getDisplayId())
+                            == brightnessInfo.brightnessMinimum);
+            brightnessUpButton.performAction(clickId);
+            TestUtils.waitUntil("Did not detect an increase in brightness.",
+                    TIMEOUT_UI_CHANGE_S,
+                    () -> displayManager.getBrightness(context.getDisplayId())
+                            > brightnessInfo.brightnessMinimum);
+
+            displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMaximum);
+            TestUtils.waitUntil("Could not change to maximum brightness",
+                    TIMEOUT_UI_CHANGE_S,
+                    () -> displayManager.getBrightness(context.getDisplayId())
+                            == brightnessInfo.brightnessMaximum);
+            brightnessDownButton.performAction(clickId);
+            TestUtils.waitUntil("Did not detect a decrease in brightness.",
+                    TIMEOUT_UI_CHANGE_S,
+                    () -> displayManager.getBrightness(context.getDisplayId())
+                            < brightnessInfo.brightnessMaximum);
+        } finally {
+            displayManager.setBrightness(context.getDisplayId(), resetBrightness);
+            closeMenu();
+        }
+    }
+}