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