@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 +51 -6
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/{util/BinaryUtils.java → BinaryUtils.java} +1 -1
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEService.java +235 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEServiceCallback.java +6 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidModule.java +94 -94
- package/ios/RTNHCEModuleIOS.mm +4 -0
- package/js/NativeHCEModule.ts +14 -1
- package/package.json +1 -1
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/IntentKeys.java +0 -11
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/service/HCEService.java +0 -90
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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)
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
}
|
package/ios/RTNHCEModuleIOS.mm
CHANGED
|
@@ -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];
|
package/js/NativeHCEModule.ts
CHANGED
|
@@ -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 =
|
|
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,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
|
-
}
|