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 = [ 60 ShadowBluetoothAdapter::class, 61 com.android.settings.testutils.shadow.ShadowFragment::class, 62 ]) 63 class DeviceListPreferenceFragmentTest { 64 @get:Rule 65 val mockito: MockitoRule = MockitoJUnit.rule() 66 67 @Mock 68 private lateinit var resource: Resources 69 70 @Mock 71 private lateinit var context: Context 72 73 @Mock 74 private lateinit var bluetoothLeScanner: BluetoothLeScanner 75 76 @Mock 77 private lateinit var cachedDeviceManager: CachedBluetoothDeviceManager 78 79 @Mock 80 private lateinit var cachedDevice: CachedBluetoothDevice 81 82 @Spy 83 private var fragment = TestFragment() 84 85 private lateinit var myDevicePreference: Preference 86 private lateinit var bluetoothAdapter: BluetoothAdapter 87 88 @Before setUpnull89 fun setUp() { 90 doReturn(context).`when`(fragment).context 91 doReturn(resource).`when`(fragment).resources 92 doNothing().`when`(fragment).onDeviceAdded(cachedDevice) 93 bluetoothAdapter = spy(BluetoothAdapter.getDefaultAdapter()) 94 fragment.mBluetoothAdapter = bluetoothAdapter 95 fragment.mCachedDeviceManager = cachedDeviceManager 96 97 myDevicePreference = Preference(RuntimeEnvironment.application) 98 } 99 100 @Test setUpdateMyDevicePreference_setTitleCorrectlynull101 fun setUpdateMyDevicePreference_setTitleCorrectly() { 102 doReturn(FOOTAGE_MAC_STRING).`when`(fragment) 103 .getString(eq(R.string.bluetooth_footer_mac_message), any()) 104 105 fragment.updateFooterPreference(myDevicePreference) 106 107 assertThat(myDevicePreference.title).isEqualTo(FOOTAGE_MAC_STRING) 108 } 109 110 @Test testEnableDisableScanning_testStateAfterEnableDisablenull111 fun testEnableDisableScanning_testStateAfterEnableDisable() { 112 fragment.enableScanning() 113 verify(fragment).startScanning() 114 assertThat(fragment.mScanEnabled).isTrue() 115 116 fragment.disableScanning() 117 verify(fragment).stopScanning() 118 assertThat(fragment.mScanEnabled).isFalse() 119 } 120 121 @Test testScanningStateChanged_testScanStartednull122 fun testScanningStateChanged_testScanStarted() { 123 fragment.enableScanning() 124 assertThat(fragment.mScanEnabled).isTrue() 125 verify(fragment).startScanning() 126 127 fragment.onScanningStateChanged(true) 128 verify(fragment, times(1)).startScanning() 129 } 130 131 @Test testScanningStateChanged_testScanFinishednull132 fun testScanningStateChanged_testScanFinished() { 133 // Could happen when last scanning not done while current scan gets enabled 134 fragment.enableScanning() 135 verify(fragment).startScanning() 136 assertThat(fragment.mScanEnabled).isTrue() 137 138 fragment.onScanningStateChanged(false) 139 verify(fragment, times(2)).startScanning() 140 } 141 142 @Test testScanningStateChanged_testScanStateMultiplenull143 fun testScanningStateChanged_testScanStateMultiple() { 144 // Could happen when last scanning not done while current scan gets enabled 145 fragment.enableScanning() 146 assertThat(fragment.mScanEnabled).isTrue() 147 verify(fragment).startScanning() 148 149 fragment.onScanningStateChanged(true) 150 verify(fragment, times(1)).startScanning() 151 152 fragment.onScanningStateChanged(false) 153 verify(fragment, times(2)).startScanning() 154 155 fragment.onScanningStateChanged(true) 156 verify(fragment, times(2)).startScanning() 157 158 fragment.disableScanning() 159 verify(fragment).stopScanning() 160 161 fragment.onScanningStateChanged(false) 162 verify(fragment, times(2)).startScanning() 163 164 fragment.onScanningStateChanged(true) 165 verify(fragment, times(2)).startScanning() 166 } 167 168 @Test testScanningStateChanged_testScanFinishedAfterDisablenull169 fun testScanningStateChanged_testScanFinishedAfterDisable() { 170 fragment.enableScanning() 171 verify(fragment).startScanning() 172 assertThat(fragment.mScanEnabled).isTrue() 173 174 fragment.disableScanning() 175 verify(fragment).stopScanning() 176 assertThat(fragment.mScanEnabled).isFalse() 177 178 fragment.onScanningStateChanged(false) 179 verify(fragment, times(1)).startScanning() 180 } 181 182 @Test testScanningStateChanged_testScanStartedAfterDisablenull183 fun testScanningStateChanged_testScanStartedAfterDisable() { 184 fragment.enableScanning() 185 verify(fragment).startScanning() 186 assertThat(fragment.mScanEnabled).isTrue() 187 188 fragment.disableScanning() 189 verify(fragment).stopScanning() 190 assertThat(fragment.mScanEnabled).isFalse() 191 192 fragment.onScanningStateChanged(true) 193 verify(fragment, times(1)).startScanning() 194 } 195 196 @Test startScanning_setLeScanFilter_shouldStartLeScannull197 fun startScanning_setLeScanFilter_shouldStartLeScan() { 198 val leScanFilter = ScanFilter.Builder() 199 .setServiceData(BluetoothUuid.HEARING_AID, byteArrayOf(0), byteArrayOf(0)) 200 .build() 201 doReturn(bluetoothLeScanner).`when`(bluetoothAdapter).bluetoothLeScanner 202 203 fragment.setFilter(listOf(leScanFilter)) 204 fragment.startScanning() 205 206 verify(bluetoothLeScanner).startScan(eq(listOf(leScanFilter)), any(), any<ScanCallback>()) 207 } 208 209 @Test <lambda>null210 fun addCachedDevices_whenFilterIsNull_onDeviceAddedIsCalled() = runBlocking { 211 val mockCachedDevice = mock(CachedBluetoothDevice::class.java) 212 whenever(cachedDeviceManager.cachedDevicesCopy).thenReturn(listOf(mockCachedDevice)) 213 fragment.lifecycleScope = this 214 215 fragment.addCachedDevices(filterForCachedDevices = null) 216 delay(100) 217 218 verify(fragment).onDeviceAdded(mockCachedDevice) 219 } 220 221 @Test <lambda>null222 fun addCachedDevices_whenFilterMatched_onDeviceAddedIsCalled() = runBlocking { 223 val mockBluetoothDevice = mock(BluetoothDevice::class.java) 224 whenever(mockBluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_NONE) 225 whenever(cachedDevice.device).thenReturn(mockBluetoothDevice) 226 whenever(cachedDeviceManager.cachedDevicesCopy).thenReturn(listOf(cachedDevice)) 227 fragment.lifecycleScope = this 228 229 fragment.addCachedDevices(BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER) 230 delay(100) 231 232 verify(fragment).onDeviceAdded(cachedDevice) 233 } 234 235 @Test <lambda>null236 fun addCachedDevices_whenFilterNoMatch_onDeviceAddedNotCalled() = runBlocking { 237 val mockBluetoothDevice = mock(BluetoothDevice::class.java) 238 whenever(mockBluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) 239 whenever(cachedDevice.device).thenReturn(mockBluetoothDevice) 240 whenever(cachedDeviceManager.cachedDevicesCopy).thenReturn(listOf(cachedDevice)) 241 fragment.lifecycleScope = this 242 243 fragment.addCachedDevices(BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER) 244 delay(100) 245 246 verify(fragment, never()).onDeviceAdded(cachedDevice) 247 } 248 249 /** 250 * Fragment to test since `DeviceListPreferenceFragment` is abstract 251 */ 252 open class TestFragment : DeviceListPreferenceFragment(null) { getMetricsCategorynull253 override fun getMetricsCategory() = 0 254 override fun initPreferencesFromPreferenceScreen() {} 255 override val deviceListKey = "device_list" getLogTagnull256 override fun getLogTag() = null 257 override fun getPreferenceScreenResId() = 0 258 } 259 260 private companion object { 261 const val FOOTAGE_MAC_STRING = "Bluetooth mac: xxxx" 262 } 263 } 264