HCE Support

Chris Bagley wrote:

I’ll make one of those now then :slight_smile:

Thanks Chris, I’d really appreciate that! :slight_smile: I’m still quite shaky with Interop and it seems a bit daunting for a case like this.

And here it is: https://github.com/cbaggers/FuseAndroidNFCExample

It’s pretty basic but it should give enough meat to get started. If you run the example it will show the message 'YAY!` if you scan an NFC device (I used my office door key).

I hope this helps getting started.

@duncangleeddean feel free to fire questions at us, we will try to get them answered :slight_smile:

Dealing with the native platforms is always ‘interesting’, we have tried a number of different approaches and so far ForeignCode has been the one that put up the least resistance for getting things done, however it doesnt stop it from being tricky.

It’ll be nice when we and the community have a bigger pool of libraries for us all to rely on. Exciting times ahead!

Thanks Chris!

Fuse is definitely the platform for me and you guys have a really great team.

Things just keep getting better!

Hi guys!
I just found this post and was wondering if what you describe here applies only to Android or if it can also be used over iOS devices.

Thanks in advance!

@Damian: the example implementation Chris made only works on Android, but it could be considered a very good starting point for adding iOS support too. That will of course involve finding a CocoaPod library and writing a Foreign code wrapper for it in Uno.

Hi Chris,

I am kind of stuck at this point because I cannot read the NFC Mifare card using the sample code you provided at:

Somehow, I don’t think the onIntent method is invoked and that means the listener somehow does not seem to work.

My phone is Honor 8 Pro running on Android 7
The card that I am using is MiFare
The card content was picked up by some other NFC Reader that I have installed on my phone

My NFC module

using Uno;
using Uno.UX;
using Uno.Threading;
using Uno.Text;
using Uno.Platform;
using Uno.Compiler.ExportTargetInterop;
using Uno.Collections;
using Fuse;
using Fuse.Scripting;
using Fuse.Reactive;

namespace Fuse.NFC
{
    /**
    */
    [UXGlobalModule]
    public sealed class NFCModule : NativeEventEmitterModule
    {
        static readonly NFCModule _instance;

        public NFCModule() : base(true, "tagDiscovered", "techDiscovered", "ndefDiscovered")
        {
            if(_instance != null) return;
            Uno.UX.Resource.SetGlobalKey(_instance = this, "NFC");

            var tagDiscovered = new NativeEvent("tagDiscovered");
            On("tagDiscovered", tagDiscovered);
            AddMember(tagDiscovered);

            var ndefDiscovered = new NativeEvent("ndefDiscovered");
            On("ndefDiscovered", ndefDiscovered);
            AddMember(ndefDiscovered);

            var techDiscovered = new NativeEvent("techDiscovered");
            On("techDiscovered", techDiscovered);
            AddMember(techDiscovered);

            Scanner.Init();
        }

        public static void OnTagDiscovered(string message)
        {
            debug_log("tagDiscovered ...");
            _instance.Emit("tagDiscovered", message);
        }

        public static void OnNdefDiscovered(string message)
        {
            _instance.Emit("ndefDiscovered", message);
        }
    }

    [ForeignInclude(Language.Java, 
        "android.nfc.NfcAdapter", 
        "android.content.Intent",
        "android.widget.Toast",
        "android.app.Activity",
        "android.nfc.NdefMessage",
        "android.nfc.NdefRecord",
        "android.nfc.cardemulation.HostApduService",
        "android.os.Bundle",
        "android.util.Log"
    )]
    extern(android)
    class Scanner
    {
        static Java.Object _handle = null;
        static Java.Object _listener = null;

        [Foreign(Language.Java)]
        public static void Init()
        @{
            NfcAdapter adapter = NfcAdapter.getDefaultAdapter(com.fuse.Activity.getRootActivity());
            if (adapter != null)
            {
                debug_log("Device has NFC Support");
                com.fuse.Activity.IntentListener listener = new com.fuse.Activity.IntentListener()
                {
                    public void onIntent (Intent newIntent)
                    {
                        debug_log("onIntent triggered...");
                        @{NFCModule.OnTagDiscovered(string):Call("YAY!")};
                    }
                };
                com.fuse.Activity.subscribeToIntents(listener, NfcAdapter.ACTION_NDEF_DISCOVERED);
                com.fuse.Activity.subscribeToIntents(listener, NfcAdapter.ACTION_TECH_DISCOVERED);
                com.fuse.Activity.subscribeToIntents(listener, NfcAdapter.ACTION_TAG_DISCOVERED);
                @{Scanner._listener:Set(listener)};
                @{Scanner._handle:Set(adapter)};
            }
            else
            {
                debug_log("NFC not supported");
            }
        @}

        // [Foreign(Language.Java)]
        // public static string Send()
        // @{
        //     Activity context = com.fuse.Activity.getRootActivity();
        //     context.runOnUiThread(new Runnable(){
        //         public void run(){
                    // Alert toast = new Alert();
                    // toast.test(com.fuse.Activity.getRootActivity(), "Some Toast Example :)");
        //         }
        //     });
            
        //     return "done";
        // @}
    }

    extern(!android)
    class Scanner
    {
        public static void Init() {}
        // public static string Send() { return ""; }
    }
}

My Manifest File

<Extensions Backend="CPlusPlus" Condition="Android">
		
		<Require Android.ResStrings.Declaration><![CDATA[

		<string name="aiddescription">aiddescription</string>
    	<string name="servicedesc">servicedesc</string>

		]]></Require>

		<Require AndroidManifest.RootElement><![CDATA[
		
			<uses-permission android:name="android.permission.NFC"/>
		
		]]></Require>
    
		<Require AndroidManifest.ActivityElement><![CDATA[

		<intent-filter>
			<action android:name="android.nfc.action.TAG_DISCOVERED"/>
			<category android:name="android.intent.category.DEFAULT"/>
		</intent-filter>
    
		<intent-filter>
            <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
        </intent-filter>

		<intent-filter>
            <action android:name="android.nfc.action.TECH_DISCOVERED"/>
        </intent-filter>

		<tech-list>
        	<tech>android.nfc.tech.Ndef</tech>
			<tech>android.nfc.tech.IsoDep</tech>
			<tech>android.nfc.tech.MifareClassic</tech>
			<tech>android.nfc.tech.MifareUltralight</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-list>

		]]></Require>

</Extensions>

My Test.js file

var Observable = require("FuseJS/Observable");
var NFC = require("NFC");

var errorMessage = Observable("");
var pageWidth = Observable();
var info = Observable("");

function test(){
	// First thing first do simple hello world beam
	//console.log("NFC: ", JSON.stringify(NFC));
}

function pagePlaced(args){
	pageWidth.value = args.width;
}

NFC.on("tagDiscovered", function(message){
	console.log("message: ", message);
});

module.exports = {
    test: test,
    errorMessage: errorMessage,
    pagePlaced: pagePlaced,
	pageWidth: pageWidth,
	info: info
};

@Nadzaruddin: if you’re using custom hardware, I imagine your code should load some drivers, use a 3rd party SDK, perform some sort of “device search” or something along those lines. Not much else we can suggest at this point.

@Uldis: It’s a not a custom hardware. Just to shed some more light, I used the following code with Android Studio and it worked fine (managed to pick up the card content).

package com.example.android.cardemulation;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentFilter.MalformedMimeTypeException;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.Ndef;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;

import java.io.UnsupportedEncodingException;
import java.util.Arrays;

public class MainActivity extends AppCompatActivity {

    public static final String MIME_TEXT_PLAIN = "text/plain";
    public static final String TAG = "NfcDemo";

     private TextView mTextView = null;
     private NfcAdapter mNfcAdapter = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTextView = (TextView) findViewById(R.id.textView_explanation);

        mNfcAdapter = NfcAdapter.getDefaultAdapter(this);

        if (mNfcAdapter == null) {
            // Stop here, we definitely need NFC
            Toast.makeText(this, "This device doesn't support NFC.", Toast.LENGTH_LONG).show();
            finish();
            return;

        }

        if (!mNfcAdapter.isEnabled()) {
            mTextView.setText("NFC is disabled.");
        } else {
            mTextView.setText("NFC is enabled");
        }

        handleIntent(getIntent());
    }


    @Override
    protected void onResume() {
        super.onResume();

        /*
         * It's important, that the activity is in the foreground (resumed). Otherwise
         * an IllegalStateException is thrown.
         */
        setupForegroundDispatch(this, mNfcAdapter);
    }

    @Override
    protected void onPause() {
        /*
         * Call this before onPause, otherwise an IllegalArgumentException is thrown as well.
         */
        stopForegroundDispatch(this, mNfcAdapter);

        super.onPause();
    }


    @Override
    protected void onNewIntent(Intent intent) {
        /*
         * This method gets called, when a new Intent gets associated with the current activity instance.
         * Instead of creating a new activity, onNewIntent will be called. For more information have a look
         * at the documentation.
         *
         * In our case this method gets called, when the user attaches a Tag to the device.
         */
        handleIntent(intent);
    }

    private void handleIntent(Intent intent) {
        String action = intent.getAction();
        if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {

            String type = intent.getType();
            if (MIME_TEXT_PLAIN.equals(type)) {

                Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
                new NdefReaderTask().execute(tag);

            } else {
                Log.d(TAG, "Wrong mime type: " + type);
            }
        } else if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(action)) {

            // In case we would still use the Tech Discovered Intent
            Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
            String[] techList = tag.getTechList();
            String searchedTech = Ndef.class.getName();

            for (String tech : techList) {
                if (searchedTech.equals(tech)) {
                    new NdefReaderTask().execute(tag);
                    break;
                }
            }
        }
    }


    /*
     * @param activity The corresponding {@link Activity} requesting the foreground dispatch.
     * @param adapter The {@link NfcAdapter} used for the foreground dispatch.
     */
    public static void setupForegroundDispatch(final Activity activity, NfcAdapter adapter) {
        final Intent intent = new Intent(activity.getApplicationContext(), activity.getClass());
        intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);

        final PendingIntent pendingIntent = PendingIntent.getActivity(activity.getApplicationContext(), 0, intent, 0);

        IntentFilter[] filters = new IntentFilter[1];
        String[][] techList = new String[][]{};

        // Notice that this is the same filter as in our manifest.
        filters[0] = new IntentFilter();
        filters[0].addAction(NfcAdapter.ACTION_NDEF_DISCOVERED);
        filters[0].addCategory(Intent.CATEGORY_DEFAULT);
        try {
            filters[0].addDataType(MIME_TEXT_PLAIN);
        } catch (MalformedMimeTypeException e) {
            throw new RuntimeException("Check your mime type.");
        }

        adapter.enableForegroundDispatch(activity, pendingIntent, filters, techList);
    }


    /*
     * @param activity The corresponding {@linkBaseActivity} requesting to stop the foreground dispatch.
     * @param adapter The {@link NfcAdapter} used for the foreground dispatch.
     */
    public static void stopForegroundDispatch(final Activity activity, NfcAdapter adapter) {
        adapter.disableForegroundDispatch(activity);
    }

    /**
     * Background task for reading the data. Do not block the UI thread while reading.
     *
     * @author Ralf Wondratschek
     *
     */
    private class NdefReaderTask extends AsyncTask<Tag, Void, String> {

        @Override
        protected String doInBackground(Tag... params) {
            Tag tag = params[0];

            Ndef ndef = Ndef.get(tag);
            if (ndef == null) {
                // NDEF is not supported by this Tag.
                return null;
            }

            NdefMessage ndefMessage = ndef.getCachedNdefMessage();

            NdefRecord[] records = ndefMessage.getRecords();
            for (NdefRecord ndefRecord : records) {
                if (ndefRecord.getTnf() == NdefRecord.TNF_WELL_KNOWN && Arrays.equals(ndefRecord.getType(), NdefRecord.RTD_TEXT)) {
                    try {
                        return readText(ndefRecord);
                    } catch (UnsupportedEncodingException e) {
                        Log.e(TAG, "Unsupported Encoding", e);
                    }
                }
            }

            return null;
        }


        private String readText(NdefRecord record) throws UnsupportedEncodingException {
        /*
         * See NFC forum specification for "Text Record Type Definition" at 3.2.1
         *
         * http://www.nfc-forum.org/specs/
         *
         * bit_7 defines encoding
         * bit_6 reserved for future use, must be 0
         * bit_5..0 length of IANA language code
         */

            byte[] payload = record.getPayload();

            // Get the Text Encoding
            String textEncoding = ((payload[0] & 128) == 0) ? "UTF-8" : "UTF-16";

            // Get the Language Code
            int languageCodeLength = payload[0] & 0063;

            // String languageCode = new String(payload, 1, languageCodeLength, "US-ASCII");
            // e.g. "en"

            // Get the Text
            return new String(payload, languageCodeLength + 1, payload.length - languageCodeLength - 1, textEncoding);
        }

        @Override
        protected void onPostExecute(String result) {
            if (result != null) {
                mTextView.setText("Read content: " + result);
            }
        }


    }
}

@Nadzaruddin: if you have a working native example, you could run fuse build -tandroid -d and then check in Android Studio how the generated project differs from that example of yours. Should be fairly easy to spot the difference.

That said, I’d like to leave this thread alone, since the original issue was resolved. If you still have problems after you’ve done some debugging, please make a new forum post and include everything that could help us help you.