xref: /aosp_15_r20/frameworks/base/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt (revision d57664e9bc4670b3ecf6748a746a57c557b6bc9e)
1 package com.android.systemui.qs.tiles
2 
3 import android.bluetooth.BluetoothDevice
4 import android.os.Handler
5 import android.os.Looper
6 import android.os.UserManager
7 import android.platform.test.flag.junit.FlagsParameterization
8 import android.platform.test.flag.junit.FlagsParameterization.allCombinationsOf
9 import android.testing.TestableLooper
10 import android.testing.TestableLooper.RunWithLooper
11 import androidx.test.filters.SmallTest
12 import com.android.internal.logging.MetricsLogger
13 import com.android.internal.telephony.flags.Flags
14 import com.android.settingslib.Utils
15 import com.android.settingslib.bluetooth.CachedBluetoothDevice
16 import com.android.systemui.SysuiTestCase
17 import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogViewModel
18 import com.android.systemui.classifier.FalsingManagerFake
19 import com.android.systemui.flags.FeatureFlagsClassic
20 import com.android.systemui.plugins.ActivityStarter
21 import com.android.systemui.plugins.FalsingManager
22 import com.android.systemui.plugins.qs.QSTile
23 import com.android.systemui.plugins.statusbar.StatusBarStateController
24 import com.android.systemui.qs.QSHost
25 import com.android.systemui.qs.QsEventLogger
26 import com.android.systemui.qs.flags.QSComposeFragment
27 import com.android.systemui.qs.flags.QsInCompose.isEnabled
28 import com.android.systemui.qs.logging.QSLogger
29 import com.android.systemui.qs.tileimpl.QSTileImpl
30 import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIconWithRes
31 import com.android.systemui.res.R
32 import com.android.systemui.statusbar.policy.BluetoothController
33 import com.android.systemui.util.mockito.any
34 import com.android.systemui.util.mockito.eq
35 import com.android.systemui.util.mockito.mock
36 import com.android.systemui.util.mockito.whenever
37 import com.google.common.truth.Truth.assertThat
38 import kotlinx.coroutines.Job
39 import org.junit.After
40 import org.junit.Before
41 import org.junit.Test
42 import org.junit.runner.RunWith
43 import org.mockito.Mock
44 import org.mockito.Mockito.times
45 import org.mockito.Mockito.verify
46 import org.mockito.Mockito.`when`
47 import org.mockito.MockitoAnnotations
48 import platform.test.runner.parameterized.ParameterizedAndroidJunit4
49 import platform.test.runner.parameterized.Parameters
50 
51 @RunWith(ParameterizedAndroidJunit4::class)
52 @RunWithLooper(setAsMainLooper = true)
53 @SmallTest
54 class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() {
55 
56     init {
57         mSetFlagsRule.setFlagsParameterization(flags)
58     }
59 
60     @Mock private lateinit var qsLogger: QSLogger
61     @Mock private lateinit var qsHost: QSHost
62     @Mock private lateinit var metricsLogger: MetricsLogger
63     private val falsingManager = FalsingManagerFake()
64     @Mock private lateinit var statusBarStateController: StatusBarStateController
65     @Mock private lateinit var activityStarter: ActivityStarter
66     @Mock private lateinit var bluetoothController: BluetoothController
67     @Mock private lateinit var uiEventLogger: QsEventLogger
68     @Mock private lateinit var featureFlags: FeatureFlagsClassic
69     @Mock private lateinit var bluetoothTileDialogViewModel: BluetoothTileDialogViewModel
70     @Mock private lateinit var clickJob: Job
71     private lateinit var testableLooper: TestableLooper
72     private lateinit var tile: FakeBluetoothTile
73 
74     @Before
setUpnull75     fun setUp() {
76         MockitoAnnotations.initMocks(this)
77         testableLooper = TestableLooper.get(this)
78 
79         whenever(qsHost.context).thenReturn(mContext)
80 
81         tile =
82             FakeBluetoothTile(
83                 qsHost,
84                 uiEventLogger,
85                 testableLooper.looper,
86                 Handler(testableLooper.looper),
87                 falsingManager,
88                 metricsLogger,
89                 statusBarStateController,
90                 activityStarter,
91                 qsLogger,
92                 bluetoothController,
93                 featureFlags,
94                 bluetoothTileDialogViewModel,
95             )
96 
97         tile.initialize()
98         testableLooper.processAllMessages()
99     }
100 
101     @After
tearDownnull102     fun tearDown() {
103         tile.destroy()
104         testableLooper.processAllMessages()
105     }
106 
107     @Test
testRestrictionCheckednull108     fun testRestrictionChecked() {
109         tile.refreshState()
110         testableLooper.processAllMessages()
111 
112         assertThat(tile.restrictionChecked).isEqualTo(UserManager.DISALLOW_BLUETOOTH)
113     }
114 
115     @Test
testIcon_whenDisabled_isOffStatenull116     fun testIcon_whenDisabled_isOffState() {
117         val state = QSTile.BooleanState()
118         disableBluetooth()
119 
120         tile.handleUpdateState(state, /* arg= */ null)
121 
122         assertThat(state.icon).isEqualTo(createExpectedIcon(R.drawable.qs_bluetooth_icon_off))
123     }
124 
125     @Test
testIcon_whenDisconnected_isOffStatenull126     fun testIcon_whenDisconnected_isOffState() {
127         val state = QSTile.BooleanState()
128         enableBluetooth()
129         setBluetoothDisconnected()
130 
131         tile.handleUpdateState(state, /* arg= */ null)
132 
133         assertThat(state.icon).isEqualTo(createExpectedIcon(R.drawable.qs_bluetooth_icon_off))
134     }
135 
136     @Test
testIcon_whenConnected_isOnStatenull137     fun testIcon_whenConnected_isOnState() {
138         val state = QSTile.BooleanState()
139         enableBluetooth()
140         setBluetoothConnected()
141 
142         tile.handleUpdateState(state, /* arg= */ null)
143 
144         assertThat(state.icon).isEqualTo(createExpectedIcon(R.drawable.qs_bluetooth_icon_on))
145     }
146 
147     @Test
testIcon_whenConnecting_isSearchStatenull148     fun testIcon_whenConnecting_isSearchState() {
149         val state = QSTile.BooleanState()
150         enableBluetooth()
151         setBluetoothConnecting()
152 
153         tile.handleUpdateState(state, /* arg= */ null)
154 
155         assertThat(state.icon).isEqualTo(createExpectedIcon(R.drawable.qs_bluetooth_icon_search))
156     }
157 
158     @Test
testSecondaryLabel_whenBatteryMetadataAvailable_isMetadataBatteryLevelStatenull159     fun testSecondaryLabel_whenBatteryMetadataAvailable_isMetadataBatteryLevelState() {
160         val cachedDevice = mock<CachedBluetoothDevice>()
161         val state = QSTile.BooleanState()
162         listenToDeviceMetadata(state, cachedDevice, 50)
163 
164         tile.handleUpdateState(state, /* arg= */ null)
165 
166         assertThat(state.secondaryLabel)
167             .isEqualTo(
168                 mContext.getString(
169                     R.string.quick_settings_bluetooth_secondary_label_battery_level,
170                     Utils.formatPercentage(50),
171                 )
172             )
173         verify(bluetoothController).addOnMetadataChangedListener(eq(cachedDevice), any(), any())
174     }
175 
176     @Test
testSecondaryLabel_whenBatteryMetadataUnavailable_isBluetoothBatteryLevelStatenull177     fun testSecondaryLabel_whenBatteryMetadataUnavailable_isBluetoothBatteryLevelState() {
178         val state = QSTile.BooleanState()
179         val cachedDevice = mock<CachedBluetoothDevice>()
180         listenToDeviceMetadata(state, cachedDevice, 50)
181         val cachedDevice2 = mock<CachedBluetoothDevice>()
182         val btDevice = mock<BluetoothDevice>()
183         whenever(cachedDevice2.device).thenReturn(btDevice)
184         whenever(btDevice.getMetadata(BluetoothDevice.METADATA_MAIN_BATTERY)).thenReturn(null)
185         whenever(cachedDevice2.minBatteryLevelWithMemberDevices).thenReturn(25)
186         addConnectedDevice(cachedDevice2)
187 
188         tile.handleUpdateState(state, /* arg= */ null)
189 
190         assertThat(state.secondaryLabel)
191             .isEqualTo(
192                 mContext.getString(
193                     R.string.quick_settings_bluetooth_secondary_label_battery_level,
194                     Utils.formatPercentage(25),
195                 )
196             )
197         verify(bluetoothController, times(1))
198             .removeOnMetadataChangedListener(eq(cachedDevice), any())
199     }
200 
201     @Test
handleClick_hasSatelliteFeatureButNoQsTileDialogAndClickIsProcessing_doNothingnull202     fun handleClick_hasSatelliteFeatureButNoQsTileDialogAndClickIsProcessing_doNothing() {
203         mSetFlagsRule.enableFlags(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
204         `when`(featureFlags.isEnabled(com.android.systemui.flags.Flags.BLUETOOTH_QS_TILE_DIALOG))
205             .thenReturn(false)
206         `when`(clickJob.isCompleted).thenReturn(false)
207         tile.mClickJob = clickJob
208 
209         tile.handleClick(null)
210 
211         verify(bluetoothController, times(0)).setBluetoothEnabled(any())
212     }
213 
214     @Test
handleClick_noSatelliteFeatureAndNoQsTileDialog_directSetBtEnablenull215     fun handleClick_noSatelliteFeatureAndNoQsTileDialog_directSetBtEnable() {
216         mSetFlagsRule.disableFlags(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
217         `when`(featureFlags.isEnabled(com.android.systemui.flags.Flags.BLUETOOTH_QS_TILE_DIALOG))
218             .thenReturn(false)
219 
220         tile.handleClick(null)
221 
222         verify(bluetoothController).setBluetoothEnabled(any())
223     }
224 
225     @Test
handleClick_noSatelliteFeatureButHasQsTileDialog_showDialognull226     fun handleClick_noSatelliteFeatureButHasQsTileDialog_showDialog() {
227         mSetFlagsRule.disableFlags(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
228         `when`(featureFlags.isEnabled(com.android.systemui.flags.Flags.BLUETOOTH_QS_TILE_DIALOG))
229             .thenReturn(true)
230 
231         tile.handleClick(null)
232 
233         verify(bluetoothTileDialogViewModel).showDialog(null)
234     }
235 
236     @Test
testMetadataListener_whenDisconnected_isUnregisterednull237     fun testMetadataListener_whenDisconnected_isUnregistered() {
238         val state = QSTile.BooleanState()
239         val cachedDevice = mock<CachedBluetoothDevice>()
240         listenToDeviceMetadata(state, cachedDevice, 50)
241         disableBluetooth()
242 
243         tile.handleUpdateState(state, null)
244 
245         verify(bluetoothController, times(1))
246             .removeOnMetadataChangedListener(eq(cachedDevice), any())
247     }
248 
249     @Test
testMetadataListener_whenTileNotListening_isUnregisterednull250     fun testMetadataListener_whenTileNotListening_isUnregistered() {
251         val state = QSTile.BooleanState()
252         val cachedDevice = mock<CachedBluetoothDevice>()
253         listenToDeviceMetadata(state, cachedDevice, 50)
254 
255         tile.handleSetListening(false)
256 
257         verify(bluetoothController, times(1))
258             .removeOnMetadataChangedListener(eq(cachedDevice), any())
259     }
260 
261     private class FakeBluetoothTile(
262         qsHost: QSHost,
263         uiEventLogger: QsEventLogger,
264         backgroundLooper: Looper,
265         mainHandler: Handler,
266         falsingManager: FalsingManager,
267         metricsLogger: MetricsLogger,
268         statusBarStateController: StatusBarStateController,
269         activityStarter: ActivityStarter,
270         qsLogger: QSLogger,
271         bluetoothController: BluetoothController,
272         featureFlags: FeatureFlagsClassic,
273         bluetoothTileDialogViewModel: BluetoothTileDialogViewModel,
274     ) :
275         BluetoothTile(
276             qsHost,
277             uiEventLogger,
278             backgroundLooper,
279             mainHandler,
280             falsingManager,
281             metricsLogger,
282             statusBarStateController,
283             activityStarter,
284             qsLogger,
285             bluetoothController,
286             featureFlags,
287             bluetoothTileDialogViewModel,
288         ) {
289         var restrictionChecked: String? = null
290 
checkIfRestrictionEnforcedByAdminOnlynull291         override fun checkIfRestrictionEnforcedByAdminOnly(
292             state: QSTile.State?,
293             userRestriction: String?,
294         ) {
295             restrictionChecked = userRestriction
296         }
297     }
298 
enableBluetoothnull299     fun enableBluetooth() {
300         whenever(bluetoothController.isBluetoothEnabled).thenReturn(true)
301     }
302 
disableBluetoothnull303     fun disableBluetooth() {
304         whenever(bluetoothController.isBluetoothEnabled).thenReturn(false)
305     }
306 
setBluetoothDisconnectednull307     fun setBluetoothDisconnected() {
308         whenever(bluetoothController.isBluetoothConnecting).thenReturn(false)
309         whenever(bluetoothController.isBluetoothConnected).thenReturn(false)
310     }
311 
setBluetoothConnectednull312     fun setBluetoothConnected() {
313         whenever(bluetoothController.isBluetoothConnecting).thenReturn(false)
314         whenever(bluetoothController.isBluetoothConnected).thenReturn(true)
315     }
316 
setBluetoothConnectingnull317     fun setBluetoothConnecting() {
318         whenever(bluetoothController.isBluetoothConnected).thenReturn(false)
319         whenever(bluetoothController.isBluetoothConnecting).thenReturn(true)
320     }
321 
addConnectedDevicenull322     fun addConnectedDevice(device: CachedBluetoothDevice) {
323         whenever(bluetoothController.connectedDevices).thenReturn(listOf(device))
324     }
325 
listenToDeviceMetadatanull326     fun listenToDeviceMetadata(
327         state: QSTile.BooleanState,
328         cachedDevice: CachedBluetoothDevice,
329         batteryLevel: Int,
330     ) {
331         val btDevice = mock<BluetoothDevice>()
332         whenever(cachedDevice.device).thenReturn(btDevice)
333         whenever(btDevice.getMetadata(BluetoothDevice.METADATA_MAIN_BATTERY))
334             .thenReturn(batteryLevel.toString().toByteArray())
335         enableBluetooth()
336         setBluetoothConnected()
337         addConnectedDevice(cachedDevice)
338         tile.handleUpdateState(state, /* arg= */ null)
339     }
340 
createExpectedIconnull341     private fun createExpectedIcon(resId: Int): QSTile.Icon {
342         return if (isEnabled) {
343             DrawableIconWithRes(mContext.getDrawable(resId), resId)
344         } else {
345             QSTileImpl.ResourceIcon.get(resId)
346         }
347     }
348 
349     companion object {
350         @JvmStatic
351         @Parameters(name = "{0}")
getParamsnull352         fun getParams(): List<FlagsParameterization> {
353             return allCombinationsOf(QSComposeFragment.FLAG_NAME)
354         }
355     }
356 }
357