diff --git a/res/xml/bluetooth_audio_sharing.xml b/res/xml/bluetooth_audio_sharing.xml index 5de8dfcf0d7..45781c00eab 100644 --- a/res/xml/bluetooth_audio_sharing.xml +++ b/res/xml/bluetooth_audio_sharing.xml @@ -19,6 +19,11 @@ xmlns:settings="http://schemas.android.com/apk/res-auto" android:title="@string/audio_sharing_title"> + + + settings:allowDividerBelow="true" + settings:controller="com.android.settings.slices.SlicePreferenceController" /> + android:title="@string/audio_sharing_title" + settings:searchable="false" /> + settings:controller="com.android.settings.connecteddevice.AvailableMediaDeviceGroupController" /> + settings:controller="com.android.settings.connecteddevice.ConnectedDeviceGroupController" /> + settings:userRestriction="no_config_bluetooth" /> + android:title="@string/previous_connected_see_all" + settings:searchable="false" /> + "com.android.settings.connecteddevice.fastpair.FastPairDeviceDashboardFragment" + android:icon="@drawable/ic_chevron_right_24dp" + android:key="fast_pair_devices_see_all" + android:order="10" + android:title="@string/connected_device_fast_pair_device_see_all" + settings:searchable="false" /> + settings:controller="com.android.settings.connecteddevice.AdvancedConnectedDeviceController" /> diff --git a/res/xml/connected_devices_advanced.xml b/res/xml/connected_devices_advanced.xml index 364dd3dda14..b08879178c1 100644 --- a/res/xml/connected_devices_advanced.xml +++ b/res/xml/connected_devices_advanced.xml @@ -21,57 +21,59 @@ + android:key="bluetooth_switchbar_screen" + android:order="-10" + android:title="@string/bluetooth_settings_title" /> + android:key="audio_sharing_settings" + android:order="-9" + android:title="@string/audio_sharing_title" + settings:controller="com.android.settings.connecteddevice.audiosharing.AudioSharingPreferenceController" + settings:searchable="true" /> + settings:searchable="false" + settings:useAdminDisabledSummary="true" + settings:userRestriction="no_near_field_communication_radio" /> + settings:keywords="@string/keywords_wifi_display_settings" /> + android:icon="@*android:drawable/ic_settings_print" + android:key="connected_device_printing" + android:order="-3" + android:summary="@string/summary_placeholder" + android:title="@string/print_settings" /> + settings:useAdminDisabledSummary="true" + settings:userRestriction="no_ultra_wideband_radio" /> + android:order="-8" /> diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceController.java index 294e8b2094d..16c988883dd 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceController.java @@ -16,22 +16,123 @@ package com.android.settings.connecteddevice.audiosharing; +import android.bluetooth.BluetoothLeBroadcast; +import android.bluetooth.BluetoothLeBroadcastMetadata; import android.content.Context; -import com.android.settings.core.BasePreferenceController; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; -public class AudioSharingPreferenceController extends BasePreferenceController { +import com.android.settings.bluetooth.Utils; +import com.android.settings.core.BasePreferenceController; +import com.android.settingslib.bluetooth.BluetoothCallback; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; +import com.android.settingslib.bluetooth.LocalBluetoothManager; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class AudioSharingPreferenceController extends BasePreferenceController + implements DefaultLifecycleObserver, BluetoothCallback { private static final String TAG = "AudioSharingPreferenceController"; - private Context mContext; + private final LocalBluetoothManager mLocalBtManager; + private final Executor mExecutor; + @Nullable private LocalBluetoothLeBroadcast mBroadcast = null; + @Nullable private Preference mPreference; + + private final BluetoothLeBroadcast.Callback mBroadcastCallback = + new BluetoothLeBroadcast.Callback() { + @Override + public void onBroadcastStarted(int reason, int broadcastId) { + if (mPreference != null) { + refreshSummary(mPreference); + } + } + + @Override + public void onBroadcastStartFailed(int reason) {} + + @Override + public void onBroadcastMetadataChanged( + int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata) {} + + @Override + public void onBroadcastStopped(int reason, int broadcastId) { + if (mPreference != null) { + refreshSummary(mPreference); + } + } + + @Override + public void onBroadcastStopFailed(int reason) {} + + @Override + public void onBroadcastUpdated(int reason, int broadcastId) {} + + @Override + public void onBroadcastUpdateFailed(int reason, int broadcastId) {} + + @Override + public void onPlaybackStarted(int reason, int broadcastId) {} + + @Override + public void onPlaybackStopped(int reason, int broadcastId) {} + }; public AudioSharingPreferenceController(Context context, String preferenceKey) { super(context, preferenceKey); - mContext = context; + mLocalBtManager = Utils.getLocalBtManager(context); + if (mLocalBtManager != null) { + mBroadcast = mLocalBtManager.getProfileManager().getLeAudioBroadcastProfile(); + } + mExecutor = Executors.newSingleThreadExecutor(); + } + + @Override + public void onStart(@NonNull LifecycleOwner owner) { + if (mLocalBtManager != null) { + mLocalBtManager.getEventManager().registerCallback(this); + } + if (mBroadcast != null) { + mBroadcast.registerServiceCallBack(mExecutor, mBroadcastCallback); + } + } + + @Override + public void onStop(@NonNull LifecycleOwner owner) { + if (mLocalBtManager != null) { + mLocalBtManager.getEventManager().unregisterCallback(this); + } + if (mBroadcast != null) { + mBroadcast.unregisterServiceCallBack(mBroadcastCallback); + } + } + + @Override + public void displayPreference(@NonNull PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); } @Override public int getAvailabilityStatus() { return AudioSharingUtils.isFeatureEnabled() ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } + + @Override + public CharSequence getSummary() { + return AudioSharingUtils.isBroadcasting(mLocalBtManager) ? "On" : "Off"; + } + + @Override + public void onBluetoothStateChanged(@AdapterState int bluetoothState) { + if (mPreference != null) { + refreshSummary(mPreference); + } + } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceControllerTest.java new file mode 100644 index 00000000000..145c08cffd5 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/AudioSharingPreferenceControllerTest.java @@ -0,0 +1,160 @@ +/* + * 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.connecteddevice.audiosharing; + +import static android.bluetooth.BluetoothAdapter.STATE_OFF; +import static android.bluetooth.BluetoothAdapter.STATE_ON; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothLeBroadcast; +import android.bluetooth.BluetoothStatusCodes; +import android.content.Context; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; + +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.bluetooth.Utils; +import com.android.settings.flags.Flags; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; +import com.android.settings.testutils.shadow.ShadowBluetoothUtils; +import com.android.settingslib.bluetooth.BluetoothEventManager; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +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; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothAdapter.class, ShadowBluetoothUtils.class}) +public class AudioSharingPreferenceControllerTest { + private static final String PREF_KEY = "audio_sharing_settings"; + private static final String SUMMARY_ON = "On"; + private static final String SUMMARY_OFF = "Off"; + + @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Spy Context mContext = ApplicationProvider.getApplicationContext(); + @Mock private PreferenceScreen mScreen; + @Mock private LocalBluetoothManager mLocalBtManager; + @Mock private BluetoothEventManager mBtEventManager; + @Mock private LocalBluetoothProfileManager mLocalBtProfileManager; + @Mock private LocalBluetoothLeBroadcast mBroadcast; + private AudioSharingPreferenceController mController; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; + private LocalBluetoothManager mLocalBluetoothManager; + private Lifecycle mLifecycle; + private LifecycleOwner mLifecycleOwner; + private Preference mPreference; + + @Before + public void setUp() { + mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + mShadowBluetoothAdapter.setEnabled(true); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + ShadowBluetoothUtils.sLocalBluetoothManager = mLocalBtManager; + mLocalBluetoothManager = Utils.getLocalBtManager(mContext); + when(mLocalBluetoothManager.getEventManager()).thenReturn(mBtEventManager); + when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalBtProfileManager); + when(mLocalBtProfileManager.getLeAudioBroadcastProfile()).thenReturn(mBroadcast); + mController = new AudioSharingPreferenceController(mContext, PREF_KEY); + mPreference = new Preference(mContext); + when(mScreen.findPreference(PREF_KEY)).thenReturn(mPreference); + } + + @Test + public void onStart_registerCallback() { + mController.onStart(mLifecycleOwner); + verify(mBtEventManager).registerCallback(mController); + verify(mBroadcast).registerServiceCallBack(any(), any(BluetoothLeBroadcast.Callback.class)); + } + + @Test + public void onStop_unregisterCallback() { + mController.onStop(mLifecycleOwner); + verify(mBtEventManager).unregisterCallback(mController); + verify(mBroadcast).unregisterServiceCallBack(any(BluetoothLeBroadcast.Callback.class)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + public void getAvailabilityStatus_flagOn() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + public void getAvailabilityStatus_flagOff() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); + } + + @Test + public void getSummary_broadcastOn() { + when(mBroadcast.isEnabled(any())).thenReturn(true); + assertThat(mController.getSummary().toString()).isEqualTo(SUMMARY_ON); + } + + @Test + public void getSummary_broadcastOff() { + when(mBroadcast.isEnabled(any())).thenReturn(false); + assertThat(mController.getSummary().toString()).isEqualTo(SUMMARY_OFF); + } + + @Test + public void onBluetoothStateChanged_refreshSummary() { + mController.displayPreference(mScreen); + when(mBroadcast.isEnabled(any())).thenReturn(true); + mController.onBluetoothStateChanged(STATE_ON); + assertThat(mPreference.getSummary().toString()).isEqualTo(SUMMARY_ON); + when(mBroadcast.isEnabled(any())).thenReturn(false); + mController.onBluetoothStateChanged(STATE_OFF); + assertThat(mPreference.getSummary().toString()).isEqualTo(SUMMARY_OFF); + } +}