@exxili/capacitor-nfc 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,465 +1,493 @@
1
- package com.exxili.capacitornfc
2
-
3
- import android.app.ActivityOptions
4
- import android.app.PendingIntent
5
- import android.content.Intent
6
- import android.content.IntentFilter
7
- import android.nfc.NdefMessage
8
- import android.nfc.NdefRecord
9
- import android.nfc.NfcAdapter
10
- import android.nfc.NfcAdapter.ACTION_NDEF_DISCOVERED
11
- import android.nfc.NfcAdapter.ACTION_TAG_DISCOVERED
12
- import android.nfc.NfcAdapter.ACTION_TECH_DISCOVERED
13
- import android.nfc.NfcAdapter.EXTRA_NDEF_MESSAGES
14
- import android.nfc.NfcAdapter.getDefaultAdapter
15
- import android.nfc.Tag
16
- import android.nfc.tech.IsoDep
17
- import android.nfc.tech.MifareClassic
18
- import android.nfc.tech.MifareUltralight
19
- import android.nfc.tech.Ndef
20
- import android.nfc.tech.NdefFormatable
21
- import android.nfc.tech.NfcA
22
- import android.nfc.tech.NfcB
23
- import android.nfc.tech.NfcBarcode
24
- import android.nfc.tech.NfcF
25
- import android.nfc.tech.NfcV
26
- import android.os.Build
27
- import android.os.Bundle
28
- import android.util.Log
29
- import androidx.annotation.RequiresApi
30
- import com.getcapacitor.JSArray
31
- import com.getcapacitor.JSObject
32
- import com.getcapacitor.Plugin
33
- import com.getcapacitor.PluginCall
34
- import com.getcapacitor.PluginMethod
35
- import com.getcapacitor.annotation.CapacitorPlugin
36
- import org.json.JSONObject
37
- import java.io.IOException
38
- import java.io.UnsupportedEncodingException
39
- import java.nio.charset.Charset
40
- import java.util.Base64
41
-
42
- @CapacitorPlugin(name = "NFC")
43
- class NFCPlugin : Plugin() {
44
- private var writeMode = false
45
- private var recordsBuffer: JSArray? = null
46
-
47
- private val techListsArray = arrayOf(arrayOf<String>(
48
- IsoDep::class.java.name,
49
- MifareClassic::class.java.name,
50
- MifareUltralight::class.java.name,
51
- Ndef::class.java.name,
52
- NdefFormatable::class.java.name,
53
- NfcBarcode::class.java.name,
54
- NfcA::class.java.name,
55
- NfcB::class.java.name,
56
- NfcF::class.java.name,
57
- NfcV::class.java.name
58
- ))
59
-
60
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
61
- public override fun handleOnNewIntent(intent: Intent?) {
62
- super.handleOnNewIntent(intent)
63
-
64
- if (intent == null || intent.action.isNullOrBlank()) {
65
- return
66
- }
67
-
68
- if (writeMode) {
69
- Log.d("NFC", "WRITE MODE START")
70
- handleWriteTag(intent)
71
- writeMode = false
72
- recordsBuffer = null
73
- }
74
- else if (ACTION_NDEF_DISCOVERED == intent.action || ACTION_TAG_DISCOVERED == intent.action || ACTION_TECH_DISCOVERED == intent.action) {
75
- Log.d("NFC", "READ MODE START")
76
- handleReadTag(intent)
77
- }
78
- }
79
-
80
- @PluginMethod
81
- fun isSupported(call: PluginCall) {
82
- val adapter = NfcAdapter.getDefaultAdapter(this.activity)
83
- val ret = JSObject()
84
- ret.put("supported", adapter != null)
85
- call.resolve(ret)
86
- }
87
-
88
- @PluginMethod
89
- fun cancelWriteAndroid(call: PluginCall) {
90
- this.writeMode = false
91
- call.resolve()
92
- }
93
-
94
- @PluginMethod
95
- fun startScan(call: PluginCall) {
96
- print("startScan called")
97
- call.reject("Android NFC scanning does not require 'startScan' method.")
98
- }
99
-
100
- @PluginMethod
101
- fun writeNDEF(call: PluginCall) {
102
- print("writeNDEF called")
103
-
104
- writeMode = true
105
- recordsBuffer = call.getArray("records")
106
-
107
- call.resolve()
108
- }
109
-
110
- override fun handleOnPause() {
111
- super.handleOnPause()
112
- getDefaultAdapter(this.activity)?.disableForegroundDispatch(this.activity)
113
- }
114
-
115
- override fun handleOnResume() {
116
- super.handleOnResume()
117
- if(getDefaultAdapter(this.activity) == null) return;
118
-
119
- val intent = Intent(context, this.activity.javaClass).apply {
120
- addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
121
- }
122
-
123
- val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
124
- PendingIntent.FLAG_MUTABLE
125
- } else {
126
- PendingIntent.FLAG_UPDATE_CURRENT
127
- }
128
-
129
- var activityOptionsBundle: Bundle? = null
130
-
131
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // API 35 (Android 15)
132
- activityOptionsBundle = ActivityOptions.makeBasic().apply {
133
- setPendingIntentCreatorBackgroundActivityStartMode(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
134
- }.toBundle()
135
- }
136
-
137
- val pendingIntent =
138
- PendingIntent.getActivity(
139
- this.activity,
140
- 0,
141
- intent,
142
- pendingIntentFlags,
143
- activityOptionsBundle
144
- )
145
-
146
- val intentFilter: Array<IntentFilter> =
147
- arrayOf(
148
- IntentFilter(ACTION_NDEF_DISCOVERED).apply {
149
- try {
150
- addDataType("text/plain")
151
- } catch (e: IntentFilter.MalformedMimeTypeException) {
152
- throw RuntimeException("failed", e)
153
- }
154
- },
155
- IntentFilter(ACTION_TECH_DISCOVERED),
156
- IntentFilter(ACTION_TAG_DISCOVERED)
157
- )
158
-
159
- getDefaultAdapter(this.activity).enableForegroundDispatch(
160
- this.activity,
161
- pendingIntent,
162
- intentFilter,
163
- techListsArray
164
- )
165
- }
166
-
167
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
168
- private fun handleWriteTag(intent: Intent) {
169
- val records = recordsBuffer?.toList<JSONObject>()
170
- if(records != null) {
171
- val ndefRecords = mutableListOf<NdefRecord>()
172
-
173
- try {
174
- for (record in records) {
175
- val payload = record.getJSONArray("payload")
176
- val type: String? = record.getString("type")
177
-
178
- if (payload.length() == 0 || type == null) {
179
- notifyListeners(
180
- "nfcError",
181
- JSObject().put(
182
- "error",
183
- "Invalid record: payload or type is missing."
184
- )
185
- )
186
- return
187
- }
188
-
189
- val payloadBytes = ByteArray(payload.length())
190
- for(i in 0 until payload.length()) {
191
- payloadBytes[i] = payload.getInt(i).toByte()
192
- }
193
-
194
- val (tnf, typeBytes) = when {
195
- type == "T" || type == "U" -> Pair(
196
- NdefRecord.TNF_WELL_KNOWN,
197
- type.toByteArray(Charsets.UTF_8)
198
- )
199
- type.contains("/") -> Pair(
200
- NdefRecord.TNF_MIME_MEDIA,
201
- type.toByteArray(Charsets.US_ASCII)
202
- )
203
- else -> Pair(
204
- NdefRecord.TNF_EXTERNAL_TYPE,
205
- type.toByteArray(Charsets.UTF_8)
206
- )
207
- }
208
-
209
- val record = if (tnf == NdefRecord.TNF_MIME_MEDIA) {
210
- try {
211
- NdefRecord.createMime(type, payloadBytes)
212
- } catch (e: IllegalArgumentException) {
213
- notifyListeners(
214
- "nfcError",
215
- JSObject().put(
216
- "error",
217
- "Invalid MIME type for record"
218
- )
219
- )
220
- return
221
- }
222
- } else {
223
- NdefRecord(
224
- tnf,
225
- typeBytes,
226
- ByteArray(0),
227
- payloadBytes
228
- )
229
- }
230
-
231
- ndefRecords.add(record)
232
- }
233
-
234
- val ndefMessage = NdefMessage(ndefRecords.toTypedArray())
235
- val tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
236
- var ndef = Ndef.get(tag)
237
-
238
- if (ndef == null) {
239
- val formatable = NdefFormatable.get(tag)
240
- if (formatable != null) {
241
- try {
242
- formatable.connect()
243
- val mimeRecord = NdefRecord.createMime("text/plain", "INIT".toByteArray(
244
- Charset.forName("US-ASCII")))
245
- val msg = NdefMessage(mimeRecord)
246
- formatable.format(msg)
247
- // Success!
248
- // Emit event to Capacitor plugin for success
249
- println("Successfully formatted and wrote NDEF message to tag!")
250
- } catch (e: IOException) {
251
- // Error connecting or formatting
252
- // Emit event to Capacitor plugin for error
253
- println("Error formatting or writing to NDEF-formatable tag: ${e.message}")
254
- } catch (e: Exception) { // Catch other potential exceptions during format, like TagLostException
255
- println("Error during NDEF formatting: ${e.message}")
256
- } finally {
257
- try {
258
- formatable.close()
259
- } catch (e: IOException) {
260
- println("Error closing NdefFormatable connection: ${e.message}")
261
- }
262
- }
263
-
264
- ndef = Ndef.get(formatable.tag)
265
- } else {
266
- notifyListeners(
267
- "nfcError",
268
- JSObject().put(
269
- "error",
270
- "Tag does not support NDEF writing."
271
- )
272
- )
273
- return
274
- }
275
- }
276
-
277
- ndef.use { // Use block ensures ndef.close() is called
278
- ndef.connect()
279
- if (!ndef.isWritable) {
280
- notifyListeners(
281
- "nfcError",
282
- JSObject().put(
283
- "error",
284
- "NFC tag is not writable"
285
- )
286
- )
287
- return
288
- }
289
- if (ndef.maxSize < ndefMessage.toByteArray().size) {
290
- notifyListeners(
291
- "nfcError",
292
- JSObject().put(
293
- "error",
294
- "Message too large for this NFC Tag (max ${ndef.maxSize} bytes)."
295
- )
296
- )
297
- return
298
- }
299
-
300
- ndef.writeNdefMessage(ndefMessage)
301
- Log.d("NFC", "NDEF message successfully written to tag.")
302
- }
303
-
304
- notifyListeners("nfcWriteSuccess", JSObject().put("success", true))
305
- }
306
- catch (e: UnsupportedEncodingException) {
307
- Log.e("NFC", "Encoding error during NDEF record creation: ${e.message}")
308
- notifyListeners(
309
- "nfcError",
310
- JSObject().put(
311
- "error",
312
- "Encoding error: ${e.message}"
313
- )
314
- )
315
- }
316
- catch (e: IOException) {
317
- Log.e("NFC", "I/O error during NFC write: ${e.message}")
318
- notifyListeners(
319
- "nfcError",
320
- JSObject().put(
321
- "error",
322
- "NFC I/O error: ${e.message}"
323
- )
324
- )
325
- }
326
- catch (e: Exception) {
327
- Log.e("NFC", "Error writing NDEF message: ${e.message}", e)
328
- notifyListeners(
329
- "nfcError",
330
- JSObject().put(
331
- "error",
332
- "Failed to write NDEF message: ${e.message}"
333
- )
334
- )
335
- }
336
- }
337
- else {
338
- notifyListeners("nfcError", JSObject().put("error", "Failed to write NFC tag"))
339
- }
340
- }
341
-
342
- @RequiresApi(Build.VERSION_CODES.TIRAMISU)
343
- private fun handleReadTag(intent: Intent) {
344
- val jsResponse = JSObject()
345
- val ndefMessages = JSArray()
346
-
347
- // Get tag information regardless of NDEF content
348
- val tag: Tag? = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
349
- val tagInfo = tag?.let { extractTagInfo(it) }
350
-
351
- // Try to obtain raw NDEF messages first (ACTION_NDEF_DISCOVERED path)
352
- val receivedMessages = intent.getParcelableArrayExtra(
353
- EXTRA_NDEF_MESSAGES,
354
- NdefMessage::class.java
355
- )
356
-
357
- if (receivedMessages != null && receivedMessages.isNotEmpty()) {
358
- // Standard NDEF-discovered path
359
- for (message in receivedMessages) {
360
- ndefMessages.put(ndefMessageToJS(message))
361
- }
362
- } else {
363
- // For ACTION_TAG_DISCOVERED or ACTION_TECH_DISCOVERED we may still have an NDEF tag.
364
- var added = false
365
- if (tag != null) {
366
- val ndef = Ndef.get(tag)
367
- if (ndef != null) {
368
- try {
369
- ndef.connect()
370
- // Prefer cached message to avoid additional IO if available
371
- val message: NdefMessage? = ndef.cachedNdefMessage ?: try {
372
- ndef.ndefMessage
373
- } catch (e: Exception) { null }
374
- if (message != null) {
375
- ndefMessages.put(ndefMessageToJS(message))
376
- added = true
377
- }
378
- } catch (e: Exception) {
379
- Log.w("NFC", "Failed to read NDEF message from TECH/TAG intent: ${e.message}")
380
- } finally {
381
- try { ndef.close() } catch (_: Exception) {}
382
- }
383
- }
384
-
385
- // If no NDEF message found, fallback to tag ID (legacy behavior)
386
- if (!added) {
387
- val tagId = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID) ?: tag.id
388
- val result = if (tagId != null) byteArrayToHexString(tagId) else ""
389
- val rec = JSObject()
390
- rec.put("type", "ID")
391
- rec.put("payload", Base64.getEncoder().encodeToString(result.toByteArray()))
392
- val ndefRecords = JSArray().apply { put(rec) }
393
- val msg = JSObject().apply { put("records", ndefRecords) }
394
- ndefMessages.put(msg)
395
- }
396
- }
397
- }
398
-
399
- jsResponse.put("messages", ndefMessages)
400
- // Always include tag information if available
401
- if (tagInfo != null) {
402
- jsResponse.put("tagInfo", tagInfo)
403
- }
404
- this.notifyListeners("nfcTag", jsResponse)
405
- }
406
-
407
- private fun extractTagInfo(tag: Tag): JSObject {
408
- val tagInfo = JSObject()
409
-
410
- // Always include UID
411
- val uid = byteArrayToHexString(tag.id)
412
- tagInfo.put("uid", uid)
413
-
414
- // Include technology types
415
- val techTypes = JSArray()
416
- for (tech in tag.techList) {
417
- techTypes.put(tech)
418
- }
419
- tagInfo.put("techTypes", techTypes)
420
-
421
- // Try to get NDEF-specific information
422
- val ndef = Ndef.get(tag)
423
- if (ndef != null) {
424
- try {
425
- ndef.connect()
426
- tagInfo.put("maxSize", ndef.maxSize)
427
- tagInfo.put("isWritable", ndef.isWritable)
428
- tagInfo.put("type", ndef.type)
429
- } catch (e: Exception) {
430
- Log.w("NFC", "Failed to read NDEF tag info: ${e.message}")
431
- } finally {
432
- try { ndef.close() } catch (_: Exception) {}
433
- }
434
- }
435
-
436
- return tagInfo
437
- }
438
-
439
- private fun ndefMessageToJS(message: NdefMessage): JSObject {
440
- val ndefRecords = JSArray()
441
- for (record in message.records) {
442
- val rec = JSObject()
443
- rec.put("type", String(record.type, Charsets.UTF_8))
444
- rec.put("payload", Base64.getEncoder().encodeToString(record.payload))
445
- ndefRecords.put(rec)
446
- }
447
- val msg = JSObject()
448
- msg.put("records", ndefRecords)
449
- return msg
450
- }
451
-
452
- private fun byteArrayToHexString(inarray: ByteArray): String {
453
- val hex = arrayOf("0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F")
454
- var out = ""
455
-
456
- for (j in inarray.indices) {
457
- val `in` = inarray[j].toInt() and 0xff
458
- val i1 = (`in` shr 4) and 0x0f
459
- out += hex[i1]
460
- val i2 = `in` and 0x0f
461
- out += hex[i2]
462
- }
463
- return out
464
- }
1
+ package com.exxili.capacitornfc
2
+
3
+ import android.app.ActivityOptions
4
+ import android.app.PendingIntent
5
+ import android.content.Intent
6
+ import android.content.IntentFilter
7
+ import android.nfc.NdefMessage
8
+ import android.nfc.NdefRecord
9
+ import android.nfc.NfcAdapter
10
+ import android.nfc.NfcAdapter.ACTION_NDEF_DISCOVERED
11
+ import android.nfc.NfcAdapter.ACTION_TAG_DISCOVERED
12
+ import android.nfc.NfcAdapter.ACTION_TECH_DISCOVERED
13
+ import android.nfc.NfcAdapter.EXTRA_NDEF_MESSAGES
14
+ import android.nfc.NfcAdapter.getDefaultAdapter
15
+ import android.nfc.Tag
16
+ import android.nfc.tech.IsoDep
17
+ import android.nfc.tech.MifareClassic
18
+ import android.nfc.tech.MifareUltralight
19
+ import android.nfc.tech.Ndef
20
+ import android.nfc.tech.NdefFormatable
21
+ import android.nfc.tech.NfcA
22
+ import android.nfc.tech.NfcB
23
+ import android.nfc.tech.NfcBarcode
24
+ import android.nfc.tech.NfcF
25
+ import android.nfc.tech.NfcV
26
+ import android.os.Build
27
+ import android.os.Bundle
28
+ import android.util.Log
29
+ import com.getcapacitor.JSArray
30
+ import com.getcapacitor.JSObject
31
+ import com.getcapacitor.Plugin
32
+ import com.getcapacitor.PluginCall
33
+ import com.getcapacitor.PluginMethod
34
+ import com.getcapacitor.annotation.CapacitorPlugin
35
+ import org.json.JSONObject
36
+ import java.io.IOException
37
+ import java.io.UnsupportedEncodingException
38
+ import java.nio.charset.Charset
39
+ import java.util.Base64
40
+
41
+ @CapacitorPlugin(name = "NFC")
42
+ class NFCPlugin : Plugin() {
43
+ private var writeMode = false
44
+ private var recordsBuffer: JSArray? = null
45
+
46
+ private val techListsArray = arrayOf(arrayOf<String>(
47
+ IsoDep::class.java.name,
48
+ MifareClassic::class.java.name,
49
+ MifareUltralight::class.java.name,
50
+ Ndef::class.java.name,
51
+ NdefFormatable::class.java.name,
52
+ NfcBarcode::class.java.name,
53
+ NfcA::class.java.name,
54
+ NfcB::class.java.name,
55
+ NfcF::class.java.name,
56
+ NfcV::class.java.name
57
+ ))
58
+
59
+ public override fun handleOnNewIntent(intent: Intent?) {
60
+ super.handleOnNewIntent(intent)
61
+
62
+ if (intent == null || intent.action.isNullOrBlank()) {
63
+ return
64
+ }
65
+
66
+ if (writeMode) {
67
+ Log.d("NFC", "WRITE MODE START")
68
+ handleWriteTag(intent)
69
+ writeMode = false
70
+ recordsBuffer = null
71
+ }
72
+ else if (ACTION_NDEF_DISCOVERED == intent.action || ACTION_TAG_DISCOVERED == intent.action || ACTION_TECH_DISCOVERED == intent.action) {
73
+ Log.d("NFC", "READ MODE START")
74
+ handleReadTag(intent)
75
+ }
76
+ }
77
+
78
+ @PluginMethod
79
+ fun isSupported(call: PluginCall) {
80
+ val adapter = NfcAdapter.getDefaultAdapter(this.activity)
81
+ val ret = JSObject()
82
+ ret.put("supported", adapter != null)
83
+ call.resolve(ret)
84
+ }
85
+
86
+ @PluginMethod
87
+ fun cancelWriteAndroid(call: PluginCall) {
88
+ this.writeMode = false
89
+ call.resolve()
90
+ }
91
+
92
+ @PluginMethod
93
+ fun startScan(call: PluginCall) {
94
+ print("startScan called")
95
+ call.reject("Android NFC scanning does not require 'startScan' method.")
96
+ }
97
+
98
+ @PluginMethod
99
+ fun writeNDEF(call: PluginCall) {
100
+ print("writeNDEF called")
101
+
102
+ writeMode = true
103
+ recordsBuffer = call.getArray("records")
104
+
105
+ call.resolve()
106
+ }
107
+
108
+ @PluginMethod
109
+ fun getStatus(call: PluginCall) {
110
+ val adapter = NfcAdapter.getDefaultAdapter(this.activity)
111
+ val ret = JSObject()
112
+ val status = when {
113
+ adapter == null -> "NOT_SUPPORTED"
114
+ adapter.isEnabled -> "ENABLED"
115
+ else -> "DISABLED"
116
+ }
117
+ ret.put("status", status)
118
+ call.resolve(ret)
119
+ }
120
+
121
+ override fun handleOnPause() {
122
+ super.handleOnPause()
123
+ getDefaultAdapter(this.activity)?.disableForegroundDispatch(this.activity)
124
+ }
125
+
126
+ override fun handleOnResume() {
127
+ super.handleOnResume()
128
+ if(getDefaultAdapter(this.activity) == null) return;
129
+
130
+ val intent = Intent(context, this.activity.javaClass).apply {
131
+ addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
132
+ }
133
+
134
+ val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
135
+ PendingIntent.FLAG_MUTABLE
136
+ } else {
137
+ PendingIntent.FLAG_UPDATE_CURRENT
138
+ }
139
+
140
+ var activityOptionsBundle: Bundle? = null
141
+
142
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // API 35 (Android 15)
143
+ activityOptionsBundle = ActivityOptions.makeBasic().apply {
144
+ setPendingIntentCreatorBackgroundActivityStartMode(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
145
+ }.toBundle()
146
+ }
147
+
148
+ val pendingIntent =
149
+ PendingIntent.getActivity(
150
+ this.activity,
151
+ 0,
152
+ intent,
153
+ pendingIntentFlags,
154
+ activityOptionsBundle
155
+ )
156
+
157
+ val intentFilter: Array<IntentFilter> =
158
+ arrayOf(
159
+ IntentFilter(ACTION_NDEF_DISCOVERED).apply {
160
+ try {
161
+ addDataType("text/plain")
162
+ } catch (e: IntentFilter.MalformedMimeTypeException) {
163
+ throw RuntimeException("failed", e)
164
+ }
165
+ },
166
+ IntentFilter(ACTION_TECH_DISCOVERED),
167
+ IntentFilter(ACTION_TAG_DISCOVERED)
168
+ )
169
+
170
+ getDefaultAdapter(this.activity).enableForegroundDispatch(
171
+ this.activity,
172
+ pendingIntent,
173
+ intentFilter,
174
+ techListsArray
175
+ )
176
+ }
177
+
178
+ private fun handleWriteTag(intent: Intent) {
179
+ val records = recordsBuffer?.toList<JSONObject>()
180
+ if(records != null) {
181
+ val ndefRecords = mutableListOf<NdefRecord>()
182
+
183
+ try {
184
+ for (record in records) {
185
+ val payload = record.getJSONArray("payload")
186
+ val type: String? = record.getString("type")
187
+
188
+ if (payload.length() == 0 || type == null) {
189
+ notifyListeners(
190
+ "nfcError",
191
+ JSObject().put(
192
+ "error",
193
+ "Invalid record: payload or type is missing."
194
+ )
195
+ )
196
+ return
197
+ }
198
+
199
+ val payloadBytes = ByteArray(payload.length())
200
+ for(i in 0 until payload.length()) {
201
+ payloadBytes[i] = payload.getInt(i).toByte()
202
+ }
203
+
204
+ val (tnf, typeBytes) = when {
205
+ type == "T" || type == "U" -> Pair(
206
+ NdefRecord.TNF_WELL_KNOWN,
207
+ type.toByteArray(Charsets.UTF_8)
208
+ )
209
+ type.contains("/") -> Pair(
210
+ NdefRecord.TNF_MIME_MEDIA,
211
+ type.toByteArray(Charsets.US_ASCII)
212
+ )
213
+ else -> Pair(
214
+ NdefRecord.TNF_EXTERNAL_TYPE,
215
+ type.toByteArray(Charsets.UTF_8)
216
+ )
217
+ }
218
+
219
+ val record = if (tnf == NdefRecord.TNF_MIME_MEDIA) {
220
+ try {
221
+ NdefRecord.createMime(type, payloadBytes)
222
+ } catch (e: IllegalArgumentException) {
223
+ notifyListeners(
224
+ "nfcError",
225
+ JSObject().put(
226
+ "error",
227
+ "Invalid MIME type for record"
228
+ )
229
+ )
230
+ return
231
+ }
232
+ } else {
233
+ NdefRecord(
234
+ tnf,
235
+ typeBytes,
236
+ ByteArray(0),
237
+ payloadBytes
238
+ )
239
+ }
240
+
241
+ ndefRecords.add(record)
242
+ }
243
+
244
+ val ndefMessage = NdefMessage(ndefRecords.toTypedArray())
245
+ val tag = getTagFromIntent(intent)
246
+ var ndef = Ndef.get(tag)
247
+
248
+ if (ndef == null) {
249
+ val formatable = NdefFormatable.get(tag)
250
+ if (formatable != null) {
251
+ try {
252
+ formatable.connect()
253
+ val mimeRecord = NdefRecord.createMime("text/plain", "INIT".toByteArray(
254
+ Charset.forName("US-ASCII")))
255
+ val msg = NdefMessage(mimeRecord)
256
+ formatable.format(msg)
257
+ // Success!
258
+ // Emit event to Capacitor plugin for success
259
+ println("Successfully formatted and wrote NDEF message to tag!")
260
+ } catch (e: IOException) {
261
+ // Error connecting or formatting
262
+ // Emit event to Capacitor plugin for error
263
+ println("Error formatting or writing to NDEF-formatable tag: ${e.message}")
264
+ } catch (e: Exception) { // Catch other potential exceptions during format, like TagLostException
265
+ println("Error during NDEF formatting: ${e.message}")
266
+ } finally {
267
+ try {
268
+ formatable.close()
269
+ } catch (e: IOException) {
270
+ println("Error closing NdefFormatable connection: ${e.message}")
271
+ }
272
+ }
273
+
274
+ ndef = Ndef.get(formatable.tag)
275
+ } else {
276
+ notifyListeners(
277
+ "nfcError",
278
+ JSObject().put(
279
+ "error",
280
+ "Tag does not support NDEF writing."
281
+ )
282
+ )
283
+ return
284
+ }
285
+ }
286
+
287
+ ndef.use { // Use block ensures ndef.close() is called
288
+ ndef.connect()
289
+ if (!ndef.isWritable) {
290
+ notifyListeners(
291
+ "nfcError",
292
+ JSObject().put(
293
+ "error",
294
+ "NFC tag is not writable"
295
+ )
296
+ )
297
+ return
298
+ }
299
+ if (ndef.maxSize < ndefMessage.toByteArray().size) {
300
+ notifyListeners(
301
+ "nfcError",
302
+ JSObject().put(
303
+ "error",
304
+ "Message too large for this NFC Tag (max ${ndef.maxSize} bytes)."
305
+ )
306
+ )
307
+ return
308
+ }
309
+
310
+ ndef.writeNdefMessage(ndefMessage)
311
+ Log.d("NFC", "NDEF message successfully written to tag.")
312
+ }
313
+
314
+ notifyListeners("nfcWriteSuccess", JSObject().put("success", true))
315
+ }
316
+ catch (e: UnsupportedEncodingException) {
317
+ Log.e("NFC", "Encoding error during NDEF record creation: ${e.message}")
318
+ notifyListeners(
319
+ "nfcError",
320
+ JSObject().put(
321
+ "error",
322
+ "Encoding error: ${e.message}"
323
+ )
324
+ )
325
+ }
326
+ catch (e: IOException) {
327
+ Log.e("NFC", "I/O error during NFC write: ${e.message}")
328
+ notifyListeners(
329
+ "nfcError",
330
+ JSObject().put(
331
+ "error",
332
+ "NFC I/O error: ${e.message}"
333
+ )
334
+ )
335
+ }
336
+ catch (e: Exception) {
337
+ Log.e("NFC", "Error writing NDEF message: ${e.message}", e)
338
+ notifyListeners(
339
+ "nfcError",
340
+ JSObject().put(
341
+ "error",
342
+ "Failed to write NDEF message: ${e.message}"
343
+ )
344
+ )
345
+ }
346
+ }
347
+ else {
348
+ notifyListeners("nfcError", JSObject().put("error", "Failed to write NFC tag"))
349
+ }
350
+ }
351
+
352
+ private fun handleReadTag(intent: Intent) {
353
+ val jsResponse = JSObject()
354
+ val ndefMessages = JSArray()
355
+
356
+ // Get tag information regardless of NDEF content
357
+ val tag: Tag? = getTagFromIntent(intent)
358
+ val tagInfo = tag?.let { extractTagInfo(it) }
359
+
360
+ // Try to obtain raw NDEF messages first (ACTION_NDEF_DISCOVERED path)
361
+ val receivedMessages = getNdefMessagesFromIntent(intent)
362
+
363
+ if (receivedMessages != null && receivedMessages.isNotEmpty()) {
364
+ // Standard NDEF-discovered path
365
+ for (message in receivedMessages) {
366
+ ndefMessages.put(ndefMessageToJS(message))
367
+ }
368
+ } else {
369
+ // For ACTION_TAG_DISCOVERED or ACTION_TECH_DISCOVERED we may still have an NDEF tag.
370
+ var added = false
371
+ if (tag != null) {
372
+ val ndef = Ndef.get(tag)
373
+ if (ndef != null) {
374
+ try {
375
+ ndef.connect()
376
+ // Prefer cached message to avoid additional IO if available
377
+ val message: NdefMessage? = ndef.cachedNdefMessage ?: try {
378
+ ndef.ndefMessage
379
+ } catch (e: Exception) { null }
380
+ if (message != null) {
381
+ ndefMessages.put(ndefMessageToJS(message))
382
+ added = true
383
+ }
384
+ } catch (e: Exception) {
385
+ Log.w("NFC", "Failed to read NDEF message from TECH/TAG intent: ${e.message}")
386
+ } finally {
387
+ try { ndef.close() } catch (_: Exception) {}
388
+ }
389
+ }
390
+
391
+ // If no NDEF message found, fallback to tag ID (legacy behavior)
392
+ if (!added) {
393
+ val tagId = intent.getByteArrayExtra(NfcAdapter.EXTRA_ID) ?: tag.id
394
+ val result = if (tagId != null) byteArrayToHexString(tagId) else ""
395
+ val rec = JSObject()
396
+ rec.put("type", "ID")
397
+ rec.put("payload", Base64.getEncoder().encodeToString(result.toByteArray()))
398
+ val ndefRecords = JSArray().apply { put(rec) }
399
+ val msg = JSObject().apply { put("records", ndefRecords) }
400
+ ndefMessages.put(msg)
401
+ }
402
+ }
403
+ }
404
+
405
+ jsResponse.put("messages", ndefMessages)
406
+ // Always include tag information if available
407
+ if (tagInfo != null) {
408
+ jsResponse.put("tagInfo", tagInfo)
409
+ }
410
+ this.notifyListeners("nfcTag", jsResponse)
411
+ }
412
+
413
+ private fun extractTagInfo(tag: Tag): JSObject {
414
+ val tagInfo = JSObject()
415
+
416
+ // Always include UID
417
+ val uid = byteArrayToHexString(tag.id)
418
+ tagInfo.put("uid", uid)
419
+
420
+ // Include technology types
421
+ val techTypes = JSArray()
422
+ for (tech in tag.techList) {
423
+ techTypes.put(tech)
424
+ }
425
+ tagInfo.put("techTypes", techTypes)
426
+
427
+ // Try to get NDEF-specific information
428
+ val ndef = Ndef.get(tag)
429
+ if (ndef != null) {
430
+ try {
431
+ ndef.connect()
432
+ tagInfo.put("maxSize", ndef.maxSize)
433
+ tagInfo.put("isWritable", ndef.isWritable)
434
+ tagInfo.put("type", ndef.type)
435
+ } catch (e: Exception) {
436
+ Log.w("NFC", "Failed to read NDEF tag info: ${e.message}")
437
+ } finally {
438
+ try { ndef.close() } catch (_: Exception) {}
439
+ }
440
+ }
441
+
442
+ return tagInfo
443
+ }
444
+
445
+ private fun ndefMessageToJS(message: NdefMessage): JSObject {
446
+ val ndefRecords = JSArray()
447
+ for (record in message.records) {
448
+ val rec = JSObject()
449
+ rec.put("type", String(record.type, Charsets.UTF_8))
450
+ rec.put("payload", Base64.getEncoder().encodeToString(record.payload))
451
+ ndefRecords.put(rec)
452
+ }
453
+ val msg = JSObject()
454
+ msg.put("records", ndefRecords)
455
+ return msg
456
+ }
457
+
458
+ // Intent.getParcelableExtra(String, Class) and getParcelableArrayExtra(String, Class)
459
+ // only exist since API 33 (Android 13). Calling them on older devices throws
460
+ // NoSuchMethodError and crashes the app, so fall back to the legacy overloads.
461
+ private fun getTagFromIntent(intent: Intent): Tag? {
462
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
463
+ intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
464
+ } else {
465
+ @Suppress("DEPRECATION")
466
+ intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
467
+ }
468
+ }
469
+
470
+ private fun getNdefMessagesFromIntent(intent: Intent): Array<NdefMessage>? {
471
+ val raw = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
472
+ intent.getParcelableArrayExtra(EXTRA_NDEF_MESSAGES, NdefMessage::class.java)
473
+ } else {
474
+ @Suppress("DEPRECATION")
475
+ intent.getParcelableArrayExtra(EXTRA_NDEF_MESSAGES)
476
+ } ?: return null
477
+ return raw.mapNotNull { it as? NdefMessage }.toTypedArray()
478
+ }
479
+
480
+ private fun byteArrayToHexString(inarray: ByteArray): String {
481
+ val hex = arrayOf("0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F")
482
+ var out = ""
483
+
484
+ for (j in inarray.indices) {
485
+ val `in` = inarray[j].toInt() and 0xff
486
+ val i1 = (`in` shr 4) and 0x0f
487
+ out += hex[i1]
488
+ val i2 = `in` and 0x0f
489
+ out += hex[i2]
490
+ }
491
+ return out
492
+ }
465
493
  }