@@ -4,6 +4,7 @@ import 'dart:io';
44import 'dart:isolate' ;
55import 'dart:ui' show IsolateNameServer, PluginUtilities;
66import 'package:cancellation_token_http/http.dart' ;
7+ import 'package:collection/collection.dart' ;
78import 'package:easy_localization/easy_localization.dart' ;
89import 'package:flutter/services.dart' ;
910import 'package:flutter/widgets.dart' ;
@@ -16,6 +17,7 @@ import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dar
1617import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart' ;
1718import 'package:immich_mobile/modules/backup/services/backup.service.dart' ;
1819import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart' ;
20+ import 'package:immich_mobile/modules/settings/services/app_settings.service.dart' ;
1921import 'package:immich_mobile/shared/services/api.service.dart' ;
2022import 'package:photo_manager/photo_manager.dart' ;
2123
@@ -39,6 +41,7 @@ class BackgroundService {
3941 bool _hasLock = false ;
4042 SendPort ? _waitingIsolate;
4143 ReceivePort ? _rp;
44+ bool _errorGracePeriodExceeded = true ;
4245
4346 bool get isForegroundInitialized {
4447 return _isForegroundInitialized;
@@ -140,8 +143,8 @@ class BackgroundService {
140143 }
141144
142145 /// Updates the notification shown by the background service
143- Future <bool > updateNotification ({
144- String title = "Immich" ,
146+ Future <bool > _updateNotification ({
147+ required String title,
145148 String ? content,
146149 }) async {
147150 if (! Platform .isAndroid) {
@@ -153,28 +156,44 @@ class BackgroundService {
153156 .invokeMethod ('updateNotification' , [title, content]);
154157 }
155158 } catch (error) {
156- debugPrint ("[updateNotification ] failed to communicate with plugin" );
159+ debugPrint ("[_updateNotification ] failed to communicate with plugin" );
157160 }
158161 return Future .value (false );
159162 }
160163
161164 /// Shows a new priority notification
162- Future <bool > showErrorNotification (
163- String title,
164- String content,
165- ) async {
165+ Future <bool > _showErrorNotification ({
166+ required String title,
167+ String ? content,
168+ String ? individualTag,
169+ }) async {
166170 if (! Platform .isAndroid) {
167171 return true ;
168172 }
169173 try {
170- if (_isBackgroundInitialized) {
174+ if (_isBackgroundInitialized && _errorGracePeriodExceeded ) {
171175 return await _backgroundChannel
172- .invokeMethod ('showError' , [title, content]);
176+ .invokeMethod ('showError' , [title, content, individualTag ]);
173177 }
174178 } catch (error) {
175- debugPrint ("[showErrorNotification ] failed to communicate with plugin" );
179+ debugPrint ("[_showErrorNotification ] failed to communicate with plugin" );
176180 }
177- return Future .value (false );
181+ return false ;
182+ }
183+
184+ Future <bool > _clearErrorNotifications () async {
185+ if (! Platform .isAndroid) {
186+ return true ;
187+ }
188+ try {
189+ if (_isBackgroundInitialized) {
190+ return await _backgroundChannel.invokeMethod ('clearErrorNotifications' );
191+ }
192+ } catch (error) {
193+ debugPrint (
194+ "[_clearErrorNotifications] failed to communicate with plugin" );
195+ }
196+ return false ;
178197 }
179198
180199 /// await to ensure this thread (foreground or background) has exclusive access
@@ -278,7 +297,15 @@ class BackgroundService {
278297 return false ;
279298 }
280299 await translationsLoaded;
281- return await _onAssetsChanged ();
300+ final bool ok = await _onAssetsChanged ();
301+ if (ok) {
302+ Hive .box (backgroundBackupInfoBox).delete (backupFailedSince);
303+ } else if (Hive .box (backgroundBackupInfoBox).get (backupFailedSince) ==
304+ null ) {
305+ Hive .box (backgroundBackupInfoBox)
306+ .put (backupFailedSince, DateTime .now ());
307+ }
308+ return ok;
282309 } catch (error) {
283310 debugPrint (error.toString ());
284311 return false ;
@@ -303,6 +330,8 @@ class BackgroundService {
303330 Hive .registerAdapter (HiveBackupAlbumsAdapter ());
304331 await Hive .openBox (userInfoBox);
305332 await Hive .openBox <HiveSavedLoginInfo >(hiveLoginInfoBox);
333+ await Hive .openBox (userSettingInfoBox);
334+ await Hive .openBox (backgroundBackupInfoBox);
306335
307336 ApiService apiService = ApiService ();
308337 apiService.setEndpoint (Hive .box (userInfoBox).get (serverEndpointKey));
@@ -313,23 +342,36 @@ class BackgroundService {
313342 await Hive .openBox <HiveBackupAlbums >(hiveBackupInfoBox);
314343 final HiveBackupAlbums ? backupAlbumInfo = box.get (backupInfoKey);
315344 if (backupAlbumInfo == null ) {
345+ _clearErrorNotifications ();
316346 return true ;
317347 }
318348
319349 await PhotoManager .setIgnorePermissionCheck (true );
350+ _errorGracePeriodExceeded = _isErrorGracePeriodExceeded ();
320351
321352 if (_canceledBySystem) {
322353 return false ;
323354 }
324355
325- final List <AssetEntity > toUpload =
326- await backupService.getAssetsToBackup (backupAlbumInfo);
356+ List <AssetEntity > toUpload =
357+ await backupService.buildUploadCandidates (backupAlbumInfo);
358+
359+ try {
360+ toUpload = await backupService.removeAlreadyUploadedAssets (toUpload);
361+ } catch (e) {
362+ _showErrorNotification (
363+ title: "backup_background_service_error_title" .tr (),
364+ content: "backup_background_service_connection_failed_message" .tr (),
365+ );
366+ return false ;
367+ }
327368
328369 if (_canceledBySystem) {
329370 return false ;
330371 }
331372
332373 if (toUpload.isEmpty) {
374+ _clearErrorNotifications ();
333375 return true ;
334376 }
335377
@@ -343,10 +385,16 @@ class BackgroundService {
343385 _onBackupError,
344386 );
345387 if (ok) {
388+ _clearErrorNotifications ();
346389 await box.put (
347390 backupInfoKey,
348391 backupAlbumInfo,
349392 );
393+ } else {
394+ _showErrorNotification (
395+ title: "backup_background_service_error_title" .tr (),
396+ content: "backup_background_service_backup_failed_message" .tr (),
397+ );
350398 }
351399 return ok;
352400 }
@@ -358,20 +406,48 @@ class BackgroundService {
358406 void _onProgress (int sent, int total) {}
359407
360408 void _onBackupError (ErrorUploadAsset errorAssetInfo) {
361- showErrorNotification (
362- "backup_background_service_upload_failure_notification"
409+ _showErrorNotification (
410+ title: "Upload failed" ,
411+ content: "backup_background_service_upload_failure_notification"
363412 .tr (args: [errorAssetInfo.fileName]),
364- errorAssetInfo.errorMessage ,
413+ individualTag : errorAssetInfo.id ,
365414 );
366415 }
367416
368417 void _onSetCurrentBackupAsset (CurrentUploadAsset currentUploadAsset) {
369- updateNotification (
418+ _updateNotification (
370419 title: "backup_background_service_in_progress_notification" .tr (),
371420 content: "backup_background_service_current_upload_notification"
372421 .tr (args: [currentUploadAsset.fileName]),
373422 );
374423 }
424+
425+ bool _isErrorGracePeriodExceeded () {
426+ final int value = AppSettingsService ()
427+ .getSetting (AppSettingsEnum .uploadErrorNotificationGracePeriod);
428+ if (value == 0 ) {
429+ return true ;
430+ } else if (value == 5 ) {
431+ return false ;
432+ }
433+ final DateTime ? failedSince =
434+ Hive .box (backgroundBackupInfoBox).get (backupFailedSince);
435+ if (failedSince == null ) {
436+ return false ;
437+ }
438+ final Duration duration = DateTime .now ().difference (failedSince);
439+ if (value == 1 ) {
440+ return duration > const Duration (minutes: 30 );
441+ } else if (value == 2 ) {
442+ return duration > const Duration (hours: 2 );
443+ } else if (value == 3 ) {
444+ return duration > const Duration (hours: 8 );
445+ } else if (value == 4 ) {
446+ return duration > const Duration (hours: 24 );
447+ }
448+ assert (false , "Invalid value" );
449+ return true ;
450+ }
375451}
376452
377453/// entry point called by Kotlin/Java code; needs to be a top-level function
0 commit comments