Browse Source

feat(mobile): configurable background backup delay (#1068)

let's the user configure how much to delay the trigger for running the backup whenever assets are changed on the device
Fynn Petersen-Frey 2 years ago
parent
commit
c23b2479f7

+ 3 - 1
mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt

@@ -54,7 +54,9 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
                 val args = call.arguments<ArrayList<*>>()!!
                 val requireUnmeteredNetwork = args.get(0) as Boolean
                 val requireCharging = args.get(1) as Boolean
-                ContentObserverWorker.configureWork(ctx, requireUnmeteredNetwork, requireCharging)
+                val triggerUpdateDelay = (args.get(2) as Number).toLong()
+                val triggerMaxDelay = (args.get(3) as Number).toLong()
+                ContentObserverWorker.configureWork(ctx, requireUnmeteredNetwork, requireCharging, triggerUpdateDelay, triggerMaxDelay)
                 result.success(true)
             }
             "disable" -> {

+ 10 - 2
mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt

@@ -37,6 +37,8 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx
         const val SHARED_PREF_SERVICE_ENABLED = "serviceEnabled"
         const val SHARED_PREF_REQUIRE_WIFI = "requireWifi"
         const val SHARED_PREF_REQUIRE_CHARGING = "requireCharging"
+        const val SHARED_PREF_TRIGGER_UPDATE_DELAY = "triggerUpdateDelay"
+        const val SHARED_PREF_TRIGGER_MAX_DELAY = "triggerMaxDelay"
 
         private const val TASK_NAME_OBSERVER = "immich/ContentObserver"
 
@@ -62,12 +64,16 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx
          */
         fun configureWork(context: Context,
                           requireWifi: Boolean = false,
-                          requireCharging: Boolean = false) {
+                          requireCharging: Boolean = false,
+                          triggerUpdateDelay: Long = 5000,
+                          triggerMaxDelay: Long = 50000) {
             context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
                 .edit()
                 .putBoolean(SHARED_PREF_SERVICE_ENABLED, true)
                 .putBoolean(SHARED_PREF_REQUIRE_WIFI, requireWifi)
                 .putBoolean(SHARED_PREF_REQUIRE_CHARGING, requireCharging)
+                .putLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, triggerUpdateDelay)
+                .putLong(SHARED_PREF_TRIGGER_MAX_DELAY, triggerMaxDelay)
                 .apply()
             BackupWorker.updateBackupWorker(context, requireWifi, requireCharging)
         }
@@ -106,12 +112,14 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx
         }
 
         private fun enqueueObserverWorker(context: Context, policy: ExistingWorkPolicy) {
+            val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
             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(5000, TimeUnit.MILLISECONDS)
+                .setTriggerContentUpdateDelay(sp.getLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, 5000), TimeUnit.MILLISECONDS)
+                .setTriggerContentMaxDelay(sp.getLong(SHARED_PREF_TRIGGER_MAX_DELAY, 50000), TimeUnit.MILLISECONDS)
                 .build()
                 
             val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java)

+ 2 - 0
mobile/assets/i18n/en-US.json

@@ -41,6 +41,7 @@
   "backup_controller_page_background_turn_off": "Turn off background service",
   "backup_controller_page_background_turn_on": "Turn on background service",
   "backup_controller_page_background_wifi": "Only on WiFi",
+  "backup_controller_page_background_delay": "Delay new assets backup: {}",
   "backup_controller_page_backup": "Backup",
   "backup_controller_page_backup_selected": "Selected: ",
   "backup_controller_page_backup_sub": "Backed up photos and videos",
@@ -134,6 +135,7 @@
   "setting_notifications_notify_hours": "{} hours",
   "setting_notifications_notify_immediately": "immediately",
   "setting_notifications_notify_minutes": "{} minutes",
+  "setting_notifications_notify_seconds": "{} seconds",
   "setting_notifications_notify_never": "never",
   "setting_notifications_subtitle": "Adjust your notification preferences",
   "setting_notifications_title": "Notifications",

+ 1 - 0
mobile/lib/constants/hive_box.dart

@@ -26,6 +26,7 @@ const String backgroundBackupInfoBox = "immichBackgroundBackupInfoBox"; // Box
 const String backupFailedSince = "immichBackupFailedSince"; // Key 1
 const String backupRequireWifi = "immichBackupRequireWifi"; // Key 2
 const String backupRequireCharging = "immichBackupRequireCharging"; // Key 3
+const String backupTriggerDelay = "immichBackupTriggerDelay"; // Key 4
 
 // Duplicate asset
 const String duplicatedAssetsBox = "immichDuplicatedAssetsBox"; // Box

+ 8 - 1
mobile/lib/modules/backup/background_service/background.service.dart

@@ -86,6 +86,8 @@ class BackgroundService {
   Future<bool> configureService({
     bool requireUnmetered = true,
     bool requireCharging = false,
+    int triggerUpdateDelay = 5000,
+    int triggerMaxDelay = 50000,
   }) async {
     if (!Platform.isAndroid) {
       return true;
@@ -93,7 +95,12 @@ class BackgroundService {
     try {
       final bool ok = await _foregroundChannel.invokeMethod(
         'configure',
-        [requireUnmetered, requireCharging],
+        [
+          requireUnmetered,
+          requireCharging,
+          triggerUpdateDelay,
+          triggerMaxDelay
+        ],
       );
       return ok;
     } catch (error) {

+ 7 - 1
mobile/lib/modules/backup/models/backup_state.model.dart

@@ -18,6 +18,7 @@ class BackUpState {
   final bool backgroundBackup;
   final bool backupRequireWifi;
   final bool backupRequireCharging;
+  final int backupTriggerDelay;
 
   /// All available albums on the device
   final List<AvailableAlbum> availableAlbums;
@@ -42,6 +43,7 @@ class BackUpState {
     required this.backgroundBackup,
     required this.backupRequireWifi,
     required this.backupRequireCharging,
+    required this.backupTriggerDelay,
     required this.availableAlbums,
     required this.selectedBackupAlbums,
     required this.excludedBackupAlbums,
@@ -59,6 +61,7 @@ class BackUpState {
     bool? backgroundBackup,
     bool? backupRequireWifi,
     bool? backupRequireCharging,
+    int? backupTriggerDelay,
     List<AvailableAlbum>? availableAlbums,
     Set<AvailableAlbum>? selectedBackupAlbums,
     Set<AvailableAlbum>? excludedBackupAlbums,
@@ -76,6 +79,7 @@ class BackUpState {
       backupRequireWifi: backupRequireWifi ?? this.backupRequireWifi,
       backupRequireCharging:
           backupRequireCharging ?? this.backupRequireCharging,
+      backupTriggerDelay: backupTriggerDelay ?? this.backupTriggerDelay,
       availableAlbums: availableAlbums ?? this.availableAlbums,
       selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums,
       excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums,
@@ -88,7 +92,7 @@ class BackUpState {
 
   @override
   String toString() {
-    return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)';
+    return 'BackUpState(backupProgress: $backupProgress, allAssetsInDatabase: $allAssetsInDatabase, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo, backgroundBackup: $backgroundBackup, backupRequireWifi: $backupRequireWifi, backupRequireCharging: $backupRequireCharging, backupTriggerDelay: $backupTriggerDelay, availableAlbums: $availableAlbums, selectedBackupAlbums: $selectedBackupAlbums, excludedBackupAlbums: $excludedBackupAlbums, allUniqueAssets: $allUniqueAssets, selectedAlbumsBackupAssetsIds: $selectedAlbumsBackupAssetsIds, currentUploadAsset: $currentUploadAsset)';
   }
 
   @override
@@ -105,6 +109,7 @@ class BackUpState {
         other.backgroundBackup == backgroundBackup &&
         other.backupRequireWifi == backupRequireWifi &&
         other.backupRequireCharging == backupRequireCharging &&
+        other.backupTriggerDelay == backupTriggerDelay &&
         collectionEquals(other.availableAlbums, availableAlbums) &&
         collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) &&
         collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) &&
@@ -126,6 +131,7 @@ class BackUpState {
         backgroundBackup.hashCode ^
         backupRequireWifi.hashCode ^
         backupRequireCharging.hashCode ^
+        backupTriggerDelay.hashCode ^
         availableAlbums.hashCode ^
         selectedBackupAlbums.hashCode ^
         excludedBackupAlbums.hashCode ^

+ 22 - 7
mobile/lib/modules/backup/providers/backup.provider.dart

@@ -38,6 +38,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
             backgroundBackup: false,
             backupRequireWifi: true,
             backupRequireCharging: false,
+            backupTriggerDelay: 5000,
             serverInfo: ServerInfoResponseDto(
               diskAvailable: "0",
               diskAvailableRaw: 0,
@@ -119,18 +120,26 @@ class BackupNotifier extends StateNotifier<BackUpState> {
     bool? enabled,
     bool? requireWifi,
     bool? requireCharging,
+    int? triggerDelay,
     required void Function(String msg) onError,
     required void Function() onBatteryInfo,
   }) async {
-    assert(enabled != null || requireWifi != null || requireCharging != null);
+    assert(
+      enabled != null ||
+          requireWifi != null ||
+          requireCharging != null ||
+          triggerDelay != null,
+    );
     if (Platform.isAndroid) {
       final bool wasEnabled = state.backgroundBackup;
       final bool wasWifi = state.backupRequireWifi;
-      final bool wasCharing = state.backupRequireCharging;
+      final bool wasCharging = state.backupRequireCharging;
+      final int oldTriggerDelay = state.backupTriggerDelay;
       state = state.copyWith(
         backgroundBackup: enabled,
         backupRequireWifi: requireWifi,
         backupRequireCharging: requireCharging,
+        backupTriggerDelay: triggerDelay,
       );
 
       if (state.backgroundBackup) {
@@ -145,17 +154,22 @@ class BackupNotifier extends StateNotifier<BackUpState> {
             await _backgroundService.configureService(
               requireUnmetered: state.backupRequireWifi,
               requireCharging: state.backupRequireCharging,
+              triggerUpdateDelay: state.backupTriggerDelay,
+              triggerMaxDelay: state.backupTriggerDelay * 10,
             );
         if (success) {
-          await Hive.box(backgroundBackupInfoBox)
-              .put(backupRequireWifi, state.backupRequireWifi);
-          await Hive.box(backgroundBackupInfoBox)
-              .put(backupRequireCharging, state.backupRequireCharging);
+          final box = Hive.box(backgroundBackupInfoBox);
+          await Future.wait([
+            box.put(backupRequireWifi, state.backupRequireWifi),
+            box.put(backupRequireCharging, state.backupRequireCharging),
+            box.put(backupTriggerDelay, state.backupTriggerDelay),
+          ]);
         } else {
           state = state.copyWith(
             backgroundBackup: wasEnabled,
             backupRequireWifi: wasWifi,
-            backupRequireCharging: wasCharing,
+            backupRequireCharging: wasCharging,
+            backupTriggerDelay: oldTriggerDelay,
           );
           onError("backup_controller_page_background_configure_error");
         }
@@ -602,6 +616,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
         excludedBackupAlbums: excludedAlbums,
         backupRequireWifi: backgroundBox.get(backupRequireWifi),
         backupRequireCharging: backgroundBox.get(backupRequireCharging),
+        backupTriggerDelay: backgroundBox.get(backupTriggerDelay),
       );
     }
     return _resumeBackup();

+ 69 - 0
mobile/lib/modules/backup/views/backup_controller_page.dart

@@ -198,6 +198,46 @@ class BackupControllerPage extends HookConsumerWidget {
       final bool isWifiRequired = backupState.backupRequireWifi;
       final bool isChargingRequired = backupState.backupRequireCharging;
       final Color activeColor = Theme.of(context).primaryColor;
+
+      String formatBackupDelaySliderValue(double v) {
+        if (v == 0.0) {
+          return 'setting_notifications_notify_seconds'.tr(args: const ['5']);
+        } else if (v == 1.0) {
+          return 'setting_notifications_notify_seconds'.tr(args: const ['30']);
+        } else if (v == 2.0) {
+          return 'setting_notifications_notify_minutes'.tr(args: const ['2']);
+        } else {
+          return 'setting_notifications_notify_minutes'.tr(args: const ['10']);
+        }
+      }
+
+      int backupDelayToMilliseconds(double v) {
+        if (v == 0.0) {
+          return 5000;
+        } else if (v == 1.0) {
+          return 30000;
+        } else if (v == 2.0) {
+          return 120000;
+        } else {
+          return 600000;
+        }
+      }
+
+      double backupDelayToSliderValue(int ms) {
+        if (ms == 5000) {
+          return 0.0;
+        } else if (ms == 30000) {
+          return 1.0;
+        } else if (ms == 120000) {
+          return 2.0;
+        } else {
+          return 3.0;
+        }
+      }
+
+      final triggerDelay =
+          useState(backupDelayToSliderValue(backupState.backupTriggerDelay));
+
       return ListTile(
         isThreeLine: true,
         leading: isBackgroundEnabled
@@ -264,6 +304,35 @@ class BackupControllerPage extends HookConsumerWidget {
                         )
                     : null,
               ),
+            if (isBackgroundEnabled)
+              ListTile(
+                isThreeLine: false,
+                dense: true,
+                enabled: hasExclusiveAccess,
+                title: const Text(
+                  'backup_controller_page_background_delay',
+                  style: TextStyle(
+                    fontWeight: FontWeight.bold,
+                  ),
+                ).tr(args: [formatBackupDelaySliderValue(triggerDelay.value)]),
+                subtitle: Slider(
+                  value: triggerDelay.value,
+                  onChanged: hasExclusiveAccess
+                      ? (double v) => triggerDelay.value = v
+                      : null,
+                  onChangeEnd: (double v) => ref
+                      .read(backupProvider.notifier)
+                      .configureBackgroundBackup(
+                        triggerDelay: backupDelayToMilliseconds(v),
+                        onError: showErrorToUser,
+                        onBatteryInfo: showBatteryOptimizationInfoToUser,
+                      ),
+                  max: 3.0,
+                  divisions: 3,
+                  label: formatBackupDelaySliderValue(triggerDelay.value),
+                  activeColor: Theme.of(context).primaryColor,
+                ),
+              ),
             ElevatedButton(
               onPressed: () =>
                   ref.read(backupProvider.notifier).configureBackgroundBackup(