react-native-zeroconf 0.16.1 → 0.17.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.
Files changed (45) hide show
  1. package/NOTICE +0 -3
  2. package/README.md +3 -3
  3. package/android/build.gradle +0 -3
  4. package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroConfImplFactory.java +1 -1
  5. package/android/src/main/java/com/balthazargronon/RCTZeroconf/Zeroconf.java +12 -2
  6. package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfModule.java +129 -4
  7. package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfOptions.java +77 -0
  8. package/android/src/main/java/com/balthazargronon/RCTZeroconf/dnssd/DnssdImpl.java +654 -0
  9. package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/NsdServiceImpl.java +393 -39
  10. package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/ServiceInfoCallbacks.java +50 -13
  11. package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/ServiceTypesQuery.java +296 -0
  12. package/android/src/main/java/com/github/druk/dnssd/DNSSDEmbedded.java +1 -1
  13. package/android/src/main/jni/mdnsresponder/mDNSShared/dnssd_clientshim.c +59 -22
  14. package/dist/index.js +235 -48
  15. package/index.d.ts +105 -7
  16. package/ios/RNZeroconf/RNZeroconf.m +285 -22
  17. package/package.json +28 -4
  18. package/src/index.js +214 -44
  19. package/windows/ExperimentalFeatures.props +33 -0
  20. package/windows/NuGet.config +11 -0
  21. package/windows/RNZeroconf/RNZeroconf.cpp +228 -0
  22. package/windows/RNZeroconf/RNZeroconf.def +3 -0
  23. package/windows/RNZeroconf/RNZeroconf.h +70 -0
  24. package/windows/RNZeroconf/RNZeroconf.rc +0 -0
  25. package/windows/RNZeroconf/RNZeroconf.vcxproj +150 -0
  26. package/windows/RNZeroconf/RNZeroconf.vcxproj.filters +50 -0
  27. package/windows/RNZeroconf/ReactPackageProvider.cpp +20 -0
  28. package/windows/RNZeroconf/ReactPackageProvider.h +24 -0
  29. package/windows/RNZeroconf/ReactPackageProvider.idl +9 -0
  30. package/windows/RNZeroconf/ZeroconfCore.cpp +1095 -0
  31. package/windows/RNZeroconf/ZeroconfCore.h +167 -0
  32. package/windows/RNZeroconf/pch.cpp +1 -0
  33. package/windows/RNZeroconf/pch.h +30 -0
  34. package/windows/RNZeroconf/resource.h +5 -0
  35. package/windows/RNZeroconf/targetver.h +8 -0
  36. package/windows/RNZeroconf.sln +43 -0
  37. package/android/src/main/java/com/balthazargronon/RCTZeroconf/rx2dnssd/DnssdImpl.java +0 -305
  38. package/android/src/main/java/com/github/druk/rx2dnssd/BonjourService.java +0 -400
  39. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2BrowseListener.java +0 -55
  40. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2Dnssd.java +0 -127
  41. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2DnssdCommon.java +0 -280
  42. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2DnssdEmbedded.java +0 -31
  43. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2QueryListener.java +0 -70
  44. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2RegisterListener.java +0 -47
  45. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2ResolveListener.java +0 -51
@@ -2,6 +2,7 @@ package com.balthazargronon.RCTZeroconf.nsd;
2
2
 
3
3
  import android.annotation.SuppressLint;
4
4
  import android.content.Context;
5
+ import android.net.Network;
5
6
  import android.net.nsd.NsdManager;
6
7
  import android.net.nsd.NsdServiceInfo;
7
8
  import android.net.wifi.WifiManager;
@@ -12,6 +13,7 @@ import android.util.Log;
12
13
 
13
14
  import com.balthazargronon.RCTZeroconf.Zeroconf;
14
15
  import com.balthazargronon.RCTZeroconf.ZeroconfModule;
16
+ import com.balthazargronon.RCTZeroconf.ZeroconfOptions;
15
17
  import com.facebook.react.bridge.Promise;
16
18
  import com.facebook.react.bridge.ReactApplicationContext;
17
19
 
@@ -28,6 +30,8 @@ import java.util.ArrayList;
28
30
  import java.util.Collections;
29
31
  import java.util.Iterator;
30
32
  import java.util.List;
33
+ import java.util.Set;
34
+ import java.util.HashSet;
31
35
  import java.util.Locale;
32
36
  import java.util.Map;
33
37
  import java.util.concurrent.ConcurrentHashMap;
@@ -59,9 +63,16 @@ public class NsdServiceImpl implements Zeroconf {
59
63
 
60
64
  private static class NsdScan {
61
65
  final String scanId;
66
+ double resolveTimeoutSeconds = 5;
62
67
  NsdManager.DiscoveryListener discoveryListener;
63
68
  // Android 14+: services are followed with registerServiceInfoCallback, null before
64
69
  @Nullable ServiceInfoCallbacks infoCallbacks;
70
+ // Service type scans query the network directly, NsdManager can't list types
71
+ @Nullable ServiceTypesQuery typesQuery;
72
+ // Types listed, and those answered by the network: the others are this app's own services,
73
+ // which the system mDNS stack doesn't answer back
74
+ final Set<String> listedTypes = new HashSet<>();
75
+ final Set<String> networkTypes = new HashSet<>();
65
76
 
66
77
  NsdScan(String scanId) {
67
78
  this.scanId = scanId;
@@ -69,12 +80,18 @@ public class NsdServiceImpl implements Zeroconf {
69
80
  }
70
81
 
71
82
  private static class PendingResolve {
72
- final String scanId;
83
+ @Nullable final String scanId;
73
84
  final NsdServiceInfo serviceInfo;
85
+ // resolveService(): settled instead of emitting events
86
+ @Nullable final Promise promise;
87
+ // Seconds before the TIMEOUT error, counted from the start of the resolve
88
+ final double timeoutSeconds;
74
89
 
75
- PendingResolve(String scanId, NsdServiceInfo serviceInfo) {
90
+ PendingResolve(@Nullable String scanId, NsdServiceInfo serviceInfo, @Nullable Promise promise, double timeoutSeconds) {
76
91
  this.scanId = scanId;
77
92
  this.serviceInfo = serviceInfo;
93
+ this.promise = promise;
94
+ this.timeoutSeconds = timeoutSeconds;
78
95
  }
79
96
  }
80
97
 
@@ -85,16 +102,65 @@ public class NsdServiceImpl implements Zeroconf {
85
102
  }
86
103
 
87
104
  @Override
88
- public void scan(final String scanId, String type, String protocol, String domain) {
105
+ public void scan(final String scanId, String type, String protocol, String domain, ZeroconfOptions options) {
89
106
  final NsdManager nsdManager = getNsdManager();
90
107
 
91
108
  this.stop(scanId);
92
- acquireMulticastLock();
93
109
 
94
110
  final NsdScan scan = new NsdScan(scanId);
111
+ scan.resolveTimeoutSeconds = options.resolveTimeoutSeconds;
112
+ final boolean typesOnly = ZeroconfModule.isServiceTypesScan(type, protocol);
113
+
114
+ Network network = null;
115
+ if (options.networkInterface != null && !typesOnly) {
116
+ String networkError = checkNetworkSupport(options.networkInterface);
117
+ network = networkError == null ? ZeroconfModule.findNetwork(reactApplicationContext, options.networkInterface) : null;
118
+ if (network == null) {
119
+ sendNetworkError(networkError, options.networkInterface, null, scanId);
120
+ return;
121
+ }
122
+ }
123
+ acquireMulticastLock();
124
+
125
+ if (typesOnly) {
126
+ scan.typesQuery = new ServiceTypesQuery(options.networkInterface, new ServiceTypesQuery.Listener() {
127
+ @Override
128
+ public void onTypeFound(String serviceType) {
129
+ synchronized (scan) {
130
+ scan.networkTypes.add(serviceType);
131
+ if (!scan.listedTypes.add(serviceType)) {
132
+ return;
133
+ }
134
+ }
135
+ sendScanEvent(ZeroconfModule.EVENT_FOUND, nameToMap(serviceType), scanId);
136
+ }
137
+
138
+ @Override
139
+ public void onTypeLost(String serviceType) {
140
+ synchronized (scan) {
141
+ scan.networkTypes.remove(serviceType);
142
+ if (localServiceTypes().contains(serviceType) || !scan.listedTypes.remove(serviceType)) {
143
+ return;
144
+ }
145
+ }
146
+ sendScanEvent(ZeroconfModule.EVENT_REMOVE, nameToMap(serviceType), scanId);
147
+ }
148
+
149
+ @Override
150
+ public void onError(String message) {
151
+ zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_LIBRARY, ZeroconfModule.ERROR_CODE_EXCEPTION, "Listing service types failed: " + message, null, scanId);
152
+ }
153
+ });
154
+ mScans.put(scanId, scan);
155
+ scan.typesQuery.start();
156
+ sendScanEvent(ZeroconfModule.EVENT_START, new WritableNativeMap(), scanId);
157
+ syncLocalServiceTypes();
158
+ return;
159
+ }
95
160
 
96
161
  if (Build.VERSION.SDK_INT >= 34) {
97
- scan.infoCallbacks = new ServiceInfoCallbacks(nsdManager, mCallbackExecutor, new ServiceInfoCallbacks.Listener() {
162
+ long timeoutMs = (long) (options.resolveTimeoutSeconds * 1000);
163
+ scan.infoCallbacks = new ServiceInfoCallbacks(nsdManager, mCallbackExecutor, mHandler, timeoutMs, new ServiceInfoCallbacks.Listener() {
98
164
  @Override
99
165
  public void onServiceUpdated(NsdServiceInfo serviceInfo) {
100
166
  if (getHostAddresses(serviceInfo).isEmpty()) {
@@ -107,6 +173,11 @@ public class NsdServiceImpl implements Zeroconf {
107
173
  public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
108
174
  zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_NSD, errorCode, "Resolving service " + serviceInfo.getServiceName() + " failed: " + ZeroconfModule.describeNsdError(errorCode), serviceInfo.getServiceName(), scanId);
109
175
  }
176
+
177
+ @Override
178
+ public void onTimeout(NsdServiceInfo serviceInfo) {
179
+ sendResolveTimeout(serviceInfo.getServiceName(), scanId);
180
+ }
110
181
  });
111
182
  }
112
183
 
@@ -144,7 +215,7 @@ public class NsdServiceImpl implements Zeroconf {
144
215
  if (scan.infoCallbacks != null) {
145
216
  scan.infoCallbacks.register(serviceInfo);
146
217
  } else {
147
- enqueueResolve(scanId, serviceInfo);
218
+ enqueueResolve(scan, serviceInfo);
148
219
  }
149
220
  }
150
221
 
@@ -159,8 +230,34 @@ public class NsdServiceImpl implements Zeroconf {
159
230
  };
160
231
 
161
232
  mScans.put(scanId, scan);
162
- String serviceType = String.format("_%s._%s.", type, protocol);
163
- nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, scan.discoveryListener);
233
+ // "_ipp._tcp,_printer" browses the _printer subtype
234
+ String serviceType = options.subtype != null
235
+ ? ZeroconfOptions.withSubtypes(String.format("_%s._%s", type, protocol), Collections.singletonList(options.subtype))
236
+ : String.format("_%s._%s.", type, protocol);
237
+ if (network != null && Build.VERSION.SDK_INT >= 33) {
238
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, network, mCallbackExecutor, scan.discoveryListener);
239
+ } else {
240
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, scan.discoveryListener);
241
+ }
242
+ }
243
+
244
+ // Choosing the network of NSD requests needs Android 13, null when supported
245
+ @Nullable
246
+ private static String checkNetworkSupport(String networkInterface) {
247
+ return Build.VERSION.SDK_INT >= 33 ? null : ZeroconfModule.ERROR_CODE_UNSUPPORTED;
248
+ }
249
+
250
+ private void sendNetworkError(@Nullable String code, String networkInterface, @Nullable Promise promise, @Nullable String scanId) {
251
+ boolean unsupported = ZeroconfModule.ERROR_CODE_UNSUPPORTED.equals(code);
252
+ String errorCode = unsupported ? ZeroconfModule.ERROR_CODE_UNSUPPORTED : ZeroconfModule.ERROR_CODE_UNKNOWN_INTERFACE;
253
+ String message = unsupported
254
+ ? "Choosing the network interface needs Android 13 or later with NSD, use DNSSD"
255
+ : "Unknown network interface " + networkInterface;
256
+ if (promise != null) {
257
+ ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_LIBRARY, errorCode, message, null);
258
+ } else {
259
+ zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_LIBRARY, errorCode, message, null, scanId);
260
+ }
164
261
  }
165
262
 
166
263
  @Override
@@ -183,10 +280,17 @@ public class NsdServiceImpl implements Zeroconf {
183
280
  scan.infoCallbacks.unregisterAll();
184
281
  }
185
282
 
283
+ if (scan.typesQuery != null) {
284
+ scan.typesQuery.stop();
285
+ // No discovery listener reports it
286
+ sendScanEvent(ZeroconfModule.EVENT_STOP, new WritableNativeMap(), scanId);
287
+ }
288
+
186
289
  synchronized (mResolveQueue) {
187
290
  Iterator<PendingResolve> iterator = mResolveQueue.iterator();
188
291
  while (iterator.hasNext()) {
189
- if (iterator.next().scanId.equals(scanId)) {
292
+ // resolveService() entries have no scan id
293
+ if (scanId.equals(iterator.next().scanId)) {
190
294
  iterator.remove();
191
295
  }
192
296
  }
@@ -222,6 +326,12 @@ public class NsdServiceImpl implements Zeroconf {
222
326
  zeroconfModule.sendEvent(getReactApplicationContext(), eventName, body);
223
327
  }
224
328
 
329
+ private static WritableMap nameToMap(String name) {
330
+ WritableMap service = new WritableNativeMap();
331
+ service.putString(ZeroconfModule.KEY_SERVICE_NAME, name);
332
+ return service;
333
+ }
334
+
225
335
  private static WritableMap serviceNameToMap(NsdServiceInfo serviceInfo) {
226
336
  WritableMap service = new WritableNativeMap();
227
337
  service.putString(ZeroconfModule.KEY_SERVICE_NAME, serviceInfo.getServiceName());
@@ -229,22 +339,119 @@ public class NsdServiceImpl implements Zeroconf {
229
339
  }
230
340
 
231
341
  @Override
232
- public void registerService(String type, String protocol, String domain, String name, int port, ReadableArray txt, Promise promise) {
233
- String serviceType = String.format("_%s._%s.", type, protocol);
342
+ public void registerService(String type, String protocol, String domain, String name, int port, ReadableArray txt, ZeroconfOptions options, Promise promise) {
343
+ Network network = null;
344
+ if (options.networkInterface != null) {
345
+ String networkError = checkNetworkSupport(options.networkInterface);
346
+ network = networkError == null ? ZeroconfModule.findNetwork(reactApplicationContext, options.networkInterface) : null;
347
+ if (network == null) {
348
+ sendNetworkError(networkError, options.networkInterface, promise, null);
349
+ return;
350
+ }
351
+ }
352
+ // "_ipp._tcp,_printer,_color" registers the subtypes too
353
+ String serviceType = options.subtypes.isEmpty()
354
+ ? String.format("_%s._%s.", type, protocol)
355
+ : ZeroconfOptions.withSubtypes(String.format("_%s._%s", type, protocol), options.subtypes);
356
+ ServiceRegistrationListener listener = new ServiceRegistrationListener(promise, serviceType, port, network);
357
+ getNsdManager().registerService(listener.buildServiceInfo(name, txt), NsdManager.PROTOCOL_DNS_SD, listener);
358
+ }
234
359
 
235
- final NsdManager nsdManager = this.getNsdManager();
236
- NsdServiceInfo serviceInfo = new NsdServiceInfo();
360
+ /**
361
+ * NsdManager can't update a registration: unregisters and registers again with the new TXT record,
362
+ * without emitting unpublished and published
363
+ */
364
+ @Override
365
+ public void updateService(String serviceName, final ReadableArray txt, final Promise promise) {
366
+ final ServiceRegistrationListener previous = mPublishedServices.remove(serviceName);
367
+ if (previous == null) {
368
+ ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_LIBRARY, ZeroconfModule.ERROR_CODE_NOT_PUBLISHED, "Service " + serviceName + " is not published", serviceName);
369
+ return;
370
+ }
371
+ final ServiceRegistrationListener next = new ServiceRegistrationListener(promise, previous.serviceType, previous.port, previous.network);
372
+ next.announceRegistered = false;
373
+ final NsdServiceInfo serviceInfo = next.buildServiceInfo(serviceName, txt);
374
+ previous.announceUnregistered = false;
375
+ previous.afterUnregistered = () -> getNsdManager().registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, next);
376
+ getNsdManager().unregisterService(previous);
377
+ }
378
+
379
+ @Override
380
+ public void resolveService(String name, String type, String protocol, String domain, ZeroconfOptions options, final Promise promise) {
381
+ final NsdServiceInfo serviceInfo = new NsdServiceInfo();
237
382
  serviceInfo.setServiceName(name);
238
- serviceInfo.setServiceType(serviceType);
239
- serviceInfo.setPort(port);
383
+ serviceInfo.setServiceType(String.format("_%s._%s", type, protocol));
384
+ if (options.networkInterface != null) {
385
+ String networkError = checkNetworkSupport(options.networkInterface);
386
+ Network network = networkError == null ? ZeroconfModule.findNetwork(reactApplicationContext, options.networkInterface) : null;
387
+ if (network == null) {
388
+ sendNetworkError(networkError, options.networkInterface, promise, null);
389
+ return;
390
+ }
391
+ if (Build.VERSION.SDK_INT >= 33) {
392
+ serviceInfo.setNetwork(network);
393
+ }
394
+ }
240
395
 
241
- for (int i = 0; i < txt.size(); i++) {
242
- ReadableArray pair = txt.getArray(i);
243
- serviceInfo.setAttribute(pair.getString(0), pair.getString(1));
396
+ if (Build.VERSION.SDK_INT < 34) {
397
+ // One resolve at a time before Android 14, shares the queue of the scans
398
+ synchronized (mResolveQueue) {
399
+ mResolveQueue.add(new PendingResolve(null, serviceInfo, promise, options.timeoutSeconds));
400
+ }
401
+ resolveNext();
402
+ return;
244
403
  }
245
404
 
246
- nsdManager.registerService(
247
- serviceInfo, NsdManager.PROTOCOL_DNS_SD, new ServiceRegistrationListener(promise));
405
+ final boolean[] settled = { false };
406
+ final NsdManager nsdManager = getNsdManager();
407
+ final NsdManager.ServiceInfoCallback[] callback = new NsdManager.ServiceInfoCallback[1];
408
+ final Runnable finish = () -> {
409
+ try {
410
+ nsdManager.unregisterServiceInfoCallback(callback[0]);
411
+ } catch (IllegalArgumentException e) {
412
+ // Already unregistered
413
+ }
414
+ };
415
+ callback[0] = new NsdManager.ServiceInfoCallback() {
416
+ @Override
417
+ public void onServiceInfoCallbackRegistrationFailed(int errorCode) {
418
+ synchronized (settled) {
419
+ if (settled[0]) return;
420
+ settled[0] = true;
421
+ }
422
+ ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_NSD, errorCode, "Resolving service " + name + " failed: " + ZeroconfModule.describeNsdError(errorCode), name);
423
+ }
424
+
425
+ @Override
426
+ public void onServiceUpdated(NsdServiceInfo updated) {
427
+ if (getHostAddresses(updated).isEmpty()) {
428
+ return;
429
+ }
430
+ synchronized (settled) {
431
+ if (settled[0]) return;
432
+ settled[0] = true;
433
+ }
434
+ promise.resolve(serviceInfoToMap(updated));
435
+ mHandler.post(finish);
436
+ }
437
+
438
+ @Override
439
+ public void onServiceLost() {
440
+ }
441
+
442
+ @Override
443
+ public void onServiceInfoCallbackUnregistered() {
444
+ }
445
+ };
446
+ nsdManager.registerServiceInfoCallback(serviceInfo, mCallbackExecutor, callback[0]);
447
+ mHandler.postDelayed(() -> {
448
+ synchronized (settled) {
449
+ if (settled[0]) return;
450
+ settled[0] = true;
451
+ }
452
+ finish.run();
453
+ ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_LIBRARY, ZeroconfModule.ERROR_CODE_TIMEOUT, "Resolving service " + name + " failed: timed out", name);
454
+ }, (long) (options.timeoutSeconds * 1000));
248
455
  }
249
456
 
250
457
  @Override
@@ -262,6 +469,53 @@ public class NsdServiceImpl implements Zeroconf {
262
469
  mPublishedServices.remove(serviceName);
263
470
  serviceListener.unregisterPromise = promise;
264
471
  nsdManager.unregisterService(serviceListener);
472
+ syncLocalServiceTypes();
473
+ }
474
+
475
+ // "_ipp._tcp,_printer" or "_http._tcp." -> "_ipp._tcp", "_http._tcp"
476
+ private static String baseServiceType(String serviceType) {
477
+ int comma = serviceType.indexOf(',');
478
+ String type = comma >= 0 ? serviceType.substring(0, comma) : serviceType;
479
+ return type.endsWith(".") ? type.substring(0, type.length() - 1) : type;
480
+ }
481
+
482
+ private Set<String> localServiceTypes() {
483
+ Set<String> types = new HashSet<>();
484
+ for (ServiceRegistrationListener listener : mPublishedServices.values()) {
485
+ types.add(baseServiceType(listener.serviceType));
486
+ }
487
+ return types;
488
+ }
489
+
490
+ // Lists the types this app publishes in service type scans, and removes those it no longer publishes
491
+ private void syncLocalServiceTypes() {
492
+ Set<String> local = localServiceTypes();
493
+ for (NsdScan scan : mScans.values()) {
494
+ if (scan.typesQuery == null) {
495
+ continue;
496
+ }
497
+ List<String> found = new ArrayList<>();
498
+ List<String> removed = new ArrayList<>();
499
+ synchronized (scan) {
500
+ for (String type : local) {
501
+ if (scan.listedTypes.add(type)) {
502
+ found.add(type);
503
+ }
504
+ }
505
+ for (String type : new ArrayList<>(scan.listedTypes)) {
506
+ if (!local.contains(type) && !scan.networkTypes.contains(type)) {
507
+ scan.listedTypes.remove(type);
508
+ removed.add(type);
509
+ }
510
+ }
511
+ }
512
+ for (String type : found) {
513
+ sendScanEvent(ZeroconfModule.EVENT_FOUND, nameToMap(type), scan.scanId);
514
+ }
515
+ for (String type : removed) {
516
+ sendScanEvent(ZeroconfModule.EVENT_REMOVE, nameToMap(type), scan.scanId);
517
+ }
518
+ }
265
519
  }
266
520
 
267
521
  @Override
@@ -282,13 +536,19 @@ public class NsdServiceImpl implements Zeroconf {
282
536
  return reactApplicationContext;
283
537
  }
284
538
 
285
- private void enqueueResolve(String scanId, NsdServiceInfo serviceInfo) {
539
+ private void enqueueResolve(NsdScan scan, NsdServiceInfo serviceInfo) {
540
+ // A resolve can't be cancelled before Android 14, so there is no retry: waiting twice as long
541
+ // matches the total time of the other implementations, which retry once
286
542
  synchronized (mResolveQueue) {
287
- mResolveQueue.add(new PendingResolve(scanId, serviceInfo));
543
+ mResolveQueue.add(new PendingResolve(scan.scanId, serviceInfo, null, scan.resolveTimeoutSeconds * 2));
288
544
  }
289
545
  resolveNext();
290
546
  }
291
547
 
548
+ private void sendResolveTimeout(String serviceName, String scanId) {
549
+ zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_LIBRARY, ZeroconfModule.ERROR_CODE_TIMEOUT, "Resolving service " + serviceName + " failed: timed out", serviceName, scanId);
550
+ }
551
+
292
552
  private void resolveNext() {
293
553
  PendingResolve next;
294
554
  synchronized (mResolveQueue) {
@@ -298,8 +558,11 @@ public class NsdServiceImpl implements Zeroconf {
298
558
  next = mResolveQueue.poll();
299
559
  mIsResolving = true;
300
560
  }
561
+ final ZeroResolveListener listener = new ZeroResolveListener(next);
301
562
  try {
302
- getNsdManager().resolveService(next.serviceInfo, new ZeroResolveListener(next.scanId));
563
+ getNsdManager().resolveService(next.serviceInfo, listener);
564
+ // The system keeps the resolve until it answers or fails, the queue waits for it
565
+ mHandler.postDelayed(listener::timedOut, (long) (next.timeoutSeconds * 1000));
303
566
  } catch (Throwable e) {
304
567
  Log.e(TAG, "resolveService failed", e);
305
568
  String serviceName = next.serviceInfo.getServiceName();
@@ -316,33 +579,76 @@ public class NsdServiceImpl implements Zeroconf {
316
579
  }
317
580
 
318
581
  private class ZeroResolveListener implements NsdManager.ResolveListener {
319
- private final String scanId;
582
+ private final PendingResolve pending;
583
+ @Nullable private final String scanId;
584
+ @Nullable private final Promise promise;
585
+ // Settled: answered, failed or timed out. A late answer after the timeout only frees the queue
586
+ private boolean settled = false;
587
+
588
+ ZeroResolveListener(PendingResolve pending) {
589
+ this.pending = pending;
590
+ this.scanId = pending.scanId;
591
+ this.promise = pending.promise;
592
+ }
320
593
 
321
- ZeroResolveListener(String scanId) {
322
- this.scanId = scanId;
594
+ private synchronized boolean settle() {
595
+ if (settled) {
596
+ return false;
597
+ }
598
+ settled = true;
599
+ return true;
600
+ }
601
+
602
+ void timedOut() {
603
+ if (!settle()) {
604
+ return;
605
+ }
606
+ String serviceName = pending.serviceInfo.getServiceName();
607
+ if (promise != null) {
608
+ ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_LIBRARY, ZeroconfModule.ERROR_CODE_TIMEOUT, "Resolving service " + serviceName + " failed: timed out", serviceName);
609
+ } else if (mScans.containsKey(scanId)) {
610
+ sendResolveTimeout(serviceName, scanId);
611
+ }
323
612
  }
324
613
 
325
614
  @Override
326
615
  public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
327
616
  if (errorCode == NsdManager.FAILURE_ALREADY_ACTIVE) {
328
617
  // Another resolve (possibly from another app or listener) is in flight: retry shortly
618
+ if (!settle()) {
619
+ onResolveDone();
620
+ return;
621
+ }
329
622
  synchronized (mResolveQueue) {
330
- mResolveQueue.addFirst(new PendingResolve(scanId, serviceInfo));
623
+ mResolveQueue.addFirst(pending);
331
624
  mIsResolving = false;
332
625
  }
333
626
  mHandler.postDelayed(NsdServiceImpl.this::resolveNext, RESOLVE_RETRY_DELAY_MS);
334
627
  return;
335
628
  }
336
- if (mScans.containsKey(scanId)) {
337
- zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_NSD, errorCode, "Resolving service " + serviceInfo.getServiceName() + " failed: " + ZeroconfModule.describeNsdError(errorCode), serviceInfo.getServiceName(), scanId);
629
+ if (!settle()) {
630
+ onResolveDone();
631
+ return;
632
+ }
633
+ String message = "Resolving service " + serviceInfo.getServiceName() + " failed: " + ZeroconfModule.describeNsdError(errorCode);
634
+ if (promise != null) {
635
+ ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_NSD, errorCode, message, serviceInfo.getServiceName());
636
+ } else if (mScans.containsKey(scanId)) {
637
+ zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_NSD, errorCode, message, serviceInfo.getServiceName(), scanId);
338
638
  }
339
639
  onResolveDone();
340
640
  }
341
641
 
342
642
  @Override
343
643
  public void onServiceResolved(NsdServiceInfo serviceInfo) {
344
- // The scan may have been stopped while resolving
345
- if (mScans.containsKey(scanId)) {
644
+ if (!settle()) {
645
+ onResolveDone();
646
+ return;
647
+ }
648
+ if (promise != null) {
649
+ promise.resolve(serviceInfoToMap(serviceInfo));
650
+ } else if (mScans.containsKey(scanId)) {
651
+ // The scan may have been stopped while resolving
346
652
  sendScanEvent(ZeroconfModule.EVENT_RESOLVE, serviceInfoToMap(serviceInfo), scanId);
347
653
  }
348
654
  onResolveDone();
@@ -352,9 +658,50 @@ public class NsdServiceImpl implements Zeroconf {
352
658
  private class ServiceRegistrationListener implements NsdManager.RegistrationListener {
353
659
  @Nullable private Promise registerPromise;
354
660
  @Nullable private Promise unregisterPromise;
355
-
356
- ServiceRegistrationListener(@Nullable Promise registerPromise) {
661
+ // What it was registered with, to register it again on updates
662
+ final String serviceType;
663
+ final int port;
664
+ @Nullable final Network network;
665
+ // Updates replace the registration without emitting unpublished and published
666
+ boolean announceRegistered = true;
667
+ boolean announceUnregistered = true;
668
+ @Nullable Runnable afterUnregistered;
669
+ // The registration callback doesn't carry the TXT record, report the one registered
670
+ private final Map<String, String> txt = new java.util.LinkedHashMap<>();
671
+
672
+ ServiceRegistrationListener(@Nullable Promise registerPromise, String serviceType, int port, @Nullable Network network) {
357
673
  this.registerPromise = registerPromise;
674
+ this.serviceType = serviceType;
675
+ this.port = port;
676
+ this.network = network;
677
+ }
678
+
679
+ NsdServiceInfo buildServiceInfo(String name, ReadableArray txt) {
680
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
681
+ serviceInfo.setServiceName(name);
682
+ serviceInfo.setServiceType(serviceType);
683
+ serviceInfo.setPort(port);
684
+ if (network != null && Build.VERSION.SDK_INT >= 33) {
685
+ serviceInfo.setNetwork(network);
686
+ }
687
+ this.txt.clear();
688
+ for (int i = 0; i < txt.size(); i++) {
689
+ ReadableArray pair = txt.getArray(i);
690
+ serviceInfo.setAttribute(pair.getString(0), pair.getString(1));
691
+ this.txt.put(pair.getString(0), pair.getString(1));
692
+ }
693
+ return serviceInfo;
694
+ }
695
+
696
+ WritableMap registeredToMap(NsdServiceInfo serviceInfo) {
697
+ WritableMap service = serviceInfoToMap(serviceInfo);
698
+ WritableMap txtMap = new WritableNativeMap();
699
+ for (Map.Entry<String, String> entry : txt.entrySet()) {
700
+ txtMap.putString(entry.getKey(), entry.getValue());
701
+ }
702
+ service.putMap(ZeroconfModule.KEY_SERVICE_TXT, txtMap);
703
+ service.putInt(ZeroconfModule.KEY_SERVICE_PORT, port);
704
+ return service;
358
705
  }
359
706
 
360
707
  @Override
@@ -365,11 +712,13 @@ public class NsdServiceImpl implements Zeroconf {
365
712
 
366
713
  final String serviceName = NsdServiceInfo.getServiceName();
367
714
  mPublishedServices.put(serviceName, this);
715
+ syncLocalServiceTypes();
368
716
 
369
- WritableMap service = serviceInfoToMap(NsdServiceInfo);
370
- zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_PUBLISHED, service);
717
+ if (announceRegistered) {
718
+ zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_PUBLISHED, registeredToMap(NsdServiceInfo));
719
+ }
371
720
  if (registerPromise != null) {
372
- registerPromise.resolve(serviceInfoToMap(NsdServiceInfo));
721
+ registerPromise.resolve(registeredToMap(NsdServiceInfo));
373
722
  registerPromise = null;
374
723
  }
375
724
  }
@@ -386,10 +735,15 @@ public class NsdServiceImpl implements Zeroconf {
386
735
  public void onServiceUnregistered(NsdServiceInfo nsdServiceInfo) {
387
736
  // Service has been unregistered. This only happens when you call
388
737
  // NsdManager.unregisterService() and pass in this listener.
389
- final WritableMap service = serviceInfoToMap(nsdServiceInfo);
390
- zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_UNREGISTERED, service);
738
+ if (announceUnregistered) {
739
+ zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_UNREGISTERED, registeredToMap(nsdServiceInfo));
740
+ }
741
+ if (afterUnregistered != null) {
742
+ afterUnregistered.run();
743
+ afterUnregistered = null;
744
+ }
391
745
  if (unregisterPromise != null) {
392
- unregisterPromise.resolve(serviceInfoToMap(nsdServiceInfo));
746
+ unregisterPromise.resolve(registeredToMap(nsdServiceInfo));
393
747
  unregisterPromise = null;
394
748
  }
395
749
  }