• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.CachedBluetoothDevice
40 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager
41 import com.android.settingslib.bluetooth.LocalBluetoothManager
42 import java.util.concurrent.ConcurrentHashMap
43 import kotlinx.coroutines.CoroutineScope
44 import kotlinx.coroutines.Dispatchers
45 import kotlinx.coroutines.launch
46 import kotlinx.coroutines.withContext
47 
48 /**
49  * Parent class for settings fragments that contain a list of Bluetooth devices.
50  *
51  * @see DevicePickerFragment
52  *
53  * TODO: Refactor this fragment
54  */
55 abstract class DeviceListPreferenceFragment(restrictedKey: String?) :
56     RestrictedDashboardFragment(restrictedKey), BluetoothCallback {
57 
58     private var filter: BluetoothDeviceFilter.Filter? = BluetoothDeviceFilter.ALL_FILTER
59     private var leScanFilters: List<ScanFilter>? = null
60 
61     @JvmField
62     @VisibleForTesting
63     var mScanEnabled = false
64 
65     @JvmField
66     var mSelectedDevice: BluetoothDevice? = null
67 
68     @JvmField
69     var mBluetoothAdapter: BluetoothAdapter? = null
70 
71     @JvmField
72     var mLocalManager: LocalBluetoothManager? = null
73 
74     @JvmField
75     var mCachedDeviceManager: CachedBluetoothDeviceManager? = null
76 
77     @JvmField
78     @VisibleForTesting
79     var mDeviceListGroup: PreferenceGroup? = null
80 
81     @VisibleForTesting
82     val devicePreferenceMap =
83         ConcurrentHashMap<CachedBluetoothDevice, BluetoothDevicePreference>()
84 
85     @JvmField
86     val mSelectedList: MutableList<BluetoothDevice> = ArrayList()
87 
88     @VisibleForTesting
89     var lifecycleScope: CoroutineScope? = null
90 
91     private var showDevicesWithoutNames = false
92 
setFilternull93     protected fun setFilter(filterType: Int) {
94         filter = BluetoothDeviceFilter.getFilter(filterType)
95     }
96 
97     /**
98      * Sets the bluetooth device scanning filter with [ScanFilter]s. It will change to start
99      * [BluetoothLeScanner] which will scan BLE device only.
100      *
101      * @param leScanFilters list of settings to filter scan result
102      */
setFilternull103     fun setFilter(leScanFilters: List<ScanFilter>?) {
104         filter = null
105         this.leScanFilters = leScanFilters
106     }
107 
onCreatenull108     override fun onCreate(savedInstanceState: Bundle?) {
109         super.onCreate(savedInstanceState)
110         mLocalManager = Utils.getLocalBtManager(activity)
111         if (mLocalManager == null) {
112             Log.e(TAG, "Bluetooth is not supported on this device")
113             return
114         }
115         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
116         mCachedDeviceManager = mLocalManager!!.cachedDeviceManager
117         showDevicesWithoutNames = SystemProperties.getBoolean(
118             BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false
119         )
120         initPreferencesFromPreferenceScreen()
121         mDeviceListGroup = findPreference<Preference>(deviceListKey) as PreferenceCategory
122     }
123 
124     /** find and update preference that already existed in preference screen  */
initPreferencesFromPreferenceScreennull125     protected abstract fun initPreferencesFromPreferenceScreen()
126 
127     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
128         super.onViewCreated(view, savedInstanceState)
129         lifecycleScope = viewLifecycleOwner.lifecycleScope
130     }
131 
onStartnull132     override fun onStart() {
133         super.onStart()
134         if (mLocalManager == null || isUiRestricted) return
135         mLocalManager!!.foregroundActivity = activity
136         mLocalManager!!.eventManager.registerCallback(this)
137     }
138 
onStopnull139     override fun onStop() {
140         super.onStop()
141         if (mLocalManager == null || isUiRestricted) {
142             return
143         }
144         removeAllDevices()
145         mLocalManager!!.foregroundActivity = null
146         mLocalManager!!.eventManager.unregisterCallback(this)
147     }
148 
removeAllDevicesnull149     fun removeAllDevices() {
150         devicePreferenceMap.clear()
151         mDeviceListGroup!!.removeAll()
152     }
153 
154     @JvmOverloads
addCachedDevicesnull155     fun addCachedDevices(filterForCachedDevices: BluetoothDeviceFilter.Filter? = null) {
156         lifecycleScope?.launch {
157             withContext(Dispatchers.Default) {
158                 mCachedDeviceManager!!.cachedDevicesCopy
159                     .filter {
160                         filterForCachedDevices == null || filterForCachedDevices.matches(it.device)
161                     }
162                     .forEach(::onDeviceAdded)
163             }
164         }
165     }
166 
onPreferenceTreeClicknull167     override fun onPreferenceTreeClick(preference: Preference): Boolean {
168         if (KEY_BT_SCAN == preference.key) {
169             startScanning()
170             return true
171         }
172         if (preference is BluetoothDevicePreference) {
173             val device = preference.cachedDevice.device
174             mSelectedDevice = device
175             mSelectedList.add(device)
176             onDevicePreferenceClick(preference)
177             return true
178         }
179         return super.onPreferenceTreeClick(preference)
180     }
181 
onDevicePreferenceClicknull182     protected open fun onDevicePreferenceClick(btPreference: BluetoothDevicePreference) {
183         btPreference.onClicked()
184     }
185 
onDeviceAddednull186     override fun onDeviceAdded(cachedDevice: CachedBluetoothDevice) {
187         lifecycleScope?.launch {
188             addDevice(cachedDevice)
189         }
190     }
191 
addDevicenull192     private suspend fun addDevice(cachedDevice: CachedBluetoothDevice) =
193         withContext(Dispatchers.Default) {
194             // TODO(b/289189853): Replace checking if `filter` is null or not to decide which type
195             // of Bluetooth scanning method will be used
196             val filterMatched = filter == null || filter!!.matches(cachedDevice.device) == true
197             // Prevent updates while the list shows one of the state messages
198             if (mBluetoothAdapter!!.state == BluetoothAdapter.STATE_ON && filterMatched) {
199                 createDevicePreference(cachedDevice)
200             }
201         }
202 
createDevicePreferencenull203     private suspend fun createDevicePreference(cachedDevice: CachedBluetoothDevice) {
204         if (mDeviceListGroup == null) {
205             Log.w(
206                 TAG,
207                 "Trying to create a device preference before the list group/category exists!",
208             )
209             return
210         }
211         // Only add device preference when it's not found in the map and there's no other state
212         // message showing in the list
213         val preference = devicePreferenceMap.computeIfAbsent(cachedDevice) {
214             BluetoothDevicePreference(
215                 prefContext,
216                 cachedDevice,
217                 showDevicesWithoutNames,
218                 BluetoothDevicePreference.SortType.TYPE_FIFO,
219             ).apply {
220                 key = cachedDevice.device.address
221                 //Set hideSecondTarget is true if it's bonded device.
222                 hideSecondTarget(true)
223             }
224         }
225         withContext(Dispatchers.Main) {
226             mDeviceListGroup!!.addPreference(preference)
227             initDevicePreference(preference)
228         }
229     }
230 
initDevicePreferencenull231     protected open fun initDevicePreference(preference: BluetoothDevicePreference?) {
232         // Does nothing by default
233     }
234 
235     @VisibleForTesting
updateFooterPreferencenull236     fun updateFooterPreference(myDevicePreference: Preference) {
237         val bidiFormatter = BidiFormatter.getInstance()
238         myDevicePreference.title = getString(
239             R.string.bluetooth_footer_mac_message,
240             bidiFormatter.unicodeWrap(mBluetoothAdapter!!.address)
241         )
242     }
243 
onDeviceDeletednull244     override fun onDeviceDeleted(cachedDevice: CachedBluetoothDevice) {
245         devicePreferenceMap.remove(cachedDevice)?.let {
246             mDeviceListGroup!!.removePreference(it)
247         }
248     }
249 
250     @VisibleForTesting
enableScanningnull251     open fun enableScanning() {
252         // BluetoothAdapter already handles repeated scan requests
253         if (!mScanEnabled) {
254             startScanning()
255             mScanEnabled = true
256         }
257     }
258 
259     @VisibleForTesting
disableScanningnull260     fun disableScanning() {
261         if (mScanEnabled) {
262             stopScanning()
263             mScanEnabled = false
264         }
265     }
266 
onScanningStateChangednull267     override fun onScanningStateChanged(started: Boolean) {
268         if (!started && mScanEnabled) {
269             startScanning()
270         }
271     }
272 
273     /**
274      * Return the key of the [PreferenceGroup] that contains the bluetooth devices
275      */
276     abstract val deviceListKey: String
277 
278     @VisibleForTesting
startScanningnull279     open fun startScanning() {
280         if (filter != null) {
281             startClassicScanning()
282         } else if (leScanFilters != null) {
283             startLeScanning()
284         }
285     }
286 
287     @VisibleForTesting
stopScanningnull288     open fun stopScanning() {
289         if (filter != null) {
290             stopClassicScanning()
291         } else if (leScanFilters != null) {
292             stopLeScanning()
293         }
294     }
295 
startClassicScanningnull296     private fun startClassicScanning() {
297         if (!mBluetoothAdapter!!.isDiscovering) {
298             mBluetoothAdapter!!.startDiscovery()
299         }
300     }
301 
stopClassicScanningnull302     private fun stopClassicScanning() {
303         if (mBluetoothAdapter!!.isDiscovering) {
304             mBluetoothAdapter!!.cancelDiscovery()
305         }
306     }
307 
308     private val leScanCallback = object : ScanCallback() {
onScanResultnull309         override fun onScanResult(callbackType: Int, result: ScanResult) {
310             handleLeScanResult(result)
311         }
312 
onBatchScanResultsnull313         override fun onBatchScanResults(results: MutableList<ScanResult>?) {
314             for (result in results.orEmpty()) {
315                 handleLeScanResult(result)
316             }
317         }
318 
onScanFailednull319         override fun onScanFailed(errorCode: Int) {
320             Log.w(TAG, "BLE Scan failed with error code $errorCode")
321         }
322     }
323 
startLeScanningnull324     private fun startLeScanning() {
325         val scanner = mBluetoothAdapter!!.bluetoothLeScanner
326         val settings = ScanSettings.Builder()
327             .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
328             .build()
329         scanner.startScan(leScanFilters, settings, leScanCallback)
330     }
331 
stopLeScanningnull332     private fun stopLeScanning() {
333         val scanner = mBluetoothAdapter!!.bluetoothLeScanner
334         scanner?.stopScan(leScanCallback)
335     }
336 
handleLeScanResultnull337     private fun handleLeScanResult(result: ScanResult) {
338         lifecycleScope?.launch {
339             withContext(Dispatchers.Default) {
340                 val device = result.device
341                 val cachedDevice = mCachedDeviceManager!!.findDevice(device)
342                     ?: mCachedDeviceManager!!.addDevice(device, leScanFilters)
343                 addDevice(cachedDevice)
344             }
345         }
346     }
347 
348     companion object {
349         private const val TAG = "DeviceListPreferenceFragment"
350         private const val KEY_BT_SCAN = "bt_scan"
351 
352         // Copied from BluetoothDeviceNoNamePreferenceController.java
353         private const val BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY =
354             "persist.bluetooth.showdeviceswithoutnames"
355     }
356 }
357