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