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.testing.AndroidTestingRunner
8 import android.testing.TestableLooper
9 import android.testing.TestableLooper.RunWithLooper
10 import androidx.test.filters.SmallTest
11 import com.android.internal.logging.MetricsLogger
12 import com.android.settingslib.Utils
13 import com.android.settingslib.bluetooth.CachedBluetoothDevice
14 import com.android.systemui.R
15 import com.android.systemui.SysuiTestCase
16 import com.android.systemui.classifier.FalsingManagerFake
17 import com.android.systemui.plugins.ActivityStarter
18 import com.android.systemui.plugins.FalsingManager
19 import com.android.systemui.plugins.qs.QSTile
20 import com.android.systemui.plugins.statusbar.StatusBarStateController
21 import com.android.systemui.qs.QSHost
22 import com.android.systemui.qs.QsEventLogger
23 import com.android.systemui.qs.logging.QSLogger
24 import com.android.systemui.qs.tileimpl.QSTileImpl
25 import com.android.systemui.statusbar.policy.BluetoothController
26 import com.android.systemui.util.mockito.any
27 import com.android.systemui.util.mockito.eq
28 import com.android.systemui.util.mockito.mock
29 import com.android.systemui.util.mockito.whenever
30 import com.google.common.truth.Truth.assertThat
31 import org.junit.After
32 import org.junit.Before
33 import org.junit.Test
34 import org.junit.runner.RunWith
35 import org.mockito.Mock
36 import org.mockito.Mockito.times
37 import org.mockito.Mockito.verify
38 import org.mockito.MockitoAnnotations
39 
40 @RunWith(AndroidTestingRunner::class)
41 @RunWithLooper(setAsMainLooper = true)
42 @SmallTest
43 class BluetoothTileTest : SysuiTestCase() {
44 
45     @Mock private lateinit var qsLogger: QSLogger
46     @Mock private lateinit var qsHost: QSHost
47     @Mock private lateinit var metricsLogger: MetricsLogger
48     private val falsingManager = FalsingManagerFake()
49     @Mock private lateinit var statusBarStateController: StatusBarStateController
50     @Mock private lateinit var activityStarter: ActivityStarter
51     @Mock private lateinit var bluetoothController: BluetoothController
52     @Mock private lateinit var uiEventLogger: QsEventLogger
53 
54     private lateinit var testableLooper: TestableLooper
55     private lateinit var tile: FakeBluetoothTile
56 
57     @Before
58     fun setUp() {
59         MockitoAnnotations.initMocks(this)
60         testableLooper = TestableLooper.get(this)
61 
62         whenever(qsHost.context).thenReturn(mContext)
63 
64         tile =
65             FakeBluetoothTile(
66                 qsHost,
67                 uiEventLogger,
68                 testableLooper.looper,
69                 Handler(testableLooper.looper),
70                 falsingManager,
71                 metricsLogger,
72                 statusBarStateController,
73                 activityStarter,
74                 qsLogger,
75                 bluetoothController,
76             )
77 
78         tile.initialize()
79         testableLooper.processAllMessages()
80     }
81 
82     @After
83     fun tearDown() {
84         tile.destroy()
85         testableLooper.processAllMessages()
86     }
87 
88     @Test
89     fun testRestrictionChecked() {
90         tile.refreshState()
91         testableLooper.processAllMessages()
92 
93         assertThat(tile.restrictionChecked).isEqualTo(UserManager.DISALLOW_BLUETOOTH)
94     }
95 
96     @Test
97     fun testIcon_whenDisabled_isOffState() {
98         val state = QSTile.BooleanState()
99         disableBluetooth()
100 
101         tile.handleUpdateState(state, /* arg= */ null)
102 
103         assertThat(state.icon)
104             .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_bluetooth_icon_off))
105     }
106 
107     @Test
108     fun testIcon_whenDisconnected_isOffState() {
109         val state = QSTile.BooleanState()
110         enableBluetooth()
111         setBluetoothDisconnected()
112 
113         tile.handleUpdateState(state, /* arg= */ null)
114 
115         assertThat(state.icon)
116             .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_bluetooth_icon_off))
117     }
118 
119     @Test
120     fun testIcon_whenConnected_isOnState() {
121         val state = QSTile.BooleanState()
122         enableBluetooth()
123         setBluetoothConnected()
124 
125         tile.handleUpdateState(state, /* arg= */ null)
126 
127         assertThat(state.icon)
128             .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_bluetooth_icon_on))
129     }
130 
131     @Test
132     fun testIcon_whenConnecting_isSearchState() {
133         val state = QSTile.BooleanState()
134         enableBluetooth()
135         setBluetoothConnecting()
136 
137         tile.handleUpdateState(state, /* arg= */ null)
138 
139         assertThat(state.icon)
140             .isEqualTo(QSTileImpl.ResourceIcon.get(R.drawable.qs_bluetooth_icon_search))
141     }
142 
143     @Test
144     fun testSecondaryLabel_whenBatteryMetadataAvailable_isMetadataBatteryLevelState() {
145         val cachedDevice = mock<CachedBluetoothDevice>()
146         val state = QSTile.BooleanState()
147         listenToDeviceMetadata(state, cachedDevice, 50)
148 
149         tile.handleUpdateState(state, /* arg= */ null)
150 
151         assertThat(state.secondaryLabel)
152             .isEqualTo(
153                 mContext.getString(
154                     R.string.quick_settings_bluetooth_secondary_label_battery_level,
155                     Utils.formatPercentage(50)
156                 )
157             )
158         verify(bluetoothController)
159             .addOnMetadataChangedListener(eq(cachedDevice), any(), any())
160     }
161 
162     @Test
163     fun testSecondaryLabel_whenBatteryMetadataUnavailable_isBluetoothBatteryLevelState() {
164         val state = QSTile.BooleanState()
165         val cachedDevice = mock<CachedBluetoothDevice>()
166         listenToDeviceMetadata(state, cachedDevice, 50)
167         val cachedDevice2 = mock<CachedBluetoothDevice>()
168         val btDevice = mock<BluetoothDevice>()
169         whenever(cachedDevice2.device).thenReturn(btDevice)
170         whenever(btDevice.getMetadata(BluetoothDevice.METADATA_MAIN_BATTERY)).thenReturn(null)
171         whenever(cachedDevice2.minBatteryLevelWithMemberDevices).thenReturn(25)
172         addConnectedDevice(cachedDevice2)
173 
174         tile.handleUpdateState(state, /* arg= */ null)
175 
176         assertThat(state.secondaryLabel)
177             .isEqualTo(
178                 mContext.getString(
179                     R.string.quick_settings_bluetooth_secondary_label_battery_level,
180                     Utils.formatPercentage(25)
181                 )
182             )
183         verify(bluetoothController, times(1))
184             .removeOnMetadataChangedListener(eq(cachedDevice), any())
185     }
186 
187     @Test
188     fun testMetadataListener_whenDisconnected_isUnregistered() {
189         val state = QSTile.BooleanState()
190         val cachedDevice = mock<CachedBluetoothDevice>()
191         listenToDeviceMetadata(state, cachedDevice, 50)
192         disableBluetooth()
193 
194         tile.handleUpdateState(state, null)
195 
196         verify(bluetoothController, times(1))
197             .removeOnMetadataChangedListener(eq(cachedDevice), any())
198     }
199 
200     @Test
201     fun testMetadataListener_whenTileNotListening_isUnregistered() {
202         val state = QSTile.BooleanState()
203         val cachedDevice = mock<CachedBluetoothDevice>()
204         listenToDeviceMetadata(state, cachedDevice, 50)
205 
206         tile.handleSetListening(false)
207 
208         verify(bluetoothController, times(1))
209             .removeOnMetadataChangedListener(eq(cachedDevice), any())
210     }
211 
212     private class FakeBluetoothTile(
213         qsHost: QSHost,
214         uiEventLogger: QsEventLogger,
215         backgroundLooper: Looper,
216         mainHandler: Handler,
217         falsingManager: FalsingManager,
218         metricsLogger: MetricsLogger,
219         statusBarStateController: StatusBarStateController,
220         activityStarter: ActivityStarter,
221         qsLogger: QSLogger,
222         bluetoothController: BluetoothController,
223     ) :
224         BluetoothTile(
225             qsHost,
226             uiEventLogger,
227             backgroundLooper,
228             mainHandler,
229             falsingManager,
230             metricsLogger,
231             statusBarStateController,
232             activityStarter,
233             qsLogger,
234             bluetoothController,
235         ) {
236         var restrictionChecked: String? = null
237 
238         override fun checkIfRestrictionEnforcedByAdminOnly(
239             state: QSTile.State?,
240             userRestriction: String?
241         ) {
242             restrictionChecked = userRestriction
243         }
244     }
245 
246     fun enableBluetooth() {
247         whenever(bluetoothController.isBluetoothEnabled).thenReturn(true)
248     }
249 
250     fun disableBluetooth() {
251         whenever(bluetoothController.isBluetoothEnabled).thenReturn(false)
252     }
253 
254     fun setBluetoothDisconnected() {
255         whenever(bluetoothController.isBluetoothConnecting).thenReturn(false)
256         whenever(bluetoothController.isBluetoothConnected).thenReturn(false)
257     }
258 
259     fun setBluetoothConnected() {
260         whenever(bluetoothController.isBluetoothConnecting).thenReturn(false)
261         whenever(bluetoothController.isBluetoothConnected).thenReturn(true)
262     }
263 
264     fun setBluetoothConnecting() {
265         whenever(bluetoothController.isBluetoothConnected).thenReturn(false)
266         whenever(bluetoothController.isBluetoothConnecting).thenReturn(true)
267     }
268 
269     fun addConnectedDevice(device: CachedBluetoothDevice) {
270         whenever(bluetoothController.connectedDevices).thenReturn(listOf(device))
271     }
272 
273     fun listenToDeviceMetadata(
274         state: QSTile.BooleanState,
275         cachedDevice: CachedBluetoothDevice,
276         batteryLevel: Int
277     ) {
278         val btDevice = mock<BluetoothDevice>()
279         whenever(cachedDevice.device).thenReturn(btDevice)
280         whenever(btDevice.getMetadata(BluetoothDevice.METADATA_MAIN_BATTERY))
281             .thenReturn(batteryLevel.toString().toByteArray())
282         enableBluetooth()
283         setBluetoothConnected()
284         addConnectedDevice(cachedDevice)
285         tile.handleUpdateState(state, /* arg= */ null)
286     }
287 }
288