Scanning Bluetooth Low Energy in Background Android 11

I’m developing an app in which I want to continuously scan for Ble advertisement packets, even if the user locks the screen. With my current implementation this works fine with Android 10, but with Android 11 it stops once the user locks the screen. For scanning Ble packets I first request a few permissions, namely:

  • coarse and fine location
  • bluetooth scan
  • access background location

I start a simple foreground service (also added foreground service permission to my Manifest) with:

    private fun startBleService() {
        serviceIntent = Intent(baseContext, ScanService::class.java)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            Log.i("Background Act", "Starting foreground service for android 8.0+")
            applicationContext.startForegroundService(serviceIntent)
        } else {
            Log.i("Background Act", "Starting foreground service for versions < android 8.0")
            applicationContext.startService(serviceIntent)
        }
    }

This will call startForeground(notificationID, notification) in the onStartCommand function of my ScanService, thus requesting to run in foreground. After this I start the actual Ble scan functionalities. I also added android:foregroundServiceType="location" to the service in the Manifest.

My ScanService Code:

class ScanService : Service() {

    private val channelID = "CustomChannelID"
    private val notificationID = 7

    private lateinit var bluetoothManager: BluetoothManager
    private lateinit var bluetoothAdapter: BluetoothAdapter
    private lateinit var bluetoothLeScanner: BluetoothLeScanner

    private var scanCounter = 0

    private var _bleSingleScanResults = HashMap<String, MutableList<String>>()
    // all scans combined in an array, internally used
    private var _bleAllScanResults = arrayListOf<HashMap<String, MutableList<String>>>()

    // only starts a new scan if its not already scanning
    private var scanning = false

    private val notificationManager by lazy {getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager}
    
    @RequiresApi(Build.VERSION_CODES.M)
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.i("OnStartCommand Service", "Is started")
        val notification: Notification =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                createChannel(notificationManager)
                Notification.Builder(this, channelID)
                    .setContentTitle("Content Title")
                    .setContentText("Content Text")
                    .setTicker("Ticker")
                    .build()
            } else {
                NotificationCompat.Builder(this, channelID)
                    .setContentTitle("BLE Scanning Service")
                    .setContentText("Scanning BLE in the background")
                    .setTicker("Ticker")
                    .build()
            }

        // use custom non-zero notification ID
        startForeground(notificationID, notification)

        //TODO: start in new thread
        scanBle()

        // If we get killed, after returning from here, restart, recreating notification again though
        return START_STICKY
    }
    
    @RequiresApi(Build.VERSION_CODES.M)
    fun scanBle() {
        bluetoothManager = this.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        bluetoothAdapter = bluetoothManager.adapter
        if (bluetoothAdapter.bluetoothLeScanner == null) {
            Log.d("BLE", "Device doesn't support BLE")
            Toast.makeText(
                this,
                "It seems like your device does not support BLE. This is a crucial part of this app. n " +
                        "Unfortunately you can't contribute to the dataset of scanned locations.",
                Toast.LENGTH_LONG
            ).show()
            return
        }
        bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
        runBLEScan()
    }

    @SuppressLint("MissingPermission") //since we check beforehand in the MainActivity for permissions already
    private fun runBLEScan() {
        val scanSettings = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .build()
        val scanFilters = listOf(ScanFilter.Builder().build())
        if (!scanning) {
            scanning = true
            Log.i("BLE", "--- STARTING BLE SCAN ---")
            bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallBackLe)
        } else Log.d("BLE Scan", "Called scanning function but is currently already scanning!")
    }

    // ALWAYS ON UI-THREAD
    private val scanCallBackLe = object : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            super.onScanResult(callbackType, result)

            // discard result if payload is null
            if(result.scanRecord == null || result.scanRecord!!.bytes == null) {
                return
            }
            println("Payload: ${result.scanRecord?.bytes?.toHexString()}")
            
            // check if device already discovered in a scan, if so increase counter, else make new
            // entry in the result HashMap
            if (_bleSingleScanResults.isEmpty() || !_bleSingleScanResults.containsKey(result.device.toString())) {
                // device wasn't seen before
                _bleSingleScanResults[result.device.toString()] =
                    mutableListOf(result.rssi.toString(), result.scanRecord!!.bytes.toHexString(), "1")
            } else {
                // update already existing entry
                val cntr = _bleSingleScanResults[result.device.toString()]!![2].toInt() + 1
                _bleSingleScanResults[result.device.toString()]!![2] = cntr.toString()
            }
        }

        override fun onScanFailed(errorCode: Int) {
            super.onScanFailed(errorCode)
            Log.d("BLE ScanResult", "Scan failed code: $errorCode")
        }
    }

    private fun ByteArray.toHexString() = joinToString("",  "[0x",  "]") { "%02X".format(it) }

    private fun createChannel(notificationManager: NotificationManager) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return
        }
        val channel =
            NotificationChannel(channelID, "Scan Service", NotificationManager.IMPORTANCE_DEFAULT)
        channel.description = "Hello! This is a notification."
        notificationManager.createNotificationChannel(channel)
    }

    @SuppressLint("MissingPermission")
    override fun onDestroy() {
        Log.d("Destroyed Service", "That's even worse")
        bluetoothLeScanner.stopScan(scanCallBackLe)
        super.onDestroy()
    }

    @SuppressLint("MissingPermission")
    override fun stopService(name: Intent?): Boolean {
        Log.d("Stopped Service", "That's bad")
        bluetoothLeScanner.stopScan(scanCallBackLe)
        stopSelf()
        return super.stopService(name)
    }
    
    override fun onBind(intent: Intent): IBinder {
        TODO("Return the communication channel to the service.")
    }

Parts of my Manifest:

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<application
        ...
        <service
            android:name=".ScanService"
            android:enabled="true"
            android:exported="false"
            android:foregroundServiceType="location" />
        ...
</application>

This works fine with Android 10 (tested on a Huawei device), but unfortunately not on Android 11 (Samsung A22). Is there any other permission I need to be able to keep scanning even if the user locks the screen in Android 11?

Leave a Comment