mirror of
https://github.com/immich-app/immich.git
synced 2025-12-17 01:11:13 +03:00
feat: beta background sync (#21243)
* feat: ios background sync # Conflicts: # mobile/ios/Runner/Info.plist * feat: Android sync * add local sync worker and rename stuff * group upload notifications * uncomment onresume beta handling * rename methods --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
232
mobile/lib/domain/services/background_worker.service.dart
Normal file
232
mobile/lib/domain/services/background_worker.service.dart
Normal file
@@ -0,0 +1,232 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/auth.service.dart';
|
||||
import 'package:immich_mobile/services/localization.service.dart';
|
||||
import 'package:immich_mobile/services/upload.service.dart';
|
||||
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class BackgroundWorkerFgService {
|
||||
final BackgroundWorkerFgHostApi _foregroundHostApi;
|
||||
|
||||
const BackgroundWorkerFgService(this._foregroundHostApi);
|
||||
|
||||
// TODO: Move this call to native side once old timeline is removed
|
||||
Future<void> enableSyncService() => _foregroundHostApi.enableSyncWorker();
|
||||
|
||||
Future<void> enableUploadService() => _foregroundHostApi.enableUploadWorker(
|
||||
PluginUtilities.getCallbackHandle(_backgroundSyncNativeEntrypoint)!.toRawHandle(),
|
||||
);
|
||||
|
||||
Future<void> disableUploadService() => _foregroundHostApi.disableUploadWorker();
|
||||
}
|
||||
|
||||
class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
late final ProviderContainer _ref;
|
||||
final Isar _isar;
|
||||
final Drift _drift;
|
||||
final DriftLogger _driftLogger;
|
||||
final BackgroundWorkerBgHostApi _backgroundHostApi;
|
||||
final Logger _logger = Logger('BackgroundUploadBgService');
|
||||
|
||||
bool _isCleanedUp = false;
|
||||
|
||||
BackgroundWorkerBgService({required Isar isar, required Drift drift, required DriftLogger driftLogger})
|
||||
: _isar = isar,
|
||||
_drift = drift,
|
||||
_driftLogger = driftLogger,
|
||||
_backgroundHostApi = BackgroundWorkerBgHostApi() {
|
||||
_ref = ProviderContainer(
|
||||
overrides: [
|
||||
dbProvider.overrideWithValue(isar),
|
||||
isarProvider.overrideWithValue(isar),
|
||||
driftProvider.overrideWith(driftOverride(drift)),
|
||||
],
|
||||
);
|
||||
BackgroundWorkerFlutterApi.setUp(this);
|
||||
}
|
||||
|
||||
bool get _isBackupEnabled => _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||
|
||||
Future<void> init() async {
|
||||
await loadTranslations();
|
||||
HttpSSLOptions.apply(applyNative: false);
|
||||
await _ref.read(authServiceProvider).setOpenApiServiceEndpoint();
|
||||
|
||||
// Initialize the file downloader
|
||||
await FileDownloader().configure(
|
||||
globalConfig: [
|
||||
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
|
||||
(Config.holdingQueue, (6, 6, 3)),
|
||||
// On Android, if files are larger than 256MB, run in foreground service
|
||||
(Config.runInForegroundIfFileLargerThan, 256),
|
||||
],
|
||||
);
|
||||
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
|
||||
await FileDownloader().trackTasks();
|
||||
configureFileDownloaderNotifications();
|
||||
|
||||
// Notify the host that the background upload service has been initialized and is ready to use
|
||||
await _backgroundHostApi.onInitialized();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onLocalSync(int? maxSeconds) async {
|
||||
_logger.info('Local background syncing started');
|
||||
final sw = Stopwatch()..start();
|
||||
|
||||
final timeout = maxSeconds != null ? Duration(seconds: maxSeconds) : null;
|
||||
await _syncAssets(hashTimeout: timeout, syncRemote: false);
|
||||
|
||||
sw.stop();
|
||||
_logger.info("Local sync completed in ${sw.elapsed.inSeconds}s");
|
||||
}
|
||||
|
||||
/* We do the following on Android upload
|
||||
* - Sync local assets
|
||||
* - Hash local assets 3 / 6 minutes
|
||||
* - Sync remote assets
|
||||
* - Check and requeue upload tasks
|
||||
*/
|
||||
@override
|
||||
Future<void> onAndroidUpload() async {
|
||||
_logger.info('Android background processing started');
|
||||
final sw = Stopwatch()..start();
|
||||
|
||||
await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6));
|
||||
await _handleBackup(processBulk: false);
|
||||
|
||||
await _cleanup();
|
||||
|
||||
sw.stop();
|
||||
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
|
||||
}
|
||||
|
||||
/* We do the following on background upload
|
||||
* - Sync local assets
|
||||
* - Hash local assets
|
||||
* - Sync remote assets
|
||||
* - Check and requeue upload tasks
|
||||
*
|
||||
* The native side will not send the maxSeconds value for processing tasks
|
||||
*/
|
||||
@override
|
||||
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
||||
_logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s');
|
||||
final sw = Stopwatch()..start();
|
||||
|
||||
final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6);
|
||||
await _syncAssets(hashTimeout: timeout);
|
||||
|
||||
final backupFuture = _handleBackup();
|
||||
if (maxSeconds != null) {
|
||||
await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {});
|
||||
} else {
|
||||
await backupFuture;
|
||||
}
|
||||
|
||||
await _cleanup();
|
||||
|
||||
sw.stop();
|
||||
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s");
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cancel() async {
|
||||
_logger.warning("Background upload cancelled");
|
||||
await _cleanup();
|
||||
}
|
||||
|
||||
Future<void> _cleanup() async {
|
||||
if (_isCleanedUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isCleanedUp = true;
|
||||
await _ref.read(backgroundSyncProvider).cancel();
|
||||
await _ref.read(backgroundSyncProvider).cancelLocal();
|
||||
await _isar.close();
|
||||
await _drift.close();
|
||||
await _driftLogger.close();
|
||||
_ref.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleBackup({bool processBulk = true}) async {
|
||||
if (!_isBackupEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
final currentUser = _ref.read(currentUserProvider);
|
||||
if (currentUser == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (processBulk) {
|
||||
return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
||||
}
|
||||
|
||||
final activeTask = await _ref.read(uploadServiceProvider).getActiveTasks(currentUser.id);
|
||||
if (activeTask.isNotEmpty) {
|
||||
await _ref.read(uploadServiceProvider).resumeBackup();
|
||||
} else {
|
||||
await _ref.read(uploadServiceProvider).startBackupSerial(currentUser.id);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _syncAssets({Duration? hashTimeout, bool syncRemote = true}) async {
|
||||
final futures = <Future<void>>[];
|
||||
|
||||
final localSyncFuture = _ref.read(backgroundSyncProvider).syncLocal().then((_) async {
|
||||
if (_isCleanedUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hashFuture = _ref.read(backgroundSyncProvider).hashAssets();
|
||||
if (hashTimeout != null) {
|
||||
hashFuture = hashFuture.timeout(
|
||||
hashTimeout,
|
||||
onTimeout: () {
|
||||
// Consume cancellation errors as we want to continue processing
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return hashFuture;
|
||||
});
|
||||
|
||||
futures.add(localSyncFuture);
|
||||
if (syncRemote) {
|
||||
final remoteSyncFuture = _ref.read(backgroundSyncProvider).syncRemote();
|
||||
futures.add(remoteSyncFuture);
|
||||
}
|
||||
|
||||
await Future.wait(futures);
|
||||
}
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> _backgroundSyncNativeEntrypoint() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
DartPluginRegistrant.ensureInitialized();
|
||||
|
||||
final (isar, drift, logDB) = await Bootstrap.initDB();
|
||||
await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false);
|
||||
await BackgroundWorkerBgService(isar: isar, drift: drift, driftLogger: logDB).init();
|
||||
}
|
||||
@@ -15,6 +15,7 @@ class HashService {
|
||||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
final StorageRepository _storageRepository;
|
||||
final NativeSyncApi _nativeSyncApi;
|
||||
final bool Function()? _cancelChecker;
|
||||
final _log = Logger('HashService');
|
||||
|
||||
HashService({
|
||||
@@ -22,13 +23,17 @@ class HashService {
|
||||
required DriftLocalAssetRepository localAssetRepository,
|
||||
required StorageRepository storageRepository,
|
||||
required NativeSyncApi nativeSyncApi,
|
||||
bool Function()? cancelChecker,
|
||||
this.batchSizeLimit = kBatchHashSizeLimit,
|
||||
this.batchFileLimit = kBatchHashFileLimit,
|
||||
}) : _localAlbumRepository = localAlbumRepository,
|
||||
_localAssetRepository = localAssetRepository,
|
||||
_storageRepository = storageRepository,
|
||||
_cancelChecker = cancelChecker,
|
||||
_nativeSyncApi = nativeSyncApi;
|
||||
|
||||
bool get isCancelled => _cancelChecker?.call() ?? false;
|
||||
|
||||
Future<void> hashAssets() async {
|
||||
final Stopwatch stopwatch = Stopwatch()..start();
|
||||
// Sorted by backupSelection followed by isCloud
|
||||
@@ -37,6 +42,11 @@ class HashService {
|
||||
);
|
||||
|
||||
for (final album in localAlbums) {
|
||||
if (isCancelled) {
|
||||
_log.warning("Hashing cancelled. Stopped processing albums.");
|
||||
break;
|
||||
}
|
||||
|
||||
final assetsToHash = await _localAlbumRepository.getAssetsToHash(album.id);
|
||||
if (assetsToHash.isNotEmpty) {
|
||||
await _hashAssets(assetsToHash);
|
||||
@@ -55,6 +65,11 @@ class HashService {
|
||||
final toHash = <_AssetToPath>[];
|
||||
|
||||
for (final asset in assetsToHash) {
|
||||
if (isCancelled) {
|
||||
_log.warning("Hashing cancelled. Stopped processing assets.");
|
||||
return;
|
||||
}
|
||||
|
||||
final file = await _storageRepository.getFileForAsset(asset.id);
|
||||
if (file == null) {
|
||||
continue;
|
||||
@@ -89,6 +104,11 @@ class HashService {
|
||||
);
|
||||
|
||||
for (int i = 0; i < hashes.length; i++) {
|
||||
if (isCancelled) {
|
||||
_log.warning("Hashing cancelled. Stopped processing batch.");
|
||||
return;
|
||||
}
|
||||
|
||||
final hash = hashes[i];
|
||||
final asset = toHash[i].asset;
|
||||
if (hash?.length == 20) {
|
||||
|
||||
@@ -123,6 +123,11 @@ class LogService {
|
||||
_flushTimer = null;
|
||||
final buffer = [..._msgBuffer];
|
||||
_msgBuffer.clear();
|
||||
|
||||
if (buffer.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
await _logRepository.insertAll(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,28 @@ class BackgroundSyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> cancelLocal() async {
|
||||
final futures = <Future>[];
|
||||
|
||||
if (_hashTask != null) {
|
||||
futures.add(_hashTask!.future);
|
||||
}
|
||||
_hashTask?.cancel();
|
||||
_hashTask = null;
|
||||
|
||||
if (_deviceAlbumSyncTask != null) {
|
||||
futures.add(_deviceAlbumSyncTask!.future);
|
||||
}
|
||||
_deviceAlbumSyncTask?.cancel();
|
||||
_deviceAlbumSyncTask = null;
|
||||
|
||||
try {
|
||||
await Future.wait(futures);
|
||||
} on CanceledError {
|
||||
// Ignore cancellation errors
|
||||
}
|
||||
}
|
||||
|
||||
// No need to cancel the task, as it can also be run when the user logs out
|
||||
Future<void> syncLocal({bool full = false}) {
|
||||
if (_deviceAlbumSyncTask != null) {
|
||||
|
||||
@@ -12,10 +12,13 @@ import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/constants/locales.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/locale_provider.dart';
|
||||
@@ -23,6 +26,7 @@ import 'package:immich_mobile/providers/routes.provider.dart';
|
||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||
import 'package:immich_mobile/routing/app_navigation_observer.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
import 'package:immich_mobile/services/deep_link.service.dart';
|
||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||
@@ -165,36 +169,6 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
||||
await ref.read(localNotificationService).setup();
|
||||
}
|
||||
|
||||
void _configureFileDownloaderNotifications() {
|
||||
FileDownloader().configureNotificationForGroup(
|
||||
kDownloadGroupImage,
|
||||
running: TaskNotification('downloading_media'.tr(), '${'file_name'.tr()}: {filename}'),
|
||||
complete: TaskNotification('download_finished'.tr(), '${'file_name'.tr()}: {filename}'),
|
||||
progressBar: true,
|
||||
);
|
||||
|
||||
FileDownloader().configureNotificationForGroup(
|
||||
kDownloadGroupVideo,
|
||||
running: TaskNotification('downloading_media'.tr(), '${'file_name'.tr()}: {filename}'),
|
||||
complete: TaskNotification('download_finished'.tr(), '${'file_name'.tr()}: {filename}'),
|
||||
progressBar: true,
|
||||
);
|
||||
|
||||
FileDownloader().configureNotificationForGroup(
|
||||
kManualUploadGroup,
|
||||
running: TaskNotification('uploading_media'.tr(), '${'file_name'.tr()}: {displayName}'),
|
||||
complete: TaskNotification('upload_finished'.tr(), '${'file_name'.tr()}: {displayName}'),
|
||||
progressBar: true,
|
||||
);
|
||||
|
||||
FileDownloader().configureNotificationForGroup(
|
||||
kBackupGroup,
|
||||
running: TaskNotification('uploading_media'.tr(), '${'file_name'.tr()}: {displayName}'),
|
||||
complete: TaskNotification('upload_finished'.tr(), '${'file_name'.tr()}: {displayName}'),
|
||||
progressBar: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<DeepLink> _deepLinkBuilder(PlatformDeepLink deepLink) async {
|
||||
final deepLinkHandler = ref.read(deepLinkServiceProvider);
|
||||
final currentRouteName = ref.read(currentRouteNameProvider.notifier).state;
|
||||
@@ -221,7 +195,7 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
||||
super.didChangeDependencies();
|
||||
Intl.defaultLocale = context.locale.toLanguageTag();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_configureFileDownloaderNotifications();
|
||||
configureFileDownloaderNotifications();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -231,7 +205,16 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
||||
initApp().then((_) => debugPrint("App Init Completed"));
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// needs to be delayed so that EasyLocalization is working
|
||||
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||
if (Store.isBetaTimelineEnabled) {
|
||||
ref.read(driftBackgroundUploadFgService).enableSyncService();
|
||||
if (ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup)) {
|
||||
ref.read(backgroundServiceProvider).disableService();
|
||||
ref.read(driftBackgroundUploadFgService).enableUploadService();
|
||||
}
|
||||
} else {
|
||||
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||
ref.read(driftBackgroundUploadFgService).disableUploadService();
|
||||
}
|
||||
});
|
||||
|
||||
ref.read(shareIntentUploadProvider.notifier).init();
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
@@ -42,10 +43,12 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||
|
||||
await ref.read(backgroundSyncProvider).syncRemote();
|
||||
await ref.read(driftBackupProvider.notifier).getBackupStatus(currentUser.id);
|
||||
await ref.read(driftBackgroundUploadFgService).enableUploadService();
|
||||
await ref.read(driftBackupProvider.notifier).startBackup(currentUser.id);
|
||||
}
|
||||
|
||||
Future<void> stopBackup() async {
|
||||
await ref.read(driftBackgroundUploadFgService).disableUploadService();
|
||||
await ref.read(driftBackupProvider.notifier).cancel();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/services/background.service.dart';
|
||||
import 'package:immich_mobile/utils/migration.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
@@ -68,12 +69,15 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
||||
await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||
await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||
await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||
await ref.read(backgroundServiceProvider).disableService();
|
||||
}
|
||||
} else {
|
||||
await ref.read(backgroundSyncProvider).cancel();
|
||||
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
|
||||
ref.read(websocketProvider.notifier).startListeningToOldEvents();
|
||||
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
|
||||
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
||||
await ref.read(driftBackgroundUploadFgService).disableUploadService();
|
||||
}
|
||||
|
||||
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||
|
||||
296
mobile/lib/platform/background_worker_api.g.dart
generated
Normal file
296
mobile/lib/platform/background_worker_api.g.dart
generated
Normal file
@@ -0,0 +1,296 @@
|
||||
// Autogenerated from Pigeon (v26.0.0), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
|
||||
|
||||
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
PlatformException _createConnectionError(String channelName) {
|
||||
return PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
}
|
||||
|
||||
List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
|
||||
if (empty) {
|
||||
return <Object?>[];
|
||||
}
|
||||
if (error == null) {
|
||||
return <Object?>[result];
|
||||
}
|
||||
return <Object?>[error.code, error.message, error.details];
|
||||
}
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
const _PigeonCodec();
|
||||
@override
|
||||
void writeValue(WriteBuffer buffer, Object? value) {
|
||||
if (value is int) {
|
||||
buffer.putUint8(4);
|
||||
buffer.putInt64(value);
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||
switch (type) {
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BackgroundWorkerFgHostApi {
|
||||
/// Constructor for [BackgroundWorkerFgHostApi]. The [binaryMessenger] named argument is
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
BackgroundWorkerFgHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<void> enableSyncWorker() async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableSyncWorker$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> enableUploadWorker(int callbackHandle) async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.enableUploadWorker$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[callbackHandle]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> disableUploadWorker() async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFgHostApi.disableUploadWorker$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BackgroundWorkerBgHostApi {
|
||||
/// Constructor for [BackgroundWorkerBgHostApi]. The [binaryMessenger] named argument is
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
BackgroundWorkerBgHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<void> onInitialized() async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerBgHostApi.onInitialized$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class BackgroundWorkerFlutterApi {
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
|
||||
Future<void> onLocalSync(int? maxSeconds);
|
||||
|
||||
Future<void> onIosUpload(bool isRefresh, int? maxSeconds);
|
||||
|
||||
Future<void> onAndroidUpload();
|
||||
|
||||
Future<void> cancel();
|
||||
|
||||
static void setUp(
|
||||
BackgroundWorkerFlutterApi? api, {
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
}) {
|
||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
{
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||
assert(
|
||||
message != null,
|
||||
'Argument for dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onLocalSync was null.',
|
||||
);
|
||||
final List<Object?> args = (message as List<Object?>?)!;
|
||||
final int? arg_maxSeconds = (args[0] as int?);
|
||||
try {
|
||||
await api.onLocalSync(arg_maxSeconds);
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||
assert(
|
||||
message != null,
|
||||
'Argument for dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload was null.',
|
||||
);
|
||||
final List<Object?> args = (message as List<Object?>?)!;
|
||||
final bool? arg_isRefresh = (args[0] as bool?);
|
||||
assert(
|
||||
arg_isRefresh != null,
|
||||
'Argument for dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onIosUpload was null, expected non-null bool.',
|
||||
);
|
||||
final int? arg_maxSeconds = (args[1] as int?);
|
||||
try {
|
||||
await api.onIosUpload(arg_isRefresh!, arg_maxSeconds);
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||
try {
|
||||
await api.onAndroidUpload();
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.cancel$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||
try {
|
||||
await api.cancel();
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/background_worker.service.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
@@ -17,6 +18,7 @@ import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
||||
import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
|
||||
import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
||||
@@ -34,6 +36,8 @@ import 'package:logging/logging.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||
|
||||
final driftBackgroundUploadFgService = Provider((ref) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
|
||||
|
||||
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
||||
return BackupNotifier(
|
||||
ref.watch(backupServiceProvider),
|
||||
|
||||
@@ -27,8 +27,12 @@ class UploadRepository {
|
||||
);
|
||||
}
|
||||
|
||||
void enqueueBackgroundAll(List<UploadTask> tasks) {
|
||||
FileDownloader().enqueueAll(tasks);
|
||||
Future<void> enqueueBackground(UploadTask task) {
|
||||
return FileDownloader().enqueue(task);
|
||||
}
|
||||
|
||||
Future<void> enqueueBackgroundAll(List<UploadTask> tasks) {
|
||||
return FileDownloader().enqueueAll(tasks);
|
||||
}
|
||||
|
||||
Future<void> deleteDatabaseRecords(String group) {
|
||||
|
||||
@@ -78,8 +78,8 @@ class UploadService {
|
||||
_taskProgressController.close();
|
||||
}
|
||||
|
||||
void enqueueTasks(List<UploadTask> tasks) {
|
||||
_uploadRepository.enqueueBackgroundAll(tasks);
|
||||
Future<void> enqueueTasks(List<UploadTask> tasks) {
|
||||
return _uploadRepository.enqueueBackgroundAll(tasks);
|
||||
}
|
||||
|
||||
Future<List<Task>> getActiveTasks(String group) {
|
||||
@@ -113,7 +113,7 @@ class UploadService {
|
||||
}
|
||||
|
||||
if (tasks.isNotEmpty) {
|
||||
enqueueTasks(tasks);
|
||||
await enqueueTasks(tasks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,13 +149,37 @@ class UploadService {
|
||||
|
||||
if (tasks.isNotEmpty && !shouldAbortQueuingTasks) {
|
||||
count += tasks.length;
|
||||
enqueueTasks(tasks);
|
||||
await enqueueTasks(tasks);
|
||||
|
||||
onEnqueueTasks(EnqueueStatus(enqueueCount: count, totalCount: candidates.length));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enqueue All does not work from the background on Android yet. This method is a temporary workaround
|
||||
// that enqueues tasks one by one.
|
||||
Future<void> startBackupSerial(String userId) async {
|
||||
await _storageRepository.clearCache();
|
||||
|
||||
shouldAbortQueuingTasks = false;
|
||||
|
||||
final candidates = await _backupRepository.getCandidates(userId);
|
||||
if (candidates.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (final asset in candidates) {
|
||||
if (shouldAbortQueuingTasks) {
|
||||
break;
|
||||
}
|
||||
|
||||
final task = await _getUploadTask(asset);
|
||||
if (task != null) {
|
||||
await _uploadRepository.enqueueBackground(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel all ongoing uploads and reset the upload queue
|
||||
///
|
||||
/// Return the number of left over tasks in the queue
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
@@ -11,6 +13,7 @@ import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||
@@ -22,6 +25,36 @@ import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
void configureFileDownloaderNotifications() {
|
||||
FileDownloader().configureNotificationForGroup(
|
||||
kDownloadGroupImage,
|
||||
running: TaskNotification('downloading_media'.t(), '${'file_name'.t()}: {filename}'),
|
||||
complete: TaskNotification('download_finished'.t(), '${'file_name'.t()}: {filename}'),
|
||||
progressBar: true,
|
||||
);
|
||||
|
||||
FileDownloader().configureNotificationForGroup(
|
||||
kDownloadGroupVideo,
|
||||
running: TaskNotification('downloading_media'.t(), '${'file_name'.t()}: {filename}'),
|
||||
complete: TaskNotification('download_finished'.t(), '${'file_name'.t()}: {filename}'),
|
||||
progressBar: true,
|
||||
);
|
||||
|
||||
FileDownloader().configureNotificationForGroup(
|
||||
kManualUploadGroup,
|
||||
running: TaskNotification('uploading_media'.t(), 'backup_background_service_in_progress_notification'.t()),
|
||||
complete: TaskNotification('upload_finished'.t(), 'backup_background_service_in_progress_notification'.t()),
|
||||
groupNotificationId: kManualUploadGroup,
|
||||
);
|
||||
|
||||
FileDownloader().configureNotificationForGroup(
|
||||
kBackupGroup,
|
||||
running: TaskNotification('uploading_media'.t(), 'backup_background_service_in_progress_notification'.t()),
|
||||
complete: TaskNotification('upload_finished'.t(), 'backup_background_service_in_progress_notification'.t()),
|
||||
groupNotificationId: kBackupGroup,
|
||||
);
|
||||
}
|
||||
|
||||
abstract final class Bootstrap {
|
||||
static Future<(Isar isar, Drift drift, DriftLogger logDb)> initDB() async {
|
||||
final drift = Drift();
|
||||
|
||||
@@ -57,7 +57,7 @@ Cancelable<T?> runInIsolateGentle<T>({
|
||||
log.severe("Error in runInIsolateGentle ${debugLabel == null ? '' : ' for $debugLabel'}", error, stack);
|
||||
} finally {
|
||||
try {
|
||||
await LogService.I.flush();
|
||||
await LogService.I.dispose();
|
||||
await logDb.close();
|
||||
await ref.read(driftProvider).close();
|
||||
|
||||
@@ -72,8 +72,8 @@ Cancelable<T?> runInIsolateGentle<T>({
|
||||
}
|
||||
|
||||
ref.dispose();
|
||||
} catch (error) {
|
||||
debugPrint("Error closing resources in isolate: $error");
|
||||
} catch (error, stack) {
|
||||
debugPrint("Error closing resources in isolate: $error, $stack");
|
||||
} finally {
|
||||
ref.dispose();
|
||||
// Delay to ensure all resources are released
|
||||
|
||||
Reference in New Issue
Block a user