From 97dbd0bb540f53661b5754172ce6fdb52446840f Mon Sep 17 00:00:00 2001 From: chelseahao Date: Fri, 18 Oct 2024 14:54:38 +0800 Subject: [PATCH] Add developer option for le audio sharing ui flow. Test: atest com.android.settings.development Bug: 368401233 Flag: com.android.settingslib.flags.audio_sharing_developer_option Change-Id: I9a8c7ad9a2620184080bcdfc9f430c3b25659b7d --- res/values/strings.xml | 7 + res/xml/development_settings.xml | 5 + ...etoothLeAudioModePreferenceController.java | 38 +-- ...luetoothLeAudioUiPreferenceController.java | 144 ++++++++++++ .../DevelopmentSettingsDashboardFragment.java | 22 +- ...oothLeAudioUiPreferenceControllerTest.java | 220 ++++++++++++++++++ 6 files changed, 417 insertions(+), 19 deletions(-) create mode 100644 src/com/android/settings/development/BluetoothLeAudioUiPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/development/BluetoothLeAudioUiPreferenceControllerTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index b62df6081ee..42c00e0099c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -260,6 +260,13 @@ Disables Bluetooth LE audio feature if the device supports LE audio hardware capabilities. Bluetooth LE Audio mode + + + Enable Bluetooth LE Audio Broadcast UI preview + + Enables the LE Audio Sharing UI preview + including personal audio sharing and private broadcast + Show LE audio toggle in Device Details diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml index a4addf6b0fa..dde397b81ba 100644 --- a/res/xml/development_settings.xml +++ b/res/xml/development_settings.xml @@ -393,6 +393,11 @@ android:entries="@array/bluetooth_leaudio_mode" android:entryValues="@array/bluetooth_leaudio_mode_values"/> + + diff --git a/src/com/android/settings/development/BluetoothLeAudioModePreferenceController.java b/src/com/android/settings/development/BluetoothLeAudioModePreferenceController.java index 739258d3fda..bf5efa76daf 100644 --- a/src/com/android/settings/development/BluetoothLeAudioModePreferenceController.java +++ b/src/com/android/settings/development/BluetoothLeAudioModePreferenceController.java @@ -34,12 +34,10 @@ import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.development.DeveloperOptionsPreferenceController; +import java.util.Objects; -/** - * Preference controller to control Bluetooth LE audio mode - */ -public class BluetoothLeAudioModePreferenceController - extends 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"; @@ -51,15 +49,13 @@ public class BluetoothLeAudioModePreferenceController private final String[] mListValues; private final String[] mListSummaries; - @VisibleForTesting - @Nullable String mNewMode; - @VisibleForTesting - BluetoothAdapter mBluetoothAdapter; + @VisibleForTesting @Nullable String mNewMode; + @VisibleForTesting BluetoothAdapter mBluetoothAdapter; boolean mChanged = false; - public BluetoothLeAudioModePreferenceController(@NonNull Context context, - @Nullable DevelopmentSettingsDashboardFragment fragment) { + public BluetoothLeAudioModePreferenceController( + @NonNull Context context, @Nullable DevelopmentSettingsDashboardFragment fragment) { super(context); mFragment = fragment; mBluetoothAdapter = context.getSystemService(BluetoothManager.class).getAdapter(); @@ -69,7 +65,8 @@ public class BluetoothLeAudioModePreferenceController } @Override - @NonNull public String getPreferenceKey() { + @NonNull + public String getPreferenceKey() { return PREFERENCE_KEY; } @@ -125,20 +122,25 @@ public class BluetoothLeAudioModePreferenceController } } - /** - * Called when the RebootDialog confirm is clicked. - */ + /** Called when the RebootDialog confirm is clicked. */ public void onRebootDialogConfirmed() { if (!mChanged) { return; } SystemProperties.set(LE_AUDIO_DYNAMIC_SWITCHER_MODE_PROPERTY, mNewMode); + if (mFragment != null && !Objects.equals(mNewMode, "broadcast")) { + mFragment.onBroadcastDisabled(); + } } - /** - * Called when the RebootDialog cancel is clicked. - */ + /** Called when the RebootDialog cancel is clicked. */ public void onRebootDialogCanceled() { mChanged = false; } + + public interface OnModeChangeListener { + + /** Called when the broadcast mode is disabled. */ + void onBroadcastDisabled(); + } } diff --git a/src/com/android/settings/development/BluetoothLeAudioUiPreferenceController.java b/src/com/android/settings/development/BluetoothLeAudioUiPreferenceController.java new file mode 100644 index 00000000000..f2ae55ff82f --- /dev/null +++ b/src/com/android/settings/development/BluetoothLeAudioUiPreferenceController.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 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.BluetoothStatusCodes; +import android.content.ContentResolver; +import android.content.Context; +import android.provider.Settings; +import android.sysprop.BluetoothProperties; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.SwitchPreferenceCompat; + +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.development.DeveloperOptionsPreferenceController; +import com.android.settingslib.flags.Flags; +import com.android.settingslib.utils.ThreadUtils; + +/** Preference controller to enable / disable the Bluetooth LE audio sharing UI flow */ +public class BluetoothLeAudioUiPreferenceController extends DeveloperOptionsPreferenceController + implements Preference.OnPreferenceChangeListener, + PreferenceControllerMixin, + BluetoothLeAudioModePreferenceController.OnModeChangeListener { + private static final String TAG = "BluetoothLeAudioUiPreferenceController"; + private static final String PREFERENCE_KEY = "bluetooth_leaudio_broadcast_ui"; + + @VisibleForTesting + static final String VALUE_KEY = "bluetooth_le_audio_sharing_ui_preview_enabled"; + + @VisibleForTesting static final int VALUE_OFF = 0; + @VisibleForTesting static final int VALUE_ON = 1; + @VisibleForTesting static final int VALUE_UNSET = -1; + @Nullable private final DevelopmentSettingsDashboardFragment mFragment; + private final BluetoothAdapter mBluetoothAdapter; + private boolean mCurrentSettingsValue = false; + private boolean mShouldToggleCurrentValue = false; + + public BluetoothLeAudioUiPreferenceController( + @NonNull Context context, @Nullable DevelopmentSettingsDashboardFragment fragment) { + super(context); + mFragment = fragment; + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + } + + @Override + public boolean isAvailable() { + return Flags.audioSharingDeveloperOption() + && BluetoothProperties.isProfileBapBroadcastSourceEnabled().orElse(false) + && BluetoothProperties.isProfileBapBroadcastAssistEnabled().orElse(false); + } + + @Override + public boolean onPreferenceChange(@NonNull Preference preference, @Nullable Object newValue) { + if (mFragment != null && newValue != null && (boolean) newValue != mCurrentSettingsValue) { + mShouldToggleCurrentValue = true; + BluetoothRebootDialog.show(mFragment); + } + return false; + } + + @Override + public void updateState(@NonNull Preference preference) { + if (mBluetoothAdapter == null) { + return; + } + var unused = ThreadUtils.postOnBackgroundThread( + () -> { + boolean shouldEnable = + mBluetoothAdapter.isEnabled() + && mBluetoothAdapter.isLeAudioBroadcastSourceSupported() + == BluetoothStatusCodes.FEATURE_SUPPORTED + && mBluetoothAdapter.isLeAudioBroadcastAssistantSupported() + == BluetoothStatusCodes.FEATURE_SUPPORTED; + boolean valueOn = + Settings.Global.getInt( + mContext.getContentResolver(), VALUE_KEY, VALUE_UNSET) + == VALUE_ON; + mContext.getMainExecutor() + .execute( + () -> { + if (!shouldEnable && valueOn) { + Log.e( + TAG, + "Error state: toggle disabled but current" + + " settings value is true."); + } + mCurrentSettingsValue = valueOn; + preference.setEnabled(shouldEnable); + ((SwitchPreferenceCompat) preference).setChecked(valueOn); + }); + }); + } + + @Override + public @NonNull String getPreferenceKey() { + return PREFERENCE_KEY; + } + + /** Called when the RebootDialog confirm is clicked. */ + public void onRebootDialogConfirmed() { + if (isAvailable() && mShouldToggleCurrentValue) { + // Blocking, ensure reboot happens after value is saved. + Log.d(TAG, "onRebootDialogConfirmed(): setting value to " + !mCurrentSettingsValue); + toggleSetting(mContext.getContentResolver(), !mCurrentSettingsValue); + } + } + + /** Called when the RebootDialog cancel is clicked. */ + public void onRebootDialogCanceled() { + mShouldToggleCurrentValue = false; + } + + @Override + public void onBroadcastDisabled() { + if (isAvailable() && mCurrentSettingsValue) { + Log.d(TAG, "onBroadcastDisabled(): setting value to false"); + // Blocking, ensure reboot happens after value is saved. + toggleSetting(mContext.getContentResolver(), false); + } + } + + private static void toggleSetting(ContentResolver contentResolver, boolean valueOn) { + Settings.Global.putInt(contentResolver, VALUE_KEY, valueOn ? VALUE_ON : VALUE_OFF); + } +} diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java index 8a970fbdd9b..b453de1ac56 100644 --- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java +++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java @@ -99,7 +99,9 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra AdbClearKeysDialogHost, LogPersistDialogHost, BluetoothRebootDialog.OnRebootDialogListener, AbstractBluetoothPreferenceController.Callback, - NfcRebootDialog.OnNfcRebootDialogConfirmedListener, BluetoothSnoopLogHost { + NfcRebootDialog.OnNfcRebootDialogConfirmedListener, + BluetoothSnoopLogHost, + BluetoothLeAudioModePreferenceController.OnModeChangeListener { private static final String TAG = "DevSettingsDashboard"; @VisibleForTesting static final int REQUEST_BIOMETRIC_PROMPT = 100; @@ -498,6 +500,10 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra getDevelopmentOptionsController( BluetoothLeAudioModePreferenceController.class); leAudioModeController.onRebootDialogConfirmed(); + + final BluetoothLeAudioUiPreferenceController leAudioUiController = + getDevelopmentOptionsController(BluetoothLeAudioUiPreferenceController.class); + leAudioUiController.onRebootDialogConfirmed(); } @Override @@ -520,6 +526,10 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra getDevelopmentOptionsController( BluetoothLeAudioModePreferenceController.class); leAudioModeController.onRebootDialogCanceled(); + + final BluetoothLeAudioUiPreferenceController leAudioUiController = + getDevelopmentOptionsController(BluetoothLeAudioUiPreferenceController.class); + leAudioUiController.onRebootDialogCanceled(); } @Override @@ -741,6 +751,7 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra controllers.add(new BluetoothMapVersionPreferenceController(context)); controllers.add(new BluetoothLeAudioPreferenceController(context, fragment)); controllers.add(new BluetoothLeAudioModePreferenceController(context, fragment)); + controllers.add(new BluetoothLeAudioUiPreferenceController(context, fragment)); controllers.add(new BluetoothLeAudioDeviceDetailsPreferenceController(context)); controllers.add(new BluetoothLeAudioAllowListPreferenceController(context)); controllers.add(new BluetoothA2dpHwOffloadPreferenceController(context, fragment)); @@ -858,6 +869,15 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra } } + @Override + public void onBroadcastDisabled() { + for (AbstractPreferenceController controller : mPreferenceControllers) { + if (controller instanceof BluetoothLeAudioUiPreferenceController) { + ((BluetoothLeAudioUiPreferenceController) controller).onBroadcastDisabled(); + } + } + } + /** * For Search. */ diff --git a/tests/robotests/src/com/android/settings/development/BluetoothLeAudioUiPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/BluetoothLeAudioUiPreferenceControllerTest.java new file mode 100644 index 00000000000..a4462e460fd --- /dev/null +++ b/tests/robotests/src/com/android/settings/development/BluetoothLeAudioUiPreferenceControllerTest.java @@ -0,0 +1,220 @@ +/* + * Copyright (C) 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 static com.android.settings.development.BluetoothLeAudioUiPreferenceController.VALUE_KEY; +import static com.android.settings.development.BluetoothLeAudioUiPreferenceController.VALUE_OFF; +import static com.android.settings.development.BluetoothLeAudioUiPreferenceController.VALUE_ON; +import static com.android.settings.development.BluetoothLeAudioUiPreferenceController.VALUE_UNSET; + +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 static org.robolectric.Shadows.shadowOf; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothStatusCodes; +import android.content.Context; +import android.os.Looper; +import android.os.SystemProperties; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.provider.Settings; + +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreferenceCompat; + +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settingslib.flags.Flags; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.shadow.api.Shadow; + +@RunWith(RobolectricTestRunner.class) +@Config( + shadows = { + ShadowBluetoothAdapter.class, + BluetoothLeAudioUiPreferenceControllerTest.ShadowBluetoothRebootDialogFragment.class + }) +public class BluetoothLeAudioUiPreferenceControllerTest { + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private static final String SOURCE_SYSTEM_PROP_KEY = + "bluetooth.profile.bap.broadcast.source.enabled"; + private static final String ASSIST_SYSTEM_PROP_KEY = + "bluetooth.profile.bap.broadcast.assist.enabled"; + @Mock private PreferenceScreen mPreferenceScreen; + @Mock private DevelopmentSettingsDashboardFragment mFragment; + @Mock private SwitchPreferenceCompat mPreference; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; + private Context mContext; + private BluetoothLeAudioUiPreferenceController mController; + + @Before + public void setup() { + mContext = RuntimeEnvironment.getApplication(); + SystemProperties.set(SOURCE_SYSTEM_PROP_KEY, "true"); + SystemProperties.set(ASSIST_SYSTEM_PROP_KEY, "true"); + // Reset value + Settings.Global.putInt(mContext.getContentResolver(), VALUE_KEY, VALUE_UNSET); + mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + mShadowBluetoothAdapter.setEnabled(true); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mController = spy(new BluetoothLeAudioUiPreferenceController(mContext, mFragment)); + when(mPreferenceScreen.findPreference(mController.getPreferenceKey())) + .thenReturn(mPreference); + mController.displayPreference(mPreferenceScreen); + } + + @Test + @DisableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION) + public void isAvailable_flagOff_returnFalse() { + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION) + public void isAvailable_flagOn_returnFalse() { + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + @EnableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION) + public void isAvailable_flagOn_propertyOff_returnFalse() { + SystemProperties.set(SOURCE_SYSTEM_PROP_KEY, "false"); + assertThat(mController.isAvailable()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION) + public void updateState_settingEnabled_checked() { + Settings.Global.putInt(mContext.getContentResolver(), VALUE_KEY, VALUE_ON); + mController.updateState(mPreference); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mPreference).setChecked(true); + } + + @Test + @EnableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION) + public void updateState_settingDisabled_notChecked() { + Settings.Global.putInt(mContext.getContentResolver(), VALUE_KEY, VALUE_OFF); + mController.updateState(mPreference); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mPreference).setChecked(false); + } + + @Test + @EnableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION) + public void updateState_featureSupported_enabled() { + mController.updateState(mPreference); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mPreference).setEnabled(true); + } + + @Test + @EnableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION) + public void updateState_featureUnsupported_disabled() { + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_NOT_SUPPORTED); + mController.updateState(mPreference); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mPreference).setEnabled(false); + } + + @Test + @EnableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION) + public void onRebootDialogConfirmed_noChange_doNothing() { + mController.onRebootDialogConfirmed(); + + int result = Settings.Global.getInt(mContext.getContentResolver(), VALUE_KEY, VALUE_UNSET); + assertThat(result).isEqualTo(VALUE_UNSET); + } + + @Test + @EnableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION) + public void onRebootDialogConfirmed_hasChange_turnOn() { + mController.onPreferenceChange(mPreference, true); + mController.onRebootDialogConfirmed(); + + int result = Settings.Global.getInt(mContext.getContentResolver(), VALUE_KEY, VALUE_UNSET); + assertThat(result).isEqualTo(VALUE_ON); + } + + @Test + @EnableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION) + public void onRebootDialogCanceled_hasChange_doNothing() { + mController.onPreferenceChange(mPreference, true); + mController.onRebootDialogCanceled(); + + int result = Settings.Global.getInt(mContext.getContentResolver(), VALUE_KEY, VALUE_UNSET); + assertThat(result).isEqualTo(VALUE_UNSET); + } + + @Test + @EnableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION) + public void onBroadcastDisabled_currentValueOn_turnOff() { + Settings.Global.putInt(mContext.getContentResolver(), VALUE_KEY, VALUE_ON); + mController.updateState(mPreference); + shadowOf(Looper.getMainLooper()).idle(); + mController.onBroadcastDisabled(); + + int result = Settings.Global.getInt(mContext.getContentResolver(), VALUE_KEY, VALUE_UNSET); + assertThat(result).isEqualTo(VALUE_OFF); + } + + @Test + @EnableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION) + public void onBroadcastDisabled_currentValueUnset_doNothing() { + mController.updateState(mPreference); + mController.onBroadcastDisabled(); + shadowOf(Looper.getMainLooper()).idle(); + + int result = Settings.Global.getInt(mContext.getContentResolver(), VALUE_KEY, VALUE_UNSET); + assertThat(result).isEqualTo(VALUE_UNSET); + } + + @Implements(BluetoothRebootDialog.class) + public static class ShadowBluetoothRebootDialogFragment { + + /** Shadow implementation of BluetoothRebootDialog#show */ + @Implementation + public static void show(DevelopmentSettingsDashboardFragment host) { + // Do nothing. + } + } +}