0

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 ScanCallbacks. 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.

  • The `BluetoothAdapter` and `BluetoothLeScanner` objects are singletons, so you can re-obtain a reference to these whenever you want and do not need to store the reference. – Emil Aug 02 '23 at 14:18
  • @Emil hi, sorry if didn't understand your comment, I'm new to android coding. I checked the references of `BluetoothManager`, `BluetoothAdapter` and `BluetoothLeScanner` before and after configuration change and they are different. For example, `android.bluetooth.BluetoothManager@d626111` before and `android.bluetooth.BluetoothManager@de98ef9` after. – l'hiverviendra Aug 02 '23 at 15:14
  • They are probably different, because on configuration change `Activity` is destroyed and recreated, and so its `Context` also recreated. And `BluetoothManager` is tied to a `Context`, so this is why they are recreated. – l'hiverviendra Aug 02 '23 at 16:02

0 Answers0