1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.accessibility; 18 19 import static androidx.test.espresso.Espresso.onView; 20 import static androidx.test.espresso.action.ViewActions.click; 21 import static androidx.test.espresso.action.ViewActions.scrollTo; 22 import static androidx.test.espresso.action.ViewActions.swipeUp; 23 import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; 24 import static androidx.test.espresso.assertion.ViewAssertions.matches; 25 import static androidx.test.espresso.matcher.RootMatchers.isDialog; 26 import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; 27 import static androidx.test.espresso.matcher.ViewMatchers.withClassName; 28 import static androidx.test.espresso.matcher.ViewMatchers.withText; 29 30 import static com.google.common.truth.Truth.assertThat; 31 32 import static org.hamcrest.Matchers.allOf; 33 import static org.hamcrest.Matchers.endsWith; 34 import static org.junit.Assume.assumeFalse; 35 import static org.mockito.ArgumentMatchers.any; 36 import static org.mockito.ArgumentMatchers.anyInt; 37 import static org.mockito.ArgumentMatchers.anyString; 38 import static org.mockito.ArgumentMatchers.eq; 39 import static org.mockito.Mockito.verify; 40 import static org.mockito.Mockito.when; 41 42 import android.accessibilityservice.AccessibilityServiceInfo; 43 import android.app.AlertDialog; 44 import android.app.Dialog; 45 import android.app.KeyguardManager; 46 import android.content.ComponentName; 47 import android.content.Context; 48 import android.content.pm.ApplicationInfo; 49 import android.content.pm.PackageInstaller; 50 import android.content.pm.PackageManager; 51 import android.content.pm.ParceledListSlice; 52 import android.content.pm.ResolveInfo; 53 import android.content.pm.ServiceInfo; 54 import android.os.Bundle; 55 import android.os.Handler; 56 import android.platform.test.flag.junit.CheckFlagsRule; 57 import android.platform.test.flag.junit.DeviceFlagsValueProvider; 58 import android.support.test.uiautomator.By; 59 import android.support.test.uiautomator.UiDevice; 60 import android.support.test.uiautomator.UiObject2; 61 import android.support.test.uiautomator.Until; 62 import android.view.View; 63 import android.view.WindowManager; 64 import android.view.accessibility.AccessibilityManager; 65 import android.view.accessibility.IAccessibilityManager; 66 import android.widget.Button; 67 68 import androidx.lifecycle.Lifecycle; 69 import androidx.test.core.app.ActivityScenario; 70 import androidx.test.ext.junit.runners.AndroidJUnit4; 71 import androidx.test.platform.app.InstrumentationRegistry; 72 73 import com.android.internal.R; 74 import com.android.internal.accessibility.dialog.AccessibilityShortcutChooserActivity; 75 76 import org.junit.After; 77 import org.junit.Before; 78 import org.junit.Rule; 79 import org.junit.Test; 80 import org.junit.runner.RunWith; 81 import org.mockito.Mock; 82 import org.mockito.Mockito; 83 import org.mockito.junit.MockitoJUnit; 84 import org.mockito.junit.MockitoRule; 85 import org.mockito.stubbing.Answer; 86 87 import java.util.Collections; 88 import java.util.concurrent.atomic.AtomicBoolean; 89 90 /** 91 * Tests for {@link AccessibilityShortcutChooserActivity}. 92 */ 93 @RunWith(AndroidJUnit4.class) 94 public class AccessibilityShortcutChooserActivityTest { 95 private static final String ONE_HANDED_MODE = "One-Handed mode"; 96 private static final String ALLOW_LABEL = "Allow"; 97 private static final String DENY_LABEL = "Deny"; 98 private static final String UNINSTALL_LABEL = "Uninstall"; 99 private static final String EDIT_LABEL = "Edit shortcuts"; 100 private static final String LIST_TITLE_LABEL = "Choose features to use"; 101 private static final String TEST_LABEL = "TEST_LABEL"; 102 private static final String TEST_PACKAGE = "TEST_LABEL"; 103 private static final ComponentName TEST_COMPONENT_NAME = new ComponentName(TEST_PACKAGE, 104 "class"); 105 private static final long UI_TIMEOUT_MS = 1000; 106 private UiDevice mDevice; 107 private ActivityScenario<TestAccessibilityShortcutChooserActivity> mScenario; 108 private TestAccessibilityShortcutChooserActivity mActivity; 109 110 @Rule 111 public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); 112 113 @Rule 114 public final MockitoRule mMockitoRule = MockitoJUnit.rule(); 115 @Mock 116 private AccessibilityServiceInfo mAccessibilityServiceInfo; 117 @Mock 118 private ResolveInfo mResolveInfo; 119 @Mock 120 private ServiceInfo mServiceInfo; 121 @Mock 122 private ApplicationInfo mApplicationInfo; 123 @Mock 124 private IAccessibilityManager mAccessibilityManagerService; 125 @Mock 126 private KeyguardManager mKeyguardManager; 127 @Mock 128 private PackageManager mPackageManager; 129 @Mock 130 private PackageInstaller mPackageInstaller; 131 132 @Before setUp()133 public void setUp() throws Exception { 134 final PackageManager pm = 135 InstrumentationRegistry.getInstrumentation().getContext().getPackageManager(); 136 assumeFalse("AccessibilityShortcutChooserActivity not supported on watch", 137 pm.hasSystemFeature(PackageManager.FEATURE_WATCH)); 138 139 mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 140 mDevice.wakeUp(); 141 when(mAccessibilityServiceInfo.getResolveInfo()).thenReturn(mResolveInfo); 142 mResolveInfo.serviceInfo = mServiceInfo; 143 mServiceInfo.applicationInfo = mApplicationInfo; 144 when(mResolveInfo.loadLabel(any(PackageManager.class))).thenReturn(TEST_LABEL); 145 when(mAccessibilityServiceInfo.getComponentName()).thenReturn(TEST_COMPONENT_NAME); 146 when(mAccessibilityManagerService.getInstalledAccessibilityServiceList( 147 anyInt())).thenReturn(new ParceledListSlice<>( 148 Collections.singletonList(mAccessibilityServiceInfo))); 149 when(mAccessibilityManagerService.isAccessibilityServiceWarningRequired(any())) 150 .thenReturn(true); 151 when(mAccessibilityManagerService.isAccessibilityTargetAllowed( 152 anyString(), anyInt(), anyInt())).thenReturn(true); 153 when(mKeyguardManager.isKeyguardLocked()).thenReturn(false); 154 when(mPackageManager.getPackageInstaller()).thenReturn(mPackageInstaller); 155 156 TestAccessibilityShortcutChooserActivity.setupForTesting( 157 mAccessibilityManagerService, mKeyguardManager, 158 mPackageManager); 159 } 160 161 @After cleanUp()162 public void cleanUp() { 163 if (mScenario != null) { 164 mScenario.close(); 165 } 166 if (mActivity != null) { 167 Dialog permissionDialog = mActivity.getPermissionDialog(); 168 if (permissionDialog != null && permissionDialog.isShowing()) { 169 permissionDialog.dismiss(); 170 } 171 } 172 } 173 174 @Test selectTestService_permissionDialog_allow_rowChecked()175 public void selectTestService_permissionDialog_allow_rowChecked() { 176 launchActivity(); 177 openShortcutsList(); 178 179 mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS); 180 clickPermissionDialogButton(R.id.accessibility_permission_enable_allow_button); 181 182 assertThat(mDevice.wait(Until.hasObject(By.checked(true)), UI_TIMEOUT_MS)).isTrue(); 183 } 184 185 @Test selectTestService_permissionDialog_deny_rowNotChecked()186 public void selectTestService_permissionDialog_deny_rowNotChecked() { 187 launchActivity(); 188 openShortcutsList(); 189 190 mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS); 191 clickPermissionDialogButton(R.id.accessibility_permission_enable_deny_button); 192 193 assertThat(mDevice.wait(Until.hasObject(By.checked(true)), UI_TIMEOUT_MS)).isFalse(); 194 } 195 196 @Test selectTestService_permissionDialog_uninstall_callsUninstaller_rowRemoved()197 public void selectTestService_permissionDialog_uninstall_callsUninstaller_rowRemoved() { 198 launchActivity(); 199 openShortcutsList(); 200 201 mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS); 202 clickPermissionDialogButton(R.id.accessibility_permission_enable_uninstall_button); 203 204 verify(mPackageInstaller).uninstall(eq(TEST_PACKAGE), any()); 205 assertThat(mDevice.wait(Until.hasObject(By.textStartsWith(TEST_LABEL)), 206 UI_TIMEOUT_MS)).isFalse(); 207 } 208 209 @Test selectTestService_permissionDialog_notShownWhenNotRequired()210 public void selectTestService_permissionDialog_notShownWhenNotRequired() throws Exception { 211 when(mAccessibilityManagerService.isAccessibilityServiceWarningRequired(any())) 212 .thenReturn(false); 213 launchActivity(); 214 openShortcutsList(); 215 216 // Clicking the test service should not show a permission dialog window, 217 assertThat(mDevice.findObject(By.text(TEST_LABEL)).clickAndWait( 218 Until.newWindow(), UI_TIMEOUT_MS)).isFalse(); 219 // and should become checked. 220 assertThat(mDevice.findObject(By.checked(true))).isNotNull(); 221 } 222 223 @Test selectTestService_notPermittedByAdmin_blockedEvenIfNoWarningRequired()224 public void selectTestService_notPermittedByAdmin_blockedEvenIfNoWarningRequired() 225 throws Exception { 226 when(mAccessibilityManagerService.isAccessibilityServiceWarningRequired(any())) 227 .thenReturn(false); 228 when(mAccessibilityManagerService.isAccessibilityTargetAllowed( 229 eq(TEST_COMPONENT_NAME.getPackageName()), anyInt(), anyInt())).thenReturn(false); 230 // This test class mocks AccessibilityManagerService, so the restricted dialog window 231 // will not actually appear and therefore cannot be used for a wait Until.newWindow(). 232 // To still allow smart waiting in this test we can instead set up the mocked method 233 // to update an atomic boolean and wait for that to be set. 234 final Object waitObject = new Object(); 235 final AtomicBoolean calledSendRestrictedDialogIntent = new AtomicBoolean(false); 236 Mockito.doAnswer((Answer<Void>) invocation -> { 237 synchronized (waitObject) { 238 calledSendRestrictedDialogIntent.set(true); 239 waitObject.notify(); 240 } 241 return null; 242 }).when(mAccessibilityManagerService).sendRestrictedDialogIntent( 243 eq(TEST_COMPONENT_NAME.getPackageName()), anyInt(), anyInt()); 244 launchActivity(); 245 openShortcutsList(); 246 247 mDevice.findObject(By.text(TEST_LABEL)).click(); 248 final long timeout = System.currentTimeMillis() + UI_TIMEOUT_MS; 249 synchronized (waitObject) { 250 while (!calledSendRestrictedDialogIntent.get() && 251 (System.currentTimeMillis() < timeout)) { 252 waitObject.wait(timeout - System.currentTimeMillis()); 253 } 254 } 255 256 assertThat(calledSendRestrictedDialogIntent.get()).isTrue(); 257 assertThat(mDevice.findObject(By.checked(true))).isNull(); 258 } 259 260 @Test clickServiceTarget_notPermittedByAdmin_sendRestrictedDialogIntent()261 public void clickServiceTarget_notPermittedByAdmin_sendRestrictedDialogIntent() 262 throws Exception { 263 when(mAccessibilityManagerService.isAccessibilityTargetAllowed( 264 eq(TEST_COMPONENT_NAME.getPackageName()), anyInt(), anyInt())).thenReturn(false); 265 launchActivity(); 266 openShortcutsList(); 267 268 onView(withText(TEST_LABEL)).perform(scrollTo(), click()); 269 270 verify(mAccessibilityManagerService).sendRestrictedDialogIntent( 271 eq(TEST_COMPONENT_NAME.getPackageName()), anyInt(), anyInt()); 272 } 273 274 @Test popEditShortcutMenuList_oneHandedModeEnabled_shouldBeInListView()275 public void popEditShortcutMenuList_oneHandedModeEnabled_shouldBeInListView() { 276 TestUtils.setOneHandedModeEnabled(this, /* enabled= */ true); 277 launchActivity(); 278 openShortcutsList(); 279 280 onView(allOf(withClassName(endsWith("ListView")), isDisplayed())).perform(swipeUp()); 281 mDevice.wait(Until.hasObject(By.text(ONE_HANDED_MODE)), UI_TIMEOUT_MS); 282 283 onView(withText(ONE_HANDED_MODE)).inRoot(isDialog()).check(matches(isDisplayed())); 284 } 285 286 @Test popEditShortcutMenuList_oneHandedModeDisabled_shouldNotBeInListView()287 public void popEditShortcutMenuList_oneHandedModeDisabled_shouldNotBeInListView() { 288 TestUtils.setOneHandedModeEnabled(this, /* enabled= */ false); 289 launchActivity(); 290 openShortcutsList(); 291 292 onView(allOf(withClassName(endsWith("ListView")), isDisplayed())).perform(swipeUp()); 293 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 294 295 onView(withText(ONE_HANDED_MODE)).inRoot(isDialog()).check(doesNotExist()); 296 } 297 298 @Test createDialog_onLockscreen_hasExpectedContent()299 public void createDialog_onLockscreen_hasExpectedContent() { 300 when(mKeyguardManager.isKeyguardLocked()).thenReturn(true); 301 launchActivity(); 302 303 final AlertDialog dialog = mActivity.getMenuDialog(); 304 305 assertThat(dialog.getButton(AlertDialog.BUTTON_POSITIVE).getVisibility()) 306 .isEqualTo(View.GONE); 307 assertThat(dialog.getWindow().getAttributes().flags 308 & WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) 309 .isEqualTo(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); 310 } 311 launchActivity()312 private void launchActivity() { 313 mScenario = ActivityScenario.launch(TestAccessibilityShortcutChooserActivity.class); 314 mScenario.onActivity(activity -> mActivity = activity); 315 mScenario.moveToState(Lifecycle.State.CREATED); 316 mScenario.moveToState(Lifecycle.State.STARTED); 317 mScenario.moveToState(Lifecycle.State.RESUMED); 318 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 319 } 320 openShortcutsList()321 private void openShortcutsList() { 322 UiObject2 editButton = mDevice.findObject(By.text(EDIT_LABEL)); 323 if (editButton != null) { 324 editButton.click(); 325 } 326 mDevice.wait(Until.hasObject(By.textStartsWith(LIST_TITLE_LABEL)), UI_TIMEOUT_MS); 327 } 328 clickPermissionDialogButton(int buttonId)329 private void clickPermissionDialogButton(int buttonId) { 330 Button button = mActivity.getPermissionDialog().findViewById(buttonId); 331 mActivity.runOnUiThread(button::performClick); 332 // Wait for the dialog to go away by waiting for the shortcut chooser 333 // to become visible again. 334 assertThat(mDevice.wait(Until.hasObject(By.textStartsWith(LIST_TITLE_LABEL)), 335 UI_TIMEOUT_MS)).isTrue(); 336 } 337 338 /** 339 * Used for testing. 340 */ 341 public static class TestAccessibilityShortcutChooserActivity extends 342 AccessibilityShortcutChooserActivity { 343 private static IAccessibilityManager sAccessibilityManagerService; 344 private static KeyguardManager sKeyguardManager; 345 private static PackageManager sPackageManager; 346 setupForTesting( IAccessibilityManager accessibilityManagerService, KeyguardManager keyguardManager, PackageManager packageManager)347 public static void setupForTesting( 348 IAccessibilityManager accessibilityManagerService, 349 KeyguardManager keyguardManager, 350 PackageManager packageManager) { 351 sAccessibilityManagerService = accessibilityManagerService; 352 sKeyguardManager = keyguardManager; 353 sPackageManager = packageManager; 354 } 355 356 @Override onCreate(Bundle savedInstanceState)357 public void onCreate(Bundle savedInstanceState) { 358 super.onCreate(savedInstanceState); 359 // Setting the Theme is necessary here for the dialog to use the proper style 360 // resources as designated in its layout XML. 361 setTheme(R.style.Theme_DeviceDefault_DayNight); 362 } 363 364 @Override getPackageManager()365 public PackageManager getPackageManager() { 366 return sPackageManager; 367 } 368 369 @Override getSystemService(String name)370 public Object getSystemService(String name) { 371 if (Context.ACCESSIBILITY_SERVICE.equals(name) 372 && sAccessibilityManagerService != null) { 373 // Warning: This new AccessibilityManager complicates UI inspection 374 // because it breaks the expected "singleton per process" quality of 375 // AccessibilityManager. Debug here if observing unexpected issues 376 // with UI inspection or interaction. 377 return new AccessibilityManager(this, new Handler(getMainLooper()), 378 sAccessibilityManagerService, /* userId= */ 0, /* serviceConnect= */ true); 379 } 380 if (Context.KEYGUARD_SERVICE.equals(name)) { 381 return sKeyguardManager; 382 } 383 384 return super.getSystemService(name); 385 } 386 } 387 } 388