I'm writing a simple Android app that scans for BLE (Bluetooth Low Energy) advertising packets and displays their contents on the screen. I have a Button on the screen that toggles BLE scanning: if scanning is not active, pressing the Button will start it; if it's active, it will stop it.
Starting and stopping the BLE scan works fine if no configuration change occurs. However if I start the scanning, then rotate my device and then try to stop it, nothing happens. That is, the scanning procedure will stay active and I will still be receiving ScanCallback
s. Pressing the Button again or rotating the device back and then pressing the Button also doesn't stop the scanning procedure.
Here's the code snippet that is responsible for BLE Scanning:
class MainActivity : ComponentActivity() {
private val mainViewModel: MainViewModel by viewModels()
private val bluetoothAdapter: BluetoothAdapter? by lazy {
val bluetoothManager: BluetoothManager =
getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter
}
private val bleScanner: BluetoothLeScanner? by lazy {
bluetoothAdapter?.bluetoothLeScanner
}
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
super.onScanResult(callbackType, result)
Log.d("ScanCallBack", "${result}")
}
}
@SuppressLint("MissingPermission")
private fun toggleScan() {
if (bleScanner == null) {
return
}
if (mainViewModel.scanActive) {
bleScanner?.stopScan(scanCallback)
} else {
bleScanner?.startScan(buildScanFilters(), buildScanSettings(), scanCallback)
}
mainViewModel.scanActive = !mainViewModel.scanActive
mainViewModel.updateScanActive(mainViewModel.scanActive)
}
}
Note: all the code related to enabling Bluetooth and granting Runtime permissions is not shown.
I read on this forum that in order to successfully stop the BLE scanning procedure, the stopScan
function needs to be passed the same ScanCallback
object, as the one that was passed into the startScan
function.
Because of the configuration change (screen rotation), MainActivity
will be recreated and all of its properties will be also reinitialized.
In my example, when I start the scanning, I pass one instance of ScanCallback
, but after the configuration change, when I stop the scanning, I pass a different instance of ScanCallback
.
I checked it using the Log.d()
function and they are indeed different.
I don't know the right way how to deal with it, so I temporarily put the ScanCallback
object into an existing ViewModel
, so it won't get destroyed and recreated after configuration change.
Now I pass ScanCallback
object located in the mainViewModel:
if (mainViewModel.scanActive) {
bleScanner?.stopScan(mainViewModel.scanCallback)
} else {
bleScanner?.startScan(buildScanFilters(), buildScanSettings(), mainViewModel.scanCallback)
}
I checked the references using Log.d
and they were indeed the same objects located at the same memory location even after configuration change. However doing this didn't help. Scanning didn't stop, I was still receving the callbacks.
So I thought, maybe BluetoothAdapter
and BluetoothLeScanner
variables are also not allowed to be recreated and their references must be kept the same.
At this point I don't know how to proceed further, because if I move these two variables into a ViewModel
, there's an error that no Context is provided to getSystemService()
function, probably because I'm calling it not from an Activity. And as I understand, moving Bluetooth-related objects to ViewModel
is not recommended anyway.
Thank you.
EDIT:
Solved this issue my retrieving BluetoothManager
from Application Context
, instead of Activity Context
.
private val bluetoothAdapter: BluetoothAdapter? by lazy {
val bluetoothManager: BluetoothManager =
applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothManager.adapter
}
This seemed to work. ScanCallback
object still needs to be placed in the ViewModel
though. Couldn't think of anything better.