• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 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 android.accessibilityservice.AccessibilityServiceInfo;
20 import android.accessibilityservice.AccessibilityShortcutInfo;
21 import android.app.settings.SettingsEnums;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.content.pm.ActivityInfo;
25 import android.content.pm.ServiceInfo;
26 import android.os.Bundle;
27 import android.os.Handler;
28 import android.os.UserHandle;
29 import android.provider.Settings;
30 import android.text.TextUtils;
31 import android.util.ArrayMap;
32 import android.view.accessibility.AccessibilityManager;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.VisibleForTesting;
36 import androidx.preference.Preference;
37 import androidx.preference.PreferenceCategory;
38 
39 import com.android.internal.accessibility.AccessibilityShortcutController;
40 import com.android.internal.accessibility.util.AccessibilityUtils;
41 import com.android.internal.content.PackageMonitor;
42 import com.android.settings.R;
43 import com.android.settings.accessibility.AccessibilityUtil.AccessibilityServiceFragmentType;
44 import com.android.settings.dashboard.DashboardFragment;
45 import com.android.settings.overlay.FeatureFactory;
46 import com.android.settings.search.BaseSearchIndexProvider;
47 import com.android.settingslib.RestrictedPreference;
48 import com.android.settingslib.core.AbstractPreferenceController;
49 import com.android.settingslib.search.SearchIndexable;
50 import com.android.settingslib.search.SearchIndexableRaw;
51 
52 import java.util.ArrayList;
53 import java.util.Collection;
54 import java.util.List;
55 import java.util.Map;
56 
57 /** Activity with the accessibility settings. */
58 @SearchIndexable(forTarget = SearchIndexable.ALL & ~SearchIndexable.ARC)
59 public class AccessibilitySettings extends DashboardFragment {
60 
61     private static final String TAG = "AccessibilitySettings";
62 
63     // Preference categories
64     private static final String CATEGORY_SCREEN_READER = "screen_reader_category";
65     private static final String CATEGORY_CAPTIONS = "captions_category";
66     private static final String CATEGORY_AUDIO = "audio_category";
67     private static final String CATEGORY_SPEECH = "speech_category";
68     private static final String CATEGORY_DISPLAY = "display_category";
69     private static final String CATEGORY_DOWNLOADED_SERVICES = "user_installed_services_category";
70     @VisibleForTesting
71     static final String CATEGORY_INTERACTION_CONTROL = "interaction_control_category";
72 
73     private static final String[] CATEGORIES = new String[]{
74             CATEGORY_SCREEN_READER, CATEGORY_CAPTIONS, CATEGORY_AUDIO, CATEGORY_DISPLAY,
75             CATEGORY_SPEECH, CATEGORY_INTERACTION_CONTROL, CATEGORY_DOWNLOADED_SERVICES
76     };
77 
78     // Extras passed to sub-fragments.
79     static final String EXTRA_PREFERENCE_KEY = "preference_key";
80     static final String EXTRA_CHECKED = "checked";
81     static final String EXTRA_TITLE = "title";
82     static final String EXTRA_TITLE_RES = "title_res";
83     static final String EXTRA_RESOLVE_INFO = "resolve_info";
84     static final String EXTRA_SUMMARY = "summary";
85     static final String EXTRA_INTRO = "intro";
86     static final String EXTRA_SETTINGS_TITLE = "settings_title";
87     static final String EXTRA_COMPONENT_NAME = "component_name";
88     static final String EXTRA_SETTINGS_COMPONENT_NAME = "settings_component_name";
89     static final String EXTRA_TILE_SERVICE_COMPONENT_NAME = "tile_service_component_name";
90     static final String EXTRA_VIDEO_RAW_RESOURCE_ID = "video_resource";
91     static final String EXTRA_LAUNCHED_FROM_SUW = "from_suw";
92     static final String EXTRA_ANIMATED_IMAGE_RES = "animated_image_res";
93     static final String EXTRA_HTML_DESCRIPTION = "html_description";
94     static final String EXTRA_TIME_FOR_LOGGING = "start_time_to_log_a11y_tool";
95     static final String EXTRA_METRICS_CATEGORY = "metrics_category";
96 
97     // Timeout before we update the services if packages are added/removed
98     // since the AccessibilityManagerService has to do that processing first
99     // to generate the AccessibilityServiceInfo we need for proper
100     // presentation.
101     private static final long DELAY_UPDATE_SERVICES_MILLIS = 1000;
102 
103     private final Handler mHandler = new Handler();
104 
105     private final Runnable mUpdateRunnable = new Runnable() {
106         @Override
107         public void run() {
108             if (getActivity() != null) {
109                 onContentChanged();
110             }
111         }
112     };
113 
114     private final PackageMonitor mSettingsPackageMonitor = new PackageMonitor() {
115         @Override
116         public void onPackageAdded(String packageName, int uid) {
117             sendUpdate();
118         }
119 
120         @Override
121         public void onPackageModified(@NonNull String packageName) {
122             sendUpdate();
123         }
124 
125         @Override
126         public void onPackageAppeared(String packageName, int reason) {
127             sendUpdate();
128         }
129 
130         @Override
131         public void onPackageDisappeared(String packageName, int reason) {
132             sendUpdate();
133         }
134 
135         @Override
136         public void onPackageRemoved(String packageName, int uid) {
137             sendUpdate();
138         }
139 
140         private void sendUpdate() {
141             mHandler.postDelayed(mUpdateRunnable, DELAY_UPDATE_SERVICES_MILLIS);
142         }
143     };
144 
145     @VisibleForTesting
146     final AccessibilitySettingsContentObserver mSettingsContentObserver;
147 
148     private final Map<String, PreferenceCategory> mCategoryToPrefCategoryMap =
149             new ArrayMap<>();
150     @VisibleForTesting
151     final Map<Preference, PreferenceCategory> mServicePreferenceToPreferenceCategoryMap =
152             new ArrayMap<>();
153     private final Map<ComponentName, PreferenceCategory> mPreBundledServiceComponentToCategoryMap =
154             new ArrayMap<>();
155 
156     private boolean mNeedPreferencesUpdate = false;
157     private boolean mIsForeground = true;
158 
AccessibilitySettings()159     public AccessibilitySettings() {
160         // Observe changes to anything that the shortcut can toggle, so we can reflect updates
161         final Collection<AccessibilityShortcutController.FrameworkFeatureInfo> features =
162                 AccessibilityShortcutController.getFrameworkShortcutFeaturesMap().values();
163         final List<String> shortcutFeatureKeys = new ArrayList<>(features.size());
164         for (AccessibilityShortcutController.FrameworkFeatureInfo feature : features) {
165             final String key = feature.getSettingKey();
166             if (key != null) {
167                 shortcutFeatureKeys.add(key);
168             }
169         }
170 
171         // Observe changes from accessibility selection menu
172         shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
173         shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
174         mSettingsContentObserver = new AccessibilitySettingsContentObserver(mHandler);
175         mSettingsContentObserver.registerKeysToObserverCallback(shortcutFeatureKeys,
176                 key -> onContentChanged());
177     }
178 
179     @Override
getMetricsCategory()180     public int getMetricsCategory() {
181         return SettingsEnums.ACCESSIBILITY;
182     }
183 
184     @Override
getHelpResource()185     public int getHelpResource() {
186         return R.string.help_uri_accessibility;
187     }
188 
189     @Override
onAttach(Context context)190     public void onAttach(Context context) {
191         super.onAttach(context);
192         use(AccessibilityHearingAidPreferenceController.class)
193                 .setFragmentManager(getFragmentManager());
194     }
195 
196     @Override
onCreate(Bundle icicle)197     public void onCreate(Bundle icicle) {
198         super.onCreate(icicle);
199         initializeAllPreferences();
200         updateAllPreferences();
201         registerContentMonitors();
202     }
203 
204     @Override
onResume()205     public void onResume() {
206         super.onResume();
207         updateAllPreferences();
208     }
209 
210     @Override
onStart()211     public void onStart() {
212         if (mNeedPreferencesUpdate) {
213             updateAllPreferences();
214             mNeedPreferencesUpdate = false;
215         }
216         mIsForeground = true;
217         super.onStart();
218     }
219 
220     @Override
onStop()221     public void onStop() {
222         mIsForeground = false;
223         super.onStop();
224     }
225 
226     @Override
onDestroy()227     public void onDestroy() {
228         unregisterContentMonitors();
229         super.onDestroy();
230     }
231 
232     @Override
getPreferenceScreenResId()233     protected int getPreferenceScreenResId() {
234         return R.xml.accessibility_settings;
235     }
236 
237     @Override
getLogTag()238     protected String getLogTag() {
239         return TAG;
240     }
241 
242     /**
243      * Returns the summary for the current state of this accessibilityService.
244      *
245      * @param context        A valid context
246      * @param info           The accessibilityService's info
247      * @param serviceEnabled Whether the accessibility service is enabled.
248      * @return The service summary
249      */
getServiceSummary(Context context, AccessibilityServiceInfo info, boolean serviceEnabled)250     public static CharSequence getServiceSummary(Context context, AccessibilityServiceInfo info,
251             boolean serviceEnabled) {
252         if (serviceEnabled && info.crashed) {
253             return context.getText(R.string.accessibility_summary_state_stopped);
254         }
255 
256         final CharSequence serviceState;
257         final int fragmentType = AccessibilityUtil.getAccessibilityServiceFragmentType(info);
258         if (fragmentType == AccessibilityServiceFragmentType.INVISIBLE_TOGGLE) {
259             final ComponentName componentName = new ComponentName(
260                     info.getResolveInfo().serviceInfo.packageName,
261                     info.getResolveInfo().serviceInfo.name);
262             final boolean shortcutEnabled = AccessibilityUtil.getUserShortcutTypesFromSettings(
263                     context, componentName) != AccessibilityUtil.UserShortcutType.EMPTY;
264             serviceState = shortcutEnabled
265                     ? context.getText(R.string.accessibility_summary_shortcut_enabled)
266                     : context.getText(R.string.accessibility_summary_shortcut_disabled);
267         } else {
268             serviceState = serviceEnabled
269                     ? context.getText(R.string.accessibility_summary_state_enabled)
270                     : context.getText(R.string.accessibility_summary_state_disabled);
271         }
272 
273         final CharSequence serviceSummary = info.loadSummary(context.getPackageManager());
274         final String stateSummaryCombo = context.getString(
275                 R.string.preference_summary_default_combination,
276                 serviceState, serviceSummary);
277 
278         return TextUtils.isEmpty(serviceSummary) ? serviceState : stateSummaryCombo;
279     }
280 
281     /**
282      * Returns the description for the current state of this accessibilityService.
283      *
284      * @param context        A valid context
285      * @param info           The accessibilityService's info
286      * @param serviceEnabled Whether the accessibility service is enabled.
287      * @return The service description
288      */
getServiceDescription(Context context, AccessibilityServiceInfo info, boolean serviceEnabled)289     public static CharSequence getServiceDescription(Context context, AccessibilityServiceInfo info,
290             boolean serviceEnabled) {
291         if (serviceEnabled && info.crashed) {
292             return context.getText(R.string.accessibility_description_state_stopped);
293         }
294 
295         return info.loadDescription(context.getPackageManager());
296     }
297 
298     @VisibleForTesting
onContentChanged()299     void onContentChanged() {
300         // If the fragment is visible then update preferences immediately, else set the flag then
301         // wait for the fragment to show up to update preferences.
302         if (mIsForeground) {
303             updateAllPreferences();
304         } else {
305             mNeedPreferencesUpdate = true;
306         }
307     }
308 
initializeAllPreferences()309     private void initializeAllPreferences() {
310         for (int i = 0; i < CATEGORIES.length; i++) {
311             PreferenceCategory prefCategory = findPreference(CATEGORIES[i]);
312             mCategoryToPrefCategoryMap.put(CATEGORIES[i], prefCategory);
313         }
314     }
315 
316     @VisibleForTesting
updateAllPreferences()317     void updateAllPreferences() {
318         updateSystemPreferences();
319         updateServicePreferences();
320         updatePreferencesState();
321     }
322 
registerContentMonitors()323     private void registerContentMonitors() {
324         final Context context = getActivity();
325 
326         mSettingsPackageMonitor.register(context, context.getMainLooper(), /* externalStorage= */
327                 false);
328         mSettingsContentObserver.register(getContentResolver());
329     }
330 
unregisterContentMonitors()331     private void unregisterContentMonitors() {
332         mSettingsPackageMonitor.unregister();
333         mSettingsContentObserver.unregister(getContentResolver());
334     }
335 
updateServicePreferences()336     protected void updateServicePreferences() {
337         // Since services category is auto generated we have to do a pass
338         // to generate it since services can come and go and then based on
339         // the global accessibility state to decided whether it is enabled.
340         final ArrayList<Preference> servicePreferences =
341                 new ArrayList<>(mServicePreferenceToPreferenceCategoryMap.keySet());
342         for (int i = 0; i < servicePreferences.size(); i++) {
343             Preference service = servicePreferences.get(i);
344             PreferenceCategory category = mServicePreferenceToPreferenceCategoryMap.get(service);
345             category.removePreference(service);
346         }
347 
348         initializePreBundledServicesMapFromArray(CATEGORY_SCREEN_READER,
349                 R.array.config_preinstalled_screen_reader_services);
350         initializePreBundledServicesMapFromArray(CATEGORY_CAPTIONS,
351                 R.array.config_preinstalled_captions_services);
352         initializePreBundledServicesMapFromArray(CATEGORY_AUDIO,
353                 R.array.config_preinstalled_audio_services);
354         initializePreBundledServicesMapFromArray(CATEGORY_DISPLAY,
355                 R.array.config_preinstalled_display_services);
356         initializePreBundledServicesMapFromArray(CATEGORY_SPEECH,
357                 R.array.config_preinstalled_speech_services);
358         initializePreBundledServicesMapFromArray(CATEGORY_INTERACTION_CONTROL,
359                 R.array.config_preinstalled_interaction_control_services);
360 
361         // ACCESSIBILITY_MENU_IN_SYSTEM is a default pre-bundled interaction control service.
362         // If the device opts out of including this service then this is a no-op.
363         mPreBundledServiceComponentToCategoryMap.put(
364                 AccessibilityUtils.ACCESSIBILITY_MENU_IN_SYSTEM,
365                 mCategoryToPrefCategoryMap.get(CATEGORY_INTERACTION_CONTROL));
366 
367         final List<RestrictedPreference> preferenceList = getInstalledAccessibilityList(
368                 getPrefContext());
369 
370         final PreferenceCategory downloadedServicesCategory =
371                 mCategoryToPrefCategoryMap.get(CATEGORY_DOWNLOADED_SERVICES);
372 
373         for (int i = 0, count = preferenceList.size(); i < count; ++i) {
374             final RestrictedPreference preference = preferenceList.get(i);
375             final ComponentName componentName = preference.getExtras().getParcelable(
376                     EXTRA_COMPONENT_NAME);
377             PreferenceCategory prefCategory = downloadedServicesCategory;
378             // Set the appropriate category if the service comes pre-installed.
379             if (mPreBundledServiceComponentToCategoryMap.containsKey(componentName)) {
380                 prefCategory = mPreBundledServiceComponentToCategoryMap.get(componentName);
381             }
382             prefCategory.addPreference(preference);
383             mServicePreferenceToPreferenceCategoryMap.put(preference, prefCategory);
384         }
385 
386         // Update the order of all the category according to the order defined in xml file.
387         updateCategoryOrderFromArray(CATEGORY_SCREEN_READER,
388                 R.array.config_order_screen_reader_services);
389         updateCategoryOrderFromArray(CATEGORY_CAPTIONS,
390                 R.array.config_order_captions_services);
391         updateCategoryOrderFromArray(CATEGORY_AUDIO,
392                 R.array.config_order_audio_services);
393         updateCategoryOrderFromArray(CATEGORY_INTERACTION_CONTROL,
394                 R.array.config_order_interaction_control_services);
395         updateCategoryOrderFromArray(CATEGORY_DISPLAY,
396                 R.array.config_order_display_services);
397         updateCategoryOrderFromArray(CATEGORY_SPEECH,
398                 R.array.config_order_speech_services);
399 
400         // Need to check each time when updateServicePreferences() called.
401         if (downloadedServicesCategory.getPreferenceCount() == 0) {
402             getPreferenceScreen().removePreference(downloadedServicesCategory);
403         } else {
404             getPreferenceScreen().addPreference(downloadedServicesCategory);
405         }
406 
407         // Hide category if it is empty.
408         updatePreferenceCategoryVisibility(CATEGORY_SCREEN_READER);
409         updatePreferenceCategoryVisibility(CATEGORY_SPEECH);
410     }
411 
getInstalledAccessibilityList(Context context)412     private List<RestrictedPreference> getInstalledAccessibilityList(Context context) {
413         final AccessibilityManager a11yManager = AccessibilityManager.getInstance(context);
414         final RestrictedPreferenceHelper preferenceHelper = new RestrictedPreferenceHelper(context);
415 
416         final List<AccessibilityShortcutInfo> installedShortcutList =
417                 a11yManager.getInstalledAccessibilityShortcutListAsUser(context,
418                         UserHandle.myUserId());
419 
420         // Remove duplicate item here, new a ArrayList to copy unmodifiable list result
421         // (getInstalledAccessibilityServiceList).
422         final List<AccessibilityServiceInfo> installedServiceList = new ArrayList<>(
423                 a11yManager.getInstalledAccessibilityServiceList());
424         installedServiceList.removeIf(
425                 target -> containsTargetNameInList(installedShortcutList, target));
426 
427         final List<RestrictedPreference> activityList =
428                 preferenceHelper.createAccessibilityActivityPreferenceList(installedShortcutList);
429 
430         final List<RestrictedPreference> serviceList =
431                 preferenceHelper.createAccessibilityServicePreferenceList(installedServiceList);
432 
433         final List<RestrictedPreference> preferenceList = new ArrayList<>();
434         preferenceList.addAll(activityList);
435         preferenceList.addAll(serviceList);
436 
437         return preferenceList;
438     }
439 
containsTargetNameInList(List<AccessibilityShortcutInfo> shortcutInfos, AccessibilityServiceInfo targetServiceInfo)440     private boolean containsTargetNameInList(List<AccessibilityShortcutInfo> shortcutInfos,
441             AccessibilityServiceInfo targetServiceInfo) {
442         final ServiceInfo serviceInfo = targetServiceInfo.getResolveInfo().serviceInfo;
443         final String servicePackageName = serviceInfo.packageName;
444         final CharSequence serviceLabel = serviceInfo.loadLabel(getPackageManager());
445 
446         for (int i = 0, count = shortcutInfos.size(); i < count; ++i) {
447             final ActivityInfo activityInfo = shortcutInfos.get(i).getActivityInfo();
448             final String activityPackageName = activityInfo.packageName;
449             final CharSequence activityLabel = activityInfo.loadLabel(getPackageManager());
450             if (servicePackageName.equals(activityPackageName)
451                     && serviceLabel.equals(activityLabel)) {
452                 return true;
453             }
454         }
455         return false;
456     }
457 
initializePreBundledServicesMapFromArray(String categoryKey, int key)458     private void initializePreBundledServicesMapFromArray(String categoryKey, int key) {
459         String[] services = getResources().getStringArray(key);
460         PreferenceCategory category = mCategoryToPrefCategoryMap.get(categoryKey);
461         for (int i = 0; i < services.length; i++) {
462             ComponentName component = ComponentName.unflattenFromString(services[i]);
463             mPreBundledServiceComponentToCategoryMap.put(component, category);
464         }
465     }
466 
467     /**
468      * Update the order of preferences in the category by matching their preference
469      * key with the string array of preference order which is defined in the xml.
470      *
471      * @param categoryKey The key of the category need to update the order
472      * @param key         The key of the string array which defines the order of category
473      */
updateCategoryOrderFromArray(String categoryKey, int key)474     private void updateCategoryOrderFromArray(String categoryKey, int key) {
475         String[] services = getResources().getStringArray(key);
476         PreferenceCategory category = mCategoryToPrefCategoryMap.get(categoryKey);
477         int preferenceCount = category.getPreferenceCount();
478         int serviceLength = services.length;
479         for (int preferenceIndex = 0; preferenceIndex < preferenceCount; preferenceIndex++) {
480             for (int serviceIndex = 0; serviceIndex < serviceLength; serviceIndex++) {
481                 if (category.getPreference(preferenceIndex).getKey()
482                         .equals(services[serviceIndex])) {
483                     category.getPreference(preferenceIndex).setOrder(serviceIndex);
484                     break;
485                 }
486             }
487         }
488     }
489 
490     /**
491      * Updates the visibility of a category according to its child preference count.
492      *
493      * @param categoryKey The key of the category which needs to check
494      */
updatePreferenceCategoryVisibility(String categoryKey)495     private void updatePreferenceCategoryVisibility(String categoryKey) {
496         final PreferenceCategory category = mCategoryToPrefCategoryMap.get(categoryKey);
497         category.setVisible(category.getPreferenceCount() != 0);
498     }
499 
500     /**
501      * Updates preferences related to system configurations.
502      */
updateSystemPreferences()503     protected void updateSystemPreferences() {
504         // Do nothing.
505     }
506 
updatePreferencesState()507     private void updatePreferencesState() {
508         final List<AbstractPreferenceController> controllers = new ArrayList<>();
509         getPreferenceControllers().forEach(controllers::addAll);
510         controllers.forEach(controller -> controller.updateState(
511                 findPreference(controller.getPreferenceKey())));
512     }
513 
514     public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
515             new BaseSearchIndexProvider(R.xml.accessibility_settings) {
516                 @Override
517                 public List<SearchIndexableRaw> getRawDataToIndex(Context context,
518                         boolean enabled) {
519                     return FeatureFactory.getFactory(context)
520                             .getAccessibilitySearchFeatureProvider().getSearchIndexableRawData(
521                                     context);
522                 }
523             };
524 }
525