Skip to content

🐞 Bug Report — DeadObjectException on Android 16 during FlutterNfcKit.poll(), FlutterNfcKit.transceive #222

@labarefo

Description

@labarefo

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

  1. Create a new Flutter app using Flutter 3.35.4.
  2. Add the dependency:

flutter_nfc_kit: ^3.6.0

  1. 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);
  }
}
  1. Build and run on an Android 16 device with NFC enabled.
  2. 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)
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions