flutter_nfc_kit icon indicating copy to clipboard operation
flutter_nfc_kit copied to clipboard

Android default NFC reader gets triggered

Open aghyad97 opened this issue 3 years ago • 25 comments

This issue only occurs on Android whenever I try to scan NFC tag using my own app where it triggers the default NFC reader of the phone.

Is there a way to prevent it and read only from the app library?

aghyad97 avatar Nov 11 '20 19:11 aghyad97

Sorry, I don't really understand your situation. On Android our native code will try to poll all types of NFC Technology with/without NDEF checking according to flags passed. You could refer to:

https://github.com/nfcim/flutter_nfc_kit/blob/6471b5931e6b6e4765bfad9bd3385880fcddd03b/android/src/main/kotlin/im/nfc/flutter_nfc_kit/FlutterNfcKitPlugin.kt#L291-L297

Can you actually detect polled tag in your code (that is, does await poll() returns?) when scanning?

Harry-Chen avatar Nov 12 '20 02:11 Harry-Chen

I have meant when I try to read NFC through my app, the default NFC reader of the phone opened. I want to prevent this and make the phone reads the NFC tag only inside my app. I hope this clarifies the issue.

aghyad97 avatar Nov 12 '20 11:11 aghyad97

I have meant when I try to read NFC through my app, the default NFC reader of the phone opened. I want to prevent this and make the phone reads the NFC tag only inside my app. I hope this clarifies the issue.

What you need might be the foreground dispatch mode, which is possible with Flutter, but needs some tricky implementation in native side. I do not have much time to handle this right now. You may try (and maybe send a PR!) that if you want to avoid the system stepping in when scanning tags.

Harry-Chen avatar Nov 14 '20 15:11 Harry-Chen

Would be nice to avoid the system stepping in when scanning tags. Found this on SO:

if (pendingIntent == null) { pendingIntent = PendingIntent.getActivity(this, 0, new Intent(), 0); } adapter.enableForegroundDispatch(this, pendingIntent, null, null);

Mythar avatar Apr 11 '21 08:04 Mythar

Would be nice to avoid the system stepping in when scanning tags. Found this on SO:

if (pendingIntent == null) { pendingIntent = PendingIntent.getActivity(this, 0, new Intent(), 0); } adapter.enableForegroundDispatch(this, pendingIntent, null, null);

Currently we are using reader mode only. I am not sure whether enabling foreground dispatch and reader mode simultaneously leads to current behaviour. Have you tried to enable foreground dispatch without doing anything in onNewIntent? If this works I am happy to enable it to prevent system dialog from intervening with tag reading.

Harry-Chen avatar Apr 11 '21 09:04 Harry-Chen

Would be nice to avoid the system stepping in when scanning tags. Found this on SO:

if (pendingIntent == null) { pendingIntent = PendingIntent.getActivity(this, 0, new Intent(), 0); } adapter.enableForegroundDispatch(this, pendingIntent, null, null);

I have tried foreground dispatching mode, but seems nothing changes . When you are not in reader mode, a tag scan will still trigger the system dialog. To solve this, I believe we have to implement a streaming mode with foreground dispatch, as I mentioned in https://github.com/nfcim/flutter_nfc_kit/issues/17#issuecomment-727222864.

Harry-Chen avatar Apr 11 '21 10:04 Harry-Chen

Hello, is there any update regarding this issue?

jacopofranza avatar Oct 16 '21 09:10 jacopofranza

Hello, is there any update regarding this issue?

@jacopofranza As mentioned above, I cannot solve this without adding support for streaming mode with foreground dispatch. However this would be a great change to our API, and requires a relatively large amount of work. So, sorry, but I cannot give any guarantee on this.

Harry-Chen avatar Oct 18 '21 11:10 Harry-Chen

Workaround: Encode the card mem directly w/o using ntag, this prevents dialog to pop.

Mythar avatar Oct 19 '21 06:10 Mythar

Workaround: Encode the card mem directly w/o using ntag, this prevents dialog to pop.

Hi Mythar, i am intersted in your workaround , but i do not understand what you wrote. How can i write using ntag (since ntag is a nfc tag ) ?

jibebec avatar Nov 21 '21 16:11 jibebec

Workaround: Encode the card mem directly w/o using ntag, this prevents dialog to pop.

Hi Mythar, i am intersted in your workaround , but i do not understand what you wrote. How can i write using ntag (since ntag is a nfc tag ) ?

As I wrote, do not write to a ntag, write to the card mem directly - depends on what software you are using to encode your cards.

Mythar avatar Nov 22 '21 12:11 Mythar

This is the real workaround to avoid the problem. In your manifest at the end of the main activity put:

<activity ... >
   ...
   <intent-filter>
      <action android:name="android.nfc.action.TECH_DISCOVERED" />
      <category android:name="android.intent.category.DEFAULT" />
   </intent-filter>
   <meta-data
      android:name="android.nfc.action.TECH_DISCOVERED"
      android:resource="@xml/nfc_tech_filter"/>
</activity>

In res/xml create a new file named "nfc_tech_filter" with the intent types you want to filter:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.IsoDep</tech>
        <tech>android.nfc.tech.NfcA</tech>
        <tech>android.nfc.tech.NfcB</tech>
        <tech>android.nfc.tech.NfcF</tech>
        <tech>android.nfc.tech.NfcV</tech>
        <tech>android.nfc.tech.Ndef</tech>
        <tech>android.nfc.tech.NdefFormatable</tech>
        <tech>android.nfc.tech.MifareClassic</tech>
        <tech>android.nfc.tech.MifareUltralight</tech>
    </tech-list>
</resources>

In the MainActivity insert the following lines to enable foreground dispatch:

    override fun onResume() {
        super.onResume()
        val adapter: NfcAdapter = NfcAdapter.getDefaultAdapter(this)
        val pendingIntent: PendingIntent = PendingIntent.getActivity(
                this, 0, Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0)
        adapter.enableForegroundDispatch(this, pendingIntent, null, null)
    }
    override fun onPause() {
        super.onPause()
        val adapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this)
        adapter?.disableForegroundDispatch(this)
    }

jacopofranza avatar Dec 15 '21 16:12 jacopofranza

@jacopofranza Thanks for this! I will look into your solution later.

Harry-Chen avatar Dec 18 '21 16:12 Harry-Chen

I wrote this for direct memory read of the NFCTagType.mifare_ultralight:

// Import
import 'dart:io';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_nfc_kit/flutter_nfc_kit.dart';


// hexStringToAscii
String hexStringToAscii(String hexString) {
  List<String> split = [];
  for (int i = 0; i < hexString.length; i = i + 2) {
    if (hexString.substring(i, i + 2) != '00') {
      split.add(hexString.substring(i, i + 2));
    }
  }
  String ascii = List.generate(split.length,
          (i) => String.fromCharCode(int.parse(split[i], radix: 16))).join();

  return ascii;
}

// NFCMemory
class NFCMemory {
  dynamic data;
  String ascii;

  // Constructor
  NFCMemory({this.data, this.ascii});

  factory NFCMemory.fromData(dynamic data) {
    if ((data != null) && (data is String) && (data != '')) {
      return new NFCMemory(
          data: data,
          ascii: hexStringToAscii(data)
      );
    } else {
      return new NFCMemory(data: data, ascii: null);
    }
  }
}

// NFCInfo
class NFCInfo {
  NFCTag tag;
  NFCMemory memory;

  // Constructor
  NFCInfo({this.tag, this.memory});
}

// NFCDirect
class NFCDirect {
  String alertMessage;
  String successMessage;

  NFCDirect({this.alertMessage, this.successMessage});

  // isNFCAvailable
  Future<bool> isAvailable() async {
    NFCAvailability availability;
    try {
      availability = await FlutterNfcKit.nfcAvailability;
    } on PlatformException {
      availability = NFCAvailability.not_supported;
    }

    if (availability != NFCAvailability.available) {
      return false;
    } else {
      return true;
    }
  }

  // readNFCMemory
  Future<NFCMemory> readMemory(NFCTag tag) async {
    NFCMemory result;
    if (tag.type == NFCTagType.mifare_ultralight) {
      // 04 08 0C
      var mem1 = await FlutterNfcKit.transceive("3004", timeout: Duration(seconds: 10)); // timeout is still Android-only, persist until next change
      var mem2 = await FlutterNfcKit.transceive("3008", timeout: Duration(seconds: 10)); // timeout is still Android-only, persist until next change
      var mem3 = await FlutterNfcKit.transceive("300C", timeout: Duration(seconds: 10)); // timeout is still Android-only, persist until next change
      if ((mem1 is String) && (mem2 is String) && (mem3 is String)) {
        result = NFCMemory.fromData(mem1 + mem2 + mem3);
      }
    }
    return result;
  }

  // iOSRead
  Future<NFCInfo> iOSRead() async {
    NFCTag tag;
    NFCMemory mem;

    try {
      tag = await FlutterNfcKit.poll(
        readIso14443A: true,
        readIso14443B: true,
        readIso15693: false,
        readIso18092: false,
        iosAlertMessage: alertMessage,
      );

      // Read memory
      mem = await readMemory(tag);

      sleep(new Duration(seconds: 1));
      await FlutterNfcKit.finish(
        iosAlertMessage: successMessage
      );
    } catch (e) {
      print(e);
    }

    return new NFCInfo(
      tag: tag,
      memory: mem
    );
  }

  // androidRead
  Future<NFCInfo> androidRead() async {
    NFCTag tag;
    NFCMemory mem;

    try {
      tag = await FlutterNfcKit.poll(
          readIso14443A: true,
          readIso14443B: true,
          readIso15693: false,
          readIso18092: false,
          timeout: Duration(seconds: 10),
          androidPlatformSound: false,
          androidCheckNDEF: false,
      );

      // Read memory
      mem = await readMemory(tag);

      sleep(new Duration(seconds: 1));
      await FlutterNfcKit.finish();
    } catch (e) {
      print(e);
    }

    return new NFCInfo(
        tag: tag,
        memory: mem
    );
  }
}

Mythar avatar Dec 22 '21 12:12 Mythar

I encountered similar issue on Android, why it happens and how I solved it:

Issue:

After write tag, I called finishNfc, and then immediately, the newly written tag triggered the default NFC behaviour and kicked user out of the app to the link that on the tag.

Why:

NFC session implementation on Android is actually simulated by turning activity into reader mode, which suppresses the default NFC behaviours. And finishing nfc turn off the reader mode, and then default Android NFC behaviour kicked in.

Solution:

On Android, I put a short delay after UI tells user NFC operation finished and before actually finish the NFC. And UI literally tells user to move the tag away from the phone. So it avoided the default NFC behaviour

Hope it helps

timnew avatar Jan 20 '22 02:01 timnew

Thanks for the help! but Why if the tag is empty it keeps firing the alert to choose the app for scan?

PietroGranati avatar Dec 01 '22 16:12 PietroGranati

I wrote this for direct memory read of the NFCTagType.mifare_ultralight:

// Import
import 'dart:io';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_nfc_kit/flutter_nfc_kit.dart';


// hexStringToAscii
String hexStringToAscii(String hexString) {
  List<String> split = [];
  for (int i = 0; i < hexString.length; i = i + 2) {
    if (hexString.substring(i, i + 2) != '00') {
      split.add(hexString.substring(i, i + 2));
    }
  }
  String ascii = List.generate(split.length,
          (i) => String.fromCharCode(int.parse(split[i], radix: 16))).join();

  return ascii;
}

// NFCMemory
class NFCMemory {
  dynamic data;
  String ascii;

  // Constructor
  NFCMemory({this.data, this.ascii});

  factory NFCMemory.fromData(dynamic data) {
    if ((data != null) && (data is String) && (data != '')) {
      return new NFCMemory(
          data: data,
          ascii: hexStringToAscii(data)
      );
    } else {
      return new NFCMemory(data: data, ascii: null);
    }
  }
}

// NFCInfo
class NFCInfo {
  NFCTag tag;
  NFCMemory memory;

  // Constructor
  NFCInfo({this.tag, this.memory});
}

// NFCDirect
class NFCDirect {
  String alertMessage;
  String successMessage;

  NFCDirect({this.alertMessage, this.successMessage});

  // isNFCAvailable
  Future<bool> isAvailable() async {
    NFCAvailability availability;
    try {
      availability = await FlutterNfcKit.nfcAvailability;
    } on PlatformException {
      availability = NFCAvailability.not_supported;
    }

    if (availability != NFCAvailability.available) {
      return false;
    } else {
      return true;
    }
  }

  // readNFCMemory
  Future<NFCMemory> readMemory(NFCTag tag) async {
    NFCMemory result;
    if (tag.type == NFCTagType.mifare_ultralight) {
      // 04 08 0C
      var mem1 = await FlutterNfcKit.transceive("3004", timeout: Duration(seconds: 10)); // timeout is still Android-only, persist until next change
      var mem2 = await FlutterNfcKit.transceive("3008", timeout: Duration(seconds: 10)); // timeout is still Android-only, persist until next change
      var mem3 = await FlutterNfcKit.transceive("300C", timeout: Duration(seconds: 10)); // timeout is still Android-only, persist until next change
      if ((mem1 is String) && (mem2 is String) && (mem3 is String)) {
        result = NFCMemory.fromData(mem1 + mem2 + mem3);
      }
    }
    return result;
  }

  // iOSRead
  Future<NFCInfo> iOSRead() async {
    NFCTag tag;
    NFCMemory mem;

    try {
      tag = await FlutterNfcKit.poll(
        readIso14443A: true,
        readIso14443B: true,
        readIso15693: false,
        readIso18092: false,
        iosAlertMessage: alertMessage,
      );

      // Read memory
      mem = await readMemory(tag);

      sleep(new Duration(seconds: 1));
      await FlutterNfcKit.finish(
        iosAlertMessage: successMessage
      );
    } catch (e) {
      print(e);
    }

    return new NFCInfo(
      tag: tag,
      memory: mem
    );
  }

  // androidRead
  Future<NFCInfo> androidRead() async {
    NFCTag tag;
    NFCMemory mem;

    try {
      tag = await FlutterNfcKit.poll(
          readIso14443A: true,
          readIso14443B: true,
          readIso15693: false,
          readIso18092: false,
          timeout: Duration(seconds: 10),
          androidPlatformSound: false,
          androidCheckNDEF: false,
      );

      // Read memory
      mem = await readMemory(tag);

      sleep(new Duration(seconds: 1));
      await FlutterNfcKit.finish();
    } catch (e) {
      print(e);
    }

    return new NFCInfo(
        tag: tag,
        memory: mem
    );
  }
}

it's not gonna work , do you have any other way to read NFC memory, dear ?

luoqiang110 avatar Mar 06 '23 15:03 luoqiang110

I encountered similar issue on Android, why it happens and how I solved it:

Issue:

After write tag, I called finishNfc, and then immediately, the newly written tag triggered the default NFC behaviour and kicked user out of the app to the link that on the tag.

Why:

NFC session implementation on Android is actually simulated by turning activity into reader mode, which suppresses the default NFC behaviours. And finishing nfc turn off the reader mode, and then default Android NFC behaviour kicked in.

Solution:

On Android, I put a short delay after UI tells user NFC operation finished and before actually finish the NFC. And UI literally tells user to move the tag away from the phone. So it avoided the default NFC behaviour

Hope it helps

Thank you so much it works perfectly for me

Muhammadjaved7209 avatar Sep 07 '23 13:09 Muhammadjaved7209

This is the real workaround to avoid the problem. In your manifest at the end of the main activity put:

<activity ... >
   ...
   <intent-filter>
      <action android:name="android.nfc.action.TECH_DISCOVERED" />
      <category android:name="android.intent.category.DEFAULT" />
   </intent-filter>
   <meta-data
      android:name="android.nfc.action.TECH_DISCOVERED"
      android:resource="@xml/nfc_tech_filter"/>
</activity>

In res/xml create a new file named "nfc_tech_filter" with the intent types you want to filter:

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.IsoDep</tech>
        <tech>android.nfc.tech.NfcA</tech>
        <tech>android.nfc.tech.NfcB</tech>
        <tech>android.nfc.tech.NfcF</tech>
        <tech>android.nfc.tech.NfcV</tech>
        <tech>android.nfc.tech.Ndef</tech>
        <tech>android.nfc.tech.NdefFormatable</tech>
        <tech>android.nfc.tech.MifareClassic</tech>
        <tech>android.nfc.tech.MifareUltralight</tech>
    </tech-list>
</resources>

In the MainActivity insert the following lines to enable foreground dispatch:

    override fun onResume() {
        super.onResume()
        val adapter: NfcAdapter = NfcAdapter.getDefaultAdapter(this)
        val pendingIntent: PendingIntent = PendingIntent.getActivity(
                this, 0, Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0)
        adapter.enableForegroundDispatch(this, pendingIntent, null, null)
    }
    override fun onPause() {
        super.onPause()
        val adapter: NfcAdapter = NfcAdapter.getDefaultAdapter(this)
        adapter.disableForegroundDispatch(this)
    }

Thank you for this solution, works nicely. Only thing to add is that NfcAdapter.getDefaultAdapter(this) returns null when the device doesn't have a NFC reader, and thus will throw a nice NullPointerException. So just make the NfcAdapter type nullable, and handle the null, for example:

     override fun onResume() {
         super.onResume()
         val adapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this)
         val pendingIntent: PendingIntent = PendingIntent.getActivity(
                 this, 0, Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), PendingIntent.FLAG_IMMUTABLE)
         adapter?.enableForegroundDispatch(this, pendingIntent, null, null)
     }
     override fun onPause() {
         super.onPause()
         val adapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this)
         adapter?.disableForegroundDispatch(this)
     }

sampie777 avatar Sep 19 '23 06:09 sampie777

@sampie777 Where do you put those functions?

My MainActivity.kt looks like, but I get errors when I try to run:

package com.myApp.myApp

import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity: FlutterFragmentActivity() {
    override fun onResume() {
        super.onResume()
        val adapter: NfcAdapter = NfcAdapter.getDefaultAdapter(this)
        val pendingIntent: PendingIntent = PendingIntent.getActivity(
                this, 0, Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0)
        adapter.enableForegroundDispatch(this, pendingIntent, null, null)
    }
    override fun onPause() {
        super.onPause()
        val adapter: NfcAdapter = NfcAdapter.getDefaultAdapter(this)
        adapter.disableForegroundDispatch(this)
    }
}

Errors:

e: file:///C:/Users/User/StudioProjects/app/android/app/src/main/kotlin/com/myApp/app/MainActivity.kt:8:22 Unresolved reference: NfcAdapter
e: file:///C:/Users/User/StudioProjects/app/android/app/src/main/kotlin/com/myApp/app/MainActivity.kt:8:35 Unresolved reference: NfcAdapter
e: file:///C:/Users/User/StudioProjects/app/android/app/src/main/kotlin/com/myApp/app/MainActivity.kt:9:28 Unresolved reference: PendingIntent
e: file:///C:/Users/User/StudioProjects/app/android/app/src/main/kotlin/com/myApp/app/MainActivity.kt:9:44 Unresolved reference: PendingIntent
e: file:///C:/Users/User/StudioProjects/app/android/app/src/main/kotlin/com/myApp/app/MainActivity.kt:10:26 Unresolved reference: Intent
e: file:///C:/Users/User/StudioProjects/app/android/app/src/main/kotlin/com/myApp/app/MainActivity.kt:10:59 Unresolved reference: Intent
e: file:///C:/Users/User/StudioProjects/app/android/app/src/main/kotlin/com/myApp/app/MainActivity.kt:15:22 Unresolved reference: NfcAdapter
e: file:///C:/Users/User/StudioProjects/app/android/app/src/main/kotlin/com/myApp/app/MainActivity.kt:15:35 Unresolved reference: NfcAdapter

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:compileDebugKotlin'.
> A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction
   > Compilation error. See log for more details

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 13s
Exception: Gradle task assembleDebug failed with exit code 1

amilkarSingular avatar Sep 22 '23 22:09 amilkarSingular

@amilkarSingular yeah you are missing imports. Should be something like this:

package <yourpackage>

import android.app.PendingIntent
import android.content.Intent
import android.nfc.NfcAdapter
import io.flutter.embedding.android.FlutterActivity

class MainActivity : FlutterActivity() {
     override fun onResume() {
         super.onResume()
         val adapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this)
         val pendingIntent: PendingIntent = PendingIntent.getActivity(
                 this, 0, Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), PendingIntent.FLAG_IMMUTABLE)
         adapter?.enableForegroundDispatch(this, pendingIntent, null, null)
     }

     override fun onPause() {
         super.onPause()
         val adapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this)
         adapter?.disableForegroundDispatch(this)
     }
}

sampie777 avatar Sep 23 '23 08:09 sampie777

@sampie777 should this code work in case I want to listen for poll results when my app is in background state (not terminated)

zigapovhe avatar Oct 06 '23 14:10 zigapovhe

@sampie777 should this code work in case I want to listen for poll results when my app is in background state (not terminated)

I don't know, as I've not tested this and I've not dug that deep into the code. As far as I know, the code only prevents Android from executing its default handler for NFC scans while the app is in foreground. The app still receives these scans.

What happens when the app is in background: I don't know. Android will resume executing the default handler for NFC scans, but I think it's not part of this code if your app will also receive these scans. You may have to register your app as a default NFC scan listener or maybe just remove the onPause method (again, I don't know what implications this might have).

The foreground lifetime of an activity happens between a call to onResume() until a corresponding call to onPause(). https://developer.android.com/reference/android/app/Activity.html?is-external=true#activity-lifecycle

sampie777 avatar Oct 09 '23 08:10 sampie777

Not Working in android because "mifareInfo":"{type=ultralight_c, size=176, blockSize=4, blockCount=44, sectorCount=null}"

AndriiKaravan avatar Nov 03 '23 19:11 AndriiKaravan

Not Working in android because "mifareInfo":"{type=ultralight_c, size=176, blockSize=4, blockCount=44, sectorCount=null}"

Please see #126

Harry-Chen avatar Nov 04 '23 04:11 Harry-Chen