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.tv.settings.accessibility;
18 
19 import static android.content.Context.ACCESSIBILITY_SERVICE;
20 
21 import static com.android.tv.settings.util.InstrumentationUtils.logToggleInteracted;
22 
23 import android.accessibilityservice.AccessibilityServiceInfo;
24 import android.app.admin.DevicePolicyManager;
25 import android.app.tvsettings.TvSettingsEnums;
26 import android.content.ComponentName;
27 import android.content.pm.ServiceInfo;
28 import android.os.Bundle;
29 import android.os.UserHandle;
30 import android.provider.Settings;
31 import android.text.TextUtils;
32 import android.view.accessibility.AccessibilityManager;
33 import android.util.ArrayMap;
34 
35 import androidx.annotation.Keep;
36 import androidx.preference.Preference;
37 import androidx.preference.PreferenceGroup;
38 import androidx.preference.PreferenceCategory;
39 import androidx.preference.SwitchPreference;
40 import androidx.preference.TwoStatePreference;
41 
42 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
43 import com.android.settingslib.RestrictedLockUtilsInternal;
44 import com.android.settingslib.RestrictedPreference;
45 import com.android.settingslib.accessibility.AccessibilityUtils;
46 import com.android.tv.settings.R;
47 import com.android.tv.settings.SettingsPreferenceFragment;
48 import com.android.tv.settings.overlay.FlavorUtils;
49 
50 import java.util.List;
51 import java.util.Set;
52 import java.util.Map;
53 
54 /**
55  * Fragment for Accessibility settings
56  */
57 @Keep
58 public class AccessibilityFragment extends SettingsPreferenceFragment {
59     private static final String TOGGLE_HIGH_TEXT_CONTRAST_KEY = "toggle_high_text_contrast";
60     private static final String TOGGLE_AUDIO_DESCRIPTION_KEY = "toggle_audio_description";
61     private static final String TOGGLE_BOLD_TEXT_KEY = "toggle_bold_text";
62     private static final String COLOR_CORRECTION_TWOPANEL_KEY = "color_correction_only_twopanel";
63     private static final String COLOR_CORRECTION_CLASSIC_KEY = "color_correction_only_classic";
64     private static final String ACCESSIBILITY_SHORTCUT_KEY = "accessibility_shortcut";
65     private static final int BOLD_TEXT_ADJUSTMENT = 500;
66     private static final int FIRST_PREFERENCE_IN_CATEGORY_INDEX = -1;
67 
68     PreferenceCategory mServicesPrefCategory;
69     PreferenceCategory mControlsPrefCategory;
70 
71     private final Map<ComponentName, PreferenceCategory>
72             mServiceComponentNameToPreferenceCategoryMap = new ArrayMap<>();
73 
74     private enum AccessibilityCategory {
75         SCREEN_READERS("accessibility_screen_readers_category",
76                 R.array.config_preinstalled_screen_reader_services),
77         DISPLAY("accessibility_display_category",
78                 R.array.config_preinstalled_display_services),
79         INTERACTION_CONTROLS("accessibility_interaction_controls_category",
80                 R.array.config_preinstalled_interaction_control_services),
81         AUDIO_AND_ONSCREEN_TEXT("accessibility_audio_and_onscreen_text_category",
82                 R.array.config_preinstalled_audio_and_onscreen_text_services),
83         EXPERIMENTAL("accessibility_experimental_category",
84                 R.array.config_preinstalled_experimental_services),
85         SERVICES("accessibility_services_category",
86                 R.array.config_preinstalled_additional_services);
87 
88         final String key;
89         final int servicesArrayId;
90 
AccessibilityCategory(String key, int servicesArrayId)91         AccessibilityCategory(String key, int servicesArrayId) {
92             this.key = key;
93             this.servicesArrayId = servicesArrayId;
94         }
95 
getKey()96         String getKey() {
97             return this.key;
98         }
99 
getServicesArrayId()100         int getServicesArrayId() {
101             return this.servicesArrayId;
102         }
103     }
104 
105     private AccessibilityManager.AccessibilityStateChangeListener
106             mAccessibilityStateChangeListener = enabled -> refreshServices();
107 
108     /**
109      * Create a new instance of the fragment
110      * @return New fragment instance
111      */
newInstance()112     public static AccessibilityFragment newInstance() {
113         return new AccessibilityFragment();
114     }
115 
116     @Override
onResume()117     public void onResume() {
118         super.onResume();
119         refreshServices();
120     }
121 
122     @Override
onStop()123     public void onStop() {
124         super.onStop();
125         AccessibilityManager am = (AccessibilityManager)
126                 getContext().getSystemService(ACCESSIBILITY_SERVICE);
127         if (am != null) {
128             am.removeAccessibilityStateChangeListener(mAccessibilityStateChangeListener);
129         }
130     }
131 
132     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)133     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
134         setPreferencesFromResource(R.xml.accessibility, null);
135 
136         final TwoStatePreference highContrastPreference =
137                 (TwoStatePreference) findPreference(TOGGLE_HIGH_TEXT_CONTRAST_KEY);
138         highContrastPreference.setChecked(Settings.Secure.getInt(getContext().getContentResolver(),
139                 Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, 0) == 1);
140 
141         final TwoStatePreference audioDescriptionPreference =
142                 (TwoStatePreference) findPreference(TOGGLE_AUDIO_DESCRIPTION_KEY);
143         audioDescriptionPreference.setChecked(Settings.Secure.getInt(
144                 getContext().getContentResolver(),
145                 Settings.Secure.ENABLED_ACCESSIBILITY_AUDIO_DESCRIPTION_BY_DEFAULT, 0) == 1);
146 
147         final TwoStatePreference boldTextPreference =
148                 (TwoStatePreference) findPreference(TOGGLE_BOLD_TEXT_KEY);
149         boldTextPreference.setChecked(Settings.Secure.getInt(
150                 getContext().getContentResolver(),
151                 Settings.Secure.FONT_WEIGHT_ADJUSTMENT, 0) == BOLD_TEXT_ADJUSTMENT);
152 
153         if (getContext()
154                 .getResources()
155                 .getBoolean(R.bool.config_showAccessibilityColorCorrection)) {
156             Preference colorCorrectionPreferenceToSetVisible =
157                     FlavorUtils.isTwoPanel(getContext())
158                             ? (Preference) findPreference(COLOR_CORRECTION_TWOPANEL_KEY)
159                             : (Preference) findPreference(COLOR_CORRECTION_CLASSIC_KEY);
160             colorCorrectionPreferenceToSetVisible.setVisible(true);
161         }
162 
163         mServicesPrefCategory = findPreference(AccessibilityCategory.SERVICES.getKey());
164         mControlsPrefCategory = findPreference(AccessibilityCategory.INTERACTION_CONTROLS.getKey());
165         populateServiceToPreferenceCategoryMaps();
166         refreshServices();
167         AccessibilityManager am = (AccessibilityManager)
168                 getContext().getSystemService(ACCESSIBILITY_SERVICE);
169         if (am != null) {
170             am.addAccessibilityStateChangeListener(mAccessibilityStateChangeListener);
171         }
172     }
173 
174     @Override
onPreferenceTreeClick(Preference preference)175     public boolean onPreferenceTreeClick(Preference preference) {
176         if (TextUtils.equals(preference.getKey(), TOGGLE_HIGH_TEXT_CONTRAST_KEY)) {
177             logToggleInteracted(
178                     TvSettingsEnums.SYSTEM_A11Y_HIGH_CONTRAST_TEXT,
179                     ((SwitchPreference) preference).isChecked());
180             Settings.Secure.putInt(getActivity().getContentResolver(),
181                     Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED,
182                     (((SwitchPreference) preference).isChecked() ? 1 : 0));
183             return true;
184         } else if (TextUtils.equals(preference.getKey(), TOGGLE_AUDIO_DESCRIPTION_KEY)) {
185             logToggleInteracted(
186                     TvSettingsEnums.SYSTEM_A11Y_AUDIO_DESCRIPTION,
187                     ((SwitchPreference) preference).isChecked());
188             Settings.Secure.putInt(getActivity().getContentResolver(),
189                     Settings.Secure.ENABLED_ACCESSIBILITY_AUDIO_DESCRIPTION_BY_DEFAULT,
190                     (((SwitchPreference) preference).isChecked() ? 1 : 0));
191             return true;
192         } else if (TextUtils.equals(preference.getKey(), TOGGLE_BOLD_TEXT_KEY)) {
193             logToggleInteracted(
194                     TvSettingsEnums.SYSTEM_A11Y_BOLD_TEXT,
195                     ((SwitchPreference) preference).isChecked());
196             Settings.Secure.putInt(getActivity().getContentResolver(),
197                     Settings.Secure.FONT_WEIGHT_ADJUSTMENT,
198                     (((SwitchPreference) preference).isChecked() ? BOLD_TEXT_ADJUSTMENT : 0));
199             return true;
200         } else {
201             return super.onPreferenceTreeClick(preference);
202         }
203     }
204 
populateServiceToPreferenceCategoryMaps()205     private void populateServiceToPreferenceCategoryMaps() {
206         for (AccessibilityCategory accessibilityCategory : AccessibilityCategory.values()) {
207             String[] services = getResources().getStringArray(
208                     accessibilityCategory.getServicesArrayId());
209             PreferenceCategory prefCategory = findPreference(accessibilityCategory.getKey());
210             for (int i = 0; i < services.length; i++) {
211                 ComponentName component = ComponentName.unflattenFromString(services[i]);
212                 mServiceComponentNameToPreferenceCategoryMap.put(component, prefCategory);
213             }
214         }
215     }
216 
refreshServices()217     private void refreshServices() {
218         DevicePolicyManager dpm = getContext().getSystemService(DevicePolicyManager.class);
219         final List<AccessibilityServiceInfo> installedServiceInfos =
220                 getActivity().getSystemService(AccessibilityManager.class)
221                         .getInstalledAccessibilityServiceList();
222         final Set<ComponentName> enabledServices =
223                 AccessibilityUtils.getEnabledServicesFromSettings(getActivity());
224         final List<String> permittedServices = dpm.getPermittedAccessibilityServices(
225                 UserHandle.myUserId());
226 
227         if (installedServiceInfos.size() == 0) {
228             Preference pref = mControlsPrefCategory.findPreference(ACCESSIBILITY_SHORTCUT_KEY);
229             if (pref != null) {
230                 mControlsPrefCategory.removePreference(pref);
231             }
232         }
233 
234         final boolean accessibilityEnabled = Settings.Secure.getInt(
235                 getActivity().getContentResolver(),
236                 Settings.Secure.ACCESSIBILITY_ENABLED, 0) == 1;
237 
238         for (final AccessibilityServiceInfo accInfo : installedServiceInfos) {
239             final ServiceInfo serviceInfo = accInfo.getResolveInfo().serviceInfo;
240             final ComponentName componentName = new ComponentName(serviceInfo.packageName,
241                     serviceInfo.name);
242             final boolean serviceEnabled = accessibilityEnabled
243                     && enabledServices.contains(componentName);
244             // permittedServices null means all accessibility services are allowed.
245             final boolean serviceAllowed = permittedServices == null
246                     || permittedServices.contains(serviceInfo.packageName);
247 
248             final String title = accInfo.getResolveInfo()
249                     .loadLabel(getActivity().getPackageManager()).toString();
250 
251             final String key = "ServicePref:" + componentName.flattenToString();
252             RestrictedPreference servicePref = findPreference(key);
253             if (servicePref == null) {
254                 servicePref = new RestrictedPreference(getContext());
255                 servicePref.setKey(key);
256             }
257             servicePref.setTitle(title);
258             servicePref.setSummary(serviceEnabled ? R.string.settings_on : R.string.settings_off);
259             AccessibilityServiceFragment.prepareArgs(servicePref.getExtras(),
260                     serviceInfo.packageName,
261                     serviceInfo.name,
262                     accInfo.getSettingsActivityName(),
263                     title);
264 
265             if (serviceAllowed || serviceEnabled) {
266                 servicePref.setEnabled(true);
267                 servicePref.setFragment(AccessibilityServiceFragment.class.getName());
268             } else {
269                 // Disable accessibility service that are not permitted.
270                 final EnforcedAdmin admin =
271                         RestrictedLockUtilsInternal.checkIfAccessibilityServiceDisallowed(
272                                 getContext(), serviceInfo.packageName, UserHandle.myUserId());
273                 if (admin != null) {
274                     servicePref.setDisabledByAdmin(admin);
275                 } else {
276                     servicePref.setEnabled(false);
277                 }
278                 servicePref.setFragment(null);
279             }
280 
281             // Make the screen reader component be the first preference in its preference category.
282             final String screenReaderFlattenedComponentName = getResources().getString(
283                     R.string.accessibility_screen_reader_flattened_component_name);
284             if (componentName.flattenToString().equals(screenReaderFlattenedComponentName)) {
285                 servicePref.setOrder(FIRST_PREFERENCE_IN_CATEGORY_INDEX);
286             }
287 
288             PreferenceCategory prefCategory = mServicesPrefCategory;
289             if (mServiceComponentNameToPreferenceCategoryMap.containsKey(componentName)) {
290                 prefCategory = mServiceComponentNameToPreferenceCategoryMap.get(componentName);
291             }
292             // The method "addPreference" only adds the preference if it is not there already.
293             prefCategory.addPreference(servicePref);
294         }
295         mServicesPrefCategory.setVisible(mServicesPrefCategory.getPreferenceCount() != 0);
296         mControlsPrefCategory.setVisible(mControlsPrefCategory.getPreferenceCount() != 0);
297     }
298 
299     @Override
getPageId()300     protected int getPageId() {
301         return TvSettingsEnums.SYSTEM_A11Y;
302     }
303 }
304