Browse Source

feat(mobile) Run background service after being killed (#789)

Fynn Petersen-Frey 2 years ago
parent
commit
a3aca4acb5

+ 8 - 5
mobile/android/app/src/main/AndroidManifest.xml

@@ -1,5 +1,5 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich">
-  <application android:label="Immich" android:name="${applicationName}" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich" xmlns:tools="http://schemas.android.com/tools">
+  <application android:label="Immich" android:name=".ImmichApp" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true">
     <activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
       <!-- Specifies an Android theme to apply to this Activity as soon as
                  the Android process has started. This theme is visible to the user
@@ -12,12 +12,15 @@
       </intent-filter>
 
     </activity>
-    <service android:name=".AppClearedService" android:stopWithTask="false" />
     <!-- Don't delete the meta-data below.
              This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
     <meta-data android:name="flutterEmbedding" android:value="2" />
-
-
+    <!-- Disables default WorkManager initialization to use our custom initialization -->
+    <provider
+        android:name="androidx.startup.InitializationProvider"
+        android:authorities="${applicationId}.androidx-startup"
+        tools:node="remove">
+    </provider>
   </application>
   <uses-permission android:name="android.permission.INTERNET" />
   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

+ 0 - 25
mobile/android/app/src/main/kotlin/com/example/mobile/AppClearedService.kt

@@ -1,25 +0,0 @@
-package app.alextran.immich
-
-import android.app.Service
-import android.content.Intent
-import android.os.IBinder
-
-/**
- * Catches the event when either the system or the user kills the app
- * (does not apply on force close!) 
- */
-class AppClearedService() : Service() {
-
-    override fun onBind(intent: Intent): IBinder? {
-        return null
-    }
-
-    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
-        return START_NOT_STICKY;
-    }
-
-    override fun onTaskRemoved(rootIntent: Intent) {
-        ContentObserverWorker.workManagerAppClearedWorkaround(applicationContext)
-        stopSelf();
-    }
-}

+ 8 - 7
mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt

@@ -10,7 +10,7 @@ import io.flutter.plugin.common.MethodChannel
  * 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) 
+ * the background service, e.g. start (enqueue), stop (cancel)
  */
 class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
 
@@ -38,14 +38,15 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
 
     override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
         val ctx = context!!
-        when(call.method) {
+        when (call.method) {
             "enable" -> {
                 val args = call.arguments<ArrayList<*>>()!!
                 ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
-                    .edit()
-                    .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long)
-                    .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String)
-                    .apply()
+                        .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)
             }
@@ -54,7 +55,7 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
                 val requireUnmeteredNetwork = args.get(0) as Boolean
                 val requireCharging = args.get(1) as Boolean
                 ContentObserverWorker.configureWork(ctx, requireUnmeteredNetwork, requireCharging)
-                result.success(true)   
+                result.success(true)
             }
             "disable" -> {
                 ContentObserverWorker.disable(ctx)

+ 3 - 4
mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt

@@ -46,9 +46,6 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx
          * @param context Android Context
          */
         fun enable(context: Context, immediate: Boolean = false) {
-            // migration to remove any old active background task
-            WorkManager.getInstance(context).cancelUniqueWork("immich/photoListener")
-
             enqueueObserverWorker(context, ExistingWorkPolicy.KEEP)
             Log.d(TAG, "enabled ContentObserverWorker")
             if (immediate) {
@@ -123,8 +120,10 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx
             WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work)
         }
 
-        private fun startBackupWorker(context: Context, delayMilliseconds: Long) {
+        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)

+ 19 - 0
mobile/android/app/src/main/kotlin/com/example/mobile/ImmichApp.kt

@@ -0,0 +1,19 @@
+package app.alextran.immich
+
+import android.app.Application
+import androidx.work.Configuration
+import androidx.work.WorkManager
+
+class ImmichApp : Application() {
+    override fun onCreate() {
+        super.onCreate()
+        val config = Configuration.Builder().build()
+        WorkManager.initialize(this, config)
+        // always start BackupWorker after WorkManager init; this fixes the following bug:
+        // After the process is killed (by user or system), the first trigger (taking a new picture) is lost.
+        // 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)
+    }
+}

+ 2 - 12
mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt

@@ -5,21 +5,11 @@ import io.flutter.embedding.engine.FlutterEngine
 import android.os.Bundle
 import android.content.Intent
 
-class MainActivity: FlutterActivity() {
+class MainActivity : FlutterActivity() {
 
     override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
         super.configureFlutterEngine(flutterEngine)
-        flutterEngine.getPlugins().add(BackgroundServicePlugin())
-    }
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        try {
-            startService(Intent(getBaseContext(), AppClearedService::class.java));
-        } catch (e: Exception) {
-            // startService must not be called when app is in background (crashes app)
-            // there is nothing we can do
-        }
+        flutterEngine.plugins.add(BackgroundServicePlugin())
     }
 
 }