Android 5 to 17: what changed for Bluetooth¶
This page is the research behind BlueLib's compatibility layer. Every signature and since value below
was read from the Android SDK database that ships with the platform tools —
$ANDROID_HOME/platforms/android-37.0/data/api-versions.xml, the same source Android Lint uses — and
cross-checked against the published behaviour-change notes. Run ./gradlew generateCompatibilityMatrix
to regenerate the matrix from the same file.
Android 5.0 — API 21 (the floor)¶
BluetoothLeScanner.startScan(…)andBluetoothLeAdvertiserreplacestartLeScan, which is deprecated from here on.BluetoothGatt.requestMtu(int)andrequestConnectionPriority(int)exist from day one, which is why BlueLib can negotiate MTU on every supported device.BluetoothGattServerCallback.onNotificationSentexists, so indications can be acknowledged.BluetoothGatt.GATT_CONNECTION_CONGESTEDis defined — status 133's quieter sibling.
Android 6.0 — API 23¶
- Runtime permissions arrive, which is why the gateway has two branches: install-time permissions below 23, runtime grants above it.
BluetoothGattServer.onMtuChanged/BluetoothDeviceMTU reporting becomes usable.
Android 8.0 — API 26¶
- Extended advertising and extended scanning: payloads beyond 31 bytes,
AdvertisingSetParameters,BluetoothLeAdvertiser.startAdvertisingSet, andBluetoothAdapter.getLeMaximumAdvertisingDataLength()for the real budget. - PHY:
BluetoothGatt.setPreferredPhy,BluetoothGattCallback.onPhyUpdate,isLe2MPhySupported,isLeCodedPhySupported. - Periodic advertising (
BluetoothLeScanner.startSync). PendingIntentbased scans (startScan(filters, settings, PendingIntent)), which is what allows background scanning without holding a foreground service.- Offloaded batching and filtering become common hardware features, so BlueLib exposes them in the capability report.
Android 10 — API 29¶
- L2CAP CoC:
BluetoothDevice.createL2capChannel(int)andcreateInsecureL2capChannel(int). BluetoothGattService.getInstanceId()/BluetoothGattCharacteristic.getInstanceId(), which is why BlueLib's profile model carries instance ids: two instances of the same characteristic in one service are otherwise indistinguishable.- Background scan and location access tighten further, reinforcing the "ask before you scan" rule.
Android 11 — API 30¶
BluetoothDevice.EXTRA_RSSIbecomes public API in theACTION_FOUNDbroadcast, which is whatdiscoverClassic(includeRssi = true)relies on. Below API 30 the field was read through a hidden extra, and BlueLib returnsnullinstead of guessing.- Android's scan quota (5
startScancalls per 30 seconds, per app) starts being enforced broadly — henceScanQuotaGovernor.
createBond(int transport) is not an API 30 method
BluetoothDevice.createBond(int transport) looks like it arrived with the transport constants in
API 30, but the SDK database marks the public method as since 37.0. On API 30–36 it existed only
as a system API, so BlueLib calls it only on Android 17 and uses the transport-less createBond()
below that. Android Lint's NewApi check caught this, which is why the check runs as an error in
CI.
Android 12 — API 31¶
- The permission split:
BLUETOOTH_SCAN,BLUETOOTH_CONNECT,BLUETOOTH_ADVERTISEreplaceBLUETOOTH+BLUETOOTH_ADMINfor apps targeting Android 12, andACCESS_FINE_LOCATIONstops being the way to scan if the app assertsneverForLocation. BluetoothGattCallback.onServiceChanged(BluetoothGatt)becomes usable, which lets BlueLib drop a stale service cache and rediscover on its own.- Reading
BluetoothAdapter.isEnabledandBluetoothDevice.getName()now requireBLUETOOTH_CONNECTand throwSecurityExceptionotherwise — the reason every platform call in BlueLib is wrapped.
Android 13 — API 33¶
- GATT values in callbacks:
onCharacteristicRead(…, byte[] value, int status),onCharacteristicChanged(…, byte[] value),writeCharacteristic(characteristic, value, writeType)andwriteDescriptor(descriptor, value). Values are only valid inside their callback before this, so BlueLib copies them. readCharacteristickeeps itsbooleanreturn; only the writes gain status-returning overloads. A library that assumes otherwise will not compile — this was verified withjavapagainstandroid.jarfor API 37.- LE Audio support queries:
isLeAudioSupported(),isLeAudioBroadcastSourceSupported(),isLeAudioBroadcastAssistantSupported(), all returningBluetoothStatusCodes. - Scan failures are typed:
SCAN_FAILED_SCANNING_TOO_FREQUENTLY(0x05) distinguishes platform throttling from real errors, andScanFiltergains AD-type based matching. - Runtime receivers must declare an export flag on Android 13+, or registration throws. Bluetooth
broadcasts come from the Bluetooth module rather than the system server, so they need
Context.RECEIVER_EXPORTED: a not-exported receiver silently never receives them.
Android 14 — API 34¶
AdvertisingSetParameters.Builder.setDiscoverable(boolean).ConnectionPriority.CONNECTION_PRIORITY_DCK(distributed connection kit).- Foreground service type
connectedDevicebecomes mandatory for long-running Bluetooth work, which is an application concern but shapes the recommendations in the Scanning guide.
Android 15 — API 35¶
- No new Bluetooth API in the diff; the release concentrates on privacy and background execution, both of which affect when an app may scan rather than how.
Android 16 — API 36¶
BluetoothSocketSettings:BluetoothDevice.createUsingSocketSettings(settings)finally lets an app request encryption and authentication explicitly for RFCOMM and L2CAP, instead of relying on the "secure" variant being secure everywhere.BluetoothDevice.EXTRA_BOND_LOSS_REASONand theBOND_LOSS_REASON_*constants, reported throughACTION_BOND_STATE_CHANGED— BlueLib maps them intoBlueLibError.BondLost. The constants are only read on API 36.1+ (ApiLevel.isAtLeast(36, 1)), because 16.0 does not send the extra.CompanionDeviceManager.removeBond(int associationId): the first public unbond API on Android, and the reasonunbond()needs Android 16 rather than the Android 13 association flow.- Ranging: the
android.rangingmodule withRangingManagerandandroid.ranging.ble.cs.BleCsRangingParams(Bluetooth Channel Sounding, plus RSSI ranging), guarded by theandroid.permission.RANGINGruntime permission and theandroid.hardware.bluetooth.le.channel_soundingfeature flag. BluetoothDevice.getIdentityAddressWithType().
Android 16.1 — API 36.1 (a minor SDK version)¶
- Connection subrating:
BluetoothGatt.requestSubrateMode(int),BluetoothGattCallback.onSubrateChange. ACTION_KEY_MISSING,BluetoothDevice.getKeyMissingCount().- This release is the reason
ApiLevel.isAtLeast(36, 1)exists:Build.VERSION.SDK_INTstays36, so comparison against major versions alone reports "not supported" on a device that supports it.
Android 17 — API 37 (the ceiling)¶
Verified with javap against the API 37 android.jar:
BluetoothDevice.connect()anddisconnect(), replacingBluetoothGatt.connect()/disconnect().BluetoothGattConnectionSettingswithconnectGatt(BluetoothGattConnectionSettings, Executor, BluetoothGattCallback). Every olderconnectGattoverload is deprecated: the settings object carries transport, auto-connect, automatic MTU and opportunistic connection flags in one place. BlueLib prefers it and keeps the legacy overloads for API 21–36.BluetoothDevice.cancelBondProcess(),BluetoothDevice.getBondStatus(int)returning aBondStatusobject (pairing algorithm and variant,BondStatus.BONDING_VARIANT_*).BluetoothDevice.fetchUuids(int transport).- PHY
PHY_LE_HDT(high data throughput) andBluetoothAdapter.isLeHighDataThroughputPhySupported(). BluetoothAdapter.ACTION_ADAPTER_STATE_CHANGEDbehaviour changes and tightened permission checks.- Autonomous re-pairing: when a bond is lost, the platform may run its own re-pairing flow and
broadcast
ACTION_KEY_MISSINGonly when that fails. BlueLib therefore never forces a manual pairing flow on Android 17; it reportsBondLost(systemRepairInProgress = …)and lets the UI decide. BluetoothGattConnectionSettingsdeprecations mean new code should not useautoConnect = truewith the old overloads; BlueLib'sGattConnectRequestkeeps the flag explicit.
Things that never became public¶
| API | Status |
|---|---|
BluetoothDevice.removeBond() |
Still hidden in API 37; use CompanionDeviceManager.removeBond |
BluetoothGatt.refresh() |
Hidden; the supported workaround is rediscovery after GATT_DATABASE_OUT_OF_SYNC |
BluetoothAdapter.enable() / disable() |
Deprecated and privileged; use ACTION_REQUEST_ENABLE |
| MTU request in the GATT server role | Does not exist; the central owns the exchange |
AdvertisingSet.getAdvertisingSetId() |
Not in the API 37 SDK surface — verified with javap, which is why BlueLib allocates its own advertising set ids |