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