fix(mobile): manual asset upload - app state handling + cancel button (#3611)

* feat(mobile): Cancel manual asset upload

* fix(mobile): re-add the missing translation keys

* feat(mobile): show manual upload error in backup page

* refactor: manual upload in-progress count

* fix(mobile): handle app state properly during manual asset upload
This commit is contained in:
shalong-tanwen
2023-08-12 21:02:58 +00:00
committed by GitHub
parent b790354f9a
commit 77a5820c3c
8 changed files with 318 additions and 201 deletions

View File

@@ -4,46 +4,51 @@ import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.d
class ManualUploadState {
final CancellationToken cancelToken;
final double progressInPercentage;
// Current Backup Asset
final CurrentUploadAsset currentUploadAsset;
final int currentAssetIndex;
/// Manual Upload
final int manualUploadsTotal;
final int manualUploadFailures;
final int manualUploadSuccess;
final bool showDetailedNotification;
/// Manual Upload Stats
final int totalAssetsToUpload;
final int successfulUploads;
final double progressInPercentage;
const ManualUploadState({
required this.progressInPercentage,
required this.cancelToken,
required this.currentUploadAsset,
required this.manualUploadsTotal,
required this.manualUploadFailures,
required this.manualUploadSuccess,
required this.totalAssetsToUpload,
required this.currentAssetIndex,
required this.successfulUploads,
required this.showDetailedNotification,
});
ManualUploadState copyWith({
double? progressInPercentage,
CancellationToken? cancelToken,
CurrentUploadAsset? currentUploadAsset,
int? manualUploadsTotal,
int? manualUploadFailures,
int? manualUploadSuccess,
int? totalAssetsToUpload,
int? successfulUploads,
int? currentAssetIndex,
bool? showDetailedNotification,
}) {
return ManualUploadState(
progressInPercentage: progressInPercentage ?? this.progressInPercentage,
cancelToken: cancelToken ?? this.cancelToken,
currentUploadAsset: currentUploadAsset ?? this.currentUploadAsset,
manualUploadsTotal: manualUploadsTotal ?? this.manualUploadsTotal,
manualUploadFailures: manualUploadFailures ?? this.manualUploadFailures,
manualUploadSuccess: manualUploadSuccess ?? this.manualUploadSuccess,
totalAssetsToUpload: totalAssetsToUpload ?? this.totalAssetsToUpload,
currentAssetIndex: currentAssetIndex ?? this.currentAssetIndex,
successfulUploads: successfulUploads ?? this.successfulUploads,
showDetailedNotification:
showDetailedNotification ?? this.showDetailedNotification,
);
}
@override
String toString() {
return 'ManualUploadState(progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, currentUploadAsset: $currentUploadAsset, manualUploadsTotal: $manualUploadsTotal, manualUploadSuccess: $manualUploadSuccess, manualUploadFailures: $manualUploadFailures)';
return 'ManualUploadState(progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, currentUploadAsset: $currentUploadAsset, totalAssetsToUpload: $totalAssetsToUpload, successfulUploads: $successfulUploads, currentAssetIndex: $currentAssetIndex, showDetailedNotification: $showDetailedNotification)';
}
@override
@@ -54,9 +59,10 @@ class ManualUploadState {
other.progressInPercentage == progressInPercentage &&
other.cancelToken == cancelToken &&
other.currentUploadAsset == currentUploadAsset &&
other.manualUploadsTotal == manualUploadsTotal &&
other.manualUploadFailures == manualUploadFailures &&
other.manualUploadSuccess == manualUploadSuccess;
other.totalAssetsToUpload == totalAssetsToUpload &&
other.currentAssetIndex == currentAssetIndex &&
other.successfulUploads == successfulUploads &&
other.showDetailedNotification == showDetailedNotification;
}
@override
@@ -64,8 +70,9 @@ class ManualUploadState {
return progressInPercentage.hashCode ^
cancelToken.hashCode ^
currentUploadAsset.hashCode ^
manualUploadsTotal.hashCode ^
manualUploadFailures.hashCode ^
manualUploadSuccess.hashCode;
totalAssetsToUpload.hashCode ^
currentAssetIndex.hashCode ^
successfulUploads.hashCode ^
showDetailedNotification.hashCode;
}
}

View File

@@ -9,14 +9,17 @@ 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/manual_upload_state.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/app_state.provider.dart';
import 'package:immich_mobile/shared/services/local_notification.service.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/utils/backup_progress.dart';
import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart';
@@ -24,24 +27,19 @@ final manualUploadProvider =
StateNotifierProvider<ManualUploadNotifier, ManualUploadState>((ref) {
return ManualUploadNotifier(
ref.watch(localNotificationService),
ref.watch(backgroundServiceProvider),
ref.watch(backupServiceProvider),
ref.watch(backupProvider.notifier),
ref,
);
});
class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
final Logger _log = Logger("ManualUploadNotifier");
final LocalNotificationService _localNotificationService;
final BackgroundService _backgroundService;
final BackupService _backupService;
final BackupNotifier _backupProvider;
final Ref ref;
ManualUploadNotifier(
this._localNotificationService,
this._backgroundService,
this._backupService,
this._backupProvider,
this.ref,
) : super(
@@ -54,15 +52,13 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
fileName: '...',
fileType: '...',
),
manualUploadsTotal: 0,
manualUploadSuccess: 0,
manualUploadFailures: 0,
totalAssetsToUpload: 0,
successfulUploads: 0,
currentAssetIndex: 0,
showDetailedNotification: false,
),
);
int get _uploadedAssetsCount =>
state.manualUploadSuccess + state.manualUploadFailures;
String _lastPrintedDetailContent = '';
String? _lastPrintedDetailTitle;
@@ -78,11 +74,12 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
_localNotificationService.showOrUpdateManualUploadStatus(
"backup_background_service_in_progress_notification".tr(),
formatAssetBackupProgress(
_uploadedAssetsCount,
state.manualUploadsTotal,
state.currentAssetIndex,
state.totalAssetsToUpload,
),
maxProgress: state.manualUploadsTotal,
progress: _uploadedAssetsCount,
maxProgress: state.totalAssetsToUpload,
progress: state.currentAssetIndex,
showActions: true,
);
}
}
@@ -103,44 +100,52 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
progress: total > 0 ? (progress * 1000) ~/ total : 0,
maxProgress: 1000,
isDetailed: true,
// Detailed noitifcation is displayed for Single asset uploads. Show actions for such case
showActions: state.totalAssetsToUpload == 1,
);
}
}
}
void _onManualAssetUploaded(
void _onAssetUploaded(
String deviceAssetId,
String deviceId,
bool isDuplicated,
) {
state = state.copyWith(manualUploadSuccess: state.manualUploadSuccess + 1);
state = state.copyWith(successfulUploads: state.successfulUploads + 1);
_backupProvider.updateServerInfo();
if (state.manualUploadsTotal > 1) {
_throttledNotifiy();
}
}
void _onManualBackupError(ErrorUploadAsset errorAssetInfo) {
state =
state.copyWith(manualUploadFailures: state.manualUploadFailures + 1);
if (state.manualUploadsTotal > 1) {
_throttledNotifiy();
}
void _onAssetUploadError(ErrorUploadAsset errorAssetInfo) {
ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo);
}
void _onProgress(int sent, int total) {
final title = "backup_background_service_current_upload_notification"
.tr(args: [state.currentUploadAsset.fileName]);
_throttledDetailNotify(title: title, progress: sent, total: total);
state = state.copyWith(
progressInPercentage: (sent.toDouble() / total.toDouble() * 100),
);
if (state.showDetailedNotification) {
final title = "backup_background_service_current_upload_notification"
.tr(args: [state.currentUploadAsset.fileName]);
_throttledDetailNotify(title: title, progress: sent, total: total);
}
}
void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
state = state.copyWith(currentUploadAsset: currentUploadAsset);
_throttledDetailNotify.title =
"backup_background_service_current_upload_notification"
.tr(args: [currentUploadAsset.fileName]);
_throttledDetailNotify.progress = 0;
_throttledDetailNotify.total = 0;
state = state.copyWith(
currentUploadAsset: currentUploadAsset,
currentAssetIndex: state.currentAssetIndex + 1,
);
if (state.totalAssetsToUpload > 1) {
_throttledNotifiy();
}
if (state.showDetailedNotification) {
_throttledDetailNotify.title =
"backup_background_service_current_upload_notification"
.tr(args: [currentUploadAsset.fileName]);
_throttledDetailNotify.progress = 0;
_throttledDetailNotify.total = 0;
}
}
Future<bool> _startUpload(Iterable<Asset> allManualUploads) async {
@@ -161,11 +166,11 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
return false;
}
// Reset state
state = state.copyWith(
manualUploadsTotal: allManualUploads.length,
manualUploadSuccess: 0,
manualUploadFailures: 0,
progressInPercentage: 0,
totalAssetsToUpload: allUploadAssets.length,
successfulUploads: 0,
currentAssetIndex: 0,
currentUploadAsset: CurrentUploadAsset(
id: '...',
fileCreatedAt: DateTime.parse('2020-10-04'),
@@ -174,8 +179,10 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
),
cancelToken: CancellationToken(),
);
// Reset Error List
ref.watch(errorBackupListProvider.notifier).empty();
if (state.manualUploadsTotal > 1) {
if (state.totalAssetsToUpload > 1) {
_throttledNotifiy();
}
@@ -184,25 +191,38 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
ref.read(appSettingsServiceProvider).getSetting<bool>(
AppSettingsEnum.backgroundBackupSingleProgress,
) ||
state.manualUploadsTotal == 1;
state.totalAssetsToUpload == 1;
state =
state.copyWith(showDetailedNotification: showDetailedNotification);
final bool ok = await _backupService.backupAsset(
allUploadAssets,
state.cancelToken,
_onManualAssetUploaded,
showDetailedNotification ? _onProgress : (sent, total) {},
showDetailedNotification ? _onSetCurrentBackupAsset : (asset) {},
_onManualBackupError,
);
final bool ok = await ref.read(backupServiceProvider).backupAsset(
allUploadAssets,
state.cancelToken,
_onAssetUploaded,
_onProgress,
_onSetCurrentBackupAsset,
_onAssetUploadError,
);
// Close detailed notification
await _localNotificationService.closeNotification(
LocalNotificationService.manualUploadDetailedNotificationID,
);
_log.info(
'[_startUpload] Manual Upload Completed - success: ${state.successfulUploads},'
' failed: ${state.totalAssetsToUpload - state.successfulUploads}',
);
bool hasErrors = false;
if ((state.manualUploadFailures != 0 &&
state.manualUploadSuccess == 0) ||
// User cancelled upload
if (!ok && state.cancelToken.isCancelled) {
await _localNotificationService.showOrUpdateManualUploadStatus(
"backup_manual_title".tr(),
"backup_manual_cancelled".tr(),
presentBanner: true,
);
hasErrors = true;
} else if (state.successfulUploads == 0 ||
(!ok && !state.cancelToken.isCancelled)) {
await _localNotificationService.showOrUpdateManualUploadStatus(
"backup_manual_title".tr(),
@@ -210,7 +230,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
presentBanner: true,
);
hasErrors = true;
} else if (state.manualUploadSuccess != 0) {
} else {
await _localNotificationService.showOrUpdateManualUploadStatus(
"backup_manual_title".tr(),
"backup_manual_success".tr(),
@@ -219,6 +239,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
}
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
_handleAppInActivity();
await _backupProvider.notifyBackgroundServiceCanRun();
return !hasErrors;
} else {
@@ -228,20 +249,34 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
} catch (e) {
debugPrint("ERROR _startUpload: ${e.toString()}");
}
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
_handleAppInActivity();
await _localNotificationService.closeNotification(
LocalNotificationService.manualUploadDetailedNotificationID,
);
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
await _backupProvider.notifyBackgroundServiceCanRun();
return false;
}
void _handleAppInActivity() {
final appState = ref.read(appStateProvider.notifier).getAppState();
// The app is currently in background. Perform the necessary cleanups which
// are on-hold for upload completion
if (appState != AppStateEnum.active || appState != AppStateEnum.resumed) {
ref.read(appStateProvider.notifier).handleAppInactivity();
}
}
void cancelBackup() {
if (_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) {
if (_backupProvider.backupProgress != BackUpProgressEnum.inProgress &&
_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) {
_backupProvider.notifyBackgroundServiceCanRun();
}
state.cancelToken.cancel();
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
if (_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) {
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
}
state = state.copyWith(progressInPercentage: 0);
}
Future<bool> uploadAssets(
@@ -250,7 +285,8 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
) async {
// assumes the background service is currently running and
// waits until it has stopped to start the backup.
final bool hasLock = await _backgroundService.acquireLock();
final bool hasLock =
await ref.read(backgroundServiceProvider).acquireLock();
if (!hasLock) {
debugPrint("[uploadAssets] could not acquire lock, exiting");
ImmichToast.show(

View File

@@ -4,8 +4,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:photo_manager/photo_manager.dart';
@@ -13,8 +15,14 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
const CurrentUploadingAssetInfoBox({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
var asset = ref.watch(backupProvider).currentUploadAsset;
var uploadProgress = ref.watch(backupProvider).progressInPercentage;
var isManualUpload = ref.watch(backupProvider).backupProgress ==
BackUpProgressEnum.manualInProgress;
var asset = !isManualUpload
? ref.watch(backupProvider).currentUploadAsset
: ref.watch(manualUploadProvider).currentUploadAsset;
var uploadProgress = !isManualUpload
? ref.watch(backupProvider).progressInPercentage
: ref.watch(manualUploadProvider).progressInPercentage;
final isShowThumbnail = useState(false);
String getAssetCreationDate() {

View File

@@ -9,6 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
import 'package:immich_mobile/modules/backup/services/backup_verification.service.dart';
import 'package:immich_mobile/modules/backup/ui/current_backup_asset_info_box.dart';
import 'package:immich_mobile/modules/backup/ui/ios_debug_info_tile.dart';
@@ -657,7 +658,9 @@ class BackupControllerPage extends HookConsumerWidget {
top: 24,
),
child: Container(
child: backupState.backupProgress == BackUpProgressEnum.inProgress
child: backupState.backupProgress == BackUpProgressEnum.inProgress ||
backupState.backupProgress ==
BackUpProgressEnum.manualInProgress
? ElevatedButton(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.grey[50],
@@ -665,7 +668,12 @@ class BackupControllerPage extends HookConsumerWidget {
// padding: const EdgeInsets.all(14),
),
onPressed: () {
ref.read(backupProvider.notifier).cancelBackup();
if (backupState.backupProgress ==
BackUpProgressEnum.manualInProgress) {
ref.read(manualUploadProvider.notifier).cancelBackup();
} else {
ref.read(backupProvider.notifier).cancelBackup();
}
},
child: const Text(
"backup_controller_page_cancel",