Tech debt cleanup: Consolidates A11yService warning dialog.
Both frameworks/base and the Settings app define almost-identical copies
of a warning dialog shown when enabling an accessibility service.
The frameworks/base version was used for contexts outside of Settings
(e.g. while editing the volume key shortcut after triggering it with
2+ features already enabled) while the Settings version was used in
the Settings app.
This change replaces the frameworks/base implementation with the more
full-featured Settings implementation.
The other change in this topic in packages/apps/Settings changes
Settings to use this consolidated single version.
Feature flag:
`adb shell device_config override accessibility android.view.accessibility.deduplicate_accessibility_warning_dialog true`
Bug: 303511250
Test: Use this dialog in Accessibility Settings
Test: Use this dialog in the a11y volume-key shortcut editor
Test: atest AccessibilityServiceWarningTest
Test: atest AccessibilityShortcutChooserActivityTest
Change-Id: I6e1acf7cecfaa0dce422f7dfac3f270b9902dfe9
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index ab9566e..5296b99 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -17,6 +17,13 @@
}
flag {
+ name: "deduplicate_accessibility_warning_dialog"
+ namespace: "accessibility"
+ description: "Removes duplicate definition of the accessibility warning dialog."
+ bug: "303511250"
+}
+
+flag {
namespace: "accessibility"
name: "force_invert_color"
description: "Enable force force-dark for smart inversion and dark theme everywhere"
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java
index 6497409..2b6913c 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceTarget.java
@@ -34,6 +34,8 @@
*/
class AccessibilityServiceTarget extends AccessibilityTarget {
+ private final AccessibilityServiceInfo mAccessibilityServiceInfo;
+
AccessibilityServiceTarget(Context context, @ShortcutType int shortcutType,
@AccessibilityFragmentType int fragmentType,
@NonNull AccessibilityServiceInfo serviceInfo) {
@@ -47,6 +49,7 @@
serviceInfo.getResolveInfo().loadLabel(context.getPackageManager()),
serviceInfo.getResolveInfo().loadIcon(context.getPackageManager()),
convertToKey(convertToUserType(shortcutType)));
+ mAccessibilityServiceInfo = serviceInfo;
}
@Override
@@ -64,4 +67,8 @@
holder.mLabelView.setEnabled(enabled);
holder.mStatusView.setEnabled(enabled);
}
+
+ public AccessibilityServiceInfo getAccessibilityServiceInfo() {
+ return mAccessibilityServiceInfo;
+ }
}
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceWarning.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceWarning.java
new file mode 100644
index 0000000..0f8ced2
--- /dev/null
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityServiceWarning.java
@@ -0,0 +1,139 @@
+/*
+ * 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.internal.accessibility.dialog;
+
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.text.BidiFormatter;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Locale;
+
+/**
+ * Utility class for creating the dialog that asks the user for explicit permission
+ * before an accessibility service is enabled.
+ */
+public class AccessibilityServiceWarning {
+
+ /**
+ * Returns an {@link AlertDialog} to be shown to confirm that the user
+ * wants to enable an {@link android.accessibilityservice.AccessibilityService}.
+ */
+ public static AlertDialog createAccessibilityServiceWarningDialog(@NonNull Context context,
+ @NonNull AccessibilityServiceInfo info,
+ @NonNull View.OnClickListener allowListener,
+ @NonNull View.OnClickListener denyListener,
+ @NonNull View.OnClickListener uninstallListener) {
+ final AlertDialog ad = new AlertDialog.Builder(context)
+ .setView(createAccessibilityServiceWarningDialogContentView(
+ context, info, allowListener, denyListener, uninstallListener))
+ .setCancelable(true)
+ .create();
+ Window window = ad.getWindow();
+ WindowManager.LayoutParams params = window.getAttributes();
+ params.privateFlags |= SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+ params.type = WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
+ window.setAttributes(params);
+ return ad;
+ }
+
+ @VisibleForTesting
+ public static View createAccessibilityServiceWarningDialogContentView(Context context,
+ AccessibilityServiceInfo info,
+ View.OnClickListener allowListener,
+ View.OnClickListener denyListener,
+ View.OnClickListener uninstallListener) {
+ final LayoutInflater inflater = context.getSystemService(LayoutInflater.class);
+ final View content = inflater.inflate(R.layout.accessibility_service_warning, null);
+
+ final Drawable icon;
+ if (info.getResolveInfo().getIconResource() == 0) {
+ icon = context.getDrawable(R.drawable.ic_accessibility_generic);
+ } else {
+ icon = info.getResolveInfo().loadIcon(context.getPackageManager());
+ }
+ final ImageView permissionDialogIcon = content.findViewById(
+ R.id.accessibility_permissionDialog_icon);
+ permissionDialogIcon.setImageDrawable(icon);
+
+ final TextView permissionDialogTitle = content.findViewById(
+ R.id.accessibility_permissionDialog_title);
+ permissionDialogTitle.setText(context.getString(R.string.accessibility_enable_service_title,
+ getServiceName(context, info)));
+
+ final Button permissionAllowButton = content.findViewById(
+ R.id.accessibility_permission_enable_allow_button);
+ final Button permissionDenyButton = content.findViewById(
+ R.id.accessibility_permission_enable_deny_button);
+ permissionAllowButton.setOnClickListener(allowListener);
+ permissionAllowButton.setOnTouchListener(getTouchConsumingListener());
+ permissionDenyButton.setOnClickListener(denyListener);
+
+ final Button uninstallButton = content.findViewById(
+ R.id.accessibility_permission_enable_uninstall_button);
+ // Show an uninstall button to help users quickly remove non-preinstalled apps.
+ if (!info.getResolveInfo().serviceInfo.applicationInfo.isSystemApp()) {
+ uninstallButton.setVisibility(View.VISIBLE);
+ uninstallButton.setOnClickListener(uninstallListener);
+ }
+ return content;
+ }
+
+ @VisibleForTesting
+ @SuppressLint("ClickableViewAccessibility") // Touches are intentionally consumed
+ public static View.OnTouchListener getTouchConsumingListener() {
+ return (view, event) -> {
+ // Filter obscured touches by consuming them.
+ if (((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0)
+ || ((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED) != 0)) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ Toast.makeText(view.getContext(),
+ R.string.accessibility_dialog_touch_filtered_warning,
+ Toast.LENGTH_SHORT).show();
+ }
+ return true;
+ }
+ return false;
+ };
+ }
+
+ // Get the service name and bidi wrap it to protect from bidi side effects.
+ private static CharSequence getServiceName(Context context, AccessibilityServiceInfo info) {
+ final Locale locale = context.getResources().getConfiguration().getLocales().get(0);
+ final CharSequence label =
+ info.getResolveInfo().loadLabel(context.getPackageManager());
+ return BidiFormatter.getInstance(locale).unicodeWrap(label);
+ }
+}
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
index 987c14c..d4eccd4 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
@@ -28,6 +28,7 @@
import android.annotation.Nullable;
import android.app.Activity;
import android.app.AlertDialog;
+import android.app.Dialog;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.DialogInterface;
@@ -56,7 +57,7 @@
"accessibility_shortcut_menu_mode";
private final List<AccessibilityTarget> mTargets = new ArrayList<>();
private AlertDialog mMenuDialog;
- private AlertDialog mPermissionDialog;
+ private Dialog mPermissionDialog;
private ShortcutTargetAdapter mTargetAdapter;
@Override
@@ -123,7 +124,7 @@
if (target instanceof AccessibilityServiceTarget) {
showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target,
- mTargetAdapter);
+ position, mTargetAdapter);
return;
}
}
@@ -149,20 +150,43 @@
}
private void showPermissionDialogIfNeeded(Context context,
- AccessibilityServiceTarget serviceTarget, ShortcutTargetAdapter targetAdapter) {
+ AccessibilityServiceTarget serviceTarget, int position,
+ ShortcutTargetAdapter targetAdapter) {
if (mPermissionDialog != null) {
return;
}
- mPermissionDialog = new AlertDialog.Builder(context)
- .setView(createEnableDialogContentView(context, serviceTarget,
- v -> {
- mPermissionDialog.dismiss();
- targetAdapter.notifyDataSetChanged();
- },
- v -> mPermissionDialog.dismiss()))
- .setOnDismissListener(dialog -> mPermissionDialog = null)
- .create();
+ if (Flags.deduplicateAccessibilityWarningDialog()) {
+ mPermissionDialog = AccessibilityServiceWarning
+ .createAccessibilityServiceWarningDialog(context,
+ serviceTarget.getAccessibilityServiceInfo(),
+ v -> {
+ serviceTarget.onCheckedChanged(true);
+ targetAdapter.notifyDataSetChanged();
+ mPermissionDialog.dismiss();
+ }, v -> {
+ serviceTarget.onCheckedChanged(false);
+ mPermissionDialog.dismiss();
+ },
+ v -> {
+ mTargets.remove(position);
+ context.getPackageManager().getPackageInstaller().uninstall(
+ serviceTarget.getComponentName().getPackageName(), null);
+ targetAdapter.notifyDataSetChanged();
+ mPermissionDialog.dismiss();
+ });
+ mPermissionDialog.setOnDismissListener(dialog -> mPermissionDialog = null);
+ } else {
+ mPermissionDialog = new AlertDialog.Builder(context)
+ .setView(createEnableDialogContentView(context, serviceTarget,
+ v -> {
+ mPermissionDialog.dismiss();
+ targetAdapter.notifyDataSetChanged();
+ },
+ v -> mPermissionDialog.dismiss()))
+ .setOnDismissListener(dialog -> mPermissionDialog = null)
+ .create();
+ }
mPermissionDialog.show();
}
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
index 0f85075..51a5ddf 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java
@@ -296,6 +296,10 @@
}
}
+ /**
+ * @deprecated Use {@link AccessibilityServiceWarning}.
+ */
+ @Deprecated
static View createEnableDialogContentView(Context context,
AccessibilityServiceTarget target, View.OnClickListener allowListener,
View.OnClickListener denyListener) {
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index 20d8d91..62d58b6 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -165,6 +165,7 @@
<!-- AccessibilityShortcutChooserActivityTest permissions -->
<uses-permission android:name="android.permission.MANAGE_ACCESSIBILITY" />
+ <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" />
<application
android:theme="@style/Theme"
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 dd116b5..088b57f 100644
--- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java
@@ -44,14 +44,19 @@
import android.accessibilityservice.AccessibilityServiceInfo;
import android.app.AlertDialog;
import android.app.KeyguardManager;
+import android.app.UiAutomation;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
+import android.graphics.Rect;
+import android.os.Bundle;
import android.os.Handler;
+import android.platform.test.annotations.RequiresFlagsDisabled;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -62,6 +67,7 @@
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.Flags;
import android.view.accessibility.IAccessibilityManager;
@@ -90,12 +96,17 @@
@RunWith(AndroidJUnit4.class)
public class AccessibilityShortcutChooserActivityTest {
private static final String ONE_HANDED_MODE = "One-Handed mode";
+ private static final String ALLOW_LABEL = "Allow";
private static final String DENY_LABEL = "Deny";
+ private static final String UNINSTALL_LABEL = "Uninstall";
private static final String EDIT_LABEL = "Edit shortcuts";
private static final String LIST_TITLE_LABEL = "Choose features to use";
private static final String TEST_LABEL = "TEST_LABEL";
- private static final ComponentName TEST_COMPONENT_NAME = new ComponentName("package", "class");
+ private static final String TEST_PACKAGE = "TEST_LABEL";
+ private static final ComponentName TEST_COMPONENT_NAME = new ComponentName(TEST_PACKAGE,
+ "class");
private static final long UI_TIMEOUT_MS = 1000;
+ private UiAutomation mUiAutomation;
private UiDevice mDevice;
private ActivityScenario<TestAccessibilityShortcutChooserActivity> mScenario;
private TestAccessibilityShortcutChooserActivity mActivity;
@@ -117,6 +128,10 @@
private IAccessibilityManager mAccessibilityManagerService;
@Mock
private KeyguardManager mKeyguardManager;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private PackageInstaller mPackageInstaller;
@Before
public void setUp() throws Exception {
@@ -125,6 +140,7 @@
assumeFalse("AccessibilityShortcutChooserActivity not supported on watch",
pm.hasSystemFeature(PackageManager.FEATURE_WATCH));
+ mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
mDevice.wakeUp();
when(mAccessibilityServiceInfo.getResolveInfo()).thenReturn(mResolveInfo);
@@ -134,12 +150,15 @@
when(mAccessibilityServiceInfo.getComponentName()).thenReturn(TEST_COMPONENT_NAME);
when(mAccessibilityManagerService.getInstalledAccessibilityServiceList(
anyInt())).thenReturn(new ParceledListSlice<>(
- Collections.singletonList(mAccessibilityServiceInfo)));
+ Collections.singletonList(mAccessibilityServiceInfo)));
when(mAccessibilityManagerService.isAccessibilityTargetAllowed(
anyString(), anyInt(), anyInt())).thenReturn(true);
when(mKeyguardManager.isKeyguardLocked()).thenReturn(false);
+ when(mPackageManager.getPackageInstaller()).thenReturn(mPackageInstaller);
+
TestAccessibilityShortcutChooserActivity.setupForTesting(
- mAccessibilityManagerService, mKeyguardManager);
+ mAccessibilityManagerService, mKeyguardManager,
+ mPackageManager);
}
@After
@@ -150,18 +169,12 @@
}
@Test
- public void doubleClickTestServiceAndClickDenyButton_permissionDialogDoesNotExist() {
+ @RequiresFlagsDisabled(Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG)
+ public void selectTestService_oldPermissionDialog_deny_dialogIsHidden() {
launchActivity();
openShortcutsList();
- // Performing the double-click is flaky so retry if needed.
- for (int attempt = 1; attempt <= 2; attempt++) {
- onView(withText(TEST_LABEL)).perform(scrollTo(), doubleClick());
- if (mDevice.wait(Until.hasObject(By.text(DENY_LABEL)), UI_TIMEOUT_MS)) {
- break;
- }
- }
-
+ mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS);
onView(withText(DENY_LABEL)).perform(scrollTo(), click());
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
@@ -170,6 +183,50 @@
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG)
+ public void selectTestService_permissionDialog_allow_rowChecked() {
+ launchActivity();
+ openShortcutsList();
+
+ mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS);
+ clickSystemDialogButton(ALLOW_LABEL);
+
+ assertThat(mDevice.wait(Until.hasObject(By.textStartsWith(LIST_TITLE_LABEL)),
+ UI_TIMEOUT_MS)).isTrue();
+ assertThat(mDevice.wait(Until.hasObject(By.checked(true)), UI_TIMEOUT_MS)).isTrue();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG)
+ public void selectTestService_permissionDialog_deny_rowNotChecked() {
+ launchActivity();
+ openShortcutsList();
+
+ mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS);
+ clickSystemDialogButton(DENY_LABEL);
+
+ assertThat(mDevice.wait(Until.hasObject(By.textStartsWith(LIST_TITLE_LABEL)),
+ UI_TIMEOUT_MS)).isTrue();
+ assertThat(mDevice.wait(Until.hasObject(By.checked(true)), UI_TIMEOUT_MS)).isFalse();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG)
+ public void selectTestService_permissionDialog_uninstall_callsUninstaller_rowRemoved() {
+ launchActivity();
+ openShortcutsList();
+
+ mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS);
+ clickSystemDialogButton(UNINSTALL_LABEL);
+
+ verify(mPackageInstaller).uninstall(eq(TEST_PACKAGE), any());
+ assertThat(mDevice.wait(Until.hasObject(By.textStartsWith(LIST_TITLE_LABEL)),
+ UI_TIMEOUT_MS)).isTrue();
+ assertThat(mDevice.wait(Until.hasObject(By.textStartsWith(TEST_LABEL)),
+ UI_TIMEOUT_MS)).isFalse();
+ }
+
+ @Test
public void clickServiceTarget_notPermittedByAdmin_sendRestrictedDialogIntent()
throws Exception {
when(mAccessibilityManagerService.isAccessibilityTargetAllowed(
@@ -239,6 +296,18 @@
mDevice.wait(Until.hasObject(By.textStartsWith(LIST_TITLE_LABEL)), UI_TIMEOUT_MS);
}
+ private void clickSystemDialogButton(String dialogButtonText) {
+ // Use UiAutomation to find the button because UiDevice struggles to find
+ // a UI element in a system dialog.
+ final AccessibilityNodeInfo button =
+ mUiAutomation.getRootInActiveWindow()
+ .findAccessibilityNodeInfosByText(dialogButtonText).stream()
+ .filter(AccessibilityNodeInfo::isClickable).findFirst().get();
+ final Rect bounds = new Rect();
+ button.getBoundsInScreen(bounds);
+ mDevice.click(bounds.centerX(), bounds.centerY());
+ }
+
/**
* Used for testing.
*/
@@ -246,12 +315,30 @@
AccessibilityShortcutChooserActivity {
private static IAccessibilityManager sAccessibilityManagerService;
private static KeyguardManager sKeyguardManager;
+ private static PackageManager sPackageManager;
public static void setupForTesting(
IAccessibilityManager accessibilityManagerService,
- KeyguardManager keyguardManager) {
+ KeyguardManager keyguardManager,
+ PackageManager packageManager) {
sAccessibilityManagerService = accessibilityManagerService;
sKeyguardManager = keyguardManager;
+ sPackageManager = packageManager;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (Flags.deduplicateAccessibilityWarningDialog()) {
+ // Setting the Theme is necessary here for the dialog to use the proper style
+ // resources as designated in its layout XML.
+ setTheme(R.style.Theme_DeviceDefault_DayNight);
+ }
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ return sPackageManager;
}
@Override
diff --git a/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java b/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java
new file mode 100644
index 0000000..b76dd51
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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.internal.accessibility.dialog;
+
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.os.RemoteException;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.widget.TextView;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.R;
+import com.android.internal.accessibility.TestUtils;
+
+import com.google.common.truth.Expect;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Unit Tests for
+ * {@link com.android.internal.accessibility.dialog.AccessibilityServiceWarning}
+ */
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+@RequiresFlagsEnabled(
+ android.view.accessibility.Flags.FLAG_DEDUPLICATE_ACCESSIBILITY_WARNING_DIALOG)
+public class AccessibilityServiceWarningTest {
+ private static final String A11Y_SERVICE_PACKAGE_LABEL = "TestA11yService";
+ private static final String A11Y_SERVICE_SUMMARY = "TestA11yService summary";
+ private static final String A11Y_SERVICE_COMPONENT_NAME =
+ "fake.package/test.a11yservice.name";
+
+ private Context mContext;
+ private AccessibilityServiceInfo mAccessibilityServiceInfo;
+ private AtomicBoolean mAllowListener;
+ private AtomicBoolean mDenyListener;
+ private AtomicBoolean mUninstallListener;
+
+ @Rule
+ public final Expect expect = Expect.create();
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ @Before
+ public void setUp() throws RemoteException {
+ MockitoAnnotations.initMocks(this);
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mAccessibilityServiceInfo = TestUtils.createFakeServiceInfo(
+ A11Y_SERVICE_PACKAGE_LABEL,
+ A11Y_SERVICE_SUMMARY,
+ A11Y_SERVICE_COMPONENT_NAME,
+ /* isAlwaysOnService*/ false);
+ mAllowListener = new AtomicBoolean(false);
+ mDenyListener = new AtomicBoolean(false);
+ mUninstallListener = new AtomicBoolean(false);
+ }
+
+ @Test
+ public void createAccessibilityServiceWarningDialog_hasExpectedWindowParams() {
+ final AlertDialog dialog =
+ AccessibilityServiceWarning.createAccessibilityServiceWarningDialog(
+ mContext,
+ mAccessibilityServiceInfo,
+ null, null, null);
+ final Window dialogWindow = dialog.getWindow();
+ assertThat(dialogWindow).isNotNull();
+
+ expect.that(dialogWindow.getAttributes().privateFlags
+ & SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS).isEqualTo(
+ SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+ expect.that(dialogWindow.getAttributes().type).isEqualTo(TYPE_SYSTEM_DIALOG);
+ }
+
+ @Test
+ public void createAccessibilityServiceWarningDialog_hasExpectedServiceName() {
+ final TextView title = createDialogContentView().findViewById(
+ R.id.accessibility_permissionDialog_title);
+ assertThat(title).isNotNull();
+
+ assertThat(title.getText().toString()).contains(A11Y_SERVICE_PACKAGE_LABEL);
+ }
+
+ @Test
+ public void createAccessibilityServiceWarningDialog_clickAllow() {
+ final View allowButton = createDialogContentView().findViewById(
+ R.id.accessibility_permission_enable_allow_button);
+ assertThat(allowButton).isNotNull();
+
+ allowButton.performClick();
+
+ expect.that(mAllowListener.get()).isTrue();
+ expect.that(mDenyListener.get()).isFalse();
+ expect.that(mUninstallListener.get()).isFalse();
+ }
+
+ @Test
+ public void createAccessibilityServiceWarningDialog_clickDeny() {
+ final View denyButton = createDialogContentView().findViewById(
+ R.id.accessibility_permission_enable_deny_button);
+ assertThat(denyButton).isNotNull();
+
+ denyButton.performClick();
+
+ expect.that(mAllowListener.get()).isFalse();
+ expect.that(mDenyListener.get()).isTrue();
+ expect.that(mUninstallListener.get()).isFalse();
+ }
+
+ @Test
+ public void createAccessibilityServiceWarningDialog_clickUninstall() {
+ final View uninstallButton = createDialogContentView().findViewById(
+ R.id.accessibility_permission_enable_uninstall_button);
+ assertThat(uninstallButton).isNotNull();
+
+ uninstallButton.performClick();
+
+ expect.that(mAllowListener.get()).isFalse();
+ expect.that(mDenyListener.get()).isFalse();
+ expect.that(mUninstallListener.get()).isTrue();
+ }
+
+ @Test
+ public void getTouchConsumingListener() {
+ final View allowButton = createDialogContentView().findViewById(
+ R.id.accessibility_permission_enable_allow_button);
+ assertThat(allowButton).isNotNull();
+ final View.OnTouchListener listener =
+ AccessibilityServiceWarning.getTouchConsumingListener();
+
+ expect.that(listener.onTouch(allowButton, createMotionEvent(0))).isFalse();
+ expect.that(listener.onTouch(allowButton,
+ createMotionEvent(MotionEvent.FLAG_WINDOW_IS_OBSCURED))).isTrue();
+ expect.that(listener.onTouch(allowButton,
+ createMotionEvent(MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED))).isTrue();
+ }
+
+ private View createDialogContentView() {
+ return AccessibilityServiceWarning.createAccessibilityServiceWarningDialogContentView(
+ mContext,
+ mAccessibilityServiceInfo,
+ (v) -> mAllowListener.set(true),
+ (v) -> mDenyListener.set(true),
+ (v) -> mUninstallListener.set(true));
+ }
+
+ private MotionEvent createMotionEvent(int flags) {
+ MotionEvent.PointerProperties[] props = new MotionEvent.PointerProperties[]{
+ new MotionEvent.PointerProperties()
+ };
+ MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[]{
+ new MotionEvent.PointerCoords()
+ };
+ return MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 1, props, coords,
+ 0, 0, 0, 0, -1, 0, InputDevice.SOURCE_TOUCHSCREEN, flags);
+ }
+}