You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	feat(android) Check server is reachable before starting background backup (#8989)
* Check that server is reachable before starting backup work * Fix iOS not starting background service --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
		| @@ -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) | ||||||
|   | |||||||
| @@ -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" | ||||||
| @@ -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 | ||||||
|  | } | ||||||
|   | |||||||
| @@ -171,9 +171,9 @@ class BackgroundServicePlugin: NSObject, FlutterPlugin { | |||||||
|             return |             return | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         // Requires 3 arguments in the array |         // Requires 3 or more arguments in the array | ||||||
|         guard args.count == 3 else { |         guard args.count >= 3 else { | ||||||
|             print("Requires 3 arguments and received \(args.count)") |             print("Requires 3 or more arguments and received \(args.count)") | ||||||
|             result(FlutterMethodNotImplemented) |             result(FlutterMethodNotImplemented) | ||||||
|             return |             return | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -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; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user