@icedevml/react-native-host-card-emulation 0.6.0 → 0.6.1
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 +2 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEService.java +81 -47
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEServiceCallback.java +1 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidModule.java +34 -11
- package/js/NativeHCEModule.ts +1 -0
- package/js/hceBackground.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -233,6 +233,7 @@ for instance - your app may emulate an NDEF tag even when it's not launched on t
|
|
|
233
233
|
```
|
|
234
234
|
3. Cleanup your event listener in `readerDeselected` -- this is obligatory because lack of proper cleanup will result in bugs and unexpected behavior.
|
|
235
235
|
```typescript
|
|
236
|
+
// inside processBackgroundHCE handler's switch
|
|
236
237
|
case 'readerDeselected':
|
|
237
238
|
// our application was deselected or the reader field was lost
|
|
238
239
|
// perform a cleanup of any resources here
|
|
@@ -240,6 +241,7 @@ for instance - your app may emulate an NDEF tag even when it's not launched on t
|
|
|
240
241
|
```
|
|
241
242
|
4. Implement the actual APDU handler that will process the C-APDU and generate a response.
|
|
242
243
|
```typescript
|
|
244
|
+
// inside processBackgroundHCE handler's switch
|
|
243
245
|
case 'received':
|
|
244
246
|
// decode incoming C-APDU to bytes
|
|
245
247
|
const capdu = Buffer.from(event.arg!, 'hex');
|
|
@@ -20,7 +20,6 @@ import androidx.annotation.RequiresApi;
|
|
|
20
20
|
|
|
21
21
|
import com.facebook.react.ReactApplication;
|
|
22
22
|
import com.facebook.react.ReactHost;
|
|
23
|
-
import com.facebook.react.ReactInstanceEventListener;
|
|
24
23
|
import com.facebook.react.bridge.Arguments;
|
|
25
24
|
import com.facebook.react.bridge.ReactContext;
|
|
26
25
|
import com.facebook.react.bridge.UiThreadUtil;
|
|
@@ -28,12 +27,13 @@ import com.facebook.react.jstasks.HeadlessJsTaskConfig;
|
|
|
28
27
|
import com.facebook.react.jstasks.HeadlessJsTaskContext;
|
|
29
28
|
import com.facebook.react.jstasks.HeadlessJsTaskEventListener;
|
|
30
29
|
|
|
30
|
+
import java.util.HashMap;
|
|
31
31
|
import java.util.List;
|
|
32
32
|
import java.util.Locale;
|
|
33
33
|
import java.util.UUID;
|
|
34
34
|
|
|
35
35
|
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
36
|
-
public class HCEService extends HostApduService implements
|
|
36
|
+
public class HCEService extends HostApduService implements HCEServiceCallback {
|
|
37
37
|
private static final String TAG = "HCEService";
|
|
38
38
|
|
|
39
39
|
private boolean isForeground;
|
|
@@ -41,6 +41,13 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
|
|
|
41
41
|
private RTNHCEAndroidModule hceModule;
|
|
42
42
|
private byte[] pendingCAPDU;
|
|
43
43
|
private volatile boolean needsResponse;
|
|
44
|
+
private final HashMap<String, Integer> taskSessionIdMap;
|
|
45
|
+
|
|
46
|
+
public HCEService() {
|
|
47
|
+
super();
|
|
48
|
+
|
|
49
|
+
taskSessionIdMap = new HashMap<>();
|
|
50
|
+
}
|
|
44
51
|
|
|
45
52
|
private boolean isAppOnForeground(Context context) {
|
|
46
53
|
/*
|
|
@@ -67,6 +74,7 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
|
|
|
67
74
|
@Override
|
|
68
75
|
public void onBackgroundHCEInit(String handle) {
|
|
69
76
|
Log.d(TAG, "HCEService:onBackgroundHCEInit");
|
|
77
|
+
Log.d(TAG, "HCEService:onBackgroundHCEInit:" + handle + ":" + backgroundSessionUUID);
|
|
70
78
|
|
|
71
79
|
if (handle == null && backgroundSessionUUID != null) {
|
|
72
80
|
Log.d(TAG, "HCEService:onBackgroundHCEInit foreground call to background session");
|
|
@@ -83,6 +91,35 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
|
|
|
83
91
|
}
|
|
84
92
|
}
|
|
85
93
|
|
|
94
|
+
@Override
|
|
95
|
+
public void onBackgroundHCEFinish(String handle) {
|
|
96
|
+
Log.d(TAG, "HCEService:onBackgroundHCEFinish");
|
|
97
|
+
Integer taskId = taskSessionIdMap.get(handle);
|
|
98
|
+
|
|
99
|
+
if (taskId == null) {
|
|
100
|
+
Log.d(TAG, "HCEService:onBackgroundHCEFinish unable to resolve taskId");
|
|
101
|
+
throw new IllegalArgumentException();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
ReactHost reactHost = ((ReactApplication) getApplication()).getReactHost();
|
|
105
|
+
|
|
106
|
+
if (reactHost == null) {
|
|
107
|
+
Log.d(TAG, "HCEService:onBackgroundHCEFinish getReactHost() returned null");
|
|
108
|
+
throw new IllegalArgumentException();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
ReactContext reactContext = reactHost.getCurrentReactContext();
|
|
112
|
+
|
|
113
|
+
if (reactContext == null) {
|
|
114
|
+
Log.d(TAG, "HCEService:onBackgroundHCEFinish getCurrentReactContext() returned null");
|
|
115
|
+
throw new IllegalArgumentException();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
HeadlessJsTaskContext headlessJsTaskContext = Companion.getInstance(reactContext);
|
|
119
|
+
headlessJsTaskContext.finishTask(taskId);
|
|
120
|
+
taskSessionIdMap.remove(handle);
|
|
121
|
+
}
|
|
122
|
+
|
|
86
123
|
@Override
|
|
87
124
|
public void onRespondAPDU(String handle, String rapdu) {
|
|
88
125
|
Log.d(TAG, "HCEService:onRespondAPDU");
|
|
@@ -110,13 +147,13 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
|
|
|
110
147
|
String capdu = BinaryUtils.ByteArrayToHexString(command).toUpperCase(Locale.ROOT);
|
|
111
148
|
|
|
112
149
|
if (isForeground) {
|
|
113
|
-
if (hceModule._isHCERunning() &&
|
|
150
|
+
if (hceModule._isHCERunning() && hceModule.isHCEActiveConnection()) {
|
|
114
151
|
Log.d(TAG, "HCEService:processCommandApdu foreground sendEvent received");
|
|
115
152
|
needsResponse = true;
|
|
116
153
|
hceModule.sendEvent("received", capdu);
|
|
117
154
|
} else {
|
|
118
155
|
Log.d(TAG, "HCEService:processCommandApdu foreground respond 6999");
|
|
119
|
-
return new byte[]
|
|
156
|
+
return new byte[]{(byte) 0x69, (byte) 0x99};
|
|
120
157
|
}
|
|
121
158
|
} else {
|
|
122
159
|
if (hceModule != null && hceModule.isHCEBackgroundHandlerReady()) {
|
|
@@ -174,13 +211,29 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
|
|
|
174
211
|
} else {
|
|
175
212
|
ReactContext reactContext = reactHost.getCurrentReactContext();
|
|
176
213
|
Log.d(TAG, "HCEService:onCreate background");
|
|
177
|
-
|
|
214
|
+
final String useSessID = UUID.randomUUID().toString();
|
|
215
|
+
backgroundSessionUUID = useSessID;
|
|
178
216
|
|
|
179
217
|
if (reactContext == null) {
|
|
180
|
-
reactHost.addReactInstanceEventListener(
|
|
218
|
+
reactHost.addReactInstanceEventListener(evReactContext -> {
|
|
219
|
+
HeadlessJsTaskContext headlessJsTaskContext = Companion.getInstance(evReactContext);
|
|
220
|
+
headlessJsTaskContext.addTaskEventListener(new HeadlessJsTaskEventListener() {
|
|
221
|
+
@Override
|
|
222
|
+
public void onHeadlessJsTaskStart(int i) {
|
|
223
|
+
Log.d(TAG, "HCEService:HeadlessJsTaskEventListener:onHeadlessJsTaskStart: " + i);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@Override
|
|
227
|
+
public void onHeadlessJsTaskFinish(int i) {
|
|
228
|
+
Log.d(TAG, "HCEService:HeadlessJsTaskEventListener:onHeadlessJsTaskFinish: " + i);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
setupRunJSTask(evReactContext, useSessID);
|
|
233
|
+
});
|
|
181
234
|
reactHost.start();
|
|
182
235
|
} else {
|
|
183
|
-
setupRunJSTask(reactContext);
|
|
236
|
+
setupRunJSTask(reactContext, useSessID);
|
|
184
237
|
}
|
|
185
238
|
}
|
|
186
239
|
}
|
|
@@ -191,37 +244,20 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
|
|
|
191
244
|
needsResponse = false;
|
|
192
245
|
|
|
193
246
|
if (isForeground) {
|
|
194
|
-
if (this.hceModule != null &&
|
|
247
|
+
if (this.hceModule != null && this.hceModule.isHCEActiveConnection()) {
|
|
195
248
|
this.hceModule.sendEvent("readerDeselected", "");
|
|
196
249
|
}
|
|
197
|
-
} else {
|
|
198
|
-
|
|
199
|
-
|
|
250
|
+
} else if (backgroundSessionUUID != null) {
|
|
251
|
+
String prevBackgroundSessionUUID = backgroundSessionUUID;
|
|
252
|
+
backgroundSessionUUID = null;
|
|
200
253
|
|
|
201
|
-
|
|
202
|
-
|
|
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);
|
|
254
|
+
if (this.hceModule != null) {
|
|
255
|
+
this.hceModule.sendBackgroundEvent(prevBackgroundSessionUUID, "readerDeselected", "");
|
|
218
256
|
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
setupRunJSTask(reactContext);
|
|
257
|
+
}
|
|
222
258
|
}
|
|
223
259
|
|
|
224
|
-
public void setupRunJSTask(@NonNull ReactContext reactContext) {
|
|
260
|
+
public void setupRunJSTask(final @NonNull ReactContext reactContext, final String useSessUUID) {
|
|
225
261
|
hceModule = (RTNHCEAndroidModule) reactContext.getNativeModule("NativeHCEModule");
|
|
226
262
|
|
|
227
263
|
if (hceModule == null) {
|
|
@@ -230,21 +266,19 @@ public class HCEService extends HostApduService implements ReactInstanceEventLis
|
|
|
230
266
|
|
|
231
267
|
hceModule.setHCEService(this);
|
|
232
268
|
|
|
233
|
-
UiThreadUtil.runOnUiThread(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
));
|
|
247
|
-
}
|
|
269
|
+
UiThreadUtil.runOnUiThread(() -> {
|
|
270
|
+
Log.d(TAG, "HCEService:setupRunJSTask:runOnUiThread startTask");
|
|
271
|
+
Bundle argBundle = new Bundle();
|
|
272
|
+
argBundle.putString("handle", useSessUUID);
|
|
273
|
+
HeadlessJsTaskContext headlessJsTaskContext = Companion.getInstance(reactContext);
|
|
274
|
+
int taskId = headlessJsTaskContext.startTask(
|
|
275
|
+
new HeadlessJsTaskConfig(
|
|
276
|
+
"handleBackgroundHCECall",
|
|
277
|
+
Arguments.fromBundle(argBundle),
|
|
278
|
+
15000,
|
|
279
|
+
false // not allowed in foreground
|
|
280
|
+
));
|
|
281
|
+
taskSessionIdMap.put(useSessUUID, taskId);
|
|
248
282
|
});
|
|
249
283
|
}
|
|
250
284
|
|
|
@@ -51,19 +51,29 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
51
51
|
|
|
52
52
|
void sendEvent(final String type, final String arg) {
|
|
53
53
|
Log.d(TAG, "RTNHCEAndroidModule:sendEvent");
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
|
|
55
|
+
if (this.mEventEmitterCallback != null) {
|
|
56
|
+
WritableMap map = Arguments.createMap();
|
|
57
|
+
map.putString("type", type);
|
|
58
|
+
map.putString("arg", arg);
|
|
59
|
+
emitOnEvent(map);
|
|
60
|
+
} else {
|
|
61
|
+
Log.d(TAG, "RTNHCEAndroidModule:sendEvent missing event emitter, ignore");
|
|
62
|
+
}
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
void sendBackgroundEvent(final String audience, final String type, final String arg) {
|
|
61
66
|
Log.d(TAG, "RTNHCEAndroidModule:sendBackgroundEvent");
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
|
|
68
|
+
if (this.mEventEmitterCallback != null) {
|
|
69
|
+
WritableMap map = Arguments.createMap();
|
|
70
|
+
map.putString("audience", audience);
|
|
71
|
+
map.putString("type", type);
|
|
72
|
+
map.putString("arg", arg);
|
|
73
|
+
emitOnBackgroundEvent(map);
|
|
74
|
+
} else {
|
|
75
|
+
Log.d(TAG, "RTNHCEAndroidModule:sendBackgroundEvent missing event emitter, ignore");
|
|
76
|
+
}
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
boolean _isHCERunning() {
|
|
@@ -78,8 +88,8 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
78
88
|
return this.hceBackgroundReady;
|
|
79
89
|
}
|
|
80
90
|
|
|
81
|
-
boolean
|
|
82
|
-
return this.hceBrokenConnection;
|
|
91
|
+
boolean isHCEActiveConnection() {
|
|
92
|
+
return !this.hceBrokenConnection;
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
void setHCEService(HCEServiceCallback serviceCallback) {
|
|
@@ -171,6 +181,19 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
171
181
|
return true;
|
|
172
182
|
}
|
|
173
183
|
|
|
184
|
+
@Override
|
|
185
|
+
public boolean finishBackgroundHCE(String handle) {
|
|
186
|
+
Log.d(TAG, "RTNHCEAndroidModule:finishBackgroundHCE");
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
this.serviceCb.onBackgroundHCEFinish(handle);
|
|
190
|
+
} catch (IllegalStateException e) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
174
197
|
@Override
|
|
175
198
|
public void startHCE(Promise promise) {
|
|
176
199
|
Log.d(TAG, "RTNHCEAndroidModule:startHCE");
|
package/js/NativeHCEModule.ts
CHANGED
package/js/hceBackground.ts
CHANGED
|
@@ -32,12 +32,14 @@ export const createBackgroundHCE = (handle: string) => {
|
|
|
32
32
|
} finally {
|
|
33
33
|
if (event.type === "readerDeselected") {
|
|
34
34
|
subscription.remove();
|
|
35
|
+
NativeHCEModule.finishBackgroundHCE(handle);
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
});
|
|
38
39
|
|
|
39
40
|
if (!NativeHCEModule.beginBackgroundHCE(handle)) {
|
|
40
41
|
subscription.remove();
|
|
42
|
+
NativeHCEModule.finishBackgroundHCE(handle);
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
}
|
package/package.json
CHANGED