@@ -3,6 +3,8 @@ package com.servicediscovery
33import android.content.Context
44import android.net.nsd.NsdManager
55import android.net.nsd.NsdServiceInfo
6+ import android.os.Handler
7+ import android.os.Looper
68import android.util.Log
79import com.facebook.react.bridge.Promise
810import com.facebook.react.bridge.ReactApplicationContext
@@ -12,6 +14,7 @@ import com.facebook.react.bridge.WritableNativeArray
1214import com.facebook.react.bridge.WritableNativeMap
1315import com.facebook.react.modules.core.DeviceEventManagerModule
1416import java.nio.charset.StandardCharsets
17+ import java.util.LinkedList
1518
1619
1720class ServiceDiscoveryModule internal constructor(context : ReactApplicationContext ) :
@@ -24,6 +27,21 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
2427 // Keep track of resolved services, because onServiceLost doesn't pass a resolved service
2528 private val resolvedServices: MutableMap <String , NsdServiceInfo > = HashMap ()
2629
30+ // Resolve queue to handle Android's single-resolve limitation
31+ private data class ResolveRequest (val service : NsdServiceInfo , var retryCount : Int = 0 )
32+ private val resolveQueue: LinkedList <ResolveRequest > = LinkedList ()
33+ private var isResolving: Boolean = false
34+ private val mainHandler = Handler (Looper .getMainLooper())
35+ private val pendingServices: MutableSet <String > = HashSet ()
36+
37+ companion object {
38+ private const val SERVICE_FOUND = " serviceFound"
39+ private const val SERVICE_LOST = " serviceLost"
40+ private const val MAX_RETRY_COUNT = 3
41+ private const val RETRY_DELAY_MS = 100L
42+ const val NAME = " ServiceDiscovery"
43+ }
44+
2745 override fun getName (): String {
2846 return NAME
2947 }
@@ -42,6 +60,86 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
4260 override fun removeListeners (count : Double ) {
4361 }
4462
63+ private fun queueServiceForResolve (service : NsdServiceInfo ) {
64+ val serviceKey = " ${service.serviceName} -${service.serviceType} "
65+ synchronized(resolveQueue) {
66+ // Skip if already pending or resolved
67+ if (pendingServices.contains(serviceKey) || resolvedServices.containsKey(service.serviceName)) {
68+ return
69+ }
70+ pendingServices.add(serviceKey)
71+ resolveQueue.add(ResolveRequest (service))
72+ Log .d(NAME , " Queued service for resolve: ${service.serviceName} (queue size: ${resolveQueue.size} )" )
73+ }
74+ processResolveQueue()
75+ }
76+
77+ private fun processResolveQueue () {
78+ synchronized(resolveQueue) {
79+ if (isResolving || resolveQueue.isEmpty()) {
80+ return
81+ }
82+ isResolving = true
83+ }
84+
85+ val request = synchronized(resolveQueue) { resolveQueue.poll() } ? : run {
86+ synchronized(resolveQueue) { isResolving = false }
87+ return
88+ }
89+
90+ Log .d(NAME , " Resolving service: ${request.service.serviceName} (attempt ${request.retryCount + 1 } )" )
91+
92+ try {
93+ nsdManager.resolveService(request.service, object : NsdManager .ResolveListener {
94+ override fun onServiceResolved (serviceInfo : NsdServiceInfo ) {
95+ try {
96+ val serviceKey = " ${serviceInfo.serviceName} -${serviceInfo.serviceType} "
97+ synchronized(resolveQueue) {
98+ pendingServices.remove(serviceKey)
99+ }
100+ resolvedServices[serviceInfo.serviceName] = serviceInfo
101+ sendEvent(SERVICE_FOUND , serviceToMap(serviceInfo))
102+ Log .d(NAME , " Resolved service: ${serviceInfo.serviceName} " )
103+ } catch (e: Exception ) {
104+ Log .e(NAME , " Error handling resolved service" , e)
105+ } finally {
106+ synchronized(resolveQueue) { isResolving = false }
107+ // Small delay before processing next to avoid overwhelming NsdManager
108+ mainHandler.postDelayed({ processResolveQueue() }, 50 )
109+ }
110+ }
111+
112+ override fun onResolveFailed (serviceInfo : NsdServiceInfo , errorCode : Int ) {
113+ Log .e(NAME , " Resolve failed for ${serviceInfo.serviceName} : errorCode=$errorCode " )
114+
115+ val serviceKey = " ${serviceInfo.serviceName} -${serviceInfo.serviceType} "
116+
117+ // Retry on FAILURE_ALREADY_ACTIVE (3) or other transient errors
118+ if (request.retryCount < MAX_RETRY_COUNT ) {
119+ request.retryCount++
120+ synchronized(resolveQueue) {
121+ resolveQueue.add(request) // Re-queue for retry
122+ }
123+ Log .d(NAME , " Re-queued ${serviceInfo.serviceName} for retry (attempt ${request.retryCount} )" )
124+ } else {
125+ synchronized(resolveQueue) {
126+ pendingServices.remove(serviceKey)
127+ }
128+ Log .e(NAME , " Giving up on ${serviceInfo.serviceName} after ${MAX_RETRY_COUNT } retries" )
129+ }
130+
131+ synchronized(resolveQueue) { isResolving = false }
132+ // Longer delay after failure before retrying
133+ mainHandler.postDelayed({ processResolveQueue() }, RETRY_DELAY_MS )
134+ }
135+ })
136+ } catch (e: Exception ) {
137+ Log .e(NAME , " Exception starting resolve" , e)
138+ synchronized(resolveQueue) { isResolving = false }
139+ mainHandler.postDelayed({ processResolveQueue() }, RETRY_DELAY_MS )
140+ }
141+ }
142+
45143 @ReactMethod
46144 override fun startSearch (type : String , promise : Promise ) {
47145 try {
@@ -55,38 +153,22 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
55153 val discoveryListener: NsdManager .DiscoveryListener =
56154 object : NsdManager .DiscoveryListener {
57155 override fun onDiscoveryStarted (regType : String ) {
58- // Service discovery started
156+ Log .d( NAME , " Discovery started for $regType " )
59157 }
60158
61159 override fun onDiscoveryStopped (regType : String ) {
62- // Service discovery stopped
160+ Log .d( NAME , " Discovery stopped for $regType " )
63161 }
64162
65163 override fun onServiceFound (service : NsdServiceInfo ) {
66164 try {
67165 // Service discovery success
68166 if (service.serviceType == serviceType) {
69- // Resolve the service with ad-hoc listener
70- nsdManager.resolveService(service, object : NsdManager .ResolveListener {
71- override fun onServiceResolved (serviceInfo : NsdServiceInfo ) {
72- try {
73- resolvedServices[serviceInfo.serviceName] = serviceInfo
74- sendEvent(SERVICE_FOUND , serviceToMap(serviceInfo))
75- } catch (e: Exception ) {
76- Log .e(NAME , " Error resolving service" , e)
77- }
78- }
79-
80- override fun onResolveFailed (
81- serviceInfo : NsdServiceInfo ,
82- errorCode : Int
83- ) {
84- Log .e(NAME , " Resolve failed: $errorCode " )
85- }
86- })
167+ // Queue the service for resolution instead of resolving immediately
168+ queueServiceForResolve(service)
87169 }
88170 } catch (e: Exception ) {
89- Log .e(NAME , " Error resolving service" , e)
171+ Log .e(NAME , " Error queuing service for resolve " , e)
90172 }
91173 }
92174
@@ -101,24 +183,27 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
101183 sendEvent(SERVICE_LOST , serviceToMap(serviceInfo))
102184 }
103185 } catch (e: Exception ) {
104- Log .e(NAME , " Error resolving service" , e)
186+ Log .e(NAME , " Error handling service lost " , e)
105187 }
106188 }
107189
108190 override fun onStartDiscoveryFailed (serviceType : String , errorCode : Int ) {
191+ Log .e(NAME , " Discovery start failed: $errorCode " )
109192 tryStopServiceDiscovery(this )
110193 }
111194
112195 override fun onStopDiscoveryFailed (serviceType : String , errorCode : Int ) {
196+ Log .e(NAME , " Discovery stop failed: $errorCode " )
113197 tryStopServiceDiscovery(this )
114198 }
115199 }
116200 discoveryListeners[serviceType] = discoveryListener
201+
117202 nsdManager.discoverServices(serviceType, NsdManager .PROTOCOL_DNS_SD , discoveryListener)
118203 promise.resolve(null )
119204 return
120205 } catch (e: Exception ) {
121- Log .e(NAME , " Error resolving service " , e)
206+ Log .e(NAME , " Error starting search " , e)
122207 promise.reject(e)
123208 }
124209 }
@@ -127,16 +212,23 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
127212 override fun stopSearch (type : String , promise : Promise ) {
128213 try {
129214 resolvedServices.clear()
215+ synchronized(resolveQueue) {
216+ resolveQueue.clear()
217+ pendingServices.clear()
218+ isResolving = false
219+ }
220+
130221 val serviceType = getServiceType(type)
131222 val discoveryListener = discoveryListeners.remove(serviceType)
132223 if (discoveryListener != null ) {
133224 tryStopServiceDiscovery(discoveryListener)
134225 }
226+
227+ promise.resolve(null )
135228 } catch (e: Exception ) {
136- Log .e(NAME , " Error resolving service " , e)
229+ Log .e(NAME , " Error stopping search " , e)
137230 promise.reject(e)
138231 }
139-
140232 }
141233
142234 private fun tryStopServiceDiscovery (listener : NsdManager .DiscoveryListener ) {
@@ -178,14 +270,8 @@ class ServiceDiscoveryModule internal constructor(context: ReactApplicationConte
178270 }
179271 service.putMap(" txt" , txt)
180272 } catch (e: Exception ) {
181- Log .e(NAME , " Error stopping search " , e)
273+ Log .e(NAME , " Error converting service to map " , e)
182274 }
183275 return service
184276 }
185-
186- companion object {
187- private const val SERVICE_FOUND = " serviceFound"
188- private const val SERVICE_LOST = " serviceLost"
189- const val NAME = " ServiceDiscovery"
190- }
191277}
0 commit comments