1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 
15 package com.android.systemui.statusbar.policy.bluetooth
16 
17 import android.bluetooth.BluetoothProfile
18 import androidx.test.filters.SmallTest
19 import com.android.settingslib.bluetooth.CachedBluetoothDevice
20 import com.android.settingslib.bluetooth.LocalBluetoothAdapter
21 import com.android.settingslib.bluetooth.LocalBluetoothManager
22 import com.android.systemui.SysuiTestCase
23 import com.android.systemui.util.mockito.mock
24 import com.android.systemui.util.mockito.whenever
25 import com.google.common.truth.Truth.assertThat
26 import kotlinx.coroutines.ExperimentalCoroutinesApi
27 import kotlinx.coroutines.test.StandardTestDispatcher
28 import kotlinx.coroutines.test.TestCoroutineScheduler
29 import kotlinx.coroutines.test.TestDispatcher
30 import kotlinx.coroutines.test.TestScope
31 import org.junit.Before
32 import org.junit.Test
33 import org.mockito.Mock
34 import org.mockito.MockitoAnnotations
35 
36 @OptIn(ExperimentalCoroutinesApi::class)
37 @SmallTest
38 class BluetoothRepositoryImplTest : SysuiTestCase() {
39 
40     private lateinit var underTest: BluetoothRepositoryImpl
41 
42     private lateinit var scheduler: TestCoroutineScheduler
43     private lateinit var dispatcher: TestDispatcher
44     private lateinit var testScope: TestScope
45 
46     @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
47     @Mock private lateinit var bluetoothAdapter: LocalBluetoothAdapter
48 
49     @Before
50     fun setUp() {
51         MockitoAnnotations.initMocks(this)
52         whenever(localBluetoothManager.bluetoothAdapter).thenReturn(bluetoothAdapter)
53 
54         scheduler = TestCoroutineScheduler()
55         dispatcher = StandardTestDispatcher(scheduler)
56         testScope = TestScope(dispatcher)
57 
58         underTest =
59             BluetoothRepositoryImpl(testScope.backgroundScope, dispatcher, localBluetoothManager)
60     }
61 
62     @Test
63     fun fetchConnectionStatusInBackground_currentDevicesEmpty_maxStateIsManagerState() {
64         whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
65 
66         val status = fetchConnectionStatus(currentDevices = emptyList())
67 
68         assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_CONNECTING)
69     }
70 
71     @Test
72     fun fetchConnectionStatusInBackground_currentDevicesEmpty_nullManager_maxStateIsDisconnected() {
73         // This CONNECTING state should be unused because localBluetoothManager is null
74         whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
75         underTest =
76             BluetoothRepositoryImpl(
77                 testScope.backgroundScope,
78                 dispatcher,
79                 localBluetoothManager = null,
80             )
81 
82         val status = fetchConnectionStatus(currentDevices = emptyList())
83 
84         assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_DISCONNECTED)
85     }
86 
87     @Test
88     fun fetchConnectionStatusInBackground_managerStateLargerThanDeviceStates_maxStateIsManager() {
89         whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
90         val device1 =
91             mock<CachedBluetoothDevice>().also {
92                 whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED)
93             }
94         val device2 =
95             mock<CachedBluetoothDevice>().also {
96                 whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED)
97             }
98 
99         val status = fetchConnectionStatus(currentDevices = listOf(device1, device2))
100 
101         assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_CONNECTING)
102     }
103 
104     @Test
105     fun fetchConnectionStatusInBackground_oneCurrentDevice_maxStateIsDeviceState() {
106         whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED)
107         val device =
108             mock<CachedBluetoothDevice>().also {
109                 whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
110             }
111 
112         val status = fetchConnectionStatus(currentDevices = listOf(device))
113 
114         assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_CONNECTING)
115     }
116 
117     @Test
118     fun fetchConnectionStatusInBackground_multipleDevices_maxStateIsHighestState() {
119         whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_DISCONNECTED)
120 
121         val device1 =
122             mock<CachedBluetoothDevice>().also {
123                 whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
124                 whenever(it.isConnected).thenReturn(false)
125             }
126         val device2 =
127             mock<CachedBluetoothDevice>().also {
128                 whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
129                 whenever(it.isConnected).thenReturn(true)
130             }
131 
132         val status = fetchConnectionStatus(currentDevices = listOf(device1, device2))
133 
134         assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_CONNECTED)
135     }
136 
137     @Test
138     fun fetchConnectionStatusInBackground_devicesNotConnected_maxStateIsDisconnected() {
139         whenever(bluetoothAdapter.connectionState).thenReturn(BluetoothProfile.STATE_CONNECTING)
140 
141         // WHEN the devices say their state is CONNECTED but [isConnected] is false
142         val device1 =
143             mock<CachedBluetoothDevice>().also {
144                 whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
145                 whenever(it.isConnected).thenReturn(false)
146             }
147         val device2 =
148             mock<CachedBluetoothDevice>().also {
149                 whenever(it.maxConnectionState).thenReturn(BluetoothProfile.STATE_CONNECTED)
150                 whenever(it.isConnected).thenReturn(false)
151             }
152 
153         val status = fetchConnectionStatus(currentDevices = listOf(device1, device2))
154 
155         // THEN the max state is DISCONNECTED
156         assertThat(status.maxConnectionState).isEqualTo(BluetoothProfile.STATE_DISCONNECTED)
157     }
158 
159     @Test
160     fun fetchConnectionStatusInBackground_currentDevicesEmpty_connectedDevicesEmpty() {
161         val status = fetchConnectionStatus(currentDevices = emptyList())
162 
163         assertThat(status.connectedDevices).isEmpty()
164     }
165 
166     @Test
167     fun fetchConnectionStatusInBackground_oneCurrentDeviceDisconnected_connectedDevicesEmpty() {
168         val device =
169             mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(false) }
170 
171         val status = fetchConnectionStatus(currentDevices = listOf(device))
172 
173         assertThat(status.connectedDevices).isEmpty()
174     }
175 
176     @Test
177     fun fetchConnectionStatusInBackground_oneCurrentDeviceConnected_connectedDevicesHasDevice() {
178         val device =
179             mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(true) }
180 
181         val status = fetchConnectionStatus(currentDevices = listOf(device))
182 
183         assertThat(status.connectedDevices).isEqualTo(listOf(device))
184     }
185 
186     @Test
187     fun fetchConnectionStatusInBackground_multipleDevices_connectedDevicesHasOnlyConnected() {
188         val device1Connected =
189             mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(true) }
190         val device2Disconnected =
191             mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(false) }
192         val device3Connected =
193             mock<CachedBluetoothDevice>().also { whenever(it.isConnected).thenReturn(true) }
194 
195         val status =
196             fetchConnectionStatus(
197                 currentDevices = listOf(device1Connected, device2Disconnected, device3Connected)
198             )
199 
200         assertThat(status.connectedDevices).isEqualTo(listOf(device1Connected, device3Connected))
201     }
202 
203     private fun fetchConnectionStatus(
204         currentDevices: Collection<CachedBluetoothDevice>
205     ): ConnectionStatusModel {
206         var receivedStatus: ConnectionStatusModel? = null
207         underTest.fetchConnectionStatusInBackground(currentDevices) { status ->
208             receivedStatus = status
209         }
210         scheduler.runCurrent()
211         return receivedStatus!!
212     }
213 }
214