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