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