1
0
mirror of https://github.com/immich-app/immich.git synced 2025-05-17 23:02:56 +02:00

feat(mobile): configurable background backup delay ()

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
mobile
android/app/src/main/kotlin/com/example/mobile
assets/i18n
lib

@ -54,7 +54,9 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
val args = call.arguments<ArrayList<*>>()!! val args = call.arguments<ArrayList<*>>()!!
val requireUnmeteredNetwork = args.get(0) as Boolean val requireUnmeteredNetwork = args.get(0) as Boolean
val requireCharging = args.get(1) 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) result.success(true)
} }
"disable" -> { "disable" -> {

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

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

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

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

@ -18,6 +18,7 @@ class BackUpState {
final bool backgroundBackup; final bool backgroundBackup;
final bool backupRequireWifi; final bool backupRequireWifi;
final bool backupRequireCharging; final bool backupRequireCharging;
final int backupTriggerDelay;
/// All available albums on the device /// All available albums on the device
final List<AvailableAlbum> availableAlbums; final List<AvailableAlbum> availableAlbums;
@ -42,6 +43,7 @@ class BackUpState {
required this.backgroundBackup, required this.backgroundBackup,
required this.backupRequireWifi, required this.backupRequireWifi,
required this.backupRequireCharging, required this.backupRequireCharging,
required this.backupTriggerDelay,
required this.availableAlbums, required this.availableAlbums,
required this.selectedBackupAlbums, required this.selectedBackupAlbums,
required this.excludedBackupAlbums, required this.excludedBackupAlbums,
@ -59,6 +61,7 @@ class BackUpState {
bool? backgroundBackup, bool? backgroundBackup,
bool? backupRequireWifi, bool? backupRequireWifi,
bool? backupRequireCharging, bool? backupRequireCharging,
int? backupTriggerDelay,
List<AvailableAlbum>? availableAlbums, List<AvailableAlbum>? availableAlbums,
Set<AvailableAlbum>? selectedBackupAlbums, Set<AvailableAlbum>? selectedBackupAlbums,
Set<AvailableAlbum>? excludedBackupAlbums, Set<AvailableAlbum>? excludedBackupAlbums,
@ -76,6 +79,7 @@ class BackUpState {
backupRequireWifi: backupRequireWifi ?? this.backupRequireWifi, backupRequireWifi: backupRequireWifi ?? this.backupRequireWifi,
backupRequireCharging: backupRequireCharging:
backupRequireCharging ?? this.backupRequireCharging, backupRequireCharging ?? this.backupRequireCharging,
backupTriggerDelay: backupTriggerDelay ?? this.backupTriggerDelay,
availableAlbums: availableAlbums ?? this.availableAlbums, availableAlbums: availableAlbums ?? this.availableAlbums,
selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums, selectedBackupAlbums: selectedBackupAlbums ?? this.selectedBackupAlbums,
excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums, excludedBackupAlbums: excludedBackupAlbums ?? this.excludedBackupAlbums,
@ -88,7 +92,7 @@ class BackUpState {
@override @override
String toString() { 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 @override
@ -105,6 +109,7 @@ class BackUpState {
other.backgroundBackup == backgroundBackup && other.backgroundBackup == backgroundBackup &&
other.backupRequireWifi == backupRequireWifi && other.backupRequireWifi == backupRequireWifi &&
other.backupRequireCharging == backupRequireCharging && other.backupRequireCharging == backupRequireCharging &&
other.backupTriggerDelay == backupTriggerDelay &&
collectionEquals(other.availableAlbums, availableAlbums) && collectionEquals(other.availableAlbums, availableAlbums) &&
collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) && collectionEquals(other.selectedBackupAlbums, selectedBackupAlbums) &&
collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) && collectionEquals(other.excludedBackupAlbums, excludedBackupAlbums) &&
@ -126,6 +131,7 @@ class BackUpState {
backgroundBackup.hashCode ^ backgroundBackup.hashCode ^
backupRequireWifi.hashCode ^ backupRequireWifi.hashCode ^
backupRequireCharging.hashCode ^ backupRequireCharging.hashCode ^
backupTriggerDelay.hashCode ^
availableAlbums.hashCode ^ availableAlbums.hashCode ^
selectedBackupAlbums.hashCode ^ selectedBackupAlbums.hashCode ^
excludedBackupAlbums.hashCode ^ excludedBackupAlbums.hashCode ^

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

@ -198,6 +198,46 @@ class BackupControllerPage extends HookConsumerWidget {
final bool isWifiRequired = backupState.backupRequireWifi; final bool isWifiRequired = backupState.backupRequireWifi;
final bool isChargingRequired = backupState.backupRequireCharging; final bool isChargingRequired = backupState.backupRequireCharging;
final Color activeColor = Theme.of(context).primaryColor; 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( return ListTile(
isThreeLine: true, isThreeLine: true,
leading: isBackgroundEnabled leading: isBackgroundEnabled
@ -264,6 +304,35 @@ class BackupControllerPage extends HookConsumerWidget {
) )
: null, : 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( ElevatedButton(
onPressed: () => onPressed: () =>
ref.read(backupProvider.notifier).configureBackgroundBackup( ref.read(backupProvider.notifier).configureBackgroundBackup(