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
This commit is contained in:
Fynn Petersen-Frey 2022-12-08 16:51:36 +01:00 committed by GitHub
parent a97b761eda
commit c23b2479f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 122 additions and 12 deletions

View File

@ -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" -> {

View File

@ -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)

View File

@ -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",

View File

@ -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

View File

@ -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) {

View File

@ -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 ^

View File

@ -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();

View File

@ -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(