1
0
mirror of https://github.com/immich-app/immich.git synced 2025-02-15 19:36:04 +02:00

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

This commit is contained in:
Fynn Petersen-Frey 2022-10-06 18:32:45 +02:00 committed by GitHub
parent 7587f858ae
commit a3aca4acb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 40 additions and 53 deletions

View File

@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich"> <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="${applicationName}" android:usesCleartextTraffic="true" android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true"> <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" />

View File

@ -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();
}
}

View File

@ -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() .edit()
.putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long) .putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true)
.putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String) .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long)
.apply() .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)

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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()) flutterEngine.plugins.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
}
} }
} }