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

NFC functionality added #6

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.bdk.f.bdk_flutter_app">

<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc.hce" android:required="true" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
Expand Down Expand Up @@ -30,5 +32,16 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />

<service android:name=".MyHostApduService" android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
<!-- category required!!! this was not included in official android HCE documentation -->
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<meta-data android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/apduservice"/>
</service>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,6 +1,92 @@
package com.bdk.f.bdk_flutter_app

import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import android.nfc.NfcAdapter
import io.flutter.Log
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.util.Calendar
import java.util.Timer
import java.util.TimerTask


class MainActivity : FlutterActivity() {
private val CHANNEL = "flutter_hce"
private var nfcAdapter: NfcAdapter? = null
private var pendingIntent: PendingIntent? = null
private val TAG = "MainActivity"
private lateinit var nfcReader: NfcReader

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
nfcReader = NfcReader(this, methodChannel) // Pass context and channel

methodChannel.setMethodCallHandler { call, result ->
when (call.method) {
"sendNfcMessage" -> {
val dataToSend = call.argument<String>("content")
dataToSend?.let { sendNFCMessage(it) }
result.success(null)
}
"readNfcMessage" -> {
nfcReader.startNfcReading()
result.success(null)
}
"isNfcEnabled" -> {
if (isNfcEnabled()) {
result.success("true")
} else {
result.success("false")
}
}
"isNfcHceSupported" -> {
if (isNfcHceSupported()) {
result.success("true")
} else {
result.success("false")
}
}
"stopNfcHce" -> {
stopNfcHce()
result.success("success")
}
else -> result.notImplemented()
}
}
}

override fun onResume() {
super.onResume()
}

override fun onPause() {
super.onPause()
}

private fun sendNFCMessage(text: String) {
Log.d(TAG, "message to send: $text")
val intent = Intent(context, MyHostApduService::class.java)
intent.putExtra("ndefMessage", text);
context.startService(intent)

}

private fun isNfcEnabled(): Boolean {
return nfcAdapter?.isEnabled == true
}
private fun isNfcHceSupported() =
isNfcEnabled() && activity?.packageManager!!.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)

private fun stopNfcHce() {
val intent = Intent(activity, MyHostApduService::class.java)
// activity?.stopService(intent)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package com.bdk.f.bdk_flutter_app

import android.content.Intent
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import android.nfc.cardemulation.HostApduService
import android.os.Build
import android.os.Bundle
import android.util.Log
import java.util.Arrays

/**
* This class emulates a NFC Forum Tag Type 4 containing a NDEF message
* The class uses the AID D2760000850101
*/
class MyHostApduService : HostApduService() {
private lateinit var mNdefRecordFile: ByteArray

override fun onCreate() {
super.onCreate()
mAppSelected = false
mCcSelected = false
mNdefSelected = false
createDefaultMessage()
}

private fun createDefaultMessage() {
val ndefDefaultMessage = createNdefMessage(DEFAULT_MESSAGE,"text/plain", NDEF_ID)
// the maximum length is 246
val ndefLen = ndefDefaultMessage!!.byteArrayLength
mNdefRecordFile = ByteArray(ndefLen + 2)
mNdefRecordFile[0] = ((ndefLen and 0xff00) / 256).toByte()
mNdefRecordFile[1] = (ndefLen and 0xff).toByte()
System.arraycopy(
ndefDefaultMessage.toByteArray(),
0,
mNdefRecordFile,
2,
ndefDefaultMessage.byteArrayLength
)
}

override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
if (intent != null) {
// intent contains a text message
if (intent.hasExtra("ndefMessage")) {
val ndef = intent.getStringExtra("ndefMessage")
// val ndefMessage = createNdefMessage(intent.getStringExtra("ndefMessage"))
if (ndef != null) {
val ndefMessage = createNdefMessage(ndef, "text/plain", NDEF_ID)

val ndefLen = ndefMessage.byteArrayLength
mNdefRecordFile = ByteArray(ndefLen + 2)
mNdefRecordFile[0] = ((ndefLen and 0xff00) / 256).toByte()
mNdefRecordFile[1] = (ndefLen and 0xff).toByte()
System.arraycopy(
ndefMessage.toByteArray(),
0,
mNdefRecordFile,
2,
ndefMessage.byteArrayLength
)
}
}
}
return super.onStartCommand(intent, flags, startId)
}

/* private fun createNdefMessage(ndefData: String?): NdefMessage? {
if (ndefData!!.isEmpty()) {
return null
}
var ndefRecord: NdefRecord? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
ndefRecord = NdefRecord.createTextRecord("en", ndefData)
}
return NdefMessage(ndefRecord)
} */

private fun createNdefMessage(content: String, mimeType: String, id: ByteArray): NdefMessage {
Log.i(TAG, "createNdefMessage(): $content")

if(mimeType == "text/plain") {
return createTextRecord("en", content, id);
}

val type = mimeType.toByteArray(charset("US-ASCII"))
val payload = content.toByteArray(charset("UTF-8"))

return NdefMessage(NdefRecord(NdefRecord.TNF_MIME_MEDIA, type, id, payload))
}

private fun createTextRecord(language: String, text: String, id: ByteArray): NdefMessage {
val languageBytes: ByteArray
val textBytes: ByteArray
try {
languageBytes = language.toByteArray(charset("US-ASCII"))
textBytes = text.toByteArray(charset("UTF-8"))
} catch (e: Error) {
throw AssertionError(e)
}

val recordPayload = ByteArray(1 + (languageBytes.size and 0x03F) + textBytes.size)

recordPayload[0] = (languageBytes.size and 0x03F).toByte()
System.arraycopy(languageBytes, 0, recordPayload, 1, languageBytes.size and 0x03F)
System.arraycopy(
textBytes,
0,
recordPayload,
1 + (languageBytes.size and 0x03F),
textBytes.size,
)

return NdefMessage(NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, id, recordPayload))
}

/**
* emulates an NFC Forum Tag Type 4
*/
override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
if (extras == null) {
Log.d(TAG, "Received null extras in processCommandApdu")
// Handle the case where extras is null if necessary
}
Log.d(TAG, "commandApdu: " + Utils.bytesToHex(commandApdu))
//if (Arrays.equals(SELECT_APP, commandApdu)) {
// check if commandApdu qualifies for SELECT_APPLICATION
if (SELECT_APPLICATION.contentEquals(commandApdu)) {
mAppSelected = true
mCcSelected = false
mNdefSelected = false
Log.d(TAG, "responseApdu: " + Utils.bytesToHex(SUCCESS_SW))
return SUCCESS_SW
// check if commandApdu qualifies for SELECT_CAPABILITY_CONTAINER
} else if (mAppSelected && SELECT_CAPABILITY_CONTAINER.contentEquals(commandApdu)) {
mCcSelected = true
mNdefSelected = false
Log.d(TAG, "responseApdu: " + Utils.bytesToHex(SUCCESS_SW))
return SUCCESS_SW
// check if commandApdu qualifies for SELECT_NDEF_FILE
} else if (mAppSelected && SELECT_NDEF_FILE.contentEquals(commandApdu)) {
// NDEF
mCcSelected = false
mNdefSelected = true
Log.d(TAG, "responseApdu: " + Utils.bytesToHex(SUCCESS_SW))
return SUCCESS_SW
// check if commandApdu qualifies for // READ_BINARY
} else if (commandApdu[0] == 0x00.toByte() && commandApdu[1] == 0xb0.toByte()) {
// READ_BINARY
// get the offset an le (length) data
//System.out.println("** " + Utils.bytesToHex(commandApdu) + " in else if (commandApdu[0] == (byte)0x00 && commandApdu[1] == (byte)0xb0) {");
val offset =
(0x00ff and commandApdu[2].toInt()) * 256 + (0x00ff and commandApdu[3].toInt())
val le = 0x00ff and commandApdu[4].toInt()
val responseApdu = ByteArray(le + SUCCESS_SW.size)
if (mCcSelected && offset == 0 && le == CAPABILITY_CONTAINER_FILE.size) {
System.arraycopy(CAPABILITY_CONTAINER_FILE, offset, responseApdu, 0, le)
System.arraycopy(SUCCESS_SW, 0, responseApdu, le, SUCCESS_SW.size)
Log.d(TAG, "responseApdu: " + Utils.bytesToHex(responseApdu))
return responseApdu
} else if (mNdefSelected) {
if (offset + le <= mNdefRecordFile.size) {
System.arraycopy(mNdefRecordFile, offset, responseApdu, 0, le)
System.arraycopy(SUCCESS_SW, 0, responseApdu, le, SUCCESS_SW.size)
Log.d(TAG, "responseApdu: " + Utils.bytesToHex(responseApdu))
return responseApdu
}
}
}

// The tag should return different errors for different reasons
// this emulation just returns the general error message
Log.d(TAG, "responseApdu: " + Utils.bytesToHex(FAILURE_SW))
return FAILURE_SW
}

/**
* onDeactivated is called when reading ends
* reset the status boolean values
*/
override fun onDeactivated(reason: Int) {
mAppSelected = false
mCcSelected = false
mNdefSelected = false
Log.i(TAG, "onDeactivated() Reason: $reason")

}

companion object {
private const val TAG = "MyHostApduService"
const val DEFAULT_MESSAGE = "This is the default message."
private var mAppSelected = false // true when SELECT_APPLICATION detected
private var mCcSelected = false // true when SELECT_CAPABILITY_CONTAINER detected
private var mNdefSelected = false // true when SELECT_NDEF_FILE detected
private val NDEF_ID = byteArrayOf(0xE1.toByte(), 0x04.toByte())

private val SELECT_APPLICATION = byteArrayOf(
0x00.toByte(),
0xA4.toByte(),
0x04.toByte(),
0x00.toByte(),
0x07.toByte(),
0xD2.toByte(),
0x76.toByte(),
0x00.toByte(),
0x00.toByte(),
0x85.toByte(),
0x01.toByte(),
0x01.toByte(),
0x00.toByte()
)
private val SELECT_CAPABILITY_CONTAINER = byteArrayOf(
0x00.toByte(),
0xa4.toByte(),
0x00.toByte(),
0x0c.toByte(),
0x02.toByte(),
0xe1.toByte(),
0x03.toByte()
)
private val SELECT_NDEF_FILE = byteArrayOf(
0x00.toByte(),
0xa4.toByte(),
0x00.toByte(),
0x0c.toByte(),
0x02.toByte(),
0xE1.toByte(),
0x04.toByte()
)
private val CAPABILITY_CONTAINER_FILE = byteArrayOf(
0x00,
0x0f, // CCLEN
0x20, // Mapping Version
0x00,
0x3b, // Maximum R-APDU data size
0x00,
0x34, // Maximum C-APDU data size
0x04,
0x06,
0xe1.toByte(),
0x04,
0x00.toByte(),
0xff.toByte(), // Maximum NDEF size, do NOT extend this value
0x00,
0xff.toByte()
)

// Status Word success
private val SUCCESS_SW = byteArrayOf(0x90.toByte(), 0x00.toByte())

// Status Word failure
private val FAILURE_SW = byteArrayOf(0x6a.toByte(), 0x82.toByte())
}
}
Loading