upload new photos in background with a service (#382)

* properly done background backup service

* new concurrency/locking management with heartbeat

fix communication erros with Kotlin plugin on start/stop service methods

better error handling for BackgroundService public methods

Add default notification message when service is running

* configurable WiFi & charging requirement for service

* use translations in background service
This commit is contained in:
Fynn Petersen-Frey 2022-08-18 16:41:59 +02:00 committed by GitHub
parent f35ebec7c6
commit 33b1410d82
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1462 additions and 79 deletions

View file

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:cancellation_token_http/http.dart';
import 'package:flutter/foundation.dart';
import 'package:hive_flutter/hive_flutter.dart';
@ -9,9 +11,11 @@ import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.d
import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
import 'package:immich_mobile/shared/services/server_info.service.dart';
import 'package:openapi/api.dart';
import 'package:photo_manager/photo_manager.dart';
@ -21,6 +25,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
this._backupService,
this._serverInfoService,
this._authState,
this._backgroundService,
this.ref,
) : super(
BackUpState(
@ -28,6 +33,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
allAssetsInDatabase: const [],
progressInPercentage: 0,
cancelToken: CancellationToken(),
backgroundBackup: false,
backupRequireWifi: true,
backupRequireCharging: false,
serverInfo: ServerInfoResponseDto(
diskAvailable: "0",
diskAvailableRaw: 0,
@ -56,6 +64,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
final BackupService _backupService;
final ServerInfoService _serverInfoService;
final AuthenticationState _authState;
final BackgroundService _backgroundService;
final Ref ref;
///
@ -66,7 +75,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// We have method to include and exclude albums
/// The total unique assets will be used for backing mechanism
///
void addAlbumForBackup(AssetPathEntity album) {
void addAlbumForBackup(AvailableAlbum album) {
if (state.excludedBackupAlbums.contains(album)) {
removeExcludedAlbumForBackup(album);
}
@ -76,7 +85,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
_updateBackupAssetCount();
}
void addExcludedAlbumForBackup(AssetPathEntity album) {
void addExcludedAlbumForBackup(AvailableAlbum album) {
if (state.selectedBackupAlbums.contains(album)) {
removeAlbumForBackup(album);
}
@ -85,8 +94,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
_updateBackupAssetCount();
}
void removeAlbumForBackup(AssetPathEntity album) {
Set<AssetPathEntity> currentSelectedAlbums = state.selectedBackupAlbums;
void removeAlbumForBackup(AvailableAlbum album) {
Set<AvailableAlbum> currentSelectedAlbums = state.selectedBackupAlbums;
currentSelectedAlbums.removeWhere((a) => a == album);
@ -94,8 +103,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
_updateBackupAssetCount();
}
void removeExcludedAlbumForBackup(AssetPathEntity album) {
Set<AssetPathEntity> currentExcludedAlbums = state.excludedBackupAlbums;
void removeExcludedAlbumForBackup(AvailableAlbum album) {
Set<AvailableAlbum> currentExcludedAlbums = state.excludedBackupAlbums;
currentExcludedAlbums.removeWhere((a) => a == album);
@ -103,6 +112,50 @@ class BackupNotifier extends StateNotifier<BackUpState> {
_updateBackupAssetCount();
}
void configureBackgroundBackup({
bool? enabled,
bool? requireWifi,
bool? requireCharging,
required void Function(String msg) onError,
}) async {
assert(enabled != null || requireWifi != null || requireCharging != null);
if (Platform.isAndroid) {
final bool wasEnabled = state.backgroundBackup;
final bool wasWifi = state.backupRequireWifi;
final bool wasCharing = state.backupRequireCharging;
state = state.copyWith(
backgroundBackup: enabled,
backupRequireWifi: requireWifi,
backupRequireCharging: requireCharging,
);
if (state.backgroundBackup) {
if (!wasEnabled) {
await _backgroundService.disableBatteryOptimizations();
}
final bool success = await _backgroundService.stopService() &&
await _backgroundService.startService(
requireUnmetered: state.backupRequireWifi,
requireCharging: state.backupRequireCharging,
);
if (!success) {
state = state.copyWith(
backgroundBackup: wasEnabled,
backupRequireWifi: wasWifi,
backupRequireCharging: wasCharing,
);
onError("backup_controller_page_background_configure_error");
}
} else {
final bool success = await _backgroundService.stopService();
if (!success) {
state = state.copyWith(backgroundBackup: wasEnabled);
onError("backup_controller_page_background_configure_error");
}
}
}
}
///
/// Get all album on the device
/// Get all selected and excluded album from the user's persistent storage
@ -144,6 +197,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
defaultValue: HiveBackupAlbums(
selectedAlbumIds: [],
excludedAlbumsIds: [],
lastSelectedBackupTime: [],
lastExcludedBackupTime: [],
),
);
@ -173,6 +228,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
HiveBackupAlbums(
selectedAlbumIds: [albumHasAllAssets.id],
excludedAlbumsIds: [],
lastSelectedBackupTime: [
DateTime.fromMillisecondsSinceEpoch(0, isUtc: true)
],
lastExcludedBackupTime: [],
),
);
@ -181,19 +240,37 @@ class BackupNotifier extends StateNotifier<BackUpState> {
// Generate AssetPathEntity from id to add to local state
try {
for (var selectedAlbumId in backupAlbumInfo!.selectedAlbumIds) {
var albumAsset = await AssetPathEntity.fromId(selectedAlbumId);
state = state.copyWith(
selectedBackupAlbums: {...state.selectedBackupAlbums, albumAsset},
Set<AvailableAlbum> selectedAlbums = {};
for (var i = 0; i < backupAlbumInfo!.selectedAlbumIds.length; i++) {
var albumAsset =
await AssetPathEntity.fromId(backupAlbumInfo.selectedAlbumIds[i]);
selectedAlbums.add(
AvailableAlbum(
albumEntity: albumAsset,
lastBackup: backupAlbumInfo.lastSelectedBackupTime.length > i
? backupAlbumInfo.lastSelectedBackupTime[i]
: DateTime.fromMillisecondsSinceEpoch(0, isUtc: true),
),
);
}
for (var excludedAlbumId in backupAlbumInfo.excludedAlbumsIds) {
var albumAsset = await AssetPathEntity.fromId(excludedAlbumId);
state = state.copyWith(
excludedBackupAlbums: {...state.excludedBackupAlbums, albumAsset},
Set<AvailableAlbum> excludedAlbums = {};
for (var i = 0; i < backupAlbumInfo.excludedAlbumsIds.length; i++) {
var albumAsset =
await AssetPathEntity.fromId(backupAlbumInfo.excludedAlbumsIds[i]);
excludedAlbums.add(
AvailableAlbum(
albumEntity: albumAsset,
lastBackup: backupAlbumInfo.lastExcludedBackupTime.length > i
? backupAlbumInfo.lastExcludedBackupTime[i]
: DateTime.fromMillisecondsSinceEpoch(0, isUtc: true),
),
);
}
state = state.copyWith(
selectedBackupAlbums: selectedAlbums,
excludedBackupAlbums: excludedAlbums,
);
} catch (e) {
debugPrint("[ERROR] Failed to generate album from id $e");
}
@ -209,14 +286,14 @@ class BackupNotifier extends StateNotifier<BackUpState> {
Set<AssetEntity> assetsFromExcludedAlbums = {};
for (var album in state.selectedBackupAlbums) {
var assets =
await album.getAssetListRange(start: 0, end: album.assetCount);
var assets = await album.albumEntity
.getAssetListRange(start: 0, end: album.assetCount);
assetsFromSelectedAlbums.addAll(assets);
}
for (var album in state.excludedBackupAlbums) {
var assets =
await album.getAssetListRange(start: 0, end: album.assetCount);
var assets = await album.albumEntity
.getAssetListRange(start: 0, end: album.assetCount);
assetsFromExcludedAlbums.addAll(assets);
}
@ -263,12 +340,16 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// and then update the UI according to those information
///
Future<void> getBackupInfo() async {
await Future.wait([
_getBackupAlbumsInfo(),
_updateServerInfo(),
]);
final bool isEnabled = await _backgroundService.isBackgroundBackupEnabled();
state = state.copyWith(backgroundBackup: isEnabled);
if (state.backupProgress != BackUpProgressEnum.inBackground) {
await Future.wait([
_getBackupAlbumsInfo(),
_updateServerInfo(),
]);
await _updateBackupAssetCount();
await _updateBackupAssetCount();
}
}
///
@ -276,6 +357,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// Hive database
///
void _updatePersistentAlbumsSelection() {
final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
Box<HiveBackupAlbums> backupAlbumInfoBox =
Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
backupAlbumInfoBox.put(
@ -283,6 +365,12 @@ class BackupNotifier extends StateNotifier<BackUpState> {
HiveBackupAlbums(
selectedAlbumIds: state.selectedBackupAlbums.map((e) => e.id).toList(),
excludedAlbumsIds: state.excludedBackupAlbums.map((e) => e.id).toList(),
lastSelectedBackupTime: state.selectedBackupAlbums
.map((e) => e.lastBackup ?? epoch)
.toList(),
lastExcludedBackupTime: state.excludedBackupAlbums
.map((e) => e.lastBackup ?? epoch)
.toList(),
),
);
}
@ -290,7 +378,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
///
/// Invoke backup process
///
void startBackupProcess() async {
Future<void> startBackupProcess() async {
assert(state.backupProgress == BackUpProgressEnum.idle);
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
await getBackupInfo();
@ -318,7 +407,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
// Perform Backup
state = state.copyWith(cancelToken: CancellationToken());
_backupService.backupAsset(
await _backupService.backupAsset(
assetsWillBeBackup,
state.cancelToken,
_onAssetUploaded,
@ -326,6 +415,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
_onSetCurrentBackupAsset,
_onBackupError,
);
await _notifyBackgroundServiceCanRun();
} else {
PhotoManager.openSetting();
}
@ -340,6 +430,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
}
void cancelBackup() {
if (state.backupProgress != BackUpProgressEnum.inProgress) {
_notifyBackgroundServiceCanRun();
}
state.cancelToken.cancel();
state = state.copyWith(
backupProgress: BackUpProgressEnum.idle,
@ -359,10 +452,21 @@ class BackupNotifier extends StateNotifier<BackUpState> {
if (state.allUniqueAssets.length -
state.selectedAlbumsBackupAssetsIds.length ==
0) {
final latestAssetBackup =
state.allUniqueAssets.map((e) => e.modifiedDateTime).reduce(
(v, e) => e.isAfter(v) ? e : v,
);
state = state.copyWith(
selectedBackupAlbums: state.selectedBackupAlbums
.map((e) => e.copyWith(lastBackup: latestAssetBackup))
.toSet(),
excludedBackupAlbums: state.excludedBackupAlbums
.map((e) => e.copyWith(lastBackup: latestAssetBackup))
.toSet(),
backupProgress: BackUpProgressEnum.done,
progressInPercentage: 0.0,
);
_updatePersistentAlbumsSelection();
}
_updateServerInfo();
@ -385,7 +489,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
}
}
void resumeBackup() {
Future<void> _resumeBackup() async {
// Check if user is login
var accessKey = Hive.box(userInfoBox).get(accessTokenKey);
@ -404,13 +508,91 @@ class BackupNotifier extends StateNotifier<BackUpState> {
return;
}
if (state.backupProgress == BackUpProgressEnum.inBackground) {
debugPrint("[resumeBackup] Background backup is running - abort");
return;
}
// Run backup
debugPrint("[resumeBackup] Start back up");
startBackupProcess();
await startBackupProcess();
}
return;
}
Future<void> resumeBackup() async {
if (Platform.isAndroid) {
// assumes the background service is currently running
// if true, waits until it has stopped to update the app state from HiveDB
// before actually resuming backup by calling the internal `_resumeBackup`
final BackUpProgressEnum previous = state.backupProgress;
state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
final bool hasLock = await _backgroundService.acquireLock();
if (!hasLock) {
return;
}
Box<HiveBackupAlbums> box =
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
HiveBackupAlbums? albums = box.get(backupInfoKey);
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
if (albums != null) {
selectedAlbums = _updateAlbumsBackupTime(
selectedAlbums,
albums.selectedAlbumIds,
albums.lastSelectedBackupTime,
);
excludedAlbums = _updateAlbumsBackupTime(
excludedAlbums,
albums.excludedAlbumsIds,
albums.lastExcludedBackupTime,
);
}
state = state.copyWith(
backupProgress: previous,
selectedBackupAlbums: selectedAlbums,
excludedBackupAlbums: excludedAlbums,
);
}
return _resumeBackup();
}
Set<AvailableAlbum> _updateAlbumsBackupTime(
Set<AvailableAlbum> albums,
List<String> ids,
List<DateTime> times,
) {
Set<AvailableAlbum> result = {};
for (int i = 0; i < ids.length; i++) {
try {
AvailableAlbum a = albums.firstWhere((e) => e.id == ids[i]);
result.add(a.copyWith(lastBackup: times[i]));
} on StateError {
debugPrint("[_updateAlbumBackupTime] failed to find album in state");
}
}
return result;
}
Future<void> _notifyBackgroundServiceCanRun() async {
const allowedStates = [
AppStateEnum.inactive,
AppStateEnum.paused,
AppStateEnum.detached,
];
if (Platform.isAndroid &&
allowedStates.contains(ref.read(appStateProvider.notifier).state)) {
try {
if (Hive.isBoxOpen(hiveBackupInfoBox)) {
await Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).close();
}
} catch (error) {
debugPrint("[_notifyBackgroundServiceCanRun] failed to close box");
}
_backgroundService.releaseLock();
}
}
}
final backupProvider =
@ -419,6 +601,7 @@ final backupProvider =
ref.watch(backupServiceProvider),
ref.watch(serverInfoServiceProvider),
ref.watch(authenticationProvider),
ref.watch(backgroundServiceProvider),
ref,
);
});