DEV Community

Ble Advertiser
Ble Advertiser

Posted on

Why Your Android BLE Advertisements Silently Fail in the Background on Android 12+ and How to Fix It

You've meticulously crafted your BLE peripheral, tested it in the foreground, only to find its advertisements vanish into thin air the moment your app goes to the background on a modern Android device. Sound familiar? You’re not alone. This insidious "silent failure" of BluetoothLeAdvertiser in the background on Android 12 (API 31) and newer is a common pain point, particularly for developers building reliable embedded and IoT connectivity solutions.

This article dissects the root causes behind these disappearing advertisements. We'll explore the critical changes in Android's background execution model, demystify the new permission landscape, and provide a robust strategy, complete with practical code examples, to ensure your BLE advertisements persist when your app is not actively in the foreground.

Core Concepts: The Shifting Sands of Android Background Execution

To understand why your advertisements are failing, you must first grasp the fundamental shift in how Android 12+ handles background work. Google's continuous push for improved battery life and user privacy has led to stricter controls over what apps can do when not actively in use.

1. Android 12+ Background Execution Limits:
Since Android 8 (Oreo), background execution limits have progressively tightened. Android 12 and 13 further restrict background operations, including network access, service starts, and even the lifespan of services. When your app goes into the background, the system actively monitors and often curtails its activities to conserve resources. Your BluetoothLeAdvertiser is not exempt from these restrictions. The system can silently stop your advertisements even if onStartSuccess was initially reported.

2. The New Permission Landscape (API 31+):
Prior to Android 12, the BLUETOOTH_ADMIN permission was largely sufficient for both scanning and advertising. Android 12 (API 31) introduced more granular Bluetooth permissions:

  • BLUETOOTH_CONNECT: Required for connecting to BLE devices.
  • BLUETOOTH_SCAN: Required for scanning for BLE devices.
  • BLUETOOTH_ADVERTISE: This is critical for us. It's a new runtime permission specifically granting the ability to advertise.

While BLUETOOTH_ADVERTISE grants your app the ability to advertise, it does not grant exemption from background execution limits. This is a crucial distinction. Obtaining BLUETOOTH_ADVERTISE does not magically make your background advertisements persist indefinitely.

3. Foreground Services: The Last Bastion of Persistent Background Work:
For an app to perform continuous, user-facing operations in the background, a Foreground Service is almost always required. A Foreground Service is a service that the user is aware of, often indicated by a persistent notification. It tells the system: "Hey, I'm doing something important that the user needs, don't kill me!" If your BluetoothLeAdvertiser is not managed by a Foreground Service, its lifespan in the background will be extremely limited, leading to silent termination.

4. SYSTEM_EXEMPT_FROM_BG_RESTRICTIONS and USE_BLUETOOTH_ADVERTISE_ALWAYS (OEM/System Apps Only):
You might encounter references to these permissions. It's vital to understand that:

  • SYSTEM_EXEMPT_FROM_BG_RESTRICTIONS: This permission exempts an app from general background execution limits. It is typically reserved for system-level apps or apps explicitly granted by the device manufacturer (OEM). You, as a standard app developer, cannot request or obtain this permission.
  • USE_BLUETOOTH_ADVERTISE_ALWAYS: This permission, available for apps with the SYSTEM_EXEMPT_FROM_BG_RESTRICTIONS privilege, explicitly allows persistent, always-on Bluetooth advertising regardless of the app's background state. Again, this is not available for general developer apps.

This means that for the vast majority of applications, achieving truly "always-on" background advertising without any user indication is no longer a viable strategy on Android 12+. The "fix" involves working within these constraints, primarily by using Foreground Service and careful resource management.

Here's a quick summary of permission requirements across relevant Android versions for advertising:

Android Version Manifest Permissions Runtime Permissions Background Advertising Strategy
< API 31 BLUETOOTH, BLUETOOTH_ADMIN, ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION (if needed) Works with Service; no strict foreground service requirement solely for advertising.
API 31+ BLUETOOTH, BLUETOOTH_ADMIN, BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT, BLUETOOTH_SCAN, ACCESS_FINE_LOCATION (if needed), FOREGROUND_SERVICE BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT, BLUETOOTH_SCAN, ACCESS_FINE_LOCATION (if needed) Requires Foreground Service for persistent background operation; BLUETOOTH_ADVERTISE alone is insufficient.

The key takeaway is that for persistent background BLE advertising on Android 12+, your strategy MUST involve a Foreground Service.

Implementation: Building a Resilient Background Advertiser

To prevent your BLE advertisements from silently failing, you need to:

  1. Target API 31+: Ensure your targetSdkVersion is at least 31.
  2. Declare Permissions: Add all necessary Bluetooth permissions and the FOREGROUND_SERVICE permission to your AndroidManifest.xml.
  3. Implement a Foreground Service: This service will host and manage your BluetoothLeAdvertiser.
  4. Handle Permissions at Runtime: Request the new BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT, and BLUETOOTH_SCAN permissions at runtime.

Step 1: AndroidManifest.xml Configuration

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- Standard Bluetooth permissions (pre-Android 12) -->
    <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />

    <!-- Android 12+ Bluetooth permissions -->
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />

    <!-- Required for location-based advertising or if your app uses location in other ways -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <!-- Required for Foreground Services -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

    <!-- Required for Foreground Services of type connectedDevice.
         On Android 14+ (API 34), you must declare a foregroundServiceType for each foreground service.
         "connectedDevice" is appropriate for BLE advertising/connectivity. -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"
        tools:targetApi="34" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyBleApp">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <!-- Declare your Foreground Service -->
        <service
            android:name=".BleAdvertiserService"
            android:enabled="true"
            android:exported="false"
            android:foregroundServiceType="connectedDevice"
            tools:targetApi="34" />

    </application>
</manifest>
Enter fullscreen mode Exit fullscreen mode

Gotcha: On Android 14 (API 34) and higher, you must declare foregroundServiceType in your AndroidManifest.xml for each Foreground Service. For BLE connectivity and advertising, connectedDevice is the most appropriate type. You also need to declare the corresponding FOREGROUND_SERVICE_CONNECTED_DEVICE permission.

Step 2: Runtime Permission Handling (in your Activity/Fragment)

Before starting your BleAdvertiserService, you need to ensure all necessary runtime permissions are granted.

// In your MainActivity or a utility class
private val REQUEST_BLE_PERMISSIONS = 101

fun requestBlePermissions(activity: Activity) {
    val permissions = mutableListOf<String>()

    // Permissions for Android 12+
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Android 12 (API 31)
        permissions.add(Manifest.permission.BLUETOOTH_CONNECT)
        permissions.add(Manifest.permission.BLUETOOTH_ADVERTISE)
        permissions.add(Manifest.permission.BLUETOOTH_SCAN)
    } else { // Android 11 and below
        permissions.add(Manifest.permission.BLUETOOTH_ADMIN)
        permissions.add(Manifest.permission.BLUETOOTH)
    }

    // Always required for location services, and often for BLE scan results or advertising if location is implied
    permissions.add(Manifest.permission.ACCESS_FINE_LOCATION)

    val permissionsToRequest = permissions.filter {
        ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
    }.toTypedArray()

    if (permissionsToRequest.isNotEmpty()) {
        ActivityCompat.requestPermissions(activity, permissionsToRequest, REQUEST_BLE_PERMISSIONS)
    } else {
        Log.d("BLE_PERM", "All BLE permissions already granted.")
        // Permissions are granted, proceed to start service or advertise
        startBleAdvertiserService(activity)
    }
}

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == REQUEST_BLE_PERMISSIONS) {
        if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
            Log.d("BLE_PERM", "All requested BLE permissions granted.")
            startBleAdvertiserService(this)
        } else {
            Log.e("BLE_PERM", "Not all BLE permissions granted. Cannot advertise.")
            // Inform the user that the app functionality might be limited
        }
    }
}

fun startBleAdvertiserService(context: Context) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // Android 12+ requires BLUETOOTH_ADVERTISE
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_ADVERTISE) != PackageManager.PERMISSION_GRANTED) {
            Log.e("BLE_ADVERTISER", "BLUETOOTH_ADVERTISE permission not granted. Cannot start service.")
            return
        }
    } else { // Older Android versions need BLUETOOTH_ADMIN
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED) {
            Log.e("BLE_ADVERTISER", "BLUETOOTH_ADMIN permission not granted. Cannot start service.")
            return
        }
    }

    // All necessary permissions for the target API are granted, proceed to start service
    val serviceIntent = Intent(context, BleAdvertiserService::class.java)
    ContextCompat.startForegroundService(context, serviceIntent) // Use startForegroundService for API 26+
    Log.d("BLE_ADVERTISER", "Attempted to start BleAdvertiserService.")
}
Enter fullscreen mode Exit fullscreen mode

Step 3: BleAdvertiserService.kt – The Heart of Your Background Advertising

This service will initialize and manage the BluetoothLeAdvertiser.

package com.example.mybleapp

import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings
import android.bluetooth.le.BluetoothLeAdvertiser
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.IBinder
import android.os.ParcelUuid
import android.util.Log
import androidx.core.app.NotificationCompat
import java.util.UUID

class BleAdvertiserService : Service() {

    private val TAG = "BleAdvertiserService"
    private val NOTIFICATION_ID = 1234
    private val NOTIFICATION_CHANNEL_ID = "ble_advertiser_channel"
    private val NOTIFICATION_CHANNEL_NAME = "BLE Advertising Service"

    private var bluetoothAdapter: BluetoothAdapter? = null
    private var bleAdvertiser: BluetoothLeAdvertiser? = null
    private var advertising: Boolean = false

    // Unique Service UUID for our advertisement
    // This is how other devices will discover our service
    private val SERVICE_UUID = UUID.fromString("0000180A-0000-1000-8000-00805F9B34FB") // Example: Device Information Service

    private val advertiseCallback = object : AdvertiseCallback() {
        override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
            super.onStartSuccess(settingsInEffect)
            Log.i(TAG, "BLE Advertisement started successfully.")
            advertising = true
        }

        override fun onStartFailure(errorCode: Int) {
            super.onStartFailure(errorCode)
            advertising = false
            val errorMsg = when (errorCode) {
                AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED -> "ADVERTISE_FAILED_ALREADY_STARTED"
                AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> "ADVERTISE_FAILED_FEATURE_UNSUPPORTED"
                AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR -> "ADVERTISE_FAILED_INTERNAL_ERROR"
                AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS"
                AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE -> "ADVERTISE_FAILED_DATA_TOO_LARGE"
                else -> "UNKNOWN_ERROR ($errorCode)"
            }
            Log.e(TAG, "BLE Advertisement failed: $errorMsg")
            // Crucial: Handle failures. Consider stopping the service or attempting to restart with a delay.
            stopSelf() // Stop the service if advertising failed critically
        }
    }

    override fun onCreate() {
        super.onCreate()
        Log.d(TAG, "Service onCreate")
        initializeBluetooth()
    }

    // Suppress permission check lint warning; runtime checks handle this.
    @SuppressLint("MissingPermission")
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d(TAG, "Service onStartCommand")

        // Start the service as a Foreground Service
        startForeground(NOTIFICATION_ID, createNotification())

        if (!advertising) {
            startAdvertising()
        }

        // We want this service to continue running until it is explicitly stopped
        // or until the system decides to stop it for memory management, etc.
        return START_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "Service onDestroy")
        stopAdvertising() // Ensure advertising is stopped cleanly
        stopForeground(true) // Remove the notification and stop foreground state
    }

    override fun onBind(intent: Intent?): IBinder? {
        return null // Not providing binding for this service
    }

    private fun initializeBluetooth() {
        val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        bluetoothAdapter = bluetoothManager.adapter
        bleAdvertiser = bluetoothAdapter?.bluetoothLeAdvertiser

        if (bluetoothAdapter == null || !bluetoothAdapter!!.isEnabled) {
            Log.e(TAG, "Bluetooth is not available or not enabled.")
            stopSelf() // Cannot advertise without Bluetooth
            return
        }
        if (bleAdvertiser == null) {
            Log.e(TAG, "Bluetooth LE Advertiser is not available on this device.")
            stopSelf() // Cannot advertise if advertiser is null
            return
        }
    }

    @SuppressLint("MissingPermission") // Permissions are handled by runtime checks in MainActivity
    private fun startAdvertising() {
        if (!hasBleAdvertisePermission()) {
            Log.e(TAG, "BLUETOOTH_ADVERTISE permission not granted. Cannot start advertising.")
            return
        }

        if (bleAdvertiser == null) {
            Log.e(TAG, "Bluetooth LE Advertiser is null. Cannot start advertising.")
            return
        }

        Log.d(TAG, "Attempting to start BLE advertising...")

        val settings = AdvertiseSettings.Builder()
            .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_POWER) // Crucial for background!
            .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_LOW) // Conserve battery
            .setConnectable(true) // Allow other devices to connect
            .setTimeout(0) // No timeout, advertise indefinitely until stopped
            .build()

        val data = AdvertiseData.Builder()
            .setIncludeDeviceName(true) // Include local device name
            .addServiceUuid(ParcelUuid(SERVICE_UUID)) // Advertise our service UUID
            // You can add more service data or manufacturer data here
            // .addServiceData(ParcelUuid(SERVICE_UUID), "Hello".toByteArray())
            // .addManufacturerData(0x004C, byteArrayOf(0x02, 0x15, ...)) // Example for iBeacon
            .build()

        // Start advertising!
        bleAdvertiser?.startAdvertising(settings, data, advertiseCallback)
    }

    @SuppressLint("MissingPermission") // Permissions are handled by runtime checks in MainActivity
    private fun stopAdvertising() {
        if (!hasBleAdvertisePermission()) {
            Log.e(TAG, "BLUETOOTH_ADVERTISE permission not granted. Cannot stop advertising.")
            return
        }
        if (bleAdvertiser != null && advertising) {
            Log.d(TAG, "Stopping BLE advertising...")
            bleAdvertiser?.stopAdvertising(advertiseCallback)
            advertising = false
        }
    }

    private fun createNotification(): Notification {
        createNotificationChannel()
        return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
            .setContentTitle("BLE Advertising Active")
            .setContentText("Your device is advertising its presence via Bluetooth Low Energy.")
            .setSmallIcon(android.R.drawable.ic_dialog_info) // Replace with your app icon
            .setPriority(NotificationCompat.PRIORITY_LOW)
            .setCategory(Notification.CATEGORY_SERVICE)
            .build()
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                NOTIFICATION_CHANNEL_ID,
                NOTIFICATION_CHANNEL_NAME,
                NotificationManager.IMPORTANCE_LOW
            ).apply {
                description = "Notification for ongoing BLE advertising."
            }
            val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            manager.createNotificationChannel(channel)
        }
    }

    private fun hasBleAdvertisePermission(): Boolean {
        // For Android 12+, check BLUETOOTH_ADVERTISE
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            return ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADVERTISE) == PackageManager.PERMISSION_GRANTED
        }
        // For older versions, BLUETOOTH_ADMIN generally covers advertising.
        // It's good practice to also check BLUETOOTH if your app interacts with classic BT.
        return ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_GRANTED
    }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices for Reliable BLE Background Advertising

Even with a Foreground Service, simply flipping the switch isn't enough. Here are critical best practices to maximize your success and prevent silent failures:

  1. Pitfall: Relying on BLUETOOTH_ADVERTISE alone to guarantee background persistence.

    • Fix: Always use a Foreground Service to host your BluetoothLeAdvertiser if you require advertising while your app is in the background. Ensure the Foreground Service notification is informative and persistent. This makes your background work "user-visible" and less likely to be aggressively killed by the system. The system will eventually kill even Foreground Services if they consume too many resources or the device is under extreme memory pressure, but they significantly extend your app's lifespan compared to a regular background service. For Android 14+, correctly declare your foregroundServiceType in the manifest.
  2. Pitfall: Over-aggressive advertising parameters in the background.

    • Fix: Opt for ADVERTISE_MODE_LOW_POWER or ADVERTISE_MODE_BALANCED and ADVERTISE_TX_POWER_LOW for all background advertising. High-frequency, high-power advertising (e.g., ADVERTISE_MODE_LOW_LATENCY) will be aggressively throttled or rejected by the system in the background, leading to silent failures or short-lived advertisements. LOW_POWER mode is your best friend here, as it signals to the system that your advertising is battery-conscious.
  3. Pitfall: Not diligently managing the advertising lifecycle and checking AdvertiseCallback status.

    • Fix: Implement comprehensive logging for both onStartSuccess and onStartFailure. Crucially, understand that an onStartSuccess does not guarantee indefinite advertising; the system can still stop it later without calling onStartFailure if the app's process is killed. Consider implementing a periodic check (e.g., using WorkManager for very infrequent checks, or a simple Handler within your Foreground Service) to verify if advertising is still active. If not, attempt to restart it. Always call stopAdvertising() when advertising is no longer needed (e.g., when the user explicitly stops the feature, or when the app comes to the foreground and background advertising is no longer required) to conserve battery and system resources.
  4. Pitfall: Incorrectly handling Bluetooth adapter state changes.

    • Fix: Your BleAdvertiserService should register a BroadcastReceiver for BluetoothAdapter.ACTION_STATE_CHANGED. If Bluetooth is turned off while your service is running, your advertisements will stop. Your receiver should detect this and gracefully stop the service or restart advertising once Bluetooth is re-enabled.

Conclusion

Android's evolving background execution model, particularly on Android 12+, presents significant challenges for developers relying on persistent BLE advertisements. The era of fire-and-forget background advertising is over. To ensure your advertisements don't silently vanish, you must embrace Foreground Services, meticulously manage new runtime permissions, and configure your advertising parameters responsibly. Implement robust error handling and lifecycle management within your AdvertiseCallback to detect and react to failures.

Review your existing BLE peripheral apps and adjust your advertising strategy to conform to these new realities. Adopting these practices will drastically improve the reliability of your BLE solutions on modern Android devices.

Top comments (0)