From b2ca208dbb4a00501f61befff3ae23c690cbc898 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 17 Sep 2025 09:11:55 -0500 Subject: [PATCH] fix: ensure background worker is scheduled when the app is dismissed (#22032) * fix: ensure background worker is scheduled when the app is dismissed * remove logs * fix: use native locks (#22081) * fix: native locks * use atomicints * change count check --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --------- Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../kotlin/app/alextran/immich/ImmichApp.kt | 3 ++ .../app/alextran/immich/MainActivity.kt | 2 ++ .../immich/background/BackgroundEngineLock.kt | 33 +++++++++++++++++++ .../immich/background/BackgroundWorker.kt | 5 +++ .../background/BackgroundWorkerApiImpl.kt | 4 +-- 5 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundEngineLock.kt diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt index 4237643233..5a3b0e1f3d 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt @@ -3,6 +3,7 @@ package app.alextran.immich import android.app.Application import androidx.work.Configuration import androidx.work.WorkManager +import app.alextran.immich.background.BackgroundWorkerApiImpl class ImmichApp : Application() { override fun onCreate() { @@ -14,6 +15,8 @@ class ImmichApp : Application() { // Thus, the BackupWorker is not started. If the system kills the process after each initialization // (because of low memory etc.), the backup is never performed. // As a workaround, we also run a backup check when initializing the application + ContentObserverWorker.startBackupWorker(context = this, delayMilliseconds = 0) + BackgroundWorkerApiImpl.enqueueBackgroundWorker(this) } } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt index d395cc2243..4e811c8dfc 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt @@ -3,6 +3,7 @@ package app.alextran.immich import android.content.Context import android.os.Build import android.os.ext.SdkExtensions +import app.alextran.immich.background.BackgroundEngineLock import app.alextran.immich.background.BackgroundWorkerApiImpl import app.alextran.immich.background.BackgroundWorkerFgHostApi import app.alextran.immich.connectivity.ConnectivityApi @@ -25,6 +26,7 @@ class MainActivity : FlutterFragmentActivity() { fun registerPlugins(ctx: Context, flutterEngine: FlutterEngine) { flutterEngine.plugins.add(BackgroundServicePlugin()) flutterEngine.plugins.add(HttpSSLOptionsPlugin()) + flutterEngine.plugins.add(BackgroundEngineLock()) val messenger = flutterEngine.dartExecutor.binaryMessenger val nativeSyncApiImpl = diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundEngineLock.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundEngineLock.kt new file mode 100644 index 0000000000..6d6f45a708 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundEngineLock.kt @@ -0,0 +1,33 @@ +package app.alextran.immich.background + +import android.util.Log +import androidx.work.WorkManager +import io.flutter.embedding.engine.FlutterEngineCache +import io.flutter.embedding.engine.plugins.FlutterPlugin +import java.util.concurrent.atomic.AtomicInteger + +private const val TAG = "BackgroundEngineLock" + +class BackgroundEngineLock : FlutterPlugin { + companion object { + const val ENGINE_CACHE_KEY = "immich::background_worker::engine" + var engineCount = AtomicInteger(0) + } + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + // work manager task is running while the main app is opened, cancel the worker + if (engineCount.incrementAndGet() > 1 && FlutterEngineCache.getInstance() + .get(ENGINE_CACHE_KEY) != null + ) { + WorkManager.getInstance(binding.applicationContext) + .cancelUniqueWork(BackgroundWorkerApiImpl.BACKGROUND_WORKER_NAME) + FlutterEngineCache.getInstance().remove(ENGINE_CACHE_KEY) + } + Log.i(TAG, "Flutter engine attached. Attached Engines count: $engineCount") + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + engineCount.decrementAndGet() + Log.i(TAG, "Flutter engine detached. Attached Engines count: $engineCount") + } +} diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt index 33eb60dc82..7d30319af4 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import io.flutter.FlutterInjector import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.FlutterEngineCache import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.embedding.engine.loader.FlutterLoader import java.util.concurrent.TimeUnit @@ -75,6 +76,9 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : loader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { engine = FlutterEngine(ctx) + FlutterEngineCache.getInstance().remove(BackgroundEngineLock.ENGINE_CACHE_KEY); + FlutterEngineCache.getInstance() + .put(BackgroundEngineLock.ENGINE_CACHE_KEY, engine!!) // Register custom plugins MainActivity.registerPlugins(ctx, engine!!) @@ -188,6 +192,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : isComplete = true engine?.destroy() engine = null + FlutterEngineCache.getInstance().remove(BackgroundEngineLock.ENGINE_CACHE_KEY); flutterApi = null notificationManager.cancel(NOTIFICATION_ID) waitForForegroundPromotion() diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerApiImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerApiImpl.kt index 4c2d98be71..6cbda073d8 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerApiImpl.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerApiImpl.kt @@ -26,7 +26,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { } companion object { - private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1" + const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1" private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1" fun enqueueMediaObserver(ctx: Context) { @@ -56,7 +56,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) .build() WorkManager.getInstance(ctx) - .enqueueUniqueWork(BACKGROUND_WORKER_NAME, ExistingWorkPolicy.REPLACE, work) + .enqueueUniqueWork(BACKGROUND_WORKER_NAME, ExistingWorkPolicy.KEEP, work) Log.i(TAG, "Enqueued background worker with name: $BACKGROUND_WORKER_NAME") }