diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java index 242ce20efe9..f489e9c545f 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java +++ b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java @@ -22,9 +22,12 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothStatusCodes; import android.content.Context; +import android.provider.Settings; import android.util.Log; import android.widget.Toast; +import androidx.annotation.NonNull; + import com.android.settings.flags.Flags; import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; @@ -113,20 +116,7 @@ public class AudioSharingUtils { boolean filterByInSharing) { List orderedDevices = new ArrayList<>(); for (List devices : groupedConnectedDevices.values()) { - CachedBluetoothDevice leadDevice = null; - for (CachedBluetoothDevice device : devices) { - if (!device.getMemberDevice().isEmpty()) { - leadDevice = device; - break; - } - } - if (leadDevice == null && !devices.isEmpty()) { - leadDevice = devices.get(0); - Log.d( - TAG, - "Empty member device, pick arbitrary device as the lead: " - + leadDevice.getDevice().getAnonymizedAddress()); - } + @Nullable CachedBluetoothDevice leadDevice = getLeadDevice(devices); if (leadDevice == null) { Log.d(TAG, "Skip due to no lead device"); continue; @@ -166,6 +156,29 @@ public class AudioSharingUtils { return orderedDevices; } + /** + * Get the lead device from a list of devices with same group id. + * + * @param devices A list of devices with same group id. + * @return The lead device + */ + @Nullable + public static CachedBluetoothDevice getLeadDevice( + @NonNull List devices) { + if (devices.isEmpty()) return null; + for (CachedBluetoothDevice device : devices) { + if (!device.getMemberDevice().isEmpty()) { + return device; + } + } + CachedBluetoothDevice leadDevice = devices.get(0); + Log.d( + TAG, + "No lead device in the group, pick arbitrary device as the lead: " + + leadDevice.getDevice().getAnonymizedAddress()); + return leadDevice; + } + /** * Fetch a list of ordered connected lead {@link AudioSharingDeviceItem}s eligible for audio * sharing. The active device is placed in the first place if it exists. The devices can be @@ -268,7 +281,7 @@ public class AudioSharingUtils { var groupedDevices = fetchConnectedDevicesByGroupId(manager); var leadDevices = buildOrderedConnectedLeadDevices(manager, groupedDevices, false); - if (!leadDevices.isEmpty() && AudioSharingUtils.isActiveLeAudioDevice(leadDevices.get(0))) { + if (!leadDevices.isEmpty() && isActiveLeAudioDevice(leadDevices.get(0))) { return Optional.of(leadDevices.get(0)); } else { Log.w(TAG, "getActiveSinksOnAssistant(): No active lead device!"); @@ -379,4 +392,17 @@ public class AudioSharingUtils { Log.d(TAG, "getGroupId return invalid id for device: " + anonymizedAddress); return BluetoothCsipSetCoordinator.GROUP_ID_INVALID; } + + /** Get the fallback active group id from SettingsProvider. */ + public static int getFallbackActiveGroupId(@NonNull Context context) { + return Settings.Secure.getInt( + context.getContentResolver(), + "bluetooth_le_broadcast_fallback_active_group_id", + BluetoothCsipSetCoordinator.GROUP_ID_INVALID); + } + + /** Post the runnable to main thread. */ + public static void postOnMainThread(@NonNull Context context, @NonNull Runnable runnable) { + context.getMainExecutor().execute(runnable); + } } diff --git a/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragment.java b/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragment.java index e47e1419c48..9d346d359da 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragment.java +++ b/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragment.java @@ -81,8 +81,12 @@ public class CallsAndAlarmsDialogFragment extends InstrumentedDialogFragment { ArrayList deviceItems = arguments.getParcelableArrayList(BUNDLE_KEY_DEVICE_ITEMS); int checkedItem = -1; - // deviceItems is ordered. The active device is put in the first place if it does exist - if (!deviceItems.isEmpty() && deviceItems.get(0).isActive()) checkedItem = 0; + for (AudioSharingDeviceItem item : deviceItems) { + int fallbackActiveGroupId = AudioSharingUtils.getFallbackActiveGroupId(getContext()); + if (item.getGroupId() == fallbackActiveGroupId) { + checkedItem = deviceItems.indexOf(item); + } + } String[] choices = deviceItems.stream().map(AudioSharingDeviceItem::getName).toArray(String[]::new); AlertDialog.Builder builder = diff --git a/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceController.java b/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceController.java index 1a2d52bebb2..2a538d59f66 100644 --- a/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceController.java +++ b/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceController.java @@ -16,6 +16,12 @@ package com.android.settings.connecteddevice.audiosharing; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothCsipSetCoordinator; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastAssistant; +import android.bluetooth.BluetoothLeBroadcastMetadata; +import android.bluetooth.BluetoothLeBroadcastReceiveState; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.util.Log; @@ -29,6 +35,7 @@ import com.android.settings.bluetooth.Utils; import com.android.settings.dashboard.DashboardFragment; import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.utils.ThreadUtils; @@ -36,6 +43,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; /** PreferenceController to control the dialog to choose the active device for calls and alarms */ public class CallsAndAlarmsPreferenceController extends AudioSharingBasePreferenceController @@ -45,13 +54,74 @@ public class CallsAndAlarmsPreferenceController extends AudioSharingBasePreferen private static final String PREF_KEY = "calls_and_alarms"; private final LocalBluetoothManager mLocalBtManager; + private final Executor mExecutor; + @Nullable private LocalBluetoothLeBroadcastAssistant mAssistant = null; private DashboardFragment mFragment; Map> mGroupedConnectedDevices = new HashMap<>(); private ArrayList mDeviceItemsInSharingSession = new ArrayList<>(); + private BluetoothLeBroadcastAssistant.Callback mBroadcastAssistantCallback = + new BluetoothLeBroadcastAssistant.Callback() { + @Override + public void onSearchStarted(int reason) {} + + @Override + public void onSearchStartFailed(int reason) {} + + @Override + public void onSearchStopped(int reason) {} + + @Override + public void onSearchStopFailed(int reason) {} + + @Override + public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {} + + @Override + public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) { + Log.d(TAG, "onSourceAdded"); + updatePreference(); + } + + @Override + public void onSourceAddFailed( + @NonNull BluetoothDevice sink, + @NonNull BluetoothLeBroadcastMetadata source, + int reason) {} + + @Override + public void onSourceModified( + @NonNull BluetoothDevice sink, int sourceId, int reason) {} + + @Override + public void onSourceModifyFailed( + @NonNull BluetoothDevice sink, int sourceId, int reason) {} + + @Override + public void onSourceRemoved( + @NonNull BluetoothDevice sink, int sourceId, int reason) { + Log.d(TAG, "onSourceRemoved"); + updatePreference(); + } + + @Override + public void onSourceRemoveFailed( + @NonNull BluetoothDevice sink, int sourceId, int reason) {} + + @Override + public void onReceiveStateChanged( + BluetoothDevice sink, + int sourceId, + BluetoothLeBroadcastReceiveState state) {} + }; + public CallsAndAlarmsPreferenceController(Context context) { super(context, PREF_KEY); mLocalBtManager = Utils.getLocalBtManager(mContext); + if (mLocalBtManager != null) { + mAssistant = mLocalBtManager.getProfileManager().getLeAudioBroadcastAssistantProfile(); + } + mExecutor = Executors.newSingleThreadExecutor(); } @Override @@ -60,7 +130,7 @@ public class CallsAndAlarmsPreferenceController extends AudioSharingBasePreferen } @Override - public void displayPreference(PreferenceScreen screen) { + public void displayPreference(@NonNull PreferenceScreen screen) { super.displayPreference(screen); mPreference.setOnPreferenceClickListener( preference -> { @@ -69,14 +139,31 @@ public class CallsAndAlarmsPreferenceController extends AudioSharingBasePreferen return true; } updateDeviceItemsInSharingSession(); - if (mDeviceItemsInSharingSession.size() >= 2) { + if (mDeviceItemsInSharingSession.size() >= 1) { CallsAndAlarmsDialogFragment.show( mFragment, mDeviceItemsInSharingSession, (AudioSharingDeviceItem item) -> { - for (CachedBluetoothDevice device : - mGroupedConnectedDevices.get(item.getGroupId())) { - device.setActive(); + if (!mGroupedConnectedDevices.containsKey(item.getGroupId())) { + return; + } + List devices = + mGroupedConnectedDevices.get(item.getGroupId()); + @Nullable + CachedBluetoothDevice lead = + AudioSharingUtils.getLeadDevice(devices); + if (lead != null) { + Log.d( + TAG, + "Set fallback active device: " + + lead.getDevice().getAnonymizedAddress()); + lead.setActive(); + updatePreference(); + } else { + Log.w( + TAG, + "Fail to set fallback active device: no lead" + + " device"); } }); } @@ -90,6 +177,9 @@ public class CallsAndAlarmsPreferenceController extends AudioSharingBasePreferen if (mLocalBtManager != null) { mLocalBtManager.getEventManager().registerCallback(this); } + if (mAssistant != null) { + mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback); + } } @Override @@ -98,52 +188,58 @@ public class CallsAndAlarmsPreferenceController extends AudioSharingBasePreferen if (mLocalBtManager != null) { mLocalBtManager.getEventManager().unregisterCallback(this); } + if (mAssistant != null) { + mAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback); + } } @Override public void updateVisibility() { if (mPreference == null) return; - var unused = - ThreadUtils.postOnBackgroundThread( - () -> { - boolean isVisible = isBroadcasting() && isBluetoothStateOn(); - if (!isVisible) { - ThreadUtils.postOnMainThread(() -> mPreference.setVisible(false)); - } else { - updateDeviceItemsInSharingSession(); - // mDeviceItemsInSharingSession is ordered. The active device is the - // first - // place if exits. - if (!mDeviceItemsInSharingSession.isEmpty() - && mDeviceItemsInSharingSession.get(0).isActive()) { - ThreadUtils.postOnMainThread( - () -> { - mPreference.setVisible(true); - mPreference.setSummary( - mDeviceItemsInSharingSession - .get(0) - .getName()); - }); - } else { - ThreadUtils.postOnMainThread( - () -> { - mPreference.setVisible(true); - mPreference.setSummary( - "No active device in sharing"); - }); - } - } - }); + var unused = ThreadUtils.postOnBackgroundThread(() -> updatePreference()); + } + + private void updatePreference() { + boolean isVisible = isBroadcasting() && isBluetoothStateOn(); + if (!isVisible) { + AudioSharingUtils.postOnMainThread(mContext, () -> mPreference.setVisible(false)); + return; + } + updateDeviceItemsInSharingSession(); + int fallbackActiveGroupId = AudioSharingUtils.getFallbackActiveGroupId(mContext); + Log.d(TAG, "updatePreference: get fallback active group " + fallbackActiveGroupId); + if (fallbackActiveGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { + for (AudioSharingDeviceItem item : mDeviceItemsInSharingSession) { + if (item.getGroupId() == fallbackActiveGroupId) { + AudioSharingUtils.postOnMainThread( + mContext, + () -> { + mPreference.setSummary(item.getName()); + mPreference.setVisible(true); + }); + return; + } + } + } + AudioSharingUtils.postOnMainThread( + mContext, + () -> { + mPreference.setSummary("No active device in sharing"); + mPreference.setVisible(true); + }); } @Override - public void onActiveDeviceChanged( - @Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) { - if (bluetoothProfile != BluetoothProfile.LE_AUDIO) { - Log.d(TAG, "Ignore onActiveDeviceChanged, not LE_AUDIO profile"); - return; + public void onProfileConnectionStateChanged( + @NonNull CachedBluetoothDevice cachedDevice, + @ConnectionState int state, + int bluetoothProfile) { + if (state == BluetoothAdapter.STATE_DISCONNECTED + && bluetoothProfile == BluetoothProfile.LE_AUDIO) { + // The fallback active device could be updated if the previous fallback device is + // disconnected. + updatePreference(); } - mPreference.setSummary(activeDevice == null ? "" : activeDevice.getName()); } /** diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragmentTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragmentTest.java new file mode 100644 index 00000000000..58a12726044 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragmentTest.java @@ -0,0 +1,110 @@ +/* + * 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 com.google.common.truth.Truth.assertThat; + +import static org.robolectric.shadows.ShadowLooper.shadowMainLooper; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothStatusCodes; +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.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; + +import com.android.settings.flags.Flags; +import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; +import com.android.settings.testutils.shadow.ShadowBluetoothAdapter; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +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 org.robolectric.shadows.androidx.fragment.FragmentController; + +import java.util.ArrayList; + +@RunWith(RobolectricTestRunner.class) +@Config( + shadows = { + ShadowAlertDialogCompat.class, + ShadowBluetoothAdapter.class, + }) +public class CallsAndAlarmsDialogFragmentTest { + @Rule public final MockitoRule mocks = MockitoJUnit.rule(); + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + private static final String TEST_DEVICE_NAME1 = "test1"; + private static final String TEST_DEVICE_NAME2 = "test2"; + private static final AudioSharingDeviceItem TEST_DEVICE_ITEM1 = + new AudioSharingDeviceItem(TEST_DEVICE_NAME1, /* groupId= */ 1, /* isActive= */ true); + + private static final AudioSharingDeviceItem TEST_DEVICE_ITEM2 = + new AudioSharingDeviceItem(TEST_DEVICE_NAME2, /* groupId= */ 1, /* isActive= */ true); + + private Fragment mParent; + private CallsAndAlarmsDialogFragment mFragment; + private ShadowBluetoothAdapter mShadowBluetoothAdapter; + + @Before + public void setUp() { + ShadowAlertDialogCompat.reset(); + mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + mShadowBluetoothAdapter.setEnabled(true); + mShadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mShadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mFragment = new CallsAndAlarmsDialogFragment(); + mParent = new Fragment(); + FragmentController.setupFragment( + mParent, FragmentActivity.class, /* containerViewId= */ 0, /* bundle= */ null); + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + public void onCreateDialog_flagOff_dialogNotExist() { + mFragment.show(mParent, new ArrayList<>(), (item) -> {}); + shadowMainLooper().idle(); + AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + assertThat(dialog).isNull(); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_LE_AUDIO_SHARING) + public void onCreateDialog_showCorrectItems() { + ArrayList deviceItemList = new ArrayList<>(); + deviceItemList.add(TEST_DEVICE_ITEM1); + deviceItemList.add(TEST_DEVICE_ITEM2); + mFragment.show(mParent, deviceItemList, (item) -> {}); + shadowMainLooper().idle(); + AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); + assertThat(dialog.getListView().getCount()).isEqualTo(2); + } +} diff --git a/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceControllerTest.java new file mode 100644 index 00000000000..aa10517ffb7 --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsPreferenceControllerTest.java @@ -0,0 +1,245 @@ +/* + * 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 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 static org.robolectric.Shadows.shadowOf; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeBroadcastAssistant; +import android.bluetooth.BluetoothLeBroadcastReceiveState; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothStatusCodes; +import android.content.Context; +import android.os.Looper; +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 android.provider.Settings; + +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.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast; +import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +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; + +import java.util.ArrayList; + +@RunWith(RobolectricTestRunner.class) +@Config( + shadows = { + ShadowBluetoothAdapter.class, + ShadowBluetoothUtils.class, + }) +public class CallsAndAlarmsPreferenceControllerTest { + private static final String PREF_KEY = "calls_and_alarms"; + private static final String SUMMARY_EMPTY = "No active device in sharing"; + private static final String TEST_DEVICE_NAME1 = "test1"; + private static final String TEST_DEVICE_NAME2 = "test2"; + private static final String TEST_SETTINGS_KEY = + "bluetooth_le_broadcast_fallback_active_group_id"; + + @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 CachedBluetoothDeviceManager mCacheManager; + @Mock private LocalBluetoothLeBroadcast mBroadcast; + @Mock private LocalBluetoothLeBroadcastAssistant mAssistant; + @Mock private BluetoothDevice mDevice1; + @Mock private BluetoothDevice mDevice2; + @Mock private BluetoothDevice mDevice3; + @Mock private CachedBluetoothDevice mCachedDevice1; + @Mock private CachedBluetoothDevice mCachedDevice2; + @Mock private CachedBluetoothDevice mCachedDevice3; + @Mock private BluetoothLeBroadcastReceiveState mState; + private CallsAndAlarmsPreferenceController 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); + when(mLocalBtProfileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(mAssistant); + mController = new CallsAndAlarmsPreferenceController(mContext); + mPreference = new Preference(mContext); + when(mScreen.findPreference(PREF_KEY)).thenReturn(mPreference); + } + + @Test + public void onStart_registerCallback() { + mController.onStart(mLifecycleOwner); + verify(mBtEventManager).registerCallback(mController); + verify(mAssistant) + .registerServiceCallBack(any(), any(BluetoothLeBroadcastAssistant.Callback.class)); + } + + @Test + public void onStop_unregisterCallback() { + mController.onStop(mLifecycleOwner); + verify(mBtEventManager).unregisterCallback(mController); + verify(mAssistant) + .unregisterServiceCallBack(any(BluetoothLeBroadcastAssistant.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 updateVisibility_broadcastOffBluetoothOff() { + when(mBroadcast.isEnabled(any())).thenReturn(false); + mShadowBluetoothAdapter.setEnabled(false); + mController.displayPreference(mScreen); + mController.updateVisibility(); + assertThat(mPreference.isVisible()).isFalse(); + } + + @Test + public void updateVisibility_broadcastOnBluetoothOff() { + when(mBroadcast.isEnabled(any())).thenReturn(true); + mShadowBluetoothAdapter.setEnabled(false); + mController.displayPreference(mScreen); + mController.updateVisibility(); + assertThat(mPreference.isVisible()).isFalse(); + } + + @Test + public void updateVisibility_broadcastOffBluetoothOn() { + when(mBroadcast.isEnabled(any())).thenReturn(false); + mController.displayPreference(mScreen); + mController.updateVisibility(); + assertThat(mPreference.isVisible()).isFalse(); + } + + @Test + public void updateVisibility_broadcastOnBluetoothOn() { + when(mBroadcast.isEnabled(any())).thenReturn(true); + when(mAssistant.getConnectedDevices()).thenReturn(new ArrayList()); + mController.displayPreference(mScreen); + mController.updateVisibility(); + shadowOf(Looper.getMainLooper()).idle(); + assertThat(mPreference.isVisible()).isTrue(); + assertThat(mPreference.getSummary().toString()).isEqualTo(SUMMARY_EMPTY); + } + + @Test + public void onProfileConnectionStateChanged_updatePreference() { + when(mBroadcast.isEnabled(any())).thenReturn(true); + when(mAssistant.getConnectedDevices()).thenReturn(new ArrayList()); + mController.displayPreference(mScreen); + mController.onProfileConnectionStateChanged( + mCachedDevice1, BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO); + shadowOf(Looper.getMainLooper()).idle(); + assertThat(mPreference.isVisible()).isTrue(); + assertThat(mPreference.getSummary().toString()).isEqualTo(SUMMARY_EMPTY); + } + + @Test + public void updatePreference_showCorrectSummary() { + final int groupId1 = 1; + final int groupId2 = 2; + Settings.Secure.putInt(mContext.getContentResolver(), TEST_SETTINGS_KEY, groupId1); + when(mCachedDevice1.getGroupId()).thenReturn(groupId1); + when(mCachedDevice1.getDevice()).thenReturn(mDevice1); + when(mCachedDevice2.getGroupId()).thenReturn(groupId1); + when(mCachedDevice2.getDevice()).thenReturn(mDevice2); + when(mCachedDevice1.getMemberDevice()).thenReturn(ImmutableSet.of(mCachedDevice2)); + when(mCachedDevice1.getName()).thenReturn(TEST_DEVICE_NAME1); + when(mCachedDevice3.getGroupId()).thenReturn(groupId2); + when(mCachedDevice3.getDevice()).thenReturn(mDevice3); + when(mCachedDevice3.getName()).thenReturn(TEST_DEVICE_NAME2); + when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCacheManager); + when(mCacheManager.findDevice(mDevice1)).thenReturn(mCachedDevice1); + when(mCacheManager.findDevice(mDevice2)).thenReturn(mCachedDevice2); + when(mCacheManager.findDevice(mDevice3)).thenReturn(mCachedDevice3); + when(mBroadcast.isEnabled(any())).thenReturn(true); + ImmutableList deviceList = ImmutableList.of(mDevice1, mDevice2, mDevice3); + when(mAssistant.getConnectedDevices()).thenReturn(deviceList); + when(mAssistant.getAllSources(any())).thenReturn(ImmutableList.of(mState)); + mController.displayPreference(mScreen); + mController.updateVisibility(); + shadowOf(Looper.getMainLooper()).idle(); + assertThat(mPreference.isVisible()).isTrue(); + assertThat(mPreference.getSummary().toString()).isEqualTo(TEST_DEVICE_NAME1); + } +}