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