diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 357818cd137..0e35fed6a11 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -233,6 +233,26 @@ + + + + Disabled + + Unicast + + Unicast and Broadcast + + + + + + disabled + + unicast + + broadcast + + Use System Default: %1$d diff --git a/res/values/strings.xml b/res/values/strings.xml index 6a8da6a4c74..b760f68e872 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -249,7 +249,8 @@ Disable Bluetooth LE audio Disables Bluetooth LE audio feature if the device supports LE audio hardware capabilities. - + + Bluetooth LE Audio mode Show LE audio toggle in Device Details diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml index d44927fe512..fb5e2809434 100644 --- a/res/xml/development_settings.xml +++ b/res/xml/development_settings.xml @@ -373,6 +373,13 @@ android:title="@string/bluetooth_disable_leaudio" android:summary="@string/bluetooth_disable_leaudio_summary" /> + + diff --git a/src/com/android/settings/development/BluetoothLeAudioModePreferenceController.java b/src/com/android/settings/development/BluetoothLeAudioModePreferenceController.java new file mode 100644 index 00000000000..06cfe65043e --- /dev/null +++ b/src/com/android/settings/development/BluetoothLeAudioModePreferenceController.java @@ -0,0 +1,139 @@ +/* + * Copyright 2024 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.os.SystemProperties; +import android.sysprop.BluetoothProperties; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.preference.ListPreference; +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; + + +/** + * Preference controller to control Bluetooth LE audio mode + */ +public class BluetoothLeAudioModePreferenceController + extends DeveloperOptionsPreferenceController + implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin { + + private static final String PREFERENCE_KEY = "bluetooth_leaudio_mode"; + + static final String LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY = + "persist.bluetooth.leaudio_dynamic_switcher.mode"; + + @Nullable private final DevelopmentSettingsDashboardFragment mFragment; + + private final String[] mListValues; + private final String[] mListSummaries; + @VisibleForTesting + @Nullable String mNewMode; + @VisibleForTesting + BluetoothAdapter mBluetoothAdapter; + + boolean mChanged = false; + + public BluetoothLeAudioModePreferenceController(@NonNull Context context, + @Nullable DevelopmentSettingsDashboardFragment fragment) { + super(context); + mFragment = fragment; + mBluetoothAdapter = context.getSystemService(BluetoothManager.class).getAdapter(); + + mListValues = context.getResources().getStringArray(R.array.bluetooth_leaudio_mode_values); + mListSummaries = context.getResources().getStringArray(R.array.bluetooth_leaudio_mode); + } + + @Override + @NonNull public String getPreferenceKey() { + return PREFERENCE_KEY; + } + + @Override + public boolean isAvailable() { + return BluetoothProperties.isProfileBapBroadcastSourceEnabled().orElse(false); + } + + @Override + public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) { + if (mFragment == null) { + return false; + } + + BluetoothRebootDialog.show(mFragment); + mChanged = true; + mNewMode = newValue.toString(); + return false; + } + + @Override + public void updateState(@NonNull Preference preference) { + if (mBluetoothAdapter == null) { + return; + } + + if (mBluetoothAdapter.isLeAudioBroadcastSourceSupported() + == BluetoothStatusCodes.FEATURE_SUPPORTED) { + SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, "broadcast"); + } else if (mBluetoothAdapter.isLeAudioSupported() + == BluetoothStatusCodes.FEATURE_SUPPORTED) { + SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, "unicast"); + } else { + SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, "disabled"); + } + + final String currentValue = SystemProperties.get(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY); + int index = 0; + for (int i = 0; i < mListValues.length; i++) { + if (TextUtils.equals(currentValue, mListValues[i])) { + index = i; + break; + } + } + + final ListPreference listPreference = (ListPreference) preference; + listPreference.setValue(mListValues[index]); + listPreference.setSummary(mListSummaries[index]); + } + + /** + * Called when the RebootDialog confirm is clicked. + */ + public void onRebootDialogConfirmed() { + if (!mChanged) { + return; + } + SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mNewMode); + } + + /** + * Called when the RebootDialog cancel is clicked. + */ + public void onRebootDialogCanceled() { + mChanged = false; + } +} diff --git a/src/com/android/settings/development/BluetoothLeAudioPreferenceController.java b/src/com/android/settings/development/BluetoothLeAudioPreferenceController.java index f1b81b4a2d6..2a544f263e3 100644 --- a/src/com/android/settings/development/BluetoothLeAudioPreferenceController.java +++ b/src/com/android/settings/development/BluetoothLeAudioPreferenceController.java @@ -21,6 +21,7 @@ import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothStatusCodes; import android.content.Context; import android.os.SystemProperties; +import android.sysprop.BluetoothProperties; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; @@ -64,6 +65,12 @@ public class BluetoothLeAudioPreferenceController return PREFERENCE_KEY; } + @Override + public boolean isAvailable() { + return BluetoothProperties.isProfileBapUnicastClientEnabled().orElse(false) + && !BluetoothProperties.isProfileBapBroadcastSourceEnabled().orElse(false); + } + @Override public boolean onPreferenceChange(Preference preference, Object newValue) { BluetoothRebootDialog.show(mFragment); diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java index 73567bcf392..504eda8132b 100644 --- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java +++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java @@ -454,6 +454,11 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra getDevelopmentOptionsController( BluetoothLeAudioPreferenceController.class); leAudioFeatureController.onRebootDialogConfirmed(); + + final BluetoothLeAudioModePreferenceController leAudioModeController = + getDevelopmentOptionsController( + BluetoothLeAudioModePreferenceController.class); + leAudioModeController.onRebootDialogConfirmed(); } @Override @@ -471,6 +476,11 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra getDevelopmentOptionsController( BluetoothLeAudioPreferenceController.class); leAudioFeatureController.onRebootDialogCanceled(); + + final BluetoothLeAudioModePreferenceController leAudioModeController = + getDevelopmentOptionsController( + BluetoothLeAudioModePreferenceController.class); + leAudioModeController.onRebootDialogCanceled(); } @Override @@ -670,6 +680,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 BluetoothLeAudioModePreferenceController(context, fragment)); controllers.add(new BluetoothLeAudioDeviceDetailsPreferenceController(context)); controllers.add(new BluetoothLeAudioAllowListPreferenceController(context, fragment)); controllers.add(new BluetoothA2dpHwOffloadPreferenceController(context, fragment)); diff --git a/tests/robotests/src/com/android/settings/development/BluetoothLeAudioModePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/BluetoothLeAudioModePreferenceControllerTest.java new file mode 100644 index 00000000000..f35fb17f8ca --- /dev/null +++ b/tests/robotests/src/com/android/settings/development/BluetoothLeAudioModePreferenceControllerTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2022 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.android.settings.development.BluetoothLeAudioModePreferenceController + .LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.os.SystemProperties; + +import androidx.preference.ListPreference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; + +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; + +@RunWith(RobolectricTestRunner.class) +public class BluetoothLeAudioModePreferenceControllerTest { + + @Mock + private PreferenceScreen mPreferenceScreen; + @Mock + private DevelopmentSettingsDashboardFragment mFragment; + @Mock + private BluetoothAdapter mBluetoothAdapter; + @Mock + private ListPreference mPreference; + + private Context mContext; + private BluetoothLeAudioModePreferenceController mController; + private String[] mListValues; + private String[] mListSummaries; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mListValues = mContext.getResources().getStringArray( + R.array.bluetooth_leaudio_mode_values); + mListSummaries = mContext.getResources().getStringArray( + R.array.bluetooth_leaudio_mode); + mController = spy(new BluetoothLeAudioModePreferenceController(mContext, mFragment)); + when(mPreferenceScreen.findPreference(mController.getPreferenceKey())) + .thenReturn(mPreference); + mController.mBluetoothAdapter = mBluetoothAdapter; + mController.displayPreference(mPreferenceScreen); + } + + @Test + public void onRebootDialogConfirmed_changeLeAudioMode_shouldSetLeAudioMode() { + mController.mChanged = true; + SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0]); + mController.mNewMode = mListValues[1]; + + mController.onRebootDialogConfirmed(); + assertThat(SystemProperties.get(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0]) + .equals(mController.mNewMode)).isTrue(); + } + + @Test + public void onRebootDialogConfirmed_notChangeLeAudioMode_shouldNotSetLeAudioMode() { + mController.mChanged = false; + SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0]); + mController.mNewMode = mListValues[1]; + + mController.onRebootDialogConfirmed(); + assertThat(SystemProperties.get(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0]) + .equals(mController.mNewMode)).isFalse(); + } + + @Test + public void onRebootDialogCanceled_shouldNotSetLeAudioMode() { + mController.mChanged = true; + SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0]); + mController.mNewMode = mListValues[1]; + + mController.onRebootDialogCanceled(); + assertThat(SystemProperties.get(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mListValues[0]) + .equals(mController.mNewMode)).isFalse(); + } +}