1 /* 2 * Copyright (C) 2017 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.doReturn; 23 import static org.mockito.Mockito.when; 24 import static org.robolectric.Shadows.shadowOf; 25 26 import static java.util.Collections.singletonList; 27 28 import android.accessibilityservice.AccessibilityServiceInfo; 29 import android.accessibilityservice.AccessibilityShortcutInfo; 30 import android.content.ComponentName; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.content.pm.ActivityInfo; 34 import android.content.pm.ApplicationInfo; 35 import android.content.pm.ResolveInfo; 36 import android.content.pm.ServiceInfo; 37 import android.database.ContentObserver; 38 import android.os.Build; 39 import android.platform.test.annotations.DisableFlags; 40 import android.platform.test.annotations.EnableFlags; 41 import android.platform.test.flag.junit.SetFlagsRule; 42 import android.provider.Settings; 43 import android.view.accessibility.AccessibilityManager; 44 import android.view.accessibility.Flags; 45 46 import androidx.fragment.app.Fragment; 47 import androidx.test.core.app.ApplicationProvider; 48 49 import com.android.internal.accessibility.util.AccessibilityUtils; 50 import com.android.settings.R; 51 import com.android.settings.SettingsActivity; 52 import com.android.settings.testutils.XmlTestUtils; 53 import com.android.settings.testutils.shadow.ShadowApplicationPackageManager; 54 import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; 55 import com.android.settings.testutils.shadow.ShadowBluetoothUtils; 56 import com.android.settings.testutils.shadow.ShadowRestrictedLockUtilsInternal; 57 import com.android.settings.testutils.shadow.ShadowUserManager; 58 import com.android.settingslib.RestrictedPreference; 59 import com.android.settingslib.bluetooth.LocalBluetoothManager; 60 import com.android.settingslib.search.SearchIndexableRaw; 61 import com.android.settingslib.testutils.shadow.ShadowColorDisplayManager; 62 63 import com.google.common.truth.BooleanSubject; 64 65 import org.junit.Before; 66 import org.junit.Rule; 67 import org.junit.Test; 68 import org.junit.runner.RunWith; 69 import org.mockito.Mock; 70 import org.mockito.Mockito; 71 import org.mockito.Spy; 72 import org.mockito.junit.MockitoJUnit; 73 import org.mockito.junit.MockitoRule; 74 import org.robolectric.RobolectricTestRunner; 75 import org.robolectric.android.controller.ActivityController; 76 import org.robolectric.annotation.Config; 77 import org.robolectric.shadow.api.Shadow; 78 import org.robolectric.shadows.ShadowAccessibilityManager; 79 import org.robolectric.shadows.ShadowContentResolver; 80 import org.xmlpull.v1.XmlPullParserException; 81 82 import java.io.IOException; 83 import java.util.ArrayList; 84 import java.util.Collection; 85 import java.util.List; 86 87 /** Test for {@link AccessibilitySettings}. */ 88 @RunWith(RobolectricTestRunner.class) 89 @Config(shadows = { 90 ShadowBluetoothAdapter.class, 91 ShadowUserManager.class, 92 ShadowColorDisplayManager.class, 93 ShadowApplicationPackageManager.class, 94 ShadowRestrictedLockUtilsInternal.class, 95 }) 96 public class AccessibilitySettingsTest { 97 private static final String PACKAGE_NAME = "com.android.test"; 98 private static final String CLASS_NAME = PACKAGE_NAME + ".test_a11y_service"; 99 private static final ComponentName COMPONENT_NAME = new ComponentName(PACKAGE_NAME, CLASS_NAME); 100 private static final String EMPTY_STRING = ""; 101 private static final String DEFAULT_SUMMARY = "default summary"; 102 private static final String DEFAULT_DESCRIPTION = "default description"; 103 private static final String DEFAULT_LABEL = "default label"; 104 private static final Boolean SERVICE_ENABLED = true; 105 private static final Boolean SERVICE_DISABLED = false; 106 107 @Rule 108 public final MockitoRule mocks = MockitoJUnit.rule(); 109 @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); 110 private final Context mContext = ApplicationProvider.getApplicationContext(); 111 @Spy 112 private final AccessibilityServiceInfo mServiceInfo = getMockAccessibilityServiceInfo( 113 PACKAGE_NAME, CLASS_NAME); 114 @Mock 115 private AccessibilityShortcutInfo mShortcutInfo; 116 private ShadowAccessibilityManager mShadowAccessibilityManager; 117 @Mock 118 private LocalBluetoothManager mLocalBluetoothManager; 119 private ActivityController<SettingsActivity> mActivityController; 120 private AccessibilitySettings mFragment; 121 122 @Before setup()123 public void setup() { 124 mShadowAccessibilityManager = Shadow.extract(AccessibilityManager.getInstance(mContext)); 125 mShadowAccessibilityManager.setInstalledAccessibilityServiceList(new ArrayList<>()); 126 mContext.setTheme(androidx.appcompat.R.style.Theme_AppCompat); 127 ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBluetoothManager; 128 setMockAccessibilityShortcutInfo(mShortcutInfo); 129 130 Intent intent = new Intent(); 131 intent.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT, 132 AccessibilitySettings.class.getName()); 133 134 mActivityController = ActivityController.of(new SettingsActivity(), intent); 135 } 136 137 @Test getNonIndexableKeys_existInXmlLayout()138 public void getNonIndexableKeys_existInXmlLayout() { 139 final List<String> niks = AccessibilitySettings.SEARCH_INDEX_DATA_PROVIDER 140 .getNonIndexableKeys(mContext); 141 final List<String> keys = 142 XmlTestUtils.getKeysFromPreferenceXml(mContext, R.xml.accessibility_settings); 143 144 assertThat(keys).containsAtLeastElementsIn(niks); 145 } 146 147 @Test getRawDataToIndex_isNull()148 public void getRawDataToIndex_isNull() { 149 final List<SearchIndexableRaw> indexableRawList = 150 AccessibilitySettings.SEARCH_INDEX_DATA_PROVIDER 151 .getRawDataToIndex(mContext, true); 152 153 assertThat(indexableRawList).isNull(); 154 } 155 156 @Test getServiceSummary_serviceCrash_showsStopped()157 public void getServiceSummary_serviceCrash_showsStopped() { 158 mServiceInfo.crashed = true; 159 160 final CharSequence summary = AccessibilitySettings.getServiceSummary(mContext, 161 mServiceInfo, SERVICE_ENABLED); 162 163 assertThat(summary).isEqualTo( 164 mContext.getString(R.string.accessibility_summary_state_stopped)); 165 } 166 167 @Test getServiceSummary_invisibleToggle_shortcutEnabled_showsOnSummary()168 public void getServiceSummary_invisibleToggle_shortcutEnabled_showsOnSummary() { 169 setInvisibleToggleFragmentType(mServiceInfo); 170 doReturn(DEFAULT_SUMMARY).when(mServiceInfo).loadSummary(any()); 171 setShortcutEnabled(mServiceInfo.getComponentName(), true); 172 173 final CharSequence summary = AccessibilitySettings.getServiceSummary(mContext, 174 mServiceInfo, SERVICE_ENABLED); 175 176 assertThat(summary).isEqualTo( 177 mContext.getString(R.string.preference_summary_default_combination, 178 mContext.getString(R.string.accessibility_summary_shortcut_enabled), 179 DEFAULT_SUMMARY)); 180 } 181 182 @Test getServiceSummary_invisibleToggle_shortcutDisabled_showsOffSummary()183 public void getServiceSummary_invisibleToggle_shortcutDisabled_showsOffSummary() { 184 setInvisibleToggleFragmentType(mServiceInfo); 185 setShortcutEnabled(mServiceInfo.getComponentName(), false); 186 doReturn(DEFAULT_SUMMARY).when(mServiceInfo).loadSummary(any()); 187 188 final CharSequence summary = AccessibilitySettings.getServiceSummary(mContext, 189 mServiceInfo, SERVICE_ENABLED); 190 191 assertThat(summary).isEqualTo( 192 mContext.getString(R.string.preference_summary_default_combination, 193 mContext.getString(R.string.generic_accessibility_feature_shortcut_off), 194 DEFAULT_SUMMARY)); 195 } 196 197 @Test getServiceSummary_enableServiceShortcutOn_showsServiceEnabledShortcutOn()198 public void getServiceSummary_enableServiceShortcutOn_showsServiceEnabledShortcutOn() { 199 doReturn(EMPTY_STRING).when(mServiceInfo).loadSummary(any()); 200 setShortcutEnabled(mServiceInfo.getComponentName(), true); 201 202 String summary = AccessibilitySettings.getServiceSummary(mContext, 203 mServiceInfo, SERVICE_ENABLED).toString(); 204 205 assertThat(summary).isEqualTo( 206 mContext.getString(R.string.generic_accessibility_service_on)); 207 } 208 209 @Test getServiceSummary_enableServiceShortcutOff_showsServiceEnabledShortcutOff()210 public void getServiceSummary_enableServiceShortcutOff_showsServiceEnabledShortcutOff() { 211 doReturn(EMPTY_STRING).when(mServiceInfo).loadSummary(any()); 212 setShortcutEnabled(mServiceInfo.getComponentName(), false); 213 214 String summary = AccessibilitySettings.getServiceSummary( 215 mContext, mServiceInfo, SERVICE_ENABLED).toString(); 216 217 assertThat(summary).isEqualTo( 218 mContext.getString(R.string.generic_accessibility_service_on)); 219 } 220 221 @Test getServiceSummary_disableServiceShortcutOff_showsDisabledShortcutOff()222 public void getServiceSummary_disableServiceShortcutOff_showsDisabledShortcutOff() { 223 doReturn(EMPTY_STRING).when(mServiceInfo).loadSummary(any()); 224 setShortcutEnabled(mServiceInfo.getComponentName(), false); 225 226 String summary = AccessibilitySettings.getServiceSummary(mContext, 227 mServiceInfo, SERVICE_DISABLED).toString(); 228 229 assertThat(summary).isEqualTo( 230 mContext.getString(R.string.generic_accessibility_service_off)); 231 } 232 233 @Test getServiceSummary_disableServiceShortcutOn_showsDisabledShortcutOn()234 public void getServiceSummary_disableServiceShortcutOn_showsDisabledShortcutOn() { 235 doReturn(EMPTY_STRING).when(mServiceInfo).loadSummary(any()); 236 setShortcutEnabled(mServiceInfo.getComponentName(), true); 237 238 String summary = AccessibilitySettings.getServiceSummary(mContext, 239 mServiceInfo, SERVICE_DISABLED).toString(); 240 241 assertThat(summary).isEqualTo( 242 mContext.getString(R.string.generic_accessibility_service_off)); 243 } 244 245 @Test getServiceSummary_enableServiceShortcutOffAndHasSummary_showsEnabledSummary()246 public void getServiceSummary_enableServiceShortcutOffAndHasSummary_showsEnabledSummary() { 247 setShortcutEnabled(mServiceInfo.getComponentName(), false); 248 doReturn(DEFAULT_SUMMARY).when(mServiceInfo).loadSummary(any()); 249 250 String summary = AccessibilitySettings.getServiceSummary(mContext, 251 mServiceInfo, SERVICE_ENABLED).toString(); 252 253 assertThat(summary).isEqualTo( 254 mContext.getString(R.string.preference_summary_default_combination, 255 mContext.getString(R.string.generic_accessibility_service_on), 256 DEFAULT_SUMMARY)); 257 } 258 259 @Test getServiceSummary_enableServiceShortcutOnAndHasSummary_showsEnabledSummary()260 public void getServiceSummary_enableServiceShortcutOnAndHasSummary_showsEnabledSummary() { 261 doReturn(DEFAULT_SUMMARY).when(mServiceInfo).loadSummary(any()); 262 setShortcutEnabled(mServiceInfo.getComponentName(), true); 263 264 String summary = AccessibilitySettings.getServiceSummary(mContext, 265 mServiceInfo, SERVICE_ENABLED).toString(); 266 267 assertThat(summary).isEqualTo( 268 mContext.getString(R.string.preference_summary_default_combination, 269 mContext.getString(R.string.generic_accessibility_service_on), 270 DEFAULT_SUMMARY)); 271 } 272 273 @Test getServiceSummary_disableServiceShortcutOnAndHasSummary_showsDisabledSummary()274 public void getServiceSummary_disableServiceShortcutOnAndHasSummary_showsDisabledSummary() { 275 doReturn(DEFAULT_SUMMARY).when(mServiceInfo).loadSummary(any()); 276 setShortcutEnabled(mServiceInfo.getComponentName(), true); 277 278 String summary = AccessibilitySettings.getServiceSummary(mContext, 279 mServiceInfo, SERVICE_DISABLED).toString(); 280 281 assertThat(summary).isEqualTo( 282 mContext.getString(R.string.preference_summary_default_combination, 283 mContext.getString(R.string.generic_accessibility_service_off), 284 DEFAULT_SUMMARY)); 285 } 286 287 @Test getServiceSummary_disableServiceShortcutOffAndHasSummary_showsDisabledSummary()288 public void getServiceSummary_disableServiceShortcutOffAndHasSummary_showsDisabledSummary() { 289 setShortcutEnabled(mServiceInfo.getComponentName(), false); 290 doReturn(DEFAULT_SUMMARY).when(mServiceInfo).loadSummary(any()); 291 292 String summary = AccessibilitySettings.getServiceSummary(mContext, 293 mServiceInfo, SERVICE_DISABLED).toString(); 294 295 assertThat(summary).isEqualTo( 296 mContext.getString(R.string.preference_summary_default_combination, 297 mContext.getString(R.string.generic_accessibility_service_off), 298 DEFAULT_SUMMARY)); 299 } 300 301 @Test getServiceDescription_serviceCrash_showsStopped()302 public void getServiceDescription_serviceCrash_showsStopped() { 303 mServiceInfo.crashed = true; 304 305 String description = AccessibilitySettings.getServiceDescription(mContext, 306 mServiceInfo, SERVICE_ENABLED).toString(); 307 308 assertThat(description).isEqualTo( 309 mContext.getString(R.string.accessibility_description_state_stopped)); 310 } 311 312 @Test getServiceDescription_haveDescription_showsDescription()313 public void getServiceDescription_haveDescription_showsDescription() { 314 doReturn(DEFAULT_DESCRIPTION).when(mServiceInfo).loadDescription(any()); 315 316 String description = AccessibilitySettings.getServiceDescription(mContext, 317 mServiceInfo, SERVICE_ENABLED).toString(); 318 319 assertThat(description).isEqualTo(DEFAULT_DESCRIPTION); 320 } 321 322 @Test 323 @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) onCreate_flagDisabled_haveRegisterToSpecificUrisAndActions()324 public void onCreate_flagDisabled_haveRegisterToSpecificUrisAndActions() { 325 setupFragment(); 326 327 assertUriObserversContainsClazz(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, 328 AccessibilitySettingsContentObserver.class).isTrue(); 329 assertUriObserversContainsClazz(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, 330 AccessibilitySettingsContentObserver.class).isTrue(); 331 assertUriObserversContainsClazz(Settings.Secure.ACCESSIBILITY_QS_TARGETS, 332 AccessibilitySettingsContentObserver.class).isFalse(); 333 } 334 335 @Test 336 @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) onCreate_flagEnabled_haveRegisterToSpecificUrisAndActions()337 public void onCreate_flagEnabled_haveRegisterToSpecificUrisAndActions() { 338 setupFragment(); 339 340 assertUriObserversContainsClazz(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, 341 AccessibilitySettingsContentObserver.class).isTrue(); 342 assertUriObserversContainsClazz(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, 343 AccessibilitySettingsContentObserver.class).isTrue(); 344 assertUriObserversContainsClazz(Settings.Secure.ACCESSIBILITY_QS_TARGETS, 345 AccessibilitySettingsContentObserver.class).isTrue(); 346 } 347 348 @Test onDestroy_unregisterObserverAndReceiver()349 public void onDestroy_unregisterObserverAndReceiver() { 350 setupFragment(); 351 352 mActivityController.pause().stop().destroy(); 353 354 assertUriObserversContainsClazz(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, 355 AccessibilitySettingsContentObserver.class).isFalse(); 356 assertUriObserversContainsClazz(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, 357 AccessibilitySettingsContentObserver.class).isFalse(); 358 assertUriObserversContainsClazz(Settings.Secure.ACCESSIBILITY_QS_TARGETS, 359 AccessibilitySettingsContentObserver.class).isFalse(); 360 } 361 362 @Test onContentChanged_updatePreferenceInForeground_preferenceUpdated()363 public void onContentChanged_updatePreferenceInForeground_preferenceUpdated() { 364 setupFragment(); 365 mShadowAccessibilityManager.setInstalledAccessibilityServiceList( 366 singletonList(mServiceInfo)); 367 368 mFragment.onContentChanged(); 369 370 RestrictedPreference preference = mFragment.getPreferenceScreen().findPreference( 371 COMPONENT_NAME.flattenToString()); 372 373 assertThat(preference).isNotNull(); 374 375 } 376 377 @Test onContentChanged_updatePreferenceInBackground_preferenceUpdated()378 public void onContentChanged_updatePreferenceInBackground_preferenceUpdated() { 379 setupFragment(); 380 mFragment.onPause(); 381 mFragment.onStop(); 382 383 mShadowAccessibilityManager.setInstalledAccessibilityServiceList( 384 singletonList(mServiceInfo)); 385 386 mFragment.onContentChanged(); 387 mFragment.onStart(); 388 mFragment.onResume(); 389 390 RestrictedPreference preference = mFragment.getPreferenceScreen().findPreference( 391 COMPONENT_NAME.flattenToString()); 392 393 assertThat(preference).isNotNull(); 394 395 } 396 397 @Test testAccessibilityMenuInSystem_IncludedInInteractionControl()398 public void testAccessibilityMenuInSystem_IncludedInInteractionControl() { 399 mShadowAccessibilityManager.setInstalledAccessibilityServiceList( 400 List.of(getMockAccessibilityServiceInfo( 401 AccessibilityUtils.ACCESSIBILITY_MENU_IN_SYSTEM))); 402 setupFragment(); 403 404 final RestrictedPreference pref = mFragment.getPreferenceScreen().findPreference( 405 AccessibilityUtils.ACCESSIBILITY_MENU_IN_SYSTEM.flattenToString()); 406 final String prefCategory = mFragment.mServicePreferenceToPreferenceCategoryMap.get( 407 pref).getKey(); 408 assertThat(prefCategory).isEqualTo(AccessibilitySettings.CATEGORY_INTERACTION_CONTROL); 409 } 410 411 @Test testAccessibilityMenuInSystem_NoPrefWhenNotInstalled()412 public void testAccessibilityMenuInSystem_NoPrefWhenNotInstalled() { 413 mShadowAccessibilityManager.setInstalledAccessibilityServiceList(List.of()); 414 setupFragment(); 415 416 final RestrictedPreference pref = mFragment.getPreferenceScreen().findPreference( 417 AccessibilityUtils.ACCESSIBILITY_MENU_IN_SYSTEM.flattenToString()); 418 assertThat(pref).isNull(); 419 } 420 getMockAccessibilityServiceInfo(String packageName, String className)421 private AccessibilityServiceInfo getMockAccessibilityServiceInfo(String packageName, 422 String className) { 423 return getMockAccessibilityServiceInfo(new ComponentName(packageName, className)); 424 } 425 getMockAccessibilityServiceInfo(ComponentName componentName)426 private AccessibilityServiceInfo getMockAccessibilityServiceInfo(ComponentName componentName) { 427 final ApplicationInfo applicationInfo = new ApplicationInfo(); 428 final ServiceInfo serviceInfo = new ServiceInfo(); 429 applicationInfo.packageName = componentName.getPackageName(); 430 serviceInfo.packageName = componentName.getPackageName(); 431 serviceInfo.name = componentName.getClassName(); 432 serviceInfo.applicationInfo = applicationInfo; 433 434 final ResolveInfo resolveInfo = new ResolveInfo(); 435 resolveInfo.serviceInfo = serviceInfo; 436 try { 437 final AccessibilityServiceInfo info = new AccessibilityServiceInfo(resolveInfo, 438 mContext); 439 info.setComponentName(componentName); 440 return info; 441 } catch (XmlPullParserException | IOException e) { 442 // Do nothing 443 } 444 445 return null; 446 } 447 setMockAccessibilityShortcutInfo(AccessibilityShortcutInfo mockInfo)448 private void setMockAccessibilityShortcutInfo(AccessibilityShortcutInfo mockInfo) { 449 final ActivityInfo activityInfo = Mockito.mock(ActivityInfo.class); 450 activityInfo.applicationInfo = new ApplicationInfo(); 451 when(mockInfo.getActivityInfo()).thenReturn(activityInfo); 452 when(activityInfo.loadLabel(any())).thenReturn(DEFAULT_LABEL); 453 when(mockInfo.loadSummary(any())).thenReturn(DEFAULT_SUMMARY); 454 when(mockInfo.loadDescription(any())).thenReturn(DEFAULT_DESCRIPTION); 455 when(mockInfo.getComponentName()).thenReturn(COMPONENT_NAME); 456 } 457 setInvisibleToggleFragmentType(AccessibilityServiceInfo info)458 private void setInvisibleToggleFragmentType(AccessibilityServiceInfo info) { 459 info.getResolveInfo().serviceInfo.applicationInfo.targetSdkVersion = Build.VERSION_CODES.R; 460 info.flags |= AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON; 461 } 462 setupFragment()463 private void setupFragment() { 464 mActivityController.create().start().resume(); 465 Fragment fragment = mActivityController.get().getSupportFragmentManager().findFragmentById( 466 R.id.main_content); 467 468 assertThat(fragment).isNotNull(); 469 assertThat(fragment).isInstanceOf(AccessibilitySettings.class); 470 471 mFragment = (AccessibilitySettings) fragment; 472 } 473 setShortcutEnabled(ComponentName componentName, boolean enabled)474 private void setShortcutEnabled(ComponentName componentName, boolean enabled) { 475 Settings.Secure.putString(mContext.getContentResolver(), 476 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, 477 enabled ? componentName.flattenToString() : ""); 478 } 479 assertUriObserversContainsClazz( String settingUri, Class<?> clazz)480 private BooleanSubject assertUriObserversContainsClazz( 481 String settingUri, Class<?> clazz) { 482 ShadowContentResolver shadowContentResolver = shadowOf(mContext.getContentResolver()); 483 Collection<ContentObserver> observers = 484 shadowContentResolver.getContentObservers( 485 Settings.Secure.getUriFor(settingUri)); 486 487 return assertThat(observers.stream().anyMatch(clazz::isInstance)); 488 } 489 } 490