Why Android BLE needs explicit state management
Android exposes a common BLE API across devices, but the application still has to deal with process lifetime, permissions, peripheral firmware, radio conditions, and vendor-specific behavior. A connection that works on a Pixel beside the peripheral is not evidence that it will recover on a Samsung after the app spends an hour in the background.
The useful response is not a larger collection of delays and retries. It is a connection model with explicit states, bounded operations, observable failures, and tests on the devices and peripherals the product will actually support.
Scanning: make it bounded and purpose-specific
Android's BLE scanning guidance recommends stopping as soon as the target is found, never scanning in a loop, and setting a time limit because scanning is battery-intensive. Use a ScanFilter when the protocol gives you a stable service UUID or manufacturer-data signature. Device names are a weaker identifier because they may be absent, duplicated, or changed by firmware.
If discovery must wake a process that is not running, investigate a filtered PendingIntent scan or CompanionDeviceManager. A timer that repeatedly starts scans is not a substitute for choosing the background API that matches the product.
Connection management: coordinate asynchronous operations
Characteristic and descriptor operations are asynchronous: the call starts work and a later callback reports its outcome. Starting another operation before the current one completes can make application state ambiguous and may cause an initiation call to be rejected.
For protocols that exchange a sequence of reads, writes, and descriptor changes, use an application-level queue scoped to the current connection. Advance it from the corresponding callback, and treat an operation that was not accepted for initiation as a failure rather than waiting for a callback that will not come.
- Serialize operations whose order matters to the peripheral protocol.
- Give each pending operation a deadline so the state machine cannot wait indefinitely.
- Cancel work from the old connection when the connection generation changes.
- Retry only operations that are safe to repeat, with a bounded attempt count and backoff.
A queue is useful when it makes protocol order and callback ownership explicit. It is not a substitute for handling initiation failures, disconnects, and timeouts.
MTU: negotiate, observe, then size packets
The default ATT MTU is 23 bytes, leaving 20 bytes for a notification or write payload after the ATT header. Requesting a larger MTU does not guarantee that value. The application must use the result delivered to onMtuChanged and keep the peripheral's own limits in mind.
There is also a platform-version detail worth encoding in tests: the current BluetoothGatt.requestMtu documentation states that Android 14 requests an ATT MTU of 517 when the first GATT client asks, then disregards later MTU requests for that ACL connection. Do not build packet sizing around the integer passed into requestMtu.
Diagnosing a status 133 failure
Status 133 is a non-specific GATT failure reported by some Android BLE implementations. It does not identify a root cause by itself. Treat bonding state, overlapping connection attempts, link loss, peripheral firmware, and device-specific Bluetooth behaviour as hypotheses to test, not as meanings encoded in the status.
- Record the connection state, operation in flight, Bluetooth state, app lifecycle state, device model, OS build, and peripheral firmware.
- Close a failed
BluetoothGattinstance before creating a replacement and prevent overlapping connection attempts in the application state machine. - If a reset peripheral no longer matches the phone's bond state, guide the user through a supported unpair and pairing flow appropriate to the product.
- Do not ship reflection calls to hidden cache-refresh methods; they are outside the public Android API contract.
Treat status 133 as a failed operation, not a diagnosis. Capture the preceding state, close the stale BluetoothGatt, and retry only when the product can do so safely. Bound the retries and expose the terminal state to the user. Without the preceding events, radio state, device model, and peripheral logs, “GATT 133” does not tell you which layer failed.
Background BLE: choose the API from the user-visible task
A continuous, user-visible data session may justify a foreground service with the connectedDevice type and its required permissions. Discovery, reconnect, and short data transfers have different options. Android's current background BLE guidance covers filtered PendingIntent scans, WorkManager, auto-connect, and companion-device APIs.
Do not assume that a boot receiver can always restart a foreground service or that requesting exemption from battery optimization fixes every vendor policy. Model what must happen after process death, test the relevant Android versions, and explain any user action in the context of a feature the user has explicitly started.
Vendor power-management differences
Some device builds apply additional power-management controls. Test the shipping OS builds used by customers, but do not force every user through vendor-specific settings before a failure is observed. When intervention is necessary, explain the affected feature, open only supported settings surfaces, and verify the result.
A private catalogue of undocumented settings intents is brittle. Keep the normal lifecycle correct first: reconnect from explicit user intent, persist resumable state, use the appropriate foreground-service type, and make process death recoverable.
Testing: include real devices
Use physical hardware for end-to-end BLE behavior and maintain a matrix based on customer devices and Android versions. A programmable development kit can reproduce protocol states, but final acceptance still needs the production radio, firmware, antenna, enclosure, and body placement.
An application-level boundary around platform BLE
A product with several BLE workflows can benefit from keeping scanning, connection state, operation coordination, MTU results, and recovery policy behind one application-level boundary. Kotlin coroutines and StateFlow are one implementation option; the important part is that callbacks update a testable state model rather than unrelated screens managing the same connection independently.
Keep platform-version and device-specific handling close to that boundary, and keep the product protocol explicit above it. A small application with one short BLE interaction may not need the same abstraction as a long-running multi-sensor product.
If you are debugging a BLE product, contact DEVSFLOW with the peripheral, Android versions, and failure trace. Those details are more useful than a general description of an unreliable connection.
For products where BLE reliability is part of the product itself, see DEVSFLOW's connected-device and neurotechnology engineering practice.