v7.1: add verbose debug logging

This commit is contained in:
Kayos 2026-03-11 12:20:04 -07:00
parent 964d175454
commit 63801d8f12
7 changed files with 229 additions and 34 deletions

View file

@ -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

View file

@ -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()
}
}

View file

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

View file

@ -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
}

View file

@ -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()
}

View file

@ -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()
}
}

View file

@ -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}"
}
}