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">
     <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
       <!-- Specifies an Android theme to apply to this Activity as soon as
                  the Android process has started. This theme is visible to the user
                  the Android process has started. This theme is visible to the user
@@ -12,12 +12,15 @@
       </intent-filter>
       </intent-filter>
 
 
     </activity>
     </activity>
-    <service android:name=".AppClearedService" android:stopWithTask="false" />
     <!-- Don't delete the meta-data below.
     <!-- Don't delete the meta-data below.
              This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
              This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
     <meta-data android:name="flutterEmbedding" android:value="2" />
     <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>
   </application>
   <uses-permission android:name="android.permission.INTERNET" />
   <uses-permission android:name="android.permission.INTERNET" />
   <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
   <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`
  * Android plugin for Dart `BackgroundService`
  *
  *
  * Receives messages/method calls from the foreground Dart side to manage
  * 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 {
 class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
 
 
@@ -38,14 +38,15 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
 
 
     override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
     override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
         val ctx = context!!
         val ctx = context!!
-        when(call.method) {
+        when (call.method) {
             "enable" -> {
             "enable" -> {
                 val args = call.arguments<ArrayList<*>>()!!
                 val args = call.arguments<ArrayList<*>>()!!
                 ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
                 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)
                 ContentObserverWorker.enable(ctx, immediate = args.get(2) as Boolean)
                 result.success(true)
                 result.success(true)
             }
             }
@@ -54,7 +55,7 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
                 val requireUnmeteredNetwork = args.get(0) as Boolean
                 val requireUnmeteredNetwork = args.get(0) as Boolean
                 val requireCharging = args.get(1) as Boolean
                 val requireCharging = args.get(1) as Boolean
                 ContentObserverWorker.configureWork(ctx, requireUnmeteredNetwork, requireCharging)
                 ContentObserverWorker.configureWork(ctx, requireUnmeteredNetwork, requireCharging)
-                result.success(true)   
+                result.success(true)
             }
             }
             "disable" -> {
             "disable" -> {
                 ContentObserverWorker.disable(ctx)
                 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
          * @param context Android Context
          */
          */
         fun enable(context: Context, immediate: Boolean = false) {
         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)
             enqueueObserverWorker(context, ExistingWorkPolicy.KEEP)
             Log.d(TAG, "enabled ContentObserverWorker")
             Log.d(TAG, "enabled ContentObserverWorker")
             if (immediate) {
             if (immediate) {
@@ -123,8 +120,10 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx
             WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work)
             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)
             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 requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true)
             val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false)
             val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false)
             BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds)
             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.os.Bundle
 import android.content.Intent
 import android.content.Intent
 
 
-class MainActivity: FlutterActivity() {
+class MainActivity : FlutterActivity() {
 
 
     override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
     override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
         super.configureFlutterEngine(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())
     }
     }
 
 
 }
 }