I can connect with the BLE device smoothly until Android 13 and data transmission is also very smooth, but the issue starts from Android 14. In Android 14, initially the logs shows connection with the device see here in the logs:
onClientRegistered() - status=0 clientIf=6
onFocusEvent true
onClientConnectionState() - status=0 clientIf=6 connected=true device=XX:XX:XX:XX:XX:C8
onConnectionStateChange: status: 0, newState: 2
gatt connected
configureMTU() - device: XX:XX:XX:XX:XX:C8 mtu: 517
discoverServices() - device: XX:XX:XX:XX:XX:C8
But, after some time the connection closes, see here in the logs:
Connection timeout hit — still not connected
closing connection
cancelOpen() - device: XX:XX:XX:XX:XX:C8
close()
unregisterApp() - mClientIf=6
My full AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"
android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<application
android:name=".MyApplication"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.App"
tools:targetApi="31">
<activity
android:name=".ui.main.MainActivity"
android:exported="true"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"
android:configChanges="orientation|screenSize|keyboardHidden|uiMode"
tools:ignore="LockedOrientationActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".service.BluetoothLeService" />
<service android:name=".service.TimerService"/>
</application>
</manifest>
This is how I am establishing the connection from the BleService
:
fun connect(address: String, activity: Activity): Boolean {
// Step 1: Check the adapter
val adapter = bluetoothAdapter ?: return false
// Step 2: Get device
val device = try {
adapter.getRemoteDevice(address)
} catch (e: IllegalArgumentException) {
Log.e("BLE", "Invalid address", e)
return false
}
// Step 3: Handle permissions for Android 12+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
ActivityCompat.checkSelfPermission(
activity,
Manifest.permission.BLUETOOTH_CONNECT
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.BLUETOOTH_CONNECT), 1
)
return false
}
// Step 4: Clean up any existing connection
bluetoothGatt?.apply {
disconnect()
close()
}
bluetoothGatt = null
// Step 5: Connect
bluetoothGatt = device.connectGatt(activity, false, bluetoothGattCallback)
return bluetoothGatt != null
}
Connecting Code from Fragment:
private fun connecting() {
println("I am in the connecting: $address")
progressDialog.show()
binding.header.text = requireActivity().getString(R.string.connecting_text)
binding.listView.isClickable = false
handler.removeCallbacksAndMessages(null)
val gattServiceIntent = Intent(requireActivity(), BluetoothLeService::class.java)
isBound = requireActivity().bindService(
gattServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE
)
connectHandler = Handler(Looper.getMainLooper())
connectHandler.postDelayed({
println("I am in the connecting2")
if (!connected && activity != null && isAdded) {
println("I am in the connecting3 $connected-$activity-$isAdded")
Log.d("asdfasdf", "Connection timeout hit — still not connected")
bluetoothService?.close()
bluetoothService = null
Toast.makeText(
requireActivity(),
"Can't connect to the device, try again.",
Toast.LENGTH_SHORT
).show()
binding.listView.isClickable = true
if (progressDialog.isShowing) progressDialog.dismiss()
address = null
devicesList.clear()
arrayAdapter.notifyDataSetChanged()
if (isBound) {
requireActivity().unbindService(serviceConnection)
isBound = false
}
startConnection()
}
}, 15000)
}
This is how setting MTU
:
private val bluetoothGattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
Log.d("asdfasdf", "onConnectionStateChange: status: $status, newState: $newState")
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
// successfully connected to the GATT Server
connectionState = STATE_CONNECTED
Log.d("asdfasdf", "gatt connected")
broadcastUpdate(ACTION_GATT_CONNECTED)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ContextCompat.checkSelfPermission(
this@BluetoothLeService,
Manifest.permission.BLUETOOTH_CONNECT
) != PackageManager.PERMISSION_GRANTED
){
Log.w("BLE","Missing BLUETOOTH_CONNECT permission, can't request MTU")
return
}
gatt?.requestMtu(517) // MTU here
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission(
this@BluetoothLeService,
Manifest.permission.BLUETOOTH_CONNECT
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
Activity(),
arrayOf(Manifest.permission.BLUETOOTH_CONNECT), 1
)
return
}
gatt?.discoverServices()
}
BluetoothProfile.STATE_DISCONNECTED -> {
// disconnected from the GATT Server
Log.d("asdfasdf", "gatt disconnected service")
connectionState = STATE_DISCONNECTED
broadcastUpdate(ACTION_GATT_DISCONNECTED)
}
}
}
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
super.onMtuChanged(gatt, mtu, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
negotiatedMtu = mtu
Log.d("BLE", "MTU changed successfully. Negotiated MTU = $mtu")
gatt?.discoverServices()
} else {
Log.w("BLE", "MTU change failed. Status = $status")
gatt?.discoverServices()
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
println("onServiceDiscovered: $gatt -- $status")
super.onServicesDiscovered(gatt, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED)
} else {
//message
}
}
override fun onCharacteristicWrite(
gatt: BluetoothGatt?,
characteristic: BluetoothGattCharacteristic?,
status: Int
) {
super.onCharacteristicWrite(gatt, characteristic, status)
if (characteristic != null) {
when (characteristic.uuid) {
COMMAND_CHAR -> {
broadcastUpdate(ACTION_GATT_COMMAND_WRITE)
}
}
}
}
@Deprecated("Deprecated in Java")
override fun onCharacteristicChanged(
gatt: BluetoothGatt?,
characteristic: BluetoothGattCharacteristic?
) {
super.onCharacteristicChanged(gatt, characteristic)
println("Receiving Characteristics..")
if (characteristic == null) return
when (characteristic.uuid) {
TRAINING_SES_CHAR -> {
val lines = characteristic.value.decodeToString().lines()
SessionOngoingFragment.data += lines[0] + "\n"
val receivedData = lines[0].split(",").toTypedArray()
dataBroadcastUpdate(ACTION_GATT_RESPONSE_TRAINING, receivedData)
}
DEVELOPMENT_DATA_CHAR -> {
broadcastUpdate(ACTION_GATT_RESPONSE_DEV)
}
else -> {
Log.w("BLE_RESPONSE", "Unhandled characteristic: ${characteristic.uuid}")
}
}
}
@Deprecated("Deprecated in Java")
override fun onCharacteristicRead(
gatt: BluetoothGatt?,
characteristic: BluetoothGattCharacteristic?,
status: Int
) {
super.onCharacteristicRead(gatt, characteristic, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.d("asdfasdf", "onCharacteristicRead ${characteristic!!.value.decodeToString()}")
val result = characteristic.value.decodeToString()
when (characteristic.uuid) {
VERSION_CHAR -> {
val version = result.substring(0, 5)
Log.d("asdfasdf", "version $version")
dataBroadcastUpdate(ACTION_GATT_VERSION, arrayOf(version))
}
RESPONSE_CHAR -> {
val initLetter = result.trim().first()
if (initLetter == 'F') {
broadcastUpdate(ACTION_GATT_FEEDBACK)
} else {
val battery = result.lines()[0].split(',').toTypedArray()
dataBroadcastUpdate(ACTION_GATT_BATTERY, battery)
}
}
}
}
}
}
permissions I am asking:
init {
requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()){ result ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (result[Manifest.permission.BLUETOOTH_SCAN] != null){
isScanPermissionGranted = result[Manifest.permission.BLUETOOTH_SCAN] == true
}
if (result[Manifest.permission.BLUETOOTH_CONNECT] != null){
isConnectPermissionGranted = result[Manifest.permission.BLUETOOTH_CONNECT] == true
}
}
if (result[Manifest.permission.ACCESS_FINE_LOCATION] != null){
isFinePermissionGranted = result[Manifest.permission.ACCESS_FINE_LOCATION] == true
}
when {
!(isScanPermissionGranted && isConnectPermissionGranted) -> {
permissionRequired()
}
!(isFinePermissionGranted) -> {
locationPermissionRequired()
}
else -> {
askPermissionForBackgroundUsage()
}
}
}
}
private fun requestPermission(){
if (Build.VERSION.SDK_INT<=Build.VERSION_CODES.R){
isFinePermissionGranted = ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
isScanPermissionGranted = true
isConnectPermissionGranted = true
}else {
isFinePermissionGranted = true
isScanPermissionGranted = ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.BLUETOOTH_SCAN
) == PackageManager.PERMISSION_GRANTED
isConnectPermissionGranted = ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.BLUETOOTH_CONNECT
) == PackageManager.PERMISSION_GRANTED
}
val list : ArrayList<String> = arrayListOf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!isScanPermissionGranted){
list.add(Manifest.permission.BLUETOOTH_SCAN)
}
if (!isConnectPermissionGranted){
list.add(Manifest.permission.BLUETOOTH_CONNECT)
}
}
if (!isFinePermissionGranted)
list.add(Manifest.permission.ACCESS_FINE_LOCATION)
if (list.isEmpty()){
askPermissionForBackgroundUsage()
}else{
requestPermissionLauncher.launch(list.toTypedArray())
}
}
see some more logs from finding to losing connection with the device.
onClientRegistered() - status=0 clientIf=6
onFocusEvent true
onClientConnectionState() - status=0 clientIf=6 connected=true
onConnectionStateChange: status: 0, newState: 2
gatt connected
configureMTU() - device: XX:XX:XX:XX:E6:C8 mtu: 517
discoverServices() - device: XX:XX:XX:XX:E6:C8
onPhyUpdate() - status=0 address=XX:XX:XX:XX:E6:C8 txPhy=2 rxPhy=2
onConfigureMTU() - Device=XX:XX:XX:XX:E6:C8 mtu=256 status=0
MTU changed successfully. Negotiated MTU = 256
discoverServices() - device: XX:XX:XX:XX:E6:C8
onSearchComplete() = Device=XX:XX:XX:XX:E6:C8 Status=0
onConnectionUpdated() - Device=XX:XX:XX:XX:E6:C8 interval=24 latency=0 timeout=500 status=0
onConnectionUpdated() - Device=XX:XX:XX:XX:E6:C8 interval=144 latency=0 timeout=400 status=0
Connection timeout hit — still not connected
closing connection
cancelOpen() - device: XX:XX:XX:XX:E6:C8
close()
ui.main.MainActivity,window dying
ui.main.MainActivity,unregisterSystemUIBroadcastReceiver
ui.main.MainActivity, unregisterSystemUIBroadcastReceiver failed java.lang.IllegalArgumentException: Receiver not registered: android.view.OplusScrollToTopManager$2@d25b5db
Can anyone help me establish a successful connection with the BLE device on Android 14? No issue till Android 13.
The issue was in the BroadcastReceiver
While registering. Here I am targeting different SDK versions.
Here, I needed to pass Context.RECEIVER_EXPORTED
instead of Context.RECEIVER_NOT_EXPORTED
while targeting TIRAMISU
and above.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requireActivity().registerReceiver(
gattUpdateReceiver,
makeGattUpdateIntentFilter(),
Context.RECEIVER_NOT_EXPORTED
)
} else {
ContextCompat.registerReceiver(
requireActivity(),
gattUpdateReceiver,
makeGattUpdateIntentFilter(),
ContextCompat.RECEIVER_NOT_EXPORTED
)
}
But passing Context.RECEIVER_EXPORTED
Directly is not a good practice unless you have specific requirements.
Then, I secured my Context.RECEIVER_EXPORTED
Passing it along with a permission.
First, I declared a permission in the AndroidManifest.xml
like this:
<!-- Signature level permission to secure RECEIVER_EXPORTED -->
<uses-permission android:name="com.your.package.SAFE_BROADCAST" />
<permission
android:name="com.your.package.SAFE_BROADCAST"
android:protectionLevel="signature" />
place it above <application/>
block.
Then use it like this:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requireActivity().registerReceiver(
gattUpdateReceiver,
makeGattUpdateIntentFilter(),
"${requireActivity().packageName}.SAFE_BROADCAST",
null,
Context.RECEIVER_EXPORTED
)
} else {
ContextCompat.registerReceiver(
requireActivity(),
gattUpdateReceiver,
makeGattUpdateIntentFilter(),
ContextCompat.RECEIVER_NOT_EXPORTED
)
}
It solved my issue.