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