v7.1: add verbose debug logging
This commit is contained in:
parent
964d175454
commit
63801d8f12
7 changed files with 229 additions and 34 deletions
|
|
@ -13,8 +13,8 @@ android {
|
|||
applicationId = "com.adamaps.varroa"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 7
|
||||
versionName = "1.7.0"
|
||||
versionCode = 8
|
||||
versionName = "1.7.1"
|
||||
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
|
|
|
|||
|
|
@ -1,19 +1,52 @@
|
|||
package com.adamaps.varroa
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import com.adamaps.varroa.ui.theme.VarroaTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "VarroaMain"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.d(TAG, "MainActivity onCreate() called")
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
VarroaTheme {
|
||||
VarroaNavGraph()
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "MainActivity onCreate() completed - UI composition set")
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
Log.d(TAG, "MainActivity onStart() - activity becoming visible")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
Log.d(TAG, "MainActivity onResume() - activity in foreground")
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
Log.d(TAG, "MainActivity onPause() - activity paused")
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
Log.d(TAG, "MainActivity onStop() - activity stopped")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(TAG, "MainActivity onDestroy() - activity being destroyed")
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.adamaps.varroa.api
|
||||
|
||||
import android.util.Log
|
||||
import com.adamaps.varroa.data.AdaMapsIngestRequest
|
||||
import com.adamaps.varroa.data.ApiResult
|
||||
import com.google.gson.Gson
|
||||
|
|
@ -36,6 +37,9 @@ class AdaMapsApiClient(
|
|||
private var apiUrl: String = "https://api.adamaps.org",
|
||||
private var apiKey: String = "mapnet-ingest-2026"
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "VarroaAdaAPI"
|
||||
}
|
||||
|
||||
// Use custom DNS resolver to handle Bee AP's lack of upstream DNS
|
||||
private val client = OkHttpClient.Builder()
|
||||
|
|
@ -49,26 +53,45 @@ class AdaMapsApiClient(
|
|||
private val json = "application/json; charset=utf-8".toMediaType()
|
||||
|
||||
fun updateConfig(url: String, key: String) {
|
||||
val oldUrl = apiUrl
|
||||
val oldKeyPrefix = apiKey.take(8)
|
||||
apiUrl = url.trimEnd('/')
|
||||
apiKey = key
|
||||
Log.d(TAG, "AdaMaps config updated - URL: $oldUrl -> $apiUrl, Key: ${oldKeyPrefix}... -> ${key.take(8)}...")
|
||||
}
|
||||
|
||||
suspend fun ingest(detections: AdaMapsIngestRequest): ApiResult<String> = withContext(Dispatchers.IO) {
|
||||
val ingestUrl = "$apiUrl/api/ingest"
|
||||
Log.d(TAG, "POST request to: $ingestUrl with ${detections.size} detections")
|
||||
|
||||
try {
|
||||
// Server expects flat array of detections, each with device_id
|
||||
val body = gson.toJson(detections).toRequestBody(json)
|
||||
val jsonPayload = gson.toJson(detections)
|
||||
Log.d(TAG, "JSON payload length: ${jsonPayload.length} chars")
|
||||
val body = jsonPayload.toRequestBody(json)
|
||||
|
||||
val req = Request.Builder()
|
||||
.url("$apiUrl/api/ingest")
|
||||
.url(ingestUrl)
|
||||
.addHeader("X-MapNet-Key", apiKey)
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.post(body)
|
||||
.build()
|
||||
|
||||
Log.d(TAG, "Sending POST request with key: ${apiKey.take(8)}...")
|
||||
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)
|
||||
Log.d(TAG, "HTTP ${resp.code} ${resp.message} - response length: ${respBody.length}")
|
||||
|
||||
if (resp.isSuccessful) {
|
||||
Log.i(TAG, "Ingest successful - HTTP ${resp.code}")
|
||||
ApiResult.Success(respBody)
|
||||
} else {
|
||||
Log.e(TAG, "Ingest failed - HTTP ${resp.code} ${resp.message}, response: ${respBody.take(200)}")
|
||||
ApiResult.Error("HTTP ${resp.code}: ${resp.message}", resp.code)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Ingest request failed", e)
|
||||
ApiResult.Error(e.message ?: "Network error")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class BeeApiClient(
|
|||
private var altUrl: String = "http://192.168.0.155:5000"
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "BeeApiClient"
|
||||
private const val TAG = "VarroaBeeAPI"
|
||||
}
|
||||
|
||||
private var client = buildClient(null)
|
||||
|
|
@ -39,10 +39,14 @@ class BeeApiClient(
|
|||
private set
|
||||
|
||||
fun updateUrls(primary: String, alt: String) {
|
||||
val oldPrimary = primaryUrl
|
||||
val oldAlt = altUrl
|
||||
primaryUrl = primary.trimEnd('/')
|
||||
altUrl = alt.trimEnd('/')
|
||||
Log.d(TAG, "URLs updated from [$oldPrimary, $oldAlt] to [$primaryUrl, $altUrl]")
|
||||
// Reset active URL when settings change
|
||||
activeUrl = null
|
||||
Log.d(TAG, "Active URL reset due to configuration change")
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -50,9 +54,10 @@ class BeeApiClient(
|
|||
* This is the preferred method when using NetworkStateMonitor.
|
||||
*/
|
||||
fun bindToNetwork(network: Network) {
|
||||
Log.d(TAG, "Binding to network: $network")
|
||||
Log.i(TAG, "Binding OkHttpClients to network: $network")
|
||||
client = buildClient(network)
|
||||
fastClient = buildFastClient(network)
|
||||
Log.d(TAG, "Network binding complete - both clients updated")
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -100,19 +105,25 @@ class BeeApiClient(
|
|||
|
||||
private suspend fun getRaw(path: String, useActiveUrl: Boolean = true): ApiResult<String> = withContext(Dispatchers.IO) {
|
||||
val baseUrl = if (useActiveUrl && activeUrl != null) activeUrl!! else primaryUrl
|
||||
val fullUrl = "$baseUrl$path"
|
||||
Log.d(TAG, "HTTP GET request to: $fullUrl")
|
||||
|
||||
try {
|
||||
val req = Request.Builder().url("$baseUrl$path").get().build()
|
||||
val req = Request.Builder().url(fullUrl).get().build()
|
||||
client.newCall(req).execute().use { resp ->
|
||||
val body = resp.body?.string() ?: ""
|
||||
if (resp.isSuccessful) {
|
||||
isConnected = true
|
||||
activeUrl = baseUrl
|
||||
Log.d(TAG, "HTTP ${resp.code} OK - response length: ${body.length} chars")
|
||||
ApiResult.Success(body)
|
||||
} else {
|
||||
Log.w(TAG, "HTTP ${resp.code} ${resp.message} from $fullUrl")
|
||||
ApiResult.Error("HTTP ${resp.code}: ${resp.message}", resp.code)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "HTTP request failed to $fullUrl", e)
|
||||
ApiResult.Error(e.message ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
|
|
@ -140,12 +151,20 @@ class BeeApiClient(
|
|||
* Returns the URL if successful, null otherwise.
|
||||
*/
|
||||
private suspend fun tryPing(url: String): String? = withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "Ping attempt to: $url/api/1/info")
|
||||
try {
|
||||
val req = Request.Builder().url("$url/api/1/info").get().build()
|
||||
fastClient.newCall(req).execute().use { resp ->
|
||||
if (resp.isSuccessful) url else null
|
||||
if (resp.isSuccessful) {
|
||||
Log.d(TAG, "Ping successful: $url (HTTP ${resp.code})")
|
||||
url
|
||||
} else {
|
||||
Log.d(TAG, "Ping failed: $url (HTTP ${resp.code})")
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Ping exception: $url - ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
@ -157,16 +176,20 @@ class BeeApiClient(
|
|||
suspend fun discoverActiveUrl(): String? = withContext(Dispatchers.IO) {
|
||||
// Fast path: if we have an active URL, try it first
|
||||
if (activeUrl != null) {
|
||||
Log.d(TAG, "Fast path - testing existing active URL: $activeUrl")
|
||||
val result = tryPing(activeUrl!!)
|
||||
if (result != null) {
|
||||
isConnected = true
|
||||
Log.i(TAG, "Active URL still works: $activeUrl")
|
||||
return@withContext result
|
||||
}
|
||||
// Active URL failed, clear it and try both
|
||||
Log.w(TAG, "Active URL $activeUrl failed - falling back to discovery")
|
||||
activeUrl = null
|
||||
}
|
||||
|
||||
// Try both URLs in parallel
|
||||
Log.d(TAG, "Starting parallel discovery: [$primaryUrl, $altUrl]")
|
||||
val primaryDeferred = async { tryPing(primaryUrl) }
|
||||
val altDeferred = async { tryPing(altUrl) }
|
||||
|
||||
|
|
@ -174,17 +197,21 @@ class BeeApiClient(
|
|||
val result = withTimeoutOrNull(4000L) {
|
||||
val primary = primaryDeferred.await()
|
||||
if (primary != null) {
|
||||
Log.d(TAG, "Primary URL responded first, cancelling alt")
|
||||
altDeferred.cancel()
|
||||
return@withTimeoutOrNull primary
|
||||
}
|
||||
Log.d(TAG, "Primary URL failed, waiting for alt")
|
||||
altDeferred.await()
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
activeUrl = result
|
||||
isConnected = true
|
||||
Log.i(TAG, "Discovery successful - active URL: $result")
|
||||
} else {
|
||||
isConnected = false
|
||||
Log.w(TAG, "Discovery failed - no URLs responded within 4s timeout")
|
||||
}
|
||||
result
|
||||
}
|
||||
|
|
@ -212,29 +239,38 @@ class BeeApiClient(
|
|||
}
|
||||
|
||||
suspend fun getLandmarks(): ApiResult<List<BeeDetection>> = withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "getLandmarks() called - activeUrl: $activeUrl")
|
||||
|
||||
// If no active URL, try discovery first
|
||||
if (activeUrl == null) {
|
||||
Log.d(TAG, "No active URL - starting discovery...")
|
||||
if (discoverActiveUrl() == null) {
|
||||
isConnected = false
|
||||
Log.e(TAG, "Discovery failed - marking Bee as offline")
|
||||
return@withContext ApiResult.Error("Bee offline")
|
||||
}
|
||||
}
|
||||
|
||||
when (val r = getRaw("/api/1/landmarks/last/200")) {
|
||||
is ApiResult.Success -> try {
|
||||
Log.d(TAG, "Raw landmarks response received - parsing JSON...")
|
||||
val type = object : TypeToken<List<BeeDetection>>() {}.type
|
||||
val list: List<BeeDetection> = gson.fromJson(r.data, type)
|
||||
isConnected = true
|
||||
Log.i(TAG, "Landmarks parsed successfully: ${list.size} detections")
|
||||
ApiResult.Success(list)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to parse landmarks JSON", e)
|
||||
ApiResult.Error("Parse error: ${e.message}")
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
Log.e(TAG, "getLandmarks() failed: ${r.message} (code: ${r.code})")
|
||||
// Connection failed, clear active URL for next attempt
|
||||
if (r.message.contains("timeout", ignoreCase = true) ||
|
||||
r.message.contains("connect", ignoreCase = true) ||
|
||||
r.message.contains("refused", ignoreCase = true) ||
|
||||
r.message.contains("unreachable", ignoreCase = true)) {
|
||||
Log.d(TAG, "Network error detected - clearing active URL")
|
||||
activeUrl = null
|
||||
isConnected = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||
class NetworkStateMonitor(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NetworkState"
|
||||
private const val TAG = "VarroaNetwork"
|
||||
|
||||
@Volatile
|
||||
private var INSTANCE: NetworkStateMonitor? = null
|
||||
|
|
@ -60,17 +60,20 @@ class NetworkStateMonitor(private val context: Context) {
|
|||
// Callback for validated internet (real connectivity)
|
||||
private val validatedCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
Log.d(TAG, "Validated network available: $network")
|
||||
Log.i(TAG, "Validated network available: $network")
|
||||
_hasValidatedInternet.value = true
|
||||
_internetNetwork.value = network
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
Log.d(TAG, "Validated network lost: $network")
|
||||
Log.w(TAG, "Validated network lost: $network")
|
||||
if (_internetNetwork.value == network) {
|
||||
_hasValidatedInternet.value = false
|
||||
_internetNetwork.value = null
|
||||
Log.d(TAG, "Internet network cleared due to loss")
|
||||
} else {
|
||||
Log.d(TAG, "Lost network $network was not our current internet network")
|
||||
}
|
||||
updateStatus()
|
||||
}
|
||||
|
|
@ -78,9 +81,13 @@ class NetworkStateMonitor(private val context: Context) {
|
|||
override fun onCapabilitiesChanged(network: Network, caps: NetworkCapabilities) {
|
||||
val hasInternet = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
val wasValidated = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
Log.d(TAG, "Network $network capabilities changed - hasInternet: $hasInternet, validated: $wasValidated")
|
||||
|
||||
if (hasInternet) {
|
||||
_hasValidatedInternet.value = true
|
||||
_internetNetwork.value = network
|
||||
Log.i(TAG, "Network $network now provides validated internet access")
|
||||
}
|
||||
updateStatus()
|
||||
}
|
||||
|
|
@ -93,21 +100,27 @@ class NetworkStateMonitor(private val context: Context) {
|
|||
val isWifi = caps?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||
val isValidated = caps?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) == true
|
||||
|
||||
Log.d(TAG, "WiFi network available: $network, validated=$isValidated")
|
||||
Log.d(TAG, "WiFi network available: $network, isWifi=$isWifi, validated=$isValidated")
|
||||
|
||||
// Bee AP is unvalidated WiFi
|
||||
if (isWifi && !isValidated) {
|
||||
Log.i(TAG, "Unvalidated WiFi detected - likely Bee AP: $network")
|
||||
_hasBeeNetwork.value = true
|
||||
_beeNetwork.value = network
|
||||
updateStatus()
|
||||
} else {
|
||||
Log.d(TAG, "WiFi network $network is validated - not Bee AP")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
Log.d(TAG, "WiFi network lost: $network")
|
||||
if (_beeNetwork.value == network) {
|
||||
Log.w(TAG, "Lost Bee network: $network")
|
||||
_hasBeeNetwork.value = false
|
||||
_beeNetwork.value = null
|
||||
} else {
|
||||
Log.d(TAG, "Lost WiFi network was not our current Bee network")
|
||||
}
|
||||
updateStatus()
|
||||
}
|
||||
|
|
@ -116,12 +129,16 @@ class NetworkStateMonitor(private val context: Context) {
|
|||
val isWifi = caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
val isValidated = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
|
||||
Log.d(TAG, "WiFi network $network capabilities changed - wifi=$isWifi, validated=$isValidated")
|
||||
|
||||
if (isWifi && !isValidated) {
|
||||
// This is likely the Bee AP
|
||||
Log.i(TAG, "WiFi network $network is unvalidated - setting as Bee network")
|
||||
_hasBeeNetwork.value = true
|
||||
_beeNetwork.value = network
|
||||
} else if (_beeNetwork.value == network && isValidated) {
|
||||
// Network became validated (switched to home WiFi)
|
||||
Log.i(TAG, "Bee network $network became validated - likely switched to home WiFi")
|
||||
_hasBeeNetwork.value = false
|
||||
_beeNetwork.value = null
|
||||
}
|
||||
|
|
@ -139,6 +156,8 @@ class NetworkStateMonitor(private val context: Context) {
|
|||
internet -> "Online (no Bee)"
|
||||
else -> "No network"
|
||||
}
|
||||
|
||||
Log.d(TAG, "Network status updated: ${_networkStatus.value} (bee=$bee, internet=$internet)")
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -146,8 +165,12 @@ class NetworkStateMonitor(private val context: Context) {
|
|||
* Call from Application.onCreate() or early in the app lifecycle.
|
||||
*/
|
||||
fun startMonitoring() {
|
||||
if (isMonitoring) return
|
||||
if (isMonitoring) {
|
||||
Log.d(TAG, "Network monitoring already started - ignoring")
|
||||
return
|
||||
}
|
||||
isMonitoring = true
|
||||
Log.i(TAG, "Starting network monitoring...")
|
||||
|
||||
// Request for validated internet (real connectivity)
|
||||
val validatedRequest = NetworkRequest.Builder()
|
||||
|
|
@ -161,11 +184,14 @@ class NetworkStateMonitor(private val context: Context) {
|
|||
.build()
|
||||
|
||||
try {
|
||||
Log.d(TAG, "Registering validated internet callback...")
|
||||
connectivityManager.registerNetworkCallback(validatedRequest, validatedCallback)
|
||||
Log.d(TAG, "Registering WiFi callback...")
|
||||
connectivityManager.registerNetworkCallback(wifiRequest, unvalidatedWifiCallback)
|
||||
Log.d(TAG, "Network monitoring started")
|
||||
Log.i(TAG, "Network monitoring callbacks registered successfully")
|
||||
|
||||
// Initial state check
|
||||
Log.d(TAG, "Performing initial network state check...")
|
||||
checkCurrentState()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to register network callbacks", e)
|
||||
|
|
@ -194,18 +220,28 @@ class NetworkStateMonitor(private val context: Context) {
|
|||
private fun checkCurrentState() {
|
||||
// Find all networks and categorize them
|
||||
val networks = connectivityManager.allNetworks
|
||||
Log.d(TAG, "Checking current network state - found ${networks.size} networks")
|
||||
|
||||
var foundBee = false
|
||||
var foundInternet = false
|
||||
|
||||
for (network in networks) {
|
||||
val caps = connectivityManager.getNetworkCapabilities(network) ?: continue
|
||||
val caps = connectivityManager.getNetworkCapabilities(network)
|
||||
if (caps == null) {
|
||||
Log.d(TAG, "Network $network has no capabilities - skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
val isWifi = caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
val isCellular = caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
val isValidated = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
val hasInternet = caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
|
||||
Log.d(TAG, "Network $network: wifi=$isWifi, cellular=$isCellular, validated=$isValidated, internet=$hasInternet")
|
||||
|
||||
if (isWifi && !isValidated) {
|
||||
// Unvalidated WiFi = Bee AP
|
||||
Log.i(TAG, "Found Bee AP network: $network")
|
||||
_hasBeeNetwork.value = true
|
||||
_beeNetwork.value = network
|
||||
foundBee = true
|
||||
|
|
@ -213,6 +249,7 @@ class NetworkStateMonitor(private val context: Context) {
|
|||
|
||||
if (hasInternet && isValidated) {
|
||||
// Validated internet connection
|
||||
Log.i(TAG, "Found validated internet network: $network (${if (isWifi) "WiFi" else if (isCellular) "cellular" else "unknown"})")
|
||||
_hasValidatedInternet.value = true
|
||||
_internetNetwork.value = network
|
||||
foundInternet = true
|
||||
|
|
@ -220,15 +257,18 @@ class NetworkStateMonitor(private val context: Context) {
|
|||
}
|
||||
|
||||
if (!foundBee) {
|
||||
Log.d(TAG, "No Bee AP network found in initial state")
|
||||
_hasBeeNetwork.value = false
|
||||
_beeNetwork.value = null
|
||||
}
|
||||
|
||||
if (!foundInternet) {
|
||||
Log.d(TAG, "No validated internet network found in initial state")
|
||||
_hasValidatedInternet.value = false
|
||||
_internetNetwork.value = null
|
||||
}
|
||||
|
||||
Log.i(TAG, "Initial state check complete - bee=$foundBee, internet=$foundInternet")
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class AdaMapsUploadWorker(
|
|||
) : CoroutineWorker(context, params) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AdaMapsUploader"
|
||||
private const val TAG = "VarroaUpload"
|
||||
private const val WORK_NAME = "adamaps_upload"
|
||||
private const val BATCH_SIZE = 50
|
||||
private const val CLEANUP_KEEP_COUNT = 1000
|
||||
|
|
@ -60,6 +60,7 @@ class AdaMapsUploadWorker(
|
|||
* Schedule periodic uploads when connected to internet.
|
||||
*/
|
||||
fun schedule(context: Context) {
|
||||
Log.d(TAG, "Scheduling periodic upload worker...")
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
|
@ -77,7 +78,7 @@ class AdaMapsUploadWorker(
|
|||
ExistingPeriodicWorkPolicy.KEEP,
|
||||
uploadRequest
|
||||
)
|
||||
Log.d(TAG, "Upload worker scheduled")
|
||||
Log.i(TAG, "Upload worker scheduled - 15min interval with network constraint")
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -110,47 +111,53 @@ class AdaMapsUploadWorker(
|
|||
private val settingsStore = SettingsDataStore(applicationContext)
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
Log.d(TAG, "Upload worker starting")
|
||||
Log.i(TAG, "AdaMapsUploadWorker starting - checking for unsynced detections")
|
||||
_isUploading.value = true
|
||||
_uploadError.value = null
|
||||
|
||||
try {
|
||||
// Load settings
|
||||
Log.d(TAG, "Loading settings from DataStore...")
|
||||
val settings = settingsStore.settings.first()
|
||||
Log.d(TAG, "Settings loaded - adamapsApiUrl: ${settings.adamapsApiUrl}, apiKey: ${settings.adamapsApiKey.take(8)}...")
|
||||
adamapsClient.updateConfig(settings.adamapsApiUrl, settings.adamapsApiKey)
|
||||
Log.d(TAG, "AdaMapsApiClient configuration updated")
|
||||
|
||||
// Process batches until no more unsynced detections
|
||||
var totalUploaded = 0
|
||||
var batchNum = 0
|
||||
|
||||
Log.d(TAG, "Starting batch processing loop (batch size: $BATCH_SIZE)")
|
||||
while (true) {
|
||||
Log.d(TAG, "Querying database for unsynced batch...")
|
||||
val batch = database.detectionDao().getUnsyncedBatch(BATCH_SIZE)
|
||||
if (batch.isEmpty()) {
|
||||
Log.d(TAG, "No more unsynced detections")
|
||||
Log.i(TAG, "No more unsynced detections found - upload complete")
|
||||
break
|
||||
}
|
||||
|
||||
batchNum++
|
||||
Log.d(TAG, "Uploading batch $batchNum with ${batch.size} detections")
|
||||
Log.i(TAG, "Processing batch #$batchNum with ${batch.size} detections (deviceIds: ${batch.map { it.deviceId }.distinct()})")
|
||||
|
||||
when (val result = uploadBatch(batch)) {
|
||||
is Result.Success -> {
|
||||
// Mark as synced
|
||||
val ids = batch.map { it.id }
|
||||
Log.d(TAG, "Marking ${ids.size} records as synced in database...")
|
||||
database.detectionDao().markSynced(ids)
|
||||
totalUploaded += batch.size
|
||||
_totalUploaded.value += batch.size
|
||||
Log.d(TAG, "Batch $batchNum uploaded successfully")
|
||||
Log.i(TAG, "Batch #$batchNum uploaded successfully - running total: $totalUploaded")
|
||||
}
|
||||
is Result.Retry -> {
|
||||
// Network error, WorkManager will retry
|
||||
Log.w(TAG, "Batch $batchNum failed, will retry")
|
||||
Log.w(TAG, "Batch #$batchNum failed with retryable error - WorkManager will retry later")
|
||||
_isUploading.value = false
|
||||
return Result.retry()
|
||||
}
|
||||
is Result.Failure -> {
|
||||
// Permanent error (shouldn't happen often)
|
||||
Log.e(TAG, "Batch $batchNum failed permanently")
|
||||
Log.e(TAG, "Batch #$batchNum failed permanently - aborting upload worker")
|
||||
_isUploading.value = false
|
||||
return Result.failure()
|
||||
}
|
||||
|
|
@ -159,19 +166,22 @@ class AdaMapsUploadWorker(
|
|||
|
||||
// Cleanup old synced records periodically
|
||||
if (totalUploaded > 0) {
|
||||
Log.d(TAG, "Cleaning up old synced records (keeping $CLEANUP_KEEP_COUNT most recent)...")
|
||||
val deleted = database.detectionDao().cleanupOldSynced(CLEANUP_KEEP_COUNT)
|
||||
if (deleted > 0) {
|
||||
Log.d(TAG, "Cleaned up $deleted old synced records")
|
||||
Log.i(TAG, "Database cleanup complete - deleted $deleted old synced records")
|
||||
} else {
|
||||
Log.d(TAG, "No old records to clean up")
|
||||
}
|
||||
}
|
||||
|
||||
_lastUploadTime.value = System.currentTimeMillis()
|
||||
_isUploading.value = false
|
||||
Log.d(TAG, "Upload worker completed: $totalUploaded detections uploaded")
|
||||
Log.i(TAG, "Upload worker completed successfully: $totalUploaded detections uploaded in $batchNum batches")
|
||||
return Result.success()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Upload worker error", e)
|
||||
Log.e(TAG, "Upload worker encountered unexpected error", e)
|
||||
_uploadError.value = e.message
|
||||
_isUploading.value = false
|
||||
return Result.retry()
|
||||
|
|
@ -179,6 +189,8 @@ class AdaMapsUploadWorker(
|
|||
}
|
||||
|
||||
private suspend fun uploadBatch(batch: List<DetectionEntity>): Result {
|
||||
Log.d(TAG, "Converting ${batch.size} DetectionEntities to AdaMapsDetection format...")
|
||||
|
||||
// Convert to ADAMaps format
|
||||
val request = batch.map { entity ->
|
||||
AdaMapsDetection(
|
||||
|
|
@ -198,18 +210,22 @@ class AdaMapsUploadWorker(
|
|||
)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Calling AdaMapsApiClient.ingest() with ${request.size} detections...")
|
||||
return when (val result = adamapsClient.ingest(request)) {
|
||||
is ApiResult.Success -> {
|
||||
Log.i(TAG, "Batch upload successful - server response: ${result.data.take(200)}...")
|
||||
Result.success()
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
_uploadError.value = result.message
|
||||
Log.e(TAG, "Batch upload failed - error: ${result.message}, HTTP code: ${result.code}")
|
||||
|
||||
// Determine if this is a retryable error
|
||||
val isRetryable = result.code == null || // Network error
|
||||
result.code >= 500 || // Server error
|
||||
result.code == 429 // Rate limited
|
||||
|
||||
Log.d(TAG, "Error classification: ${if (isRetryable) "retryable" else "permanent"}")
|
||||
if (isRetryable) Result.retry() else Result.failure()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import kotlin.math.min
|
|||
class BeeCollectorService : LifecycleService() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "BeeCollector"
|
||||
private const val TAG = "VarroaBee"
|
||||
const val NOTIF_CHANNEL_ID = "varroa_collector"
|
||||
const val NOTIF_ID = 1001
|
||||
|
||||
|
|
@ -89,21 +89,28 @@ class BeeCollectorService : LifecycleService() {
|
|||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "BeeCollectorService onCreate() called")
|
||||
createNotificationChannel()
|
||||
beeClient = BeeApiClient()
|
||||
database = VarroaDatabase.getInstance(applicationContext)
|
||||
networkMonitor = NetworkStateMonitor.getInstance(applicationContext)
|
||||
settingsStore = SettingsDataStore(applicationContext)
|
||||
Log.d(TAG, "BeeCollectorService onCreate() completed - all dependencies initialized")
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
Log.d(TAG, "onStartCommand() called with action: ${intent?.action}, startId: $startId")
|
||||
when (intent?.action) {
|
||||
ACTION_STOP -> {
|
||||
Log.i(TAG, "Received ACTION_STOP - stopping service")
|
||||
stopCollecting()
|
||||
stopSelf()
|
||||
}
|
||||
else -> startCollecting()
|
||||
else -> {
|
||||
Log.i(TAG, "Starting collection service")
|
||||
startCollecting()
|
||||
}
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
|
@ -114,68 +121,93 @@ class BeeCollectorService : LifecycleService() {
|
|||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(TAG, "onDestroy() called - cleaning up BeeCollectorService")
|
||||
stopCollecting()
|
||||
super.onDestroy()
|
||||
Log.d(TAG, "BeeCollectorService destroyed successfully")
|
||||
}
|
||||
|
||||
private fun startCollecting() {
|
||||
Log.i(TAG, "Starting collection - moving to foreground")
|
||||
startForeground(NOTIF_ID, buildNotification(0))
|
||||
_isRunning.value = true
|
||||
_beeStatus.value = "Connecting..."
|
||||
_sessionCollected.value = 0
|
||||
Log.d(TAG, "Collection state initialized - isRunning=true, sessionCollected=0")
|
||||
|
||||
lifecycleScope.launch {
|
||||
Log.d(TAG, "Loading settings from DataStore...")
|
||||
val settings = settingsStore.settings.first()
|
||||
Log.d(TAG, "Settings loaded - beeApiUrl: ${settings.beeApiUrl}, altUrl: ${settings.beeAltApiUrl}, pollInterval: ${settings.pollIntervalSeconds}s")
|
||||
|
||||
beeClient.updateUrls(settings.beeApiUrl, settings.beeAltApiUrl)
|
||||
Log.d(TAG, "BeeApiClient URLs updated")
|
||||
|
||||
// Bind to Bee network (unvalidated WiFi)
|
||||
Log.d(TAG, "Attempting to bind to Bee network...")
|
||||
bindToBeeNetwork()
|
||||
|
||||
// Fetch device ID once
|
||||
Log.d(TAG, "Fetching device ID from Bee...")
|
||||
fetchDeviceId()
|
||||
|
||||
// Start polling loop
|
||||
Log.i(TAG, "Starting poll loop with ${settings.pollIntervalSeconds}s interval")
|
||||
startPollLoop(settings.pollIntervalSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopCollecting() {
|
||||
Log.i(TAG, "Stopping collection service")
|
||||
pollJob?.cancel()
|
||||
Log.d(TAG, "Poll job cancelled")
|
||||
_isRunning.value = false
|
||||
_beeStatus.value = "Stopped"
|
||||
_beeConnected.value = false
|
||||
Log.d(TAG, "Collection stopped - isRunning=false, beeConnected=false")
|
||||
}
|
||||
|
||||
private fun bindToBeeNetwork() {
|
||||
// Use NetworkStateMonitor to get the unvalidated WiFi network (Bee AP)
|
||||
Log.d(TAG, "Getting Bee network from NetworkStateMonitor...")
|
||||
val beeNet = networkMonitor.getBeeNetworkForBinding()
|
||||
if (beeNet != null) {
|
||||
Log.i(TAG, "Found Bee network: $beeNet - binding BeeApiClient")
|
||||
beeClient.bindToNetwork(beeNet)
|
||||
Log.d(TAG, "Bound to Bee network: $beeNet")
|
||||
} else {
|
||||
// Fallback to legacy binding
|
||||
Log.w(TAG, "No Bee network found in NetworkStateMonitor - using legacy WiFi binding")
|
||||
beeClient.bindToWifiNetwork(applicationContext)
|
||||
Log.d(TAG, "Using legacy WiFi binding")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchDeviceId() {
|
||||
Log.d(TAG, "Requesting device info from Bee API...")
|
||||
when (val result = beeClient.getDeviceInfo()) {
|
||||
is ApiResult.Success -> {
|
||||
val deviceId = result.data.deviceId ?: result.data.serial ?: "unknown"
|
||||
_currentDeviceId.value = deviceId
|
||||
Log.d(TAG, "Device ID: $deviceId")
|
||||
Log.i(TAG, "Device ID retrieved: $deviceId (from ${if (result.data.deviceId != null) "deviceId" else if (result.data.serial != null) "serial" else "fallback"})")
|
||||
}
|
||||
is ApiResult.Error -> {
|
||||
Log.w(TAG, "Failed to get device ID: ${result.message}")
|
||||
Log.e(TAG, "Failed to get device ID: ${result.message}, code: ${result.code}")
|
||||
_currentDeviceId.value = "unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startPollLoop(intervalSeconds: Int) {
|
||||
pollJob?.cancel()
|
||||
Log.d(TAG, "Previous poll job cancelled")
|
||||
pollJob = lifecycleScope.launch {
|
||||
Log.i(TAG, "Poll loop started with ${intervalSeconds}s interval")
|
||||
var pollCount = 0
|
||||
while (true) {
|
||||
pollCount++
|
||||
Log.d(TAG, "Poll cycle #$pollCount starting...")
|
||||
|
||||
// Re-bind to Bee network periodically in case it changed
|
||||
bindToBeeNetwork()
|
||||
|
||||
|
|
@ -186,18 +218,21 @@ class BeeCollectorService : LifecycleService() {
|
|||
} else {
|
||||
currentBackoffMs
|
||||
}
|
||||
Log.d(TAG, "Poll cycle #$pollCount completed - next attempt in ${delayMs}ms (connected=${_beeConnected.value})")
|
||||
delay(delayMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runPollCycle() {
|
||||
Log.d(TAG, "Calling beeClient.getLandmarks()...")
|
||||
when (val result = beeClient.getLandmarks()) {
|
||||
is ApiResult.Success -> {
|
||||
_beeConnected.value = true
|
||||
consecutiveFailures = 0
|
||||
currentBackoffMs = MIN_BACKOFF_MS
|
||||
_lastError.value = null
|
||||
Log.i(TAG, "Successfully connected to Bee - received ${result.data.size} detections")
|
||||
|
||||
val activeUrl = beeClient.getActiveUrl()
|
||||
_beeStatus.value = if (activeUrl?.contains("155") == true) {
|
||||
|
|
@ -205,6 +240,7 @@ class BeeCollectorService : LifecycleService() {
|
|||
} else {
|
||||
"Connected (AP mode)"
|
||||
}
|
||||
Log.d(TAG, "Active URL: $activeUrl, Status: ${_beeStatus.value}")
|
||||
|
||||
// Filter for new detections
|
||||
val newDetections = result.data.filter { d ->
|
||||
|
|
@ -213,8 +249,12 @@ class BeeCollectorService : LifecycleService() {
|
|||
else { seenKeys.add(key); true }
|
||||
}
|
||||
|
||||
Log.d(TAG, "Filtered detections: ${result.data.size} total, ${newDetections.size} new, ${result.data.size - newDetections.size} duplicates")
|
||||
if (newDetections.isNotEmpty()) {
|
||||
Log.i(TAG, "Storing ${newDetections.size} new detections to database...")
|
||||
storeDetections(newDetections)
|
||||
} else {
|
||||
Log.d(TAG, "No new detections to store")
|
||||
}
|
||||
|
||||
updateNotification(_sessionCollected.value)
|
||||
|
|
@ -222,6 +262,7 @@ class BeeCollectorService : LifecycleService() {
|
|||
is ApiResult.Error -> {
|
||||
_beeConnected.value = false
|
||||
consecutiveFailures++
|
||||
Log.e(TAG, "Poll failed - error: ${result.message}, code: ${result.code}, consecutive failures: $consecutiveFailures")
|
||||
|
||||
currentBackoffMs = min(
|
||||
(MIN_BACKOFF_MS * Math.pow(BACKOFF_MULTIPLIER, (consecutiveFailures - 1).toDouble())).toLong(),
|
||||
|
|
@ -235,9 +276,11 @@ class BeeCollectorService : LifecycleService() {
|
|||
result.message.contains("unreachable", ignoreCase = true)) {
|
||||
_beeStatus.value = "Bee offline (retry in ${currentBackoffMs / 1000}s)"
|
||||
_lastError.value = null
|
||||
Log.w(TAG, "Bee appears offline - will retry in ${currentBackoffMs}ms")
|
||||
} else {
|
||||
_beeStatus.value = "Connection error"
|
||||
_lastError.value = result.message
|
||||
Log.e(TAG, "Connection error (non-timeout): ${result.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -245,6 +288,7 @@ class BeeCollectorService : LifecycleService() {
|
|||
|
||||
private suspend fun storeDetections(detections: List<BeeDetection>) {
|
||||
val deviceId = _currentDeviceId.value
|
||||
Log.d(TAG, "Converting ${detections.size} BeeDetections to DetectionEntities with deviceId: $deviceId")
|
||||
|
||||
val entities = detections.map { d ->
|
||||
DetectionEntity(
|
||||
|
|
@ -265,15 +309,18 @@ class BeeCollectorService : LifecycleService() {
|
|||
}
|
||||
|
||||
try {
|
||||
Log.d(TAG, "Inserting ${entities.size} entities into database...")
|
||||
val inserted = database.detectionDao().insertAll(entities)
|
||||
val newCount = inserted.count { it != -1L }
|
||||
val duplicateCount = inserted.size - newCount
|
||||
|
||||
Log.i(TAG, "Database insert result: $newCount new records, $duplicateCount duplicates (ignored)")
|
||||
if (newCount > 0) {
|
||||
_sessionCollected.update { it + newCount }
|
||||
Log.d(TAG, "Stored $newCount new detections (${inserted.size - newCount} duplicates)")
|
||||
Log.i(TAG, "Session total updated: ${_sessionCollected.value} detections collected")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to store detections", e)
|
||||
Log.e(TAG, "Failed to store detections to database", e)
|
||||
_lastError.value = "DB error: ${e.message}"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue