1 /* 2 * Copyright (C) 2023 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 17 package com.android.settings.bluetooth 18 19 import android.bluetooth.BluetoothAdapter 20 import android.bluetooth.BluetoothDevice 21 import android.bluetooth.BluetoothUuid 22 import android.bluetooth.le.BluetoothLeScanner 23 import android.bluetooth.le.ScanCallback 24 import android.bluetooth.le.ScanFilter 25 import android.content.Context 26 import android.content.res.Resources 27 import androidx.preference.Preference 28 import com.android.settings.R 29 import com.android.settings.testutils.shadow.ShadowBluetoothAdapter 30 import com.android.settingslib.bluetooth.BluetoothDeviceFilter 31 import com.android.settingslib.bluetooth.CachedBluetoothDevice 32 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager 33 import com.google.common.truth.Truth.assertThat 34 import kotlinx.coroutines.delay 35 import kotlinx.coroutines.runBlocking 36 import org.junit.Before 37 import org.junit.Rule 38 import org.junit.Test 39 import org.junit.runner.RunWith 40 import org.mockito.ArgumentMatchers.any 41 import org.mockito.ArgumentMatchers.eq 42 import org.mockito.Mock 43 import org.mockito.Mockito.doNothing 44 import org.mockito.Mockito.doReturn 45 import org.mockito.Mockito.mock 46 import org.mockito.Mockito.never 47 import org.mockito.Mockito.spy 48 import org.mockito.Mockito.times 49 import org.mockito.Mockito.verify 50 import org.mockito.Spy 51 import org.mockito.junit.MockitoJUnit 52 import org.mockito.junit.MockitoRule 53 import org.robolectric.RobolectricTestRunner 54 import org.robolectric.RuntimeEnvironment 55 import org.robolectric.annotation.Config 56 import org.mockito.Mockito.`when` as whenever 57 58 @RunWith(RobolectricTestRunner::class) 59 @Config(shadows = [ShadowBluetoothAdapter::class]) 60 class DeviceListPreferenceFragmentTest { 61 @get:Rule 62 val mockito: MockitoRule = MockitoJUnit.rule() 63 64 @Mock 65 private lateinit var resource: Resources 66 67 @Mock 68 private lateinit var context: Context 69 70 @Mock 71 private lateinit var bluetoothLeScanner: BluetoothLeScanner 72 73 @Mock 74 private lateinit var cachedDeviceManager: CachedBluetoothDeviceManager 75 76 @Mock 77 private lateinit var cachedDevice: CachedBluetoothDevice 78 79 @Spy 80 private var fragment = TestFragment() 81 82 private lateinit var myDevicePreference: Preference 83 private lateinit var bluetoothAdapter: BluetoothAdapter 84 85 @Before setUpnull86 fun setUp() { 87 doReturn(context).`when`(fragment).context 88 doReturn(resource).`when`(fragment).resources 89 doNothing().`when`(fragment).onDeviceAdded(cachedDevice) 90 bluetoothAdapter = spy(BluetoothAdapter.getDefaultAdapter()) 91 fragment.mBluetoothAdapter = bluetoothAdapter 92 fragment.mCachedDeviceManager = cachedDeviceManager 93 94 myDevicePreference = Preference(RuntimeEnvironment.application) 95 } 96 97 @Test setUpdateMyDevicePreference_setTitleCorrectlynull98 fun setUpdateMyDevicePreference_setTitleCorrectly() { 99 doReturn(FOOTAGE_MAC_STRING).`when`(fragment) 100 .getString(eq(R.string.bluetooth_footer_mac_message), any()) 101 102 fragment.updateFooterPreference(myDevicePreference) 103 104 assertThat(myDevicePreference.title).isEqualTo(FOOTAGE_MAC_STRING) 105 } 106 107 @Test testEnableDisableScanning_testStateAfterEnableDisablenull108 fun testEnableDisableScanning_testStateAfterEnableDisable() { 109 fragment.enableScanning() 110 verify(fragment).startScanning() 111 assertThat(fragment.mScanEnabled).isTrue() 112 113 fragment.disableScanning() 114 verify(fragment).stopScanning() 115 assertThat(fragment.mScanEnabled).isFalse() 116 } 117 118 @Test testScanningStateChanged_testScanStartednull119 fun testScanningStateChanged_testScanStarted() { 120 fragment.enableScanning() 121 assertThat(fragment.mScanEnabled).isTrue() 122 verify(fragment).startScanning() 123 124 fragment.onScanningStateChanged(true) 125 verify(fragment, times(1)).startScanning() 126 } 127 128 @Test testScanningStateChanged_testScanFinishednull129 fun testScanningStateChanged_testScanFinished() { 130 // Could happen when last scanning not done while current scan gets enabled 131 fragment.enableScanning() 132 verify(fragment).startScanning() 133 assertThat(fragment.mScanEnabled).isTrue() 134 135 fragment.onScanningStateChanged(false) 136 verify(fragment, times(2)).startScanning() 137 } 138 139 @Test testScanningStateChanged_testScanStateMultiplenull140 fun testScanningStateChanged_testScanStateMultiple() { 141 // Could happen when last scanning not done while current scan gets enabled 142 fragment.enableScanning() 143 assertThat(fragment.mScanEnabled).isTrue() 144 verify(fragment).startScanning() 145 146 fragment.onScanningStateChanged(true) 147 verify(fragment, times(1)).startScanning() 148 149 fragment.onScanningStateChanged(false) 150 verify(fragment, times(2)).startScanning() 151 152 fragment.onScanningStateChanged(true) 153 verify(fragment, times(2)).startScanning() 154 155 fragment.disableScanning() 156 verify(fragment).stopScanning() 157 158 fragment.onScanningStateChanged(false) 159 verify(fragment, times(2)).startScanning() 160 161 fragment.onScanningStateChanged(true) 162 verify(fragment, times(2)).startScanning() 163 } 164 165 @Test testScanningStateChanged_testScanFinishedAfterDisablenull166 fun testScanningStateChanged_testScanFinishedAfterDisable() { 167 fragment.enableScanning() 168 verify(fragment).startScanning() 169 assertThat(fragment.mScanEnabled).isTrue() 170 171 fragment.disableScanning() 172 verify(fragment).stopScanning() 173 assertThat(fragment.mScanEnabled).isFalse() 174 175 fragment.onScanningStateChanged(false) 176 verify(fragment, times(1)).startScanning() 177 } 178 179 @Test testScanningStateChanged_testScanStartedAfterDisablenull180 fun testScanningStateChanged_testScanStartedAfterDisable() { 181 fragment.enableScanning() 182 verify(fragment).startScanning() 183 assertThat(fragment.mScanEnabled).isTrue() 184 185 fragment.disableScanning() 186 verify(fragment).stopScanning() 187 assertThat(fragment.mScanEnabled).isFalse() 188 189 fragment.onScanningStateChanged(true) 190 verify(fragment, times(1)).startScanning() 191 } 192 193 @Test startScanning_setLeScanFilter_shouldStartLeScannull194 fun startScanning_setLeScanFilter_shouldStartLeScan() { 195 val leScanFilter = ScanFilter.Builder() 196 .setServiceData(BluetoothUuid.HEARING_AID, byteArrayOf(0), byteArrayOf(0)) 197 .build() 198 doReturn(bluetoothLeScanner).`when`(bluetoothAdapter).bluetoothLeScanner 199 200 fragment.setFilter(listOf(leScanFilter)) 201 fragment.startScanning() 202 203 verify(bluetoothLeScanner).startScan(eq(listOf(leScanFilter)), any(), any<ScanCallback>()) 204 } 205 206 @Test <lambda>null207 fun addCachedDevices_whenFilterIsNull_onDeviceAddedIsCalled() = runBlocking { 208 val mockCachedDevice = mock(CachedBluetoothDevice::class.java) 209 whenever(cachedDeviceManager.cachedDevicesCopy).thenReturn(listOf(mockCachedDevice)) 210 fragment.lifecycleScope = this 211 212 fragment.addCachedDevices(filterForCachedDevices = null) 213 delay(100) 214 215 verify(fragment).onDeviceAdded(mockCachedDevice) 216 } 217 218 @Test <lambda>null219 fun addCachedDevices_whenFilterMatched_onDeviceAddedIsCalled() = runBlocking { 220 val mockBluetoothDevice = mock(BluetoothDevice::class.java) 221 whenever(mockBluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_NONE) 222 whenever(cachedDevice.device).thenReturn(mockBluetoothDevice) 223 whenever(cachedDeviceManager.cachedDevicesCopy).thenReturn(listOf(cachedDevice)) 224 fragment.lifecycleScope = this 225 226 fragment.addCachedDevices(BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER) 227 delay(100) 228 229 verify(fragment).onDeviceAdded(cachedDevice) 230 } 231 232 @Test <lambda>null233 fun addCachedDevices_whenFilterNoMatch_onDeviceAddedNotCalled() = runBlocking { 234 val mockBluetoothDevice = mock(BluetoothDevice::class.java) 235 whenever(mockBluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) 236 whenever(cachedDevice.device).thenReturn(mockBluetoothDevice) 237 whenever(cachedDeviceManager.cachedDevicesCopy).thenReturn(listOf(cachedDevice)) 238 fragment.lifecycleScope = this 239 240 fragment.addCachedDevices(BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER) 241 delay(100) 242 243 verify(fragment, never()).onDeviceAdded(cachedDevice) 244 } 245 246 /** 247 * Fragment to test since `DeviceListPreferenceFragment` is abstract 248 */ 249 open class TestFragment : DeviceListPreferenceFragment(null) { getMetricsCategorynull250 override fun getMetricsCategory() = 0 251 override fun initPreferencesFromPreferenceScreen() {} 252 override val deviceListKey = "device_list" getLogTagnull253 override fun getLogTag() = null 254 override fun getPreferenceScreenResId() = 0 255 } 256 257 private companion object { 258 const val FOOTAGE_MAC_STRING = "Bluetooth mac: xxxx" 259 } 260 } 261