fix(sync): run sync worker as foreground service

The background sync was being killed by Android when the app went to
background or screen turned off. This fix:

- Adds FOREGROUND_SERVICE and FOREGROUND_SERVICE_DATA_SYNC permissions
- Adds POST_NOTIFICATIONS permission for the sync notification
- Runs DataSyncWorker as a foreground service with a notification
- Creates a low-priority notification channel for sync status

This ensures the sync continues even when the app is not in foreground.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Davide Ferrari
2026-01-05 09:54:02 +01:00
co-authored by Claude Opus 4.5
parent 1875070a61
commit d4f8e864ca
2 changed files with 58 additions and 1 deletions
+3
View File
@@ -3,6 +3,9 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name=".MateDroidApp"
@@ -1,10 +1,17 @@
package com.matedroid.data.sync
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.pm.ServiceInfo
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import com.matedroid.R
import com.matedroid.data.repository.ApiResult
import com.matedroid.data.repository.TeslamateRepository
import dagger.assisted.Assisted
@@ -16,7 +23,7 @@ import kotlinx.coroutines.coroutineScope
/**
* Background worker for syncing stats data from TeslamateApi.
*
* Runs on app launch and syncs all cars in parallel:
* Runs as a foreground service to prevent being killed when app is in background.
* - Phase 1: Sync summaries (fast, for Quick Stats)
* - Phase 2: Sync details (slow, for Deep Stats)
*/
@@ -32,11 +39,16 @@ class DataSyncWorker @AssistedInject constructor(
companion object {
const val TAG = "DataSyncWorker"
const val WORK_NAME = "data_sync_work"
const val NOTIFICATION_ID = 1001
const val CHANNEL_ID = "sync_channel"
}
override suspend fun doWork(): Result {
Log.d(TAG, "Starting data sync worker")
// Run as foreground service to prevent being killed
setForeground(createForegroundInfo("Syncing data..."))
// Get list of cars
val carsResult = teslamateRepository.getCars()
val cars = when (carsResult) {
@@ -74,4 +86,46 @@ class DataSyncWorker @AssistedInject constructor(
return if (allSuccess) Result.success() else Result.success() // Still success to not retry indefinitely
}
/**
* Create foreground info for the notification.
*/
private fun createForegroundInfo(progress: String): ForegroundInfo {
val context = applicationContext
// Create notification channel (required for Android 8.0+)
createNotificationChannel()
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle("MateDroid Sync")
.setContentText(progress)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ForegroundInfo(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} else {
ForegroundInfo(NOTIFICATION_ID, notification)
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Data Sync",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Background sync for stats data"
}
val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
}