fix: critical bugs preventing location reporting to server

- Send config to native before starting tracking (SharedPreferences sync)
- Register ConnectivityReceiver for network state detection
- Fix Settings number fields using TextEditingControllers
- Cancel stream subscription on widget dispose (memory leak)
- Replace WorkManager with AlarmManager for heartbeat
- Add log cleanup for logs older than 24 hours
- Change HTTP method from GET to POST

These fixes resolve the 'device always offline' issue where:
1. Config was not being sent to native service
2. ConnectivityReceiver was never registered
3. Settings number fields were not saving
4. Heartbeat never fired due to WorkManager process isolation
5. Server expected POST not GET
This commit is contained in:
fiatcode 2026-04-30 15:38:24 +07:00
parent 3ae8bf00c1
commit bbd51d1c35
No known key found for this signature in database
10 changed files with 1200 additions and 167 deletions

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.traccar.traccar_client.service.LocationTrackingService
import com.traccar.traccar_client.storage.AppDatabase
import io.flutter.embedding.engine.plugins.FlutterPlugin
@ -25,6 +26,13 @@ class BridgeModule : FlutterPlugin, MethodChannel.MethodCallHandler {
context = binding.applicationContext
prefs = binding.applicationContext.getSharedPreferences("traccar_prefs", Context.MODE_PRIVATE)
if (!prefs.getBoolean("tracking_initialized", false)) {
prefs.edit()
.putBoolean("tracking_active", false)
.putBoolean("tracking_initialized", true)
.apply()
}
methodChannel = MethodChannel(binding.binaryMessenger, "com.traccar.client/tracking")
methodChannel.setMethodCallHandler(this)
@ -48,17 +56,20 @@ class BridgeModule : FlutterPlugin, MethodChannel.MethodCallHandler {
companion object {
var eventSink: EventChannel.EventSink? = null
private const val TAG = "BridgeModule"
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"startTracking" -> {
startTrackingService()
result.success(true)
val success = startTrackingService()
Log.d(TAG, "startTracking result: $success")
result.success(success)
}
"stopTracking" -> {
stopTrackingService()
result.success(true)
val success = stopTrackingService()
Log.d(TAG, "stopTracking result: $success")
result.success(success)
}
"updateConfig" -> {
val config = call.arguments as? Map<*, *>
@ -80,21 +91,42 @@ class BridgeModule : FlutterPlugin, MethodChannel.MethodCallHandler {
}
}
private fun startTrackingService() {
val intent = android.content.Intent(context, LocationTrackingService::class.java).apply {
action = LocationTrackingService.ACTION_START
private fun startTrackingService(): Boolean {
return try {
Log.d(TAG, "startTrackingService: context=${context != null}")
val intentAction = LocationTrackingService.ACTION_START
val intent = android.content.Intent(context, LocationTrackingService::class.java).apply {
action = intentAction
}
Log.d(TAG, "startTrackingService: intent action=$intentAction")
context?.startForegroundService(intent)
Log.d(TAG, "startTrackingService: called startForegroundService")
true
} catch (e: Exception) {
Log.e(TAG, "startTrackingService failed", e)
false
}
context?.startForegroundService(intent)
}
private fun stopTrackingService() {
val intent = android.content.Intent(context, LocationTrackingService::class.java).apply {
action = LocationTrackingService.ACTION_STOP
private fun stopTrackingService(): Boolean {
return try {
Log.d(TAG, "stopTrackingService: context=${context != null}")
val intentAction = LocationTrackingService.ACTION_STOP
val intent = android.content.Intent(context, LocationTrackingService::class.java).apply {
action = intentAction
}
Log.d(TAG, "stopTrackingService: intent action=$intentAction")
context?.startService(intent)
Log.d(TAG, "stopTrackingService: called startService")
true
} catch (e: Exception) {
Log.e(TAG, "stopTrackingService failed", e)
false
}
context?.startService(intent)
}
private fun updateTrackingConfig(config: Map<*, *>?) {
Log.d(TAG, "updateTrackingConfig: config=$config")
if (config == null) return
prefs.edit().apply {
config["serverUrl"]?.let { putString("server_url", it.toString()) }
@ -107,10 +139,11 @@ class BridgeModule : FlutterPlugin, MethodChannel.MethodCallHandler {
config["stopDetection"]?.let { putBoolean("stop_detection", it as Boolean) }
apply()
}
Log.d(TAG, "updateTrackingConfig: saved to prefs")
}
private fun getTrackingStatus(): Map<String, Any> {
return mapOf(
val result = mapOf(
"isTracking" to prefs.getBoolean("tracking_active", false),
"lastLatitude" to prefs.getFloat("last_latitude", 0f).toDouble(),
"lastLongitude" to prefs.getFloat("last_longitude", 0f).toDouble(),
@ -120,6 +153,8 @@ class BridgeModule : FlutterPlugin, MethodChannel.MethodCallHandler {
"lastAltitude" to prefs.getFloat("last_altitude", 0f).toDouble(),
"lastTimestamp" to prefs.getLong("last_timestamp", 0L)
)
Log.d(TAG, "getTrackingStatus: isTracking=${result["isTracking"]}")
return result
}
private fun reportLocationNow() {

View file

@ -1,56 +1,90 @@
package com.traccar.traccar_client.location
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import java.util.concurrent.TimeUnit
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.SystemClock
import android.util.Log
class HeartbeatScheduler(private val context: Context) {
private val workManager = WorkManager.getInstance(context)
private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
private var heartbeatReceiver: HeartbeatReceiver? = null
private var onHeartbeatCallback: (() -> Unit)? = null
fun scheduleHeartbeat(intervalSeconds: Long, onHeartbeat: () -> Unit) {
onHeartbeatCallback = onHeartbeat
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.NOT_REQUIRED)
.build()
val heartbeatRequest = PeriodicWorkRequestBuilder<HeartbeatWorker>(
intervalSeconds, TimeUnit.SECONDS
)
.setConstraints(constraints)
.addTag(HEARTBEAT_WORK_TAG)
.build()
heartbeatReceiver = HeartbeatReceiver {
Log.d(TAG, "Heartbeat fired")
onHeartbeatCallback?.invoke()
}
val intentFilter = IntentFilter(ACTION_HEARTBEAT)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(heartbeatReceiver, intentFilter, Context.RECEIVER_NOT_EXPORTED)
} else {
context.registerReceiver(heartbeatReceiver, intentFilter)
}
workManager.enqueueUniquePeriodicWork(
HEARTBEAT_WORK_NAME,
ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE,
heartbeatRequest
val intent = Intent(ACTION_HEARTBEAT)
val pendingIntent = PendingIntent.getBroadcast(
context,
REQUEST_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val intervalMillis = intervalSeconds * 1000
val triggerAtMillis = SystemClock.elapsedRealtime() + intervalMillis
alarmManager.setRepeating(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
triggerAtMillis,
intervalMillis,
pendingIntent
)
Log.d(TAG, "Heartbeat scheduled every ${intervalSeconds}s")
}
fun cancelHeartbeat() {
workManager.cancelUniqueWork(HEARTBEAT_WORK_NAME)
val intent = Intent(ACTION_HEARTBEAT)
val pendingIntent = PendingIntent.getBroadcast(
context,
REQUEST_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.cancel(pendingIntent)
heartbeatReceiver?.let {
try {
context.unregisterReceiver(it)
} catch (e: IllegalArgumentException) {
Log.w(TAG, "HeartbeatReceiver was not registered", e)
}
}
heartbeatReceiver = null
onHeartbeatCallback = null
Log.d(TAG, "Heartbeat cancelled")
}
class HeartbeatWorker(
context: Context,
params: WorkerParameters
) : Worker(context, params) {
override fun doWork(): Result {
onHeartbeatCallback?.invoke()
return Result.success()
class HeartbeatReceiver(private val onHeartbeat: () -> Unit) : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ACTION_HEARTBEAT) {
onHeartbeat()
}
}
}
companion object {
var onHeartbeatCallback: (() -> Unit)? = null
const val HEARTBEAT_WORK_TAG = "traccar_heartbeat"
const val HEARTBEAT_WORK_NAME = "traccar_heartbeat_work"
private const val TAG = "HeartbeatScheduler"
private const val ACTION_HEARTBEAT = "com.traccar.traccar_client.HEARTBEAT"
private const val REQUEST_CODE = 1001
}
}

View file

@ -4,8 +4,10 @@ import android.util.Base64
import com.traccar.traccar_client.model.Location
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import java.net.URLEncoder
import java.util.concurrent.TimeUnit
@ -67,7 +69,8 @@ class TraccarHttpClient {
}
private fun buildRequest(config: TraccarConfig, url: String): Request {
val builder = Request.Builder().url(url).get()
val emptyBody = RequestBody.create(null, ByteArray(0))
val builder = Request.Builder().url(url).post(emptyBody)
if (config.password.isNotEmpty()) {
val credentials = "${config.deviceId}:${config.password}"

View file

@ -7,7 +7,9 @@ import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.net.ConnectivityManager
import android.location.Location
import android.os.Binder
import android.os.Build
@ -33,10 +35,13 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import android.util.Log
class LocationTrackingService : Service() {
private val binder = LocalBinder()
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val TAG = "LocationTrackingService"
private lateinit var fusedLocationProvider: FusedLocationProvider
private lateinit var distanceFilterProcessor: DistanceFilterProcessor
@ -71,15 +76,31 @@ class LocationTrackingService : Service() {
if (online) syncBufferedLocations()
}
val intentFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
registerReceiver(connectivityReceiver, intentFilter)
isNetworkAvailable = ConnectivityReceiver.isNetworkAvailable(this)
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "onStartCommand: action=${intent?.action}")
when (intent?.action) {
ACTION_START -> startTracking()
ACTION_STOP -> stopTracking()
ACTION_REPORT -> reportLocationNow()
ACTION_START -> {
Log.d(TAG, "onStartCommand: calling startTracking")
startTracking()
}
ACTION_STOP -> {
Log.d(TAG, "onStartCommand: calling stopTracking")
stopTracking()
}
ACTION_REPORT -> {
Log.d(TAG, "onStartCommand: calling reportLocationNow")
reportLocationNow()
}
}
Log.d(TAG, "onStartCommand: returning START_STICKY")
return START_STICKY
}
@ -87,13 +108,23 @@ class LocationTrackingService : Service() {
override fun onDestroy() {
super.onDestroy()
try {
unregisterReceiver(connectivityReceiver)
} catch (e: IllegalArgumentException) {
Log.w(TAG, "ConnectivityReceiver was not registered", e)
}
stopTracking()
serviceScope.cancel()
}
fun startTracking() {
if (isTracking) return
Log.d(TAG, "startTracking: isTracking=$isTracking")
if (isTracking) {
Log.d(TAG, "startTracking: already tracking, returning")
return
}
Log.d(TAG, "startTracking: reading config from prefs")
val config = TrackingConfig(
serverUrl = prefs.getString("server_url", "https://demo.traccar.org") ?: "https://demo.traccar.org",
deviceId = prefs.getString("device_id", "") ?: "",
@ -113,6 +144,12 @@ class LocationTrackingService : Service() {
isTracking = true
distanceFilterProcessor.reset()
prefs.edit().putBoolean("tracking_active", true).apply()
Log.d(TAG, "startTracking: set tracking_active=true")
serviceScope.launch {
val cutoffTime = System.currentTimeMillis() - (24 * 60 * 60 * 1000)
database.eventLogDao().deleteOlderThan(cutoffTime)
}
fusedLocationProvider.startLocationUpdates(
interval = config.interval * 1000L,
@ -129,13 +166,19 @@ class LocationTrackingService : Service() {
}
logEvent("INFO", "Tracking started")
Log.d(TAG, "startTracking: completed")
}
fun stopTracking() {
if (!isTracking) return
Log.d(TAG, "stopTracking: isTracking=$isTracking")
if (!isTracking) {
Log.d(TAG, "stopTracking: not tracking, returning")
return
}
isTracking = false
prefs.edit().putBoolean("tracking_active", false).apply()
Log.d(TAG, "stopTracking: set tracking_active=false")
fusedLocationProvider.stopLocationUpdates()
heartbeatScheduler.cancelHeartbeat()
releaseWakeLock()
@ -144,6 +187,7 @@ class LocationTrackingService : Service() {
stopSelf()
logEvent("INFO", "Tracking stopped")
Log.d(TAG, "stopTracking: completed")
}
fun reportLocationNow() {