-
Notifications
You must be signed in to change notification settings - Fork 156
Description
Environment:
- Flutter 3.35.4
- flutter_nfc_kit 3.6.0
- Android 16 (API 35)
- Works fine on Android 15 and below
Description:
Since upgrading to Android 16, NFC polling randomly fails with the following error:
Too many transaction errors, throttling freezer binder callback.
E/NFC ( 7894): NFC service dead - attempting to recover
E/NFC ( 7894): android.os.DeadObjectException
E/NFC ( 7894): at android.os.BinderProxy.transactNative(Native Method)
E/NFC ( 7894): at android.os.BinderProxy.transact(BinderProxy.java:661)
E/NFC ( 7894): at android.nfc.INfcAdapter$Stub$Proxy.setReaderMode(INfcAdapter.java:1408)
E/NFC ( 7894): at android.nfc.NfcActivityManager.lambda$setReaderMode$0(NfcActivityManager.java:188)
E/NFC ( 7894): at android.nfc.NfcActivityManager.$r8$lambda$krSWHo-jOsngSRB_31LgKqxiBIw(Unknown Source:0)
E/NFC ( 7894): at android.nfc.NfcActivityManager$$ExternalSyntheticLambda1.call(D8$$SyntheticClass:0)
E/NFC ( 7894): at android.nfc.NfcAdapter.callService(NfcAdapter.java:2594)
E/NFC ( 7894): at android.nfc.NfcActivityManager.setReaderMode(NfcActivityManager.java:188)
E/NFC ( 7894): at android.nfc.NfcActivityManager.disableReaderMode(NfcActivityManager.java:181)
E/NFC ( 7894): at android.nfc.NfcAdapter.disableReaderMode(NfcAdapter.java:1759)
E/NFC ( 7894): at im.nfc.flutter_nfc_kit.FlutterNfcKitPlugin.handleMethodCall$lambda$3(FlutterNfcKitPlugin.kt:341)
E/NFC ( 7894): at im.nfc.flutter_nfc_kit.FlutterNfcKitPlugin.$r8$lambda$9MJSxQ4FZ-YYol3BkfA9vL8MwZI(Unknown Source:0)
E/NFC ( 7894): at im.nfc.flutter_nfc_kit.FlutterNfcKitPlugin$$ExternalSyntheticLambda9.invoke(D8$$SyntheticClass:0)
E/NFC ( 7894): at im.nfc.flutter_nfc_kit.FlutterNfcKitPlugin$Companion.runOnNfcThread$lambda$1(FlutterNfcKitPlugin.kt:77)
E/NFC ( 7894): at im.nfc.flutter_nfc_kit.FlutterNfcKitPlugin$Companion.$r8$lambda$qSEZW8-Rgr4k31_LRwzij_teb8U(Unknown Source:0)
E/NFC ( 7894): at im.nfc.flutter_nfc_kit.FlutterNfcKitPlugin$Companion$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
E/NFC ( 7894): at android.os.Handler.handleCallback(Handler.java:995)
E/NFC ( 7894): at android.os.Handler.dispatchMessage(Handler.java:103)
E/NFC ( 7894): at android.os.Looper.loopOnce(Looper.java:273)
E/NFC ( 7894): at android.os.Looper.loop(Looper.java:363)
E/NFC ( 7894): at android.os.HandlerThread.run(HandlerThread.java:85)
E/NFC ( 7894): could not retrieve NFC service during service recovery
E/im.nfc.flutter_nfc_kit.FlutterNfcKitPlugin( 7894): Close tag error
E/im.nfc.flutter_nfc_kit.FlutterNfcKitPlugin( 7894): java.lang.RuntimeException: android.os.DeadObjectException
...This occurs during FlutterNfcKit.poll(), FlutterNfcKit.transceive — the call takes longer to respond and often fails.
On Android 15 or lower, the exact same code runs perfectly.
Reproduction:
Run the following minimal app on Android 16 device.
Press “Start DASH Detection”.
Observe intermittent or consistent DeadObjectException during connection.
Steps to Reproduce
- Create a new Flutter app using Flutter 3.35.4.
- Add the dependency:
flutter_nfc_kit: ^3.6.0
- Use the following minimal test code:
Demo code
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_nfc_kit/flutter_nfc_kit.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String _status = '';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
_status,
style: Theme.of(context).textTheme.headlineMedium, textAlign: TextAlign.center,
),
ElevatedButton(
onPressed: () async {
bool nfcAvailable = await _NfcService.isDeviceNfcAvalable();
if (!nfcAvailable) {
setState(() {
_status = 'NFC is not available on this device';
});
return;
}
setState(() {
_status = 'Starting DASH detection...';
});
bool success = await startDetection();
setState(() {
_status = 'DASH detection ended with result: $success';
});
},
child: const Text('Start DASH Detection'),
),
ElevatedButton(
onPressed: () async {
_onReading = false;
await _safeDisconnect();
setState(() {
_status = 'NOT CONNECTED';
});
setState(() {
_status = 'DASH Detection Stopped and Disconnected.';
});
},
child: const Text('Stop DASH Detection'),
),
],
),
),
);
}
bool _onReading = true;
final _maxDurationInSeconds = 60 * 5; // 5 minutes
Future<NFCTag?> _connect() async {
final connectStartTime = DateTime.now();
NFCTag? tag;
int count = 1;
while (_onReading) {
try {
if (DateTime.now().difference(connectStartTime).inSeconds > _maxDurationInSeconds) {
break;
}
debugPrint('Connecting to NFC tag... --------------------------------------------------------->');
await _safeDisconnect();
tag = await _NfcService.connect();
debugPrint('connect success --------------------------------------------------------->');
setState(() {
_status = 'CONNECTED';
});
break;
} on PlatformException catch (e) {
setState(() {
_status = 'failed to connect. $count retrying...';
});
count++;
debugPrint('error during connect: $e ///////////////////////////////////////////////');
await Future.delayed(const Duration(seconds: 1));
} catch (ignore) {
debugPrint('Unknown connect error: $ignore');
await Future.delayed(const Duration(milliseconds: 300));
}
}
return tag;
}
Future<bool> startDetection() async {
setState(() {
_status = 'NOT CONNECTED';
});
_onReading = true;
DateTime? lastSoundTime;
setState(() {
_status = 'Connecting...';
});
NFCTag? tag = await _connect();
if (tag != null) {
var detectionStartTime = DateTime.now();
while (_onReading) {
try {
if (DateTime.now().difference(detectionStartTime).inSeconds > _maxDurationInSeconds) {
return false;
}
debugPrint('Checking DASH availability... ********************************************************************************>>>>>>>');
var isAvailable = await _NfcService.isDeviceAvailable();
debugPrint('DASH availability: $isAvailable ********************************************************************************>>>>>>>');
if(!_onReading) {
break;
}
if (isAvailable) {
detectionStartTime = DateTime.now();
setState(() {
_status = 'DASH AVAILABLE';
});
if(lastSoundTime == null || DateTime.now().difference(lastSoundTime).inSeconds > 3) {
lastSoundTime = DateTime.now();
}
} else {
setState(() {
_status = 'DASH NOT AVAILABLE';
});
}
await Future.delayed(const Duration(milliseconds: 500));
_safeDisconnect();
tag = await _connect();
if(tag == null) {
return false;
}
} on PlatformException catch (e) {
setState(() {
_status = 'NOT CONNECTED';
});
await _safeDisconnect();
await Future.delayed(Duration(milliseconds: 500));
setState(() {
_status = '1 - Reconnecting...';
});
tag = await _connect();
if (tag == null) {
return false;
}
await Future.delayed(const Duration(seconds: 1));
} catch (ignore) {
setState(() {
_status = 'NOT CONNECTED';
});
// Ignore errors, will retry until successful
debugPrint('Unknown error during DASH detection: $ignore. Retrying... ?????????????????????????????????');
await Future.delayed(const Duration(milliseconds: 300));
}
}
}
return true;
}
Future<void> _safeDisconnect() async {
try {
await _NfcService.disconnect();
} catch (e) {
debugPrint('Ignore disconnect error: $e');
} finally {
await Future.delayed(Duration(milliseconds: 200));
}
}
}
class _NfcService {
static Future<NFCTag?> connect() {
return FlutterNfcKit.poll(timeout: Duration(seconds: 10))
.then((nfcTag) async {
await Future.delayed(const Duration(milliseconds: 200));
debugPrint('NFC Tag connected: ${nfcTag.id}');
return nfcTag;
});
}
static Future<void> disconnect() async {
try {
await FlutterNfcKit.finish();
await Future.delayed(const Duration(milliseconds: 500));
} catch (e) {
debugPrint('NFC disconnect Error: $e');
}
}
static Future<Uint8List> transceive(Uint8List command) {
return FlutterNfcKit.transceive(command);
}
static Future<bool> isDeviceNfcAvalable() {
return FlutterNfcKit.nfcAvailability
.then((value) => value == NFCAvailability.available);
}
static Future<bool> isDeviceAvailable() {
return transceive(Uint8List.fromList([0x02, 0x2B]))
.then((value) => value.isNotEmpty);
}
}- Build and run on an Android 16 device with NFC enabled.
- Observe logcat during tag detection.
Expected Behavior
FlutterNfcKit.poll() should reliably enable reader mode and connect to the NFC tag, as on Android 15 and earlier.
Actual Behavior
poll() takes longer than expected to respond.
Sometimes it fails immediately with DeadObjectException.
The error originates from INfcAdapter.setReaderMode() inside the native plugin.
Random DeadObjectException in INfcAdapter.setReaderMode().
Notes:
Seems related to changes in the Android 16 NFC service or binder lifecycle.
Potential fix might involve checking for DeadObjectException and re-acquiring NfcAdapter in native code before calling setReaderMode.
Additional Notes
-
The same app functions normally on Android 13–15.
-
Appears related to Android 16 changes in NfcAdapter.setReaderMode() and service binder lifecycle.
-
Might require updating the native code in FlutterNfcKitPlugin.kt to:
- Catch DeadObjectException explicitly. - Re-acquire a new NfcAdapter instance if the binder is dead. - Ensure calls to setReaderMode() happen on the correct thread.
Possible Fix Direction
kotlin
try {
nfcAdapter.setReaderMode(activity, readerCallback, flags, null)
} catch (e: DeadObjectException) {
// Reinitialize NfcAdapter before retrying
nfcAdapter = NfcAdapter.getDefaultAdapter(context)
nfcAdapter.setReaderMode(activity, readerCallback, flags, null)
}