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:
parent
3ae8bf00c1
commit
bbd51d1c35
10 changed files with 1200 additions and 167 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue