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 theSYSTEM_EXEMPT_FROM_BG_RESTRICTIONSprivilege, 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:
- Target API 31+: Ensure your
targetSdkVersionis at least 31. - Declare Permissions: Add all necessary Bluetooth permissions and the
FOREGROUND_SERVICEpermission to yourAndroidManifest.xml. - Implement a Foreground Service: This service will host and manage your
BluetoothLeAdvertiser. - Handle Permissions at Runtime: Request the new
BLUETOOTH_ADVERTISE,BLUETOOTH_CONNECT, andBLUETOOTH_SCANpermissions 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>
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.")
}
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
}
}
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:
-
Pitfall: Relying on
BLUETOOTH_ADVERTISEalone to guarantee background persistence.- Fix: Always use a
Foreground Serviceto host yourBluetoothLeAdvertiserif you require advertising while your app is in the background. Ensure theForeground Servicenotification 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 yourforegroundServiceTypein the manifest.
- Fix: Always use a
-
Pitfall: Over-aggressive advertising parameters in the background.
- Fix: Opt for
ADVERTISE_MODE_LOW_POWERorADVERTISE_MODE_BALANCEDandADVERTISE_TX_POWER_LOWfor 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_POWERmode is your best friend here, as it signals to the system that your advertising is battery-conscious.
- Fix: Opt for
-
Pitfall: Not diligently managing the advertising lifecycle and checking
AdvertiseCallbackstatus.- Fix: Implement comprehensive logging for both
onStartSuccessandonStartFailure. Crucially, understand that anonStartSuccessdoes not guarantee indefinite advertising; the system can still stop it later without callingonStartFailureif the app's process is killed. Consider implementing a periodic check (e.g., usingWorkManagerfor very infrequent checks, or a simpleHandlerwithin your Foreground Service) to verify if advertising is still active. If not, attempt to restart it. Always callstopAdvertising()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.
- Fix: Implement comprehensive logging for both
-
Pitfall: Incorrectly handling Bluetooth adapter state changes.
- Fix: Your
BleAdvertiserServiceshould register aBroadcastReceiverforBluetoothAdapter.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.
- Fix: Your
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)