@dawidzawada/bonjour-zeroconf 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.
Files changed (125) hide show
  1. package/BonjourZeroconf.podspec +30 -0
  2. package/LICENSE +20 -0
  3. package/README.md +35 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +128 -0
  6. package/android/gradle.properties +5 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf+AddressResolver.kt +182 -0
  10. package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf+Listeners.kt +45 -0
  11. package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf.kt +183 -0
  12. package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconfPackage.kt +22 -0
  13. package/ios/AddressResolverError.swift +20 -0
  14. package/ios/BonjourZeroconf+AddressResolver.swift +133 -0
  15. package/ios/BonjourZeroconf+Listeners.swift +26 -0
  16. package/ios/BonjourZeroconf.swift +201 -0
  17. package/ios/LocalNetworkAuthorization.swift +66 -0
  18. package/ios/LocalNetworkPermission+Listeners.swift +14 -0
  19. package/ios/LocalNetworkPermission.swift +41 -0
  20. package/ios/ServiceCache.swift +30 -0
  21. package/ios/Utils/Loggy.swift +37 -0
  22. package/lib/module/index.js +9 -0
  23. package/lib/module/index.js.map +1 -0
  24. package/lib/module/package.json +1 -0
  25. package/lib/module/permissions.ios.js +23 -0
  26. package/lib/module/permissions.ios.js.map +1 -0
  27. package/lib/module/permissions.js +9 -0
  28. package/lib/module/permissions.js.map +1 -0
  29. package/lib/module/specs/BonjourFail.js +9 -0
  30. package/lib/module/specs/BonjourFail.js.map +1 -0
  31. package/lib/module/specs/BonjourListener.js +2 -0
  32. package/lib/module/specs/BonjourListener.js.map +1 -0
  33. package/lib/module/specs/BonjourZeroconf.nitro.js +4 -0
  34. package/lib/module/specs/BonjourZeroconf.nitro.js.map +1 -0
  35. package/lib/module/specs/LocalNetworkPermission.nitro.js +4 -0
  36. package/lib/module/specs/LocalNetworkPermission.nitro.js.map +1 -0
  37. package/lib/module/specs/ScanResult.js +2 -0
  38. package/lib/module/specs/ScanResult.js.map +1 -0
  39. package/lib/module/useIsScanning.js +19 -0
  40. package/lib/module/useIsScanning.js.map +1 -0
  41. package/lib/typescript/package.json +1 -0
  42. package/lib/typescript/src/index.d.ts +9 -0
  43. package/lib/typescript/src/index.d.ts.map +1 -0
  44. package/lib/typescript/src/permissions.d.ts +3 -0
  45. package/lib/typescript/src/permissions.d.ts.map +1 -0
  46. package/lib/typescript/src/permissions.ios.d.ts +3 -0
  47. package/lib/typescript/src/permissions.ios.d.ts.map +1 -0
  48. package/lib/typescript/src/specs/BonjourFail.d.ts +6 -0
  49. package/lib/typescript/src/specs/BonjourFail.d.ts.map +1 -0
  50. package/lib/typescript/src/specs/BonjourListener.d.ts +4 -0
  51. package/lib/typescript/src/specs/BonjourListener.d.ts.map +1 -0
  52. package/lib/typescript/src/specs/BonjourZeroconf.nitro.d.ts +19 -0
  53. package/lib/typescript/src/specs/BonjourZeroconf.nitro.d.ts.map +1 -0
  54. package/lib/typescript/src/specs/LocalNetworkPermission.nitro.d.ts +9 -0
  55. package/lib/typescript/src/specs/LocalNetworkPermission.nitro.d.ts.map +1 -0
  56. package/lib/typescript/src/specs/ScanResult.d.ts +8 -0
  57. package/lib/typescript/src/specs/ScanResult.d.ts.map +1 -0
  58. package/lib/typescript/src/useIsScanning.d.ts +2 -0
  59. package/lib/typescript/src/useIsScanning.d.ts.map +1 -0
  60. package/nitro.json +20 -0
  61. package/nitrogen/generated/android/c++/JBonjourFail.hpp +62 -0
  62. package/nitrogen/generated/android/c++/JBonjourListener.hpp +68 -0
  63. package/nitrogen/generated/android/c++/JFunc_void.hpp +74 -0
  64. package/nitrogen/generated/android/c++/JFunc_void_BonjourFail.hpp +76 -0
  65. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +74 -0
  66. package/nitrogen/generated/android/c++/JFunc_void_std__vector_ScanResult_.hpp +97 -0
  67. package/nitrogen/generated/android/c++/JHybridBonjourZeroconfSpec.cpp +96 -0
  68. package/nitrogen/generated/android/c++/JHybridBonjourZeroconfSpec.hpp +69 -0
  69. package/nitrogen/generated/android/c++/JScanOptions.hpp +57 -0
  70. package/nitrogen/generated/android/c++/JScanResult.hpp +74 -0
  71. package/nitrogen/generated/android/dawidzawada_bonjourzeroconf+autolinking.cmake +82 -0
  72. package/nitrogen/generated/android/dawidzawada_bonjourzeroconf+autolinking.gradle +27 -0
  73. package/nitrogen/generated/android/dawidzawada_bonjourzeroconfOnLoad.cpp +52 -0
  74. package/nitrogen/generated/android/dawidzawada_bonjourzeroconfOnLoad.hpp +25 -0
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourFail.kt +22 -0
  76. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourListener.kt +42 -0
  77. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/Func_void.kt +80 -0
  78. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/Func_void_BonjourFail.kt +80 -0
  79. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/Func_void_bool.kt +80 -0
  80. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/Func_void_std__vector_ScanResult_.kt +80 -0
  81. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/HybridBonjourZeroconfSpec.kt +90 -0
  82. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/ScanOptions.kt +38 -0
  83. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/ScanResult.kt +50 -0
  84. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/dawidzawada_bonjourzeroconfOnLoad.kt +35 -0
  85. package/nitrogen/generated/ios/BonjourZeroconf+autolinking.rb +60 -0
  86. package/nitrogen/generated/ios/BonjourZeroconf-Swift-Cxx-Bridge.cpp +90 -0
  87. package/nitrogen/generated/ios/BonjourZeroconf-Swift-Cxx-Bridge.hpp +282 -0
  88. package/nitrogen/generated/ios/BonjourZeroconf-Swift-Cxx-Umbrella.hpp +65 -0
  89. package/nitrogen/generated/ios/BonjourZeroconfAutolinking.mm +41 -0
  90. package/nitrogen/generated/ios/BonjourZeroconfAutolinking.swift +40 -0
  91. package/nitrogen/generated/ios/c++/HybridBonjourZeroconfSpecSwift.cpp +11 -0
  92. package/nitrogen/generated/ios/c++/HybridBonjourZeroconfSpecSwift.hpp +120 -0
  93. package/nitrogen/generated/ios/c++/HybridLocalNetworkPermissionSpecSwift.cpp +11 -0
  94. package/nitrogen/generated/ios/c++/HybridLocalNetworkPermissionSpecSwift.hpp +87 -0
  95. package/nitrogen/generated/ios/swift/BonjourFail.swift +44 -0
  96. package/nitrogen/generated/ios/swift/BonjourListener.swift +47 -0
  97. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  98. package/nitrogen/generated/ios/swift/Func_void_BonjourFail.swift +47 -0
  99. package/nitrogen/generated/ios/swift/Func_void_bool.swift +47 -0
  100. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  101. package/nitrogen/generated/ios/swift/Func_void_std__vector_ScanResult_.swift +47 -0
  102. package/nitrogen/generated/ios/swift/HybridBonjourZeroconfSpec.swift +60 -0
  103. package/nitrogen/generated/ios/swift/HybridBonjourZeroconfSpec_cxx.swift +203 -0
  104. package/nitrogen/generated/ios/swift/HybridLocalNetworkPermissionSpec.swift +57 -0
  105. package/nitrogen/generated/ios/swift/HybridLocalNetworkPermissionSpec_cxx.swift +155 -0
  106. package/nitrogen/generated/ios/swift/ScanOptions.swift +48 -0
  107. package/nitrogen/generated/ios/swift/ScanResult.swift +149 -0
  108. package/nitrogen/generated/shared/c++/BonjourFail.hpp +63 -0
  109. package/nitrogen/generated/shared/c++/BonjourListener.hpp +75 -0
  110. package/nitrogen/generated/shared/c++/HybridBonjourZeroconfSpec.cpp +26 -0
  111. package/nitrogen/generated/shared/c++/HybridBonjourZeroconfSpec.hpp +80 -0
  112. package/nitrogen/generated/shared/c++/HybridLocalNetworkPermissionSpec.cpp +22 -0
  113. package/nitrogen/generated/shared/c++/HybridLocalNetworkPermissionSpec.hpp +66 -0
  114. package/nitrogen/generated/shared/c++/ScanOptions.hpp +75 -0
  115. package/nitrogen/generated/shared/c++/ScanResult.hpp +92 -0
  116. package/package.json +169 -0
  117. package/src/index.ts +22 -0
  118. package/src/permissions.ios.ts +27 -0
  119. package/src/permissions.ts +7 -0
  120. package/src/specs/BonjourFail.ts +5 -0
  121. package/src/specs/BonjourListener.ts +3 -0
  122. package/src/specs/BonjourZeroconf.nitro.ts +20 -0
  123. package/src/specs/LocalNetworkPermission.nitro.ts +7 -0
  124. package/src/specs/ScanResult.ts +7 -0
  125. package/src/useIsScanning.ts +17 -0
@@ -0,0 +1,30 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "BonjourZeroconf"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/dawidzawada/bonjour-zeroconf.git", :tag => "#{s.version}" }
15
+
16
+
17
+ s.source_files = [
18
+ "ios/**/*.{swift}",
19
+ "ios/**/*.{m,mm}",
20
+ "cpp/**/*.{hpp,cpp}",
21
+ ]
22
+
23
+ s.dependency 'React-jsi'
24
+ s.dependency 'React-callinvoker'
25
+
26
+ load 'nitrogen/generated/ios/BonjourZeroconf+autolinking.rb'
27
+ add_nitrogen_files(s)
28
+
29
+ install_modules_dependencies(s)
30
+ end
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dawid Zawada
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # @dawidzawada/bonjour-zeroconf
2
+
3
+ Zeroconf devices scanner using Bonjour (iOS) and NSD (Android) for React Native & Expo apps. Powered by Nitro Modules.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ npm install @dawidzawada/bonjour-zeroconf react-native-nitro-modules
9
+
10
+ > `react-native-nitro-modules` is required as this library relies on [Nitro Modules](https://nitro.margelo.com/).
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```js
16
+ import { Scanner } from '@dawidzawada/bonjour-zeroconf';
17
+
18
+ // ...
19
+
20
+ Scanner.scan('_bonjour._tcp', 'local');
21
+ ```
22
+
23
+ ## Contributing
24
+
25
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
26
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
27
+ - [Code of conduct](CODE_OF_CONDUCT.md)
28
+
29
+ ## License
30
+
31
+ MIT
32
+
33
+ ---
34
+
35
+ Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
@@ -0,0 +1,24 @@
1
+ project(dawidzawada_bonjourzeroconf)
2
+ cmake_minimum_required(VERSION 3.9.0)
3
+
4
+ set(PACKAGE_NAME dawidzawada_bonjourzeroconf)
5
+ set(CMAKE_VERBOSE_MAKEFILE ON)
6
+ set(CMAKE_CXX_STANDARD 20)
7
+
8
+ # Define C++ library and add all sources
9
+ add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp)
10
+
11
+ # Add Nitrogen specs :)
12
+ include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/dawidzawada_bonjourzeroconf+autolinking.cmake)
13
+
14
+ # Set up local includes
15
+ include_directories("src/main/cpp" "../cpp")
16
+
17
+ find_library(LOG_LIB log)
18
+
19
+ # Link all libraries together
20
+ target_link_libraries(
21
+ ${PACKAGE_NAME}
22
+ ${LOG_LIB}
23
+ android # <-- Android core
24
+ )
@@ -0,0 +1,128 @@
1
+ buildscript {
2
+ ext.getExtOrDefault = {name ->
3
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['BonjourZeroconf_' + name]
4
+ }
5
+
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+
11
+ dependencies {
12
+ classpath "com.android.tools.build:gradle:8.7.2"
13
+ // noinspection DifferentKotlinGradleVersion
14
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
15
+ }
16
+ }
17
+
18
+ def reactNativeArchitectures() {
19
+ def value = rootProject.getProperties().get("reactNativeArchitectures")
20
+ return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
21
+ }
22
+
23
+ apply plugin: "com.android.library"
24
+ apply plugin: "kotlin-android"
25
+ apply from: '../nitrogen/generated/android/dawidzawada_bonjourzeroconf+autolinking.gradle'
26
+
27
+ apply plugin: "com.facebook.react"
28
+
29
+ def getExtOrIntegerDefault(name) {
30
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["BonjourZeroconf_" + name]).toInteger()
31
+ }
32
+
33
+ android {
34
+ namespace "com.margelo.nitro.dawidzawada.bonjourzeroconf"
35
+
36
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
37
+
38
+ defaultConfig {
39
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
40
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
41
+
42
+ externalNativeBuild {
43
+ cmake {
44
+ cppFlags "-frtti -fexceptions -Wall -fstack-protector-all"
45
+ arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
46
+ abiFilters (*reactNativeArchitectures())
47
+
48
+ buildTypes {
49
+ debug {
50
+ cppFlags "-O1 -g"
51
+ }
52
+ release {
53
+ cppFlags "-O2"
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ externalNativeBuild {
61
+ cmake {
62
+ path "CMakeLists.txt"
63
+ }
64
+ }
65
+
66
+ packagingOptions {
67
+ excludes = [
68
+ "META-INF",
69
+ "META-INF/**",
70
+ "**/libc++_shared.so",
71
+ "**/libfbjni.so",
72
+ "**/libjsi.so",
73
+ "**/libfolly_json.so",
74
+ "**/libfolly_runtime.so",
75
+ "**/libglog.so",
76
+ "**/libhermes.so",
77
+ "**/libhermes-executor-debug.so",
78
+ "**/libhermes_executor.so",
79
+ "**/libreactnative.so",
80
+ "**/libreactnativejni.so",
81
+ "**/libturbomodulejsijni.so",
82
+ "**/libreact_nativemodule_core.so",
83
+ "**/libjscexecutor.so"
84
+ ]
85
+ }
86
+
87
+ buildFeatures {
88
+ buildConfig true
89
+ prefab true
90
+ }
91
+
92
+ buildTypes {
93
+ release {
94
+ minifyEnabled false
95
+ }
96
+ }
97
+
98
+ lintOptions {
99
+ disable "GradleCompatible"
100
+ }
101
+
102
+ compileOptions {
103
+ sourceCompatibility JavaVersion.VERSION_1_8
104
+ targetCompatibility JavaVersion.VERSION_1_8
105
+ }
106
+
107
+ sourceSets {
108
+ main {
109
+ java.srcDirs += [
110
+ "generated/java",
111
+ "generated/jni"
112
+ ]
113
+ }
114
+ }
115
+ }
116
+
117
+ repositories {
118
+ mavenCentral()
119
+ google()
120
+ }
121
+
122
+ def kotlin_version = getExtOrDefault("kotlinVersion")
123
+
124
+ dependencies {
125
+ implementation "com.facebook.react:react-android"
126
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
127
+ implementation project(":react-native-nitro-modules")
128
+ }
@@ -0,0 +1,5 @@
1
+ BonjourZeroconf_kotlinVersion=2.0.21
2
+ BonjourZeroconf_minSdkVersion=24
3
+ BonjourZeroconf_targetSdkVersion=34
4
+ BonjourZeroconf_compileSdkVersion=35
5
+ BonjourZeroconf_ndkVersion=27.1.12297006
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,6 @@
1
+ #include <jni.h>
2
+ #include "dawidzawada_bonjourzeroconfOnLoad.hpp"
3
+
4
+ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
5
+ return margelo::nitro::dawidzawada_bonjourzeroconf::initialize(vm);
6
+ }
@@ -0,0 +1,182 @@
1
+ package com.margelo.nitro.dawidzawada.bonjourzeroconf
2
+
3
+ import android.net.nsd.NsdManager
4
+ import android.net.nsd.NsdServiceInfo
5
+ import android.util.Log
6
+ import androidx.annotation.RequiresApi
7
+ import com.margelo.nitro.dawidzawada.bonjourzeroconf.BonjourZeroconf.Companion.TAG
8
+ import kotlinx.coroutines.suspendCancellableCoroutine
9
+ import kotlinx.coroutines.sync.withLock
10
+ import kotlinx.coroutines.withTimeoutOrNull
11
+ import java.util.concurrent.Executors
12
+
13
+ suspend fun BonjourZeroconf.resolveService(service: NsdServiceInfo, serviceKey: String, timeout: Long) {
14
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
15
+ // New API (Android 14+)
16
+ resolveServiceNew(service, serviceKey, timeout)
17
+ } else {
18
+ resolveServiceLegacy(service, serviceKey, timeout)
19
+ }
20
+ }
21
+
22
+ @RequiresApi(34)
23
+ suspend fun BonjourZeroconf.resolveServiceNew(service: NsdServiceInfo, serviceKey: String, timeout: Long) {
24
+ try {
25
+ val resolved = withTimeoutOrNull(timeout) {
26
+ suspendCancellableCoroutine { continuation ->
27
+ val executor = Executors.newSingleThreadExecutor()
28
+
29
+ val manager = nsdManager
30
+ if (manager == null) {
31
+ executor.shutdown()
32
+ continuation.resume(null) {}
33
+ return@suspendCancellableCoroutine
34
+ }
35
+
36
+ val callback = object : NsdManager.ServiceInfoCallback {
37
+ fun unregisterCallback() {
38
+ try {
39
+ manager.unregisterServiceInfoCallback(this)
40
+ } catch (e: Exception) {
41
+ Log.e(TAG, "Error unregistering", e)
42
+ }
43
+ }
44
+
45
+ override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
46
+ Log.e(TAG, "Registration failed: ${service.serviceName}, error: $errorCode")
47
+ notifyScanFailListeners(BonjourFail.RESOLVE_FAILED)
48
+ unregisterCallback()
49
+ continuation.resume(null) {}
50
+ }
51
+
52
+ override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
53
+ Log.d(TAG, "Service updated: ${serviceInfo.serviceName}")
54
+ unregisterCallback()
55
+ continuation.resume(serviceInfo) {}
56
+ }
57
+
58
+ override fun onServiceLost() {
59
+ Log.d(TAG, "Service lost during resolution: ${service.serviceName}")
60
+ }
61
+
62
+ override fun onServiceInfoCallbackUnregistered() {
63
+ Log.d(TAG, "Callback unregistered: ${service.serviceName}")
64
+ executor.shutdown()
65
+ }
66
+ }
67
+
68
+ try {
69
+ manager.registerServiceInfoCallback(service, executor, callback)
70
+
71
+ continuation.invokeOnCancellation {
72
+ try {
73
+ manager.unregisterServiceInfoCallback(callback)
74
+ } catch (e: Exception) {
75
+ Log.e(TAG, "Error unregistering on cancellation", e)
76
+ executor.shutdown()
77
+ }
78
+ }
79
+ } catch (e: Exception) {
80
+ notifyScanFailListeners(BonjourFail.RESOLVE_FAILED)
81
+ Log.e(TAG, "Exception registering callback", e)
82
+ executor.shutdown()
83
+ continuation.resume(null) {}
84
+ }
85
+ }
86
+ }
87
+
88
+ resolved?.let { serviceInfo ->
89
+ extractScanResult(serviceInfo)?.let { scanResult ->
90
+ serviceCache[serviceKey] = scanResult
91
+ notifyScanResultsListeners()
92
+ }
93
+ } ?: Log.w(TAG, "Failed to resolve service: $serviceKey")
94
+
95
+ } catch (e: Exception) {
96
+ notifyScanFailListeners(BonjourFail.RESOLVE_FAILED)
97
+ Log.e(TAG, "Error during service resolution", e)
98
+ }
99
+ }
100
+
101
+ @Suppress("DEPRECATION")
102
+ suspend fun BonjourZeroconf.resolveServiceLegacy(service: NsdServiceInfo, serviceKey: String, timeout: Long) {
103
+ try {
104
+ val resolved = legacyResolveMutex.withLock {
105
+ withTimeoutOrNull(timeout) {
106
+ suspendCancellableCoroutine { continuation ->
107
+ val resolveListener = object : NsdManager.ResolveListener {
108
+ override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
109
+ Log.e(TAG, "Resolve failed: ${serviceInfo.serviceName}, error: $errorCode")
110
+
111
+ notifyScanFailListeners(BonjourFail.RESOLVE_FAILED)
112
+ continuation.resume(null) {}
113
+ }
114
+
115
+ override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
116
+ Log.d(TAG, "Service resolved: ${serviceInfo.serviceName}")
117
+ continuation.resume(serviceInfo) {}
118
+ }
119
+ }
120
+
121
+ try {
122
+ val manager = nsdManager
123
+ if (manager == null) {
124
+ continuation.resume(null) {}
125
+ return@suspendCancellableCoroutine
126
+ }
127
+
128
+ manager.resolveService(service, resolveListener)
129
+ } catch (e: Exception) {
130
+ notifyScanFailListeners(BonjourFail.RESOLVE_FAILED)
131
+ Log.e(TAG, "Exception resolving service", e)
132
+ continuation.resume(null) {}
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ resolved?.let { serviceInfo ->
139
+ extractScanResult(serviceInfo)?.let { scanResult ->
140
+ serviceCache[serviceKey] = scanResult
141
+ notifyScanResultsListeners()
142
+ }
143
+ } ?: Log.w(TAG, "Failed to resolve service: $serviceKey")
144
+
145
+ } catch (e: Exception) {
146
+ Log.e(TAG, "Error during service resolution", e)
147
+ }
148
+ }
149
+
150
+ private fun BonjourZeroconf.extractScanResult(serviceInfo: NsdServiceInfo): ScanResult? {
151
+ return try {
152
+ val host = serviceInfo.host ?: return null
153
+ val port = serviceInfo.port
154
+
155
+ val (ipv4, ipv6) = when {
156
+ host.address.size == 4 -> host.hostAddress to null
157
+ host.address.size == 16 -> null to formatIPv6Address(host.address)
158
+ else -> null to null
159
+ }
160
+
161
+ ScanResult(
162
+ name = serviceInfo.serviceName,
163
+ ipv4 = ipv4,
164
+ ipv6 = ipv6,
165
+ hostname = host.hostName,
166
+ port = port.toDouble()
167
+ )
168
+ } catch (e: Exception) {
169
+ notifyScanFailListeners(BonjourFail.EXTRACTION_FAILED)
170
+ Log.e(TAG, "Failed to extract scan result", e)
171
+ null
172
+ }
173
+ }
174
+
175
+ private fun formatIPv6Address(bytes: ByteArray): String {
176
+ require(bytes.size == 16) { "IPv6 address must be 16 bytes" }
177
+
178
+ return (0 until 16 step 2).joinToString(":") { i ->
179
+ val segment = ((bytes[i].toInt() and 0xFF) shl 8) or (bytes[i + 1].toInt() and 0xFF)
180
+ segment.toString(16)
181
+ }
182
+ }
@@ -0,0 +1,45 @@
1
+ package com.margelo.nitro.dawidzawada.bonjourzeroconf
2
+
3
+ import android.util.Log
4
+ import com.margelo.nitro.dawidzawada.bonjourzeroconf.BonjourZeroconf.Companion.TAG
5
+ import kotlinx.coroutines.Dispatchers
6
+ import kotlinx.coroutines.launch
7
+
8
+ fun BonjourZeroconf.notifyScanResultsListeners() {
9
+ val results = serviceCache.values.toTypedArray()
10
+ scope.launch(Dispatchers.Main) {
11
+ scanResultsListeners.values.forEach { listener ->
12
+ try {
13
+ listener(results)
14
+ } catch (e: Exception) {
15
+ Log.e(TAG, "Error notifying scan results listener", e)
16
+ }
17
+ }
18
+ }
19
+ }
20
+
21
+ fun BonjourZeroconf.updateScanningState(newState: Boolean) {
22
+ _isScanning = newState
23
+ scope.launch(Dispatchers.Main) {
24
+ scanStateListeners.values.forEach { listener ->
25
+ try {
26
+ listener(newState)
27
+ } catch (e: Exception) {
28
+ Log.e(TAG, "Error notifying scan state listener", e)
29
+ }
30
+ }
31
+ }
32
+ }
33
+
34
+
35
+ fun BonjourZeroconf.notifyScanFailListeners(fail: BonjourFail) {
36
+ scope.launch(Dispatchers.Main) {
37
+ scanFailListeners.values.forEach { listener ->
38
+ try {
39
+ listener(fail)
40
+ } catch (e: Exception) {
41
+ Log.e(TAG, "Error notifying scan fail listener", e)
42
+ }
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,183 @@
1
+ package com.margelo.nitro.dawidzawada.bonjourzeroconf
2
+
3
+ import android.content.Context
4
+ import android.net.nsd.NsdManager
5
+ import android.net.nsd.NsdServiceInfo
6
+ import android.util.Log
7
+ import com.facebook.proguard.annotations.DoNotStrip
8
+ import java.util.UUID
9
+ import java.util.concurrent.ConcurrentHashMap
10
+ import com.margelo.nitro.NitroModules
11
+ import kotlinx.coroutines.CoroutineScope
12
+ import kotlinx.coroutines.Dispatchers
13
+ import kotlinx.coroutines.SupervisorJob
14
+ import kotlinx.coroutines.launch
15
+ import kotlinx.coroutines.sync.Mutex
16
+
17
+ @DoNotStrip
18
+ class BonjourZeroconf : HybridBonjourZeroconfSpec() {
19
+
20
+ companion object {
21
+ internal const val TAG = "BonjourZeroconf"
22
+ internal const val DEFAULT_RESOLVE_TIMEOUT_MS = 10_000L
23
+ }
24
+
25
+ @Volatile
26
+ internal var _isScanning = false
27
+
28
+ internal val scanStateListeners = ConcurrentHashMap<UUID, (Boolean) -> Unit>()
29
+ internal val scanResultsListeners = ConcurrentHashMap<UUID, (Array<ScanResult>) -> Unit>()
30
+ internal val scanFailListeners = ConcurrentHashMap<UUID, (BonjourFail) -> Unit>()
31
+
32
+ internal var nsdManager: NsdManager? = null
33
+ internal var currentDiscoveryListener: NsdManager.DiscoveryListener? = null
34
+ internal val serviceCache = ConcurrentHashMap<String, ScanResult>()
35
+ internal val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
36
+ internal val legacyResolveMutex = Mutex()
37
+
38
+ override val isScanning: Boolean
39
+ get() = _isScanning
40
+
41
+ override fun scan(type: String, domain: String, options: ScanOptions?) {
42
+ if (_isScanning) {
43
+ return
44
+ }
45
+
46
+ val context = NitroModules.applicationContext
47
+ ?: throw IllegalStateException("Application context is not available.")
48
+
49
+ if (nsdManager == null) {
50
+ nsdManager = context.getSystemService(Context.NSD_SERVICE) as? NsdManager
51
+ ?: throw IllegalStateException("NsdManager service is not available on this device.")
52
+ }
53
+
54
+ val resolveTimeout = options?.addressResolveTimeout?.toLong() ?: DEFAULT_RESOLVE_TIMEOUT_MS
55
+
56
+
57
+ currentDiscoveryListener = createDiscoveryListener(resolveTimeout).also { listener ->
58
+ try {
59
+ Log.i(TAG, "Starting scan for type: $type")
60
+ updateScanningState(true)
61
+ nsdManager?.discoverServices(type, NsdManager.PROTOCOL_DNS_SD, listener)
62
+ ?: throw IllegalStateException("NsdManager is not initialized")
63
+ } catch (e: Exception) {
64
+ Log.e(TAG, "Failed to start discovery", e)
65
+ updateScanningState(false)
66
+ throw RuntimeException("Failed to start service discovery: ${e.message}", e)
67
+ }
68
+ }
69
+ }
70
+
71
+ override fun stop() {
72
+ currentDiscoveryListener?.let { listener ->
73
+ try {
74
+ nsdManager?.stopServiceDiscovery(listener)
75
+ Log.i(TAG, "Stopped service discovery")
76
+ } catch (e: Exception) {
77
+ Log.e(TAG, "Error stopping discovery", e)
78
+ }
79
+ }
80
+
81
+ currentDiscoveryListener = null
82
+ serviceCache.clear()
83
+ }
84
+
85
+ override fun listenForScanResults(onResult: (Array<ScanResult>) -> Unit): BonjourListener {
86
+ val id = UUID.randomUUID()
87
+ scanResultsListeners[id] = onResult
88
+
89
+ val currentResults = serviceCache.values.toTypedArray()
90
+ if (currentResults.isNotEmpty()) {
91
+ scope.launch(Dispatchers.Main) {
92
+ try {
93
+ onResult(currentResults)
94
+ } catch (e: Exception) {
95
+ Log.e(TAG, "Scan results listener error", e)
96
+ }
97
+ }
98
+ }
99
+
100
+ return BonjourListener {
101
+ scanResultsListeners.remove(id)
102
+ }
103
+ }
104
+
105
+ override fun listenForScanState(onChange: (Boolean) -> Unit): BonjourListener {
106
+ val id = UUID.randomUUID()
107
+ scanStateListeners[id] = onChange
108
+
109
+ scope.launch(Dispatchers.Main) {
110
+ try {
111
+ onChange(_isScanning)
112
+ } catch (e: Exception) {
113
+ Log.e(TAG, "Scan state listener error", e)
114
+ }
115
+ }
116
+
117
+ return BonjourListener {
118
+ scanStateListeners.remove(id)
119
+ }
120
+ }
121
+
122
+ override fun listenForScanFail(onFail: (BonjourFail) -> Unit): BonjourListener {
123
+ val id = UUID.randomUUID()
124
+ scanFailListeners[id] = onFail
125
+
126
+ return BonjourListener {
127
+ scanFailListeners.remove(id)
128
+ }
129
+ }
130
+
131
+ private fun createDiscoveryListener(resolveTimeout: Long) = object : NsdManager.DiscoveryListener {
132
+
133
+ override fun onDiscoveryStarted(serviceType: String) {
134
+ Log.d(TAG, "Discovery started: $serviceType")
135
+ }
136
+
137
+ override fun onServiceFound(service: NsdServiceInfo) {
138
+ val serviceKey = createServiceKey(service)
139
+ Log.d(TAG, "Service found: ${service.serviceName} (key: $serviceKey)")
140
+
141
+ if (serviceCache.containsKey(serviceKey)) {
142
+ return
143
+ }
144
+
145
+ scope.launch {
146
+ try {
147
+ resolveService(service, serviceKey, resolveTimeout)
148
+ } catch (e: Exception) {
149
+ Log.e(TAG, "Error resolving service: $serviceKey", e)
150
+ }
151
+ }
152
+ }
153
+
154
+ override fun onServiceLost(service: NsdServiceInfo) {
155
+ val serviceKey = createServiceKey(service)
156
+ Log.d(TAG, "Service lost: ${service.serviceName} (key: $serviceKey)")
157
+
158
+ serviceCache.remove(serviceKey)?.let {
159
+ notifyScanResultsListeners()
160
+ }
161
+ }
162
+
163
+ override fun onDiscoveryStopped(serviceType: String) {
164
+ Log.d(TAG, "Discovery stopped: $serviceType")
165
+ updateScanningState(false)
166
+ }
167
+
168
+ override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
169
+ Log.e(TAG, "Start discovery failed: $serviceType, error: $errorCode")
170
+ notifyScanFailListeners(BonjourFail.DISCOVERY_FAILED)
171
+ updateScanningState(false)
172
+ }
173
+
174
+ override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
175
+ notifyScanFailListeners(BonjourFail.DISCOVERY_FAILED)
176
+ Log.e(TAG, "Stop discovery failed: $serviceType, error: $errorCode")
177
+ }
178
+ }
179
+
180
+ private fun createServiceKey(service: NsdServiceInfo): String {
181
+ return "${service.serviceName}.${service.serviceType}"
182
+ }
183
+ }
@@ -0,0 +1,22 @@
1
+ package com.margelo.nitro.dawidzawada.bonjourzeroconf
2
+
3
+ import com.facebook.react.TurboReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+
8
+ class BonjourZeroconfPackage : TurboReactPackage() {
9
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
10
+ return null
11
+ }
12
+
13
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
14
+ return ReactModuleInfoProvider { HashMap() }
15
+ }
16
+
17
+ companion object {
18
+ init {
19
+ System.loadLibrary("dawidzawada_bonjourzeroconf")
20
+ }
21
+ }
22
+ }