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