1 /* 2 * 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 package com.android.systemui.bluetooth.qsdialog 17 18 import android.bluetooth.BluetoothDevice 19 import android.testing.AndroidTestingRunner 20 import android.testing.TestableLooper 21 import androidx.test.filters.SmallTest 22 import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession 23 import com.android.dx.mockito.inline.extended.StaticMockitoSession 24 import com.android.settingslib.bluetooth.BluetoothUtils 25 import com.android.settingslib.bluetooth.CachedBluetoothDevice 26 import com.android.settingslib.bluetooth.LeAudioProfile 27 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant 28 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager 29 import com.android.systemui.SysuiTestCase 30 import com.android.systemui.kosmos.testDispatcher 31 import com.android.systemui.kosmos.testScope 32 import com.android.systemui.plugins.activityStarter 33 import com.android.systemui.statusbar.phone.SystemUIDialog 34 import com.android.systemui.testKosmos 35 import kotlinx.coroutines.ExperimentalCoroutinesApi 36 import kotlinx.coroutines.test.UnconfinedTestDispatcher 37 import kotlinx.coroutines.test.runTest 38 import org.junit.After 39 import org.junit.Before 40 import org.junit.Rule 41 import org.junit.Test 42 import org.junit.runner.RunWith 43 import org.mockito.ArgumentMatchers 44 import org.mockito.Mock 45 import org.mockito.Mockito 46 import org.mockito.Mockito.verify 47 import org.mockito.junit.MockitoJUnit 48 import org.mockito.junit.MockitoRule 49 import org.mockito.kotlin.whenever 50 51 @SmallTest 52 @RunWith(AndroidTestingRunner::class) 53 @TestableLooper.RunWithLooper(setAsMainLooper = true) 54 @OptIn(ExperimentalCoroutinesApi::class) 55 class DeviceItemActionInteractorTest : SysuiTestCase() { 56 @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() <lambda>null57 private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() } 58 private lateinit var actionInteractorImpl: DeviceItemActionInteractor 59 private lateinit var mockitoSession: StaticMockitoSession 60 private lateinit var activeMediaDeviceItem: DeviceItem 61 private lateinit var notConnectedDeviceItem: DeviceItem 62 private lateinit var connectedMediaDeviceItem: DeviceItem 63 private lateinit var connectedOtherDeviceItem: DeviceItem 64 @Mock private lateinit var dialog: SystemUIDialog 65 @Mock private lateinit var profileManager: LocalBluetoothProfileManager 66 @Mock private lateinit var leAudioProfile: LeAudioProfile 67 @Mock private lateinit var assistantProfile: LocalBluetoothLeBroadcastAssistant 68 @Mock private lateinit var bluetoothDevice: BluetoothDevice 69 @Mock private lateinit var bluetoothDeviceGroupId2: BluetoothDevice 70 @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice 71 72 @Before setUpnull73 fun setUp() { 74 mockitoSession = 75 mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking() 76 activeMediaDeviceItem = 77 DeviceItem( 78 type = DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE, 79 cachedBluetoothDevice = cachedBluetoothDevice, 80 deviceName = DEVICE_NAME, 81 connectionSummary = DEVICE_CONNECTION_SUMMARY, 82 iconWithDescription = null, 83 background = null 84 ) 85 notConnectedDeviceItem = 86 DeviceItem( 87 type = DeviceItemType.SAVED_BLUETOOTH_DEVICE, 88 cachedBluetoothDevice = cachedBluetoothDevice, 89 deviceName = DEVICE_NAME, 90 connectionSummary = DEVICE_CONNECTION_SUMMARY, 91 iconWithDescription = null, 92 background = null 93 ) 94 connectedMediaDeviceItem = 95 DeviceItem( 96 type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE, 97 cachedBluetoothDevice = cachedBluetoothDevice, 98 deviceName = DEVICE_NAME, 99 connectionSummary = DEVICE_CONNECTION_SUMMARY, 100 iconWithDescription = null, 101 background = null 102 ) 103 connectedOtherDeviceItem = 104 DeviceItem( 105 type = DeviceItemType.CONNECTED_BLUETOOTH_DEVICE, 106 cachedBluetoothDevice = cachedBluetoothDevice, 107 deviceName = DEVICE_NAME, 108 connectionSummary = DEVICE_CONNECTION_SUMMARY, 109 iconWithDescription = null, 110 background = null 111 ) 112 actionInteractorImpl = kosmos.deviceItemActionInteractor 113 } 114 115 @After tearDownnull116 fun tearDown() { 117 mockitoSession.finishMocking() 118 } 119 120 @Test testOnClick_connectedMedia_setActivenull121 fun testOnClick_connectedMedia_setActive() { 122 with(kosmos) { 123 testScope.runTest { 124 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) 125 whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) 126 actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) 127 verify(cachedBluetoothDevice).setActive() 128 verify(bluetoothTileDialogLogger) 129 .logDeviceClick( 130 cachedBluetoothDevice.address, 131 DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE 132 ) 133 } 134 } 135 } 136 137 @Test testOnClick_activeMedia_disconnectnull138 fun testOnClick_activeMedia_disconnect() { 139 with(kosmos) { 140 testScope.runTest { 141 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) 142 whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) 143 actionInteractorImpl.onClick(activeMediaDeviceItem, dialog) 144 verify(cachedBluetoothDevice).disconnect() 145 verify(bluetoothTileDialogLogger) 146 .logDeviceClick( 147 cachedBluetoothDevice.address, 148 DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE 149 ) 150 } 151 } 152 } 153 154 @Test testOnClick_connectedOtherDevice_disconnectnull155 fun testOnClick_connectedOtherDevice_disconnect() { 156 with(kosmos) { 157 testScope.runTest { 158 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) 159 whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) 160 actionInteractorImpl.onClick(connectedOtherDeviceItem, dialog) 161 verify(cachedBluetoothDevice).disconnect() 162 verify(bluetoothTileDialogLogger) 163 .logDeviceClick( 164 cachedBluetoothDevice.address, 165 DeviceItemType.CONNECTED_BLUETOOTH_DEVICE 166 ) 167 } 168 } 169 } 170 171 @Test testOnClick_saved_connectnull172 fun testOnClick_saved_connect() { 173 with(kosmos) { 174 testScope.runTest { 175 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) 176 whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) 177 actionInteractorImpl.onClick(notConnectedDeviceItem, dialog) 178 verify(cachedBluetoothDevice).connect() 179 verify(bluetoothTileDialogLogger) 180 .logDeviceClick( 181 cachedBluetoothDevice.address, 182 DeviceItemType.SAVED_BLUETOOTH_DEVICE 183 ) 184 } 185 } 186 } 187 188 @Test testOnClick_audioSharingDisabled_shouldNotLaunchSettingsnull189 fun testOnClick_audioSharingDisabled_shouldNotLaunchSettings() { 190 with(kosmos) { 191 testScope.runTest { 192 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) 193 whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false) 194 195 actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) 196 verify(activityStarter, Mockito.never()) 197 .postStartActivityDismissingKeyguard( 198 ArgumentMatchers.any(), 199 ArgumentMatchers.anyInt(), 200 ArgumentMatchers.any() 201 ) 202 } 203 } 204 } 205 206 @Test testOnClick_inAudioSharing_clickedDeviceHasSource_shouldNotLaunchSettingsnull207 fun testOnClick_inAudioSharing_clickedDeviceHasSource_shouldNotLaunchSettings() { 208 with(kosmos) { 209 testScope.runTest { 210 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) 211 whenever(cachedBluetoothDevice.connectableProfiles) 212 .thenReturn(listOf(leAudioProfile)) 213 214 whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) 215 whenever(localBluetoothManager.profileManager).thenReturn(profileManager) 216 whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) 217 whenever(profileManager.leAudioBroadcastAssistantProfile) 218 .thenReturn(assistantProfile) 219 220 whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true) 221 whenever( 222 BluetoothUtils.hasConnectedBroadcastSource( 223 ArgumentMatchers.any(), 224 ArgumentMatchers.any() 225 ) 226 ) 227 .thenReturn(true) 228 229 actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) 230 verify(activityStarter, Mockito.never()) 231 .postStartActivityDismissingKeyguard( 232 ArgumentMatchers.any(), 233 ArgumentMatchers.anyInt(), 234 ArgumentMatchers.any() 235 ) 236 } 237 } 238 } 239 240 @Test testOnClick_inAudioSharing_clickedDeviceNoSource_shouldLaunchSettingsnull241 fun testOnClick_inAudioSharing_clickedDeviceNoSource_shouldLaunchSettings() { 242 with(kosmos) { 243 testScope.runTest { 244 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) 245 whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice) 246 whenever(cachedBluetoothDevice.connectableProfiles) 247 .thenReturn(listOf(leAudioProfile)) 248 249 whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) 250 whenever(localBluetoothManager.profileManager).thenReturn(profileManager) 251 whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) 252 whenever(profileManager.leAudioBroadcastAssistantProfile) 253 .thenReturn(assistantProfile) 254 255 whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true) 256 whenever( 257 BluetoothUtils.hasConnectedBroadcastSource( 258 ArgumentMatchers.any(), 259 ArgumentMatchers.any() 260 ) 261 ) 262 .thenReturn(false) 263 264 actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) 265 verify(activityStarter) 266 .postStartActivityDismissingKeyguard( 267 ArgumentMatchers.any(), 268 ArgumentMatchers.anyInt(), 269 ArgumentMatchers.any() 270 ) 271 } 272 } 273 } 274 275 @Test testOnClick_noConnectedLeDevice_shouldNotLaunchSettingsnull276 fun testOnClick_noConnectedLeDevice_shouldNotLaunchSettings() { 277 with(kosmos) { 278 testScope.runTest { 279 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) 280 281 whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) 282 whenever(localBluetoothManager.profileManager).thenReturn(profileManager) 283 whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) 284 whenever(profileManager.leAudioBroadcastAssistantProfile) 285 .thenReturn(assistantProfile) 286 287 actionInteractorImpl.onClick(notConnectedDeviceItem, dialog) 288 verify(activityStarter, Mockito.never()) 289 .postStartActivityDismissingKeyguard( 290 ArgumentMatchers.any(), 291 ArgumentMatchers.anyInt(), 292 ArgumentMatchers.any() 293 ) 294 } 295 } 296 } 297 298 @Test testOnClick_hasOneConnectedLeDevice_clickedNonLe_shouldNotLaunchSettingsnull299 fun testOnClick_hasOneConnectedLeDevice_clickedNonLe_shouldNotLaunchSettings() { 300 with(kosmos) { 301 testScope.runTest { 302 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) 303 304 whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) 305 whenever(localBluetoothManager.profileManager).thenReturn(profileManager) 306 whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) 307 whenever(profileManager.leAudioBroadcastAssistantProfile) 308 .thenReturn(assistantProfile) 309 310 whenever( 311 assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any()) 312 ) 313 .thenReturn(listOf(bluetoothDevice)) 314 315 actionInteractorImpl.onClick(notConnectedDeviceItem, dialog) 316 verify(activityStarter, Mockito.never()) 317 .postStartActivityDismissingKeyguard( 318 ArgumentMatchers.any(), 319 ArgumentMatchers.anyInt(), 320 ArgumentMatchers.any() 321 ) 322 } 323 } 324 } 325 326 @Test testOnClick_hasOneConnectedLeDevice_clickedLe_shouldLaunchSettingsnull327 fun testOnClick_hasOneConnectedLeDevice_clickedLe_shouldLaunchSettings() { 328 with(kosmos) { 329 testScope.runTest { 330 whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice) 331 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) 332 whenever(cachedBluetoothDevice.profiles).thenReturn(listOf(leAudioProfile)) 333 whenever(leAudioProfile.isEnabled(ArgumentMatchers.any())).thenReturn(true) 334 335 whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) 336 whenever(localBluetoothManager.profileManager).thenReturn(profileManager) 337 whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) 338 whenever(profileManager.leAudioBroadcastAssistantProfile) 339 .thenReturn(assistantProfile) 340 341 whenever( 342 assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any()) 343 ) 344 .thenReturn(listOf(bluetoothDevice)) 345 346 actionInteractorImpl.onClick(notConnectedDeviceItem, dialog) 347 verify(activityStarter) 348 .postStartActivityDismissingKeyguard( 349 ArgumentMatchers.any(), 350 ArgumentMatchers.anyInt(), 351 ArgumentMatchers.any() 352 ) 353 } 354 } 355 } 356 357 @Test testOnClick_hasOneConnectedLeDevice_clickedConnectedLe_shouldNotLaunchSettingsnull358 fun testOnClick_hasOneConnectedLeDevice_clickedConnectedLe_shouldNotLaunchSettings() { 359 with(kosmos) { 360 testScope.runTest { 361 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) 362 363 whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) 364 whenever(localBluetoothManager.profileManager).thenReturn(profileManager) 365 whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) 366 whenever(profileManager.leAudioBroadcastAssistantProfile) 367 .thenReturn(assistantProfile) 368 369 whenever( 370 assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any()) 371 ) 372 .thenReturn(listOf(bluetoothDevice)) 373 374 actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) 375 verify(activityStarter, Mockito.never()) 376 .postStartActivityDismissingKeyguard( 377 ArgumentMatchers.any(), 378 ArgumentMatchers.anyInt(), 379 ArgumentMatchers.any() 380 ) 381 } 382 } 383 } 384 385 @Test testOnClick_hasTwoConnectedLeDevice_clickedNotConnectedLe_shouldNotLaunchSettingsnull386 fun testOnClick_hasTwoConnectedLeDevice_clickedNotConnectedLe_shouldNotLaunchSettings() { 387 with(kosmos) { 388 testScope.runTest { 389 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) 390 391 whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) 392 whenever(localBluetoothManager.profileManager).thenReturn(profileManager) 393 whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) 394 whenever(profileManager.leAudioBroadcastAssistantProfile) 395 .thenReturn(assistantProfile) 396 397 whenever( 398 assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any()) 399 ) 400 .thenReturn(listOf(bluetoothDevice, bluetoothDeviceGroupId2)) 401 whenever(leAudioProfile.getGroupId(ArgumentMatchers.any())).thenAnswer { 402 val device = it.arguments.first() as BluetoothDevice 403 if (device == bluetoothDevice) GROUP_ID_1 else GROUP_ID_2 404 } 405 406 actionInteractorImpl.onClick(notConnectedDeviceItem, dialog) 407 verify(activityStarter, Mockito.never()) 408 .postStartActivityDismissingKeyguard( 409 ArgumentMatchers.any(), 410 ArgumentMatchers.anyInt(), 411 ArgumentMatchers.any() 412 ) 413 } 414 } 415 } 416 417 @Test testOnClick_hasTwoConnectedLeDevice_clickedConnectedLe_shouldLaunchSettingsnull418 fun testOnClick_hasTwoConnectedLeDevice_clickedConnectedLe_shouldLaunchSettings() { 419 with(kosmos) { 420 testScope.runTest { 421 whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice) 422 whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS) 423 whenever(cachedBluetoothDevice.profiles).thenReturn(listOf(leAudioProfile)) 424 whenever(leAudioProfile.isEnabled(ArgumentMatchers.any())).thenReturn(true) 425 426 whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true) 427 whenever(localBluetoothManager.profileManager).thenReturn(profileManager) 428 whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile) 429 whenever(profileManager.leAudioBroadcastAssistantProfile) 430 .thenReturn(assistantProfile) 431 432 whenever( 433 assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any()) 434 ) 435 .thenReturn(listOf(bluetoothDevice, bluetoothDeviceGroupId2)) 436 whenever(leAudioProfile.getGroupId(ArgumentMatchers.any())).thenAnswer { 437 val device = it.arguments.first() as BluetoothDevice 438 if (device == bluetoothDevice) GROUP_ID_1 else GROUP_ID_2 439 } 440 441 actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog) 442 verify(activityStarter) 443 .postStartActivityDismissingKeyguard( 444 ArgumentMatchers.any(), 445 ArgumentMatchers.anyInt(), 446 ArgumentMatchers.any() 447 ) 448 } 449 } 450 } 451 452 private companion object { 453 const val DEVICE_NAME = "device" 454 const val DEVICE_CONNECTION_SUMMARY = "active" 455 const val DEVICE_ADDRESS = "address" 456 const val GROUP_ID_1 = 1 457 const val GROUP_ID_2 = 2 458 } 459 } 460