1
0
mirror of https://github.com/immich-app/immich.git synced 2025-02-03 18:33:20 +02:00

feat(android) Check server is reachable before starting background backup (#8594)

* Bump androidx work version to 2.9.0

* Check that server is reachable before starting backup work

* Dart format

* Cleanup debug logs

* Fix analysis
This commit is contained in:
devjn 2024-04-20 16:39:04 +03:00 committed by GitHub
parent 3abfe3c99e
commit 71b6d8b569
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 116 additions and 34 deletions

View File

@ -90,7 +90,7 @@ flutter {
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
implementation "androidx.work:work-runtime-ktx:$work_version" implementation "androidx.work:work-runtime:$work_version"
implementation "androidx.concurrent:concurrent-futures:$concurrent_version" implementation "androidx.concurrent:concurrent-futures:$concurrent_version"
implementation "com.google.guava:guava:$guava_version" implementation "com.google.guava:guava:$guava_version"
implementation "com.github.bumptech.glide:glide:$glide_version" implementation "com.github.bumptech.glide:glide:$glide_version"

View File

@ -52,6 +52,7 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
.putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true) .putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true)
.putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long) .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long)
.putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String) .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String)
.putString(BackupWorker.SHARED_PREF_SERVER_URL, args.get(3) as String)
.apply() .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)

View File

@ -11,8 +11,8 @@ import android.os.PowerManager
import android.os.SystemClock import android.os.SystemClock
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.concurrent.futures.CallbackToFutureAdapter
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.concurrent.futures.ResolvableFuture
import androidx.work.BackoffPolicy import androidx.work.BackoffPolicy
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
@ -30,6 +30,16 @@ import io.flutter.embedding.engine.loader.FlutterLoader
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.view.FlutterCallbackInformation import io.flutter.view.FlutterCallbackInformation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import java.io.IOException
import java.net.HttpURLConnection
import java.net.InetAddress
import java.net.URL
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/** /**
@ -42,7 +52,6 @@ import java.util.concurrent.TimeUnit
*/ */
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<Result>()
private var engine: FlutterEngine? = null private var engine: FlutterEngine? = null
private lateinit var backgroundChannel: MethodChannel private lateinit var backgroundChannel: MethodChannel
private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -52,10 +61,57 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
private var notificationDetailBuilder: NotificationCompat.Builder? = null private var notificationDetailBuilder: NotificationCompat.Builder? = null
private var fgFuture: ListenableFuture<Void>? = null private var fgFuture: ListenableFuture<Void>? = null
override fun startWork(): ListenableFuture<ListenableWorker.Result> { private val job = Job()
private lateinit var completer: CallbackToFutureAdapter.Completer<Result>
private val resolvableFuture = CallbackToFutureAdapter.getFuture { completer ->
this.completer = completer
null
}
init {
resolvableFuture.addListener(
Runnable {
if (resolvableFuture.isCancelled) {
job.cancel()
}
},
taskExecutor.serialTaskExecutor
)
}
override fun startWork(): ListenableFuture<ListenableWorker.Result> {
Log.d(TAG, "startWork") Log.d(TAG, "startWork")
val ctx = applicationContext
val prefs = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
prefs.getString(SHARED_PREF_SERVER_URL, null)
?.takeIf { it.isNotEmpty() }
?.let { serverUrl -> doCoroutineWork(serverUrl) }
?: doWork()
return resolvableFuture
}
/**
* This function is used to check if server URL is reachable before starting the backup work.
* Check must be done in a background to avoid blocking the main thread.
*/
private fun doCoroutineWork(serverUrl : String) {
CoroutineScope(Dispatchers.Default + job).launch {
val isReachable = isUrlReachableHttp(serverUrl)
withContext(Dispatchers.Main) {
if (isReachable) {
doWork()
} else {
// Fail when the URL is not reachable
completer.set(Result.failure())
}
}
}
}
private fun doWork() {
Log.d(TAG, "doWork")
val ctx = applicationContext val ctx = applicationContext
if (!flutterLoader.initialized()) { if (!flutterLoader.initialized()) {
@ -79,8 +135,6 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
runDart() runDart()
} }
return resolvableFuture
} }
/** /**
@ -139,7 +193,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
engine = null engine = null
if (result != null) { if (result != null) {
Log.d(TAG, "stopEngine result=${result}") Log.d(TAG, "stopEngine result=${result}")
resolvableFuture.set(result) this.completer.set(result)
} }
waitOnSetForegroundAsync() waitOnSetForegroundAsync()
} }
@ -270,6 +324,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle" const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle"
const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle" const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle"
const val SHARED_PREF_LAST_CHANGE = "lastChange" const val SHARED_PREF_LAST_CHANGE = "lastChange"
const val SHARED_PREF_SERVER_URL = "serverUrl"
private const val TASK_NAME_BACKUP = "immich/BackupWorker" private const val TASK_NAME_BACKUP = "immich/BackupWorker"
private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService" private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService"
@ -304,7 +359,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS) val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS)
if (workInfoList != null) { if (workInfoList != null) {
for (workInfo in workInfoList) { for (workInfo in workInfoList) {
if (workInfo.getState() == WorkInfo.State.ENQUEUED) { if (workInfo.state == WorkInfo.State.ENQUEUED) {
val workRequest = buildWorkRequest(requireWifi, requireCharging) val workRequest = buildWorkRequest(requireWifi, requireCharging)
wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest) wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest)
Log.d(TAG, "updateBackupWorker updated BackupWorker constraints") Log.d(TAG, "updateBackupWorker updated BackupWorker constraints")
@ -360,3 +415,26 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
} }
private const val TAG = "BackupWorker" private const val TAG = "BackupWorker"
/**
* Check if the given URL is reachable via HTTP
*/
suspend fun isUrlReachableHttp(url: String, timeoutMillis: Long = 5000L): Boolean {
return withTimeoutOrNull(timeoutMillis) {
var httpURLConnection: HttpURLConnection? = null
try {
httpURLConnection = (URL(url).openConnection() as HttpURLConnection).apply {
requestMethod = "HEAD"
connectTimeout = timeoutMillis.toInt()
readTimeout = timeoutMillis.toInt()
}
httpURLConnection.connect()
httpURLConnection.responseCode == HttpURLConnection.HTTP_OK
} catch (e: Exception) {
Log.e(TAG, "Failed to reach server URL: $e")
false
} finally {
httpURLConnection?.disconnect()
}
} == true
}

View File

@ -1,7 +1,7 @@
buildscript { buildscript {
ext.kotlin_version = '1.8.20' ext.kotlin_version = '1.8.22'
ext.kotlin_coroutines_version = '1.7.1' ext.kotlin_coroutines_version = '1.7.1'
ext.work_version = '2.7.1' ext.work_version = '2.9.0'
ext.concurrent_version = '1.1.0' ext.concurrent_version = '1.1.0'
ext.guava_version = '33.0.0-android' ext.guava_version = '33.0.0-android'
ext.glide_version = '4.14.2' ext.glide_version = '4.14.2'

View File

@ -20,6 +20,7 @@ import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/services/api.service.dart'; import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/backup_progress.dart';
import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/diff.dart';
import 'package:immich_mobile/utils/url_helper.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:path_provider_ios/path_provider_ios.dart'; import 'package:path_provider_ios/path_provider_ios.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
@ -68,8 +69,10 @@ class BackgroundService {
final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!; final callback = PluginUtilities.getCallbackHandle(_nativeEntry)!;
final String title = final String title =
"backup_background_service_default_notification".tr(); "backup_background_service_default_notification".tr();
final bool ok = await _foregroundChannel final bool ok = await _foregroundChannel.invokeMethod(
.invokeMethod('enable', [callback.toRawHandle(), title, immediate]); 'enable',
[callback.toRawHandle(), title, immediate, getServerUrl()],
);
return ok; return ok;
} catch (error) { } catch (error) {
return false; return false;