@icedevml/react-native-host-card-emulation 0.4.1 → 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
@@ -64,9 +64,8 @@ Afterwards, follow the subsections below for each platform that you need to supp
64
64
  ...
65
65
 
66
66
  + <uses-permission android:name="android.permission.NFC" />
67
+ + <!-- set required="true" if your app would be entirely useless on devices without HCE support -->
67
68
  + <uses-feature android:name="android.hardware.nfc.hce" android:required="false" />
68
- + <permission android:name="com.itsecrnd.rtnhceandroid.HCE_BROADCAST" android:protectionLevel="signature" />
69
- + <uses-permission android:name="com.itsecrnd.rtnhceandroid.HCE_BROADCAST"/>
70
69
 
71
70
  ...
72
71
 
@@ -77,7 +76,7 @@ Afterwards, follow the subsections below for each platform that you need to supp
77
76
  ...
78
77
 
79
78
  + <service
80
- + android:name="com.itsecrnd.rtnhceandroid.service.HCEService"
79
+ + android:name="com.itsecrnd.rtnhceandroid.HCEService"
81
80
  + android:exported="true"
82
81
  + android:enabled="true"
83
82
  + android:permission="android.permission.BIND_NFC_SERVICE">
@@ -124,7 +123,7 @@ This module provides a uniform low-level HCE API for both mobile platforms.
124
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.
125
124
  ```typescript
126
125
  useEffect(() => {
127
- const removeListener = NativeHCEModule?.onEvent(async (event: HCEModuleEvent) => {
126
+ const subscription = NativeHCEModule?.onEvent(async (event: HCEModuleEvent) => {
128
127
  switch (event.type) {
129
128
  /* ... implement event handlers here ... */
130
129
  }
@@ -132,7 +131,7 @@ This module provides a uniform low-level HCE API for both mobile platforms.
132
131
 
133
132
  // cleanup the event listener when the effect is unmounted
134
133
  return () => {
135
- removeListener();
134
+ subscription.remove();
136
135
  }
137
136
  }, []);
138
137
  ```
@@ -163,7 +162,7 @@ This module provides a uniform low-level HCE API for both mobile platforms.
163
162
  break;
164
163
  ```
165
164
  For those events, trigger mechanisms are platform dependent:
166
- * iOS: 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.
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.
167
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.
168
167
  4. Receive incoming C-APDU and respond to it:
169
168
  ```typescript
@@ -182,6 +181,7 @@ This module provides a uniform low-level HCE API for both mobile platforms.
182
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.
183
182
 
184
183
  ### iOS: Acquiring exclusive NFC access
184
+
185
185
  If you need to utilize `NFCPresentmentIntentAssertion` for enhanced user experience, call:
186
186
  ```typescript
187
187
  NativeHCEModule?.acquireExclusiveNFC();
@@ -195,6 +195,51 @@ This function will throw an exception if:
195
195
 
196
196
  Call `NativeHCEModule?.isExclusiveNFC()` to check if exclusive NFC access is still active.
197
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`:
205
+ ```typescript
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;
241
+ ```
242
+
198
243
  ### More resources
199
244
 
200
245
  * [Module's API specification](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/native-hce-module/js/NativeHCEModule.ts)
@@ -20,7 +20,7 @@
20
20
  *
21
21
  */
22
22
 
23
- package com.itsecrnd.rtnhceandroid.util;
23
+ package com.itsecrnd.rtnhceandroid;
24
24
 
25
25
  public class BinaryUtils {
26
26
  /**
@@ -0,0 +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
+
7
+ package com.itsecrnd.rtnhceandroid;
8
+
9
+ import static com.facebook.react.jstasks.HeadlessJsTaskContext.Companion;
10
+
11
+ import android.app.ActivityManager;
12
+ import android.content.Context;
13
+ import android.nfc.cardemulation.HostApduService;
14
+ import android.os.Build;
15
+ import android.os.Bundle;
16
+ import android.util.Log;
17
+
18
+ import androidx.annotation.NonNull;
19
+ import androidx.annotation.RequiresApi;
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;
32
+ import java.util.Locale;
33
+
34
+ @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
35
+ public class HCEService extends HostApduService implements ReactInstanceEventListener, HCEServiceCallback {
36
+ private static final String TAG = "HCEService";
37
+
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;
60
+ }
61
+ }
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
+ }
75
+
76
+ @Override
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
+ }
88
+
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
+ }
114
+
115
+ return null;
116
+ }
117
+
118
+ @Override
119
+ public void onCreate() {
120
+ Log.d(TAG, "HCEService:onCreate");
121
+
122
+ ReactHost reactHost = ((ReactApplication) getApplication()).getReactHost();
123
+
124
+ if (reactHost == null) {
125
+ throw new RuntimeException("BUG! getReactHost() returned null.");
126
+ }
127
+
128
+ isForeground = isAppOnForeground(getApplicationContext());
129
+ pendingCAPDU = null;
130
+ hceModule = null;
131
+ needsResponse = false;
132
+
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
+ }
166
+ }
167
+
168
+ @Override
169
+ public void onDeactivated(int reason) {
170
+ Log.d(TAG, "HCEService:onDeactivated: " + reason);
171
+ needsResponse = false;
172
+
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();
232
+
233
+ Log.d(TAG, "HCEService:onDestroy");
234
+ }
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
- import static com.itsecrnd.rtnhceandroid.IntentKeys.PERMISSION_HCE_BROADCAST;
9
-
10
- import android.content.BroadcastReceiver;
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;
@@ -29,102 +18,81 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
29
18
 
30
19
  private volatile boolean sessionRunning;
31
20
  private volatile boolean hceRunning;
32
- private volatile boolean hceBreakConnection;
33
21
  private volatile boolean hceDeselected;
22
+ private volatile boolean hceBrokenConnection;
34
23
 
35
- private void sendEvent(final String type, final String arg) {
36
- WritableMap map = Arguments.createMap();
37
- map.putString("type", type);
38
- map.putString("arg", arg);
39
- emitOnEvent(map);
40
- }
24
+ private volatile boolean hceBackgroundReady;
41
25
 
42
- @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
43
- private final BroadcastReceiver receiver = new BroadcastReceiver() {
44
- @Override
45
- public void onReceive(Context context, Intent intent) {
46
- try {
47
- String action = intent.getAction();
48
-
49
- if (action == null) {
50
- Log.w(TAG, "Received intent with null action in module receiver.");
51
- return;
52
- }
53
-
54
- if (action.equals(ACTION_RECEIVE_C_APDU)) {
55
- String capdu = intent.getStringExtra("capdu");
56
-
57
- if (!hceBreakConnection && hceRunning) {
58
- sendEvent("received", capdu);
59
- } else {
60
- // HCE is already disabled or was just disabled while the HCEService was running
61
- // don't forward C-APDUs to the application and just respond 6999 to everything
62
- Intent rintent = new Intent(ACTION_SEND_R_APDU);
63
- rintent.setPackage(getReactApplicationContext().getPackageName());
64
- rintent.putExtra("rapdu", "6999");
65
- getReactApplicationContext().getApplicationContext().sendBroadcast(rintent, PERMISSION_HCE_BROADCAST);
66
- }
67
- } else if (action.equals(ACTION_READER_DETECT)) {
68
- if (hceRunning) {
69
- hceDeselected = false;
70
- sendEvent("readerDetected", "");
71
- }
72
- } else if (action.equals(ACTION_READER_LOST)) {
73
- if (hceRunning && !hceBreakConnection && !hceDeselected) {
74
- // only if we haven't send a fake disconnect event already
75
- hceDeselected = true;
76
- sendEvent("readerDeselected", "");
77
- }
78
-
79
- hceBreakConnection = false;
80
- } else {
81
- Log.w(TAG, "Received unknown action intent in module receiver: " + action);
82
- }
83
- } catch (Exception e) {
84
- Log.e(TAG, "Failed to handle broadcast in RTNHCEAndroidModule.", e);
85
- }
86
- }
87
- };
26
+ private HCEServiceCallback serviceCb;
88
27
 
89
28
  RTNHCEAndroidModule(ReactApplicationContext context) {
90
29
  super(context);
91
30
 
31
+ this.serviceCb = null;
32
+
92
33
  this.sessionRunning = false;
93
34
  this.hceRunning = false;
94
- this.hceBreakConnection = false;
95
35
  this.hceDeselected = true;
96
-
97
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
98
- IntentFilter filter = new IntentFilter();
99
- filter.addAction(ACTION_RECEIVE_C_APDU);
100
- filter.addAction(ACTION_READER_DETECT);
101
- filter.addAction(ACTION_READER_LOST);
102
-
103
- getReactApplicationContext().getApplicationContext().registerReceiver(
104
- receiver,
105
- filter,
106
- PERMISSION_HCE_BROADCAST,
107
- null,
108
- RECEIVER_EXPORTED
109
- );
110
- }
36
+ this.hceBrokenConnection = false;
37
+ this.hceBackgroundReady = false;
111
38
  }
112
39
 
113
40
  @Override
114
41
  public void invalidate() {
115
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
116
- getReactApplicationContext().getApplicationContext().unregisterReceiver(receiver);
117
- }
42
+ Log.d(TAG, "RTNHCEAndroidModule:invalidate");
118
43
  }
119
44
 
120
45
  @Override
121
46
  @NonNull
122
47
  public String getName() {
48
+ Log.d(TAG, "RTNHCEAndroidModule:getName");
123
49
  return RTNHCEAndroidModule.NAME;
124
50
  }
125
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
+
126
93
  @Override
127
94
  public boolean isPlatformSupported() {
95
+ Log.d(TAG, "RTNHCEAndroidModule:isPlatformSupported");
128
96
  if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
129
97
  return false;
130
98
  }
@@ -136,17 +104,21 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
136
104
 
137
105
  @Override
138
106
  public void acquireExclusiveNFC(Promise promise) {
107
+ Log.d(TAG, "RTNHCEAndroidModule:acquireExclusiveNFC");
139
108
  promise.reject("err_platform_unsupported", "acquireExclusiveNFC() is not supported on Android.");
140
109
  }
141
110
 
142
111
  @Override
143
112
  public boolean isExclusiveNFC() {
113
+ Log.d(TAG, "RTNHCEAndroidModule:isExclusiveNFC");
144
114
  // unsupported on Android, always false
145
115
  return false;
146
116
  }
147
117
 
148
118
  @Override
149
119
  public void beginSession(Promise promise) {
120
+ Log.d(TAG, "RTNHCEAndroidModule:beginSession");
121
+
150
122
  if (!isPlatformSupported()) {
151
123
  promise.reject("err_platform_unsupported", "Unsupported Android version or missing NFC/HCE feature.");
152
124
  return;
@@ -163,24 +135,41 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
163
135
 
164
136
  @Override
165
137
  public void setSessionAlertMessage(String message) {
138
+ Log.d(TAG, "RTNHCEAndroidModule:setSessionAlertMessage");
166
139
  // unsupported on Android, no-op
167
140
  }
168
141
 
169
142
  @Override
170
143
  public void invalidateSession() {
144
+ Log.d(TAG, "RTNHCEAndroidModule:invalidateSession");
171
145
  if (this.sessionRunning) {
146
+ this.hceRunning = false;
172
147
  this.sessionRunning = false;
148
+ this.hceBrokenConnection = true;
173
149
  sendEvent("sessionInvalidated", "userInvalidated");
174
150
  }
175
151
  }
176
152
 
177
153
  @Override
178
154
  public boolean isSessionRunning() {
155
+ Log.d(TAG, "RTNHCEAndroidModule:isSessionRunning");
179
156
  return this.sessionRunning;
180
157
  }
181
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
+
182
170
  @Override
183
171
  public void startHCE(Promise promise) {
172
+ Log.d(TAG, "RTNHCEAndroidModule:startHCE");
184
173
  if (!this.sessionRunning) {
185
174
  promise.reject("err_no_session", "No session is active.");
186
175
  return;
@@ -192,11 +181,13 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
192
181
  }
193
182
 
194
183
  this.hceRunning = true;
184
+ this.hceBrokenConnection = false;
195
185
  promise.resolve(null);
196
186
  }
197
187
 
198
188
  @Override
199
189
  public void stopHCE(String status, Promise promise) {
190
+ Log.d(TAG, "RTNHCEAndroidModule:stopHCE");
200
191
  if (!this.sessionRunning) {
201
192
  promise.reject("err_no_session", "No session is active.");
202
193
  return;
@@ -207,7 +198,7 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
207
198
 
208
199
  if (!this.hceDeselected) {
209
200
  this.hceDeselected = true;
210
- this.hceBreakConnection = true;
201
+ this.hceBrokenConnection = true;
211
202
  sendEvent("readerDeselected", "");
212
203
  }
213
204
  }
@@ -218,24 +209,33 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
218
209
  @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
219
210
  @Override
220
211
  public void respondAPDU(String rapdu, Promise promise) {
221
- if (!this.sessionRunning) {
222
- promise.reject("err_no_session", "No session is active.");
223
- return;
224
- } else if (!this.hceRunning) {
225
- promise.reject("err_no_hce", "HCE is not running.");
226
- return;
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
+ }
227
222
  }
228
223
 
229
- Intent intent = new Intent(ACTION_SEND_R_APDU);
230
- intent.setPackage(getReactApplicationContext().getPackageName());
231
- intent.putExtra("rapdu", rapdu);
232
- getReactApplicationContext().getApplicationContext().sendBroadcast(intent, PERMISSION_HCE_BROADCAST);
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
+ }
233
232
 
234
233
  promise.resolve(null);
235
234
  }
236
235
 
237
236
  @Override
238
237
  public void isHCERunning(Promise promise) {
238
+ Log.d(TAG, "RTNHCEAndroidModule:isHCERunning");
239
239
  promise.resolve(this.hceRunning);
240
240
  }
241
241
  }
@@ -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];
@@ -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.1",
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,11 +0,0 @@
1
- package com.itsecrnd.rtnhceandroid;
2
-
3
- public class IntentKeys {
4
- public static final String PERMISSION_HCE_BROADCAST = "com.itsecrnd.rtnhceandroid.HCE_BROADCAST";
5
- public static final String ACTION_RECEIVE_C_APDU = "com.itsecrnd.rtnhceandroid.ACTION_RECEIVE_C_APDU";
6
- public static final String ACTION_SEND_R_APDU = "com.itsecrnd.rtnhceandroid.ACTION_SEND_R_APDU";
7
- public static final String ACTION_READER_DETECT = "com.itsecrnd.rtnhceandroid.ACTION_READER_DETECT";
8
- public static final String ACTION_READER_LOST = "com.itsecrnd.rtnhceandroid.ACTION_READER_LOST";
9
- public static final String KEY_CAPDU = "capdu";
10
- public static final String KEY_RAPDU = "rapdu";
11
- }
@@ -1,90 +0,0 @@
1
- package com.itsecrnd.rtnhceandroid.service;
2
-
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_CAPDU;
8
- import static com.itsecrnd.rtnhceandroid.IntentKeys.KEY_RAPDU;
9
- import static com.itsecrnd.rtnhceandroid.IntentKeys.PERMISSION_HCE_BROADCAST;
10
-
11
- import android.content.BroadcastReceiver;
12
- import android.content.Context;
13
- import android.content.Intent;
14
- import android.content.IntentFilter;
15
- import android.nfc.cardemulation.HostApduService;
16
- import android.os.Build;
17
- import android.os.Bundle;
18
- import android.util.Log;
19
-
20
- import androidx.annotation.RequiresApi;
21
-
22
- import com.itsecrnd.rtnhceandroid.util.BinaryUtils;
23
-
24
- import java.util.Locale;
25
-
26
- @RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
27
- public class HCEService extends HostApduService {
28
- private static final String TAG = "HCEService";
29
-
30
- private final BroadcastReceiver receiver = new BroadcastReceiver() {
31
- @Override
32
- public void onReceive(Context context, Intent intent) {
33
- try {
34
- String action = intent.getAction();
35
-
36
- if (action != null && action.equals(ACTION_SEND_R_APDU)) {
37
- String rapdu = intent.getStringExtra(KEY_RAPDU);
38
-
39
- if (rapdu == null) {
40
- throw new RuntimeException("Bug! ACTION_SEND_RAPDU intent KEY_RAPDU is null.");
41
- }
42
-
43
- byte[] dec = BinaryUtils.HexStringToByteArray(rapdu.toUpperCase(Locale.ROOT));
44
- sendResponseApdu(dec);
45
- } else {
46
- Log.w(TAG, "Received unknown action: " + action);
47
- }
48
- } catch (Exception e) {
49
- Log.e(TAG, "Failed to handle broadcast in HCEService.", e);
50
- }
51
- }
52
- };
53
-
54
- @Override
55
- public byte[] processCommandApdu(byte[] command, Bundle extras) {
56
- String capdu = BinaryUtils.ByteArrayToHexString(command).toUpperCase(Locale.ROOT);
57
-
58
- Intent intent = new Intent(ACTION_RECEIVE_C_APDU);
59
- intent.setPackage(getApplicationContext().getPackageName());
60
- intent.putExtra(KEY_CAPDU, capdu);
61
- getApplicationContext().sendBroadcast(intent, PERMISSION_HCE_BROADCAST);
62
-
63
- return null;
64
- }
65
-
66
- @Override
67
- public void onCreate() {
68
- Log.d(TAG, "HCEService onCreate");
69
-
70
- IntentFilter filter = new IntentFilter();
71
- filter.addAction(ACTION_SEND_R_APDU);
72
- getApplicationContext().registerReceiver(
73
- receiver, filter, PERMISSION_HCE_BROADCAST, null, RECEIVER_EXPORTED);
74
-
75
- Intent intent = new Intent(ACTION_READER_DETECT);
76
- intent.setPackage(getApplicationContext().getPackageName());
77
- getApplicationContext().sendBroadcast(intent, PERMISSION_HCE_BROADCAST);
78
- }
79
-
80
- @Override
81
- public void onDeactivated(int reason) {
82
- Log.d(TAG, "HCEService onDeactivated: " + reason);
83
-
84
- getApplicationContext().unregisterReceiver(receiver);
85
-
86
- Intent intent = new Intent(ACTION_READER_LOST);
87
- intent.setPackage(getApplicationContext().getPackageName());
88
- getApplicationContext().sendBroadcast(intent, PERMISSION_HCE_BROADCAST);
89
- }
90
- }