diff --git a/res/values/strings.xml b/res/values/strings.xml index 261829b56ef..5f40087d8f6 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -221,14 +221,17 @@ Cancel - + Disable Bluetooth LE audio - + Disables Bluetooth LE audio feature if the device supports LE audio hardware capabilities. - + + Show LE audio toggle in Device Details + + Enable Bluetooth LE audio Allow List - + Enable Bluetooth LE audio allow list feature. diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml index eb17fbf0828..68e4e78945b 100644 --- a/res/xml/development_settings.xml +++ b/res/xml/development_settings.xml @@ -350,6 +350,10 @@ android:title="@string/bluetooth_disable_leaudio" android:summary="@string/bluetooth_disable_leaudio_summary" /> + + > mProfileDeviceMap = new HashMap>(); private boolean mIsLeContactSharingEnabled = false; + private boolean mIsLeAudioToggleEnabled = false; @VisibleForTesting PreferenceCategory mProfilesContainer; @@ -97,6 +98,8 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll mProfilesContainer.setLayoutResource(R.layout.preference_bluetooth_profile_category); mIsLeContactSharingEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, SettingsUIDeviceConfig.BT_LE_AUDIO_CONTACT_SHARING_ENABLED, true); + mIsLeAudioToggleEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, + SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, false); // Call refresh here even though it will get called later in onResume, to avoid the // list of switches appearing to "pop" into the page. refresh(); @@ -142,6 +145,10 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll profilePref.setEnabled(!mCachedDevice.isBusy()); } + if (profile instanceof LeAudioProfile && !mIsLeAudioToggleEnabled) { + profilePref.setVisible(false); + } + if (profile instanceof MapProfile) { profilePref.setChecked(device.getMessageAccessPermission() == BluetoothDevice.ACCESS_ALLOWED); diff --git a/src/com/android/settings/core/SettingsUIDeviceConfig.java b/src/com/android/settings/core/SettingsUIDeviceConfig.java index 94074dfa335..404b0b4ef39 100644 --- a/src/com/android/settings/core/SettingsUIDeviceConfig.java +++ b/src/com/android/settings/core/SettingsUIDeviceConfig.java @@ -42,4 +42,9 @@ public class SettingsUIDeviceConfig { * {@code true} whether or not event_log for generic actions is enabled. Default is true. */ public static final String GENERIC_EVENT_LOGGING_ENABLED = "event_logging_enabled"; + /** + * {@code true} whether to show LE Audio toggle in device detail page. Default is false. + */ + public static final String BT_LE_AUDIO_DEVICE_DETAIL_ENABLED = + "bt_le_audio_device_detail_enabled"; } diff --git a/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceController.java b/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceController.java new file mode 100644 index 00000000000..0945f0da746 --- /dev/null +++ b/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceController.java @@ -0,0 +1,102 @@ +/* + * Copyright 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.development; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothStatusCodes; +import android.content.Context; +import android.provider.DeviceConfig; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.SwitchPreference; + +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.SettingsUIDeviceConfig; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + +/** + * Preference controller to control whether display Bluetooth LE audio toggle in device detail + * settings page or not. + */ +public class BluetoothLeAudioDeviceDetailsPreferenceController + extends DeveloperOptionsPreferenceController + implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin { + + private static final String PREFERENCE_KEY = "bluetooth_show_leaudio_device_details"; + static int sLeAudioSupportedStateCache = BluetoothStatusCodes.ERROR_UNKNOWN; + + @VisibleForTesting + BluetoothAdapter mBluetoothAdapter; + + public BluetoothLeAudioDeviceDetailsPreferenceController(Context context) { + super(context); + mBluetoothAdapter = context.getSystemService(BluetoothManager.class).getAdapter(); + } + + @Override + public String getPreferenceKey() { + return PREFERENCE_KEY; + } + + @Override + public boolean isAvailable() { + if (sLeAudioSupportedStateCache == BluetoothStatusCodes.ERROR_UNKNOWN + && mBluetoothAdapter != null) { + int isLeAudioSupported = mBluetoothAdapter.isLeAudioSupported(); + if (isLeAudioSupported != BluetoothStatusCodes.ERROR_BLUETOOTH_NOT_ENABLED) { + sLeAudioSupportedStateCache = isLeAudioSupported; + } + } + + // Display the option only if LE Audio is supported + return (sLeAudioSupportedStateCache == BluetoothStatusCodes.FEATURE_SUPPORTED); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean isEnabled = (Boolean) newValue; + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_SETTINGS_UI, + SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, + isEnabled ? "true" : "false", false); + return true; + } + + @Override + public void updateState(Preference preference) { + if (!isAvailable()) { + return; + } + + final boolean leAudioDeviceDetailEnabled = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_SETTINGS_UI, + SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, false); + + ((SwitchPreference) mPreference).setChecked(leAudioDeviceDetailEnabled); + } + + @Override + protected void onDeveloperOptionsSwitchDisabled() { + super.onDeveloperOptionsSwitchDisabled(); + // Reset the toggle to null when the developer option is disabled + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_SETTINGS_UI, + SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, "null", false); + } +} diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java index f8ce97531d6..87d8c17e307 100644 --- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java +++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java @@ -660,6 +660,7 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra controllers.add(new BluetoothAvrcpVersionPreferenceController(context)); controllers.add(new BluetoothMapVersionPreferenceController(context)); controllers.add(new BluetoothLeAudioPreferenceController(context, fragment)); + controllers.add(new BluetoothLeAudioDeviceDetailsPreferenceController(context)); controllers.add(new BluetoothLeAudioAllowListPreferenceController(context, fragment)); controllers.add(new BluetoothA2dpHwOffloadPreferenceController(context, fragment)); controllers.add(new BluetoothLeAudioHwOffloadPreferenceController(context, fragment)); diff --git a/tests/robotests/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceControllerTest.java new file mode 100644 index 00000000000..b405f9ec1d5 --- /dev/null +++ b/tests/robotests/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceControllerTest.java @@ -0,0 +1,133 @@ +/* + * 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.development; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothStatusCodes; +import android.content.Context; +import android.provider.DeviceConfig; + +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.settings.core.SettingsUIDeviceConfig; +import com.android.settings.testutils.shadow.ShadowDeviceConfig; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowDeviceConfig.class}) +public class BluetoothLeAudioDeviceDetailsPreferenceControllerTest { + + @Mock + private PreferenceScreen mPreferenceScreen; + @Mock + private BluetoothAdapter mBluetoothAdapter; + @Mock + private SwitchPreference mPreference; + + private Context mContext; + private BluetoothLeAudioDeviceDetailsPreferenceController mController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mController = spy(new BluetoothLeAudioDeviceDetailsPreferenceController(mContext)); + when(mPreferenceScreen.findPreference(mController.getPreferenceKey())) + .thenReturn(mPreference); + mController.mBluetoothAdapter = mBluetoothAdapter; + mController.displayPreference(mPreferenceScreen); + } + + @After + public void tearDown() { + ShadowDeviceConfig.reset(); + } + + @Test + public void onPreferenceChanged_settingEnabled_shouldTurnOnLeAudioDeviceDetailSetting() { + mController.sLeAudioSupportedStateCache = BluetoothStatusCodes.FEATURE_SUPPORTED; + mController.onPreferenceChange(mPreference, true /* new value */); + final boolean isEnabled = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_SETTINGS_UI, + SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, false); + + assertThat(isEnabled).isTrue(); + } + + @Test + public void onPreferenceChanged_settingDisabled_shouldTurnOffLeAudioDeviceDetailSetting() { + mController.sLeAudioSupportedStateCache = BluetoothStatusCodes.FEATURE_SUPPORTED; + mController.onPreferenceChange(mPreference, false /* new value */); + final boolean isEnabled = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_SETTINGS_UI, + SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, false); + + assertThat(isEnabled).isFalse(); + } + + @Test + public void updateState_settingEnabled_preferenceShouldBeChecked() { + mController.sLeAudioSupportedStateCache = BluetoothStatusCodes.FEATURE_SUPPORTED; + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SETTINGS_UI, + SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, "true", false); + mController.updateState(mPreference); + + verify(mPreference).setChecked(true); + } + + @Test + public void updateState_settingDisabled_preferenceShouldNotBeChecked() { + mController.sLeAudioSupportedStateCache = BluetoothStatusCodes.FEATURE_SUPPORTED; + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SETTINGS_UI, + SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, "false", false); + mController.updateState(mPreference); + + verify(mPreference).setChecked(false); + } + + @Test + public void isAvailable_leAudioSupported() { + mController.sLeAudioSupportedStateCache = BluetoothStatusCodes.ERROR_UNKNOWN; + when(mBluetoothAdapter.isLeAudioSupported()) + .thenReturn(BluetoothStatusCodes.FEATURE_SUPPORTED); + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void isAvailable_leAudioNotSupported() { + mController.sLeAudioSupportedStateCache = BluetoothStatusCodes.ERROR_UNKNOWN; + when(mBluetoothAdapter.isLeAudioSupported()) + .thenReturn(BluetoothStatusCodes.FEATURE_NOT_SUPPORTED); + assertThat(mController.isAvailable()).isFalse(); + } +}