package com.taaazzz.photosync.bg import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.Service import android.content.Intent import android.content.pm.ServiceInfo import android.database.ContentObserver import android.net.Uri import android.os.Build import android.os.Handler import android.os.IBinder import android.os.Looper import android.provider.MediaStore // Service de PREMIER PLAN (notification permanente obligatoire sur Android). Il // pose un ContentObserver sur MediaStore (images + videos) : des qu'une nouvelle // photo/video apparait, il declenche la tache HeadlessJS qui lance la sync, MEME // quand l'app est fermee. C'est l'equivalent du comportement de QFile. class PhotoSyncObserverService : Service() { private val handler = Handler(Looper.getMainLooper()) private var pending: Runnable? = null private var imgObs: ContentObserver? = null private var vidObs: ContentObserver? = null override fun onBind(intent: Intent?): IBinder? = null override fun onCreate() { super.onCreate() startInForeground() Uploader.initBaseline(this) Diag.ping("obs-created") imgObs = makeObserver() vidObs = makeObserver() try { contentResolver.registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, imgObs!!) contentResolver.registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true, vidObs!!) Diag.ping("obs-registered") } catch (e: Exception) { Diag.ping("obs-reg-fail-" + (e.message ?: "?")) } } private fun makeObserver() = object : ContentObserver(handler) { override fun onChange(selfChange: Boolean, uri: Uri?) { Diag.ping("obs-change"); scheduleSync() } } // Anti-rafale : un burst de photos ne declenche qu'UNE sync, et on laisse au // fichier le temps de finir de s'ecrire (surtout une video). private fun scheduleSync() { pending?.let { handler.removeCallbacks(it) } val r = Runnable { Diag.ping("sched-fire") // Upload NATIF sur un thread dedie (le reseau est interdit sur le main thread, // et le natif marche en arriere-plan la ou le JS headless restait fige). Thread { Uploader.run(applicationContext) }.start() } pending = r handler.postDelayed(r, 8000L) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = START_STICKY override fun onDestroy() { super.onDestroy() imgObs?.let { try { contentResolver.unregisterContentObserver(it) } catch (e: Exception) {} } vidObs?.let { try { contentResolver.unregisterContentObserver(it) } catch (e: Exception) {} } pending?.let { handler.removeCallbacks(it) } } private fun startInForeground() { val channelId = "photosync_bg" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val nm = getSystemService(NotificationManager::class.java) if (nm != null && nm.getNotificationChannel(channelId) == null) { val ch = NotificationChannel(channelId, "Sauvegarde PhotoSync", NotificationManager.IMPORTANCE_LOW) ch.setShowBadge(false) nm.createNotificationChannel(ch) } } val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Notification.Builder(this, channelId) } else { @Suppress("DEPRECATION") Notification.Builder(this) } val notif = builder .setContentTitle("PhotoSync") .setContentText("Sauvegarde automatique active") .setSmallIcon(applicationInfo.icon) .setOngoing(true) .build() if (Build.VERSION.SDK_INT >= 34) { startForeground(NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) } else { startForeground(NOTIF_ID, notif) } } companion object { private const val NOTIF_ID = 4711 } }