|
@@ -27,11 +27,11 @@ final backgroundServiceProvider = Provider(
|
|
|
/// Background backup service
|
|
|
class BackgroundService {
|
|
|
static const String _portNameLock = "immichLock";
|
|
|
- BackgroundService();
|
|
|
static const MethodChannel _foregroundChannel =
|
|
|
MethodChannel('immich/foregroundChannel');
|
|
|
static const MethodChannel _backgroundChannel =
|
|
|
MethodChannel('immich/backgroundChannel');
|
|
|
+ static final NumberFormat numberFormat = NumberFormat("###0.##");
|
|
|
bool _isBackgroundInitialized = false;
|
|
|
CancellationToken? _cancellationToken;
|
|
|
bool _canceledBySystem = false;
|
|
@@ -40,6 +40,10 @@ class BackgroundService {
|
|
|
SendPort? _waitingIsolate;
|
|
|
ReceivePort? _rp;
|
|
|
bool _errorGracePeriodExceeded = true;
|
|
|
+ int _uploadedAssetsCount = 0;
|
|
|
+ int _assetsToUploadCount = 0;
|
|
|
+ int _lastDetailProgressUpdate = 0;
|
|
|
+ String _lastPrintedProgress = "";
|
|
|
|
|
|
bool get isBackgroundInitialized {
|
|
|
return _isBackgroundInitialized;
|
|
@@ -125,22 +129,29 @@ class BackgroundService {
|
|
|
}
|
|
|
|
|
|
/// Updates the notification shown by the background service
|
|
|
- Future<bool> _updateNotification({
|
|
|
- required String title,
|
|
|
+ Future<bool?> _updateNotification({
|
|
|
+ String? title,
|
|
|
String? content,
|
|
|
+ int progress = 0,
|
|
|
+ int max = 0,
|
|
|
+ bool indeterminate = false,
|
|
|
+ bool isDetail = false,
|
|
|
+ bool onlyIfFG = false,
|
|
|
}) async {
|
|
|
if (!Platform.isAndroid) {
|
|
|
return true;
|
|
|
}
|
|
|
try {
|
|
|
if (_isBackgroundInitialized) {
|
|
|
- return await _backgroundChannel
|
|
|
- .invokeMethod('updateNotification', [title, content]);
|
|
|
+ return _backgroundChannel.invokeMethod<bool>(
|
|
|
+ 'updateNotification',
|
|
|
+ [title, content, progress, max, indeterminate, isDetail, onlyIfFG],
|
|
|
+ );
|
|
|
}
|
|
|
} catch (error) {
|
|
|
debugPrint("[_updateNotification] failed to communicate with plugin");
|
|
|
}
|
|
|
- return Future.value(false);
|
|
|
+ return false;
|
|
|
}
|
|
|
|
|
|
/// Shows a new priority notification
|
|
@@ -274,6 +285,7 @@ class BackgroundService {
|
|
|
case "onAssetsChanged":
|
|
|
final Future<bool> translationsLoaded = loadTranslations();
|
|
|
try {
|
|
|
+ _clearErrorNotifications();
|
|
|
final bool hasAccess = await acquireLock();
|
|
|
if (!hasAccess) {
|
|
|
debugPrint("[_callHandler] could not acquire lock, exiting");
|
|
@@ -313,19 +325,23 @@ class BackgroundService {
|
|
|
apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey));
|
|
|
apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey));
|
|
|
BackupService backupService = BackupService(apiService);
|
|
|
+ AppSettingsService settingsService = AppSettingsService();
|
|
|
|
|
|
final Box<HiveBackupAlbums> box =
|
|
|
await Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox);
|
|
|
final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey);
|
|
|
if (backupAlbumInfo == null) {
|
|
|
- _clearErrorNotifications();
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
await PhotoManager.setIgnorePermissionCheck(true);
|
|
|
|
|
|
do {
|
|
|
- final bool backupOk = await _runBackup(backupService, backupAlbumInfo);
|
|
|
+ final bool backupOk = await _runBackup(
|
|
|
+ backupService,
|
|
|
+ settingsService,
|
|
|
+ backupAlbumInfo,
|
|
|
+ );
|
|
|
if (backupOk) {
|
|
|
await Hive.box(backgroundBackupInfoBox).delete(backupFailedSince);
|
|
|
await box.put(
|
|
@@ -346,9 +362,14 @@ class BackgroundService {
|
|
|
|
|
|
Future<bool> _runBackup(
|
|
|
BackupService backupService,
|
|
|
+ AppSettingsService settingsService,
|
|
|
HiveBackupAlbums backupAlbumInfo,
|
|
|
) async {
|
|
|
- _errorGracePeriodExceeded = _isErrorGracePeriodExceeded();
|
|
|
+ _errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService);
|
|
|
+ final bool notifyTotalProgress = settingsService
|
|
|
+ .getSetting<bool>(AppSettingsEnum.backgroundBackupTotalProgress);
|
|
|
+ final bool notifySingleProgress = settingsService
|
|
|
+ .getSetting<bool>(AppSettingsEnum.backgroundBackupSingleProgress);
|
|
|
|
|
|
if (_canceledBySystem) {
|
|
|
return false;
|
|
@@ -372,22 +393,29 @@ class BackgroundService {
|
|
|
}
|
|
|
|
|
|
if (toUpload.isEmpty) {
|
|
|
- _clearErrorNotifications();
|
|
|
return true;
|
|
|
}
|
|
|
+ _assetsToUploadCount = toUpload.length;
|
|
|
+ _uploadedAssetsCount = 0;
|
|
|
+ _updateNotification(
|
|
|
+ title: "backup_background_service_in_progress_notification".tr(),
|
|
|
+ content: notifyTotalProgress ? _formatAssetBackupProgress() : null,
|
|
|
+ progress: 0,
|
|
|
+ max: notifyTotalProgress ? _assetsToUploadCount : 0,
|
|
|
+ indeterminate: !notifyTotalProgress,
|
|
|
+ onlyIfFG: !notifyTotalProgress,
|
|
|
+ );
|
|
|
|
|
|
_cancellationToken = CancellationToken();
|
|
|
final bool ok = await backupService.backupAsset(
|
|
|
toUpload,
|
|
|
_cancellationToken!,
|
|
|
- _onAssetUploaded,
|
|
|
- _onProgress,
|
|
|
- _onSetCurrentBackupAsset,
|
|
|
+ notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId) {},
|
|
|
+ notifySingleProgress ? _onProgress : (sent, total) {},
|
|
|
+ notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {},
|
|
|
_onBackupError,
|
|
|
);
|
|
|
- if (ok) {
|
|
|
- _clearErrorNotifications();
|
|
|
- } else {
|
|
|
+ if (!ok && !_cancellationToken!.isCancelled) {
|
|
|
_showErrorNotification(
|
|
|
title: "backup_background_service_error_title".tr(),
|
|
|
content: "backup_background_service_backup_failed_message".tr(),
|
|
@@ -396,16 +424,43 @@ class BackgroundService {
|
|
|
return ok;
|
|
|
}
|
|
|
|
|
|
+ String _formatAssetBackupProgress() {
|
|
|
+ final int percent = (_uploadedAssetsCount * 100) ~/ _assetsToUploadCount;
|
|
|
+ return "$percent% ($_uploadedAssetsCount/$_assetsToUploadCount)";
|
|
|
+ }
|
|
|
+
|
|
|
void _onAssetUploaded(String deviceAssetId, String deviceId) {
|
|
|
debugPrint("Uploaded $deviceAssetId from $deviceId");
|
|
|
+ _uploadedAssetsCount++;
|
|
|
+ _updateNotification(
|
|
|
+ progress: _uploadedAssetsCount,
|
|
|
+ max: _assetsToUploadCount,
|
|
|
+ content: _formatAssetBackupProgress(),
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
- void _onProgress(int sent, int total) {}
|
|
|
+ void _onProgress(int sent, int total) {
|
|
|
+ final int now = Timeline.now;
|
|
|
+ // limit updates to 10 per second (or Android drops important notifications)
|
|
|
+ if (now > _lastDetailProgressUpdate + 100000) {
|
|
|
+ final String msg = _humanReadableBytesProgress(sent, total);
|
|
|
+ // only update if message actually differs (to stop many useless notification updates on large assets or slow connections)
|
|
|
+ if (msg != _lastPrintedProgress) {
|
|
|
+ _lastDetailProgressUpdate = now;
|
|
|
+ _lastPrintedProgress = msg;
|
|
|
+ _updateNotification(
|
|
|
+ progress: sent,
|
|
|
+ max: total,
|
|
|
+ isDetail: true,
|
|
|
+ content: msg,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
void _onBackupError(ErrorUploadAsset errorAssetInfo) {
|
|
|
_showErrorNotification(
|
|
|
- title: "Upload failed",
|
|
|
- content: "backup_background_service_upload_failure_notification"
|
|
|
+ title: "backup_background_service_upload_failure_notification"
|
|
|
.tr(args: [errorAssetInfo.fileName]),
|
|
|
individualTag: errorAssetInfo.id,
|
|
|
);
|
|
@@ -413,14 +468,17 @@ class BackgroundService {
|
|
|
|
|
|
void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
|
|
|
_updateNotification(
|
|
|
- title: "backup_background_service_in_progress_notification".tr(),
|
|
|
- content: "backup_background_service_current_upload_notification"
|
|
|
+ title: "backup_background_service_current_upload_notification"
|
|
|
.tr(args: [currentUploadAsset.fileName]),
|
|
|
+ content: "",
|
|
|
+ isDetail: true,
|
|
|
+ progress: 0,
|
|
|
+ max: 0,
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- bool _isErrorGracePeriodExceeded() {
|
|
|
- final int value = AppSettingsService()
|
|
|
+ bool _isErrorGracePeriodExceeded(AppSettingsService appSettingsService) {
|
|
|
+ final int value = appSettingsService
|
|
|
.getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod);
|
|
|
if (value == 0) {
|
|
|
return true;
|
|
@@ -445,6 +503,26 @@ class BackgroundService {
|
|
|
assert(false, "Invalid value");
|
|
|
return true;
|
|
|
}
|
|
|
+
|
|
|
+ /// prints percentage and absolute progress in useful (kilo/mega/giga)bytes
|
|
|
+ static String _humanReadableBytesProgress(int bytes, int bytesTotal) {
|
|
|
+ String unit = "KB"; // Kilobyte
|
|
|
+ if (bytesTotal >= 0x40000000) {
|
|
|
+ unit = "GB"; // Gigabyte
|
|
|
+ bytes >>= 20;
|
|
|
+ bytesTotal >>= 20;
|
|
|
+ } else if (bytesTotal >= 0x100000) {
|
|
|
+ unit = "MB"; // Megabyte
|
|
|
+ bytes >>= 10;
|
|
|
+ bytesTotal >>= 10;
|
|
|
+ } else if (bytesTotal < 0x400) {
|
|
|
+ return "$bytes / $bytesTotal B";
|
|
|
+ }
|
|
|
+ final int percent = (bytes * 100) ~/ bytesTotal;
|
|
|
+ final String done = numberFormat.format(bytes / 1024.0);
|
|
|
+ final String total = numberFormat.format(bytesTotal / 1024.0);
|
|
|
+ return "$percent% ($done/$total$unit)";
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/// entry point called by Kotlin/Java code; needs to be a top-level function
|