Skip to content

Commit

Permalink
Android/Jvm NsdManager (#479)
Browse files Browse the repository at this point in the history
* Create NsdManager proxy service for android with stub JVM

* Adjust code style

* Remove code sensing
  • Loading branch information
ashdavies authored Aug 20, 2023
1 parent c05d525 commit 5b30e9b
Show file tree
Hide file tree
Showing 8 changed files with 301 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ spotless {
val ktLintVersion = libs.versions.pinterest.ktlint.get()
val editorConfig = mapOf(
"ij_kotlin_allow_trailing_comma_on_call_site" to "true",
"ktlint_standard_comment-wrapping" to "disabled",
"ktlint_standard_function-naming" to "disabled",
"ktlint_standard_property-naming" to "disabled",
"ij_kotlin_allow_trailing_comma" to "true",
Expand Down
13 changes: 13 additions & 0 deletions nsd-manager/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
plugins {
id("io.ashdavies.default")
}

android {
namespace = "io.ashdavies.nsd"
}

kotlin {
commonMain.dependencies {
implementation(projects.platformSupport)
}
}
2 changes: 2 additions & 0 deletions nsd-manager/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package io.ashdavies.nsd

import android.os.Build
import androidx.annotation.RequiresApi
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.net.Inet4Address
import java.net.Inet6Address
import kotlin.coroutines.resumeWithException
import android.net.nsd.NsdManager as AndroidNsdManager
import android.net.nsd.NsdServiceInfo as AndroidNsdServiceInfo

public actual typealias NsdManager = AndroidNsdManager

public actual typealias NsdServiceInfo = AndroidNsdServiceInfo

public actual fun NsdServiceInfo.getHostAddressOrNull(
type: NsdHostAddress.Type,
): NsdHostAddress? {
val hostAddressList = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> hostAddresses
else -> {
@Suppress("DEPRECATION")
listOf(host)
}
}

return hostAddressList
.firstOrNull {
when (type) {
NsdHostAddress.Type.IPv4 -> it is Inet4Address
NsdHostAddress.Type.IPv6 -> it is Inet6Address
}
}
?.hostAddress
?.let { NsdHostAddress(it, type) }
}

public actual fun NsdManager.discoverServices(
serviceType: String,
protocolType: Int,
): Flow<List<NsdServiceInfo>> = channelFlow {
val servicesFound = mutableListOf<NsdServiceInfo>()
val listener = object : AndroidNsdManager.DiscoveryListener {
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
cancel("Service start discovery failed ($errorCode)")
}

override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
cancel("Service stop discovery failed ($errorCode)")
}

override fun onDiscoveryStarted(serviceType: String) = Unit

override fun onDiscoveryStopped(serviceType: String) {
channel.close()
}

override fun onServiceFound(serviceInfo: NsdServiceInfo) {
servicesFound += serviceInfo
trySend(servicesFound)
}

override fun onServiceLost(serviceInfo: NsdServiceInfo) {
servicesFound -= serviceInfo
trySend(servicesFound)
}
}

discoverServices(
/* serviceType = */ serviceType,
/* protocolType = */ protocolType,
/* listener = */ listener,
)

awaitClose {
stopServiceDiscovery(listener)
}
}

public actual fun NsdManager.resolveService(
serviceInfo: NsdServiceInfo,
coroutineDispatcher: CoroutineDispatcher,
): Flow<NsdServiceInfo> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
return resolveServiceApi34(serviceInfo, coroutineDispatcher)
}

return flow {
withContext(coroutineDispatcher) {
emit(resolveServiceApi16(serviceInfo))
}
}
}

private suspend fun NsdManager.resolveServiceApi16(
serviceInfo: NsdServiceInfo,
): NsdServiceInfo = suspendCancellableCoroutine { continuation ->
val listener = object : AndroidNsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
continuation.resumeWithException(IllegalStateException("Service resolution failed with an error ($errorCode)"))
}

override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
continuation.resumeWith(Result.success(serviceInfo))
}
}

@Suppress("DEPRECATION")
resolveService(
/* serviceInfo = */ serviceInfo,
/* listener = */ listener,
)
}

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private fun NsdManager.resolveServiceApi34(
serviceInfo: NsdServiceInfo,
coroutineDispatcher: CoroutineDispatcher,
): Flow<NsdServiceInfo> = callbackFlow {
val callback = object : AndroidNsdManager.ServiceInfoCallback {
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
cancel("Service info callback registration failed ($errorCode)")
}

override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
trySend(serviceInfo)
}

override fun onServiceLost() = Unit

override fun onServiceInfoCallbackUnregistered() {
channel.close()
}
}

registerServiceInfoCallback(
/* serviceInfo = */ serviceInfo,
/* executor = */ coroutineDispatcher.asExecutor(),
/* listener = */ callback,
)

awaitClose { unregisterServiceInfoCallback(callback) }
}
62 changes: 62 additions & 0 deletions nsd-manager/src/commonMain/kotlin/io/ashdavies/nsd/NsdAgent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.ashdavies.nsd

import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flowOf

private const val HTTP_TCP = "_http._tcp"
private const val PROTOCOL_DNS_SD = 1

public fun interface NsdAgent {
public fun resolve(serviceName: String): Flow<NsdState>
}

public sealed interface NsdState {

public data object Discovering : NsdState

public data class Discovered(
val services: List<String>,
) : NsdState

public data class Resolved(
val serviceName: String,
val hostAddress: String,
val port: Int,
) : NsdState
}

public fun NsdAgent(manager: NsdManager): NsdAgent = NsdAgent { serviceName ->
channelFlow {
send(NsdState.Discovering)

@OptIn(ExperimentalCoroutinesApi::class)
manager
.discoverServices(HTTP_TCP, PROTOCOL_DNS_SD)
.flatMapLatest { serviceList ->
val serviceInfo = serviceList.filter { it.getServiceName() == serviceName }
val serviceNames = serviceInfo.map { it.getServiceName() }

send(NsdState.Discovered(serviceNames))
flowOf(*serviceInfo.toTypedArray())
}
.flatMapMerge { serviceInfo ->
manager.resolveService(serviceInfo)
}
.collect { serviceInfo ->
val hostAddress = serviceInfo.getHostAddressOrNull()
if (hostAddress != null) {
val resolved = NsdState.Resolved(
serviceName = serviceInfo.getServiceName(),
hostAddress = hostAddress.value,
port = serviceInfo.getPort(),
)

send(resolved)
}
}
}
}
36 changes: 36 additions & 0 deletions nsd-manager/src/commonMain/kotlin/io/ashdavies/nsd/NsdManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.ashdavies.nsd

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow

public data class NsdHostAddress(
public val value: String,
public val type: Type,
) {
public enum class Type {
IPv4,
IPv6,
}
}

public expect class NsdManager

public expect class NsdServiceInfo {
public fun getServiceName(): String
public fun getPort(): Int
}

public expect fun NsdServiceInfo.getHostAddressOrNull(
type: NsdHostAddress.Type = NsdHostAddress.Type.IPv4,
): NsdHostAddress?

public expect fun NsdManager.discoverServices(
serviceType: String,
protocolType: Int,
): Flow<List<NsdServiceInfo>>

public expect fun NsdManager.resolveService(
serviceInfo: NsdServiceInfo,
coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
): Flow<NsdServiceInfo>
34 changes: 34 additions & 0 deletions nsd-manager/src/jvmMain/kotlin/io/ashdavies/nsd/NsdManager.jvm.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.ashdavies.nsd

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow

public actual class NsdManager

public actual class NsdServiceInfo {
public actual fun getServiceName(): String {
unsupportedOperation()
}

public actual fun getPort(): Int {
unsupportedOperation()
}
}

public actual fun NsdServiceInfo.getHostAddressOrNull(
type: NsdHostAddress.Type,
): NsdHostAddress? = unsupportedOperation()

public actual fun NsdManager.discoverServices(
serviceType: String,
protocolType: Int,
): Flow<List<NsdServiceInfo>> = unsupportedOperation()

public actual fun NsdManager.resolveService(
serviceInfo: NsdServiceInfo,
coroutineDispatcher: CoroutineDispatcher,
): Flow<NsdServiceInfo> = unsupportedOperation()

private fun unsupportedOperation(): Nothing {
error("UnsupportedOperation")
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ include(
":local-storage",
":micro-yaml",
":notion-client",
":nsd-manager",
":notion-console",
":parcelable-support",
":platform-support",
Expand Down

0 comments on commit 5b30e9b

Please sign in to comment.