2025-08-28 19:41:54 +05:30
|
|
|
import 'dart:async';
|
2025-09-11 22:31:15 +05:30
|
|
|
import 'dart:io';
|
2025-08-28 19:41:54 +05:30
|
|
|
import 'dart:ui';
|
|
|
|
|
|
|
|
|
|
import 'package:background_downloader/background_downloader.dart';
|
2025-09-11 22:31:15 +05:30
|
|
|
import 'package:cancellation_token_http/http.dart';
|
2025-08-28 19:41:54 +05:30
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
|
|
|
import 'package:immich_mobile/constants/constants.dart';
|
2025-09-11 22:31:15 +05:30
|
|
|
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
|
|
|
|
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
|
|
|
|
import 'package:immich_mobile/generated/intl_keys.g.dart';
|
2025-08-28 19:41:54 +05:30
|
|
|
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';
|
2025-09-11 22:31:15 +05:30
|
|
|
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
2025-08-28 19:41:54 +05:30
|
|
|
import 'package:immich_mobile/providers/user.provider.dart';
|
2025-09-03 02:05:58 +05:30
|
|
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
2025-08-28 19:41:54 +05:30
|
|
|
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';
|
2025-09-11 22:31:15 +05:30
|
|
|
import 'package:immich_mobile/services/server_info.service.dart';
|
2025-08-28 19:41:54 +05:30
|
|
|
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';
|
2025-09-10 22:45:42 +05:30
|
|
|
import 'package:worker_manager/worker_manager.dart';
|
2025-09-12 18:56:00 -04:00
|
|
|
import 'package:immich_mobile/utils/debug_print.dart';
|
2025-08-28 19:41:54 +05:30
|
|
|
|
|
|
|
|
class BackgroundWorkerFgService {
|
|
|
|
|
final BackgroundWorkerFgHostApi _foregroundHostApi;
|
|
|
|
|
|
|
|
|
|
const BackgroundWorkerFgService(this._foregroundHostApi);
|
|
|
|
|
|
|
|
|
|
// TODO: Move this call to native side once old timeline is removed
|
2025-09-03 20:27:30 +05:30
|
|
|
Future<void> enable() => _foregroundHostApi.enable();
|
2025-08-28 19:41:54 +05:30
|
|
|
|
2025-09-03 20:27:30 +05:30
|
|
|
Future<void> disable() => _foregroundHostApi.disable();
|
2025-08-28 19:41:54 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
|
|
|
|
late final ProviderContainer _ref;
|
|
|
|
|
final Isar _isar;
|
|
|
|
|
final Drift _drift;
|
|
|
|
|
final DriftLogger _driftLogger;
|
|
|
|
|
final BackgroundWorkerBgHostApi _backgroundHostApi;
|
2025-09-11 22:31:15 +05:30
|
|
|
final CancellationToken _cancellationToken = CancellationToken();
|
2025-09-10 22:45:42 +05:30
|
|
|
final Logger _logger = Logger('BackgroundWorkerBgService');
|
2025-08-28 19:41:54 +05:30
|
|
|
|
|
|
|
|
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 {
|
2025-09-03 02:05:58 +05:30
|
|
|
try {
|
|
|
|
|
HttpSSLOptions.apply(applyNative: false);
|
2025-09-10 22:45:42 +05:30
|
|
|
|
|
|
|
|
await Future.wait([
|
|
|
|
|
loadTranslations(),
|
|
|
|
|
workerManager.init(dynamicSpawning: true),
|
|
|
|
|
_ref.read(authServiceProvider).setOpenApiServiceEndpoint(),
|
|
|
|
|
// Initialize the file downloader
|
|
|
|
|
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),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false),
|
|
|
|
|
FileDownloader().trackTasks(),
|
|
|
|
|
_ref.read(fileMediaRepositoryProvider).enableBackgroundAccess(),
|
|
|
|
|
]);
|
|
|
|
|
|
2025-09-03 02:05:58 +05:30
|
|
|
configureFileDownloaderNotifications();
|
2025-09-04 22:14:33 +05:30
|
|
|
|
2025-09-11 22:31:15 +05:30
|
|
|
if (Platform.isAndroid) {
|
|
|
|
|
await _backgroundHostApi.showNotification(
|
|
|
|
|
IntlKeys.uploading_media.t(),
|
|
|
|
|
IntlKeys.backup_background_service_in_progress_notification.t(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 22:45:42 +05:30
|
|
|
// Notify the host that the background worker service has been initialized and is ready to use
|
|
|
|
|
_backgroundHostApi.onInitialized();
|
2025-09-03 02:05:58 +05:30
|
|
|
} catch (error, stack) {
|
|
|
|
|
_logger.severe("Failed to initialize background worker", error, stack);
|
|
|
|
|
_backgroundHostApi.close();
|
|
|
|
|
}
|
2025-08-28 19:41:54 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<void> onAndroidUpload() async {
|
2025-09-03 02:05:58 +05:30
|
|
|
try {
|
|
|
|
|
_logger.info('Android background processing started');
|
|
|
|
|
final sw = Stopwatch()..start();
|
|
|
|
|
|
|
|
|
|
await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6));
|
2025-09-11 22:31:15 +05:30
|
|
|
await _handleBackup();
|
2025-09-03 02:05:58 +05:30
|
|
|
|
|
|
|
|
sw.stop();
|
|
|
|
|
_logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s");
|
|
|
|
|
} catch (error, stack) {
|
|
|
|
|
_logger.severe("Failed to complete Android background processing", error, stack);
|
|
|
|
|
} finally {
|
|
|
|
|
await _cleanup();
|
|
|
|
|
}
|
2025-08-28 19:41:54 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async {
|
2025-09-03 02:05:58 +05:30
|
|
|
try {
|
|
|
|
|
_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;
|
|
|
|
|
}
|
2025-08-28 19:41:54 +05:30
|
|
|
|
2025-09-03 02:05:58 +05:30
|
|
|
sw.stop();
|
|
|
|
|
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s");
|
|
|
|
|
} catch (error, stack) {
|
|
|
|
|
_logger.severe("Failed to complete iOS background upload", error, stack);
|
|
|
|
|
} finally {
|
|
|
|
|
await _cleanup();
|
2025-08-28 19:41:54 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<void> cancel() async {
|
2025-09-03 02:05:58 +05:30
|
|
|
_logger.warning("Background worker cancelled");
|
|
|
|
|
try {
|
|
|
|
|
await _cleanup();
|
|
|
|
|
} catch (error, stack) {
|
2025-09-12 18:56:00 -04:00
|
|
|
dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack');
|
2025-09-03 02:05:58 +05:30
|
|
|
}
|
2025-08-28 19:41:54 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> _cleanup() async {
|
|
|
|
|
if (_isCleanedUp) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-03 02:05:58 +05:30
|
|
|
try {
|
2025-09-11 14:02:03 -05:00
|
|
|
final backgroundSyncManager = _ref.read(backgroundSyncProvider);
|
2025-09-03 02:05:58 +05:30
|
|
|
_isCleanedUp = true;
|
2025-09-11 14:02:03 -05:00
|
|
|
_ref.dispose();
|
|
|
|
|
|
2025-09-11 22:31:15 +05:30
|
|
|
_cancellationToken.cancel();
|
2025-09-03 02:05:58 +05:30
|
|
|
_logger.info("Cleaning up background worker");
|
2025-09-08 14:18:13 -05:00
|
|
|
final cleanupFutures = [
|
2025-09-11 22:31:15 +05:30
|
|
|
workerManager.dispose().catchError((_) async {
|
|
|
|
|
// Discard any errors on the dispose call
|
|
|
|
|
return;
|
|
|
|
|
}),
|
2025-09-08 14:18:13 -05:00
|
|
|
_drift.close(),
|
|
|
|
|
_driftLogger.close(),
|
2025-09-11 14:02:03 -05:00
|
|
|
backgroundSyncManager.cancel(),
|
|
|
|
|
backgroundSyncManager.cancelLocal(),
|
2025-09-08 14:18:13 -05:00
|
|
|
];
|
|
|
|
|
|
2025-09-03 02:05:58 +05:30
|
|
|
if (_isar.isOpen) {
|
2025-09-08 14:18:13 -05:00
|
|
|
cleanupFutures.add(_isar.close());
|
2025-09-03 02:05:58 +05:30
|
|
|
}
|
2025-09-08 14:18:13 -05:00
|
|
|
await Future.wait(cleanupFutures);
|
2025-09-04 22:14:33 +05:30
|
|
|
_logger.info("Background worker resources cleaned up");
|
2025-09-03 02:05:58 +05:30
|
|
|
} catch (error, stack) {
|
2025-09-12 18:56:00 -04:00
|
|
|
dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack');
|
2025-09-03 02:05:58 +05:30
|
|
|
}
|
2025-08-28 19:41:54 +05:30
|
|
|
}
|
|
|
|
|
|
2025-09-11 22:31:15 +05:30
|
|
|
Future<void> _handleBackup() async {
|
2025-09-11 14:02:03 -05:00
|
|
|
await runZonedGuarded(
|
|
|
|
|
() async {
|
|
|
|
|
if (!_isBackupEnabled || _isCleanedUp) {
|
|
|
|
|
_logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-08-28 19:41:54 +05:30
|
|
|
|
2025-09-11 14:02:03 -05:00
|
|
|
_logger.info("[_handleBackup 2] Enqueuing assets for backup from the background service");
|
2025-09-10 22:45:42 +05:30
|
|
|
|
2025-09-11 14:02:03 -05:00
|
|
|
final currentUser = _ref.read(currentUserProvider);
|
|
|
|
|
if (currentUser == null) {
|
|
|
|
|
_logger.warning("[_handleBackup 3] No current user found. Skipping backup from background");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-08-28 19:41:54 +05:30
|
|
|
|
2025-09-11 14:02:03 -05:00
|
|
|
_logger.info("[_handleBackup 4] Resume backup from background");
|
|
|
|
|
if (Platform.isIOS) {
|
|
|
|
|
return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
|
|
|
|
}
|
2025-08-28 19:41:54 +05:30
|
|
|
|
2025-09-11 14:02:03 -05:00
|
|
|
final canPing = await _ref.read(serverInfoServiceProvider).ping();
|
|
|
|
|
if (!canPing) {
|
|
|
|
|
_logger.warning("[_handleBackup 5] Server is not reachable. Skipping backup from background");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-09-11 22:31:15 +05:30
|
|
|
|
2025-09-11 14:02:03 -05:00
|
|
|
final networkCapabilities = await _ref.read(connectivityApiProvider).getCapabilities();
|
2025-09-11 22:31:15 +05:30
|
|
|
|
2025-09-11 14:02:03 -05:00
|
|
|
return _ref
|
|
|
|
|
.read(uploadServiceProvider)
|
|
|
|
|
.startBackupWithHttpClient(currentUser.id, networkCapabilities.hasWifi, _cancellationToken);
|
|
|
|
|
},
|
|
|
|
|
(error, stack) {
|
2025-09-12 18:56:00 -04:00
|
|
|
dPrint(() => "Error in backup zone $error, $stack");
|
2025-09-11 14:02:03 -05:00
|
|
|
},
|
|
|
|
|
);
|
2025-08-28 19:41:54 +05:30
|
|
|
}
|
|
|
|
|
|
2025-09-03 20:27:30 +05:30
|
|
|
Future<void> _syncAssets({Duration? hashTimeout}) async {
|
2025-09-10 16:17:37 -05:00
|
|
|
await _ref.read(backgroundSyncProvider).syncLocal();
|
|
|
|
|
if (_isCleanedUp) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-08-28 19:41:54 +05:30
|
|
|
|
2025-09-10 16:17:37 -05:00
|
|
|
await _ref.read(backgroundSyncProvider).syncRemote();
|
|
|
|
|
if (_isCleanedUp) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-08-28 19:41:54 +05:30
|
|
|
|
2025-09-10 16:17:37 -05:00
|
|
|
var hashFuture = _ref.read(backgroundSyncProvider).hashAssets();
|
|
|
|
|
if (hashTimeout != null) {
|
|
|
|
|
hashFuture = hashFuture.timeout(
|
|
|
|
|
hashTimeout,
|
|
|
|
|
onTimeout: () {
|
|
|
|
|
// Consume cancellation errors as we want to continue processing
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-08-28 19:41:54 +05:30
|
|
|
|
2025-09-10 16:17:37 -05:00
|
|
|
await hashFuture;
|
2025-08-28 19:41:54 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-03 02:05:58 +05:30
|
|
|
/// Native entry invoked from the background worker. If renaming or moving this to a different
|
|
|
|
|
/// library, make sure to update the entry points and URI in native workers as well
|
2025-08-28 19:41:54 +05:30
|
|
|
@pragma('vm:entry-point')
|
2025-09-03 02:05:58 +05:30
|
|
|
Future<void> backgroundSyncNativeEntrypoint() async {
|
2025-08-28 19:41:54 +05:30
|
|
|
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();
|
|
|
|
|
}
|