From cdcf50e3d2fc5c4110cfbb78c02f27717878ddc7 Mon Sep 17 00:00:00 2001 From: Haijie Hong Date: Wed, 8 Jan 2025 12:57:56 +0800 Subject: [PATCH] Add audio sharing entrypoint in device details BUG: 383935069 Test: local tested Flag: com.android.settingslib.flags.enable_le_audio_sharing Change-Id: Ib90422d262eba84c3a308d8e4c5652d8c5f96808 --- res/xml/bluetooth_device_details_fragment.xml | 4 + ...luetoothDetailsAudioSharingController.java | 132 ++++++++++++++ .../BluetoothDeviceDetailsFragment.java | 3 + .../audiostreams/AudioStreamsHelper.java | 2 +- ...oothDetailsAudioSharingControllerTest.java | 169 ++++++++++++++++++ 5 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 src/com/android/settings/bluetooth/BluetoothDetailsAudioSharingController.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioSharingControllerTest.java diff --git a/res/xml/bluetooth_device_details_fragment.xml b/res/xml/bluetooth_device_details_fragment.xml index f2e9e73f1b8..0c8662664f4 100644 --- a/res/xml/bluetooth_device_details_fragment.xml +++ b/res/xml/bluetooth_device_details_fragment.xml @@ -68,6 +68,10 @@ settings:controller="com.android.settings.slices.SlicePreferenceController" settings:allowDividerAbove="true"/> + + diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsAudioSharingController.java b/src/com/android/settings/bluetooth/BluetoothDetailsAudioSharingController.java new file mode 100644 index 00000000000..bb4f2a78b47 --- /dev/null +++ b/src/com/android/settings/bluetooth/BluetoothDetailsAudioSharingController.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2025 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.bluetooth; + +import static com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast.EXTRA_START_LE_AUDIO_SHARING; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.connecteddevice.audiosharing.AudioSharingDashboardFragment; +import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsDashboardFragment; +import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsHelper; +import com.android.settings.core.SubSettingLauncher; +import com.android.settingslib.bluetooth.BluetoothUtils; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.core.lifecycle.Lifecycle; + +/** Controller for audio sharing control preferences. */ +public class BluetoothDetailsAudioSharingController extends BluetoothDetailsController { + private static final String KEY_AUDIO_SHARING_CONTROL = "audio_sharing_control"; + private static final String KEY_AUDIO_SHARING = "audio_sharing"; + private static final String KEY_FIND_AUDIO_STREAM = "find_audio_stream"; + + @Nullable PreferenceCategory mProfilesContainer; + LocalBluetoothManager mLocalBluetoothManager; + + public BluetoothDetailsAudioSharingController( + @NonNull Context context, + @NonNull PreferenceFragmentCompat fragment, + @NonNull LocalBluetoothManager localBtManager, + @NonNull CachedBluetoothDevice device, + @NonNull Lifecycle lifecycle) { + super(context, fragment, device, lifecycle); + mLocalBluetoothManager = localBtManager; + } + + @Override + public boolean isAvailable() { + return BluetoothUtils.isAudioSharingUIAvailable(mContext) + && mCachedDevice.isConnectedLeAudioDevice(); + } + + @Override + protected void init(PreferenceScreen screen) { + mProfilesContainer = screen.findPreference(KEY_AUDIO_SHARING_CONTROL); + } + + @Override + protected void refresh() { + if (mProfilesContainer == null) { + return; + } + if (!isAvailable()) { + mProfilesContainer.setVisible(false); + return; + } + mProfilesContainer.setVisible(true); + mProfilesContainer.removeAll(); + mProfilesContainer.addPreference(createAudioSharingPreference()); + if ((BluetoothUtils.isActiveLeAudioDevice(mCachedDevice) + || AudioStreamsHelper.hasConnectedBroadcastSource( + mCachedDevice, mLocalBluetoothManager)) + && !BluetoothUtils.isBroadcasting(mLocalBluetoothManager)) { + mProfilesContainer.addPreference(createFindAudioStreamPreference()); + } + } + + private Preference createAudioSharingPreference() { + Preference audioSharingPref = new Preference(mContext); + audioSharingPref.setKey(KEY_AUDIO_SHARING); + audioSharingPref.setTitle(R.string.audio_sharing_title); + audioSharingPref.setIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing); + audioSharingPref.setOnPreferenceClickListener( + preference -> { + Bundle args = new Bundle(); + args.putBoolean(EXTRA_START_LE_AUDIO_SHARING, true); + new SubSettingLauncher(mContext) + .setDestination(AudioSharingDashboardFragment.class.getName()) + .setSourceMetricsCategory(SettingsEnums.BLUETOOTH_DEVICE_DETAILS) + .setArguments(args) + .launch(); + return true; + }); + return audioSharingPref; + } + + private Preference createFindAudioStreamPreference() { + Preference findAudioStreamPref = new Preference(mContext); + findAudioStreamPref.setKey(KEY_FIND_AUDIO_STREAM); + findAudioStreamPref.setTitle(R.string.audio_streams_main_page_title); + findAudioStreamPref.setIcon(com.android.settingslib.R.drawable.ic_bt_le_audio_sharing); + findAudioStreamPref.setOnPreferenceClickListener( + preference -> { + new SubSettingLauncher(mContext) + .setDestination(AudioStreamsDashboardFragment.class.getName()) + .setSourceMetricsCategory(SettingsEnums.BLUETOOTH_DEVICE_DETAILS) + .launch(); + return true; + }); + return findAudioStreamPref; + } + + @Override + @NonNull + public String getPreferenceKey() { + return KEY_AUDIO_SHARING_CONTROL; + } +} diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java index 984cd861a5d..a8f28e9874e 100644 --- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java +++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java @@ -440,6 +440,9 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment context, this, mCachedDevice, lifecycle)); controllers.add(new BluetoothDetailsButtonsController(context, this, mCachedDevice, lifecycle)); + controllers.add( + new BluetoothDetailsAudioSharingController( + context, this, mManager, mCachedDevice, lifecycle)); controllers.add(new BluetoothDetailsCompanionAppsController(context, this, mCachedDevice, lifecycle)); controllers.add(new BluetoothDetailsAudioDeviceTypeController(context, this, mManager, diff --git a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java index c86222ca362..4d6c4cab938 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java +++ b/src/com/android/settings/connecteddevice/audiosharing/audiostreams/AudioStreamsHelper.java @@ -269,7 +269,7 @@ public class AudioStreamsHelper { * @param localBtManager The BT manager to provide BT functions. * @return Whether the device has connected to a broadcast source. */ - private static boolean hasConnectedBroadcastSource( + public static boolean hasConnectedBroadcastSource( CachedBluetoothDevice cachedDevice, LocalBluetoothManager localBtManager) { if (localBtManager == null) { Log.d(TAG, "Skip check hasConnectedBroadcastSource due to bt manager is null"); diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioSharingControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioSharingControllerTest.java new file mode 100644 index 00000000000..2ce68e486de --- /dev/null +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsAudioSharingControllerTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2025 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.bluetooth; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothStatusCodes; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.preference.PreferenceCategory; + +import com.android.settings.R; +import com.android.settings.connecteddevice.audiosharing.audiostreams.AudioStreamsHelper; +import com.android.settings.connecteddevice.audiosharing.audiostreams.testshadows.ShadowAudioStreamsHelper; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.flags.Flags; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; + +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothAdapter.class, ShadowAudioStreamsHelper.class}) +public class BluetoothDetailsAudioSharingControllerTest extends BluetoothDetailsControllerTestBase { + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private LocalBluetoothManager mLocalManager; + @Mock private AudioStreamsHelper mAudioStreamsHelper; + @Mock private BluetoothLeBroadcastReceiveState mBroadcastReceiveState; + + private ShadowBluetoothAdapter mShadowBluetoothAdapter; + private BluetoothDetailsAudioSharingController mController; + private PreferenceCategory mContainer; + + @Override + public void setUp() { + super.setUp(); + mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + ShadowAudioStreamsHelper.setUseMock(mAudioStreamsHelper); + mController = + new BluetoothDetailsAudioSharingController( + mContext, mFragment, mLocalManager, mCachedDevice, mLifecycle); + mContainer = new PreferenceCategory(mContext); + mContainer.setKey(mController.getPreferenceKey()); + mScreen.addPreference(mContainer); + setupDevice(mDeviceConfig); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + public void notConnected_noAudioSharingPreferences() { + when(mCachedDevice.isConnectedLeAudioDevice()).thenReturn(false); + + showScreen(mController); + + assertThat(mContainer.isVisible()).isFalse(); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + public void connected_showOnePreference() { + when(mCachedDevice.isConnectedLeAudioDevice()).thenReturn(true); + when(mCachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO)).thenReturn(false); + when(mLocalManager + .getProfileManager() + .getLeAudioBroadcastAssistantProfile() + .getAllSources(mDevice)) + .thenReturn(List.of()); + when(mLocalManager + .getProfileManager() + .getLeAudioBroadcastProfile() + .isEnabled(mDevice)) + .thenReturn(true); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + + showScreen(mController); + + assertThat(mContainer.isVisible()).isTrue(); + assertThat(mContainer.getPreferenceCount()).isEqualTo(1); + assertThat(mContainer.getPreference(0).getTitle()) + .isEqualTo(mContext.getString(R.string.audio_sharing_title)); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + public void connected_active_showTwoPreference() { + when(mCachedDevice.isConnectedLeAudioDevice()).thenReturn(true); + when(mCachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO)).thenReturn(true); + when(mLocalManager + .getProfileManager() + .getLeAudioBroadcastAssistantProfile() + .getAllSources(mDevice)) + .thenReturn(List.of()); + when(mLocalManager + .getProfileManager() + .getLeAudioBroadcastProfile() + .isEnabled(mDevice)) + .thenReturn(false); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + + showScreen(mController); + + assertThat(mContainer.isVisible()).isTrue(); + assertThat(mContainer.getPreferenceCount()).isEqualTo(2); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + public void connected_hasConnectedBroadcastSource_showTwoPreference() { + when(mCachedDevice.isConnectedLeAudioDevice()).thenReturn(true); + when(mCachedDevice.isActiveDevice(BluetoothProfile.LE_AUDIO)).thenReturn(false); + when(mLocalManager + .getProfileManager() + .getLeAudioBroadcastAssistantProfile() + .getAllSources(mDevice)) + .thenReturn(List.of(mBroadcastReceiveState)); + when(mLocalManager + .getProfileManager() + .getLeAudioBroadcastProfile() + .isEnabled(mDevice)) + .thenReturn(false); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + + showScreen(mController); + + assertThat(mContainer.isVisible()).isTrue(); + assertThat(mContainer.getPreferenceCount()).isEqualTo(2); + } +}