@icedevml/react-native-host-card-emulation 0.4.0 → 0.5.0

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.
package/README.md CHANGED
@@ -23,7 +23,17 @@ Make sure that your project has it enabled. Note that the New Architecture is en
23
23
  *(no prerequisite steps are required for Android)*
24
24
 
25
25
  ## Installation
26
- Install `@icedevml/react-native-host-card-emulation` package within your React Native project. Then, follow the subsections below for each platform that you need to support.
26
+ Install this package within your React Native project:
27
+
28
+ ```
29
+ yarn add @icedevml/react-native-host-card-emulation
30
+ ```
31
+ or using npm
32
+ ```
33
+ npm install --save @icedevml/react-native-host-card-emulation
34
+ ```
35
+
36
+ Afterwards, follow the subsections below for each platform that you need to support.
27
37
 
28
38
  ### iOS
29
39
  1. Make sure to set up a proper signing team and bundle identifier in "Signing & Capabilities" configuration section of your project.
@@ -54,7 +64,8 @@ Install `@icedevml/react-native-host-card-emulation` package within your React N
54
64
  ...
55
65
 
56
66
  + <uses-permission android:name="android.permission.NFC" />
57
- + <uses-feature android:name="android.hardware.nfc.hce" android:required="true" />
67
+ + <!-- set required="true" if your app would be entirely useless on devices without HCE support -->
68
+ + <uses-feature android:name="android.hardware.nfc.hce" android:required="false" />
58
69
 
59
70
  ...
60
71
 
@@ -68,7 +79,7 @@ Install `@icedevml/react-native-host-card-emulation` package within your React N
68
79
  + android:name="com.itsecrnd.rtnhceandroid.HCEService"
69
80
  + android:exported="true"
70
81
  + android:enabled="true"
71
- + android:permission="android.permission.BIND_NFC_SERVICE" >
82
+ + android:permission="android.permission.BIND_NFC_SERVICE">
72
83
  + <intent-filter>
73
84
  + <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
74
85
  + <category android:name="android.intent.category.DEFAULT"/>
@@ -100,19 +111,35 @@ Install `@icedevml/react-native-host-card-emulation` package within your React N
100
111
 
101
112
  This module provides a uniform low-level HCE API for both mobile platforms.
102
113
 
103
- 1. Subscribe to the event stream (there is a single global event stream for the entire module):
114
+ > [!NOTE]
115
+ > Raw native module's API specification is available in [packages/native-hce-module/js/NativeHCEModule.ts](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/native-hce-module/js/NativeHCEModule.ts).
116
+ > Check it out in order to understand what methods you can call against the module and what are the expected parameters/return values.
117
+
118
+ > [!NOTE]
119
+ > See [Demo App's Code (Example Library Usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-hce-module-app/App.tsx) example for more insignt about the library's API.
120
+
121
+ ### Quick start guide
122
+
123
+ 1. Subscribe to the event stream. There is a single global event stream, so you can subscribe early and remain subscribed to events for the entire application's lifetime.
104
124
  ```typescript
105
- NativeHCEModule?.onEvent(async (event: HCEModuleEvent) => {
106
- switch (event.type) {
107
- /* ... your handler here ... */
125
+ useEffect(() => {
126
+ const subscription = NativeHCEModule?.onEvent(async (event: HCEModuleEvent) => {
127
+ switch (event.type) {
128
+ /* ... implement event handlers here ... */
129
+ }
130
+ });
131
+
132
+ // cleanup the event listener when the effect is unmounted
133
+ return () => {
134
+ subscription.remove();
108
135
  }
109
- });
136
+ }, []);
110
137
  ```
111
- 2. When user indicates that he wants to perform the HCE action, call the following function from the button's onClick routine:
138
+ 2. When user indicates that he/she wants to perform the HCE action, call the following function from the button's onClick routine:
112
139
  ```typescript
113
140
  await NativeHCEModule?.beginSession();
114
141
  ```
115
- This will emit `sessionStarted` event on both platforms. You can start the HCE straight from that event:
142
+ This will emit `sessionStarted` event right after the function call, on both iOS and Android platforms. After the session is started, you can decide to start HCE emulation right away in the event handler:
116
143
  ```typescript
117
144
  // inside NativeHCEModule?.onEvent handler's switch
118
145
  case 'sessionStarted':
@@ -120,10 +147,10 @@ This module provides a uniform low-level HCE API for both mobile platforms.
120
147
  await NativeHCEModule?.startHCE();
121
148
  break;
122
149
  ```
123
- 3. Calling `startHCE()` causes:
124
- * iOS: Launch of the NFC scan system UI. The smartphone will start listening for C-APDUs from the readers right after that UI appears on screen. The `readerDetected` event will be emitted as soon as the NFC tag field presence is observed. The `readerDeselected` event will be emitted if the reader is physically disconnected or a non-matching AID is selected by the reader.
125
- * Android: No user interface appears (you have to implement it on your own scanning UI, within your app). The `readerDetected` event will be emitted as soon as the first matching SELECT AID command is observed. The `readerDeselected` event will be emitted if the reader is physically disconnected or a non-matching AID is selected by the reader.
126
- 4. Optionally, you can handle `readerDetected` and `readerDeselected events to enhance user's experience.
150
+ Calling `await NativeHCEModule?.startHCE()` causes:
151
+ * iOS: Your smartphone will start listening for C-APDUs (Command APDUs originating from reader devices) right after that function is called, which will be additionally indicated by the operating system popping out the NFC scanning user interface prompt.
152
+ * Android: HCE commands will be forwarded to your application from that point on. No specific user interface is displayed (you have to implement it on your own, within your app).
153
+ 3. Optionally, you can handle `readerDetected` and `readerDeselected` events to enhance user's experience.
127
154
  ```typescript
128
155
  // inside NativeHCEModule?.onEvent handler's switch
129
156
  case 'readerDetected':
@@ -134,29 +161,86 @@ This module provides a uniform low-level HCE API for both mobile platforms.
134
161
  NativeHCEModule?.setSessionAlertMessage('Lost reader'); // only for iOS, no-op in Android
135
162
  break;
136
163
  ```
137
- 5. Handle CAPDU
164
+ For those events, trigger mechanisms are platform dependent:
165
+ * iOS: The `readerDetected` event will be emitted as soon as the NFC reader's field presence is observed. The `readerDeselected` event will be emitted if the reader is physically disconnected or a non-matching AID is selected by the reader.
166
+ * Android: The `readerDetected` event will be emitted as soon as the first matching SELECT AID command is observed. The `readerDeselected` event will be emitted if the reader is physically disconnected or a non-matching AID is selected by the reader.
167
+ 4. Receive incoming C-APDU and respond to it:
138
168
  ```typescript
139
169
  // inside NativeHCEModule?.onEvent handler's switch
140
170
  case 'received':
141
- NativeHCEModule?.setSessionAlertMessage('Keep holding the tag');
171
+ NativeHCEModule?.setSessionAlertMessage('Keep holding the tag'); // only for iOS, no-op on Android
142
172
 
173
+ // decode incoming C-APDU to bytes
143
174
  const capdu = Buffer.from(event.arg!, 'hex');
144
175
  console.log('Received C-APDU, capdu.toString('hex'));
145
176
 
146
- // let's say we always want to respond with [0x0A] + Status Word 0x9000
177
+ // for the demo purposes, we always want to respond with [0x0A] + status code 0x9000 (success)
147
178
  await NativeHCEModule?.respondAPDU(Buffer.from([0x0A, 0x90, 0x00], "hex"));
148
179
  break;
149
180
  ```
150
- 6. (iOS) Optionally, if you need to utilize `NFCPresentmentIntentAssertion` then at any time you can call:
181
+ You don't have to respond to the APDU right away from within the event handler, but please remember that the reader might time out if you will be lingering with the response for too long.
182
+
183
+ ### iOS: Acquiring exclusive NFC access
184
+
185
+ If you need to utilize `NFCPresentmentIntentAssertion` for enhanced user experience, call:
186
+ ```typescript
187
+ NativeHCEModule?.acquireExclusiveNFC();
188
+ ```
189
+ This function will acquire an exclusive NFC access for 15 seconds. On system services or other applications will be able to interfere with NFC during that period. For example, the NFC background tag reading will be disabled so it would not generate any distracting notifications.
190
+
191
+ This function will throw an exception if:
192
+ * the presentment intent assertion was already acquired and is still active;
193
+ * you are in the cooldown period where you are not allowed to acquire the presentment intent assertion (cooldown is 15 seconds after the previous assertion had expired);
194
+ * the feature is not supported or the device is not eligible for whatever reason;
195
+
196
+ Call `NativeHCEModule?.isExclusiveNFC()` to check if exclusive NFC access is still active.
197
+
198
+ ### Android: Handle HCE calls when the app is not running
199
+
200
+ Exclusively on Android platform it is possible to handle Host Card Emulation even when the app is not running in foreground.
201
+ In such case, no user interface will be available, the handler will run in headless mode. This is still extremely useful in certain cases,
202
+ for instance - your app may emulate an NDEF tag even when it's not launched on the foreground.
203
+
204
+ 1. Register `handleBackgroundHCECall` headless task in your app's `index.js`:
151
205
  ```typescript
152
- NativeHCEModule?.acquireExclusiveNFC();
206
+ AppRegistry.registerHeadlessTask('handleBackgroundHCECall', () => {
207
+ return async () => {
208
+ return await runBackground();
209
+ }
210
+ });
211
+ ```
212
+ 2. Register onBackgroundEvent listener in your `runBackground()` function and call to `await NativeHCEModule?.initBackgroundHCE()` at the very end, after the event listener is fully set up.
213
+ ```typescript
214
+ async function runBackground() {
215
+ let subscription = NativeHCEModule?.onBackgroundEvent(async (event: HCEModuleBackgroundEvent) => {
216
+ switch (event.type) {
217
+ /* ... background HCE event handler here ... */
218
+ }
219
+ });
220
+
221
+ await NativeHCEModule?.initBackgroundHCE();
222
+ }
223
+ ```
224
+ 3. Cleanup your event listener in `readerDeselected` -- this is obligatory because lack of proper cleanup will result in bugs and unexpected behavior.
225
+ ```typescript
226
+ case 'readerDeselected':
227
+ // cleanup, remove the event listener
228
+ subscription.remove();
229
+ break;
230
+ ```
231
+ 4. Implement the actual APDU handler that will process the C-APDU and generate a response.
232
+ ```typescript
233
+ case 'received':
234
+ // decode incoming C-APDU to bytes
235
+ const capdu = Buffer.from(event.arg!, 'hex');
236
+ console.log('Received C-APDU, capdu.toString('hex'));
237
+
238
+ // for the demo purposes, we always want to respond with [0x0A] + status code 0x9000 (success)
239
+ await NativeHCEModule?.respondAPDU(Buffer.from([0x0A, 0x90, 0x00], "hex"));
240
+ break;
153
241
  ```
154
- Which will throw an exception if the presentment intent assertion was already acquired and is still valid
155
- (usually 15 seconds) or if the cooldown period for acquiring new assertion was not yet expired (usually also 15 seconds).
156
242
 
157
- > [!NOTE]
158
- > See [Demo App's Code (Example Library Usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-hce-module-app/App.tsx) example for more insignt about the library's API.
243
+ ### More resources
159
244
 
160
- > [!NOTE]
161
- > Raw native module's API specification is available in [packages/native-hce-module/js/NativeHCEModule.ts](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/native-hce-module/js/NativeHCEModule.ts).
162
- > Check it out in order to understand what methods you can call against the module and what are the expected parameters/return values.
245
+ * [Module's API specification](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/native-hce-module/js/NativeHCEModule.ts)
246
+ * [Demo App's Code (Example Library Usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-hce-module-app/App.tsx)
@@ -1,107 +1,235 @@
1
+ /*
2
+ * Some of the code inspired by:
3
+ * https://github.com/appidea/react-native-hce
4
+ * https://github.com/transistorsoft/react-native-background-fetch
5
+ */
6
+
1
7
  package com.itsecrnd.rtnhceandroid;
2
8
 
3
- import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_READER_DETECT;
4
- import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_READER_LOST;
5
- import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_RECEIVE_C_APDU;
6
- import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_SEND_R_APDU;
7
- import static com.itsecrnd.rtnhceandroid.IntentKeys.KEY_AUTH;
8
- import static com.itsecrnd.rtnhceandroid.IntentKeys.KEY_CAPDU;
9
- import static com.itsecrnd.rtnhceandroid.IntentKeys.KEY_RAPDU;
9
+ import static com.facebook.react.jstasks.HeadlessJsTaskContext.Companion;
10
10
 
11
- import android.content.BroadcastReceiver;
11
+ import android.app.ActivityManager;
12
12
  import android.content.Context;
13
- import android.content.Intent;
14
- import android.content.IntentFilter;
15
13
  import android.nfc.cardemulation.HostApduService;
16
14
  import android.os.Build;
17
15
  import android.os.Bundle;
18
16
  import android.util.Log;
19
17
 
18
+ import androidx.annotation.NonNull;
20
19
  import androidx.annotation.RequiresApi;
21
20
 
21
+ import com.facebook.react.ReactApplication;
22
+ import com.facebook.react.ReactHost;
23
+ import com.facebook.react.ReactInstanceEventListener;
24
+ import com.facebook.react.bridge.Arguments;
25
+ import com.facebook.react.bridge.ReactContext;
26
+ import com.facebook.react.bridge.UiThreadUtil;
27
+ import com.facebook.react.jstasks.HeadlessJsTaskConfig;
28
+ import com.facebook.react.jstasks.HeadlessJsTaskContext;
29
+ import com.facebook.react.jstasks.HeadlessJsTaskEventListener;
30
+
31
+ import java.util.List;
22
32
  import java.util.Locale;
23
33
 
24
- import javax.crypto.SecretKey;
25
- import javax.crypto.spec.SecretKeySpec;
26
-
27
34
  @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
28
- public class HCEService extends HostApduService {
35
+ public class HCEService extends HostApduService implements ReactInstanceEventListener, HCEServiceCallback {
29
36
  private static final String TAG = "HCEService";
30
37
 
31
- private SecretKey encSecretKey;
32
-
33
- private final BroadcastReceiver receiver = new BroadcastReceiver() {
34
- @Override
35
- public void onReceive(Context context, Intent intent) {
36
- try {
37
- String action = intent.getAction();
38
-
39
- if (action != null) {
40
- AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra(KEY_AUTH));
41
-
42
- if (action.equals(ACTION_SEND_R_APDU)) {
43
- String rapdu = AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra(KEY_RAPDU));
44
- byte[] dec = BinaryUtils.HexStringToByteArray(rapdu.toUpperCase(Locale.ROOT));
45
- sendResponseApdu(dec);
46
- }
47
- }
48
- } catch (Exception e) {
49
- Log.e(TAG, "Failed to handle broadcast in HCEService.", e);
38
+ private boolean isForeground;
39
+ private RTNHCEAndroidModule hceModule;
40
+ private byte[] pendingCAPDU;
41
+ private volatile boolean needsResponse;
42
+
43
+ private boolean isAppOnForeground(Context context) {
44
+ /*
45
+ We need to check if app is in foreground otherwise the app will crash.
46
+ https://stackoverflow.com/questions/8489993/check-android-application-is-in-foreground-or-not
47
+ */
48
+ ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
49
+ List<ActivityManager.RunningAppProcessInfo> appProcesses =
50
+ activityManager.getRunningAppProcesses();
51
+ if (appProcesses == null) {
52
+ return false;
53
+ }
54
+ final String packageName = context.getPackageName();
55
+ for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
56
+ if (appProcess.importance ==
57
+ ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
58
+ appProcess.processName.equals(packageName)) {
59
+ return true;
50
60
  }
51
61
  }
52
- };
62
+ return false;
63
+ }
64
+
65
+ @Override
66
+ public void onBackgroundHCEInit() {
67
+ Log.d(TAG, "HCEService:onBackgroundHCEInit");
68
+
69
+ if (pendingCAPDU != null) {
70
+ Log.d(TAG, "HCEService:onBackgroundHCEInit send pendingCAPDU");
71
+ hceModule.sendBackgroundEvent("received", BinaryUtils.ByteArrayToHexString(pendingCAPDU));
72
+ pendingCAPDU = null;
73
+ }
74
+ }
53
75
 
54
76
  @Override
55
- public byte[] processCommandApdu(byte[] command, Bundle extras) {
56
- String capdu = AESGCMUtil.encryptData(encSecretKey, BinaryUtils.ByteArrayToHexString(command));
77
+ public void onRespondAPDU(String rapdu) {
78
+ Log.d(TAG, "HCEService:onRespondAPDU");
79
+
80
+ if (!needsResponse) {
81
+ Log.d(TAG, "HCEService:onRespondAPDU not waiting for a response");
82
+ throw new IllegalStateException();
83
+ }
84
+
85
+ needsResponse = false;
86
+ sendResponseApdu(BinaryUtils.HexStringToByteArray(rapdu));
87
+ }
57
88
 
58
- Intent intent = new Intent(ACTION_RECEIVE_C_APDU);
59
- intent.setPackage(getApplicationContext().getPackageName());
60
- intent.putExtra(KEY_AUTH, AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
61
- intent.putExtra(KEY_CAPDU, capdu);
62
- getApplicationContext().sendBroadcast(intent);
89
+ @Override
90
+ public byte[] processCommandApdu(byte[] command, Bundle extras) {
91
+ Log.d(TAG, "HCEService:processCommandApdu");
92
+ String capdu = BinaryUtils.ByteArrayToHexString(command).toUpperCase(Locale.ROOT);
93
+
94
+ if (isForeground) {
95
+ if (hceModule._isHCERunning() && !hceModule.isHCEBrokenConnection()) {
96
+ Log.d(TAG, "HCEService:processCommandApdu foreground sendEvent received");
97
+ needsResponse = true;
98
+ hceModule.sendEvent("received", capdu);
99
+ } else {
100
+ Log.d(TAG, "HCEService:processCommandApdu foreground respond 6999");
101
+ return new byte[] { (byte) 0x69, (byte) 0x99 };
102
+ }
103
+ } else {
104
+ if (hceModule != null && hceModule.isHCEBackgroundHandlerReady()) {
105
+ Log.d(TAG, "HCEService:processCommandApdu background sendBackgroundEvent received");
106
+ needsResponse = true;
107
+ hceModule.sendBackgroundEvent("received", capdu);
108
+ } else {
109
+ Log.d(TAG, "HCEService:processCommandApdu background pendingCAPDU");
110
+ needsResponse = true;
111
+ pendingCAPDU = command;
112
+ }
113
+ }
63
114
 
64
115
  return null;
65
116
  }
66
117
 
67
118
  @Override
68
119
  public void onCreate() {
69
- Log.d(TAG, "HCEService onCreate");
120
+ Log.d(TAG, "HCEService:onCreate");
70
121
 
71
- String prefsName = getApplicationContext().getPackageName() + ".nativehcemodule";
72
- String encKey = getApplicationContext()
73
- .getSharedPreferences(prefsName, Context.MODE_PRIVATE)
74
- .getString("encKey", "");
122
+ ReactHost reactHost = ((ReactApplication) getApplication()).getReactHost();
75
123
 
76
- if (encKey.isEmpty()) {
77
- throw new RuntimeException("Failed to load encKey.");
124
+ if (reactHost == null) {
125
+ throw new RuntimeException("BUG! getReactHost() returned null.");
78
126
  }
79
127
 
80
- byte[] bArr = BinaryUtils.HexStringToByteArray(encKey);
81
- encSecretKey = new SecretKeySpec(bArr, 0, bArr.length, "AES");
128
+ isForeground = isAppOnForeground(getApplicationContext());
129
+ pendingCAPDU = null;
130
+ hceModule = null;
131
+ needsResponse = false;
82
132
 
83
- /*
84
- * See a comment about registerReceiver() in RTNHCEAndroidModule.
85
- */
86
- IntentFilter filter = new IntentFilter();
87
- filter.addAction(ACTION_SEND_R_APDU);
88
- getApplicationContext().registerReceiver(receiver, filter, RECEIVER_EXPORTED);
89
-
90
- Intent intent = new Intent(ACTION_READER_DETECT);
91
- intent.setPackage(getApplicationContext().getPackageName());
92
- intent.putExtra(KEY_AUTH, AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
93
- getApplicationContext().sendBroadcast(intent);
133
+ if (isForeground) {
134
+ Log.d(TAG, "HCEService:onCreate foreground");
135
+
136
+ ReactContext reactContext = reactHost.getCurrentReactContext();
137
+
138
+ if (reactContext == null) {
139
+ throw new RuntimeException("BUG! getCurrentReactContext() returned null in foreground.");
140
+ }
141
+
142
+ this.hceModule = ((RTNHCEAndroidModule) reactContext.getNativeModule("NativeHCEModule"));
143
+
144
+ if (this.hceModule == null) {
145
+ throw new RuntimeException("BUG! getNativeModule() returned null in foreground.");
146
+ }
147
+
148
+ this.hceModule.setHCEService(this);
149
+
150
+ if (this.hceModule._isHCERunning()) {
151
+ this.hceModule.sendEvent("readerDetected", "");
152
+ } else {
153
+ this.hceModule.setHCEBrokenConnection();
154
+ }
155
+ } else {
156
+ ReactContext reactContext = reactHost.getCurrentReactContext();
157
+ Log.d(TAG, "HCEService:onCreate background");
158
+
159
+ if (reactContext == null) {
160
+ reactHost.addReactInstanceEventListener(this);
161
+ reactHost.start();
162
+ } else {
163
+ setupRunJSTask(reactContext);
164
+ }
165
+ }
94
166
  }
95
167
 
96
168
  @Override
97
169
  public void onDeactivated(int reason) {
98
- Log.d(TAG, "HCEService onDeactivated: " + reason);
170
+ Log.d(TAG, "HCEService:onDeactivated: " + reason);
171
+ needsResponse = false;
99
172
 
100
- getApplicationContext().unregisterReceiver(receiver);
173
+ if (isForeground) {
174
+ if (this.hceModule != null && !this.hceModule.isHCEBrokenConnection()) {
175
+ this.hceModule.sendEvent("readerDeselected", "");
176
+ }
177
+ } else {
178
+ this.hceModule.sendBackgroundEvent("readerDeselected", "");
179
+ }
180
+
181
+ if (this.hceModule != null) {
182
+ this.hceModule.setHCEService(null);
183
+ }
184
+ }
185
+
186
+ @Override
187
+ public void onReactContextInitialized(@NonNull ReactContext reactContext) {
188
+ HeadlessJsTaskContext headlessJsTaskContext = Companion.getInstance(reactContext);
189
+ headlessJsTaskContext.addTaskEventListener(new HeadlessJsTaskEventListener() {
190
+ @Override
191
+ public void onHeadlessJsTaskStart(int i) {
192
+ Log.d(TAG, "HCEService:HeadlessJsTaskEventListener:onHeadlessJsTaskStart: " + i);
193
+ }
194
+
195
+ @Override
196
+ public void onHeadlessJsTaskFinish(int i) {
197
+ Log.d(TAG, "HCEService:HeadlessJsTaskEventListener:onHeadlessJsTaskFinish: " + i);
198
+ }
199
+ });
200
+
201
+ setupRunJSTask(reactContext);
202
+ }
203
+
204
+ public void setupRunJSTask(@NonNull ReactContext reactContext) {
205
+ hceModule = (RTNHCEAndroidModule) reactContext.getNativeModule("NativeHCEModule");
206
+
207
+ if (hceModule == null) {
208
+ throw new RuntimeException("BUG! getNativeModule() returned null in background.");
209
+ }
210
+
211
+ hceModule.setHCEService(this);
212
+
213
+ UiThreadUtil.runOnUiThread(new Runnable() {
214
+ @Override
215
+ public void run() {
216
+ Log.d(TAG, "HCEService:setupRunJSTask:runOnUiThread startTask");
217
+ HeadlessJsTaskContext headlessJsTaskContext = Companion.getInstance(reactContext);
218
+ headlessJsTaskContext.startTask(
219
+ new HeadlessJsTaskConfig(
220
+ "handleBackgroundHCECall",
221
+ Arguments.fromBundle(new Bundle()),
222
+ 15000,
223
+ false // not allowed in foreground
224
+ ));
225
+ }
226
+ });
227
+ }
228
+
229
+ @Override
230
+ public void onDestroy() {
231
+ super.onDestroy();
101
232
 
102
- Intent intent = new Intent(ACTION_READER_LOST);
103
- intent.setPackage(getApplicationContext().getPackageName());
104
- intent.putExtra(KEY_AUTH, AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
105
- getApplicationContext().sendBroadcast(intent);
233
+ Log.d(TAG, "HCEService:onDestroy");
106
234
  }
107
235
  }
@@ -0,0 +1,6 @@
1
+ package com.itsecrnd.rtnhceandroid;
2
+
3
+ public interface HCEServiceCallback {
4
+ void onBackgroundHCEInit();
5
+ void onRespondAPDU(String rapdu);
6
+ }
@@ -1,16 +1,5 @@
1
1
  package com.itsecrnd.rtnhceandroid;
2
2
 
3
- import static android.content.Context.RECEIVER_EXPORTED;
4
- import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_READER_DETECT;
5
- import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_READER_LOST;
6
- import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_RECEIVE_C_APDU;
7
- import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_SEND_R_APDU;
8
-
9
- import android.content.BroadcastReceiver;
10
- import android.content.ComponentName;
11
- import android.content.Context;
12
- import android.content.Intent;
13
- import android.content.IntentFilter;
14
3
  import android.content.pm.PackageManager;
15
4
  import android.os.Build;
16
5
  import android.util.Log;
@@ -23,137 +12,118 @@ import com.facebook.react.bridge.Promise;
23
12
  import com.facebook.react.bridge.ReactApplicationContext;
24
13
  import com.facebook.react.bridge.WritableMap;
25
14
 
26
- import javax.crypto.SecretKey;
27
-
28
- @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
29
15
  public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
30
16
  public static final String TAG = "RTNHCEAndroidModule";
31
17
  public static final String NAME = "NativeHCEModule";
32
18
 
33
19
  private volatile boolean sessionRunning;
34
20
  private volatile boolean hceRunning;
35
- private volatile boolean hceBreakConnection;
36
21
  private volatile boolean hceDeselected;
37
- private final SecretKey encSecretKey;
22
+ private volatile boolean hceBrokenConnection;
38
23
 
39
- private void sendEvent(final String type, final String arg) {
40
- WritableMap map = Arguments.createMap();
41
- map.putString("type", type);
42
- map.putString("arg", arg);
43
- emitOnEvent(map);
44
- }
24
+ private volatile boolean hceBackgroundReady;
45
25
 
46
- private final BroadcastReceiver receiver = new BroadcastReceiver() {
47
- @Override
48
- public void onReceive(Context context, Intent intent) {
49
- try {
50
- String action = intent.getAction();
51
-
52
- if (action != null) {
53
- AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra("auth"));
54
-
55
- if (action.equals(ACTION_RECEIVE_C_APDU)) {
56
- String capdu = AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra("capdu"));
57
-
58
- if (!hceBreakConnection && hceRunning) {
59
- sendEvent("received", capdu);
60
- } else {
61
- Intent rintent = new Intent(ACTION_SEND_R_APDU);
62
- rintent.setPackage(getReactApplicationContext().getPackageName());
63
- rintent.putExtra("auth", AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
64
- rintent.putExtra("rapdu", AESGCMUtil.encryptData(encSecretKey, "6999"));
65
- getReactApplicationContext().getApplicationContext().sendBroadcast(rintent);
66
- }
67
- } else if (hceRunning && action.equals(ACTION_READER_DETECT)) {
68
- hceDeselected = false;
69
- sendEvent("readerDetected", "");
70
- } else if (action.equals(ACTION_READER_LOST)) {
71
- if (hceRunning && !hceBreakConnection && !hceDeselected) {
72
- // only if we haven't send a fake disconnect event already
73
- hceDeselected = true;
74
- sendEvent("readerDeselected", "");
75
- }
76
-
77
- hceBreakConnection = false;
78
- }
79
- }
80
- } catch (Exception e) {
81
- Log.e(TAG, "Failed to handle broadcast in RTNHCEAndroidModule.", e);
82
- }
83
- }
84
- };
26
+ private HCEServiceCallback serviceCb;
85
27
 
86
28
  RTNHCEAndroidModule(ReactApplicationContext context) {
87
29
  super(context);
88
- String encKey;
30
+
31
+ this.serviceCb = null;
32
+
89
33
  this.sessionRunning = false;
90
34
  this.hceRunning = false;
91
- this.hceBreakConnection = false;
92
35
  this.hceDeselected = true;
93
-
94
- try {
95
- encSecretKey = AESGCMUtil.generateKey();
96
- encKey = BinaryUtils.ByteArrayToHexString(encSecretKey.getEncoded());
97
- } catch (Exception e) {
98
- throw new RuntimeException("Failed to generate encryption key.", e);
99
- }
100
-
101
- String prefsName = getReactApplicationContext().getPackageName() + ".nativehcemodule";
102
- getReactApplicationContext()
103
- .getSharedPreferences(prefsName, Context.MODE_PRIVATE)
104
- .edit()
105
- .putString("encKey", encKey)
106
- .commit();
107
-
108
- IntentFilter filter = new IntentFilter();
109
- filter.addAction(ACTION_RECEIVE_C_APDU);
110
- filter.addAction(ACTION_READER_DETECT);
111
- filter.addAction(ACTION_READER_LOST);
112
-
113
- /*
114
- * FIXME It seems like those intents are not going through across this module and HostApduService.
115
- * This happens even though those things are within the same application and package.
116
- * It also seems like there is no way to bind HostApduService (or it's a skill issue).
117
- *
118
- * For now we will just encrypt&authenticate all messages using AES/GCM and use exported
119
- * receivers. This is not a perfect solution but it is going to be secure and sufficient for now.
120
- *
121
- * If somebody knows a better way then any pull requests would be greatly appreciated.
122
- */
123
- getReactApplicationContext().getApplicationContext().registerReceiver(receiver, filter, RECEIVER_EXPORTED);
36
+ this.hceBrokenConnection = false;
37
+ this.hceBackgroundReady = false;
124
38
  }
125
39
 
126
40
  @Override
127
41
  public void invalidate() {
128
- getReactApplicationContext().getApplicationContext().unregisterReceiver(receiver);
42
+ Log.d(TAG, "RTNHCEAndroidModule:invalidate");
129
43
  }
130
44
 
131
45
  @Override
132
46
  @NonNull
133
47
  public String getName() {
48
+ Log.d(TAG, "RTNHCEAndroidModule:getName");
134
49
  return RTNHCEAndroidModule.NAME;
135
50
  }
136
51
 
52
+ void sendEvent(final String type, final String arg) {
53
+ Log.d(TAG, "RTNHCEAndroidModule:sendEvent");
54
+ WritableMap map = Arguments.createMap();
55
+ map.putString("type", type);
56
+ map.putString("arg", arg);
57
+ emitOnEvent(map);
58
+ }
59
+
60
+ void sendBackgroundEvent(final String type, final String arg) {
61
+ Log.d(TAG, "RTNHCEAndroidModule:sendBackgroundEvent");
62
+ WritableMap map = Arguments.createMap();
63
+ map.putString("type", type);
64
+ map.putString("arg", arg);
65
+ emitOnBackgroundEvent(map);
66
+ }
67
+
68
+ boolean _isHCERunning() {
69
+ return this.hceRunning;
70
+ }
71
+
72
+ void setHCEBrokenConnection() {
73
+ this.hceBrokenConnection = true;
74
+ }
75
+
76
+ boolean isHCEBackgroundHandlerReady() {
77
+ return this.hceBackgroundReady;
78
+ }
79
+
80
+ boolean isHCEBrokenConnection() {
81
+ return this.hceBrokenConnection;
82
+ }
83
+
84
+ void setHCEService(HCEServiceCallback serviceCallback) {
85
+ Log.d(TAG, "RTNHCEAndroidModule:setHCEService");
86
+
87
+ this.serviceCb = serviceCallback;
88
+ this.hceDeselected = true;
89
+ this.hceBackgroundReady = false;
90
+ this.hceBrokenConnection = false;
91
+ }
92
+
137
93
  @Override
138
94
  public boolean isPlatformSupported() {
95
+ Log.d(TAG, "RTNHCEAndroidModule:isPlatformSupported");
96
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
97
+ return false;
98
+ }
99
+
139
100
  PackageManager pm = getReactApplicationContext().getPackageManager();
140
101
  return pm.hasSystemFeature(PackageManager.FEATURE_NFC)
141
- && pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION);
102
+ && pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION);
142
103
  }
143
104
 
144
105
  @Override
145
106
  public void acquireExclusiveNFC(Promise promise) {
107
+ Log.d(TAG, "RTNHCEAndroidModule:acquireExclusiveNFC");
146
108
  promise.reject("err_platform_unsupported", "acquireExclusiveNFC() is not supported on Android.");
147
109
  }
148
110
 
149
111
  @Override
150
112
  public boolean isExclusiveNFC() {
113
+ Log.d(TAG, "RTNHCEAndroidModule:isExclusiveNFC");
151
114
  // unsupported on Android, always false
152
115
  return false;
153
116
  }
154
117
 
155
118
  @Override
156
119
  public void beginSession(Promise promise) {
120
+ Log.d(TAG, "RTNHCEAndroidModule:beginSession");
121
+
122
+ if (!isPlatformSupported()) {
123
+ promise.reject("err_platform_unsupported", "Unsupported Android version or missing NFC/HCE feature.");
124
+ return;
125
+ }
126
+
157
127
  if (!this.sessionRunning) {
158
128
  this.sessionRunning = true;
159
129
  sendEvent("sessionStarted", "");
@@ -165,41 +135,70 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
165
135
 
166
136
  @Override
167
137
  public void setSessionAlertMessage(String message) {
138
+ Log.d(TAG, "RTNHCEAndroidModule:setSessionAlertMessage");
168
139
  // unsupported on Android, no-op
169
140
  }
170
141
 
171
142
  @Override
172
143
  public void invalidateSession() {
144
+ Log.d(TAG, "RTNHCEAndroidModule:invalidateSession");
173
145
  if (this.sessionRunning) {
146
+ this.hceRunning = false;
174
147
  this.sessionRunning = false;
148
+ this.hceBrokenConnection = true;
175
149
  sendEvent("sessionInvalidated", "userInvalidated");
176
150
  }
177
151
  }
178
152
 
179
153
  @Override
180
154
  public boolean isSessionRunning() {
155
+ Log.d(TAG, "RTNHCEAndroidModule:isSessionRunning");
181
156
  return this.sessionRunning;
182
157
  }
183
158
 
159
+ @Override
160
+ public void initBackgroundHCE(Promise promise) {
161
+ Log.d(TAG, "RTNHCEAndroidModule:initBackgroundHCE");
162
+
163
+ this.hceBackgroundReady = true;
164
+
165
+ promise.resolve(null);
166
+
167
+ this.serviceCb.onBackgroundHCEInit();
168
+ }
169
+
184
170
  @Override
185
171
  public void startHCE(Promise promise) {
172
+ Log.d(TAG, "RTNHCEAndroidModule:startHCE");
173
+ if (!this.sessionRunning) {
174
+ promise.reject("err_no_session", "No session is active.");
175
+ return;
176
+ }
177
+
186
178
  if (this.hceRunning) {
187
179
  promise.reject("err_start_emulation", "Error trying to start emulation. Emulation already started.");
188
180
  return;
189
181
  }
190
182
 
191
183
  this.hceRunning = true;
184
+ this.hceBrokenConnection = false;
192
185
  promise.resolve(null);
193
186
  }
194
187
 
195
188
  @Override
196
189
  public void stopHCE(String status, Promise promise) {
190
+ Log.d(TAG, "RTNHCEAndroidModule:stopHCE");
191
+ if (!this.sessionRunning) {
192
+ promise.reject("err_no_session", "No session is active.");
193
+ return;
194
+ }
195
+
197
196
  if (this.hceRunning) {
198
197
  this.hceRunning = false;
199
198
 
200
199
  if (!this.hceDeselected) {
201
200
  this.hceDeselected = true;
202
- this.hceBreakConnection = true;
201
+ this.hceBrokenConnection = true;
203
202
  sendEvent("readerDeselected", "");
204
203
  }
205
204
  }
@@ -207,22 +206,36 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
207
206
  promise.resolve(null);
208
207
  }
209
208
 
209
+ @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
210
210
  @Override
211
211
  public void respondAPDU(String rapdu, Promise promise) {
212
- String encRapdu = AESGCMUtil.encryptData(encSecretKey, rapdu);
212
+ Log.d(TAG, "RTNHCEAndroidModule:respondAPDU");
213
+
214
+ if (!this.hceBackgroundReady) {
215
+ if (!this.sessionRunning) {
216
+ promise.reject("err_no_session", "No session is active.");
217
+ return;
218
+ } else if (!this.hceRunning) {
219
+ promise.reject("err_no_hce", "HCE is not running.");
220
+ return;
221
+ }
222
+ }
213
223
 
214
- Intent intent = new Intent(ACTION_SEND_R_APDU);
215
- intent.setPackage(getReactApplicationContext().getPackageName());
216
- intent.putExtra("auth", AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
217
- intent.putExtra("rapdu", encRapdu);
218
- getReactApplicationContext().getApplicationContext().sendBroadcast(intent);
224
+ Log.d(TAG, "respondAPDU(): Send to service");
225
+
226
+ try {
227
+ serviceCb.onRespondAPDU(rapdu);
228
+ } catch (IllegalStateException e) {
229
+ promise.reject("err_no_capdu", "There is no C-APDU to respond to at the time.");
230
+ return;
231
+ }
219
232
 
220
233
  promise.resolve(null);
221
234
  }
222
235
 
223
236
  @Override
224
237
  public void isHCERunning(Promise promise) {
225
- // unsupported on Android, always true
226
- promise.resolve(true);
238
+ Log.d(TAG, "RTNHCEAndroidModule:isHCERunning");
239
+ promise.resolve(this.hceRunning);
227
240
  }
228
241
  }
@@ -1,9 +1,6 @@
1
1
  package com.itsecrnd.rtnhceandroid;
2
2
 
3
- import android.os.Build;
4
-
5
3
  import androidx.annotation.Nullable;
6
- import androidx.annotation.RequiresApi;
7
4
 
8
5
  import com.facebook.react.TurboReactPackage;
9
6
  import com.facebook.react.bridge.NativeModule;
@@ -14,7 +11,6 @@ import com.facebook.react.module.model.ReactModuleInfoProvider;
14
11
  import java.util.HashMap;
15
12
  import java.util.Map;
16
13
 
17
- @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
18
14
  public class RTNHCEAndroidPackage extends TurboReactPackage {
19
15
  @Nullable
20
16
  @Override
@@ -86,6 +86,10 @@
86
86
  }
87
87
  }
88
88
 
89
+ - (void)initBackgroundHCE:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
90
+ reject(@"err_platform_unsupported", @"Background HCE is not supported on iOS.", nil);
91
+ }
92
+
89
93
  - (void)startHCE:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
90
94
  if (@available(iOS 17.4, *)) {
91
95
  return [hcemoduleios startHCEWithResolve:resolve reject:reject];
@@ -13,13 +13,8 @@ extension StringProtocol {
13
13
  }
14
14
 
15
15
  extension Data {
16
- struct HexEncodingOptions: OptionSet {
17
- let rawValue: Int
18
- static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
19
- }
20
-
21
- func hexEncodedString(options: HexEncodingOptions = []) -> String {
22
- let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx"
16
+ func hexEncodedString() -> String {
17
+ let format = "%02hhX"
23
18
  return map { String(format: format, $0) }.joined()
24
19
  }
25
20
  }
@@ -1,7 +1,8 @@
1
1
  import type {TurboModule, CodegenTypes} from 'react-native';
2
2
  import {TurboModuleRegistry} from 'react-native';
3
3
 
4
- export type HCEModuleEventType = 'sessionStarted'
4
+ export type HCEModuleEventType =
5
+ 'sessionStarted'
5
6
  | 'sessionInvalidated'
6
7
  /**
7
8
  * iOS: Whether the reader field was detected.
@@ -19,11 +20,20 @@ export type HCEModuleEventType = 'sessionStarted'
19
20
  | 'received';
20
21
  export type HCEModuleStopReason = 'success' | 'failure';
21
22
 
23
+ export type HCEModuleBackgroundEventType =
24
+ 'readerDeselected'
25
+ | 'received';
26
+
22
27
  export type HCEModuleEvent = {
23
28
  type: HCEModuleEventType
24
29
  arg: string | null,
25
30
  }
26
31
 
32
+ export type HCEModuleBackgroundEvent = {
33
+ type: HCEModuleBackgroundEventType
34
+ arg: string | null,
35
+ }
36
+
27
37
  export interface Spec extends TurboModule {
28
38
  /**
29
39
  * Checks if a given smartphone supports required HCE features.
@@ -63,6 +73,8 @@ export interface Spec extends TurboModule {
63
73
  */
64
74
  isSessionRunning(): boolean;
65
75
 
76
+ initBackgroundHCE(): Promise<void>;
77
+
66
78
  /**
67
79
  * iOS: Start the card emulation.
68
80
  * Android: Enable HCE service.
@@ -84,6 +96,7 @@ export interface Spec extends TurboModule {
84
96
  isHCERunning(): Promise<boolean>;
85
97
 
86
98
  readonly onEvent: CodegenTypes.EventEmitter<HCEModuleEvent>;
99
+ readonly onBackgroundEvent: CodegenTypes.EventEmitter<HCEModuleBackgroundEvent>;
87
100
  }
88
101
 
89
102
  export default TurboModuleRegistry.getEnforcing<Spec>(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icedevml/react-native-host-card-emulation",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Host Card Emulation (HCE) React Native Turbo Module for iOS and Android platforms.",
5
5
  "react-native": "js/index",
6
6
  "source": "js/index",
@@ -1,89 +0,0 @@
1
- package com.itsecrnd.rtnhceandroid;
2
-
3
- import android.annotation.SuppressLint;
4
-
5
- import java.nio.charset.StandardCharsets;
6
- import java.security.InvalidAlgorithmParameterException;
7
- import java.security.InvalidKeyException;
8
- import java.security.NoSuchAlgorithmException;
9
- import java.security.SecureRandom;
10
- import java.util.Base64;
11
-
12
- import javax.crypto.BadPaddingException;
13
- import javax.crypto.Cipher;
14
- import javax.crypto.IllegalBlockSizeException;
15
- import javax.crypto.KeyGenerator;
16
- import javax.crypto.NoSuchPaddingException;
17
- import javax.crypto.SecretKey;
18
- import javax.crypto.spec.GCMParameterSpec;
19
-
20
- @SuppressLint("NewApi")
21
- public class AESGCMUtil {
22
- public static String randomString() {
23
- byte[] rnd = new byte[32];
24
- new SecureRandom().nextBytes(rnd);
25
- return BinaryUtils.ByteArrayToHexString(rnd);
26
- }
27
-
28
- public static SecretKey generateKey() {
29
- KeyGenerator keyGen;
30
- try {
31
- keyGen = KeyGenerator.getInstance("AES");
32
- } catch (NoSuchAlgorithmException e) {
33
- throw new RuntimeException("Failed to get AES algorithm.", e);
34
- }
35
- keyGen.init(256);
36
- return keyGen.generateKey();
37
- }
38
-
39
- public static String encryptData(SecretKey key, String plaintext) {
40
- try {
41
- byte[] iv = new byte[32];
42
- new SecureRandom().nextBytes(iv);
43
-
44
- Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
45
- GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
46
- cipher.init(Cipher.ENCRYPT_MODE, key, gcmParameterSpec);
47
-
48
- byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
49
- byte[] ciphertextBytes = cipher.doFinal(plaintextBytes);
50
-
51
- return Base64.getEncoder().encodeToString(iv) + ';' +
52
- Base64.getEncoder().encodeToString(ciphertextBytes);
53
- } catch (NoSuchPaddingException
54
- | NoSuchAlgorithmException
55
- | IllegalBlockSizeException
56
- | InvalidAlgorithmParameterException
57
- | BadPaddingException
58
- | InvalidKeyException e) {
59
- throw new RuntimeException("Crypto error.", e);
60
- }
61
- }
62
-
63
- public static String decryptData(SecretKey key, String ciphertext) {
64
- try {
65
- Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
66
- String[] parts = ciphertext.split(";");
67
-
68
- if (parts.length != 2) {
69
- throw new IllegalArgumentException("Invalid ciphertext parameter.");
70
- }
71
-
72
- byte[] iv = Base64.getDecoder().decode(parts[0]);
73
- byte[] ciphertextBytes = Base64.getDecoder().decode(parts[1]);
74
-
75
- GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
76
- cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
77
-
78
- byte[] plaintextBytes = cipher.doFinal(ciphertextBytes);
79
- return new String(plaintextBytes, StandardCharsets.UTF_8);
80
- } catch (NoSuchPaddingException
81
- | IllegalBlockSizeException
82
- | InvalidAlgorithmParameterException
83
- | BadPaddingException
84
- | InvalidKeyException
85
- | NoSuchAlgorithmException e) {
86
- throw new RuntimeException("Crypto error.", e);
87
- }
88
- }
89
- }
@@ -1,11 +0,0 @@
1
- package com.itsecrnd.rtnhceandroid;
2
-
3
- public class IntentKeys {
4
- public static final String ACTION_RECEIVE_C_APDU = "com.itsecrnd.rtnhceandroid.ACTION_RECEIVE_C_APDU";
5
- public static final String ACTION_SEND_R_APDU = "com.itsecrnd.rtnhceandroid.ACTION_SEND_R_APDU";
6
- public static final String ACTION_READER_DETECT = "com.itsecrnd.rtnhceandroid.ACTION_READER_DETECT";
7
- public static final String ACTION_READER_LOST = "com.itsecrnd.rtnhceandroid.ACTION_READER_LOST";
8
- public static final String KEY_AUTH = "auth";
9
- public static final String KEY_CAPDU = "capdu";
10
- public static final String KEY_RAPDU = "rapdu";
11
- }