react-native-zeroconf 0.16.1 → 0.17.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.
Files changed (44) hide show
  1. package/NOTICE +0 -3
  2. package/README.md +1 -1
  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 +72 -0
  8. package/android/src/main/java/com/balthazargronon/RCTZeroconf/dnssd/DnssdImpl.java +633 -0
  9. package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/NsdServiceImpl.java +331 -34
  10. package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/ServiceTypesQuery.java +296 -0
  11. package/android/src/main/java/com/github/druk/dnssd/DNSSDEmbedded.java +1 -1
  12. package/android/src/main/jni/mdnsresponder/mDNSShared/dnssd_clientshim.c +59 -22
  13. package/dist/index.js +234 -48
  14. package/index.d.ts +101 -6
  15. package/ios/RNZeroconf/RNZeroconf.m +285 -22
  16. package/package.json +28 -4
  17. package/src/index.js +214 -44
  18. package/windows/ExperimentalFeatures.props +33 -0
  19. package/windows/NuGet.config +11 -0
  20. package/windows/RNZeroconf/RNZeroconf.cpp +227 -0
  21. package/windows/RNZeroconf/RNZeroconf.def +3 -0
  22. package/windows/RNZeroconf/RNZeroconf.h +70 -0
  23. package/windows/RNZeroconf/RNZeroconf.rc +0 -0
  24. package/windows/RNZeroconf/RNZeroconf.vcxproj +150 -0
  25. package/windows/RNZeroconf/RNZeroconf.vcxproj.filters +50 -0
  26. package/windows/RNZeroconf/ReactPackageProvider.cpp +20 -0
  27. package/windows/RNZeroconf/ReactPackageProvider.h +24 -0
  28. package/windows/RNZeroconf/ReactPackageProvider.idl +9 -0
  29. package/windows/RNZeroconf/ZeroconfCore.cpp +1042 -0
  30. package/windows/RNZeroconf/ZeroconfCore.h +165 -0
  31. package/windows/RNZeroconf/pch.cpp +1 -0
  32. package/windows/RNZeroconf/pch.h +30 -0
  33. package/windows/RNZeroconf/resource.h +5 -0
  34. package/windows/RNZeroconf/targetver.h +8 -0
  35. package/windows/RNZeroconf.sln +43 -0
  36. package/android/src/main/java/com/balthazargronon/RCTZeroconf/rx2dnssd/DnssdImpl.java +0 -305
  37. package/android/src/main/java/com/github/druk/rx2dnssd/BonjourService.java +0 -400
  38. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2BrowseListener.java +0 -55
  39. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2Dnssd.java +0 -127
  40. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2DnssdCommon.java +0 -280
  41. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2DnssdEmbedded.java +0 -31
  42. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2QueryListener.java +0 -70
  43. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2RegisterListener.java +0 -47
  44. 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;
@@ -62,6 +66,12 @@ public class NsdServiceImpl implements Zeroconf {
62
66
  NsdManager.DiscoveryListener discoveryListener;
63
67
  // Android 14+: services are followed with registerServiceInfoCallback, null before
64
68
  @Nullable ServiceInfoCallbacks infoCallbacks;
69
+ // Service type scans query the network directly, NsdManager can't list types
70
+ @Nullable ServiceTypesQuery typesQuery;
71
+ // Types listed, and those answered by the network: the others are this app's own services,
72
+ // which the system mDNS stack doesn't answer back
73
+ final Set<String> listedTypes = new HashSet<>();
74
+ final Set<String> networkTypes = new HashSet<>();
65
75
 
66
76
  NsdScan(String scanId) {
67
77
  this.scanId = scanId;
@@ -69,12 +79,15 @@ public class NsdServiceImpl implements Zeroconf {
69
79
  }
70
80
 
71
81
  private static class PendingResolve {
72
- final String scanId;
82
+ @Nullable final String scanId;
73
83
  final NsdServiceInfo serviceInfo;
84
+ // resolveService(): settled instead of emitting events
85
+ @Nullable final Promise promise;
74
86
 
75
- PendingResolve(String scanId, NsdServiceInfo serviceInfo) {
87
+ PendingResolve(@Nullable String scanId, NsdServiceInfo serviceInfo, @Nullable Promise promise) {
76
88
  this.scanId = scanId;
77
89
  this.serviceInfo = serviceInfo;
90
+ this.promise = promise;
78
91
  }
79
92
  }
80
93
 
@@ -85,13 +98,60 @@ public class NsdServiceImpl implements Zeroconf {
85
98
  }
86
99
 
87
100
  @Override
88
- public void scan(final String scanId, String type, String protocol, String domain) {
101
+ public void scan(final String scanId, String type, String protocol, String domain, ZeroconfOptions options) {
89
102
  final NsdManager nsdManager = getNsdManager();
90
103
 
91
104
  this.stop(scanId);
92
- acquireMulticastLock();
93
105
 
94
106
  final NsdScan scan = new NsdScan(scanId);
107
+ final boolean typesOnly = ZeroconfModule.isServiceTypesScan(type, protocol);
108
+
109
+ Network network = null;
110
+ if (options.networkInterface != null && !typesOnly) {
111
+ String networkError = checkNetworkSupport(options.networkInterface);
112
+ network = networkError == null ? ZeroconfModule.findNetwork(reactApplicationContext, options.networkInterface) : null;
113
+ if (network == null) {
114
+ sendNetworkError(networkError, options.networkInterface, null, scanId);
115
+ return;
116
+ }
117
+ }
118
+ acquireMulticastLock();
119
+
120
+ if (typesOnly) {
121
+ scan.typesQuery = new ServiceTypesQuery(options.networkInterface, new ServiceTypesQuery.Listener() {
122
+ @Override
123
+ public void onTypeFound(String serviceType) {
124
+ synchronized (scan) {
125
+ scan.networkTypes.add(serviceType);
126
+ if (!scan.listedTypes.add(serviceType)) {
127
+ return;
128
+ }
129
+ }
130
+ sendScanEvent(ZeroconfModule.EVENT_FOUND, nameToMap(serviceType), scanId);
131
+ }
132
+
133
+ @Override
134
+ public void onTypeLost(String serviceType) {
135
+ synchronized (scan) {
136
+ scan.networkTypes.remove(serviceType);
137
+ if (localServiceTypes().contains(serviceType) || !scan.listedTypes.remove(serviceType)) {
138
+ return;
139
+ }
140
+ }
141
+ sendScanEvent(ZeroconfModule.EVENT_REMOVE, nameToMap(serviceType), scanId);
142
+ }
143
+
144
+ @Override
145
+ public void onError(String message) {
146
+ zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_LIBRARY, ZeroconfModule.ERROR_CODE_EXCEPTION, "Listing service types failed: " + message, null, scanId);
147
+ }
148
+ });
149
+ mScans.put(scanId, scan);
150
+ scan.typesQuery.start();
151
+ sendScanEvent(ZeroconfModule.EVENT_START, new WritableNativeMap(), scanId);
152
+ syncLocalServiceTypes();
153
+ return;
154
+ }
95
155
 
96
156
  if (Build.VERSION.SDK_INT >= 34) {
97
157
  scan.infoCallbacks = new ServiceInfoCallbacks(nsdManager, mCallbackExecutor, new ServiceInfoCallbacks.Listener() {
@@ -159,8 +219,34 @@ public class NsdServiceImpl implements Zeroconf {
159
219
  };
160
220
 
161
221
  mScans.put(scanId, scan);
162
- String serviceType = String.format("_%s._%s.", type, protocol);
163
- nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, scan.discoveryListener);
222
+ // "_ipp._tcp,_printer" browses the _printer subtype
223
+ String serviceType = options.subtype != null
224
+ ? ZeroconfOptions.withSubtypes(String.format("_%s._%s", type, protocol), Collections.singletonList(options.subtype))
225
+ : String.format("_%s._%s.", type, protocol);
226
+ if (network != null && Build.VERSION.SDK_INT >= 33) {
227
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, network, mCallbackExecutor, scan.discoveryListener);
228
+ } else {
229
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, scan.discoveryListener);
230
+ }
231
+ }
232
+
233
+ // Choosing the network of NSD requests needs Android 13, null when supported
234
+ @Nullable
235
+ private static String checkNetworkSupport(String networkInterface) {
236
+ return Build.VERSION.SDK_INT >= 33 ? null : ZeroconfModule.ERROR_CODE_UNSUPPORTED;
237
+ }
238
+
239
+ private void sendNetworkError(@Nullable String code, String networkInterface, @Nullable Promise promise, @Nullable String scanId) {
240
+ boolean unsupported = ZeroconfModule.ERROR_CODE_UNSUPPORTED.equals(code);
241
+ String errorCode = unsupported ? ZeroconfModule.ERROR_CODE_UNSUPPORTED : ZeroconfModule.ERROR_CODE_UNKNOWN_INTERFACE;
242
+ String message = unsupported
243
+ ? "Choosing the network interface needs Android 13 or later with NSD, use DNSSD"
244
+ : "Unknown network interface " + networkInterface;
245
+ if (promise != null) {
246
+ ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_LIBRARY, errorCode, message, null);
247
+ } else {
248
+ zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_LIBRARY, errorCode, message, null, scanId);
249
+ }
164
250
  }
165
251
 
166
252
  @Override
@@ -183,6 +269,12 @@ public class NsdServiceImpl implements Zeroconf {
183
269
  scan.infoCallbacks.unregisterAll();
184
270
  }
185
271
 
272
+ if (scan.typesQuery != null) {
273
+ scan.typesQuery.stop();
274
+ // No discovery listener reports it
275
+ sendScanEvent(ZeroconfModule.EVENT_STOP, new WritableNativeMap(), scanId);
276
+ }
277
+
186
278
  synchronized (mResolveQueue) {
187
279
  Iterator<PendingResolve> iterator = mResolveQueue.iterator();
188
280
  while (iterator.hasNext()) {
@@ -222,6 +314,12 @@ public class NsdServiceImpl implements Zeroconf {
222
314
  zeroconfModule.sendEvent(getReactApplicationContext(), eventName, body);
223
315
  }
224
316
 
317
+ private static WritableMap nameToMap(String name) {
318
+ WritableMap service = new WritableNativeMap();
319
+ service.putString(ZeroconfModule.KEY_SERVICE_NAME, name);
320
+ return service;
321
+ }
322
+
225
323
  private static WritableMap serviceNameToMap(NsdServiceInfo serviceInfo) {
226
324
  WritableMap service = new WritableNativeMap();
227
325
  service.putString(ZeroconfModule.KEY_SERVICE_NAME, serviceInfo.getServiceName());
@@ -229,22 +327,119 @@ public class NsdServiceImpl implements Zeroconf {
229
327
  }
230
328
 
231
329
  @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);
330
+ public void registerService(String type, String protocol, String domain, String name, int port, ReadableArray txt, ZeroconfOptions options, Promise promise) {
331
+ Network network = null;
332
+ if (options.networkInterface != null) {
333
+ String networkError = checkNetworkSupport(options.networkInterface);
334
+ network = networkError == null ? ZeroconfModule.findNetwork(reactApplicationContext, options.networkInterface) : null;
335
+ if (network == null) {
336
+ sendNetworkError(networkError, options.networkInterface, promise, null);
337
+ return;
338
+ }
339
+ }
340
+ // "_ipp._tcp,_printer,_color" registers the subtypes too
341
+ String serviceType = options.subtypes.isEmpty()
342
+ ? String.format("_%s._%s.", type, protocol)
343
+ : ZeroconfOptions.withSubtypes(String.format("_%s._%s", type, protocol), options.subtypes);
344
+ ServiceRegistrationListener listener = new ServiceRegistrationListener(promise, serviceType, port, network);
345
+ getNsdManager().registerService(listener.buildServiceInfo(name, txt), NsdManager.PROTOCOL_DNS_SD, listener);
346
+ }
234
347
 
235
- final NsdManager nsdManager = this.getNsdManager();
236
- NsdServiceInfo serviceInfo = new NsdServiceInfo();
348
+ /**
349
+ * NsdManager can't update a registration: unregisters and registers again with the new TXT record,
350
+ * without emitting unpublished and published
351
+ */
352
+ @Override
353
+ public void updateService(String serviceName, final ReadableArray txt, final Promise promise) {
354
+ final ServiceRegistrationListener previous = mPublishedServices.remove(serviceName);
355
+ if (previous == null) {
356
+ ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_LIBRARY, ZeroconfModule.ERROR_CODE_NOT_PUBLISHED, "Service " + serviceName + " is not published", serviceName);
357
+ return;
358
+ }
359
+ final ServiceRegistrationListener next = new ServiceRegistrationListener(promise, previous.serviceType, previous.port, previous.network);
360
+ next.announceRegistered = false;
361
+ final NsdServiceInfo serviceInfo = next.buildServiceInfo(serviceName, txt);
362
+ previous.announceUnregistered = false;
363
+ previous.afterUnregistered = () -> getNsdManager().registerService(serviceInfo, NsdManager.PROTOCOL_DNS_SD, next);
364
+ getNsdManager().unregisterService(previous);
365
+ }
366
+
367
+ @Override
368
+ public void resolveService(String name, String type, String protocol, String domain, ZeroconfOptions options, final Promise promise) {
369
+ final NsdServiceInfo serviceInfo = new NsdServiceInfo();
237
370
  serviceInfo.setServiceName(name);
238
- serviceInfo.setServiceType(serviceType);
239
- serviceInfo.setPort(port);
371
+ serviceInfo.setServiceType(String.format("_%s._%s", type, protocol));
372
+ if (options.networkInterface != null) {
373
+ String networkError = checkNetworkSupport(options.networkInterface);
374
+ Network network = networkError == null ? ZeroconfModule.findNetwork(reactApplicationContext, options.networkInterface) : null;
375
+ if (network == null) {
376
+ sendNetworkError(networkError, options.networkInterface, promise, null);
377
+ return;
378
+ }
379
+ if (Build.VERSION.SDK_INT >= 33) {
380
+ serviceInfo.setNetwork(network);
381
+ }
382
+ }
240
383
 
241
- for (int i = 0; i < txt.size(); i++) {
242
- ReadableArray pair = txt.getArray(i);
243
- serviceInfo.setAttribute(pair.getString(0), pair.getString(1));
384
+ if (Build.VERSION.SDK_INT < 34) {
385
+ // One resolve at a time before Android 14, shares the queue of the scans
386
+ synchronized (mResolveQueue) {
387
+ mResolveQueue.add(new PendingResolve(null, serviceInfo, promise));
388
+ }
389
+ resolveNext();
390
+ return;
244
391
  }
245
392
 
246
- nsdManager.registerService(
247
- serviceInfo, NsdManager.PROTOCOL_DNS_SD, new ServiceRegistrationListener(promise));
393
+ final boolean[] settled = { false };
394
+ final NsdManager nsdManager = getNsdManager();
395
+ final NsdManager.ServiceInfoCallback[] callback = new NsdManager.ServiceInfoCallback[1];
396
+ final Runnable finish = () -> {
397
+ try {
398
+ nsdManager.unregisterServiceInfoCallback(callback[0]);
399
+ } catch (IllegalArgumentException e) {
400
+ // Already unregistered
401
+ }
402
+ };
403
+ callback[0] = new NsdManager.ServiceInfoCallback() {
404
+ @Override
405
+ public void onServiceInfoCallbackRegistrationFailed(int errorCode) {
406
+ synchronized (settled) {
407
+ if (settled[0]) return;
408
+ settled[0] = true;
409
+ }
410
+ ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_NSD, errorCode, "Resolving service " + name + " failed: " + ZeroconfModule.describeNsdError(errorCode), name);
411
+ }
412
+
413
+ @Override
414
+ public void onServiceUpdated(NsdServiceInfo updated) {
415
+ if (getHostAddresses(updated).isEmpty()) {
416
+ return;
417
+ }
418
+ synchronized (settled) {
419
+ if (settled[0]) return;
420
+ settled[0] = true;
421
+ }
422
+ promise.resolve(serviceInfoToMap(updated));
423
+ mHandler.post(finish);
424
+ }
425
+
426
+ @Override
427
+ public void onServiceLost() {
428
+ }
429
+
430
+ @Override
431
+ public void onServiceInfoCallbackUnregistered() {
432
+ }
433
+ };
434
+ nsdManager.registerServiceInfoCallback(serviceInfo, mCallbackExecutor, callback[0]);
435
+ mHandler.postDelayed(() -> {
436
+ synchronized (settled) {
437
+ if (settled[0]) return;
438
+ settled[0] = true;
439
+ }
440
+ finish.run();
441
+ ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_LIBRARY, ZeroconfModule.ERROR_CODE_TIMEOUT, "Resolving service " + name + " failed: timed out", name);
442
+ }, (long) (options.timeoutSeconds * 1000));
248
443
  }
249
444
 
250
445
  @Override
@@ -262,6 +457,53 @@ public class NsdServiceImpl implements Zeroconf {
262
457
  mPublishedServices.remove(serviceName);
263
458
  serviceListener.unregisterPromise = promise;
264
459
  nsdManager.unregisterService(serviceListener);
460
+ syncLocalServiceTypes();
461
+ }
462
+
463
+ // "_ipp._tcp,_printer" or "_http._tcp." -> "_ipp._tcp", "_http._tcp"
464
+ private static String baseServiceType(String serviceType) {
465
+ int comma = serviceType.indexOf(',');
466
+ String type = comma >= 0 ? serviceType.substring(0, comma) : serviceType;
467
+ return type.endsWith(".") ? type.substring(0, type.length() - 1) : type;
468
+ }
469
+
470
+ private Set<String> localServiceTypes() {
471
+ Set<String> types = new HashSet<>();
472
+ for (ServiceRegistrationListener listener : mPublishedServices.values()) {
473
+ types.add(baseServiceType(listener.serviceType));
474
+ }
475
+ return types;
476
+ }
477
+
478
+ // Lists the types this app publishes in service type scans, and removes those it no longer publishes
479
+ private void syncLocalServiceTypes() {
480
+ Set<String> local = localServiceTypes();
481
+ for (NsdScan scan : mScans.values()) {
482
+ if (scan.typesQuery == null) {
483
+ continue;
484
+ }
485
+ List<String> found = new ArrayList<>();
486
+ List<String> removed = new ArrayList<>();
487
+ synchronized (scan) {
488
+ for (String type : local) {
489
+ if (scan.listedTypes.add(type)) {
490
+ found.add(type);
491
+ }
492
+ }
493
+ for (String type : new ArrayList<>(scan.listedTypes)) {
494
+ if (!local.contains(type) && !scan.networkTypes.contains(type)) {
495
+ scan.listedTypes.remove(type);
496
+ removed.add(type);
497
+ }
498
+ }
499
+ }
500
+ for (String type : found) {
501
+ sendScanEvent(ZeroconfModule.EVENT_FOUND, nameToMap(type), scan.scanId);
502
+ }
503
+ for (String type : removed) {
504
+ sendScanEvent(ZeroconfModule.EVENT_REMOVE, nameToMap(type), scan.scanId);
505
+ }
506
+ }
265
507
  }
266
508
 
267
509
  @Override
@@ -284,7 +526,7 @@ public class NsdServiceImpl implements Zeroconf {
284
526
 
285
527
  private void enqueueResolve(String scanId, NsdServiceInfo serviceInfo) {
286
528
  synchronized (mResolveQueue) {
287
- mResolveQueue.add(new PendingResolve(scanId, serviceInfo));
529
+ mResolveQueue.add(new PendingResolve(scanId, serviceInfo, null));
288
530
  }
289
531
  resolveNext();
290
532
  }
@@ -299,7 +541,7 @@ public class NsdServiceImpl implements Zeroconf {
299
541
  mIsResolving = true;
300
542
  }
301
543
  try {
302
- getNsdManager().resolveService(next.serviceInfo, new ZeroResolveListener(next.scanId));
544
+ getNsdManager().resolveService(next.serviceInfo, new ZeroResolveListener(next.scanId, next.promise));
303
545
  } catch (Throwable e) {
304
546
  Log.e(TAG, "resolveService failed", e);
305
547
  String serviceName = next.serviceInfo.getServiceName();
@@ -316,10 +558,12 @@ public class NsdServiceImpl implements Zeroconf {
316
558
  }
317
559
 
318
560
  private class ZeroResolveListener implements NsdManager.ResolveListener {
319
- private final String scanId;
561
+ @Nullable private final String scanId;
562
+ @Nullable private final Promise promise;
320
563
 
321
- ZeroResolveListener(String scanId) {
564
+ ZeroResolveListener(@Nullable String scanId, @Nullable Promise promise) {
322
565
  this.scanId = scanId;
566
+ this.promise = promise;
323
567
  }
324
568
 
325
569
  @Override
@@ -327,22 +571,27 @@ public class NsdServiceImpl implements Zeroconf {
327
571
  if (errorCode == NsdManager.FAILURE_ALREADY_ACTIVE) {
328
572
  // Another resolve (possibly from another app or listener) is in flight: retry shortly
329
573
  synchronized (mResolveQueue) {
330
- mResolveQueue.addFirst(new PendingResolve(scanId, serviceInfo));
574
+ mResolveQueue.addFirst(new PendingResolve(scanId, serviceInfo, promise));
331
575
  mIsResolving = false;
332
576
  }
333
577
  mHandler.postDelayed(NsdServiceImpl.this::resolveNext, RESOLVE_RETRY_DELAY_MS);
334
578
  return;
335
579
  }
336
- if (mScans.containsKey(scanId)) {
337
- zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_NSD, errorCode, "Resolving service " + serviceInfo.getServiceName() + " failed: " + ZeroconfModule.describeNsdError(errorCode), serviceInfo.getServiceName(), scanId);
580
+ String message = "Resolving service " + serviceInfo.getServiceName() + " failed: " + ZeroconfModule.describeNsdError(errorCode);
581
+ if (promise != null) {
582
+ ZeroconfModule.reject(promise, ZeroconfModule.ERROR_DOMAIN_NSD, errorCode, message, serviceInfo.getServiceName());
583
+ } else if (mScans.containsKey(scanId)) {
584
+ zeroconfModule.sendError(ZeroconfModule.ERROR_DOMAIN_NSD, errorCode, message, serviceInfo.getServiceName(), scanId);
338
585
  }
339
586
  onResolveDone();
340
587
  }
341
588
 
342
589
  @Override
343
590
  public void onServiceResolved(NsdServiceInfo serviceInfo) {
344
- // The scan may have been stopped while resolving
345
- if (mScans.containsKey(scanId)) {
591
+ if (promise != null) {
592
+ promise.resolve(serviceInfoToMap(serviceInfo));
593
+ } else if (mScans.containsKey(scanId)) {
594
+ // The scan may have been stopped while resolving
346
595
  sendScanEvent(ZeroconfModule.EVENT_RESOLVE, serviceInfoToMap(serviceInfo), scanId);
347
596
  }
348
597
  onResolveDone();
@@ -352,9 +601,50 @@ public class NsdServiceImpl implements Zeroconf {
352
601
  private class ServiceRegistrationListener implements NsdManager.RegistrationListener {
353
602
  @Nullable private Promise registerPromise;
354
603
  @Nullable private Promise unregisterPromise;
355
-
356
- ServiceRegistrationListener(@Nullable Promise registerPromise) {
604
+ // What it was registered with, to register it again on updates
605
+ final String serviceType;
606
+ final int port;
607
+ @Nullable final Network network;
608
+ // Updates replace the registration without emitting unpublished and published
609
+ boolean announceRegistered = true;
610
+ boolean announceUnregistered = true;
611
+ @Nullable Runnable afterUnregistered;
612
+ // The registration callback doesn't carry the TXT record, report the one registered
613
+ private final Map<String, String> txt = new java.util.LinkedHashMap<>();
614
+
615
+ ServiceRegistrationListener(@Nullable Promise registerPromise, String serviceType, int port, @Nullable Network network) {
357
616
  this.registerPromise = registerPromise;
617
+ this.serviceType = serviceType;
618
+ this.port = port;
619
+ this.network = network;
620
+ }
621
+
622
+ NsdServiceInfo buildServiceInfo(String name, ReadableArray txt) {
623
+ NsdServiceInfo serviceInfo = new NsdServiceInfo();
624
+ serviceInfo.setServiceName(name);
625
+ serviceInfo.setServiceType(serviceType);
626
+ serviceInfo.setPort(port);
627
+ if (network != null && Build.VERSION.SDK_INT >= 33) {
628
+ serviceInfo.setNetwork(network);
629
+ }
630
+ this.txt.clear();
631
+ for (int i = 0; i < txt.size(); i++) {
632
+ ReadableArray pair = txt.getArray(i);
633
+ serviceInfo.setAttribute(pair.getString(0), pair.getString(1));
634
+ this.txt.put(pair.getString(0), pair.getString(1));
635
+ }
636
+ return serviceInfo;
637
+ }
638
+
639
+ WritableMap registeredToMap(NsdServiceInfo serviceInfo) {
640
+ WritableMap service = serviceInfoToMap(serviceInfo);
641
+ WritableMap txtMap = new WritableNativeMap();
642
+ for (Map.Entry<String, String> entry : txt.entrySet()) {
643
+ txtMap.putString(entry.getKey(), entry.getValue());
644
+ }
645
+ service.putMap(ZeroconfModule.KEY_SERVICE_TXT, txtMap);
646
+ service.putInt(ZeroconfModule.KEY_SERVICE_PORT, port);
647
+ return service;
358
648
  }
359
649
 
360
650
  @Override
@@ -365,11 +655,13 @@ public class NsdServiceImpl implements Zeroconf {
365
655
 
366
656
  final String serviceName = NsdServiceInfo.getServiceName();
367
657
  mPublishedServices.put(serviceName, this);
658
+ syncLocalServiceTypes();
368
659
 
369
- WritableMap service = serviceInfoToMap(NsdServiceInfo);
370
- zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_PUBLISHED, service);
660
+ if (announceRegistered) {
661
+ zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_PUBLISHED, registeredToMap(NsdServiceInfo));
662
+ }
371
663
  if (registerPromise != null) {
372
- registerPromise.resolve(serviceInfoToMap(NsdServiceInfo));
664
+ registerPromise.resolve(registeredToMap(NsdServiceInfo));
373
665
  registerPromise = null;
374
666
  }
375
667
  }
@@ -386,10 +678,15 @@ public class NsdServiceImpl implements Zeroconf {
386
678
  public void onServiceUnregistered(NsdServiceInfo nsdServiceInfo) {
387
679
  // Service has been unregistered. This only happens when you call
388
680
  // NsdManager.unregisterService() and pass in this listener.
389
- final WritableMap service = serviceInfoToMap(nsdServiceInfo);
390
- zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_UNREGISTERED, service);
681
+ if (announceUnregistered) {
682
+ zeroconfModule.sendEvent(getReactApplicationContext(), ZeroconfModule.EVENT_UNREGISTERED, registeredToMap(nsdServiceInfo));
683
+ }
684
+ if (afterUnregistered != null) {
685
+ afterUnregistered.run();
686
+ afterUnregistered = null;
687
+ }
391
688
  if (unregisterPromise != null) {
392
- unregisterPromise.resolve(serviceInfoToMap(nsdServiceInfo));
689
+ unregisterPromise.resolve(registeredToMap(nsdServiceInfo));
393
690
  unregisterPromise = null;
394
691
  }
395
692
  }