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.settings.accessibility; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import static org.mockito.ArgumentMatchers.any; 22 import static org.mockito.Mockito.doNothing; 23 import static org.mockito.Mockito.doReturn; 24 import static org.mockito.Mockito.spy; 25 import static org.mockito.Mockito.verify; 26 import static org.mockito.Mockito.when; 27 28 import android.accessibilityservice.AccessibilityServiceInfo; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.pm.ApplicationInfo; 33 import android.content.pm.PackageManager; 34 import android.content.pm.ResolveInfo; 35 import android.content.pm.ServiceInfo; 36 import android.os.Bundle; 37 import android.service.quicksettings.TileService; 38 import android.view.accessibility.AccessibilityManager; 39 40 import androidx.annotation.NonNull; 41 import androidx.preference.PreferenceManager; 42 import androidx.preference.PreferenceScreen; 43 import androidx.test.core.app.ApplicationProvider; 44 45 import com.android.internal.accessibility.common.ShortcutConstants; 46 import com.android.settings.SettingsActivity; 47 import com.android.settings.accessibility.shortcuts.EditShortcutsPreferenceFragment; 48 import com.android.settings.widget.SettingsMainSwitchPreference; 49 50 import org.junit.Before; 51 import org.junit.Test; 52 import org.junit.runner.RunWith; 53 import org.mockito.Answers; 54 import org.mockito.ArgumentCaptor; 55 import org.mockito.Mock; 56 import org.mockito.MockitoAnnotations; 57 import org.robolectric.RobolectricTestRunner; 58 import org.robolectric.Shadows; 59 import org.robolectric.shadow.api.Shadow; 60 import org.robolectric.shadows.ShadowAccessibilityManager; 61 import org.robolectric.shadows.ShadowPackageManager; 62 63 import java.util.List; 64 import java.util.Set; 65 66 /** Tests for {@link ToggleAccessibilityServicePreferenceFragment} */ 67 @RunWith(RobolectricTestRunner.class) 68 public class ToggleAccessibilityServicePreferenceFragmentTest { 69 70 private static final String PLACEHOLDER_PACKAGE_NAME = "com.placeholder.example"; 71 private static final String PLACEHOLDER_PACKAGE_NAME2 = "com.placeholder.example2"; 72 private static final String PLACEHOLDER_SERVICE_CLASS_NAME = "a11yservice1"; 73 private static final String PLACEHOLDER_SERVICE_CLASS_NAME2 = "a11yservice2"; 74 private static final String PLACEHOLDER_TILE_CLASS_NAME = 75 PLACEHOLDER_PACKAGE_NAME + "tile.placeholder"; 76 private static final String PLACEHOLDER_TILE_CLASS_NAME2 = 77 PLACEHOLDER_PACKAGE_NAME + "tile.placeholder2"; 78 private static final ComponentName PLACEHOLDER_TILE_COMPONENT_NAME = new ComponentName( 79 PLACEHOLDER_PACKAGE_NAME, PLACEHOLDER_TILE_CLASS_NAME); 80 private static final String PLACEHOLDER_TILE_NAME = 81 PLACEHOLDER_PACKAGE_NAME + "tile.placeholder"; 82 private static final String PLACEHOLDER_TILE_NAME2 = 83 PLACEHOLDER_PACKAGE_NAME + "tile.placeholder2"; 84 private static final int NO_DIALOG = -1; 85 86 private TestToggleAccessibilityServicePreferenceFragment mFragment; 87 private PreferenceScreen mScreen; 88 private Context mContext; 89 90 private ShadowAccessibilityManager mShadowAccessibilityManager; 91 @Mock(answer = Answers.RETURNS_DEEP_STUBS) 92 private PreferenceManager mPreferenceManager; 93 @Mock 94 private AccessibilityManager mMockAccessibilityManager; 95 96 @Before setUpTestFragment()97 public void setUpTestFragment() { 98 MockitoAnnotations.initMocks(this); 99 100 mContext = spy(ApplicationProvider.getApplicationContext()); 101 mFragment = spy(new TestToggleAccessibilityServicePreferenceFragment()); 102 mFragment.setArguments(new Bundle()); 103 when(mFragment.getPreferenceManager()).thenReturn(mPreferenceManager); 104 when(mFragment.getPreferenceManager().getContext()).thenReturn(mContext); 105 when(mFragment.getContext()).thenReturn(mContext); 106 mScreen = spy(new PreferenceScreen(mContext, /* attrs= */ null)); 107 when(mScreen.getPreferenceManager()).thenReturn(mPreferenceManager); 108 doReturn(mScreen).when(mFragment).getPreferenceScreen(); 109 mShadowAccessibilityManager = Shadow.extract(AccessibilityManager.getInstance(mContext)); 110 } 111 112 @Test getAccessibilityServiceInfo()113 public void getAccessibilityServiceInfo() throws Throwable { 114 final AccessibilityServiceInfo info1 = getFakeAccessibilityServiceInfo( 115 PLACEHOLDER_PACKAGE_NAME, 116 PLACEHOLDER_SERVICE_CLASS_NAME); 117 final AccessibilityServiceInfo info2 = getFakeAccessibilityServiceInfo( 118 PLACEHOLDER_PACKAGE_NAME, 119 PLACEHOLDER_SERVICE_CLASS_NAME2); 120 final AccessibilityServiceInfo info3 = getFakeAccessibilityServiceInfo( 121 PLACEHOLDER_PACKAGE_NAME2, 122 PLACEHOLDER_SERVICE_CLASS_NAME); 123 final AccessibilityServiceInfo info4 = getFakeAccessibilityServiceInfo( 124 PLACEHOLDER_PACKAGE_NAME2, 125 PLACEHOLDER_SERVICE_CLASS_NAME2); 126 mShadowAccessibilityManager.setInstalledAccessibilityServiceList( 127 List.of(info1, info2, info3, info4)); 128 129 mFragment.mComponentName = info3.getComponentName(); 130 131 assertThat(mFragment.getAccessibilityServiceInfo()).isEqualTo(info3); 132 } 133 134 @Test getAccessibilityServiceInfo_notFound()135 public void getAccessibilityServiceInfo_notFound() throws Throwable { 136 mShadowAccessibilityManager.setInstalledAccessibilityServiceList(List.of()); 137 138 mFragment.mComponentName = getFakeAccessibilityServiceInfo(PLACEHOLDER_PACKAGE_NAME, 139 PLACEHOLDER_SERVICE_CLASS_NAME).getComponentName(); 140 141 assertThat(mFragment.getAccessibilityServiceInfo()).isNull(); 142 } 143 144 @Test serviceSupportsAccessibilityButton()145 public void serviceSupportsAccessibilityButton() throws Throwable { 146 final AccessibilityServiceInfo infoWithA11yButton = getFakeAccessibilityServiceInfo( 147 PLACEHOLDER_PACKAGE_NAME, 148 PLACEHOLDER_SERVICE_CLASS_NAME); 149 infoWithA11yButton.flags = AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON; 150 final AccessibilityServiceInfo infoWithoutA11yButton = getFakeAccessibilityServiceInfo( 151 PLACEHOLDER_PACKAGE_NAME2, 152 PLACEHOLDER_SERVICE_CLASS_NAME2); 153 infoWithoutA11yButton.flags = 0; 154 mShadowAccessibilityManager.setInstalledAccessibilityServiceList( 155 List.of(infoWithA11yButton, infoWithoutA11yButton)); 156 157 mFragment.mComponentName = infoWithA11yButton.getComponentName(); 158 assertThat(mFragment.serviceSupportsAccessibilityButton()).isTrue(); 159 mFragment.mComponentName = infoWithoutA11yButton.getComponentName(); 160 assertThat(mFragment.serviceSupportsAccessibilityButton()).isFalse(); 161 } 162 163 @Test enableService_warningRequired_showWarning()164 public void enableService_warningRequired_showWarning() throws Throwable { 165 setupServiceWarningRequired(true); 166 mFragment.mToggleServiceSwitchPreference = 167 new SettingsMainSwitchPreference(mContext, /* attrs= */null); 168 169 mFragment.onCheckedChanged(null, true); 170 171 assertThat(mFragment.mLastShownDialogId).isEqualTo( 172 AccessibilityDialogUtils.DialogEnums.ENABLE_WARNING_FROM_TOGGLE); 173 } 174 175 @Test enableService_warningNotRequired_dontShowWarning()176 public void enableService_warningNotRequired_dontShowWarning() throws Throwable { 177 final AccessibilityServiceInfo info = setupServiceWarningRequired(false); 178 mFragment.mToggleServiceSwitchPreference = 179 new SettingsMainSwitchPreference(mContext, /* attrs= */null); 180 mFragment.mPreferenceKey = info.getComponentName().flattenToString(); 181 182 mFragment.onCheckedChanged(null, true); 183 184 assertThat(mFragment.mLastShownDialogId).isEqualTo( 185 AccessibilityDialogUtils.DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL); 186 } 187 188 @Test toggleShortcutPreference_warningRequired_showWarning()189 public void toggleShortcutPreference_warningRequired_showWarning() throws Throwable { 190 setupServiceWarningRequired(true); 191 mFragment.mShortcutPreference = new ShortcutPreference(mContext, /* attrs= */null); 192 193 mFragment.mShortcutPreference.setChecked(true); 194 mFragment.onToggleClicked(mFragment.mShortcutPreference); 195 196 assertThat(mFragment.mLastShownDialogId).isEqualTo( 197 AccessibilityDialogUtils.DialogEnums.ENABLE_WARNING_FROM_SHORTCUT_TOGGLE); 198 assertThat(mFragment.mShortcutPreference.isChecked()).isFalse(); 199 } 200 201 @Test toggleShortcutPreference_warningNotRequired_dontShowWarning()202 public void toggleShortcutPreference_warningNotRequired_dontShowWarning() throws Throwable { 203 setupServiceWarningRequired(false); 204 mFragment.mShortcutPreference = new ShortcutPreference(mContext, /* attrs= */null); 205 206 mFragment.mShortcutPreference.setChecked(true); 207 mFragment.onToggleClicked(mFragment.mShortcutPreference); 208 209 assertThat(mFragment.mLastShownDialogId).isEqualTo( 210 AccessibilityDialogUtils.DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL); 211 assertThat(mFragment.mShortcutPreference.isChecked()).isTrue(); 212 } 213 214 @Test clickShortcutSettingsPreference_warningRequired_showWarning()215 public void clickShortcutSettingsPreference_warningRequired_showWarning() throws Throwable { 216 setupServiceWarningRequired(true); 217 mFragment.mShortcutPreference = new ShortcutPreference(mContext, /* attrs= */null); 218 219 mFragment.onSettingsClicked(mFragment.mShortcutPreference); 220 221 assertThat(mFragment.mLastShownDialogId).isEqualTo( 222 AccessibilityDialogUtils.DialogEnums.ENABLE_WARNING_FROM_SHORTCUT); 223 } 224 225 @Test clickShortcutSettingsPreference_warningNotRequired_dontShowWarning_launchActivity()226 public void clickShortcutSettingsPreference_warningNotRequired_dontShowWarning_launchActivity() 227 throws Throwable { 228 setupServiceWarningRequired(false); 229 mFragment.mShortcutPreference = new ShortcutPreference(mContext, /* attrs= */null); 230 doNothing().when(mContext).startActivity(any()); 231 232 mFragment.onSettingsClicked(mFragment.mShortcutPreference); 233 234 ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); 235 verify(mContext).startActivity(captor.capture()); 236 assertThat(captor.getValue().getExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) 237 .isEqualTo(EditShortcutsPreferenceFragment.class.getName()); 238 } 239 240 @Test getDefaultShortcutTypes_isAccessibilityTool_hasAssociatedTile_qsTypeIsDefault()241 public void getDefaultShortcutTypes_isAccessibilityTool_hasAssociatedTile_qsTypeIsDefault() 242 throws Throwable { 243 PreferredShortcuts.clearPreferredShortcuts(mContext); 244 setupAccessibilityServiceInfoForFragment( 245 /* isAccessibilityTool= */ true, 246 /* tileService= */ PLACEHOLDER_TILE_COMPONENT_NAME 247 /* warningRequired= */); 248 249 assertThat(mFragment.getDefaultShortcutTypes()) 250 .isEqualTo(ShortcutConstants.UserShortcutType.QUICK_SETTINGS); 251 } 252 253 @Test getDefaultShortcutTypes_isNotAccessibilityTool_hasAssociatedTile_softwareTypeIsDefault()254 public void getDefaultShortcutTypes_isNotAccessibilityTool_hasAssociatedTile_softwareTypeIsDefault() 255 throws Throwable { 256 PreferredShortcuts.clearPreferredShortcuts(mContext); 257 setupAccessibilityServiceInfoForFragment( 258 /* isAccessibilityTool= */ false, 259 /* tileService= */ PLACEHOLDER_TILE_COMPONENT_NAME 260 /* warningRequired= */); 261 262 assertThat(mFragment.getDefaultShortcutTypes()) 263 .isEqualTo(ShortcutConstants.UserShortcutType.SOFTWARE); 264 } 265 266 @Test getDefaultShortcutTypes_isAccessibilityTool_noAssociatedTile_softwareTypeIsDefault()267 public void getDefaultShortcutTypes_isAccessibilityTool_noAssociatedTile_softwareTypeIsDefault() 268 throws Throwable { 269 PreferredShortcuts.clearPreferredShortcuts(mContext); 270 setupAccessibilityServiceInfoForFragment( 271 /* isAccessibilityTool= */ true, 272 /* tileService= */ null 273 /* warningRequired= */); 274 275 assertThat(mFragment.getDefaultShortcutTypes()) 276 .isEqualTo(ShortcutConstants.UserShortcutType.SOFTWARE); 277 } 278 279 @Test getDefaultShortcutTypes_isNotAccessibilityTool_noAssociatedTile_softwareTypeIsDefault()280 public void getDefaultShortcutTypes_isNotAccessibilityTool_noAssociatedTile_softwareTypeIsDefault() 281 throws Throwable { 282 PreferredShortcuts.clearPreferredShortcuts(mContext); 283 setupAccessibilityServiceInfoForFragment( 284 /* isAccessibilityTool= */ false, 285 /* tileService= */ null 286 /* warningRequired= */); 287 288 assertThat(mFragment.getDefaultShortcutTypes()) 289 .isEqualTo(ShortcutConstants.UserShortcutType.SOFTWARE); 290 } 291 292 @Test toggleShortcutPreference_noUserPreferredShortcut_hasQsTile_enableQsShortcut()293 public void toggleShortcutPreference_noUserPreferredShortcut_hasQsTile_enableQsShortcut() 294 throws Throwable { 295 PreferredShortcuts.clearPreferredShortcuts(mContext); 296 setupAccessibilityServiceInfoForFragment( 297 /* isAccessibilityTool= */ true, 298 /* tileService= */ PLACEHOLDER_TILE_COMPONENT_NAME 299 /* warningRequired= */); 300 mFragment.mShortcutPreference = new ShortcutPreference(mContext, /* attrs= */ null); 301 302 mFragment.mShortcutPreference.setChecked(true); 303 mFragment.onToggleClicked(mFragment.mShortcutPreference); 304 305 verify(mMockAccessibilityManager) 306 .enableShortcutsForTargets(true, 307 ShortcutConstants.UserShortcutType.QUICK_SETTINGS, 308 Set.of(mFragment.mComponentName.flattenToString()), mContext.getUserId()); 309 } 310 311 @Test toggleShortcutPreference_noUserPreferredShortcut_noQsTile_enableSoftwareShortcut()312 public void toggleShortcutPreference_noUserPreferredShortcut_noQsTile_enableSoftwareShortcut() 313 throws Throwable { 314 PreferredShortcuts.clearPreferredShortcuts(mContext); 315 setupAccessibilityServiceInfoForFragment( 316 /* isAccessibilityTool= */ true, 317 /* tileService= */ null 318 /* warningRequired= */); 319 mFragment.mShortcutPreference = new ShortcutPreference(mContext, /* attrs= */ null); 320 321 mFragment.mShortcutPreference.setChecked(true); 322 mFragment.onToggleClicked(mFragment.mShortcutPreference); 323 324 verify(mMockAccessibilityManager) 325 .enableShortcutsForTargets(true, 326 ShortcutConstants.UserShortcutType.SOFTWARE, 327 Set.of(mFragment.mComponentName.flattenToString()), mContext.getUserId()); 328 } 329 330 @Test toggleShortcutPreference_userPreferVolumeKeysShortcut_noQsTile_enableVolumeKeysShortcut()331 public void toggleShortcutPreference_userPreferVolumeKeysShortcut_noQsTile_enableVolumeKeysShortcut() 332 throws Throwable { 333 setupAccessibilityServiceInfoForFragment( 334 /* isAccessibilityTool= */ true, 335 /* tileService= */ null 336 /* warningRequired= */); 337 String componentName = mFragment.mComponentName.flattenToString(); 338 PreferredShortcuts.saveUserShortcutType( 339 mContext, 340 new PreferredShortcut(componentName, ShortcutConstants.UserShortcutType.HARDWARE)); 341 mFragment.mShortcutPreference = new ShortcutPreference(mContext, /* attrs= */ null); 342 343 mFragment.mShortcutPreference.setChecked(true); 344 mFragment.onToggleClicked(mFragment.mShortcutPreference); 345 346 verify(mMockAccessibilityManager) 347 .enableShortcutsForTargets( 348 true, 349 ShortcutConstants.UserShortcutType.HARDWARE, 350 Set.of(componentName), 351 mContext.getUserId()); 352 } 353 354 @Test toggleShortcutPreference_userPreferVolumeKeysShortcut_hasQsTile_enableVolumeKeysShortcut()355 public void toggleShortcutPreference_userPreferVolumeKeysShortcut_hasQsTile_enableVolumeKeysShortcut() 356 throws Throwable { 357 setupAccessibilityServiceInfoForFragment( 358 /* isAccessibilityTool= */ true, 359 /* tileService= */ PLACEHOLDER_TILE_COMPONENT_NAME 360 /* warningRequired= */); 361 String componentName = mFragment.mComponentName.flattenToString(); 362 PreferredShortcuts.saveUserShortcutType( 363 mContext, 364 new PreferredShortcut(componentName, ShortcutConstants.UserShortcutType.HARDWARE)); 365 mFragment.mShortcutPreference = new ShortcutPreference(mContext, /* attrs= */ null); 366 367 mFragment.mShortcutPreference.setChecked(true); 368 mFragment.onToggleClicked(mFragment.mShortcutPreference); 369 370 verify(mMockAccessibilityManager) 371 .enableShortcutsForTargets( 372 true, 373 ShortcutConstants.UserShortcutType.HARDWARE, 374 Set.of(componentName), 375 mContext.getUserId()); 376 } 377 setupTileService(String packageName, String name, String tileName)378 private void setupTileService(String packageName, String name, String tileName) { 379 final Intent tileProbe = new Intent(TileService.ACTION_QS_TILE); 380 final ResolveInfo info = new ResolveInfo(); 381 info.serviceInfo = new FakeServiceInfo(packageName, name, tileName); 382 final ShadowPackageManager shadowPackageManager = 383 Shadows.shadowOf(mContext.getPackageManager()); 384 shadowPackageManager.addResolveInfoForIntent(tileProbe, info); 385 } 386 setupServiceWarningRequired(boolean required)387 private AccessibilityServiceInfo setupServiceWarningRequired(boolean required) 388 throws Throwable { 389 final AccessibilityServiceInfo info = getFakeAccessibilityServiceInfo( 390 PLACEHOLDER_PACKAGE_NAME, 391 PLACEHOLDER_SERVICE_CLASS_NAME); 392 info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON; 393 mFragment.mComponentName = info.getComponentName(); 394 when(mContext.getSystemService(AccessibilityManager.class)) 395 .thenReturn(mMockAccessibilityManager); 396 when(mMockAccessibilityManager.isAccessibilityServiceWarningRequired(any())) 397 .thenReturn(required); 398 when(mFragment.getAccessibilityServiceInfo()).thenReturn(info); 399 return info; 400 } 401 setupAccessibilityServiceInfoForFragment( boolean isAccessibilityTool, ComponentName tileService)402 private void setupAccessibilityServiceInfoForFragment( 403 boolean isAccessibilityTool, ComponentName tileService) throws Throwable { 404 AccessibilityServiceInfo info = setupServiceWarningRequired(false); 405 info.setAccessibilityTool(isAccessibilityTool); 406 mShadowAccessibilityManager.setInstalledAccessibilityServiceList(List.of(info)); 407 mFragment.mComponentName = info.getComponentName(); 408 when(mFragment.getTileComponentName()).thenReturn(tileService); 409 when(mFragment.getAccessibilityServiceInfo()).thenReturn(info); 410 } 411 412 private static class FakeServiceInfo extends ServiceInfo { 413 private String mTileName; 414 FakeServiceInfo(String packageName, String name, String tileName)415 FakeServiceInfo(String packageName, String name, String tileName) { 416 this.packageName = packageName; 417 this.name = name; 418 mTileName = tileName; 419 } 420 loadLabel(PackageManager mgr)421 public String loadLabel(PackageManager mgr) { 422 return mTileName; 423 } 424 } 425 426 @NonNull getFakeAccessibilityServiceInfo(String packageName, String className)427 private AccessibilityServiceInfo getFakeAccessibilityServiceInfo(String packageName, 428 String className) throws Throwable { 429 final ApplicationInfo applicationInfo = new ApplicationInfo(); 430 final ServiceInfo serviceInfo = new ServiceInfo(); 431 applicationInfo.packageName = packageName; 432 serviceInfo.packageName = packageName; 433 serviceInfo.name = className; 434 serviceInfo.applicationInfo = applicationInfo; 435 final ResolveInfo resolveInfo = new ResolveInfo(); 436 resolveInfo.serviceInfo = serviceInfo; 437 final AccessibilityServiceInfo info = new AccessibilityServiceInfo(resolveInfo, mContext); 438 ComponentName componentName = ComponentName.createRelative(packageName, className); 439 info.setComponentName(componentName); 440 return info; 441 } 442 443 private static class TestToggleAccessibilityServicePreferenceFragment 444 extends ToggleAccessibilityServicePreferenceFragment { 445 int mLastShownDialogId = NO_DIALOG; 446 447 @Override getTileComponentName()448 protected ComponentName getTileComponentName() { 449 return PLACEHOLDER_TILE_COMPONENT_NAME; 450 } 451 452 @Override showDialog(int dialogId)453 protected void showDialog(int dialogId) { 454 mLastShownDialogId = dialogId; 455 } 456 } 457 } 458