@icedevml/react-native-host-card-emulation 0.9.2 → 1.0.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.
@@ -1,274 +1,340 @@
1
1
  package com.itsecrnd.rtnhceandroid;
2
2
 
3
+ import android.app.Activity;
4
+ import android.content.ComponentName;
3
5
  import android.content.pm.PackageManager;
4
- import android.os.Build;
6
+ import android.nfc.NfcAdapter;
7
+ import android.nfc.cardemulation.CardEmulation;
5
8
  import android.util.Log;
6
9
 
7
10
  import androidx.annotation.NonNull;
8
- import androidx.annotation.RequiresApi;
11
+ import androidx.annotation.Nullable;
9
12
 
10
13
  import com.facebook.react.bridge.Arguments;
14
+ import com.facebook.react.bridge.LifecycleEventListener;
11
15
  import com.facebook.react.bridge.Promise;
12
16
  import com.facebook.react.bridge.ReactApplicationContext;
17
+ import com.facebook.react.bridge.UiThreadUtil;
13
18
  import com.facebook.react.bridge.WritableMap;
19
+ import com.facebook.react.common.LifecycleState;
20
+ import com.facebook.react.jstasks.HeadlessJsTaskContext;
21
+ import com.facebook.react.jstasks.HeadlessJsTaskEventListener;
14
22
 
15
23
  public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
16
24
  public static final String TAG = "RTNHCEAndroidModule";
17
25
  public static final String NAME = "NativeHCEModule";
18
26
 
19
- private volatile boolean sessionRunning;
20
- private volatile boolean hceRunning;
21
- private volatile boolean hceDeselected;
22
- private volatile boolean hceBrokenConnection;
27
+ static final class BackgroundHandlerDeclaration {
28
+ final boolean registered;
29
+ final boolean allowWhenLocked;
23
30
 
24
- private volatile boolean hceBackgroundReady;
31
+ BackgroundHandlerDeclaration(boolean registered, boolean allowWhenLocked) {
32
+ this.registered = registered;
33
+ this.allowWhenLocked = allowWhenLocked;
34
+ }
35
+ }
25
36
 
26
- private HCEServiceCallback serviceCb;
37
+ // null until the JS side declares whether it has a background handler
38
+ private volatile BackgroundHandlerDeclaration backgroundHandlerDeclaration;
27
39
 
28
- RTNHCEAndroidModule(ReactApplicationContext context) {
29
- super(context);
40
+ // the activity for which HCEService is the preferred service, main thread only
41
+ @Nullable
42
+ private Activity preferredServiceActivity;
43
+
44
+ private final HeadlessJsTaskEventListener headlessJsTaskEventListener = new HeadlessJsTaskEventListener() {
45
+ @Override
46
+ public void onHeadlessJsTaskStart(int taskId) {
47
+ // nothing to do
48
+ }
49
+
50
+ @Override
51
+ public void onHeadlessJsTaskFinish(int taskId) {
52
+ HCEManager.getInstance().onHeadlessTaskFinished(RTNHCEAndroidModule.this, taskId);
53
+ }
54
+ };
55
+
56
+ /*
57
+ Android only allows setting the preferred service for a resumed activity
58
+ and the preference has to be removed once the activity is paused.
59
+ */
60
+ private final LifecycleEventListener lifecycleEventListener = new LifecycleEventListener() {
61
+ @Override
62
+ public void onHostResume() {
63
+ updatePreferredService(true);
64
+ }
30
65
 
31
- this.serviceCb = null;
66
+ @Override
67
+ public void onHostPause() {
68
+ updatePreferredService(false);
69
+ }
32
70
 
33
- this.sessionRunning = false;
34
- this.hceRunning = false;
35
- this.hceDeselected = true;
36
- this.hceBrokenConnection = false;
37
- this.hceBackgroundReady = false;
71
+ @Override
72
+ public void onHostDestroy() {
73
+ updatePreferredService(false);
74
+ }
75
+ };
76
+
77
+ RTNHCEAndroidModule(ReactApplicationContext context) {
78
+ super(context);
79
+ HeadlessJsTaskContext.getInstance(context).addTaskEventListener(headlessJsTaskEventListener);
80
+ context.addLifecycleEventListener(lifecycleEventListener);
38
81
  }
39
82
 
40
83
  @Override
41
84
  public void invalidate() {
42
- Log.d(TAG, "RTNHCEAndroidModule:invalidate");
85
+ Log.d(TAG, "invalidate");
86
+ ReactApplicationContext context = getReactApplicationContext();
87
+ HeadlessJsTaskContext.getInstance(context).removeTaskEventListener(headlessJsTaskEventListener);
88
+ context.removeLifecycleEventListener(lifecycleEventListener);
89
+ HCEManager.getInstance().onModuleInvalidated(this);
90
+ UiThreadUtil.runOnUiThread(() -> updatePreferredService(false));
91
+ super.invalidate();
43
92
  }
44
93
 
45
94
  @Override
46
95
  @NonNull
47
96
  public String getName() {
48
- Log.d(TAG, "RTNHCEAndroidModule:getName");
49
- return RTNHCEAndroidModule.NAME;
97
+ return NAME;
50
98
  }
51
99
 
52
- void sendEvent(final String type, final String arg) {
53
- Log.d(TAG, "RTNHCEAndroidModule:sendEvent");
100
+ ReactApplicationContext getContext() {
101
+ return getReactApplicationContext();
102
+ }
54
103
 
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
- }
104
+ @Nullable
105
+ BackgroundHandlerDeclaration getBackgroundHandlerDeclaration() {
106
+ return backgroundHandlerDeclaration;
63
107
  }
64
108
 
65
- void sendBackgroundEvent(final String audience, final String type, final String arg) {
66
- Log.d(TAG, "RTNHCEAndroidModule:sendBackgroundEvent");
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");
109
+ @Nullable
110
+ private CardEmulation getCardEmulation() {
111
+ try {
112
+ NfcAdapter adapter = NfcAdapter.getDefaultAdapter(getReactApplicationContext());
113
+ return adapter != null ? CardEmulation.getInstance(adapter) : null;
114
+ } catch (RuntimeException e) {
115
+ // UnsupportedOperationException if the device doesn't support HCE
116
+ Log.w(TAG, "getCardEmulation: CardEmulation is not available", e);
117
+ return null;
76
118
  }
77
119
  }
78
120
 
79
- boolean _isHCERunning() {
80
- return this.hceRunning;
81
- }
121
+ /**
122
+ * Make HCEService the preferred service while the app's activity is resumed and the foreground session
123
+ * has running emulation, so that it wins the routing of AIDs that other apps also have registered.
124
+ */
125
+ // main thread
126
+ private void updatePreferredService(boolean hostResumed) {
127
+ Activity activity = hostResumed ? getReactApplicationContext().getCurrentActivity() : null;
128
+ boolean wanted = activity != null && HCEManager.getInstance().hasRunningEmulation(this);
82
129
 
83
- void setHCEBrokenConnection() {
84
- this.hceBrokenConnection = true;
85
- }
130
+ if (wanted && activity == preferredServiceActivity) {
131
+ return;
132
+ }
86
133
 
87
- boolean isHCEBackgroundHandlerReady() {
88
- return this.hceBackgroundReady;
89
- }
134
+ CardEmulation cardEmulation = null;
135
+
136
+ if (preferredServiceActivity != null) {
137
+ cardEmulation = getCardEmulation();
138
+
139
+ try {
140
+ if (cardEmulation != null) {
141
+ cardEmulation.unsetPreferredService(preferredServiceActivity);
142
+ }
143
+ } catch (RuntimeException e) {
144
+ // IllegalArgumentException if the activity isn't resumed anymore, the preference is gone anyway
145
+ Log.w(TAG, "updatePreferredService: unsetPreferredService() failed", e);
146
+ }
147
+
148
+ preferredServiceActivity = null;
149
+ }
90
150
 
91
- boolean isHCEActiveConnection() {
92
- return !this.hceBrokenConnection;
151
+ if (!wanted) {
152
+ return;
153
+ }
154
+
155
+ if (cardEmulation == null) {
156
+ cardEmulation = getCardEmulation();
157
+ }
158
+
159
+ if (cardEmulation == null) {
160
+ return;
161
+ }
162
+
163
+ try {
164
+ ComponentName service = new ComponentName(getReactApplicationContext(), HCEService.class);
165
+
166
+ if (cardEmulation.setPreferredService(activity, service)) {
167
+ preferredServiceActivity = activity;
168
+ } else {
169
+ Log.w(TAG, "updatePreferredService: setPreferredService() refused, is HCEService declared in the manifest?");
170
+ }
171
+ } catch (RuntimeException e) {
172
+ Log.w(TAG, "updatePreferredService: setPreferredService() failed", e);
173
+ }
93
174
  }
94
175
 
95
- void setHCEService(HCEServiceCallback serviceCallback) {
96
- Log.d(TAG, "RTNHCEAndroidModule:setHCEService");
176
+ private void refreshPreferredService() {
177
+ UiThreadUtil.runOnUiThread(() -> updatePreferredService(
178
+ getReactApplicationContext().getLifecycleState() == LifecycleState.RESUMED));
179
+ }
97
180
 
98
- this.serviceCb = serviceCallback;
99
- this.hceDeselected = true;
100
- this.hceBackgroundReady = false;
101
- this.hceBrokenConnection = false;
181
+ void emitEvent(String sessionId, String type) {
182
+ emitEvent(sessionId, type, 0, "", false, "");
102
183
  }
103
184
 
104
- @Override
105
- public boolean isPlatformSupported() {
106
- Log.d(TAG, "RTNHCEAndroidModule:isPlatformSupported");
107
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
108
- return false;
185
+ void emitEvent(String sessionId, String type, long apduId, String command, boolean deviceLocked, String reason) {
186
+ if (this.mEventEmitterCallback == null) {
187
+ Log.w(TAG, "emitEvent: missing event emitter, dropping " + type);
188
+ return;
109
189
  }
110
190
 
191
+ WritableMap map = Arguments.createMap();
192
+ map.putString("sessionId", sessionId);
193
+ map.putString("type", type);
194
+ map.putDouble("apduId", (double) apduId);
195
+ map.putString("command", command);
196
+ map.putBoolean("deviceLocked", deviceLocked);
197
+ map.putString("reason", reason);
198
+ emitOnEvent(map);
199
+ }
200
+
201
+ private boolean isHCESupported() {
111
202
  PackageManager pm = getReactApplicationContext().getPackageManager();
112
203
  return pm.hasSystemFeature(PackageManager.FEATURE_NFC)
113
204
  && pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION);
114
205
  }
115
206
 
116
207
  @Override
117
- public void acquireExclusiveNFC(Promise promise) {
118
- Log.d(TAG, "RTNHCEAndroidModule:acquireExclusiveNFC");
119
- promise.reject("err_platform_unsupported", "acquireExclusiveNFC() is not supported on Android.");
120
- }
208
+ public void getCapabilities(Promise promise) {
209
+ boolean hceSupported = isHCESupported();
210
+ boolean nfcEnabled = false;
121
211
 
122
- @Override
123
- public void releaseExclusiveNFC() {
124
- Log.d(TAG, "RTNHCEAndroidModule:releaseExclusiveNFC");
125
- // unsupported on Android, no-op
126
- }
212
+ try {
213
+ NfcAdapter adapter = NfcAdapter.getDefaultAdapter(getReactApplicationContext());
214
+ nfcEnabled = adapter != null && adapter.isEnabled();
215
+ } catch (RuntimeException e) {
216
+ Log.w(TAG, "getCapabilities: unable to check NFC adapter state", e);
217
+ }
127
218
 
128
- @Override
129
- public boolean isExclusiveNFC() {
130
- Log.d(TAG, "RTNHCEAndroidModule:isExclusiveNFC");
131
- // unsupported on Android, always false
132
- return false;
219
+ WritableMap map = Arguments.createMap();
220
+ map.putString("platform", "android");
221
+ map.putBoolean("hceSupported", hceSupported);
222
+ map.putBoolean("nfcEnabled", nfcEnabled);
223
+ map.putBoolean("eligible", hceSupported);
224
+ map.putBoolean("backgroundSupported", hceSupported);
225
+ map.putBoolean("presentmentSupported", false);
226
+ promise.resolve(map);
133
227
  }
134
228
 
135
229
  @Override
136
- public void beginSession(Promise promise) {
137
- Log.d(TAG, "RTNHCEAndroidModule:beginSession");
138
-
139
- if (!isPlatformSupported()) {
140
- promise.reject("err_platform_unsupported", "Unsupported Android version or missing NFC/HCE feature.");
230
+ public void beginSession(String sessionId, boolean allowWhenLocked, Promise promise) {
231
+ if (!isHCESupported()) {
232
+ promise.reject("err_platform_unsupported", "The device doesn't support NFC host card emulation.");
141
233
  return;
142
234
  }
143
235
 
144
- if (!this.sessionRunning) {
145
- this.sessionRunning = true;
146
- sendEvent("sessionStarted", "");
147
- promise.resolve(null);
148
- } else {
149
- promise.reject("err_card_session_exists", "Session already exists.");
236
+ try {
237
+ HCEManager.getInstance().beginSession(this, sessionId, allowWhenLocked);
238
+ } catch (IllegalStateException e) {
239
+ promise.reject("err_session_exists", "A session already exists.");
240
+ return;
150
241
  }
151
- }
152
242
 
153
- @Override
154
- public void setSessionAlertMessage(String message) {
155
- Log.d(TAG, "RTNHCEAndroidModule:setSessionAlertMessage");
156
- // unsupported on Android, no-op
243
+ promise.resolve(null);
157
244
  }
158
245
 
159
246
  @Override
160
- public void invalidateSession() {
161
- Log.d(TAG, "RTNHCEAndroidModule:invalidateSession");
162
- if (this.sessionRunning) {
163
- this.hceRunning = false;
164
- this.sessionRunning = false;
165
- this.hceBrokenConnection = true;
166
- sendEvent("sessionInvalidated", "userInvalidated");
167
- }
247
+ public void invalidateSession(String sessionId) {
248
+ HCEManager.getInstance().invalidateSession(this, sessionId);
249
+ refreshPreferredService();
168
250
  }
169
251
 
170
252
  @Override
171
- public boolean isSessionRunning() {
172
- Log.d(TAG, "RTNHCEAndroidModule:isSessionRunning");
173
- return this.sessionRunning;
253
+ public void setSessionAlertMessage(String sessionId, String message) {
254
+ // unsupported on Android, no-op
174
255
  }
175
256
 
176
257
  @Override
177
- public boolean beginBackgroundHCE(String handle) {
178
- Log.d(TAG, "RTNHCEAndroidModule:beginBackgroundHCE");
179
-
258
+ public void startEmulation(String sessionId, Promise promise) {
180
259
  try {
181
- this.serviceCb.onBackgroundHCEInit(handle);
260
+ HCEManager.getInstance().startEmulation(this, sessionId);
182
261
  } catch (IllegalStateException e) {
183
- return false;
262
+ promise.reject("err_no_session", "The session is not active.");
263
+ return;
184
264
  }
185
265
 
186
- this.hceBackgroundReady = true;
187
- return true;
266
+ refreshPreferredService();
267
+ promise.resolve(null);
188
268
  }
189
269
 
190
270
  @Override
191
- public boolean finishBackgroundHCE(String handle) {
192
- Log.d(TAG, "RTNHCEAndroidModule:finishBackgroundHCE");
193
-
271
+ public void stopEmulation(String sessionId, String status, Promise promise) {
194
272
  try {
195
- this.serviceCb.onBackgroundHCEFinish(handle);
273
+ HCEManager.getInstance().stopEmulation(this, sessionId);
196
274
  } catch (IllegalStateException e) {
197
- return false;
275
+ promise.reject("err_no_session", "The session is not active.");
276
+ return;
198
277
  }
199
278
 
200
- return true;
279
+ refreshPreferredService();
280
+ promise.resolve(null);
201
281
  }
202
282
 
203
283
  @Override
204
- public void startHCE(Promise promise) {
205
- Log.d(TAG, "RTNHCEAndroidModule:startHCE");
206
- if (!this.sessionRunning) {
207
- promise.reject("err_no_session", "No session is active.");
208
- return;
209
- }
210
-
211
- if (this.hceRunning) {
212
- promise.reject("err_start_emulation", "Error trying to start emulation. Emulation already started.");
213
- return;
214
- }
215
-
216
- this.hceRunning = true;
217
- this.hceBrokenConnection = false;
218
- promise.resolve(null);
284
+ public void isEmulationRunning(String sessionId, Promise promise) {
285
+ promise.resolve(HCEManager.getInstance().isEmulationRunning(this, sessionId));
219
286
  }
220
287
 
221
288
  @Override
222
- public void stopHCE(String status, Promise promise) {
223
- Log.d(TAG, "RTNHCEAndroidModule:stopHCE");
224
- if (!this.sessionRunning) {
225
- promise.reject("err_no_session", "No session is active.");
289
+ public void respondApdu(String sessionId, double apduId, String rapdu, Promise promise) {
290
+ byte[] response;
291
+
292
+ try {
293
+ response = BinaryUtils.HexStringToByteArray(rapdu);
294
+ } catch (IllegalArgumentException e) {
295
+ promise.reject("err_invalid_rapdu", "The R-APDU is not a valid hex-encoded string.");
226
296
  return;
227
297
  }
228
298
 
229
- if (this.hceRunning) {
230
- this.hceRunning = false;
299
+ if (response.length < 2) {
300
+ promise.reject("err_invalid_rapdu", "The R-APDU must contain at least the status word.");
301
+ return;
302
+ }
231
303
 
232
- if (!this.hceDeselected) {
233
- this.hceDeselected = true;
234
- this.hceBrokenConnection = true;
235
- sendEvent("readerDeselected", "");
236
- }
304
+ try {
305
+ HCEManager.getInstance().respondApdu(this, sessionId, (long) apduId, response);
306
+ } catch (IllegalStateException e) {
307
+ promise.reject("err_stale_apdu", "The C-APDU is not awaiting a response (reader lost or session closed).");
308
+ return;
237
309
  }
238
310
 
239
311
  promise.resolve(null);
240
312
  }
241
313
 
242
- @RequiresApi(api = Build.VERSION_CODES.N)
243
314
  @Override
244
- public void respondAPDU(String handle, String rapdu, Promise promise) {
245
- Log.d(TAG, "RTNHCEAndroidModule:respondAPDU");
246
-
247
- if (!this.hceBackgroundReady) {
248
- if (!this.sessionRunning) {
249
- promise.reject("err_no_session", "No session is active.");
250
- return;
251
- } else if (!this.hceRunning) {
252
- promise.reject("err_no_hce", "HCE is not running.");
253
- return;
254
- }
255
- }
315
+ public void acquireExclusiveNFC(Promise promise) {
316
+ promise.reject("err_platform_unsupported", "acquireExclusiveNFC() is not supported on Android.");
317
+ }
256
318
 
257
- Log.d(TAG, "respondAPDU(): Send to service");
319
+ @Override
320
+ public void releaseExclusiveNFC() {
321
+ // unsupported on Android, no-op
322
+ }
258
323
 
259
- try {
260
- serviceCb.onRespondAPDU(handle, rapdu);
261
- } catch (IllegalStateException e) {
262
- promise.reject("err_illegal_state", "There is no C-APDU to respond to at the time or the provided handle is invalid.");
263
- return;
264
- }
324
+ @Override
325
+ public boolean isExclusiveNFC() {
326
+ // unsupported on Android, always false
327
+ return false;
328
+ }
265
329
 
266
- promise.resolve(null);
330
+ @Override
331
+ public void setBackgroundHandlerRegistered(boolean registered, boolean allowWhenLocked) {
332
+ this.backgroundHandlerDeclaration = new BackgroundHandlerDeclaration(registered, allowWhenLocked);
333
+ UiThreadUtil.runOnUiThread(() -> HCEManager.getInstance().onBackgroundHandlerDeclared(this));
267
334
  }
268
335
 
269
336
  @Override
270
- public void isHCERunning(Promise promise) {
271
- Log.d(TAG, "RTNHCEAndroidModule:isHCERunning");
272
- promise.resolve(this.hceRunning);
337
+ public boolean backgroundSessionReady(String sessionId) {
338
+ return HCEManager.getInstance().backgroundSessionReady(this, sessionId);
273
339
  }
274
340
  }
@@ -1,8 +1,9 @@
1
1
  package com.itsecrnd.rtnhceandroid;
2
2
 
3
+ import androidx.annotation.NonNull;
3
4
  import androidx.annotation.Nullable;
4
5
 
5
- import com.facebook.react.TurboReactPackage;
6
+ import com.facebook.react.BaseReactPackage;
6
7
  import com.facebook.react.bridge.NativeModule;
7
8
  import com.facebook.react.bridge.ReactApplicationContext;
8
9
  import com.facebook.react.module.model.ReactModuleInfo;
@@ -11,10 +12,10 @@ import com.facebook.react.module.model.ReactModuleInfoProvider;
11
12
  import java.util.HashMap;
12
13
  import java.util.Map;
13
14
 
14
- public class RTNHCEAndroidPackage extends TurboReactPackage {
15
+ public class RTNHCEAndroidPackage extends BaseReactPackage {
15
16
  @Nullable
16
17
  @Override
17
- public NativeModule getModule(String name, ReactApplicationContext reactContext) {
18
+ public NativeModule getModule(@NonNull String name, @NonNull ReactApplicationContext reactContext) {
18
19
  if (name.equals(RTNHCEAndroidModule.NAME)) {
19
20
  return new RTNHCEAndroidModule(reactContext);
20
21
  } else {
@@ -22,6 +23,7 @@ public class RTNHCEAndroidPackage extends TurboReactPackage {
22
23
  }
23
24
  }
24
25
 
26
+ @NonNull
25
27
  @Override
26
28
  public ReactModuleInfoProvider getReactModuleInfoProvider() {
27
29
  return () -> {
@@ -33,7 +35,6 @@ public class RTNHCEAndroidPackage extends TurboReactPackage {
33
35
  RTNHCEAndroidModule.NAME,
34
36
  false, // canOverrideExistingModule
35
37
  false, // needsEagerInit
36
- true, // hasConstants
37
38
  false, // isCxxModule
38
39
  true // isTurboModule
39
40
  ));