feat: fix notifications on Android 13+/16, add Log tab, fix ON_CHANGE detection

- Request POST_NOTIFICATIONS permission at runtime in MainActivity (primary fix
  for notifications never appearing on Android 13+ phones including Android 16)
- Register all 4 notification channels eagerly in SyncFlowApp.onCreate() instead
  of lazily inside workers
- Add FOREGROUND_SERVICE_SHORT_SERVICE permission + shortService foreground type
  for Android 16 foreground service compatibility
- Add global activity Log tab (new tab 2 in main nav) showing all sync events
  across all pairs, grouped by date with pair name, event icon, and file detail
- Fix FileWatchService ON_CHANGE detection: ContentObserver on SAF tree URIs only
  fires for SAF-API writes, not raw filesystem writes. Now resolves primary:/*
  tree URIs to /storage/emulated/0/* and uses FileObserver for reliable detection
- Bump version to 1.0.21 (build 22)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 21:34:48 +00:00
parent 739e6ece46
commit a7c5ed713a
9 changed files with 332 additions and 33 deletions
@@ -1,6 +1,9 @@
package com.syncflow
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.syncflow.data.db.SyncPairDao
@@ -22,6 +25,7 @@ class SyncFlowApp : Application(), Configuration.Provider {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
createNotificationChannels()
// Start file watcher on every app launch for any existing ON_CHANGE pairs
CoroutineScope(Dispatchers.IO).launch {
val hasOnChange = syncPairDao.getEnabled().any { it.scheduleType == ScheduleType.ON_CHANGE }
@@ -29,6 +33,27 @@ class SyncFlowApp : Application(), Configuration.Provider {
}
}
private fun createNotificationChannels() {
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
listOf(
NotificationChannel("sync_progress", "Sync progress", NotificationManager.IMPORTANCE_LOW).apply {
description = "Shown while a sync is running"
},
NotificationChannel("sync_complete", "Sync complete", NotificationManager.IMPORTANCE_LOW).apply {
description = "Summary after each successful sync"
},
NotificationChannel("sync_alerts", "Sync errors", NotificationManager.IMPORTANCE_DEFAULT).apply {
description = "Alerts for sync failures and conflicts"
},
NotificationChannel("sync_watching", "File watching", NotificationManager.IMPORTANCE_MIN).apply {
description = "Background service watching folders for changes"
setShowBadge(false)
},
).forEach { channel ->
if (nm.getNotificationChannel(channel.id) == null) nm.createNotificationChannel(channel)
}
}
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)