1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settingslib.volume.data.repository
18 
19 import android.bluetooth.BluetoothAdapter
20 import android.bluetooth.BluetoothDevice
21 import android.bluetooth.BluetoothLeBroadcast
22 import android.bluetooth.BluetoothLeBroadcastAssistant
23 import android.bluetooth.BluetoothLeBroadcastReceiveState
24 import android.bluetooth.BluetoothProfile
25 import android.bluetooth.BluetoothVolumeControl
26 import android.content.ContentResolver
27 import android.content.Context
28 import android.database.ContentObserver
29 import android.platform.test.flag.junit.SetFlagsRule
30 import android.provider.Settings
31 import androidx.test.core.app.ApplicationProvider
32 import androidx.test.ext.junit.runners.AndroidJUnit4
33 import androidx.test.filters.SmallTest
34 import com.android.settingslib.bluetooth.BluetoothCallback
35 import com.android.settingslib.bluetooth.BluetoothEventManager
36 import com.android.settingslib.bluetooth.BluetoothUtils
37 import com.android.settingslib.bluetooth.CachedBluetoothDevice
38 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager
39 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
40 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
41 import com.android.settingslib.bluetooth.LocalBluetoothManager
42 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
43 import com.android.settingslib.bluetooth.VolumeControlProfile
44 import com.google.common.truth.Truth
45 import kotlinx.coroutines.ExperimentalCoroutinesApi
46 import kotlinx.coroutines.flow.launchIn
47 import kotlinx.coroutines.flow.onEach
48 import kotlinx.coroutines.test.TestScope
49 import kotlinx.coroutines.test.runCurrent
50 import kotlinx.coroutines.test.runTest
51 import org.junit.After
52 import org.junit.Before
53 import org.junit.Rule
54 import org.junit.Test
55 import org.junit.runner.RunWith
56 import org.mockito.ArgumentCaptor
57 import org.mockito.ArgumentMatchers.any
58 import org.mockito.ArgumentMatchers.eq
59 import org.mockito.Captor
60 import org.mockito.Mock
61 import org.mockito.Mockito.never
62 import org.mockito.Mockito.verify
63 import org.mockito.Mockito.`when`
64 import org.mockito.Spy
65 import org.mockito.junit.MockitoJUnit
66 import org.mockito.junit.MockitoRule
67 
68 @OptIn(ExperimentalCoroutinesApi::class)
69 @SmallTest
70 @RunWith(AndroidJUnit4::class)
71 class AudioSharingRepositoryTest {
72     @get:Rule val mockito: MockitoRule = MockitoJUnit.rule()
73 
74     @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule()
75 
76     @Mock private lateinit var btManager: LocalBluetoothManager
77 
78     @Mock private lateinit var profileManager: LocalBluetoothProfileManager
79 
80     @Mock private lateinit var broadcast: LocalBluetoothLeBroadcast
81 
82     @Mock private lateinit var assistant: LocalBluetoothLeBroadcastAssistant
83 
84     @Mock private lateinit var volumeControl: VolumeControlProfile
85 
86     @Mock private lateinit var eventManager: BluetoothEventManager
87 
88     @Mock private lateinit var deviceManager: CachedBluetoothDeviceManager
89 
90     @Mock private lateinit var device1: BluetoothDevice
91 
92     @Mock private lateinit var device2: BluetoothDevice
93 
94     @Mock private lateinit var cachedDevice1: CachedBluetoothDevice
95 
96     @Mock private lateinit var cachedDevice2: CachedBluetoothDevice
97 
98     @Mock private lateinit var receiveState: BluetoothLeBroadcastReceiveState
99 
100     @Captor
101     private lateinit var broadcastCallbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback>
102 
103     @Captor
104     private lateinit var assistantCallbackCaptor:
105             ArgumentCaptor<BluetoothLeBroadcastAssistant.Callback>
106 
107     @Captor private lateinit var btCallbackCaptor: ArgumentCaptor<BluetoothCallback>
108 
109     @Captor private lateinit var contentObserverCaptor: ArgumentCaptor<ContentObserver>
110 
111     @Captor
112     private lateinit var volumeCallbackCaptor: ArgumentCaptor<BluetoothVolumeControl.Callback>
113 
114     private val logger = FakeAudioSharingRepositoryLogger()
115     private val testScope = TestScope()
116     private val context: Context = ApplicationProvider.getApplicationContext()
117     @Spy private val contentResolver: ContentResolver = context.contentResolver
118     private lateinit var underTest: AudioSharingRepository
119 
120     @Before
121     fun setup() {
122         `when`(btManager.profileManager).thenReturn(profileManager)
123         `when`(profileManager.leAudioBroadcastProfile).thenReturn(broadcast)
124         `when`(profileManager.leAudioBroadcastAssistantProfile).thenReturn(assistant)
125         `when`(profileManager.volumeControlProfile).thenReturn(volumeControl)
126         `when`(btManager.eventManager).thenReturn(eventManager)
127         `when`(btManager.cachedDeviceManager).thenReturn(deviceManager)
128         `when`(broadcast.isEnabled(null)).thenReturn(true)
129         `when`(cachedDevice1.groupId).thenReturn(TEST_GROUP_ID1)
130         `when`(cachedDevice1.device).thenReturn(device1)
131         `when`(deviceManager.findDevice(device1)).thenReturn(cachedDevice1)
132         `when`(cachedDevice2.groupId).thenReturn(TEST_GROUP_ID2)
133         `when`(cachedDevice2.device).thenReturn(device2)
134         `when`(deviceManager.findDevice(device2)).thenReturn(cachedDevice2)
135         `when`(receiveState.bisSyncState).thenReturn(arrayListOf(TEST_RECEIVE_STATE_CONTENT))
136         `when`(assistant.getAllSources(any())).thenReturn(listOf(receiveState))
137         Settings.Secure.putInt(
138             contentResolver,
139             BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
140             TEST_GROUP_ID_INVALID
141         )
142         underTest =
143             AudioSharingRepositoryImpl(
144                 contentResolver,
145                 btManager,
146                 testScope.backgroundScope,
147                 testScope.testScheduler,
148                 logger
149             )
150     }
151 
152     @After
153     fun tearDown() {
154         logger.reset()
155     }
156 
157     @Test
158     fun audioSharingStateChange_profileReady_emitValues() {
159         testScope.runTest {
160             `when`(broadcast.isProfileReady).thenReturn(true)
161             `when`(assistant.isProfileReady).thenReturn(true)
162             `when`(volumeControl.isProfileReady).thenReturn(true)
163             val states = mutableListOf<Boolean?>()
164             underTest.inAudioSharing.onEach { states.add(it) }.launchIn(backgroundScope)
165             runCurrent()
166             triggerAudioSharingStateChange(TriggerType.BROADCAST_STOP, broadcastStopped)
167             runCurrent()
168             triggerAudioSharingStateChange(TriggerType.BROADCAST_START, broadcastStarted)
169             runCurrent()
170 
171             Truth.assertThat(states).containsExactly(false, true, false, true)
172             Truth.assertThat(logger.logs)
173                 .containsAtLeastElementsIn(
174                     listOf(
175                         "onAudioSharingStateChanged state=true",
176                         "onAudioSharingStateChanged state=false",
177                     )
178                 ).inOrder()
179         }
180     }
181 
182     @Test
183     fun audioSharingStateChange_profileNotReady_broadcastCallbackNotRegistered() {
184         testScope.runTest {
185             val states = mutableListOf<Boolean?>()
186             underTest.inAudioSharing.onEach { states.add(it) }.launchIn(backgroundScope)
187             runCurrent()
188             verify(broadcast, never()).registerServiceCallBack(any(), any())
189 
190             Truth.assertThat(states).containsExactly(false)
191         }
192     }
193 
194     @Test
195     fun primaryGroupIdChange_emitValues() {
196         testScope.runTest {
197             val groupIds = mutableListOf<Int?>()
198             underTest.primaryGroupId.onEach { groupIds.add(it) }.launchIn(backgroundScope)
199             runCurrent()
200             triggerContentObserverChange()
201             runCurrent()
202 
203             Truth.assertThat(groupIds)
204                 .containsExactly(
205                     TEST_GROUP_ID_INVALID,
206                     TEST_GROUP_ID2
207                 )
208         }
209     }
210 
211     @Test
212     fun secondaryGroupIdChange_profileNotReady_assistantCallbackNotRegistered() {
213         testScope.runTest {
214             val groupIds = mutableListOf<Int?>()
215             underTest.secondaryGroupId.onEach { groupIds.add(it) }.launchIn(backgroundScope)
216             runCurrent()
217             verify(assistant, never()).registerServiceCallBack(any(), any())
218         }
219     }
220 
221     @Test
222     fun secondaryGroupIdChange_profileReady_emitValues() {
223         testScope.runTest {
224             `when`(broadcast.isProfileReady).thenReturn(true)
225             `when`(assistant.isProfileReady).thenReturn(true)
226             `when`(volumeControl.isProfileReady).thenReturn(true)
227             val groupIds = mutableListOf<Int?>()
228             underTest.secondaryGroupId.onEach { groupIds.add(it) }.launchIn(backgroundScope)
229             runCurrent()
230             triggerSourceAdded()
231             runCurrent()
232             triggerContentObserverChange()
233             runCurrent()
234             triggerSourceRemoved()
235             runCurrent()
236             triggerSourceAdded()
237             runCurrent()
238             triggerProfileConnectionChange(
239                 BluetoothAdapter.STATE_CONNECTING, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
240             )
241             runCurrent()
242             triggerProfileConnectionChange(
243                 BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO
244             )
245             runCurrent()
246             triggerProfileConnectionChange(
247                 BluetoothAdapter.STATE_DISCONNECTED, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT
248             )
249             runCurrent()
250 
251             Truth.assertThat(groupIds)
252                 .containsExactly(
253                     TEST_GROUP_ID_INVALID,
254                     TEST_GROUP_ID2,
255                     TEST_GROUP_ID1,
256                     TEST_GROUP_ID_INVALID,
257                     TEST_GROUP_ID2,
258                     TEST_GROUP_ID_INVALID
259                 )
260             Truth.assertThat(logger.logs)
261                 .containsAtLeastElementsIn(
262                     listOf(
263                         "onSecondaryGroupIdChanged groupId=$TEST_GROUP_ID_INVALID",
264                         "onSecondaryGroupIdChanged groupId=$TEST_GROUP_ID2",
265                         "onSecondaryGroupIdChanged groupId=$TEST_GROUP_ID1",
266                     )
267                 ).inOrder()
268         }
269     }
270 
271     @Test
272     fun volumeMapChange_profileReady_emitValues() {
273         testScope.runTest {
274             `when`(broadcast.isProfileReady).thenReturn(true)
275             `when`(assistant.isProfileReady).thenReturn(true)
276             `when`(volumeControl.isProfileReady).thenReturn(true)
277             val volumeMaps = mutableListOf<GroupIdToVolumes?>()
278             underTest.volumeMap.onEach { volumeMaps.add(it) }.launchIn(backgroundScope)
279             runCurrent()
280             triggerVolumeMapChange(Pair(device1, TEST_VOLUME1))
281             runCurrent()
282             triggerVolumeMapChange(Pair(device1, TEST_VOLUME2))
283             runCurrent()
284             triggerAudioSharingStateChange(TriggerType.BROADCAST_STOP, broadcastStopped)
285             runCurrent()
286             verify(volumeControl).unregisterCallback(any())
287             runCurrent()
288 
289             val expectedMap1 = mapOf(TEST_GROUP_ID1 to TEST_VOLUME1)
290             val expectedMap2 = mapOf(TEST_GROUP_ID1 to TEST_VOLUME2)
291             Truth.assertThat(volumeMaps)
292                 .containsExactly(
293                     emptyMap<Int, Int>(),
294                     expectedMap1,
295                     expectedMap2
296                 )
297             Truth.assertThat(logger.logs)
298                 .containsAtLeastElementsIn(
299                     listOf(
300                         "onVolumeMapChanged map={}",
301                         "onVolumeMapChanged map=$expectedMap1",
302                         "onVolumeMapChanged map=$expectedMap2",
303                     )
304                 ).inOrder()
305         }
306     }
307 
308     @Test
309     fun volumeMapChange_profileNotReady_volumeControlCallbackNotRegistered() {
310         testScope.runTest {
311             val volumeMaps = mutableListOf<GroupIdToVolumes?>()
312             underTest.volumeMap.onEach { volumeMaps.add(it) }.launchIn(backgroundScope)
313             runCurrent()
314             verify(volumeControl, never()).registerCallback(any(), any())
315         }
316     }
317 
318     @Test
319     fun setSecondaryVolume_setValue() {
320         testScope.runTest {
321             Settings.Secure.putInt(
322                 contentResolver,
323                 BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
324                 TEST_GROUP_ID2
325             )
326             `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2))
327             underTest.setSecondaryVolume(TEST_VOLUME1)
328 
329             runCurrent()
330             verify(volumeControl).setDeviceVolume(device1, TEST_VOLUME1, true)
331             Truth.assertThat(logger.logs)
332                 .isEqualTo(
333                     listOf(
334                         "onSetVolumeRequested volume=$TEST_VOLUME1",
335                     )
336                 )
337         }
338     }
339 
340     private fun triggerAudioSharingStateChange(
341         type: TriggerType,
342         broadcastAction: BluetoothLeBroadcast.Callback.() -> Unit
343     ) {
344         verify(broadcast).registerServiceCallBack(any(), broadcastCallbackCaptor.capture())
345         when (type) {
346             TriggerType.BROADCAST_START -> {
347                 `when`(broadcast.isEnabled(null)).thenReturn(true)
348                 broadcastCallbackCaptor.value.broadcastAction()
349             }
350 
351             TriggerType.BROADCAST_STOP -> {
352                 `when`(broadcast.isEnabled(null)).thenReturn(false)
353                 broadcastCallbackCaptor.value.broadcastAction()
354             }
355         }
356     }
357 
358     private fun triggerSourceAdded() {
359         verify(assistant).registerServiceCallBack(any(), assistantCallbackCaptor.capture())
360         Settings.Secure.putInt(
361             contentResolver,
362             BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
363             TEST_GROUP_ID1
364         )
365         `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2))
366         assistantCallbackCaptor.value.sourceAdded(device1, receiveState)
367     }
368 
369     private fun triggerSourceRemoved() {
370         verify(assistant).registerServiceCallBack(any(), assistantCallbackCaptor.capture())
371         `when`(assistant.allConnectedDevices).thenReturn(listOf(device1))
372         Settings.Secure.putInt(
373             contentResolver,
374             BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
375             TEST_GROUP_ID1
376         )
377         assistantCallbackCaptor.value.sourceRemoved(device2)
378     }
379 
380     private fun triggerProfileConnectionChange(state: Int, profile: Int) {
381         verify(eventManager).registerCallback(btCallbackCaptor.capture())
382         `when`(assistant.allConnectedDevices).thenReturn(listOf(device1))
383         Settings.Secure.putInt(
384             contentResolver,
385             BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
386             TEST_GROUP_ID1
387         )
388         btCallbackCaptor.value.onProfileConnectionStateChanged(cachedDevice2, state, profile)
389     }
390 
391     private fun triggerContentObserverChange() {
392         verify(contentResolver)
393             .registerContentObserver(
394                 eq(Settings.Secure.getUriFor(BluetoothUtils.getPrimaryGroupIdUriForBroadcast())),
395                 eq(false),
396                 contentObserverCaptor.capture()
397             )
398         `when`(assistant.allConnectedDevices).thenReturn(listOf(device1, device2))
399         Settings.Secure.putInt(
400             contentResolver,
401             BluetoothUtils.getPrimaryGroupIdUriForBroadcast(),
402             TEST_GROUP_ID2
403         )
404         contentObserverCaptor.value.primaryChanged()
405     }
406 
407     private fun triggerVolumeMapChange(change: Pair<BluetoothDevice, Int>) {
408         verify(volumeControl).registerCallback(any(), volumeCallbackCaptor.capture())
409         volumeCallbackCaptor.value.onDeviceVolumeChanged(change.first, change.second)
410     }
411 
412     private enum class TriggerType {
413         BROADCAST_START,
414         BROADCAST_STOP
415     }
416 
417     private companion object {
418         const val TEST_GROUP_ID_INVALID = -1
419         const val TEST_GROUP_ID1 = 1
420         const val TEST_GROUP_ID2 = 2
421         const val TEST_SOURCE_ID = 1
422         const val TEST_BROADCAST_ID = 1
423         const val TEST_REASON = 1
424         const val TEST_RECEIVE_STATE_CONTENT = 1L
425         const val TEST_VOLUME1 = 10
426         const val TEST_VOLUME2 = 20
427 
428         val broadcastStarted: BluetoothLeBroadcast.Callback.() -> Unit = {
429             onBroadcastStarted(TEST_REASON, TEST_BROADCAST_ID)
430         }
431         val broadcastStopped: BluetoothLeBroadcast.Callback.() -> Unit = {
432             onBroadcastStopped(TEST_REASON, TEST_BROADCAST_ID)
433         }
434         val sourceAdded:
435                 BluetoothLeBroadcastAssistant.Callback.(
436                     sink: BluetoothDevice, state: BluetoothLeBroadcastReceiveState
437                 ) -> Unit =
438             { sink, state ->
439                 onReceiveStateChanged(sink, TEST_SOURCE_ID, state)
440             }
441         val sourceRemoved: BluetoothLeBroadcastAssistant.Callback.(sink: BluetoothDevice) -> Unit =
442             { sink ->
443                 onSourceRemoved(sink, TEST_SOURCE_ID, TEST_REASON)
444             }
445         val primaryChanged: ContentObserver.() -> Unit = { onChange(false) }
446     }
447 }
448