[Audiosharing] Handle fallback device for calls and alarms.
Test: atest
Bug: 305620450
Change-Id: I1c7a49b6aed7d47b7fdbcc6b475b8e99edc52443
diff --git a/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java b/src/com/android/settings/connecteddevice/audiosharing/AudioSharingUtils.java
index 242ce20..f489e9c 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.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 @@
boolean filterByInSharing) {
List<CachedBluetoothDevice> orderedDevices = new ArrayList<>();
for (List<CachedBluetoothDevice> 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;
@@ -167,6 +157,29 @@
}
/**
+ * 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<CachedBluetoothDevice> 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
* filtered by whether it is already in the audio sharing session.
@@ -268,7 +281,7 @@
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 @@
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 e47e141..9d346d3 100644
--- a/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragment.java
+++ b/src/com/android/settings/connecteddevice/audiosharing/CallsAndAlarmsDialogFragment.java
@@ -81,8 +81,12 @@
ArrayList<AudioSharingDeviceItem> 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 1a2d52b..2a538d5 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.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.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 @@
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<Integer, List<CachedBluetoothDevice>> mGroupedConnectedDevices = new HashMap<>();
private ArrayList<AudioSharingDeviceItem> 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 @@
}
@Override
- public void displayPreference(PreferenceScreen screen) {
+ public void displayPreference(@NonNull PreferenceScreen screen) {
super.displayPreference(screen);
mPreference.setOnPreferenceClickListener(
preference -> {
@@ -69,14 +139,31 @@
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<CachedBluetoothDevice> 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 @@
if (mLocalBtManager != null) {
mLocalBtManager.getEventManager().registerCallback(this);
}
+ if (mAssistant != null) {
+ mAssistant.registerServiceCallBack(mExecutor, mBroadcastAssistantCallback);
+ }
}
@Override
@@ -98,52 +188,58 @@
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 0000000..58a1272
--- /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<AudioSharingDeviceItem> 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 0000000..aa10517
--- /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<BluetoothDevice>());
+ 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<BluetoothDevice>());
+ 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<BluetoothDevice> 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);
+ }
+}