varroa/app/src/main/java/com/adamaps/varroa/api/AdaMapsApiClient.kt
Kayos f06ce3f319 v6: fix detection send queue
Root cause: Server expects flat array of detections with device_id on each
item, but client was sending nested {device_id, detections: [...]}.

The /health endpoint worked (ADAMaps showed green) but /api/ingest failed
validation: 'Missing required fields: device_id, lat, lon' on every request.
Items queued indefinitely, sent=0.

Fixed by:
- Adding device_id field to AdaMapsDetection
- Changed AdaMapsIngestRequest to typealias for List<AdaMapsDetection>
- Updated toAdaMapsDetection() to accept deviceId parameter
- Updated ForwardingService to send flat array format
2026-03-11 10:37:56 -07:00

89 lines
3 KiB
Kotlin

package com.adamaps.varroa.api
import com.adamaps.varroa.data.AdaMapsIngestRequest
import com.adamaps.varroa.data.ApiResult
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Dns
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.net.InetAddress
import java.util.concurrent.TimeUnit
/**
* Custom DNS resolver that hardcodes api.adamaps.org to bypass Android's DNS
* when connected to Bee's AP (which has no upstream DNS server).
*/
private object AdaMapsDns : Dns {
// api.adamaps.org resolves to this IP
private val ADAMAPS_IP = "142.44.213.229"
override fun lookup(hostname: String): List<InetAddress> {
return if (hostname.equals("api.adamaps.org", ignoreCase = true)) {
// Return hardcoded IP to bypass DNS lookup
listOf(InetAddress.getByName(ADAMAPS_IP))
} else {
// Fall back to system DNS for other hosts
Dns.SYSTEM.lookup(hostname)
}
}
}
class AdaMapsApiClient(
private var apiUrl: String = "https://api.adamaps.org",
private var apiKey: String = "mapnet-ingest-2026"
) {
// Use custom DNS resolver to handle Bee AP's lack of upstream DNS
private val client = OkHttpClient.Builder()
.dns(AdaMapsDns)
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
private val gson = Gson()
private val json = "application/json; charset=utf-8".toMediaType()
fun updateConfig(url: String, key: String) {
apiUrl = url.trimEnd('/')
apiKey = key
}
suspend fun ingest(detections: AdaMapsIngestRequest): ApiResult<String> = withContext(Dispatchers.IO) {
try {
// Server expects flat array of detections, each with device_id
val body = gson.toJson(detections).toRequestBody(json)
val req = Request.Builder()
.url("$apiUrl/api/ingest")
.addHeader("X-MapNet-Key", apiKey)
.addHeader("Content-Type", "application/json")
.post(body)
.build()
client.newCall(req).execute().use { resp ->
val respBody = resp.body?.string() ?: ""
if (resp.isSuccessful) ApiResult.Success(respBody)
else ApiResult.Error("HTTP ${resp.code}: ${resp.message}", resp.code)
}
} catch (e: Exception) {
ApiResult.Error(e.message ?: "Network error")
}
}
suspend fun checkReachability(): Boolean = withContext(Dispatchers.IO) {
try {
val req = Request.Builder()
.url("$apiUrl/health")
.head()
.build()
client.newCall(req).execute().use { resp ->
resp.code < 500
}
} catch (e: Exception) {
false
}
}
}