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