@icedevml/react-native-host-card-emulation 0.4.1 → 0.6.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 +67 -12
- 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 +257 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEServiceCallback.java +6 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidModule.java +99 -95
- package/ios/RTNHCEModuleIOS.mm +6 -2
- package/ios/RTNHCEModuleIOSImpl.swift +6 -1
- package/js/NativeHCEModule.ts +16 -2
- package/js/hceBackground.ts +43 -0
- 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
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# React Native Package: @icedevml/react-native-host-card-emulation
|
|
2
2
|
|
|
3
3
|
Host Card Emulation Module for React Native on iOS and Android platforms.
|
|
4
4
|
|
|
5
|
+
> [!NOTE]
|
|
6
|
+
> Pull requests are welcome! Please [report an issue](https://github.com/icedevml/react-native-host-card-emulation/issues) if you are running into any difficulties.
|
|
7
|
+
|
|
5
8
|
> [!IMPORTANT]
|
|
6
9
|
> As per Apple's policy, **Host Card Emulation feature is only available for users based in the European
|
|
7
10
|
> Economic Area (EEA)** with an iPhone running iOS 17.4 or later. Applications require a special HCE entitlement
|
|
8
11
|
> in order to use this feature, which is managed by Apple. Read more in the official support article:
|
|
9
12
|
> "[HCE-based contactless NFC transactions for apps in the European Economic Area (EEA)](https://developer.apple.com/support/hce-transactions-in-apps/)".
|
|
13
|
+
>
|
|
14
|
+
> However, you can still use this package without any limitations if you only want to cover for Android implementation.
|
|
10
15
|
|
|
11
16
|
## Prerequisites
|
|
12
17
|
This package was designed for React Native [New Architecture](https://reactnative.dev/architecture/landing-page) only.
|
|
@@ -64,9 +69,8 @@ Afterwards, follow the subsections below for each platform that you need to supp
|
|
|
64
69
|
...
|
|
65
70
|
|
|
66
71
|
+ <uses-permission android:name="android.permission.NFC" />
|
|
72
|
+
+ <!-- set required="true" if your app would be entirely useless on devices without HCE support -->
|
|
67
73
|
+ <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
74
|
|
|
71
75
|
...
|
|
72
76
|
|
|
@@ -77,7 +81,7 @@ Afterwards, follow the subsections below for each platform that you need to supp
|
|
|
77
81
|
...
|
|
78
82
|
|
|
79
83
|
+ <service
|
|
80
|
-
+ android:name="com.itsecrnd.rtnhceandroid.
|
|
84
|
+
+ android:name="com.itsecrnd.rtnhceandroid.HCEService"
|
|
81
85
|
+ android:exported="true"
|
|
82
86
|
+ android:enabled="true"
|
|
83
87
|
+ android:permission="android.permission.BIND_NFC_SERVICE">
|
|
@@ -113,18 +117,18 @@ Afterwards, follow the subsections below for each platform that you need to supp
|
|
|
113
117
|
This module provides a uniform low-level HCE API for both mobile platforms.
|
|
114
118
|
|
|
115
119
|
> [!NOTE]
|
|
116
|
-
> Raw native module's API specification is available in [packages/
|
|
120
|
+
> Raw native module's API specification is available in [packages/host-card-emulation/js/NativeHCEModule.ts](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/host-card-emulation/js/NativeHCEModule.ts).
|
|
117
121
|
> Check it out in order to understand what methods you can call against the module and what are the expected parameters/return values.
|
|
118
122
|
|
|
119
123
|
> [!NOTE]
|
|
120
|
-
> See [Demo App's Code (Example Library Usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-
|
|
124
|
+
> See [Demo App's Code (Example Library Usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-ndef-app/App.tsx) example for more insignt about the library's API.
|
|
121
125
|
|
|
122
126
|
### Quick start guide
|
|
123
127
|
|
|
124
128
|
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
129
|
```typescript
|
|
126
130
|
useEffect(() => {
|
|
127
|
-
const
|
|
131
|
+
const subscription = NativeHCEModule?.onEvent(async (event: HCEModuleEvent) => {
|
|
128
132
|
switch (event.type) {
|
|
129
133
|
/* ... implement event handlers here ... */
|
|
130
134
|
}
|
|
@@ -132,7 +136,7 @@ This module provides a uniform low-level HCE API for both mobile platforms.
|
|
|
132
136
|
|
|
133
137
|
// cleanup the event listener when the effect is unmounted
|
|
134
138
|
return () => {
|
|
135
|
-
|
|
139
|
+
subscription.remove();
|
|
136
140
|
}
|
|
137
141
|
}, []);
|
|
138
142
|
```
|
|
@@ -163,7 +167,7 @@ This module provides a uniform low-level HCE API for both mobile platforms.
|
|
|
163
167
|
break;
|
|
164
168
|
```
|
|
165
169
|
For those events, trigger mechanisms are platform dependent:
|
|
166
|
-
* iOS: The `readerDetected` event will be emitted as soon as the NFC
|
|
170
|
+
* 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
171
|
* 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
172
|
4. Receive incoming C-APDU and respond to it:
|
|
169
173
|
```typescript
|
|
@@ -176,12 +180,13 @@ This module provides a uniform low-level HCE API for both mobile platforms.
|
|
|
176
180
|
console.log('Received C-APDU, capdu.toString('hex'));
|
|
177
181
|
|
|
178
182
|
// for the demo purposes, we always want to respond with [0x0A] + status code 0x9000 (success)
|
|
179
|
-
await NativeHCEModule?.respondAPDU(Buffer.from([0x0A, 0x90, 0x00], "hex"));
|
|
183
|
+
await NativeHCEModule?.respondAPDU(null, Buffer.from([0x0A, 0x90, 0x00], "hex"));
|
|
180
184
|
break;
|
|
181
185
|
```
|
|
182
186
|
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
187
|
|
|
184
188
|
### iOS: Acquiring exclusive NFC access
|
|
189
|
+
|
|
185
190
|
If you need to utilize `NFCPresentmentIntentAssertion` for enhanced user experience, call:
|
|
186
191
|
```typescript
|
|
187
192
|
NativeHCEModule?.acquireExclusiveNFC();
|
|
@@ -195,7 +200,57 @@ This function will throw an exception if:
|
|
|
195
200
|
|
|
196
201
|
Call `NativeHCEModule?.isExclusiveNFC()` to check if exclusive NFC access is still active.
|
|
197
202
|
|
|
203
|
+
### Android: Handle HCE calls when the app is not running
|
|
204
|
+
|
|
205
|
+
Exclusively on Android platform it is possible to handle Host Card Emulation even when the app is not running in foreground.
|
|
206
|
+
In such case, no user interface will be available, the handler will run in headless mode. This is still extremely useful in certain cases,
|
|
207
|
+
for instance - your app may emulate an NDEF tag even when it's not launched on the foreground.
|
|
208
|
+
|
|
209
|
+
1. Register `handleBackgroundHCECall` headless task in your app's `index.js`:
|
|
210
|
+
```typescript
|
|
211
|
+
import { createBackgroundHCE } from '@icedevml/react-native-host-card-emulation/js/hceBackground';
|
|
212
|
+
|
|
213
|
+
AppRegistry.registerHeadlessTask('handleBackgroundHCECall', () => {
|
|
214
|
+
return async (taskData) => {
|
|
215
|
+
return await runBackgroundHCETask(createBackgroundHCE(taskData.handle));
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
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.
|
|
220
|
+
```typescript
|
|
221
|
+
import { Buffer } from 'buffer/';
|
|
222
|
+
import {
|
|
223
|
+
ProcessBackgroundHCEFunc,
|
|
224
|
+
} from '@icedevml/react-native-host-card-emulation/js/hceBackground';
|
|
225
|
+
|
|
226
|
+
export default async function runBackgroundHCETask(processBackgroundHCE: ProcessBackgroundHCEFunc) {
|
|
227
|
+
processBackgroundHCE(async (event, respondAPDU) => {
|
|
228
|
+
switch (event.type) {
|
|
229
|
+
/* ... background HCE event handler here ... */
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
3. Cleanup your event listener in `readerDeselected` -- this is obligatory because lack of proper cleanup will result in bugs and unexpected behavior.
|
|
235
|
+
```typescript
|
|
236
|
+
case 'readerDeselected':
|
|
237
|
+
// our application was deselected or the reader field was lost
|
|
238
|
+
// perform a cleanup of any resources here
|
|
239
|
+
break;
|
|
240
|
+
```
|
|
241
|
+
4. Implement the actual APDU handler that will process the C-APDU and generate a response.
|
|
242
|
+
```typescript
|
|
243
|
+
case 'received':
|
|
244
|
+
// decode incoming C-APDU to bytes
|
|
245
|
+
const capdu = Buffer.from(event.arg!, 'hex');
|
|
246
|
+
console.log('Received C-APDU, capdu.toString('hex'));
|
|
247
|
+
|
|
248
|
+
// for the demo purposes, we always want to respond with [0x0A] + status code 0x9000 (success)
|
|
249
|
+
await respondAPDU(Buffer.from([0x0A, 0x90, 0x00]).toString("hex"));
|
|
250
|
+
break;
|
|
251
|
+
```
|
|
252
|
+
|
|
198
253
|
### More resources
|
|
199
254
|
|
|
200
|
-
* [Module's API specification](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/
|
|
201
|
-
* [Demo App's Code (Example Library Usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-
|
|
255
|
+
* [Module's API specification](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/host-card-emulation/js/NativeHCEModule.ts)
|
|
256
|
+
* [Demo App's Code (Example Library Usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-ndef-app/App.tsx)
|
|
@@ -0,0 +1,257 @@
|
|
|
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
|
+
import java.util.UUID;
|
|
34
|
+
|
|
35
|
+
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
36
|
+
public class HCEService extends HostApduService implements ReactInstanceEventListener, HCEServiceCallback {
|
|
37
|
+
private static final String TAG = "HCEService";
|
|
38
|
+
|
|
39
|
+
private boolean isForeground;
|
|
40
|
+
private String backgroundSessionUUID;
|
|
41
|
+
private RTNHCEAndroidModule hceModule;
|
|
42
|
+
private byte[] pendingCAPDU;
|
|
43
|
+
private volatile boolean needsResponse;
|
|
44
|
+
|
|
45
|
+
private boolean isAppOnForeground(Context context) {
|
|
46
|
+
/*
|
|
47
|
+
We need to check if app is in foreground otherwise the app will crash.
|
|
48
|
+
https://stackoverflow.com/questions/8489993/check-android-application-is-in-foreground-or-not
|
|
49
|
+
*/
|
|
50
|
+
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
|
51
|
+
List<ActivityManager.RunningAppProcessInfo> appProcesses =
|
|
52
|
+
activityManager.getRunningAppProcesses();
|
|
53
|
+
if (appProcesses == null) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
final String packageName = context.getPackageName();
|
|
57
|
+
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
|
|
58
|
+
if (appProcess.importance ==
|
|
59
|
+
ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
|
|
60
|
+
appProcess.processName.equals(packageName)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Override
|
|
68
|
+
public void onBackgroundHCEInit(String handle) {
|
|
69
|
+
Log.d(TAG, "HCEService:onBackgroundHCEInit");
|
|
70
|
+
|
|
71
|
+
if (handle == null && backgroundSessionUUID != null) {
|
|
72
|
+
Log.d(TAG, "HCEService:onBackgroundHCEInit foreground call to background session");
|
|
73
|
+
throw new IllegalStateException();
|
|
74
|
+
} else if (handle != null && !handle.equals(backgroundSessionUUID)) {
|
|
75
|
+
Log.d(TAG, "HCEService:onBackgroundHCEInit mismatched background session handle");
|
|
76
|
+
throw new IllegalStateException();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (pendingCAPDU != null) {
|
|
80
|
+
Log.d(TAG, "HCEService:onBackgroundHCEInit send pendingCAPDU");
|
|
81
|
+
hceModule.sendBackgroundEvent(backgroundSessionUUID, "received", BinaryUtils.ByteArrayToHexString(pendingCAPDU));
|
|
82
|
+
pendingCAPDU = null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Override
|
|
87
|
+
public void onRespondAPDU(String handle, String rapdu) {
|
|
88
|
+
Log.d(TAG, "HCEService:onRespondAPDU");
|
|
89
|
+
|
|
90
|
+
if (handle == null && backgroundSessionUUID != null) {
|
|
91
|
+
Log.d(TAG, "HCEService:onRespondAPDU foreground call to background session");
|
|
92
|
+
throw new IllegalStateException();
|
|
93
|
+
} else if (handle != null && !handle.equals(backgroundSessionUUID)) {
|
|
94
|
+
Log.d(TAG, "HCEService:onRespondAPDU mismatched background session handle");
|
|
95
|
+
throw new IllegalStateException();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!needsResponse) {
|
|
99
|
+
Log.d(TAG, "HCEService:onRespondAPDU not waiting for a response");
|
|
100
|
+
throw new IllegalStateException();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
needsResponse = false;
|
|
104
|
+
sendResponseApdu(BinaryUtils.HexStringToByteArray(rapdu));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@Override
|
|
108
|
+
public byte[] processCommandApdu(byte[] command, Bundle extras) {
|
|
109
|
+
Log.d(TAG, "HCEService:processCommandApdu");
|
|
110
|
+
String capdu = BinaryUtils.ByteArrayToHexString(command).toUpperCase(Locale.ROOT);
|
|
111
|
+
|
|
112
|
+
if (isForeground) {
|
|
113
|
+
if (hceModule._isHCERunning() && !hceModule.isHCEBrokenConnection()) {
|
|
114
|
+
Log.d(TAG, "HCEService:processCommandApdu foreground sendEvent received");
|
|
115
|
+
needsResponse = true;
|
|
116
|
+
hceModule.sendEvent("received", capdu);
|
|
117
|
+
} else {
|
|
118
|
+
Log.d(TAG, "HCEService:processCommandApdu foreground respond 6999");
|
|
119
|
+
return new byte[] { (byte) 0x69, (byte) 0x99 };
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
if (hceModule != null && hceModule.isHCEBackgroundHandlerReady()) {
|
|
123
|
+
Log.d(TAG, "HCEService:processCommandApdu background sendBackgroundEvent received");
|
|
124
|
+
needsResponse = true;
|
|
125
|
+
hceModule.sendBackgroundEvent(backgroundSessionUUID, "received", capdu);
|
|
126
|
+
} else {
|
|
127
|
+
Log.d(TAG, "HCEService:processCommandApdu background pendingCAPDU");
|
|
128
|
+
needsResponse = true;
|
|
129
|
+
pendingCAPDU = command;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Override
|
|
137
|
+
public void onCreate() {
|
|
138
|
+
Log.d(TAG, "HCEService:onCreate");
|
|
139
|
+
|
|
140
|
+
ReactHost reactHost = ((ReactApplication) getApplication()).getReactHost();
|
|
141
|
+
|
|
142
|
+
if (reactHost == null) {
|
|
143
|
+
throw new RuntimeException("BUG! getReactHost() returned null.");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
isForeground = isAppOnForeground(getApplicationContext());
|
|
147
|
+
pendingCAPDU = null;
|
|
148
|
+
hceModule = null;
|
|
149
|
+
needsResponse = false;
|
|
150
|
+
|
|
151
|
+
if (isForeground) {
|
|
152
|
+
Log.d(TAG, "HCEService:onCreate foreground");
|
|
153
|
+
this.backgroundSessionUUID = null;
|
|
154
|
+
|
|
155
|
+
ReactContext reactContext = reactHost.getCurrentReactContext();
|
|
156
|
+
|
|
157
|
+
if (reactContext == null) {
|
|
158
|
+
throw new RuntimeException("BUG! getCurrentReactContext() returned null in foreground.");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.hceModule = ((RTNHCEAndroidModule) reactContext.getNativeModule("NativeHCEModule"));
|
|
162
|
+
|
|
163
|
+
if (this.hceModule == null) {
|
|
164
|
+
throw new RuntimeException("BUG! getNativeModule() returned null in foreground.");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.hceModule.setHCEService(this);
|
|
168
|
+
|
|
169
|
+
if (this.hceModule._isHCERunning()) {
|
|
170
|
+
this.hceModule.sendEvent("readerDetected", "");
|
|
171
|
+
} else {
|
|
172
|
+
this.hceModule.setHCEBrokenConnection();
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
ReactContext reactContext = reactHost.getCurrentReactContext();
|
|
176
|
+
Log.d(TAG, "HCEService:onCreate background");
|
|
177
|
+
this.backgroundSessionUUID = UUID.randomUUID().toString();
|
|
178
|
+
|
|
179
|
+
if (reactContext == null) {
|
|
180
|
+
reactHost.addReactInstanceEventListener(this);
|
|
181
|
+
reactHost.start();
|
|
182
|
+
} else {
|
|
183
|
+
setupRunJSTask(reactContext);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@Override
|
|
189
|
+
public void onDeactivated(int reason) {
|
|
190
|
+
Log.d(TAG, "HCEService:onDeactivated: " + reason);
|
|
191
|
+
needsResponse = false;
|
|
192
|
+
|
|
193
|
+
if (isForeground) {
|
|
194
|
+
if (this.hceModule != null && !this.hceModule.isHCEBrokenConnection()) {
|
|
195
|
+
this.hceModule.sendEvent("readerDeselected", "");
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
this.hceModule.sendBackgroundEvent(backgroundSessionUUID, "readerDeselected", "");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (this.hceModule != null) {
|
|
202
|
+
this.hceModule.setHCEService(null);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@Override
|
|
207
|
+
public void onReactContextInitialized(@NonNull ReactContext reactContext) {
|
|
208
|
+
HeadlessJsTaskContext headlessJsTaskContext = Companion.getInstance(reactContext);
|
|
209
|
+
headlessJsTaskContext.addTaskEventListener(new HeadlessJsTaskEventListener() {
|
|
210
|
+
@Override
|
|
211
|
+
public void onHeadlessJsTaskStart(int i) {
|
|
212
|
+
Log.d(TAG, "HCEService:HeadlessJsTaskEventListener:onHeadlessJsTaskStart: " + i);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@Override
|
|
216
|
+
public void onHeadlessJsTaskFinish(int i) {
|
|
217
|
+
Log.d(TAG, "HCEService:HeadlessJsTaskEventListener:onHeadlessJsTaskFinish: " + i);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
setupRunJSTask(reactContext);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
public void setupRunJSTask(@NonNull ReactContext reactContext) {
|
|
225
|
+
hceModule = (RTNHCEAndroidModule) reactContext.getNativeModule("NativeHCEModule");
|
|
226
|
+
|
|
227
|
+
if (hceModule == null) {
|
|
228
|
+
throw new RuntimeException("BUG! getNativeModule() returned null in background.");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
hceModule.setHCEService(this);
|
|
232
|
+
|
|
233
|
+
UiThreadUtil.runOnUiThread(new Runnable() {
|
|
234
|
+
@Override
|
|
235
|
+
public void run() {
|
|
236
|
+
Log.d(TAG, "HCEService:setupRunJSTask:runOnUiThread startTask");
|
|
237
|
+
Bundle argBundle = new Bundle();
|
|
238
|
+
argBundle.putString("handle", backgroundSessionUUID);
|
|
239
|
+
HeadlessJsTaskContext headlessJsTaskContext = Companion.getInstance(reactContext);
|
|
240
|
+
headlessJsTaskContext.startTask(
|
|
241
|
+
new HeadlessJsTaskConfig(
|
|
242
|
+
"handleBackgroundHCECall",
|
|
243
|
+
Arguments.fromBundle(argBundle),
|
|
244
|
+
15000,
|
|
245
|
+
false // not allowed in foreground
|
|
246
|
+
));
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@Override
|
|
252
|
+
public void onDestroy() {
|
|
253
|
+
super.onDestroy();
|
|
254
|
+
|
|
255
|
+
Log.d(TAG, "HCEService:onDestroy");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -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,82 @@ 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 audience, final String type, final String arg) {
|
|
61
|
+
Log.d(TAG, "RTNHCEAndroidModule:sendBackgroundEvent");
|
|
62
|
+
WritableMap map = Arguments.createMap();
|
|
63
|
+
map.putString("audience", audience);
|
|
64
|
+
map.putString("type", type);
|
|
65
|
+
map.putString("arg", arg);
|
|
66
|
+
emitOnBackgroundEvent(map);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
boolean _isHCERunning() {
|
|
70
|
+
return this.hceRunning;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
void setHCEBrokenConnection() {
|
|
74
|
+
this.hceBrokenConnection = true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
boolean isHCEBackgroundHandlerReady() {
|
|
78
|
+
return this.hceBackgroundReady;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
boolean isHCEBrokenConnection() {
|
|
82
|
+
return this.hceBrokenConnection;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
void setHCEService(HCEServiceCallback serviceCallback) {
|
|
86
|
+
Log.d(TAG, "RTNHCEAndroidModule:setHCEService");
|
|
87
|
+
|
|
88
|
+
this.serviceCb = serviceCallback;
|
|
89
|
+
this.hceDeselected = true;
|
|
90
|
+
this.hceBackgroundReady = false;
|
|
91
|
+
this.hceBrokenConnection = false;
|
|
92
|
+
}
|
|
93
|
+
|
|
126
94
|
@Override
|
|
127
95
|
public boolean isPlatformSupported() {
|
|
96
|
+
Log.d(TAG, "RTNHCEAndroidModule:isPlatformSupported");
|
|
128
97
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
129
98
|
return false;
|
|
130
99
|
}
|
|
@@ -136,17 +105,21 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
136
105
|
|
|
137
106
|
@Override
|
|
138
107
|
public void acquireExclusiveNFC(Promise promise) {
|
|
108
|
+
Log.d(TAG, "RTNHCEAndroidModule:acquireExclusiveNFC");
|
|
139
109
|
promise.reject("err_platform_unsupported", "acquireExclusiveNFC() is not supported on Android.");
|
|
140
110
|
}
|
|
141
111
|
|
|
142
112
|
@Override
|
|
143
113
|
public boolean isExclusiveNFC() {
|
|
114
|
+
Log.d(TAG, "RTNHCEAndroidModule:isExclusiveNFC");
|
|
144
115
|
// unsupported on Android, always false
|
|
145
116
|
return false;
|
|
146
117
|
}
|
|
147
118
|
|
|
148
119
|
@Override
|
|
149
120
|
public void beginSession(Promise promise) {
|
|
121
|
+
Log.d(TAG, "RTNHCEAndroidModule:beginSession");
|
|
122
|
+
|
|
150
123
|
if (!isPlatformSupported()) {
|
|
151
124
|
promise.reject("err_platform_unsupported", "Unsupported Android version or missing NFC/HCE feature.");
|
|
152
125
|
return;
|
|
@@ -163,24 +136,44 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
163
136
|
|
|
164
137
|
@Override
|
|
165
138
|
public void setSessionAlertMessage(String message) {
|
|
139
|
+
Log.d(TAG, "RTNHCEAndroidModule:setSessionAlertMessage");
|
|
166
140
|
// unsupported on Android, no-op
|
|
167
141
|
}
|
|
168
142
|
|
|
169
143
|
@Override
|
|
170
144
|
public void invalidateSession() {
|
|
145
|
+
Log.d(TAG, "RTNHCEAndroidModule:invalidateSession");
|
|
171
146
|
if (this.sessionRunning) {
|
|
147
|
+
this.hceRunning = false;
|
|
172
148
|
this.sessionRunning = false;
|
|
149
|
+
this.hceBrokenConnection = true;
|
|
173
150
|
sendEvent("sessionInvalidated", "userInvalidated");
|
|
174
151
|
}
|
|
175
152
|
}
|
|
176
153
|
|
|
177
154
|
@Override
|
|
178
155
|
public boolean isSessionRunning() {
|
|
156
|
+
Log.d(TAG, "RTNHCEAndroidModule:isSessionRunning");
|
|
179
157
|
return this.sessionRunning;
|
|
180
158
|
}
|
|
181
159
|
|
|
160
|
+
@Override
|
|
161
|
+
public boolean beginBackgroundHCE(String handle) {
|
|
162
|
+
Log.d(TAG, "RTNHCEAndroidModule:beginBackgroundHCE");
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
this.serviceCb.onBackgroundHCEInit(handle);
|
|
166
|
+
} catch (IllegalStateException e) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.hceBackgroundReady = true;
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
182
174
|
@Override
|
|
183
175
|
public void startHCE(Promise promise) {
|
|
176
|
+
Log.d(TAG, "RTNHCEAndroidModule:startHCE");
|
|
184
177
|
if (!this.sessionRunning) {
|
|
185
178
|
promise.reject("err_no_session", "No session is active.");
|
|
186
179
|
return;
|
|
@@ -192,11 +185,13 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
192
185
|
}
|
|
193
186
|
|
|
194
187
|
this.hceRunning = true;
|
|
188
|
+
this.hceBrokenConnection = false;
|
|
195
189
|
promise.resolve(null);
|
|
196
190
|
}
|
|
197
191
|
|
|
198
192
|
@Override
|
|
199
193
|
public void stopHCE(String status, Promise promise) {
|
|
194
|
+
Log.d(TAG, "RTNHCEAndroidModule:stopHCE");
|
|
200
195
|
if (!this.sessionRunning) {
|
|
201
196
|
promise.reject("err_no_session", "No session is active.");
|
|
202
197
|
return;
|
|
@@ -207,7 +202,7 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
207
202
|
|
|
208
203
|
if (!this.hceDeselected) {
|
|
209
204
|
this.hceDeselected = true;
|
|
210
|
-
this.
|
|
205
|
+
this.hceBrokenConnection = true;
|
|
211
206
|
sendEvent("readerDeselected", "");
|
|
212
207
|
}
|
|
213
208
|
}
|
|
@@ -217,25 +212,34 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
217
212
|
|
|
218
213
|
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
219
214
|
@Override
|
|
220
|
-
public void respondAPDU(String rapdu, Promise promise) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
215
|
+
public void respondAPDU(String handle, String rapdu, Promise promise) {
|
|
216
|
+
Log.d(TAG, "RTNHCEAndroidModule:respondAPDU");
|
|
217
|
+
|
|
218
|
+
if (!this.hceBackgroundReady) {
|
|
219
|
+
if (!this.sessionRunning) {
|
|
220
|
+
promise.reject("err_no_session", "No session is active.");
|
|
221
|
+
return;
|
|
222
|
+
} else if (!this.hceRunning) {
|
|
223
|
+
promise.reject("err_no_hce", "HCE is not running.");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
227
226
|
}
|
|
228
227
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
228
|
+
Log.d(TAG, "respondAPDU(): Send to service");
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
serviceCb.onRespondAPDU(handle, rapdu);
|
|
232
|
+
} catch (IllegalStateException e) {
|
|
233
|
+
promise.reject("err_illegal_state", "There is no C-APDU to respond to at the time or the provided handle is invalid.");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
233
236
|
|
|
234
237
|
promise.resolve(null);
|
|
235
238
|
}
|
|
236
239
|
|
|
237
240
|
@Override
|
|
238
241
|
public void isHCERunning(Promise promise) {
|
|
242
|
+
Log.d(TAG, "RTNHCEAndroidModule:isHCERunning");
|
|
239
243
|
promise.resolve(this.hceRunning);
|
|
240
244
|
}
|
|
241
245
|
}
|
package/ios/RTNHCEModuleIOS.mm
CHANGED
|
@@ -86,6 +86,10 @@
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
- (nonnull NSNumber *)beginBackgroundHCE:(nonnull NSString *)handle {
|
|
90
|
+
return @0;
|
|
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];
|
|
@@ -102,9 +106,9 @@
|
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
- (void)respondAPDU:(nonnull NSString *)rapdu resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
|
|
109
|
+
- (void)respondAPDU:(NSString * _Nullable)handle rapdu:(nonnull NSString *)rapdu resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
|
|
106
110
|
if (@available(iOS 17.4, *)) {
|
|
107
|
-
return [hcemoduleios
|
|
111
|
+
return [hcemoduleios respondAPDUWithHandle:handle rapdu:rapdu resolve:resolve reject:reject];
|
|
108
112
|
} else {
|
|
109
113
|
reject(@"err_platform_unsupported", @"This iOS version does not support HCE.", nil);
|
|
110
114
|
}
|
|
@@ -145,8 +145,13 @@ extension Data {
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
@objc public func respondAPDU(rapdu: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
148
|
+
@objc public func respondAPDU(handle: NSString?, rapdu: NSString, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
149
149
|
Task.init {
|
|
150
|
+
guard handle == nil else {
|
|
151
|
+
reject("err_illegal_state", "The provided handle is invalid.", nil)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
150
155
|
guard let capdu = self.receivedCardAPDU else {
|
|
151
156
|
reject("err_no_apdu_received", "There is no received APDU to respond to.", nil)
|
|
152
157
|
return
|
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,21 @@ 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
|
+
audience: string,
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
export interface Spec extends TurboModule {
|
|
28
39
|
/**
|
|
29
40
|
* Checks if a given smartphone supports required HCE features.
|
|
@@ -63,6 +74,8 @@ export interface Spec extends TurboModule {
|
|
|
63
74
|
*/
|
|
64
75
|
isSessionRunning(): boolean;
|
|
65
76
|
|
|
77
|
+
beginBackgroundHCE(handle: string): boolean;
|
|
78
|
+
|
|
66
79
|
/**
|
|
67
80
|
* iOS: Start the card emulation.
|
|
68
81
|
* Android: Enable HCE service.
|
|
@@ -76,7 +89,7 @@ export interface Spec extends TurboModule {
|
|
|
76
89
|
/**
|
|
77
90
|
* Send R-APDU (hex-encoded string) as a response to the last received C-APDU event.
|
|
78
91
|
*/
|
|
79
|
-
respondAPDU(rapdu: string): Promise<void>;
|
|
92
|
+
respondAPDU(handle: string | null, rapdu: string): Promise<void>;
|
|
80
93
|
/**
|
|
81
94
|
* iOS: Checks if the card emulation is running.
|
|
82
95
|
* Android: Checks if the HCE service is enabled.
|
|
@@ -84,6 +97,7 @@ export interface Spec extends TurboModule {
|
|
|
84
97
|
isHCERunning(): Promise<boolean>;
|
|
85
98
|
|
|
86
99
|
readonly onEvent: CodegenTypes.EventEmitter<HCEModuleEvent>;
|
|
100
|
+
readonly onBackgroundEvent: CodegenTypes.EventEmitter<HCEModuleBackgroundEvent>;
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
export default TurboModuleRegistry.getEnforcing<Spec>(
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import NativeHCEModule, {HCEModuleBackgroundEvent} from "./NativeHCEModule";
|
|
2
|
+
|
|
3
|
+
export interface BackgroundEventHandler {
|
|
4
|
+
(
|
|
5
|
+
event: HCEModuleBackgroundEvent,
|
|
6
|
+
respondAPDU: (rapdu: string) => Promise<void>,
|
|
7
|
+
): Promise<void>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ProcessBackgroundHCEFunc {
|
|
11
|
+
(
|
|
12
|
+
handler: BackgroundEventHandler
|
|
13
|
+
): void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const createBackgroundHCE = (handle: string) => {
|
|
17
|
+
const respondAPDU = (rapdu: string) => {
|
|
18
|
+
return NativeHCEModule.respondAPDU(handle, rapdu);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (handler: BackgroundEventHandler) => {
|
|
22
|
+
const subscription = NativeHCEModule.onBackgroundEvent(async (event) => {
|
|
23
|
+
if (event.audience !== handle) {
|
|
24
|
+
// ignore
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await handler(event, respondAPDU);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
throw e;
|
|
32
|
+
} finally {
|
|
33
|
+
if (event.type === "readerDeselected") {
|
|
34
|
+
subscription.remove();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!NativeHCEModule.beginBackgroundHCE(handle)) {
|
|
40
|
+
subscription.remove();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
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
|
-
}
|