platypush-app/app/src/main/java/tech/platypush/platypush/Services.kt

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")