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