From d280be742e885d1505f4ee882823fa948b309b27 Mon Sep 17 00:00:00 2001 From: Caitlin Shkuratov Date: Fri, 28 Apr 2023 18:37:50 +0000 Subject: [PATCH] [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 --- .../policy/BluetoothControllerImpl.java | 30 +++ .../policy/bluetooth/BluetoothRepository.kt | 109 +++++++++ .../policy/dagger/StatusBarPolicyModule.java | 13 +- .../policy/BluetoothControllerImplTest.java | 122 +++++++++- .../bluetooth/BluetoothRepositoryImplTest.kt | 213 ++++++++++++++++++ .../bluetooth/FakeBluetoothRepository.kt | 48 ++++ 6 files changed, 526 insertions(+), 9 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/policy/bluetooth/BluetoothRepository.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/bluetooth/BluetoothRepositoryImplTest.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/bluetooth/FakeBluetoothRepository.kt diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java index 5208064d2c0bb..5cc3d52f44942 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java @@ -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 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 newList = status.getConnectedDevices(); + int state = status.getMaxConnectionState(); synchronized (mConnectedDevices) { mConnectedDevices.clear(); mConnectedDevices.addAll(newList); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/bluetooth/BluetoothRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/bluetooth/BluetoothRepository.kt new file mode 100644 index 0000000000000..80f3d76f08975 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/bluetooth/BluetoothRepository.kt @@ -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, + 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, + callback: ConnectionStatusFetchedCallback, + ) { + scope.launch { + val result = fetchConnectionStatus(currentDevices) + callback.onConnectionStatusFetched(result) + } + } + + private suspend fun fetchConnectionStatus( + currentDevices: Collection, + ): 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, +) + +/** Callback notified when the new status has been fetched. */ +fun interface ConnectionStatusFetchedCallback { + fun onConnectionStatusFetched(status: ConnectionStatusModel) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java index 1b7353923adaf..e1a7b6d59f413 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java @@ -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); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java index 2b1370544bfd0..7402b4d64b16b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BluetoothControllerImplTest.java @@ -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 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()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/bluetooth/BluetoothRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/bluetooth/BluetoothRepositoryImplTest.kt new file mode 100644 index 0000000000000..6f40f15ca00aa --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/bluetooth/BluetoothRepositoryImplTest.kt @@ -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().also { + whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED) + } + val device2 = + mock().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().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().also { + whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTING) + whenever(it.isConnected).thenReturn(false) + } + val device2 = + mock().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().also { + whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED) + whenever(it.isConnected).thenReturn(false) + } + val device2 = + mock().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().also { whenever(it.isConnected).thenReturn(false) } + + val status = fetchConnectionStatus(currentDevices = listOf(device)) + + assertThat(status.connectedDevices).isEmpty() + } + + @Test + fun fetchConnectionStatusInBackground_oneCurrentDeviceConnected_connectedDevicesHasDevice() { + val device = + mock().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().also { whenever(it.isConnected).thenReturn(true) } + val device2Disconnected = + mock().also { whenever(it.isConnected).thenReturn(false) } + val device3Connected = + mock().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 + ): ConnectionStatusModel { + var receivedStatus: ConnectionStatusModel? = null + underTest.fetchConnectionStatusInBackground(currentDevices) { status -> + receivedStatus = status + } + scheduler.runCurrent() + return receivedStatus!! + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/bluetooth/FakeBluetoothRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/bluetooth/FakeBluetoothRepository.kt new file mode 100644 index 0000000000000..d8c0f777d4cc5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/bluetooth/FakeBluetoothRepository.kt @@ -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, + callback: ConnectionStatusFetchedCallback + ) { + impl.fetchConnectionStatusInBackground(currentDevices, callback) + scheduler.runCurrent() + } +}