[Bluetooth] Define bluetooth repo to make fetches in the background.

We've been talking about modernizing the bluetooth code for a bit, and
moving these calls to the background thread seems like the perfect time
to start `BluetoothRepository`.

Fixes: 271058380
Test: atest BluetoothControllerImplTest BluetoothRepositoryImplTest
Test: manual: connect and disconnect various bluetooth devices and
ensure QS tile and status bar are updated appropriately

Change-Id: Id58c52481cbc165ed28c42985edb2f28469dc5fd
This commit is contained in:
Caitlin Shkuratov
2023-04-28 18:37:50 +00:00
parent e6d80348a5
commit d280be742e
6 changed files with 526 additions and 9 deletions

View File

@@ -38,7 +38,11 @@ import com.android.systemui.bluetooth.BluetoothLogger;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepository;
import com.android.systemui.statusbar.policy.bluetooth.ConnectionStatusModel;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -50,14 +54,20 @@ import java.util.concurrent.Executor;
import javax.inject.Inject;
/**
* Controller for information about bluetooth connections.
*
* Note: Right now, this class and {@link BluetoothRepository} co-exist. Any new code should go in
* {@link BluetoothRepository}, but external clients should query this file for now.
*/
@SysUISingleton
public class BluetoothControllerImpl implements BluetoothController, BluetoothCallback,
CachedBluetoothDevice.Callback, LocalBluetoothProfileManager.ServiceListener {
private static final String TAG = "BluetoothController";
private final FeatureFlags mFeatureFlags;
private final DumpManager mDumpManager;
private final BluetoothLogger mLogger;
private final BluetoothRepository mBluetoothRepository;
private final LocalBluetoothManager mLocalBluetoothManager;
private final UserManager mUserManager;
private final int mCurrentUser;
@@ -79,14 +89,18 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa
@Inject
public BluetoothControllerImpl(
Context context,
FeatureFlags featureFlags,
UserTracker userTracker,
DumpManager dumpManager,
BluetoothLogger logger,
BluetoothRepository bluetoothRepository,
@Main Looper mainLooper,
@Nullable LocalBluetoothManager localBluetoothManager,
@Nullable BluetoothAdapter bluetoothAdapter) {
mFeatureFlags = featureFlags;
mDumpManager = dumpManager;
mLogger = logger;
mBluetoothRepository = bluetoothRepository;
mLocalBluetoothManager = localBluetoothManager;
mHandler = new H(mainLooper);
if (mLocalBluetoothManager != null) {
@@ -229,6 +243,16 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa
}
private void updateConnected() {
if (mFeatureFlags.isEnabled(Flags.NEW_BLUETOOTH_REPOSITORY)) {
mBluetoothRepository.fetchConnectionStatusInBackground(
getDevices(), this::onConnectionStatusFetched);
} else {
updateConnectedOld();
}
}
/** Used only if {@link Flags.NEW_BLUETOOTH_REPOSITORY} is *not* enabled. */
private void updateConnectedOld() {
// Make sure our connection state is up to date.
int state = mLocalBluetoothManager.getBluetoothAdapter().getConnectionState();
List<CachedBluetoothDevice> newList = new ArrayList<>();
@@ -249,6 +273,12 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa
// connected.
state = BluetoothAdapter.STATE_DISCONNECTED;
}
onConnectionStatusFetched(new ConnectionStatusModel(state, newList));
}
private void onConnectionStatusFetched(ConnectionStatusModel status) {
List<CachedBluetoothDevice> newList = status.getConnectedDevices();
int state = status.getMaxConnectionState();
synchronized (mConnectedDevices) {
mConnectedDevices.clear();
mConnectedDevices.addAll(newList);

View File

@@ -0,0 +1,109 @@
/*
* Copyright (C) 2023 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.systemui.statusbar.policy.bluetooth
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothProfile
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Repository for information about bluetooth connections.
*
* Note: Right now, this class and [BluetoothController] co-exist. Any new code should go in this
* implementation, but external clients should query [BluetoothController] instead of this class for
* now.
*/
interface BluetoothRepository {
/**
* Fetches the connection statuses for the given [currentDevices] and invokes [callback] once
* those statuses have been fetched. The fetching occurs on a background thread because IPCs may
* be required to fetch the statuses (see b/271058380).
*/
fun fetchConnectionStatusInBackground(
currentDevices: Collection<CachedBluetoothDevice>,
callback: ConnectionStatusFetchedCallback,
)
}
/** Implementation of [BluetoothRepository]. */
@SysUISingleton
class BluetoothRepositoryImpl
@Inject
constructor(
@Application private val scope: CoroutineScope,
@Background private val bgDispatcher: CoroutineDispatcher,
private val localBluetoothManager: LocalBluetoothManager?,
) : BluetoothRepository {
override fun fetchConnectionStatusInBackground(
currentDevices: Collection<CachedBluetoothDevice>,
callback: ConnectionStatusFetchedCallback,
) {
scope.launch {
val result = fetchConnectionStatus(currentDevices)
callback.onConnectionStatusFetched(result)
}
}
private suspend fun fetchConnectionStatus(
currentDevices: Collection<CachedBluetoothDevice>,
): ConnectionStatusModel {
return withContext(bgDispatcher) {
val minimumMaxConnectionState =
localBluetoothManager?.bluetoothAdapter?.connectionState
?: BluetoothProfile.STATE_DISCONNECTED
var maxConnectionState =
if (currentDevices.isEmpty()) {
minimumMaxConnectionState
} else {
currentDevices
.maxOf { it.maxConnectionState }
.coerceAtLeast(minimumMaxConnectionState)
}
val connectedDevices = currentDevices.filter { it.isConnected }
if (
connectedDevices.isEmpty() && maxConnectionState == BluetoothAdapter.STATE_CONNECTED
) {
// If somehow we think we are connected, but have no connected devices, we aren't
// connected.
maxConnectionState = BluetoothAdapter.STATE_DISCONNECTED
}
ConnectionStatusModel(maxConnectionState, connectedDevices)
}
}
}
data class ConnectionStatusModel(
/** The maximum connection state out of all current devices. */
val maxConnectionState: Int,
/** A list of devices that are currently connected. */
val connectedDevices: List<CachedBluetoothDevice>,
)
/** Callback notified when the new status has been fetched. */
fun interface ConnectionStatusFetchedCallback {
fun onConnectionStatusFetched(status: ConnectionStatusModel)
}

View File

@@ -62,15 +62,16 @@ import com.android.systemui.statusbar.policy.WalletController;
import com.android.systemui.statusbar.policy.WalletControllerImpl;
import com.android.systemui.statusbar.policy.ZenModeController;
import com.android.systemui.statusbar.policy.ZenModeControllerImpl;
import java.util.concurrent.Executor;
import javax.inject.Named;
import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepository;
import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepositoryImpl;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import java.util.concurrent.Executor;
import javax.inject.Named;
/** Dagger Module for code in the statusbar.policy package. */
@Module
@@ -82,6 +83,10 @@ public interface StatusBarPolicyModule {
@Binds
BluetoothController provideBluetoothController(BluetoothControllerImpl controllerImpl);
/** */
@Binds
BluetoothRepository provideBluetoothRepository(BluetoothRepositoryImpl impl);
/** */
@Binds
CastController provideCastController(CastControllerImpl controllerImpl);

View File

@@ -14,6 +14,8 @@
package com.android.systemui.statusbar.policy;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -44,7 +46,11 @@ import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.bluetooth.BluetoothLogger;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepository;
import com.android.systemui.statusbar.policy.bluetooth.FakeBluetoothRepository;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
@@ -69,6 +75,7 @@ public class BluetoothControllerImplTest extends SysuiTestCase {
private DumpManager mMockDumpManager;
private BluetoothControllerImpl mBluetoothControllerImpl;
private BluetoothAdapter mMockAdapter;
private final FakeFeatureFlags mFakeFeatureFlags = new FakeFeatureFlags();
private List<CachedBluetoothDevice> mDevices;
@@ -89,17 +96,26 @@ public class BluetoothControllerImplTest extends SysuiTestCase {
.thenReturn(mock(LocalBluetoothProfileManager.class));
mMockDumpManager = mock(DumpManager.class);
mBluetoothControllerImpl = new BluetoothControllerImpl(mContext,
BluetoothRepository bluetoothRepository =
new FakeBluetoothRepository(mMockBluetoothManager);
mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, true);
mBluetoothControllerImpl = new BluetoothControllerImpl(
mContext,
mFakeFeatureFlags,
mUserTracker,
mMockDumpManager,
mock(BluetoothLogger.class),
bluetoothRepository,
mTestableLooper.getLooper(),
mMockBluetoothManager,
mMockAdapter);
}
@Test
public void testNoConnectionWithDevices() {
public void testNoConnectionWithDevices_repoFlagOff() {
mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, false);
CachedBluetoothDevice device = mock(CachedBluetoothDevice.class);
when(device.isConnected()).thenReturn(true);
when(device.getMaxConnectionState()).thenReturn(BluetoothProfile.STATE_CONNECTED);
@@ -113,7 +129,27 @@ public class BluetoothControllerImplTest extends SysuiTestCase {
}
@Test
public void testOnServiceConnected_updatesConnectionState() {
public void testNoConnectionWithDevices_repoFlagOn() {
mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, true);
CachedBluetoothDevice device = mock(CachedBluetoothDevice.class);
when(device.isConnected()).thenReturn(true);
when(device.getMaxConnectionState()).thenReturn(BluetoothProfile.STATE_CONNECTED);
mDevices.add(device);
when(mMockLocalAdapter.getConnectionState())
.thenReturn(BluetoothAdapter.STATE_DISCONNECTED);
mBluetoothControllerImpl.onConnectionStateChanged(null,
BluetoothAdapter.STATE_DISCONNECTED);
assertTrue(mBluetoothControllerImpl.isBluetoothConnected());
}
@Test
public void testOnServiceConnected_updatesConnectionState_repoFlagOff() {
mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, false);
when(mMockLocalAdapter.getConnectionState()).thenReturn(BluetoothAdapter.STATE_CONNECTING);
mBluetoothControllerImpl.onServiceConnected();
@@ -122,6 +158,58 @@ public class BluetoothControllerImplTest extends SysuiTestCase {
assertFalse(mBluetoothControllerImpl.isBluetoothConnected());
}
@Test
public void testOnServiceConnected_updatesConnectionState_repoFlagOn() {
mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, true);
when(mMockLocalAdapter.getConnectionState()).thenReturn(BluetoothAdapter.STATE_CONNECTING);
mBluetoothControllerImpl.onServiceConnected();
assertTrue(mBluetoothControllerImpl.isBluetoothConnecting());
assertFalse(mBluetoothControllerImpl.isBluetoothConnected());
}
@Test
public void getConnectedDevices_onlyReturnsConnected_repoFlagOff() {
mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, false);
CachedBluetoothDevice device1Disconnected = mock(CachedBluetoothDevice.class);
when(device1Disconnected.isConnected()).thenReturn(false);
mDevices.add(device1Disconnected);
CachedBluetoothDevice device2Connected = mock(CachedBluetoothDevice.class);
when(device2Connected.isConnected()).thenReturn(true);
mDevices.add(device2Connected);
mBluetoothControllerImpl.onDeviceAdded(device1Disconnected);
mBluetoothControllerImpl.onDeviceAdded(device2Connected);
assertThat(mBluetoothControllerImpl.getConnectedDevices()).hasSize(1);
assertThat(mBluetoothControllerImpl.getConnectedDevices().get(0))
.isEqualTo(device2Connected);
}
@Test
public void getConnectedDevices_onlyReturnsConnected_repoFlagOn() {
mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, true);
CachedBluetoothDevice device1Disconnected = mock(CachedBluetoothDevice.class);
when(device1Disconnected.isConnected()).thenReturn(false);
mDevices.add(device1Disconnected);
CachedBluetoothDevice device2Connected = mock(CachedBluetoothDevice.class);
when(device2Connected.isConnected()).thenReturn(true);
mDevices.add(device2Connected);
mBluetoothControllerImpl.onDeviceAdded(device1Disconnected);
mBluetoothControllerImpl.onDeviceAdded(device2Connected);
assertThat(mBluetoothControllerImpl.getConnectedDevices()).hasSize(1);
assertThat(mBluetoothControllerImpl.getConnectedDevices().get(0))
.isEqualTo(device2Connected);
}
@Test
public void testOnBluetoothStateChange_updatesBluetoothState() {
mBluetoothControllerImpl.onBluetoothStateChanged(BluetoothAdapter.STATE_OFF);
@@ -147,8 +235,9 @@ public class BluetoothControllerImplTest extends SysuiTestCase {
}
@Test
public void testOnACLConnectionStateChange_updatesBluetoothStateOnConnection()
throws Exception {
public void testOnACLConnectionStateChange_updatesBluetoothStateOnConnection_repoFlagOff() {
mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, false);
BluetoothController.Callback callback = mock(BluetoothController.Callback.class);
mBluetoothControllerImpl.addCallback(callback);
@@ -167,6 +256,29 @@ public class BluetoothControllerImplTest extends SysuiTestCase {
verify(callback, atLeastOnce()).onBluetoothStateChange(anyBoolean());
}
@Test
public void testOnACLConnectionStateChange_updatesBluetoothStateOnConnection_repoFlagOn() {
mFakeFeatureFlags.set(Flags.NEW_BLUETOOTH_REPOSITORY, true);
BluetoothController.Callback callback = mock(BluetoothController.Callback.class);
mBluetoothControllerImpl.addCallback(callback);
assertFalse(mBluetoothControllerImpl.isBluetoothConnected());
CachedBluetoothDevice device = mock(CachedBluetoothDevice.class);
mDevices.add(device);
when(device.isConnected()).thenReturn(true);
when(device.getMaxConnectionState()).thenReturn(BluetoothProfile.STATE_CONNECTED);
reset(callback);
mBluetoothControllerImpl.onAclConnectionStateChanged(device,
BluetoothProfile.STATE_CONNECTED);
mTestableLooper.processAllMessages();
assertTrue(mBluetoothControllerImpl.isBluetoothConnected());
verify(callback, atLeastOnce()).onBluetoothStateChange(anyBoolean());
}
@Test
public void testOnActiveDeviceChanged_updatesAudioActive() {
assertFalse(mBluetoothControllerImpl.isBluetoothAudioActive());

View File

@@ -0,0 +1,213 @@
/*
* Copyright (C) 2023 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.systemui.statusbar.policy.bluetooth
import android.bluetooth.BluetoothProfile
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothAdapter
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.TestScope
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class BluetoothRepositoryImplTest : SysuiTestCase() {
private lateinit var underTest: BluetoothRepositoryImpl
private lateinit var scheduler: TestCoroutineScheduler
private lateinit var dispatcher: TestDispatcher
private lateinit var testScope: TestScope
@Mock private lateinit var localBluetoothManager: LocalBluetoothManager
@Mock private lateinit var bluetoothAdapter: LocalBluetoothAdapter
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(localBluetoothManager.bluetoothAdapter).thenReturn(bluetoothAdapter)
scheduler = TestCoroutineScheduler()
dispatcher = StandardTestDispatcher(scheduler)
testScope = TestScope(dispatcher)
underTest =
BluetoothRepositoryImpl(testScope.backgroundScope, dispatcher, localBluetoothManager)
}
@Test
fun fetchConnectionStatusInBackground_currentDevicesEmpty_maxStateIsManagerState() {
whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
val status = fetchConnectionStatus(currentDevices = emptyList())
assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_CONNECTING)
}
@Test
fun fetchConnectionStatusInBackground_currentDevicesEmpty_nullManager_maxStateIsDisconnected() {
// This CONNECTING state should be unused because localBluetoothManager is null
whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
underTest =
BluetoothRepositoryImpl(
testScope.backgroundScope,
dispatcher,
localBluetoothManager = null,
)
val status = fetchConnectionStatus(currentDevices = emptyList())
assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_DISCONNECTED)
}
@Test
fun fetchConnectionStatusInBackground_managerStateLargerThanDeviceStates_maxStateIsManager() {
whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
val device1 =
mock<CachedBluetoothDevice>().also {
whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED)
}
val device2 =
mock<CachedBluetoothDevice>().also {
whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED)
}
val status = fetchConnectionStatus(currentDevices = listOf(device1, device2))
assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_CONNECTING)
}
@Test
fun fetchConnectionStatusInBackground_oneCurrentDevice_maxStateIsDeviceState() {
whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED)
val device =
mock<CachedBluetoothDevice>().also {
whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
}
val status = fetchConnectionStatus(currentDevices = listOf(device))
assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_CONNECTING)
}
@Test
fun fetchConnectionStatusInBackground_multipleDevices_maxStateIsHighestState() {
whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED)
val device1 =
mock<CachedBluetoothDevice>().also {
whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
whenever(it.isConnected).thenReturn(false)
}
val device2 =
mock<CachedBluetoothDevice>().also {
whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
whenever(it.isConnected).thenReturn(true)
}
val status = fetchConnectionStatus(currentDevices = listOf(device1, device2))
assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_CONNECTED)
}
@Test
fun fetchConnectionStatusInBackground_devicesNotConnected_maxStateIsDisconnected() {
whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
// WHEN the devices say their state is CONNECTED but [isConnected] is false
val device1 =
mock<CachedBluetoothDevice>().also {
whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
whenever(it.isConnected).thenReturn(false)
}
val device2 =
mock<CachedBluetoothDevice>().also {
whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
whenever(it.isConnected).thenReturn(false)
}
val status = fetchConnectionStatus(currentDevices = listOf(device1, device2))
// THEN the max state is DISCONNECTED
assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_DISCONNECTED)
}
@Test
fun fetchConnectionStatusInBackground_currentDevicesEmpty_connectedDevicesEmpty() {
val status = fetchConnectionStatus(currentDevices = emptyList())
assertThat(status.connectedDevices).isEmpty()
}
@Test
fun fetchConnectionStatusInBackground_oneCurrentDeviceDisconnected_connectedDevicesEmpty() {
val device =
mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(false) }
val status = fetchConnectionStatus(currentDevices = listOf(device))
assertThat(status.connectedDevices).isEmpty()
}
@Test
fun fetchConnectionStatusInBackground_oneCurrentDeviceConnected_connectedDevicesHasDevice() {
val device =
mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(true) }
val status = fetchConnectionStatus(currentDevices = listOf(device))
assertThat(status.connectedDevices).isEqualTo(listOf(device))
}
@Test
fun fetchConnectionStatusInBackground_multipleDevices_connectedDevicesHasOnlyConnected() {
val device1Connected =
mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(true) }
val device2Disconnected =
mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(false) }
val device3Connected =
mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(true) }
val status =
fetchConnectionStatus(
currentDevices = listOf(device1Connected, device2Disconnected, device3Connected)
)
assertThat(status.connectedDevices).isEqualTo(listOf(device1Connected, device3Connected))
}
private fun fetchConnectionStatus(
currentDevices: Collection<CachedBluetoothDevice>
): ConnectionStatusModel {
var receivedStatus: ConnectionStatusModel? = null
underTest.fetchConnectionStatusInBackground(currentDevices) { status ->
receivedStatus = status
}
scheduler.runCurrent()
return receivedStatus!!
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2023 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.systemui.statusbar.policy.bluetooth
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope
/**
* Fake [BluetoothRepository] that delegates to the real [BluetoothRepositoryImpl].
*
* We only need this because [BluetoothRepository] is called from Java, which can't use [TestScope],
* [StandardTestDispatcher], etc. to create a test version of the repo. This class uses those test
* items under-the-hood so Java classes can indirectly access them.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class FakeBluetoothRepository(localBluetoothManager: LocalBluetoothManager) : BluetoothRepository {
private val scheduler = TestCoroutineScheduler()
private val dispatcher = StandardTestDispatcher(scheduler)
private val testScope = TestScope(dispatcher)
private val impl =
BluetoothRepositoryImpl(testScope.backgroundScope, dispatcher, localBluetoothManager)
override fun fetchConnectionStatusInBackground(
currentDevices: Collection<CachedBluetoothDevice>,
callback: ConnectionStatusFetchedCallback
) {
impl.fetchConnectionStatusInBackground(currentDevices, callback)
scheduler.runCurrent()
}
}