250 lines
8.2 KiB
Kotlin
250 lines
8.2 KiB
Kotlin
|
package tech.platypush.platypush
|
||
|
|
||
|
import android.content.ContentValues.TAG
|
||
|
import android.content.Context
|
||
|
import android.net.nsd.NsdManager
|
||
|
import android.net.nsd.NsdServiceInfo
|
||
|
import android.util.Log
|
||
|
import java.io.File
|
||
|
import java.io.FileWriter
|
||
|
import java.lang.RuntimeException
|
||
|
import java.util.*
|
||
|
import java.util.concurrent.locks.ReentrantLock
|
||
|
import kotlin.concurrent.withLock
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Service data class
|
||
|
*/
|
||
|
data class Service(val host: String, val port: Int, val name: String?) {
|
||
|
fun toMap(): Map<String, Any?> {
|
||
|
return mapOf(
|
||
|
"host" to host,
|
||
|
"port" to port,
|
||
|
"name" to name
|
||
|
)
|
||
|
}
|
||
|
|
||
|
override fun toString(): String {
|
||
|
return JSON.dump(toMap())
|
||
|
}
|
||
|
|
||
|
companion object {
|
||
|
fun load(obj: Map<String, Any?>): Service {
|
||
|
return Service(
|
||
|
host = obj["host"] as String,
|
||
|
port = obj["port"] as Int,
|
||
|
name = obj["name"] as String?
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* ZeroConf/Bonjour/mDNS event listener
|
||
|
*/
|
||
|
class Listener(private val scanner: Scanner): NsdManager.DiscoveryListener {
|
||
|
val serviceType = "_platypush-http._tcp."
|
||
|
|
||
|
override fun onDiscoveryStarted(regType: String) {
|
||
|
Log.d(TAG, "Service discovery started")
|
||
|
}
|
||
|
|
||
|
override fun onServiceFound(service: NsdServiceInfo) {
|
||
|
Log.d(TAG, "Service discovery succeeded: $service")
|
||
|
if (service.serviceType != serviceType) {
|
||
|
Log.d(TAG, "Unknown service type: ${service.serviceType}")
|
||
|
return
|
||
|
}
|
||
|
|
||
|
scanner.resolveService(service)
|
||
|
}
|
||
|
|
||
|
override fun onServiceLost(service: NsdServiceInfo) {
|
||
|
Log.w(TAG, "Service lost: $service")
|
||
|
}
|
||
|
|
||
|
override fun onDiscoveryStopped(serviceType: String) {
|
||
|
Log.i(TAG, "Discovery stopped: $serviceType")
|
||
|
}
|
||
|
|
||
|
override fun onStartDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||
|
Log.e(TAG, "Discovery start failed: Error code: $errorCode")
|
||
|
scanner.stopScan()
|
||
|
}
|
||
|
|
||
|
override fun onStopDiscoveryFailed(serviceType: String, errorCode: Int) {
|
||
|
Log.e(TAG, "Discovery stop failed: Error code: $errorCode")
|
||
|
scanner.stopScan()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* ZeroConf/Bonjour/mDNS service scanner and resolver
|
||
|
*/
|
||
|
class Scanner(context: Context) {
|
||
|
private val nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
|
||
|
private val listener = Listener(this)
|
||
|
private val services = HashMap<Pair<String, Int>, Service>()
|
||
|
private val resolverLock = ReentrantLock()
|
||
|
|
||
|
fun startScan() {
|
||
|
nsdManager.discoverServices(listener.serviceType, NsdManager.PROTOCOL_DNS_SD, listener)
|
||
|
}
|
||
|
|
||
|
fun stopScan() {
|
||
|
nsdManager.stopServiceDiscovery(listener)
|
||
|
}
|
||
|
|
||
|
fun getServices(): Collection<Service> {
|
||
|
return services.values
|
||
|
}
|
||
|
|
||
|
fun resolveService(service: NsdServiceInfo) {
|
||
|
// Service resolution is a critical section
|
||
|
resolverLock.lock()
|
||
|
val scanner = this
|
||
|
|
||
|
nsdManager.resolveService(service, object : NsdManager.ResolveListener {
|
||
|
override fun onResolveFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
|
||
|
resolverLock.unlock()
|
||
|
val msg = "Resolve of service ${serviceInfo.serviceName} failed"
|
||
|
|
||
|
// Retry logic
|
||
|
when (errorCode) {
|
||
|
NsdManager.FAILURE_ALREADY_ACTIVE -> {
|
||
|
Thread.sleep(100)
|
||
|
scanner.resolveService(serviceInfo)
|
||
|
Log.w(TAG, "$msg: Resolver already active")
|
||
|
}
|
||
|
|
||
|
NsdManager.FAILURE_MAX_LIMIT -> {
|
||
|
Thread.sleep(5000)
|
||
|
scanner.resolveService(serviceInfo)
|
||
|
Log.e(TAG, "$msg: Maximum number of resolve requests reached")
|
||
|
}
|
||
|
|
||
|
NsdManager.FAILURE_INTERNAL_ERROR -> {
|
||
|
Log.e(TAG, "$msg: Internal error")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
override fun onServiceResolved(serviceInfo: NsdServiceInfo) {
|
||
|
services[Pair(serviceInfo.host.hostAddress, serviceInfo.port)] = Service(
|
||
|
serviceInfo.host.hostAddress, serviceInfo.port, serviceInfo.serviceName)
|
||
|
resolverLock.unlock()
|
||
|
Log.i(TAG, "Resolve succeeded: $serviceInfo")
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Services manager class
|
||
|
*/
|
||
|
class Manager(context: Context) {
|
||
|
private val servicesFile = File(context.filesDir, "services.json")
|
||
|
private val servicesFileLock = ReentrantLock()
|
||
|
private val emptyServices = mapOf<String, List<Service>>(
|
||
|
"saved" to emptyList(),
|
||
|
"lastConnected" to emptyList()
|
||
|
)
|
||
|
|
||
|
private fun initServicesFile(reset: Boolean = false) {
|
||
|
if (reset || !servicesFile.exists())
|
||
|
saveServices(emptyServices)
|
||
|
}
|
||
|
|
||
|
fun loadServices(): Map<String, List<Service>> {
|
||
|
initServicesFile()
|
||
|
var services: Map<String, List<Service>> = emptyServices
|
||
|
|
||
|
try {
|
||
|
services = deserializeServices(servicesFile.readText())
|
||
|
} catch (e: Exception) {
|
||
|
Log.e(TAG, "Error while parsing $servicesFile: resetting it", e)
|
||
|
initServicesFile(reset = true)
|
||
|
}
|
||
|
|
||
|
return services
|
||
|
}
|
||
|
|
||
|
private fun saveServices(services: Map<String, List<Service>>) {
|
||
|
servicesFileLock.withLock {
|
||
|
FileWriter(servicesFile).use {
|
||
|
it.write(serializeServices(services))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fun saveService(host: String, port: Int, name: String, overwrite: Boolean = false) {
|
||
|
val service = Service(host, port, name)
|
||
|
val services = this.loadServices().toMutableMap()
|
||
|
val savedServices = services["saved"]?.toMutableList() ?: ArrayList()
|
||
|
val dupIndex = getSavedServiceIndex(service, savedServices)
|
||
|
|
||
|
if (dupIndex != null && !overwrite)
|
||
|
throw DuplicateServiceException(service)
|
||
|
|
||
|
savedServices.add(service)
|
||
|
services["saved"] = savedServices
|
||
|
saveServices(services)
|
||
|
}
|
||
|
|
||
|
fun removeService(host: String? = null, port: Int? = null, name: String? = null) {
|
||
|
val service = Service(host=host ?: "", port=port ?: 0, name=name)
|
||
|
val services = this.loadServices().toMutableMap()
|
||
|
val savedServices = ArrayList<Service>(services["saved"] ?: emptyList<Service>())
|
||
|
val srvIndex = getSavedServiceIndex(service, savedServices)
|
||
|
?: throw NoSuchServiceException(host, port, name)
|
||
|
|
||
|
savedServices.removeAt(srvIndex)
|
||
|
services["saved"] = savedServices
|
||
|
saveServices(services)
|
||
|
}
|
||
|
|
||
|
private fun getSavedServiceIndex(service: Service, services: List<Service>): Int? {
|
||
|
val matchingSrvIndexes = services.indices.filter {
|
||
|
(services[it].host == service.host && services[it].port == service.port) ||
|
||
|
services[it].name == service.name
|
||
|
}
|
||
|
|
||
|
return if (matchingSrvIndexes.isNotEmpty()) matchingSrvIndexes[0] else null
|
||
|
}
|
||
|
|
||
|
fun serializeServices(services: Map<String, List<Service>>): String {
|
||
|
val parsedServices = HashMap<String, List<Map<String, Any?>>>()
|
||
|
|
||
|
for (srvType in listOf("saved", "lastConnected")) {
|
||
|
val outputList = LinkedList<Map<String, Any?>>()
|
||
|
for (srv in services[srvType] ?: emptyList())
|
||
|
outputList.add(srv.toMap())
|
||
|
parsedServices[srvType] = outputList.toList()
|
||
|
}
|
||
|
|
||
|
return JSON.dump(parsedServices)
|
||
|
}
|
||
|
|
||
|
private fun deserializeServices(servicesJson: String): Map<String, List<Service>> {
|
||
|
@Suppress("UNCHECKED_CAST")
|
||
|
val services = JSON.load(servicesJson) as Map<String, List<Map<String, Any?>>>
|
||
|
val parsedServices = HashMap<String, List<Service>>()
|
||
|
|
||
|
for (srvType in listOf("saved", "lastConnected")) {
|
||
|
val outputList = LinkedList<Service>()
|
||
|
for (srv in services[srvType] ?: emptyList())
|
||
|
outputList.add(Service.load(srv))
|
||
|
parsedServices[srvType] = outputList
|
||
|
}
|
||
|
|
||
|
return parsedServices
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class DuplicateServiceException(service: Service) : RuntimeException("Duplicate service: $service")
|
||
|
class NoSuchServiceException(host: String? = null, port: Int? = null, name: String? = null):
|
||
|
RuntimeException("No such host: host=$host, port=$port, name=$name")
|