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