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