Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android default NFC reader gets triggered #17

Open
aghyad97 opened this issue Nov 11, 2020 · 26 comments
Open

Android default NFC reader gets triggered #17

aghyad97 opened this issue Nov 11, 2020 · 26 comments

Comments

@aghyad97
Copy link

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?

@Harry-Chen
Copy link
Contributor

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:

var readerFlags = FLAG_READER_NFC_A or FLAG_READER_NFC_B or FLAG_READER_NFC_V or FLAG_READER_NFC_F
if (!platformSound) {
readerFlags = readerFlags or FLAG_READER_NO_PLATFORM_SOUNDS
}
if (!checkNDEF) {
readerFlags = readerFlags or FLAG_READER_SKIP_NDEF_CHECK
}

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

@aghyad97
Copy link
Author

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.

@Harry-Chen
Copy link
Contributor

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.

@Mythar
Copy link

Mythar commented Apr 11, 2021

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);

@Harry-Chen
Copy link
Contributor

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
Copy link
Contributor

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 #17 (comment).

@jacopofranza
Copy link

Hello, is there any update regarding this issue?

@Harry-Chen
Copy link
Contributor

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.

@Mythar

This comment has been minimized.

@jibebec
Copy link

jibebec commented Nov 21, 2021

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 ) ?

@Mythar
Copy link

Mythar commented Nov 22, 2021

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.

@jacopofranza
Copy link

jacopofranza commented Dec 15, 2021

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)
    }

@Harry-Chen
Copy link
Contributor

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

@Mythar
Copy link

Mythar commented Dec 22, 2021

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
    );
  }
}

@timnew
Copy link
Contributor

timnew commented Jan 20, 2022

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

@PietroGranati
Copy link

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

@luoqiang110
Copy link

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 ?

@Harry-Chen Harry-Chen pinned this issue Sep 6, 2023
@Muhammadjaved7209
Copy link

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

@sampie777
Copy link

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)
     }

@amilkarSingular
Copy link

amilkarSingular commented Sep 22, 2023

@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

@sampie777
Copy link

@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)
     }
}

@zigapovhe
Copy link

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

@sampie777
Copy link

@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

@AndriiKaravan

This comment was marked as off-topic.

@Harry-Chen

This comment was marked as off-topic.

@brux88
Copy link

brux88 commented Aug 19, 2024

are there no solutions beyond the previous walkaround?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests