diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle
index a26d055cba..52750232cc 100644
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -46,7 +46,7 @@ android {
defaultConfig {
applicationId "app.alextran.immich"
minSdkVersion 26
- targetSdkVersion 33
+ targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml
index 1bac79daf5..9222b38de0 100644
--- a/mobile/android/app/src/main/AndroidManifest.xml
+++ b/mobile/android/app/src/main/AndroidManifest.xml
@@ -1,9 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -51,23 +80,13 @@
+ tools:node="remove" />
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
@@ -79,4 +98,4 @@
-
\ No newline at end of file
+
diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt
index 6541ad5755..8520413cff 100644
--- a/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt
+++ b/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt
@@ -1,115 +1,123 @@
-package app.alextran.immich
-
-import android.content.Context
-import android.util.Log
-import io.flutter.embedding.engine.plugins.FlutterPlugin
-import io.flutter.plugin.common.BinaryMessenger
-import io.flutter.plugin.common.MethodCall
-import io.flutter.plugin.common.MethodChannel
-import java.security.MessageDigest
-import java.io.File
-import java.io.FileInputStream
-import kotlinx.coroutines.*
-
-/**
- * Android plugin for Dart `BackgroundService`
- *
- * Receives messages/method calls from the foreground Dart side to manage
- * the background service, e.g. start (enqueue), stop (cancel)
- */
-class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
-
- private var methodChannel: MethodChannel? = null
- private var context: Context? = null
- private val sha1: MessageDigest = MessageDigest.getInstance("SHA-1")
-
- override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
- onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
- }
-
- private fun onAttachedToEngine(ctx: Context, messenger: BinaryMessenger) {
- context = ctx
- methodChannel = MethodChannel(messenger, "immich/foregroundChannel")
- methodChannel?.setMethodCallHandler(this)
- }
-
- override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
- onDetachedFromEngine()
- }
-
- private fun onDetachedFromEngine() {
- methodChannel?.setMethodCallHandler(null)
- methodChannel = null
- }
-
- override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
- val ctx = context!!
- when (call.method) {
- "enable" -> {
- val args = call.arguments>()!!
- ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
- .edit()
- .putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true)
- .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long)
- .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String)
- .apply()
- ContentObserverWorker.enable(ctx, immediate = args.get(2) as Boolean)
- result.success(true)
- }
- "configure" -> {
- val args = call.arguments>()!!
- val requireUnmeteredNetwork = args.get(0) as Boolean
- val requireCharging = args.get(1) as Boolean
- val triggerUpdateDelay = (args.get(2) as Number).toLong()
- val triggerMaxDelay = (args.get(3) as Number).toLong()
- ContentObserverWorker.configureWork(ctx, requireUnmeteredNetwork, requireCharging, triggerUpdateDelay, triggerMaxDelay)
- result.success(true)
- }
- "disable" -> {
- ContentObserverWorker.disable(ctx)
- BackupWorker.stopWork(ctx)
- result.success(true)
- }
- "isEnabled" -> {
- result.success(ContentObserverWorker.isEnabled(ctx))
- }
- "isIgnoringBatteryOptimizations" -> {
- result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx))
- }
- "digestFiles" -> {
- val args = call.arguments>()!!
- GlobalScope.launch(Dispatchers.IO) {
- val buf = ByteArray(BUFSIZE)
- val digest: MessageDigest = MessageDigest.getInstance("SHA-1")
- val hashes = arrayOfNulls(args.size)
- for (i in args.indices) {
- val path = args[i]
- var len = 0
- try {
- val file = FileInputStream(path)
- try {
- while (true) {
- len = file.read(buf)
- if (len != BUFSIZE) break
- digest.update(buf)
- }
- } finally {
- file.close()
- }
- digest.update(buf, 0, len)
- hashes[i] = digest.digest()
- } catch (e: Exception) {
- // skip this file
- Log.w(TAG, "Failed to hash file ${args[i]}: $e")
- }
- }
- result.success(hashes.asList())
- }
- }
- else -> result.notImplemented()
- }
- }
-}
-
-private const val TAG = "BackgroundServicePlugin"
-private const val BUFSIZE = 2*1024*1024;
+package app.alextran.immich
+
+import android.content.Context
+import android.util.Log
+import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.plugin.common.BinaryMessenger
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import java.security.MessageDigest
+import java.io.FileInputStream
+import kotlinx.coroutines.*
+
+/**
+ * Android plugin for Dart `BackgroundService`
+ *
+ * Receives messages/method calls from the foreground Dart side to manage
+ * the background service, e.g. start (enqueue), stop (cancel)
+ */
+class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
+
+ private var methodChannel: MethodChannel? = null
+ private var context: Context? = null
+
+ override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
+ onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
+ }
+
+ private fun onAttachedToEngine(ctx: Context, messenger: BinaryMessenger) {
+ context = ctx
+ methodChannel = MethodChannel(messenger, "immich/foregroundChannel")
+ methodChannel?.setMethodCallHandler(this)
+ }
+
+ override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
+ onDetachedFromEngine()
+ }
+
+ private fun onDetachedFromEngine() {
+ methodChannel?.setMethodCallHandler(null)
+ methodChannel = null
+ }
+
+ override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
+ val ctx = context!!
+ when (call.method) {
+ "enable" -> {
+ val args = call.arguments>()!!
+ ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
+ .edit()
+ .putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true)
+ .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args[0] as Long)
+ .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args[1] as String)
+ .apply()
+ ContentObserverWorker.enable(ctx, immediate = args[2] as Boolean)
+ result.success(true)
+ }
+
+ "configure" -> {
+ val args = call.arguments>()!!
+ val requireUnmeteredNetwork = args[0] as Boolean
+ val requireCharging = args[1] as Boolean
+ val triggerUpdateDelay = (args[2] as Number).toLong()
+ val triggerMaxDelay = (args[3] as Number).toLong()
+ ContentObserverWorker.configureWork(
+ ctx,
+ requireUnmeteredNetwork,
+ requireCharging,
+ triggerUpdateDelay,
+ triggerMaxDelay
+ )
+ result.success(true)
+ }
+
+ "disable" -> {
+ ContentObserverWorker.disable(ctx)
+ BackupWorker.stopWork(ctx)
+ result.success(true)
+ }
+
+ "isEnabled" -> {
+ result.success(ContentObserverWorker.isEnabled(ctx))
+ }
+
+ "isIgnoringBatteryOptimizations" -> {
+ result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx))
+ }
+
+ "digestFiles" -> {
+ val args = call.arguments>()!!
+ GlobalScope.launch(Dispatchers.IO) {
+ val buf = ByteArray(BUFFER_SIZE)
+ val digest: MessageDigest = MessageDigest.getInstance("SHA-1")
+ val hashes = arrayOfNulls(args.size)
+ for (i in args.indices) {
+ val path = args[i]
+ var len = 0
+ try {
+ val file = FileInputStream(path)
+ file.use { assetFile ->
+ while (true) {
+ len = assetFile.read(buf)
+ if (len != BUFFER_SIZE) break
+ digest.update(buf)
+ }
+ }
+ digest.update(buf, 0, len)
+ hashes[i] = digest.digest()
+ } catch (e: Exception) {
+ // skip this file
+ Log.w(TAG, "Failed to hash file ${args[i]}: $e")
+ }
+ }
+ result.success(hashes.asList())
+ }
+ }
+
+ else -> result.notImplemented()
+ }
+ }
+}
+
+private const val TAG = "BackgroundServicePlugin"
+private const val BUFFER_SIZE = 2 * 1024 * 1024;
diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt
index b6b78c2cba..052a4e4c1f 100644
--- a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt
+++ b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt
@@ -4,6 +4,7 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
+import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
import android.os.Build
import android.os.Handler
import android.os.Looper
@@ -40,323 +41,351 @@ import java.util.concurrent.TimeUnit
* Called by Android WorkManager when all constraints for the work are met,
* i.e. battery is not low and optionally Wifi and charging are active.
*/
-class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), MethodChannel.MethodCallHandler {
+class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params),
+ MethodChannel.MethodCallHandler {
- private val resolvableFuture = ResolvableFuture.create()
- private var engine: FlutterEngine? = null
- private lateinit var backgroundChannel: MethodChannel
- private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext)
- private var timeBackupStarted: Long = 0L
- private var notificationBuilder: NotificationCompat.Builder? = null
- private var notificationDetailBuilder: NotificationCompat.Builder? = null
- private var fgFuture: ListenableFuture? = null
+ private val resolvableFuture = ResolvableFuture.create()
+ private var engine: FlutterEngine? = null
+ private lateinit var backgroundChannel: MethodChannel
+ private val notificationManager =
+ ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext)
+ private var timeBackupStarted: Long = 0L
+ private var notificationBuilder: NotificationCompat.Builder? = null
+ private var notificationDetailBuilder: NotificationCompat.Builder? = null
+ private var fgFuture: ListenableFuture? = null
- override fun startWork(): ListenableFuture {
+ override fun startWork(): ListenableFuture {
- Log.d(TAG, "startWork")
+ Log.d(TAG, "startWork")
- val ctx = applicationContext
+ val ctx = applicationContext
- if (!flutterLoader.initialized()) {
- flutterLoader.startInitialization(ctx)
+ if (!flutterLoader.initialized()) {
+ flutterLoader.startInitialization(ctx)
+ }
+
+ // Create a Notification channel
+ createChannel()
+
+ Log.d(TAG, "isIgnoringBatteryOptimizations $isIgnoringBatteryOptimizations")
+ if (isIgnoringBatteryOptimizations) {
+ // normal background services can only up to 10 minutes
+ // foreground services are allowed to run indefinitely
+ // requires battery optimizations to be disabled (either manually by the user
+ // or by the system learning that immich is important to the user)
+ val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
+ .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
+ showInfo(getInfoBuilder(title, indeterminate = true).build())
+ }
+
+ engine = FlutterEngine(ctx)
+
+ flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
+ runDart()
+ }
+
+ return resolvableFuture
+ }
+
+ /**
+ * Starts the Dart runtime/engine and calls `_nativeEntry` function in
+ * `background.service.dart` to run the actual backup logic.
+ */
+ private fun runDart() {
+ val callbackDispatcherHandle = applicationContext.getSharedPreferences(
+ SHARED_PREF_NAME, Context.MODE_PRIVATE
+ ).getLong(SHARED_PREF_CALLBACK_KEY, 0L)
+ val callbackInformation =
+ FlutterCallbackInformation.lookupCallbackInformation(callbackDispatcherHandle)
+ val appBundlePath = flutterLoader.findAppBundlePath()
+
+ engine?.let { engine ->
+ backgroundChannel = MethodChannel(engine.dartExecutor, "immich/backgroundChannel")
+ backgroundChannel.setMethodCallHandler(this@BackupWorker)
+ engine.dartExecutor.executeDartCallback(
+ DartExecutor.DartCallback(
+ applicationContext.assets,
+ appBundlePath,
+ callbackInformation
+ )
+ )
+ }
+ }
+
+ override fun onStopped() {
+ Log.d(TAG, "onStopped")
+ // called when the system has to stop this worker because constraints are
+ // no longer met or the system needs resources for more important tasks
+ Handler(Looper.getMainLooper()).postAtFrontOfQueue {
+ backgroundChannel.invokeMethod("systemStop", null)
+ }
+ waitOnSetForegroundAsync()
+ // cannot await/get(block) on resolvableFuture as its already cancelled (would throw CancellationException)
+ // instead, wait for 5 seconds until forcefully stopping backup work
+ Handler(Looper.getMainLooper()).postDelayed({
+ stopEngine(null)
+ }, 5000)
+ }
+
+ private fun waitOnSetForegroundAsync() {
+ val fgFuture = this.fgFuture
+ if (fgFuture != null && !fgFuture.isCancelled && !fgFuture.isDone) {
+ try {
+ fgFuture.get(500, TimeUnit.MILLISECONDS)
+ } catch (e: Exception) {
+ // ignored, there is nothing to be done
+ }
+ }
+ }
+
+ private fun stopEngine(result: Result?) {
+ clearBackgroundNotification()
+ engine?.destroy()
+ engine = null
+ if (result != null) {
+ Log.d(TAG, "stopEngine result=${result}")
+ resolvableFuture.set(result)
+ }
+ waitOnSetForegroundAsync()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ override fun onMethodCall(call: MethodCall, r: MethodChannel.Result) {
+ when (call.method) {
+ "initialized" -> {
+ timeBackupStarted = SystemClock.uptimeMillis()
+ backgroundChannel.invokeMethod(
+ "onAssetsChanged",
+ null,
+ object : MethodChannel.Result {
+ override fun notImplemented() {
+ stopEngine(Result.failure())
+ }
+
+ override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
+ stopEngine(Result.failure())
+ }
+
+ override fun success(receivedResult: Any?) {
+ val success = receivedResult as Boolean
+ stopEngine(if (success) Result.success() else Result.retry())
+ }
+ }
+ )
+ }
+
+ "updateNotification" -> {
+ val args = call.arguments>()!!
+ val title = args[0] as String?
+ val content = args[1] as String?
+ val progress = args[2] as Int
+ val max = args[3] as Int
+ val indeterminate = args[4] as Boolean
+ val isDetail = args[5] as Boolean
+ val onlyIfFG = args[6] as Boolean
+ if (!onlyIfFG || isIgnoringBatteryOptimizations) {
+ showInfo(
+ getInfoBuilder(title, content, isDetail, progress, max, indeterminate).build(),
+ isDetail
+ )
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- // Create a Notification channel if necessary
- createChannel()
- }
- if (isIgnoringBatteryOptimizations) {
- // normal background services can only up to 10 minutes
- // foreground services are allowed to run indefinitely
- // requires battery optimizations to be disabled (either manually by the user
- // or by the system learning that immich is important to the user)
- val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
- .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
- showInfo(getInfoBuilder(title, indeterminate=true).build())
- }
- engine = FlutterEngine(ctx)
+ }
- flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
- runDart()
- }
+ "showError" -> {
+ val args = call.arguments>()!!
+ val title = args[0] as String
+ val content = args[1] as String?
+ val individualTag = args[2] as String?
+ showError(title, content, individualTag)
+ }
- return resolvableFuture
+ "clearErrorNotifications" -> clearErrorNotifications()
+ "hasContentChanged" -> {
+ val lastChange = applicationContext
+ .getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
+ .getLong(SHARED_PREF_LAST_CHANGE, timeBackupStarted)
+ val hasContentChanged = lastChange > timeBackupStarted;
+ timeBackupStarted = SystemClock.uptimeMillis()
+ r.success(hasContentChanged)
+ }
+
+ else -> r.notImplemented()
+ }
+ }
+
+ private fun showError(title: String, content: String?, individualTag: String?) {
+ val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID)
+ .setContentTitle(title)
+ .setTicker(title)
+ .setContentText(content)
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .build()
+ notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification)
+ }
+
+ private fun clearErrorNotifications() {
+ notificationManager.cancel(NOTIFICATION_ERROR_ID)
+ }
+
+ private fun clearBackgroundNotification() {
+ notificationManager.cancel(NOTIFICATION_ID)
+ notificationManager.cancel(NOTIFICATION_DETAIL_ID)
+ }
+
+ private fun showInfo(notification: Notification, isDetail: Boolean = false) {
+ val id = if (isDetail) NOTIFICATION_DETAIL_ID else NOTIFICATION_ID
+
+ if (isIgnoringBatteryOptimizations && !isDetail) {
+ fgFuture = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE))
+ } else {
+ setForegroundAsync(ForegroundInfo(id, notification))
+ }
+ } else {
+ notificationManager.notify(id, notification)
+ }
+ }
+
+ private fun getInfoBuilder(
+ title: String? = null,
+ content: String? = null,
+ isDetail: Boolean = false,
+ progress: Int = 0,
+ max: Int = 0,
+ indeterminate: Boolean = false,
+ ): NotificationCompat.Builder {
+ var builder = if (isDetail) notificationDetailBuilder else notificationBuilder
+ if (builder == null) {
+ builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .setOnlyAlertOnce(true)
+ .setOngoing(true)
+ if (isDetail) {
+ notificationDetailBuilder = builder
+ } else {
+ notificationBuilder = builder
+ }
+ }
+ if (title != null) {
+ builder.setTicker(title).setContentTitle(title)
+ }
+ if (content != null) {
+ builder.setContentText(content)
+ }
+ return builder.setProgress(max, progress, indeterminate)
+ }
+
+ private fun createChannel() {
+ val foreground = NotificationChannel(
+ NOTIFICATION_CHANNEL_ID,
+ NOTIFICATION_CHANNEL_ID,
+ NotificationManager.IMPORTANCE_LOW
+ )
+ notificationManager.createNotificationChannel(foreground)
+ val error = NotificationChannel(
+ NOTIFICATION_CHANNEL_ERROR_ID,
+ NOTIFICATION_CHANNEL_ERROR_ID,
+ NotificationManager.IMPORTANCE_HIGH
+ )
+ notificationManager.createNotificationChannel(error)
+ }
+
+ companion object {
+ const val SHARED_PREF_NAME = "immichBackgroundService"
+ const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle"
+ const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle"
+ const val SHARED_PREF_LAST_CHANGE = "lastChange"
+
+ private const val TASK_NAME_BACKUP = "immich/BackupWorker"
+ private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService"
+ private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError"
+ private const val NOTIFICATION_DEFAULT_TITLE = "Immich"
+ private const val NOTIFICATION_ID = 1
+ private const val NOTIFICATION_ERROR_ID = 2
+ private const val NOTIFICATION_DETAIL_ID = 3
+ private const val ONE_MINUTE = 60000L
+
+ /**
+ * Enqueues the BackupWorker to run once the constraints are met
+ */
+ fun enqueueBackupWorker(
+ context: Context,
+ requireWifi: Boolean = false,
+ requireCharging: Boolean = false,
+ delayMilliseconds: Long = 0L
+ ) {
+ val workRequest = buildWorkRequest(requireWifi, requireCharging, delayMilliseconds)
+ WorkManager.getInstance(context)
+ .enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.KEEP, workRequest)
+ Log.d(TAG, "enqueueBackupWorker: BackupWorker enqueued")
}
/**
- * Starts the Dart runtime/engine and calls `_nativeEntry` function in
- * `background.service.dart` to run the actual backup logic.
+ * Updates the constraints of an already enqueued BackupWorker
*/
- private fun runDart() {
- val callbackDispatcherHandle = applicationContext.getSharedPreferences(
- SHARED_PREF_NAME, Context.MODE_PRIVATE).getLong(SHARED_PREF_CALLBACK_KEY, 0L)
- val callbackInformation = FlutterCallbackInformation.lookupCallbackInformation(callbackDispatcherHandle)
- val appBundlePath = flutterLoader.findAppBundlePath()
-
- engine?.let { engine ->
- backgroundChannel = MethodChannel(engine.dartExecutor, "immich/backgroundChannel")
- backgroundChannel.setMethodCallHandler(this@BackupWorker)
- engine.dartExecutor.executeDartCallback(
- DartExecutor.DartCallback(
- applicationContext.assets,
- appBundlePath,
- callbackInformation
- )
- )
- }
- }
-
- override fun onStopped() {
- Log.d(TAG, "onStopped")
- // called when the system has to stop this worker because constraints are
- // no longer met or the system needs resources for more important tasks
- Handler(Looper.getMainLooper()).postAtFrontOfQueue {
- backgroundChannel.invokeMethod("systemStop", null)
- }
- waitOnSetForegroundAsync()
- // cannot await/get(block) on resolvableFuture as its already cancelled (would throw CancellationException)
- // instead, wait for 5 seconds until forcefully stopping backup work
- Handler(Looper.getMainLooper()).postDelayed({
- stopEngine(null)
- }, 5000)
- }
-
- private fun waitOnSetForegroundAsync() {
- val fgFuture = this.fgFuture
- if (fgFuture != null && !fgFuture.isCancelled() && !fgFuture.isDone()) {
- try {
- fgFuture.get(500, TimeUnit.MILLISECONDS)
- }
- catch (e: Exception) {
- // ignored, there is nothing to be done
+ fun updateBackupWorker(
+ context: Context,
+ requireWifi: Boolean = false,
+ requireCharging: Boolean = false
+ ) {
+ try {
+ val wm = WorkManager.getInstance(context)
+ val workInfoFuture = wm.getWorkInfosForUniqueWork(TASK_NAME_BACKUP)
+ val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS)
+ if (workInfoList != null) {
+ for (workInfo in workInfoList) {
+ if (workInfo.state == WorkInfo.State.ENQUEUED) {
+ val workRequest = buildWorkRequest(requireWifi, requireCharging)
+ wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest)
+ Log.d(TAG, "updateBackupWorker updated BackupWorker constraints")
+ return
}
+ }
}
+ Log.d(TAG, "updateBackupWorker: BackupWorker not enqueued")
+ } catch (e: Exception) {
+ Log.d(TAG, "updateBackupWorker failed: $e")
+ }
}
- private fun stopEngine(result: Result?) {
- clearBackgroundNotification()
- engine?.destroy()
- engine = null
- if (result != null) {
- Log.d(TAG, "stopEngine result=${result}")
- resolvableFuture.set(result)
- }
- waitOnSetForegroundAsync()
+ /**
+ * Stops the currently running worker (if any) and removes it from the work queue
+ */
+ fun stopWork(context: Context) {
+ WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_BACKUP)
+ Log.d(TAG, "stopWork: BackupWorker cancelled")
}
- override fun onMethodCall(call: MethodCall, r: MethodChannel.Result) {
- when (call.method) {
- "initialized" -> {
- timeBackupStarted = SystemClock.uptimeMillis()
- backgroundChannel.invokeMethod(
- "onAssetsChanged",
- null,
- object : MethodChannel.Result {
- override fun notImplemented() {
- stopEngine(Result.failure())
- }
-
- override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
- stopEngine(Result.failure())
- }
-
- override fun success(receivedResult: Any?) {
- val success = receivedResult as Boolean
- stopEngine(if(success) Result.success() else Result.retry())
- }
- }
- )
- }
- "updateNotification" -> {
- val args = call.arguments>()!!
- val title = args.get(0) as String?
- val content = args.get(1) as String?
- val progress = args.get(2) as Int
- val max = args.get(3) as Int
- val indeterminate = args.get(4) as Boolean
- val isDetail = args.get(5) as Boolean
- val onlyIfFG = args.get(6) as Boolean
- if (!onlyIfFG || isIgnoringBatteryOptimizations) {
- showInfo(getInfoBuilder(title, content, isDetail, progress, max, indeterminate).build(), isDetail)
- }
- }
- "showError" -> {
- val args = call.arguments>()!!
- val title = args.get(0) as String
- val content = args.get(1) as String?
- val individualTag = args.get(2) as String?
- showError(title, content, individualTag)
- }
- "clearErrorNotifications" -> clearErrorNotifications()
- "hasContentChanged" -> {
- val lastChange = applicationContext
- .getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
- .getLong(SHARED_PREF_LAST_CHANGE, timeBackupStarted)
- val hasContentChanged = lastChange > timeBackupStarted;
- timeBackupStarted = SystemClock.uptimeMillis()
- r.success(hasContentChanged)
- }
- else -> r.notImplemented()
- }
+ /**
+ * Returns `true` if the app is ignoring battery optimizations
+ */
+ fun isIgnoringBatteryOptimizations(ctx: Context): Boolean {
+ val powerManager = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager
+ return powerManager.isIgnoringBatteryOptimizations(ctx.packageName)
}
- private fun showError(title: String, content: String?, individualTag: String?) {
- val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID)
- .setContentTitle(title)
- .setTicker(title)
- .setContentText(content)
- .setSmallIcon(R.mipmap.ic_launcher)
- .build()
- notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification)
+ private fun buildWorkRequest(
+ requireWifi: Boolean = false,
+ requireCharging: Boolean = false,
+ delayMilliseconds: Long = 0L
+ ): OneTimeWorkRequest {
+ val constraints = Constraints.Builder()
+ .setRequiredNetworkType(if (requireWifi) NetworkType.UNMETERED else NetworkType.CONNECTED)
+ .setRequiresBatteryNotLow(true)
+ .setRequiresCharging(requireCharging)
+ .build();
+
+ val work = OneTimeWorkRequest.Builder(BackupWorker::class.java)
+ .setConstraints(constraints)
+ .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS)
+ .setInitialDelay(delayMilliseconds, TimeUnit.MILLISECONDS)
+ .build()
+ return work
}
- private fun clearErrorNotifications() {
- notificationManager.cancel(NOTIFICATION_ERROR_ID)
- }
-
- private fun clearBackgroundNotification() {
- notificationManager.cancel(NOTIFICATION_ID)
- notificationManager.cancel(NOTIFICATION_DETAIL_ID)
- }
-
- private fun showInfo(notification: Notification, isDetail: Boolean = false) {
- val id = if(isDetail) NOTIFICATION_DETAIL_ID else NOTIFICATION_ID
- if (isIgnoringBatteryOptimizations && !isDetail) {
- fgFuture = setForegroundAsync(ForegroundInfo(id, notification))
- } else {
- notificationManager.notify(id, notification)
- }
- }
-
- private fun getInfoBuilder(
- title: String? = null,
- content: String? = null,
- isDetail: Boolean = false,
- progress: Int = 0,
- max: Int = 0,
- indeterminate: Boolean = false,
- ): NotificationCompat.Builder {
- var builder = if(isDetail) notificationDetailBuilder else notificationBuilder
- if (builder == null) {
- builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
- .setSmallIcon(R.mipmap.ic_launcher)
- .setOnlyAlertOnce(true)
- .setOngoing(true)
- if (isDetail) {
- notificationDetailBuilder = builder
- } else {
- notificationBuilder = builder
- }
- }
- if (title != null) {
- builder.setTicker(title).setContentTitle(title)
- }
- if (content != null) {
- builder.setContentText(content)
- }
- return builder.setProgress(max, progress, indeterminate)
- }
-
- @RequiresApi(Build.VERSION_CODES.O)
- private fun createChannel() {
- val foreground = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW)
- notificationManager.createNotificationChannel(foreground)
- val error = NotificationChannel(NOTIFICATION_CHANNEL_ERROR_ID, NOTIFICATION_CHANNEL_ERROR_ID, NotificationManager.IMPORTANCE_HIGH)
- notificationManager.createNotificationChannel(error)
- }
-
- companion object {
- const val SHARED_PREF_NAME = "immichBackgroundService"
- const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle"
- const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle"
- const val SHARED_PREF_LAST_CHANGE = "lastChange"
-
- private const val TASK_NAME_BACKUP = "immich/BackupWorker"
- private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService"
- private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError"
- private const val NOTIFICATION_DEFAULT_TITLE = "Immich"
- private const val NOTIFICATION_ID = 1
- private const val NOTIFICATION_ERROR_ID = 2
- private const val NOTIFICATION_DETAIL_ID = 3
- private const val ONE_MINUTE = 60000L
-
- /**
- * Enqueues the BackupWorker to run once the constraints are met
- */
- fun enqueueBackupWorker(context: Context,
- requireWifi: Boolean = false,
- requireCharging: Boolean = false,
- delayMilliseconds: Long = 0L) {
- val workRequest = buildWorkRequest(requireWifi, requireCharging, delayMilliseconds)
- WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.KEEP, workRequest)
- Log.d(TAG, "enqueueBackupWorker: BackupWorker enqueued")
- }
-
- /**
- * Updates the constraints of an already enqueued BackupWorker
- */
- fun updateBackupWorker(context: Context,
- requireWifi: Boolean = false,
- requireCharging: Boolean = false) {
- try {
- val wm = WorkManager.getInstance(context)
- val workInfoFuture = wm.getWorkInfosForUniqueWork(TASK_NAME_BACKUP)
- val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS)
- if (workInfoList != null) {
- for (workInfo in workInfoList) {
- if (workInfo.state == WorkInfo.State.ENQUEUED) {
- val workRequest = buildWorkRequest(requireWifi, requireCharging)
- wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest)
- Log.d(TAG, "updateBackupWorker updated BackupWorker constraints")
- return
- }
- }
- }
- Log.d(TAG, "updateBackupWorker: BackupWorker not enqueued")
- } catch (e: Exception) {
- Log.d(TAG, "updateBackupWorker failed: ${e}")
- }
- }
-
- /**
- * Stops the currently running worker (if any) and removes it from the work queue
- */
- fun stopWork(context: Context) {
- WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_BACKUP)
- Log.d(TAG, "stopWork: BackupWorker cancelled")
- }
-
- /**
- * Returns `true` if the app is ignoring battery optimizations
- */
- fun isIgnoringBatteryOptimizations(ctx: Context): Boolean {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- val pwrm = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager
- val name = ctx.packageName
- return pwrm.isIgnoringBatteryOptimizations(name)
- }
- return true
- }
-
- private fun buildWorkRequest(requireWifi: Boolean = false,
- requireCharging: Boolean = false,
- delayMilliseconds: Long = 0L): OneTimeWorkRequest {
- val constraints = Constraints.Builder()
- .setRequiredNetworkType(if (requireWifi) NetworkType.UNMETERED else NetworkType.CONNECTED)
- .setRequiresBatteryNotLow(true)
- .setRequiresCharging(requireCharging)
- .build();
-
- val work = OneTimeWorkRequest.Builder(BackupWorker::class.java)
- .setConstraints(constraints)
- .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS)
- .setInitialDelay(delayMilliseconds, TimeUnit.MILLISECONDS)
- .build()
- return work
- }
-
- private val flutterLoader = FlutterLoader()
- }
+ private val flutterLoader = FlutterLoader()
+ }
}
private const val TAG = "BackupWorker"
diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt
index 59ca6d5638..9cb2ec7779 100644
--- a/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt
+++ b/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt
@@ -1,144 +1,144 @@
-package app.alextran.immich
-
-import android.content.Context
-import android.os.SystemClock
-import android.provider.MediaStore
-import android.util.Log
-import androidx.work.Constraints
-import androidx.work.Worker
-import androidx.work.WorkerParameters
-import androidx.work.ExistingWorkPolicy
-import androidx.work.OneTimeWorkRequest
-import androidx.work.WorkManager
-import androidx.work.Operation
-import java.util.concurrent.TimeUnit
-
-/**
- * Worker executed by Android WorkManager observing content changes (new photos/videos)
- *
- * Immediately enqueues the BackupWorker when running.
- * As this work is not triggered periodically, but on content change, the
- * worker enqueues itself again after each run.
- */
-class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
-
- override fun doWork(): Result {
- if (!isEnabled(applicationContext)) {
- return Result.failure()
- }
- if (getTriggeredContentUris().size > 0) {
- startBackupWorker(applicationContext, delayMilliseconds = 0)
- }
- enqueueObserverWorker(applicationContext, ExistingWorkPolicy.REPLACE)
- return Result.success()
- }
-
- companion object {
- const val SHARED_PREF_SERVICE_ENABLED = "serviceEnabled"
- const val SHARED_PREF_REQUIRE_WIFI = "requireWifi"
- const val SHARED_PREF_REQUIRE_CHARGING = "requireCharging"
- const val SHARED_PREF_TRIGGER_UPDATE_DELAY = "triggerUpdateDelay"
- const val SHARED_PREF_TRIGGER_MAX_DELAY = "triggerMaxDelay"
-
- private const val TASK_NAME_OBSERVER = "immich/ContentObserver"
-
- /**
- * Enqueues the `ContentObserverWorker`.
- *
- * @param context Android Context
- */
- fun enable(context: Context, immediate: Boolean = false) {
- enqueueObserverWorker(context, ExistingWorkPolicy.KEEP)
- Log.d(TAG, "enabled ContentObserverWorker")
- if (immediate) {
- startBackupWorker(context, delayMilliseconds = 5000)
- }
- }
-
- /**
- * Configures the `BackupWorker` to run when all constraints are met.
- *
- * @param context Android Context
- * @param requireWifi if true, task only runs if connected to wifi
- * @param requireCharging if true, task only runs if device is charging
- */
- fun configureWork(context: Context,
- requireWifi: Boolean = false,
- requireCharging: Boolean = false,
- triggerUpdateDelay: Long = 5000,
- triggerMaxDelay: Long = 50000) {
- context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
- .edit()
- .putBoolean(SHARED_PREF_SERVICE_ENABLED, true)
- .putBoolean(SHARED_PREF_REQUIRE_WIFI, requireWifi)
- .putBoolean(SHARED_PREF_REQUIRE_CHARGING, requireCharging)
- .putLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, triggerUpdateDelay)
- .putLong(SHARED_PREF_TRIGGER_MAX_DELAY, triggerMaxDelay)
- .apply()
- BackupWorker.updateBackupWorker(context, requireWifi, requireCharging)
- }
-
- /**
- * Stops the currently running worker (if any) and removes it from the work queue
- */
- fun disable(context: Context) {
- context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
- .edit().putBoolean(SHARED_PREF_SERVICE_ENABLED, false).apply()
- WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_OBSERVER)
- Log.d(TAG, "disabled ContentObserverWorker")
- }
-
- /**
- * Return true if the user has enabled the background backup service
- */
- fun isEnabled(ctx: Context): Boolean {
- return ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
- .getBoolean(SHARED_PREF_SERVICE_ENABLED, false)
- }
-
- /**
- * Enqueue and replace the worker without the content trigger but with a short delay
- */
- fun workManagerAppClearedWorkaround(context: Context) {
- val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java)
- .setInitialDelay(500, TimeUnit.MILLISECONDS)
- .build()
- WorkManager
- .getInstance(context)
- .enqueueUniqueWork(TASK_NAME_OBSERVER, ExistingWorkPolicy.REPLACE, work)
- .getResult()
- .get()
- Log.d(TAG, "workManagerAppClearedWorkaround")
- }
-
- private fun enqueueObserverWorker(context: Context, policy: ExistingWorkPolicy) {
- val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
- val constraints = Constraints.Builder()
- .addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
- .addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
- .addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
- .addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
- .setTriggerContentUpdateDelay(sp.getLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, 5000), TimeUnit.MILLISECONDS)
- .setTriggerContentMaxDelay(sp.getLong(SHARED_PREF_TRIGGER_MAX_DELAY, 50000), TimeUnit.MILLISECONDS)
- .build()
-
- val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java)
- .setConstraints(constraints)
- .build()
- WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work)
- }
-
- fun startBackupWorker(context: Context, delayMilliseconds: Long) {
- val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
- if (!sp.getBoolean(SHARED_PREF_SERVICE_ENABLED, false))
- return
- val requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true)
- val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false)
- BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds)
- sp.edit().putLong(BackupWorker.SHARED_PREF_LAST_CHANGE, SystemClock.uptimeMillis()).apply()
- }
-
- }
-}
-
-private const val TAG = "ContentObserverWorker"
\ No newline at end of file
+package app.alextran.immich
+
+import android.content.Context
+import android.os.SystemClock
+import android.provider.MediaStore
+import android.util.Log
+import androidx.work.Constraints
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import androidx.work.ExistingWorkPolicy
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkManager
+import androidx.work.Operation
+import java.util.concurrent.TimeUnit
+
+/**
+ * Worker executed by Android WorkManager observing content changes (new photos/videos)
+ *
+ * Immediately enqueues the BackupWorker when running.
+ * As this work is not triggered periodically, but on content change, the
+ * worker enqueues itself again after each run.
+ */
+class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
+
+ override fun doWork(): Result {
+ if (!isEnabled(applicationContext)) {
+ return Result.failure()
+ }
+ if (triggeredContentUris.size > 0) {
+ startBackupWorker(applicationContext, delayMilliseconds = 0)
+ }
+ enqueueObserverWorker(applicationContext, ExistingWorkPolicy.REPLACE)
+ return Result.success()
+ }
+
+ companion object {
+ const val SHARED_PREF_SERVICE_ENABLED = "serviceEnabled"
+ private const val SHARED_PREF_REQUIRE_WIFI = "requireWifi"
+ private const val SHARED_PREF_REQUIRE_CHARGING = "requireCharging"
+ private const val SHARED_PREF_TRIGGER_UPDATE_DELAY = "triggerUpdateDelay"
+ private const val SHARED_PREF_TRIGGER_MAX_DELAY = "triggerMaxDelay"
+
+ private const val TASK_NAME_OBSERVER = "immich/ContentObserver"
+
+ /**
+ * Enqueues the `ContentObserverWorker`.
+ *
+ * @param context Android Context
+ */
+ fun enable(context: Context, immediate: Boolean = false) {
+ enqueueObserverWorker(context, ExistingWorkPolicy.KEEP)
+ Log.d(TAG, "enabled ContentObserverWorker")
+ if (immediate) {
+ startBackupWorker(context, delayMilliseconds = 5000)
+ }
+ }
+
+ /**
+ * Configures the `BackupWorker` to run when all constraints are met.
+ *
+ * @param context Android Context
+ * @param requireWifi if true, task only runs if connected to wifi
+ * @param requireCharging if true, task only runs if device is charging
+ */
+ fun configureWork(context: Context,
+ requireWifi: Boolean = false,
+ requireCharging: Boolean = false,
+ triggerUpdateDelay: Long = 5000,
+ triggerMaxDelay: Long = 50000) {
+ context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
+ .edit()
+ .putBoolean(SHARED_PREF_SERVICE_ENABLED, true)
+ .putBoolean(SHARED_PREF_REQUIRE_WIFI, requireWifi)
+ .putBoolean(SHARED_PREF_REQUIRE_CHARGING, requireCharging)
+ .putLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, triggerUpdateDelay)
+ .putLong(SHARED_PREF_TRIGGER_MAX_DELAY, triggerMaxDelay)
+ .apply()
+ BackupWorker.updateBackupWorker(context, requireWifi, requireCharging)
+ }
+
+ /**
+ * Stops the currently running worker (if any) and removes it from the work queue
+ */
+ fun disable(context: Context) {
+ context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
+ .edit().putBoolean(SHARED_PREF_SERVICE_ENABLED, false).apply()
+ WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_OBSERVER)
+ Log.d(TAG, "disabled ContentObserverWorker")
+ }
+
+ /**
+ * Return true if the user has enabled the background backup service
+ */
+ fun isEnabled(ctx: Context): Boolean {
+ return ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
+ .getBoolean(SHARED_PREF_SERVICE_ENABLED, false)
+ }
+
+ /**
+ * Enqueue and replace the worker without the content trigger but with a short delay
+ */
+ fun workManagerAppClearedWorkaround(context: Context) {
+ val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java)
+ .setInitialDelay(500, TimeUnit.MILLISECONDS)
+ .build()
+ WorkManager
+ .getInstance(context)
+ .enqueueUniqueWork(TASK_NAME_OBSERVER, ExistingWorkPolicy.REPLACE, work)
+ .result
+ .get()
+ Log.d(TAG, "workManagerAppClearedWorkaround")
+ }
+
+ private fun enqueueObserverWorker(context: Context, policy: ExistingWorkPolicy) {
+ val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
+ val constraints = Constraints.Builder()
+ .addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
+ .addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
+ .addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
+ .addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
+ .setTriggerContentUpdateDelay(sp.getLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, 5000), TimeUnit.MILLISECONDS)
+ .setTriggerContentMaxDelay(sp.getLong(SHARED_PREF_TRIGGER_MAX_DELAY, 50000), TimeUnit.MILLISECONDS)
+ .build()
+
+ val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java)
+ .setConstraints(constraints)
+ .build()
+ WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work)
+ }
+
+ fun startBackupWorker(context: Context, delayMilliseconds: Long) {
+ val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
+ if (!sp.getBoolean(SHARED_PREF_SERVICE_ENABLED, false))
+ return
+ val requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true)
+ val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false)
+ BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds)
+ sp.edit().putLong(BackupWorker.SHARED_PREF_LAST_CHANGE, SystemClock.uptimeMillis()).apply()
+ }
+
+ }
+}
+
+private const val TAG = "ContentObserverWorker"