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