From 63801d8f124ff3836fd04e2bd64338215f43a06a Mon Sep 17 00:00:00 2001 From: Kayos Date: Wed, 11 Mar 2026 12:20:04 -0700 Subject: [PATCH] v7.1: add verbose debug logging --- app/build.gradle.kts | 4 +- .../java/com/adamaps/varroa/MainActivity.kt | 33 +++++++++++ .../adamaps/varroa/api/AdaMapsApiClient.kt | 31 ++++++++-- .../com/adamaps/varroa/api/BeeApiClient.kt | 44 ++++++++++++-- .../varroa/network/NetworkStateMonitor.kt | 54 ++++++++++++++--- .../varroa/service/AdaMapsUploadWorker.kt | 38 ++++++++---- .../varroa/service/BeeCollectorService.kt | 59 +++++++++++++++++-- 7 files changed, 229 insertions(+), 34 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ddd662e..bb69638 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 diff --git a/app/src/main/java/com/adamaps/varroa/MainActivity.kt b/app/src/main/java/com/adamaps/varroa/MainActivity.kt index 40dc3fb..9aa0390 100644 --- a/app/src/main/java/com/adamaps/varroa/MainActivity.kt +++ b/app/src/main/java/com/adamaps/varroa/MainActivity.kt @@ -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() } } diff --git a/app/src/main/java/com/adamaps/varroa/api/AdaMapsApiClient.kt b/app/src/main/java/com/adamaps/varroa/api/AdaMapsApiClient.kt index a06c8a1..a7452af 100644 --- a/app/src/main/java/com/adamaps/varroa/api/AdaMapsApiClient.kt +++ b/app/src/main/java/com/adamaps/varroa/api/AdaMapsApiClient.kt @@ -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 = 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") } } diff --git a/app/src/main/java/com/adamaps/varroa/api/BeeApiClient.kt b/app/src/main/java/com/adamaps/varroa/api/BeeApiClient.kt index 7443b02..3f5d685 100644 --- a/app/src/main/java/com/adamaps/varroa/api/BeeApiClient.kt +++ b/app/src/main/java/com/adamaps/varroa/api/BeeApiClient.kt @@ -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 = 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> = 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>() {}.type val list: List = 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 } diff --git a/app/src/main/java/com/adamaps/varroa/network/NetworkStateMonitor.kt b/app/src/main/java/com/adamaps/varroa/network/NetworkStateMonitor.kt index 218c310..23e1b4c 100644 --- a/app/src/main/java/com/adamaps/varroa/network/NetworkStateMonitor.kt +++ b/app/src/main/java/com/adamaps/varroa/network/NetworkStateMonitor.kt @@ -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() } diff --git a/app/src/main/java/com/adamaps/varroa/service/AdaMapsUploadWorker.kt b/app/src/main/java/com/adamaps/varroa/service/AdaMapsUploadWorker.kt index efb16e4..8b04f7f 100644 --- a/app/src/main/java/com/adamaps/varroa/service/AdaMapsUploadWorker.kt +++ b/app/src/main/java/com/adamaps/varroa/service/AdaMapsUploadWorker.kt @@ -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): 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() } } diff --git a/app/src/main/java/com/adamaps/varroa/service/BeeCollectorService.kt b/app/src/main/java/com/adamaps/varroa/service/BeeCollectorService.kt index 4284932..e68e8a3 100644 --- a/app/src/main/java/com/adamaps/varroa/service/BeeCollectorService.kt +++ b/app/src/main/java/com/adamaps/varroa/service/BeeCollectorService.kt @@ -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) { 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}" } }