From b0e120166d43603a3a8316d67f4fbab19da273d5 Mon Sep 17 00:00:00 2001 From: ywy Date: Thu, 16 Oct 2025 11:34:52 +0800 Subject: [PATCH] fix settings crash because of bluetooth modified --- .../Settings/res/xml/top_level_settings.xml | 4 +- .../accessibility/AccessibilitySettings.java | 610 ++++++++++++++++++ .../settings/network/TetheredRepository.kt | 102 +++ 3 files changed, 714 insertions(+), 2 deletions(-) create mode 100644 aosp/packages/apps/Settings/src/com/android/settings/accessibility/AccessibilitySettings.java create mode 100644 aosp/packages/apps/Settings/src/com/android/settings/network/TetheredRepository.kt diff --git a/aosp/packages/apps/Settings/res/xml/top_level_settings.xml b/aosp/packages/apps/Settings/res/xml/top_level_settings.xml index 7815b5f73..8c4e5fedc 100644 --- a/aosp/packages/apps/Settings/res/xml/top_level_settings.xml +++ b/aosp/packages/apps/Settings/res/xml/top_level_settings.xml @@ -40,7 +40,7 @@ settings:highlightableMenuKey="@string/menu_key_communal" settings:controller="com.android.settings.communal.CommunalPreferenceController"/> - + settings:controller="com.android.settings.connecteddevice.TopLevelConnectedDevicesPreferenceController"/>--> mCategoryToPrefCategoryMap = + new ArrayMap<>(); + @VisibleForTesting + final Map mServicePreferenceToPreferenceCategoryMap = + new ArrayMap<>(); + private final Map mPreBundledServiceComponentToCategoryMap = + new ArrayMap<>(); + + private boolean mNeedPreferencesUpdate = false; + private boolean mIsForeground = true; + + public AccessibilitySettings() { + // Observe changes to anything that the shortcut can toggle, so we can reflect updates + final Collection features = + AccessibilityShortcutController.getFrameworkShortcutFeaturesMap().values(); + final List shortcutFeatureKeys = new ArrayList<>(features.size()); + for (AccessibilityShortcutController.FrameworkFeatureInfo feature : features) { + final String key = feature.getSettingKey(); + if (key != null) { + shortcutFeatureKeys.add(key); + } + } + + // Observe changes from accessibility selection menu + shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS); + shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE); + shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_STICKY_KEYS); + shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_SLOW_KEYS); + shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_BOUNCE_KEYS); + mSettingsContentObserver = new AccessibilitySettingsContentObserver(mHandler); + mSettingsContentObserver.registerKeysToObserverCallback(shortcutFeatureKeys, + key -> onContentChanged()); + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.ACCESSIBILITY; + } + + @Override + public int getHelpResource() { + return R.string.help_uri_accessibility; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + /* TODO蓝牙服务未开发,当前关闭该服务,注释蓝牙相关流程(支持助听器)防止无障碍服务崩溃 + use(AccessibilityHearingAidPreferenceController.class) + .setFragmentManager(getFragmentManager()); + */ + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + initializeAllPreferences(); + updateAllPreferences(); + registerContentMonitors(); + registerInputDeviceListener(); + } + + @Override + public void onResume() { + super.onResume(); + updateAllPreferences(); + } + + @Override + public void onStart() { + if (mNeedPreferencesUpdate) { + updateAllPreferences(); + mNeedPreferencesUpdate = false; + } + mIsForeground = true; + super.onStart(); + } + + @Override + public void onStop() { + mIsForeground = false; + super.onStop(); + } + + @Override + public void onDestroy() { + unregisterContentMonitors(); + unRegisterInputDeviceListener(); + super.onDestroy(); + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.accessibility_settings; + } + + @Override + protected String getLogTag() { + return TAG; + } + + /** + * Returns the summary for the current state of this accessibilityService. + * + * @param context A valid context + * @param info The accessibilityService's info + * @param serviceEnabled Whether the accessibility service is enabled. + * @return The service summary + */ + public static CharSequence getServiceSummary(Context context, AccessibilityServiceInfo info, + boolean serviceEnabled) { + if (serviceEnabled && info.crashed) { + return context.getText(R.string.accessibility_summary_state_stopped); + } + + final CharSequence serviceState; + final int fragmentType = AccessibilityUtil.getAccessibilityServiceFragmentType(info); + if (fragmentType == AccessibilityServiceFragmentType.INVISIBLE_TOGGLE) { + final ComponentName componentName = new ComponentName( + info.getResolveInfo().serviceInfo.packageName, + info.getResolveInfo().serviceInfo.name); + final boolean shortcutEnabled = AccessibilityUtil.getUserShortcutTypesFromSettings( + context, componentName) != AccessibilityUtil.UserShortcutType.EMPTY; + serviceState = shortcutEnabled + ? context.getText(R.string.accessibility_summary_shortcut_enabled) + : context.getText(R.string.generic_accessibility_feature_shortcut_off); + } else { + serviceState = serviceEnabled + ? context.getText(R.string.generic_accessibility_service_on) + : context.getText(R.string.generic_accessibility_service_off); + } + + final CharSequence serviceSummary = info.loadSummary(context.getPackageManager()); + final String stateSummaryCombo = context.getString( + R.string.preference_summary_default_combination, + serviceState, serviceSummary); + + return TextUtils.isEmpty(serviceSummary) ? serviceState : stateSummaryCombo; + } + + /** + * Returns the description for the current state of this accessibilityService. + * + * @param context A valid context + * @param info The accessibilityService's info + * @param serviceEnabled Whether the accessibility service is enabled. + * @return The service description + */ + public static CharSequence getServiceDescription(Context context, AccessibilityServiceInfo info, + boolean serviceEnabled) { + if (serviceEnabled && info.crashed) { + return context.getText(R.string.accessibility_description_state_stopped); + } + + return info.loadDescription(context.getPackageManager()); + } + + @VisibleForTesting + void onContentChanged() { + // If the fragment is visible then update preferences immediately, else set the flag then + // wait for the fragment to show up to update preferences. + if (mIsForeground) { + updateAllPreferences(); + } else { + mNeedPreferencesUpdate = true; + } + } + + private void initializeAllPreferences() { + for (int i = 0; i < CATEGORIES.length; i++) { + PreferenceCategory prefCategory = findPreference(CATEGORIES[i]); + mCategoryToPrefCategoryMap.put(CATEGORIES[i], prefCategory); + } + } + + @VisibleForTesting + void updateAllPreferences() { + updateServicePreferences(); + updatePreferencesState(); + updateSystemPreferences(); + } + + private void registerContentMonitors() { + final Context context = getActivity(); + + mSettingsPackageMonitor.register(context, context.getMainLooper(), /* externalStorage= */ + false); + mSettingsContentObserver.register(getContentResolver()); + } + + private void registerInputDeviceListener() { + InputManager mIm = getSystemService(InputManager.class); + if (mIm == null) { + return; + } + mIm.registerInputDeviceListener(this, null); + } + + private void unRegisterInputDeviceListener() { + InputManager mIm = getSystemService(InputManager.class); + if (mIm == null) { + return; + } + mIm.unregisterInputDeviceListener(this); + } + + private void unregisterContentMonitors() { + mSettingsPackageMonitor.unregister(); + mSettingsContentObserver.unregister(getContentResolver()); + } + + protected void updateServicePreferences() { + // Since services category is auto generated we have to do a pass + // to generate it since services can come and go and then based on + // the global accessibility state to decided whether it is enabled. + final ArrayList servicePreferences = + new ArrayList<>(mServicePreferenceToPreferenceCategoryMap.keySet()); + for (int i = 0; i < servicePreferences.size(); i++) { + Preference service = servicePreferences.get(i); + PreferenceCategory category = mServicePreferenceToPreferenceCategoryMap.get(service); + category.removePreference(service); + } + + initializePreBundledServicesMapFromArray(CATEGORY_SCREEN_READER, + R.array.config_preinstalled_screen_reader_services); + initializePreBundledServicesMapFromArray(CATEGORY_CAPTIONS, + R.array.config_preinstalled_captions_services); + initializePreBundledServicesMapFromArray(CATEGORY_AUDIO, + R.array.config_preinstalled_audio_services); + initializePreBundledServicesMapFromArray(CATEGORY_DISPLAY, + R.array.config_preinstalled_display_services); + initializePreBundledServicesMapFromArray(CATEGORY_SPEECH, + R.array.config_preinstalled_speech_services); + initializePreBundledServicesMapFromArray(CATEGORY_INTERACTION_CONTROL, + R.array.config_preinstalled_interaction_control_services); + + // ACCESSIBILITY_MENU_IN_SYSTEM is a default pre-bundled interaction control service. + // If the device opts out of including this service then this is a no-op. + mPreBundledServiceComponentToCategoryMap.put( + AccessibilityUtils.ACCESSIBILITY_MENU_IN_SYSTEM, + mCategoryToPrefCategoryMap.get(CATEGORY_INTERACTION_CONTROL)); + + final List preferenceList = getInstalledAccessibilityList( + getPrefContext()); + + final PreferenceCategory downloadedServicesCategory = + mCategoryToPrefCategoryMap.get(CATEGORY_DOWNLOADED_SERVICES); + + for (int i = 0, count = preferenceList.size(); i < count; ++i) { + final RestrictedPreference preference = preferenceList.get(i); + final ComponentName componentName = preference.getExtras().getParcelable( + EXTRA_COMPONENT_NAME); + PreferenceCategory prefCategory = downloadedServicesCategory; + // Set the appropriate category if the service comes pre-installed. + if (mPreBundledServiceComponentToCategoryMap.containsKey(componentName)) { + prefCategory = mPreBundledServiceComponentToCategoryMap.get(componentName); + } + prefCategory.addPreference(preference); + mServicePreferenceToPreferenceCategoryMap.put(preference, prefCategory); + } + + // Update the order of all the category according to the order defined in xml file. + updateCategoryOrderFromArray(CATEGORY_SCREEN_READER, + R.array.config_order_screen_reader_services); + updateCategoryOrderFromArray(CATEGORY_CAPTIONS, + R.array.config_order_captions_services); + updateCategoryOrderFromArray(CATEGORY_AUDIO, + R.array.config_order_audio_services); + updateCategoryOrderFromArray(CATEGORY_INTERACTION_CONTROL, + R.array.config_order_interaction_control_services); + updateCategoryOrderFromArray(CATEGORY_DISPLAY, + R.array.config_order_display_services); + updateCategoryOrderFromArray(CATEGORY_SPEECH, + R.array.config_order_speech_services); + + // Need to check each time when updateServicePreferences() called. + if (downloadedServicesCategory.getPreferenceCount() == 0) { + getPreferenceScreen().removePreference(downloadedServicesCategory); + } else { + getPreferenceScreen().addPreference(downloadedServicesCategory); + } + + // Hide category if it is empty. + updatePreferenceCategoryVisibility(CATEGORY_SCREEN_READER); + updatePreferenceCategoryVisibility(CATEGORY_SPEECH); + updatePreferenceCategoryVisibility(CATEGORY_KEYBOARD_OPTIONS); + } + + private List getInstalledAccessibilityList(Context context) { + final AccessibilityManager a11yManager = AccessibilityManager.getInstance(context); + final RestrictedPreferenceHelper preferenceHelper = new RestrictedPreferenceHelper(context); + + final List installedShortcutList = + a11yManager.getInstalledAccessibilityShortcutListAsUser(context, + UserHandle.myUserId()); + + // Remove duplicate item here, new a ArrayList to copy unmodifiable list result + // (getInstalledAccessibilityServiceList). + final List installedServiceList = new ArrayList<>( + a11yManager.getInstalledAccessibilityServiceList()); + installedServiceList.removeIf( + target -> containsTargetNameInList(installedShortcutList, target)); + + final List activityList = + preferenceHelper.createAccessibilityActivityPreferenceList(installedShortcutList); + + final List serviceList = + preferenceHelper.createAccessibilityServicePreferenceList(installedServiceList); + + final List preferenceList = new ArrayList<>(); + preferenceList.addAll(activityList); + preferenceList.addAll(serviceList); + + return preferenceList; + } + + private boolean containsTargetNameInList(List shortcutInfos, + AccessibilityServiceInfo targetServiceInfo) { + final ServiceInfo serviceInfo = targetServiceInfo.getResolveInfo().serviceInfo; + final String servicePackageName = serviceInfo.packageName; + final CharSequence serviceLabel = serviceInfo.loadLabel(getPackageManager()); + + for (int i = 0, count = shortcutInfos.size(); i < count; ++i) { + final ActivityInfo activityInfo = shortcutInfos.get(i).getActivityInfo(); + final String activityPackageName = activityInfo.packageName; + final CharSequence activityLabel = activityInfo.loadLabel(getPackageManager()); + if (servicePackageName.equals(activityPackageName) + && serviceLabel.equals(activityLabel)) { + return true; + } + } + return false; + } + + private void initializePreBundledServicesMapFromArray(String categoryKey, int key) { + String[] services = getResources().getStringArray(key); + PreferenceCategory category = mCategoryToPrefCategoryMap.get(categoryKey); + for (int i = 0; i < services.length; i++) { + ComponentName component = ComponentName.unflattenFromString(services[i]); + mPreBundledServiceComponentToCategoryMap.put(component, category); + } + } + + /** + * Update the order of preferences in the category by matching their preference + * key with the string array of preference order which is defined in the xml. + * + * @param categoryKey The key of the category need to update the order + * @param key The key of the string array which defines the order of category + */ + private void updateCategoryOrderFromArray(String categoryKey, int key) { + String[] services = getResources().getStringArray(key); + PreferenceCategory category = mCategoryToPrefCategoryMap.get(categoryKey); + int preferenceCount = category.getPreferenceCount(); + int serviceLength = services.length; + for (int preferenceIndex = 0; preferenceIndex < preferenceCount; preferenceIndex++) { + for (int serviceIndex = 0; serviceIndex < serviceLength; serviceIndex++) { + if (category.getPreference(preferenceIndex).getKey() + .equals(services[serviceIndex])) { + category.getPreference(preferenceIndex).setOrder(serviceIndex); + break; + } + } + } + } + + /** + * Updates the visibility of a category according to its child preference count. + * + * @param categoryKey The key of the category which needs to check + */ + private void updatePreferenceCategoryVisibility(String categoryKey) { + final PreferenceCategory category = mCategoryToPrefCategoryMap.get(categoryKey); + category.setVisible(category.getPreferenceCount() != 0); + } + + /** + * Updates preferences related to system configurations. + */ + protected void updateSystemPreferences() { + updateKeyboardPreferencesVisibility(); + } + + private void updatePreferencesState() { + final List controllers = new ArrayList<>(); + getPreferenceControllers().forEach(controllers::addAll); + controllers.forEach(controller -> controller.updateState( + findPreference(controller.getPreferenceKey()))); + } + + private void updateKeyboardPreferencesVisibility() { + if (!mCategoryToPrefCategoryMap.containsKey(CATEGORY_KEYBOARD_OPTIONS)) { + return; + } + boolean isVisible = isAnyHardKeyboardsExist() + && isAnyKeyboardPreferenceAvailable(); + mCategoryToPrefCategoryMap.get(CATEGORY_KEYBOARD_OPTIONS).setVisible( + isVisible); + if (isVisible) { + //set summary here. + findPreference(KeyboardBounceKeyPreferenceController.PREF_KEY).setSummary( + getContext().getString(R.string.bounce_keys_summary, + PhysicalKeyboardFragment.BOUNCE_KEYS_THRESHOLD)); + findPreference(KeyboardSlowKeyPreferenceController.PREF_KEY).setSummary( + getContext().getString(R.string.slow_keys_summary, + PhysicalKeyboardFragment.SLOW_KEYS_THRESHOLD)); + } + } + + private boolean isAnyHardKeyboardsExist() { + for (int deviceId : InputDevice.getDeviceIds()) { + final InputDevice device = InputDevice.getDevice(deviceId); + if (device != null && !device.isVirtual() && device.isFullKeyboard()) { + return true; + } + } + return false; + } + + private boolean isAnyKeyboardPreferenceAvailable() { + for (List controllerList : getPreferenceControllers()) { + for (AbstractPreferenceController controller : controllerList) { + if (controller.getPreferenceKey().equals( + KeyboardBounceKeyPreferenceController.PREF_KEY) + || controller.getPreferenceKey().equals( + KeyboardSlowKeyPreferenceController.PREF_KEY) + || controller.getPreferenceKey().equals( + KeyboardStickyKeyPreferenceController.PREF_KEY)) { + if (controller.isAvailable()) { + return true; + } + } + } + } + return false; + } + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.accessibility_settings) { + @Override + public List getRawDataToIndex(Context context, + boolean enabled) { + return FeatureFactory.getFeatureFactory() + .getAccessibilitySearchFeatureProvider().getSearchIndexableRawData( + context); + } + }; + + @Override + public void onInputDeviceAdded(int deviceId) {} + + @Override + public void onInputDeviceRemoved(int deviceId) {} + + @Override + public void onInputDeviceChanged(int deviceId) { + mHandler.postDelayed(mUpdateRunnable, DELAY_UPDATE_SERVICES_MILLIS); + } +} diff --git a/aosp/packages/apps/Settings/src/com/android/settings/network/TetheredRepository.kt b/aosp/packages/apps/Settings/src/com/android/settings/network/TetheredRepository.kt new file mode 100644 index 000000000..eb2660f94 --- /dev/null +++ b/aosp/packages/apps/Settings/src/com/android/settings/network/TetheredRepository.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.network + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothManager +import android.bluetooth.BluetoothPan +import android.bluetooth.BluetoothProfile +import android.content.Context +import android.content.IntentFilter +import android.net.TetheringInterface +import android.net.TetheringManager +import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch + +class TetheredRepository(private val context: Context) { + private val tetheringManager = context.getSystemService(TetheringManager::class.java)!! + + private val adapter = context.getSystemService(BluetoothManager::class.java)!!.adapter + + fun tetheredTypesFlow(): Flow> = + combine( + tetheredInterfacesFlow(), + isBluetoothTetheringOnFlow(), + ) { tetheringInterfaces, isBluetoothTetheringOn -> + val mutableSet = tetheringInterfaces.map { it.type }.toMutableSet() + if (isBluetoothTetheringOn) mutableSet += TetheringManager.TETHERING_BLUETOOTH + mutableSet + }.conflate().flowOn(Dispatchers.Default) + + private fun tetheredInterfacesFlow(): Flow> = callbackFlow { + val callback = object : TetheringManager.TetheringEventCallback { + override fun onTetheredInterfacesChanged(interfaces: Set) { + trySend(interfaces) + } + } + + tetheringManager.registerTetheringEventCallback(Dispatchers.Default.asExecutor(), callback) + + awaitClose { tetheringManager.unregisterTetheringEventCallback(callback) } + }.conflate().flowOn(Dispatchers.Default) + + @OptIn(ExperimentalCoroutinesApi::class) + private fun isBluetoothTetheringOnFlow(): Flow = + merge( + flowOf(null), // kick an initial value + context.broadcastReceiverFlow(IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)), + ).flatMapLatest { + if (adapter != null && adapter.getState() == BluetoothAdapter.STATE_ON) { // TODO蓝牙服务未开发,当前关闭该服务,增加空值判断防止设置中网络和互联网设置崩溃 + isBluetoothPanTetheringOnFlow() + } else { + flowOf(false) + } + }.conflate().flowOn(Dispatchers.Default) + + private fun isBluetoothPanTetheringOnFlow() = callbackFlow { + var connectedProxy: BluetoothProfile? = null + + val listener = object : BluetoothProfile.ServiceListener { + override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { + connectedProxy = proxy + launch(Dispatchers.Default) { + trySend((proxy as BluetoothPan).isTetheringOn) + } + } + + override fun onServiceDisconnected(profile: Int) {} + } + + adapter.getProfileProxy(context, listener, BluetoothProfile.PAN) + + awaitClose { + connectedProxy?.let { adapter.closeProfileProxy(BluetoothProfile.PAN, it) } + } + }.conflate().flowOn(Dispatchers.Default) +} -- Gitee