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 package com.android.settings.bluetooth
17 
18 import android.bluetooth.BluetoothAdapter
19 import android.bluetooth.BluetoothDevice
20 import android.bluetooth.le.BluetoothLeScanner
21 import android.bluetooth.le.ScanCallback
22 import android.bluetooth.le.ScanFilter
23 import android.bluetooth.le.ScanResult
24 import android.bluetooth.le.ScanSettings
25 import android.os.Bundle
26 import android.os.SystemProperties
27 import android.text.BidiFormatter
28 import android.util.Log
29 import android.view.View
30 import androidx.annotation.VisibleForTesting
31 import androidx.lifecycle.lifecycleScope
32 import androidx.preference.Preference
33 import androidx.preference.PreferenceCategory
34 import androidx.preference.PreferenceGroup
35 import com.android.settings.R
36 import com.android.settings.dashboard.RestrictedDashboardFragment
37 import com.android.settingslib.bluetooth.BluetoothCallback
38 import com.android.settingslib.bluetooth.BluetoothDeviceFilter
39 import com.android.settingslib.bluetooth.BluetoothUtils
40 import com.android.settingslib.bluetooth.CachedBluetoothDevice
41 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager
42 import com.android.settingslib.bluetooth.LocalBluetoothManager
43 import com.android.settingslib.flags.Flags
44 import java.util.concurrent.ConcurrentHashMap
45 import kotlinx.coroutines.CoroutineScope
46 import kotlinx.coroutines.Dispatchers
47 import kotlinx.coroutines.launch
48 import kotlinx.coroutines.withContext
49 
50 /**
51  * Parent class for settings fragments that contain a list of Bluetooth devices.
52  *
53  * @see DevicePickerFragment
54  *
55  * TODO: Refactor this fragment
56  */
57 abstract class DeviceListPreferenceFragment(restrictedKey: String?) :
58     RestrictedDashboardFragment(restrictedKey), BluetoothCallback {
59 
60     enum class ScanType {
61         CLASSIC, LE
62     }
63 
64     private var scanType = ScanType.CLASSIC
65     private var filter: BluetoothDeviceFilter.Filter = BluetoothDeviceFilter.ALL_FILTER
66     private var leScanFilters: List<ScanFilter>? = null
67 
68     @JvmField
69     @VisibleForTesting
70     var mScanEnabled = false
71 
72     @JvmField
73     var mSelectedDevice: BluetoothDevice? = null
74 
75     @JvmField
76     var mBluetoothAdapter: BluetoothAdapter? = null
77 
78     @JvmField
79     var mLocalManager: LocalBluetoothManager? = null
80 
81     @JvmField
82     var mCachedDeviceManager: CachedBluetoothDeviceManager? = null
83 
84     @JvmField
85     @VisibleForTesting
86     var mDeviceListGroup: PreferenceGroup? = null
87 
88     @VisibleForTesting
89     val devicePreferenceMap =
90         ConcurrentHashMap<CachedBluetoothDevice, BluetoothDevicePreference>()
91 
92     @JvmField
93     val mSelectedList: MutableList<BluetoothDevice> = ArrayList()
94 
95     @VisibleForTesting
96     var lifecycleScope: CoroutineScope? = null
97 
98     private var showDevicesWithoutNames = false
99 
setFilternull100     protected fun setFilter(filterType: Int) {
101         this.scanType = ScanType.CLASSIC
102         this.filter = BluetoothDeviceFilter.getFilter(filterType)
103     }
104 
105     /**
106      * Sets the bluetooth device scanning filter with [ScanFilter]s. It will change to start
107      * [BluetoothLeScanner] which will scan BLE device only.
108      *
109      * @param leScanFilters list of settings to filter scan result
110      */
setFilternull111     fun setFilter(leScanFilters: List<ScanFilter>?) {
112         this.scanType = ScanType.LE
113         this.leScanFilters = leScanFilters
114     }
115 
onCreatenull116     override fun onCreate(savedInstanceState: Bundle?) {
117         super.onCreate(savedInstanceState)
118         mLocalManager = Utils.getLocalBtManager(activity)
119         if (mLocalManager == null) {
120             Log.e(TAG, "Bluetooth is not supported on this device")
121             return
122         }
123         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
124         mCachedDeviceManager = mLocalManager!!.cachedDeviceManager
125         showDevicesWithoutNames = SystemProperties.getBoolean(
126             BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false
127         )
128         initPreferencesFromPreferenceScreen()
129         mDeviceListGroup = findPreference<Preference>(deviceListKey) as PreferenceCategory
130     }
131 
132     /** find and update preference that already existed in preference screen  */
initPreferencesFromPreferenceScreennull133     protected abstract fun initPreferencesFromPreferenceScreen()
134 
135     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
136         super.onViewCreated(view, savedInstanceState)
137         lifecycleScope = viewLifecycleOwner.lifecycleScope
138     }
139 
onStartnull140     override fun onStart() {
141         super.onStart()
142         if (mLocalManager == null || isUiRestricted) return
143         mLocalManager!!.foregroundActivity = activity
144         mLocalManager!!.eventManager.registerCallback(this)
145     }
146 
onStopnull147     override fun onStop() {
148         super.onStop()
149         if (mLocalManager == null || isUiRestricted) {
150             return
151         }
152         removeAllDevices()
153         mLocalManager!!.foregroundActivity = null
154         mLocalManager!!.eventManager.unregisterCallback(this)
155     }
156 
removeAllDevicesnull157     fun removeAllDevices() {
158         devicePreferenceMap.clear()
159         mDeviceListGroup!!.removeAll()
160     }
161 
162     @JvmOverloads
addCachedDevicesnull163     fun addCachedDevices(filterForCachedDevices: BluetoothDeviceFilter.Filter? = null) {
164         lifecycleScope?.launch {
165             withContext(Dispatchers.Default) {
166                 mCachedDeviceManager!!.cachedDevicesCopy
167                     .filter {
168                         filterForCachedDevices == null || filterForCachedDevices.matches(it.device)
169                     }
170                     .forEach(::onDeviceAdded)
171             }
172         }
173     }
174 
onPreferenceTreeClicknull175     override fun onPreferenceTreeClick(preference: Preference): Boolean {
176         if (KEY_BT_SCAN == preference.key) {
177             startScanning()
178             return true
179         }
180         if (preference is BluetoothDevicePreference) {
181             val device = preference.cachedDevice.device
182             mSelectedDevice = device
183             mSelectedList.add(device)
184             onDevicePreferenceClick(preference)
185             return true
186         }
187         return super.onPreferenceTreeClick(preference)
188     }
189 
onDevicePreferenceClicknull190     protected open fun onDevicePreferenceClick(btPreference: BluetoothDevicePreference) {
191         btPreference.onClicked()
192     }
193 
onDeviceAddednull194     override fun onDeviceAdded(cachedDevice: CachedBluetoothDevice) {
195         lifecycleScope?.launch {
196             addDevice(cachedDevice)
197         }
198     }
199 
addDevicenull200     private suspend fun addDevice(cachedDevice: CachedBluetoothDevice) =
201         withContext(Dispatchers.Default) {
202             if (mBluetoothAdapter!!.state != BluetoothAdapter.STATE_ON) {
203                 // Prevent updates while the list shows one of the state messages
204                 return@withContext
205             }
206             // LE filters was already applied at scan time. We just need to check if the classic
207             // filter matches
208             if (scanType == ScanType.LE
209                 || (scanType == ScanType.CLASSIC && filter.matches(cachedDevice.device) == true)) {
210                 createDevicePreference(cachedDevice)
211             }
212         }
213 
createDevicePreferencenull214     private suspend fun createDevicePreference(cachedDevice: CachedBluetoothDevice) {
215         if (mDeviceListGroup == null) {
216             Log.w(
217                 TAG,
218                 "Trying to create a device preference before the list group/category exists!",
219             )
220             return
221         }
222         if (Flags.enableHideExclusivelyManagedBluetoothDevice()) {
223             if (cachedDevice.device.bondState == BluetoothDevice.BOND_BONDED
224                 && BluetoothUtils.isExclusivelyManagedBluetoothDevice(
225                     prefContext, cachedDevice.device)) {
226                 Log.d(TAG, "Trying to create preference for a exclusively managed device")
227                 return
228             }
229         }
230         // Only add device preference when it's not found in the map and there's no other state
231         // message showing in the list
232         val preference = devicePreferenceMap.computeIfAbsent(cachedDevice) {
233             BluetoothDevicePreference(
234                 prefContext,
235                 cachedDevice,
236                 showDevicesWithoutNames,
237                 BluetoothDevicePreference.SortType.TYPE_FIFO,
238             ).apply {
239                 key = cachedDevice.device.address
240                 //Set hideSecondTarget is true if it's bonded device.
241                 hideSecondTarget(true)
242             }
243         }
244         withContext(Dispatchers.Main) {
245             mDeviceListGroup!!.addPreference(preference)
246             initDevicePreference(preference)
247         }
248     }
249 
initDevicePreferencenull250     protected open fun initDevicePreference(preference: BluetoothDevicePreference?) {
251         // Does nothing by default
252     }
253 
254     @VisibleForTesting
updateFooterPreferencenull255     fun updateFooterPreference(myDevicePreference: Preference) {
256         val bidiFormatter = BidiFormatter.getInstance()
257         myDevicePreference.title = getString(
258             R.string.bluetooth_footer_mac_message,
259             bidiFormatter.unicodeWrap(mBluetoothAdapter!!.address)
260         )
261     }
262 
onDeviceDeletednull263     override fun onDeviceDeleted(cachedDevice: CachedBluetoothDevice) {
264         devicePreferenceMap.remove(cachedDevice)?.let {
265             mDeviceListGroup!!.removePreference(it)
266         }
267     }
268 
269     @VisibleForTesting
enableScanningnull270     open fun enableScanning() {
271         // BluetoothAdapter already handles repeated scan requests
272         if (!mScanEnabled) {
273             startScanning()
274             mScanEnabled = true
275         }
276     }
277 
278     @VisibleForTesting
disableScanningnull279     fun disableScanning() {
280         if (mScanEnabled) {
281             stopScanning()
282             mScanEnabled = false
283         }
284     }
285 
onScanningStateChangednull286     override fun onScanningStateChanged(started: Boolean) {
287         if (!started && mScanEnabled) {
288             startScanning()
289         }
290     }
291 
292     /**
293      * Return the key of the [PreferenceGroup] that contains the bluetooth devices
294      */
295     abstract val deviceListKey: String
296 
297     @VisibleForTesting
startScanningnull298     open fun startScanning() {
299         if (scanType == ScanType.LE) {
300             startLeScanning()
301         } else {
302             startClassicScanning()
303         }
304     }
305 
306     @VisibleForTesting
stopScanningnull307     open fun stopScanning() {
308         if (scanType == ScanType.LE) {
309             stopLeScanning()
310         } else {
311             stopClassicScanning()
312         }
313     }
314 
startClassicScanningnull315     private fun startClassicScanning() {
316         if (!mBluetoothAdapter!!.isDiscovering) {
317             mBluetoothAdapter!!.startDiscovery()
318         }
319     }
320 
stopClassicScanningnull321     private fun stopClassicScanning() {
322         if (mBluetoothAdapter!!.isDiscovering) {
323             mBluetoothAdapter!!.cancelDiscovery()
324         }
325     }
326 
327     private val leScanCallback = object : ScanCallback() {
onScanResultnull328         override fun onScanResult(callbackType: Int, result: ScanResult) {
329             handleLeScanResult(result)
330         }
331 
onBatchScanResultsnull332         override fun onBatchScanResults(results: MutableList<ScanResult>?) {
333             for (result in results.orEmpty()) {
334                 handleLeScanResult(result)
335             }
336         }
337 
onScanFailednull338         override fun onScanFailed(errorCode: Int) {
339             Log.w(TAG, "BLE Scan failed with error code $errorCode")
340         }
341     }
342 
startLeScanningnull343     private fun startLeScanning() {
344         val scanner = mBluetoothAdapter!!.bluetoothLeScanner
345         val settings = ScanSettings.Builder()
346             .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
347             .build()
348         scanner.startScan(leScanFilters, settings, leScanCallback)
349     }
350 
stopLeScanningnull351     private fun stopLeScanning() {
352         val scanner = mBluetoothAdapter!!.bluetoothLeScanner
353         scanner?.stopScan(leScanCallback)
354     }
355 
handleLeScanResultnull356     private fun handleLeScanResult(result: ScanResult) {
357         lifecycleScope?.launch {
358             withContext(Dispatchers.Default) {
359                 val device = result.device
360                 val cachedDevice = mCachedDeviceManager!!.findDevice(device)
361                     ?: mCachedDeviceManager!!.addDevice(device, leScanFilters)
362                 addDevice(cachedDevice)
363             }
364         }
365     }
366 
367     companion object {
368         private const val TAG = "DeviceListPreferenceFragment"
369         private const val KEY_BT_SCAN = "bt_scan"
370 
371         // Copied from BluetoothDeviceNoNamePreferenceController.java
372         private const val BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY =
373             "persist.bluetooth.showdeviceswithoutnames"
374     }
375 }
376