feat: rich sync notifications (progress + result + error)
Three notification channels: - sync_progress (LOW): foreground notification while syncing, shows pair name - sync_complete (LOW): result after success — "↑X ↓X" or "Up to date" - sync_alerts (DEFAULT): error notification with message on failure Notifications respect per-pair notifyOnComplete / notifyOnError settings. All notifications tap-through to MainActivity. Foreground info now names the pair being synced instead of the generic "Syncing…" text. Bump to 1.0.14 (code 15). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,12 +2,15 @@ package com.syncflow.worker
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.*
|
||||
import com.syncflow.MainActivity
|
||||
import com.syncflow.R
|
||||
import com.syncflow.data.db.SyncPairDao
|
||||
import com.syncflow.data.db.entities.toDomain
|
||||
@@ -33,44 +36,116 @@ class SyncWorker @AssistedInject constructor(
|
||||
val pairId = inputData.getLong(KEY_PAIR_ID, -1L)
|
||||
if (pairId == -1L) return Result.failure()
|
||||
|
||||
setForeground(buildForegroundInfo("Syncing…"))
|
||||
|
||||
val pair = syncPairDao.getById(pairId) ?: return Result.failure()
|
||||
val account = accountRepository.getAccount(pair.accountId) ?: return Result.failure()
|
||||
|
||||
ensureChannels()
|
||||
setForeground(buildForegroundInfo(pair.name, "Syncing…"))
|
||||
|
||||
return try {
|
||||
val domainPair = pair.toDomain()
|
||||
val provider = providerFactory.create(account)
|
||||
syncEngine.sync(domainPair, provider)
|
||||
val result = syncEngine.sync(domainPair, provider)
|
||||
|
||||
if (result.error != null && pair.notifyOnError) {
|
||||
notify(
|
||||
id = pairId.toInt() + RESULT_ID_OFFSET,
|
||||
channelId = CHANNEL_ALERTS,
|
||||
title = "${pair.name} — Sync failed",
|
||||
text = result.error.message ?: "Unknown error",
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT,
|
||||
)
|
||||
} else if (pair.notifyOnComplete && result.error == null) {
|
||||
val summary = buildString {
|
||||
if (result.uploaded > 0) append("↑${result.uploaded} ")
|
||||
if (result.downloaded > 0) append("↓${result.downloaded} ")
|
||||
if (result.deleted > 0) append("🗑${result.deleted} ")
|
||||
if (result.conflicts > 0) append("⚠${result.conflicts} conflict${if (result.conflicts != 1) "s" else ""}")
|
||||
}.trim().ifEmpty { "Up to date" }
|
||||
notify(
|
||||
id = pairId.toInt() + RESULT_ID_OFFSET,
|
||||
channelId = CHANNEL_COMPLETE,
|
||||
title = "${pair.name} — Synced",
|
||||
text = summary,
|
||||
priority = NotificationCompat.PRIORITY_LOW,
|
||||
)
|
||||
}
|
||||
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "SyncWorker failed for pair $pairId")
|
||||
if (pair.notifyOnError) {
|
||||
notify(
|
||||
id = pairId.toInt() + RESULT_ID_OFFSET,
|
||||
channelId = CHANNEL_ALERTS,
|
||||
title = "${pair.name} — Sync failed",
|
||||
text = e.message ?: "Unknown error",
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT,
|
||||
)
|
||||
}
|
||||
if (runAttemptCount < 3) Result.retry() else Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildForegroundInfo(progress: String): ForegroundInfo {
|
||||
val channelId = "sync_channel"
|
||||
private fun ensureChannels() {
|
||||
val nm = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
if (nm.getNotificationChannel(channelId) == null) {
|
||||
nm.createNotificationChannel(NotificationChannel(channelId, "Sync", NotificationManager.IMPORTANCE_LOW))
|
||||
}
|
||||
val notification = NotificationCompat.Builder(applicationContext, channelId)
|
||||
.setContentTitle("SyncFlow")
|
||||
.setContentText(progress)
|
||||
if (nm.getNotificationChannel(CHANNEL_PROGRESS) == null)
|
||||
nm.createNotificationChannel(NotificationChannel(CHANNEL_PROGRESS, "Sync progress", NotificationManager.IMPORTANCE_LOW).apply {
|
||||
description = "Shown while a sync is running"
|
||||
})
|
||||
if (nm.getNotificationChannel(CHANNEL_COMPLETE) == null)
|
||||
nm.createNotificationChannel(NotificationChannel(CHANNEL_COMPLETE, "Sync complete", NotificationManager.IMPORTANCE_LOW).apply {
|
||||
description = "Summary after each successful sync"
|
||||
})
|
||||
if (nm.getNotificationChannel(CHANNEL_ALERTS) == null)
|
||||
nm.createNotificationChannel(NotificationChannel(CHANNEL_ALERTS, "Sync errors", NotificationManager.IMPORTANCE_DEFAULT).apply {
|
||||
description = "Alerts for sync failures and conflicts"
|
||||
})
|
||||
}
|
||||
|
||||
private fun buildForegroundInfo(pairName: String, status: String): ForegroundInfo {
|
||||
val tapIntent = PendingIntent.getActivity(
|
||||
applicationContext, 0,
|
||||
Intent(applicationContext, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP },
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_PROGRESS)
|
||||
.setContentTitle(pairName)
|
||||
.setContentText(status)
|
||||
.setSmallIcon(R.drawable.ic_sync)
|
||||
.setContentIntent(tapIntent)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} else {
|
||||
else
|
||||
ForegroundInfo(NOTIFICATION_ID, notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notify(id: Int, channelId: String, title: String, text: String, priority: Int) {
|
||||
val tapIntent = PendingIntent.getActivity(
|
||||
applicationContext, id,
|
||||
Intent(applicationContext, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP },
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val nm = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
nm.notify(id, NotificationCompat.Builder(applicationContext, channelId)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setSmallIcon(R.drawable.ic_sync)
|
||||
.setPriority(priority)
|
||||
.setContentIntent(tapIntent)
|
||||
.setAutoCancel(true)
|
||||
.build())
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KEY_PAIR_ID = "pair_id"
|
||||
private const val NOTIFICATION_ID = 1001
|
||||
private const val RESULT_ID_OFFSET = 2000
|
||||
private const val CHANNEL_PROGRESS = "sync_progress"
|
||||
private const val CHANNEL_COMPLETE = "sync_complete"
|
||||
private const val CHANNEL_ALERTS = "sync_alerts"
|
||||
|
||||
fun buildOneTimeRequest(pairId: Long, wifiOnly: Boolean, chargingOnly: Boolean): OneTimeWorkRequest {
|
||||
val constraints = Constraints.Builder()
|
||||
|
||||
+2
-2
@@ -1,2 +1,2 @@
|
||||
VERSION_NAME=1.0.13
|
||||
VERSION_CODE=14
|
||||
VERSION_NAME=1.0.14
|
||||
VERSION_CODE=15
|
||||
|
||||
Reference in New Issue
Block a user