diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt index 3d48b8be5e..b9826f80e9 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt @@ -61,9 +61,8 @@ private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface BackgroundWorkerFgHostApi { - fun enableSyncWorker() - fun enableUploadWorker() - fun disableUploadWorker() + fun enable() + fun disable() companion object { /** The codec used by BackgroundWorkerFgHostApi. */ @@ -75,11 +74,11 @@ interface BackgroundWorkerFgHostApi { fun setUp(binaryMessenger: BinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker$separatedMessageChannelSuffix", codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> val wrapped: List = try { - api.enableSyncWorker() + api.enable() listOf(null) } catch (exception: Throwable) { BackgroundWorkerPigeonUtils.wrapError(exception) @@ -91,27 +90,11 @@ interface BackgroundWorkerFgHostApi { } } run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$separatedMessageChannelSuffix", codec) + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> val wrapped: List = try { - api.enableUploadWorker() - listOf(null) - } catch (exception: Throwable) { - BackgroundWorkerPigeonUtils.wrapError(exception) - } - reply.reply(wrapped) - } - } else { - channel.setMessageHandler(null) - } - } - run { - val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker$separatedMessageChannelSuffix", codec) - if (api != null) { - channel.setMessageHandler { _, reply -> - val wrapped: List = try { - api.disableUploadWorker() + api.disable() listOf(null) } catch (exception: Throwable) { BackgroundWorkerPigeonUtils.wrapError(exception) @@ -182,23 +165,6 @@ class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, p BackgroundWorkerPigeonCodec() } } - fun onLocalSync(maxSecondsArg: Long?, callback: (Result) -> Unit) -{ - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(maxSecondsArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) - } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(BackgroundWorkerPigeonUtils.createConnectionError(channelName))) - } - } - } fun onIosUpload(isRefreshArg: Boolean, maxSecondsArg: Long?, callback: (Result) -> Unit) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt index ed96b44769..d24852b1fa 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt @@ -16,11 +16,6 @@ import io.flutter.embedding.engine.loader.FlutterLoader private const val TAG = "BackgroundWorker" -enum class BackgroundTaskType { - LOCAL_SYNC, - UPLOAD, -} - class BackgroundWorker(context: Context, params: WorkerParameters) : ListenableWorker(context, params), BackgroundWorkerBgHostApi { private val ctx: Context = context.applicationContext @@ -84,13 +79,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : * This method acts as a bridge between the native Android background task system and Flutter. */ override fun onInitialized() { - val taskTypeIndex = inputData.getInt(BackgroundWorkerApiImpl.WORKER_DATA_TASK_TYPE, 0) - val taskType = BackgroundTaskType.entries[taskTypeIndex] - - when (taskType) { - BackgroundTaskType.LOCAL_SYNC -> flutterApi?.onLocalSync(null) { handleHostResult(it) } - BackgroundTaskType.UPLOAD -> flutterApi?.onAndroidUpload { handleHostResult(it) } - } + flutterApi?.onAndroidUpload { handleHostResult(it) } } override fun close() { diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerApiImpl.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerApiImpl.kt index 6cd4fbe0bf..4c2d98be71 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerApiImpl.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorkerApiImpl.kt @@ -3,10 +3,8 @@ package app.alextran.immich.background import android.content.Context import android.provider.MediaStore import android.util.Log -import androidx.core.content.edit import androidx.work.BackoffPolicy import androidx.work.Constraints -import androidx.work.Data import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest import androidx.work.WorkManager @@ -16,18 +14,13 @@ private const val TAG = "BackgroundUploadImpl" class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { private val ctx: Context = context.applicationContext - override fun enableSyncWorker() { - enqueueMediaObserver(ctx) - Log.i(TAG, "Scheduled media observer") - } - override fun enableUploadWorker() { - updateUploadEnabled(ctx, true) - Log.i(TAG, "Scheduled background upload tasks") + override fun enable() { + enqueueMediaObserver(ctx) } - override fun disableUploadWorker() { - updateUploadEnabled(ctx, false) + override fun disable() { + WorkManager.getInstance(ctx).cancelUniqueWork(OBSERVER_WORKER_NAME) WorkManager.getInstance(ctx).cancelUniqueWork(BACKGROUND_WORKER_NAME) Log.i(TAG, "Cancelled background upload tasks") } @@ -36,25 +29,14 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1" private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1" - const val WORKER_DATA_TASK_TYPE = "taskType" - - const val SHARED_PREF_NAME = "Immich::Background" - const val SHARED_PREF_BACKUP_ENABLED = "Background::backup::enabled" - - private fun updateUploadEnabled(context: Context, enabled: Boolean) { - context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE).edit { - putBoolean(SHARED_PREF_BACKUP_ENABLED, enabled) - } - } - fun enqueueMediaObserver(ctx: Context) { val constraints = Constraints.Builder() .addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true) .addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true) .addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true) .addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true) - .setTriggerContentUpdateDelay(5, TimeUnit.SECONDS) - .setTriggerContentMaxDelay(1, TimeUnit.MINUTES) + .setTriggerContentUpdateDelay(30, TimeUnit.SECONDS) + .setTriggerContentMaxDelay(3, TimeUnit.MINUTES) .build() val work = OneTimeWorkRequest.Builder(MediaObserver::class.java) @@ -66,15 +48,13 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi { Log.i(TAG, "Enqueued media observer worker with name: $OBSERVER_WORKER_NAME") } - fun enqueueBackgroundWorker(ctx: Context, taskType: BackgroundTaskType) { + fun enqueueBackgroundWorker(ctx: Context) { val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build() - val data = Data.Builder() - data.putInt(WORKER_DATA_TASK_TYPE, taskType.ordinal) val work = OneTimeWorkRequest.Builder(BackgroundWorker::class.java) .setConstraints(constraints) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES) - .setInputData(data.build()).build() + .build() WorkManager.getInstance(ctx) .enqueueUniqueWork(BACKGROUND_WORKER_NAME, ExistingWorkPolicy.REPLACE, work) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/MediaObserver.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/MediaObserver.kt index 0ec6eeb3a5..7283411ac0 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/MediaObserver.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/MediaObserver.kt @@ -6,29 +6,17 @@ import androidx.work.Worker import androidx.work.WorkerParameters class MediaObserver(context: Context, params: WorkerParameters) : Worker(context, params) { - private val ctx: Context = context.applicationContext + private val ctx: Context = context.applicationContext - override fun doWork(): Result { - Log.i("MediaObserver", "Content change detected, starting background worker") + override fun doWork(): Result { + Log.i("MediaObserver", "Content change detected, starting background worker") + // Re-enqueue itself to listen for future changes + BackgroundWorkerApiImpl.enqueueMediaObserver(ctx) - // Enqueue backup worker only if there are new media changes - if (triggeredContentUris.isNotEmpty()) { - val type = - if (isBackupEnabled(ctx)) BackgroundTaskType.UPLOAD else BackgroundTaskType.LOCAL_SYNC - BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx, type) - } - - // Re-enqueue itself to listen for future changes - BackgroundWorkerApiImpl.enqueueMediaObserver(ctx) - return Result.success() - } - - private fun isBackupEnabled(context: Context): Boolean { - val prefs = - context.getSharedPreferences( - BackgroundWorkerApiImpl.SHARED_PREF_NAME, - Context.MODE_PRIVATE - ) - return prefs.getBoolean(BackgroundWorkerApiImpl.SHARED_PREF_BACKUP_ENABLED, false) + // Enqueue backup worker only if there are new media changes + if (triggeredContentUris.isNotEmpty()) { + BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx) } + return Result.success() + } } diff --git a/mobile/ios/Runner/Background/BackgroundWorker.g.swift b/mobile/ios/Runner/Background/BackgroundWorker.g.swift index b81a5f7576..bfc0b26d9b 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.g.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.g.swift @@ -73,9 +73,8 @@ class BackgroundWorkerPigeonCodec: FlutterStandardMessageCodec, @unchecked Senda /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol BackgroundWorkerFgHostApi { - func enableSyncWorker() throws - func enableUploadWorker() throws - func disableUploadWorker() throws + func enable() throws + func disable() throws } /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. @@ -84,44 +83,31 @@ class BackgroundWorkerFgHostApiSetup { /// Sets up an instance of `BackgroundWorkerFgHostApi` to handle messages through the `binaryMessenger`. static func setUp(binaryMessenger: FlutterBinaryMessenger, api: BackgroundWorkerFgHostApi?, messageChannelSuffix: String = "") { let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" - let enableSyncWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let enableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { - enableSyncWorkerChannel.setMessageHandler { _, reply in + enableChannel.setMessageHandler { _, reply in do { - try api.enableSyncWorker() + try api.enable() reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { - enableSyncWorkerChannel.setMessageHandler(nil) + enableChannel.setMessageHandler(nil) } - let enableUploadWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + let disableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { - enableUploadWorkerChannel.setMessageHandler { _, reply in + disableChannel.setMessageHandler { _, reply in do { - try api.enableUploadWorker() + try api.disable() reply(wrapResult(nil)) } catch { reply(wrapError(error)) } } } else { - enableUploadWorkerChannel.setMessageHandler(nil) - } - let disableUploadWorkerChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) - if let api = api { - disableUploadWorkerChannel.setMessageHandler { _, reply in - do { - try api.disableUploadWorker() - reply(wrapResult(nil)) - } catch { - reply(wrapError(error)) - } - } - } else { - disableUploadWorkerChannel.setMessageHandler(nil) + disableChannel.setMessageHandler(nil) } } } @@ -167,7 +153,6 @@ class BackgroundWorkerBgHostApiSetup { } /// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. protocol BackgroundWorkerFlutterApiProtocol { - func onLocalSync(maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result) -> Void) func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result) -> Void) func onAndroidUpload(completion: @escaping (Result) -> Void) func cancel(completion: @escaping (Result) -> Void) @@ -182,24 +167,6 @@ class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol { var codec: BackgroundWorkerPigeonCodec { return BackgroundWorkerPigeonCodec.shared } - func onLocalSync(maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result) -> Void) { - let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync\(messageChannelSuffix)" - let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage([maxSecondsArg] as [Any?]) { response in - guard let listResponse = response as? [Any?] else { - completion(.failure(createConnectionError(withChannelName: channelName))) - return - } - if listResponse.count > 1 { - let code: String = listResponse[0] as! String - let message: String? = nilOrValue(listResponse[1]) - let details: String? = nilOrValue(listResponse[2]) - completion(.failure(PigeonError(code: code, message: message, details: details))) - } else { - completion(.success(())) - } - } - } func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result) -> Void) { let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload\(messageChannelSuffix)" let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) diff --git a/mobile/ios/Runner/Background/BackgroundWorker.swift b/mobile/ios/Runner/Background/BackgroundWorker.swift index fb0fed6b5c..eeaa071653 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.swift @@ -1,7 +1,7 @@ import BackgroundTasks import Flutter -enum BackgroundTaskType { case localSync, refreshUpload, processingUpload } +enum BackgroundTaskType { case refresh, processing } /* * DEBUG: Testing Background Tasks in Xcode @@ -9,10 +9,6 @@ enum BackgroundTaskType { case localSync, refreshUpload, processingUpload } * To test background task functionality during development: * 1. Pause the application in Xcode debugger * 2. In the debugger console, enter one of the following commands: - - ## For local sync (short-running sync): - - e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.background.localSync"] ## For background refresh (short-running sync): @@ -24,8 +20,6 @@ enum BackgroundTaskType { case localSync, refreshUpload, processingUpload } * To simulate task expiration (useful for testing expiration handlers): - e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.localSync"] - e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.refreshUpload"] e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"app.alextran.immich.background.processingUpload"] @@ -120,17 +114,9 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { * This method acts as a bridge between the native iOS background task system and Flutter. */ func onInitialized() throws { - switch self.taskType { - case .refreshUpload, .processingUpload: - flutterApi?.onIosUpload(isRefresh: self.taskType == .refreshUpload, - maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in - self.handleHostResult(result: result) - }) - case .localSync: - flutterApi?.onLocalSync(maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in - self.handleHostResult(result: result) - }) - } + flutterApi?.onIosUpload(isRefresh: self.taskType == .refresh, maxSeconds: maxSeconds.map { Int64($0) }, completion: { result in + self.handleHostResult(result: result) + }) } /** @@ -177,6 +163,10 @@ class BackgroundWorker: BackgroundWorkerBgHostApi { * - Parameter success: Indicates whether the background task completed successfully */ private func complete(success: Bool) { + if(isComplete) { + return + } + isComplete = true engine.destroyContext() completionHandler(success) diff --git a/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift b/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift index 0bc46ff6b2..941e90cd44 100644 --- a/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift +++ b/mobile/ios/Runner/Background/BackgroundWorkerApiImpl.swift @@ -1,77 +1,40 @@ import BackgroundTasks class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { - func enableSyncWorker() throws { - BackgroundWorkerApiImpl.scheduleLocalSync() - print("BackgroundUploadImpl:enableSyncWorker Local Sync worker scheduled") - } - - func enableUploadWorker() throws { - BackgroundWorkerApiImpl.updateUploadEnabled(true) - - BackgroundWorkerApiImpl.scheduleRefreshUpload() - BackgroundWorkerApiImpl.scheduleProcessingUpload() - print("BackgroundUploadImpl:enableUploadWorker Scheduled background upload tasks") + + func enable() throws { + BackgroundWorkerApiImpl.scheduleRefreshWorker() + BackgroundWorkerApiImpl.scheduleProcessingWorker() + print("BackgroundUploadImpl:enbale Background worker scheduled") } - func disableUploadWorker() throws { - BackgroundWorkerApiImpl.updateUploadEnabled(false) - BackgroundWorkerApiImpl.cancelUploadTasks() - print("BackgroundUploadImpl:disableUploadWorker Disabled background upload tasks") + func disable() throws { + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.refreshTaskID); + BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: BackgroundWorkerApiImpl.processingTaskID); + print("BackgroundUploadImpl:disableUploadWorker Disabled background workers") } - public static let backgroundUploadEnabledKey = "immich:background:backup:enabled" - - private static let localSyncTaskID = "app.alextran.immich.background.localSync" - private static let refreshUploadTaskID = "app.alextran.immich.background.refreshUpload" - private static let processingUploadTaskID = "app.alextran.immich.background.processingUpload" - - private static func updateUploadEnabled(_ isEnabled: Bool) { - return UserDefaults.standard.set(isEnabled, forKey: BackgroundWorkerApiImpl.backgroundUploadEnabledKey) - } - - private static func cancelUploadTasks() { - BackgroundWorkerApiImpl.updateUploadEnabled(false) - BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: refreshUploadTaskID); - BGTaskScheduler.shared.cancel(taskRequestWithIdentifier: processingUploadTaskID); - } + private static let refreshTaskID = "app.alextran.immich.background.refreshUpload" + private static let processingTaskID = "app.alextran.immich.background.processingUpload" public static func registerBackgroundWorkers() { BGTaskScheduler.shared.register( - forTaskWithIdentifier: processingUploadTaskID, using: nil) { task in + forTaskWithIdentifier: processingTaskID, using: nil) { task in if task is BGProcessingTask { handleBackgroundProcessing(task: task as! BGProcessingTask) } } BGTaskScheduler.shared.register( - forTaskWithIdentifier: refreshUploadTaskID, using: nil) { task in + forTaskWithIdentifier: refreshTaskID, using: nil) { task in if task is BGAppRefreshTask { - handleBackgroundRefresh(task: task as! BGAppRefreshTask, taskType: .refreshUpload) + handleBackgroundRefresh(task: task as! BGAppRefreshTask) } } - - BGTaskScheduler.shared.register( - forTaskWithIdentifier: localSyncTaskID, using: nil) { task in - if task is BGAppRefreshTask { - handleBackgroundRefresh(task: task as! BGAppRefreshTask, taskType: .localSync) - } - } - } - - private static func scheduleLocalSync() { - let backgroundRefresh = BGAppRefreshTaskRequest(identifier: localSyncTaskID) - backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins - - do { - try BGTaskScheduler.shared.submit(backgroundRefresh) - } catch { - print("Could not schedule the local sync task \(error.localizedDescription)") - } } - private static func scheduleRefreshUpload() { - let backgroundRefresh = BGAppRefreshTaskRequest(identifier: refreshUploadTaskID) + private static func scheduleRefreshWorker() { + let backgroundRefresh = BGAppRefreshTaskRequest(identifier: refreshTaskID) backgroundRefresh.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60) // 5 mins do { @@ -81,8 +44,8 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { } } - private static func scheduleProcessingUpload() { - let backgroundProcessing = BGProcessingTaskRequest(identifier: processingUploadTaskID) + private static func scheduleProcessingWorker() { + let backgroundProcessing = BGProcessingTaskRequest(identifier: processingTaskID) backgroundProcessing.requiresNetworkConnectivity = true backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) // 15 mins @@ -94,29 +57,16 @@ class BackgroundWorkerApiImpl: BackgroundWorkerFgHostApi { } } - private static func handleBackgroundRefresh(task: BGAppRefreshTask, taskType: BackgroundTaskType) { - let maxSeconds: Int? - - switch taskType { - case .localSync: - maxSeconds = 15 - scheduleLocalSync() - case .refreshUpload: - maxSeconds = 20 - scheduleRefreshUpload() - case .processingUpload: - print("Unexpected background refresh task encountered") - return; - } - + private static func handleBackgroundRefresh(task: BGAppRefreshTask) { + scheduleRefreshWorker() // Restrict the refresh task to run only for a maximum of (maxSeconds) seconds - runBackgroundWorker(task: task, taskType: taskType, maxSeconds: maxSeconds) + runBackgroundWorker(task: task, taskType: .refresh, maxSeconds: 20) } private static func handleBackgroundProcessing(task: BGProcessingTask) { - scheduleProcessingUpload() + scheduleProcessingWorker() // There are no restrictions for processing tasks. Although, the OS could signal expiration at any time - runBackgroundWorker(task: task, taskType: .processingUpload, maxSeconds: nil) + runBackgroundWorker(task: task, taskType: .processing, maxSeconds: nil) } /** diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 2c4473c039..04e5e01392 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -1,190 +1,189 @@ - - AppGroupId - $(CUSTOM_GROUP_ID) - BGTaskSchedulerPermittedIdentifiers - - app.alextran.immich.background.localSync - app.alextran.immich.background.refreshUpload - app.alextran.immich.background.processingUpload - app.alextran.immich.backgroundFetch - app.alextran.immich.backgroundProcessing - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - ${PRODUCT_NAME} - CFBundleDocumentTypes - - - CFBundleTypeName - ShareHandler - LSHandlerRank - Alternate - LSItemContentTypes - - public.file-url - public.image - public.text - public.movie - public.url - public.data - - - - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleLocalizations - - en - ar - ca - cs - da - de - es - fi - fr - he - hi - hu - it - ja - ko - lv - mn - nb - nl - pl - pt - ro - ru - sk - sl - sr - sv - th - uk - vi - zh - - CFBundleName - immich_mobile - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.140.0 - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - Share Extension - CFBundleURLSchemes - - ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) - - - - CFBundleTypeRole - Editor - CFBundleURLName - Deep Link - CFBundleURLSchemes - - immich - - - - CFBundleVersion - 219 - FLTEnableImpeller - - ITSAppUsesNonExemptEncryption - - LSApplicationQueriesSchemes - - https - - LSRequiresIPhoneOS - - LSSupportsOpeningDocumentsInPlace - No - MGLMapboxMetricsEnabledSettingShownInApp - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSBonjourServices - - _googlecast._tcp - _CC1AD845._googlecast._tcp - - NSCameraUsageDescription - We need to access the camera to let you take beautiful video using this app - NSFaceIDUsageDescription - We need to use FaceID to allow access to your locked folder - NSLocalNetworkUsageDescription - We need local network permission to connect to the local server using IP address and + + AppGroupId + $(CUSTOM_GROUP_ID) + BGTaskSchedulerPermittedIdentifiers + + app.alextran.immich.background.refreshUpload + app.alextran.immich.background.processingUpload + app.alextran.immich.backgroundFetch + app.alextran.immich.backgroundProcessing + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + ${PRODUCT_NAME} + CFBundleDocumentTypes + + + CFBundleTypeName + ShareHandler + LSHandlerRank + Alternate + LSItemContentTypes + + public.file-url + public.image + public.text + public.movie + public.url + public.data + + + + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLocalizations + + en + ar + ca + cs + da + de + es + fi + fr + he + hi + hu + it + ja + ko + lv + mn + nb + nl + pl + pt + ro + ru + sk + sl + sr + sv + th + uk + vi + zh + + CFBundleName + immich_mobile + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.140.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + Share Extension + CFBundleURLSchemes + + ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) + + + + CFBundleTypeRole + Editor + CFBundleURLName + Deep Link + CFBundleURLSchemes + + immich + + + + CFBundleVersion + 219 + FLTEnableImpeller + + ITSAppUsesNonExemptEncryption + + LSApplicationQueriesSchemes + + https + + LSRequiresIPhoneOS + + LSSupportsOpeningDocumentsInPlace + No + MGLMapboxMetricsEnabledSettingShownInApp + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSBonjourServices + + _googlecast._tcp + _CC1AD845._googlecast._tcp + + NSCameraUsageDescription + We need to access the camera to let you take beautiful video using this app + NSFaceIDUsageDescription + We need to use FaceID to allow access to your locked folder + NSLocalNetworkUsageDescription + We need local network permission to connect to the local server using IP address and allow the casting feature to work - NSLocationAlwaysAndWhenInUseUsageDescription - We require this permission to access the local WiFi name for background upload mechanism - NSLocationUsageDescription - We require this permission to access the local WiFi name - NSLocationWhenInUseUsageDescription - We require this permission to access the local WiFi name - NSMicrophoneUsageDescription - We need to access the microphone to let you take beautiful video using this app - NSPhotoLibraryAddUsageDescription - We need to manage backup your photos album - NSPhotoLibraryUsageDescription - We need to manage backup your photos album - NSUserActivityTypes - - INSendMessageIntent - - UIApplicationSupportsIndirectInputEvents - - UIBackgroundModes - - fetch - processing - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIStatusBarHidden - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - io.flutter.embedded_views_preview - - - \ No newline at end of file + NSLocationAlwaysAndWhenInUseUsageDescription + We require this permission to access the local WiFi name for background upload mechanism + NSLocationUsageDescription + We require this permission to access the local WiFi name + NSLocationWhenInUseUsageDescription + We require this permission to access the local WiFi name + NSMicrophoneUsageDescription + We need to access the microphone to let you take beautiful video using this app + NSPhotoLibraryAddUsageDescription + We need to manage backup your photos album + NSPhotoLibraryUsageDescription + We need to manage backup your photos album + NSUserActivityTypes + + INSendMessageIntent + + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + fetch + processing + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + io.flutter.embedded_views_preview + + + diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index b237840b75..cf8c6e7961 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -30,11 +30,9 @@ class BackgroundWorkerFgService { const BackgroundWorkerFgService(this._foregroundHostApi); // TODO: Move this call to native side once old timeline is removed - Future enableSyncService() => _foregroundHostApi.enableSyncWorker(); + Future enable() => _foregroundHostApi.enable(); - Future enableUploadService() => _foregroundHostApi.enableUploadWorker(); - - Future disableUploadService() => _foregroundHostApi.disableUploadWorker(); + Future disable() => _foregroundHostApi.disable(); } class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { @@ -93,30 +91,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { } } - @override - Future onLocalSync(int? maxSeconds) async { - try { - _logger.info('Local background syncing started'); - final sw = Stopwatch()..start(); - - final timeout = maxSeconds != null ? Duration(seconds: maxSeconds) : null; - await _syncAssets(hashTimeout: timeout, syncRemote: false); - - sw.stop(); - _logger.info("Local sync completed in ${sw.elapsed.inSeconds}s"); - } catch (error, stack) { - _logger.severe("Failed to complete local sync", error, stack); - } finally { - await _cleanup(); - } - } - - /* We do the following on Android upload - * - Sync local assets - * - Hash local assets 3 / 6 minutes - * - Sync remote assets - * - Check and requeue upload tasks - */ @override Future onAndroidUpload() async { try { @@ -135,14 +109,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { } } - /* We do the following on background upload - * - Sync local assets - * - Hash local assets - * - Sync remote assets - * - Check and requeue upload tasks - * - * The native side will not send the maxSeconds value for processing tasks - */ @override Future onIosUpload(bool isRefresh, int? maxSeconds) async { try { @@ -222,7 +188,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { } } - Future _syncAssets({Duration? hashTimeout, bool syncRemote = true}) async { + Future _syncAssets({Duration? hashTimeout}) async { final futures = >[]; final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async { @@ -244,10 +210,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { }); futures.add(localSyncFuture); - if (syncRemote) { - final remoteSyncFuture = _ref.read(backgroundSyncProvider).syncRemote(); - futures.add(remoteSyncFuture); - } + futures.add(_ref.read(backgroundSyncProvider).syncRemote()); await Future.wait(futures); } diff --git a/mobile/lib/domain/services/hash.service.dart b/mobile/lib/domain/services/hash.service.dart index 90720fdc76..584ebf8c85 100644 --- a/mobile/lib/domain/services/hash.service.dart +++ b/mobile/lib/domain/services/hash.service.dart @@ -35,6 +35,7 @@ class HashService { bool get isCancelled => _cancelChecker?.call() ?? false; Future hashAssets() async { + _log.info("Starting hashing of assets"); final Stopwatch stopwatch = Stopwatch()..start(); // Sorted by backupSelection followed by isCloud final localAlbums = await _localAlbumRepository.getAll( diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index afc9c13181..9066c5bfc7 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -16,7 +16,6 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; @@ -26,7 +25,6 @@ import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/routing/app_navigation_observer.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/deep_link.service.dart'; import 'package:immich_mobile/services/local_notification.service.dart'; @@ -207,12 +205,9 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve // needs to be delayed so that EasyLocalization is working if (Store.isBetaTimelineEnabled) { ref.read(backgroundServiceProvider).disableService(); - ref.read(driftBackgroundUploadFgService).enableSyncService(); - if (ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup)) { - ref.read(driftBackgroundUploadFgService).enableUploadService(); - } + ref.read(driftBackgroundUploadFgService).enable(); } else { - ref.read(driftBackgroundUploadFgService).disableUploadService(); + ref.read(driftBackgroundUploadFgService).disable(); ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); } }); diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index 5140c62a0d..b125c35908 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -8,7 +8,6 @@ import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; -import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -43,12 +42,10 @@ class _DriftBackupPageState extends ConsumerState { await ref.read(backgroundSyncProvider).syncRemote(); await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id); - await ref.read(driftBackgroundUploadFgService).enableUploadService(); await ref.read(driftBackupProvider.notifier).startBackup(currentUser.id); } Future stopBackup() async { - await ref.read(driftBackgroundUploadFgService).disableUploadService(); await ref.read(driftBackupProvider.notifier).cancel(); } diff --git a/mobile/lib/pages/common/change_experience.page.dart b/mobile/lib/pages/common/change_experience.page.dart index 9bb2895907..ffdba1fb71 100644 --- a/mobile/lib/pages/common/change_experience.page.dart +++ b/mobile/lib/pages/common/change_experience.page.dart @@ -79,7 +79,7 @@ class _ChangeExperiencePageState extends ConsumerState { ref.read(readonlyModeProvider.notifier).setReadonlyMode(false); await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider)); await ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); - await ref.read(driftBackgroundUploadFgService).disableUploadService(); + await ref.read(driftBackgroundUploadFgService).disable(); } await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); diff --git a/mobile/lib/platform/background_worker_api.g.dart b/mobile/lib/platform/background_worker_api.g.dart index 12e4d2c0c5..4b5689f4df 100644 --- a/mobile/lib/platform/background_worker_api.g.dart +++ b/mobile/lib/platform/background_worker_api.g.dart @@ -59,9 +59,9 @@ class BackgroundWorkerFgHostApi { final String pigeonVar_messageChannelSuffix; - Future enableSyncWorker() async { + Future enable() async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enable$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -82,32 +82,9 @@ class BackgroundWorkerFgHostApi { } } - Future enableUploadWorker() async { + Future disable() async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); - final List? pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } - } - - Future disableUploadWorker() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disable$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -192,8 +169,6 @@ class BackgroundWorkerBgHostApi { abstract class BackgroundWorkerFlutterApi { static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - Future onLocalSync(int? maxSeconds); - Future onIosUpload(bool isRefresh, int? maxSeconds); Future onAndroidUpload(); @@ -206,35 +181,6 @@ abstract class BackgroundWorkerFlutterApi { String messageChannelSuffix = '', }) { messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - { - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync$messageChannelSuffix', - pigeonChannelCodec, - binaryMessenger: binaryMessenger, - ); - if (api == null) { - pigeonVar_channel.setMessageHandler(null); - } else { - pigeonVar_channel.setMessageHandler((Object? message) async { - assert( - message != null, - 'Argument for dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync was null.', - ); - final List args = (message as List?)!; - final int? arg_maxSeconds = (args[0] as int?); - try { - await api.onLocalSync(arg_maxSeconds); - return wrapResponse(empty: true); - } on PlatformException catch (e) { - return wrapResponse(error: e); - } catch (e) { - return wrapResponse( - error: PlatformException(code: 'error', message: e.toString()), - ); - } - }); - } - } { final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( 'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$messageChannelSuffix', diff --git a/mobile/pigeon/background_worker_api.dart b/mobile/pigeon/background_worker_api.dart index b0c785f2e1..69684b82b1 100644 --- a/mobile/pigeon/background_worker_api.dart +++ b/mobile/pigeon/background_worker_api.dart @@ -13,12 +13,9 @@ import 'package:pigeon/pigeon.dart'; ) @HostApi() abstract class BackgroundWorkerFgHostApi { - void enableSyncWorker(); + void enable(); - void enableUploadWorker(); - - // Disables the background upload service - void disableUploadWorker(); + void disable(); } @HostApi() @@ -32,10 +29,6 @@ abstract class BackgroundWorkerBgHostApi { @FlutterApi() abstract class BackgroundWorkerFlutterApi { - // Android & iOS: Called when the local sync is triggered - @async - void onLocalSync(int? maxSeconds); - // iOS Only: Called when the iOS background upload is triggered @async void onIosUpload(bool isRefresh, int? maxSeconds);