mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05: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:
		
							parent
							
								
									3abfe3c99e
								
							
						
					
					
						commit
						71b6d8b569
					
				@ -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"
 | 
				
			||||||
 | 
				
			|||||||
@ -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"
 | 
				
			||||||
@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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'
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user