fix: android crash on app pause (#21768)

* revert service locks

* rename backgroundWorkerFgServiceProvider

* refactor: parallel background worker init (#21769)

* refactor: parallel background worker init

* fix: hashing not running from the background engine (#21773)

* init and dispose workmanager from background engine

* log message contend

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>

---------

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:
shenlong 2025-09-10 22:45:42 +05:30 committed by GitHub
parent 2c7b980eed
commit e239b8d2fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 44 additions and 343 deletions

View File

@ -5,7 +5,6 @@ import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/logger_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/platform/background_worker_api.g.dart';
@ -24,6 +23,7 @@ import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:immich_mobile/utils/http_ssl_options.dart'; import 'package:immich_mobile/utils/http_ssl_options.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:worker_manager/worker_manager.dart';
class BackgroundWorkerFgService { class BackgroundWorkerFgService {
final BackgroundWorkerFgHostApi _foregroundHostApi; final BackgroundWorkerFgHostApi _foregroundHostApi;
@ -42,8 +42,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
final Drift _drift; final Drift _drift;
final DriftLogger _driftLogger; final DriftLogger _driftLogger;
final BackgroundWorkerBgHostApi _backgroundHostApi; final BackgroundWorkerBgHostApi _backgroundHostApi;
final Logger _logger = Logger('BackgroundUploadBgService'); final Logger _logger = Logger('BackgroundWorkerBgService');
late final IsolateLockManager _lockManager;
bool _isCleanedUp = false; bool _isCleanedUp = false;
@ -59,7 +58,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
driftProvider.overrideWith(driftOverride(drift)), driftProvider.overrideWith(driftOverride(drift)),
], ],
); );
_lockManager = IsolateLockManager(onCloseRequest: _cleanup);
BackgroundWorkerFlutterApi.setUp(this); BackgroundWorkerFlutterApi.setUp(this);
} }
@ -67,41 +65,30 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
Future<void> init() async { Future<void> init() async {
try { try {
await loadTranslations();
HttpSSLOptions.apply(applyNative: false); HttpSSLOptions.apply(applyNative: false);
await _ref.read(authServiceProvider).setOpenApiServiceEndpoint();
await Future.wait([
loadTranslations(),
workerManager.init(dynamicSpawning: true),
_ref.read(authServiceProvider).setOpenApiServiceEndpoint(),
// Initialize the file downloader // Initialize the file downloader
await FileDownloader().configure( FileDownloader().configure(
globalConfig: [ globalConfig: [
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3 // maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
(Config.holdingQueue, (6, 6, 3)), (Config.holdingQueue, (6, 6, 3)),
// On Android, if files are larger than 256MB, run in foreground service // On Android, if files are larger than 256MB, run in foreground service
(Config.runInForegroundIfFileLargerThan, 256), (Config.runInForegroundIfFileLargerThan, 256),
], ],
); ),
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false); FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false),
await FileDownloader().trackTasks(); FileDownloader().trackTasks(),
_ref.read(fileMediaRepositoryProvider).enableBackgroundAccess(),
]);
configureFileDownloaderNotifications(); configureFileDownloaderNotifications();
await _ref.read(fileMediaRepositoryProvider).enableBackgroundAccess();
// Notify the host that the background upload service has been initialized and is ready to use // Notify the host that the background worker service has been initialized and is ready to use
debugPrint("Acquiring background worker lock"); _backgroundHostApi.onInitialized();
if (await _lockManager.acquireLock().timeout(
const Duration(seconds: 5),
onTimeout: () {
_lockManager.cancel();
return false;
},
)) {
_logger.info("Acquired background worker lock");
await _backgroundHostApi.onInitialized();
return;
}
_logger.warning("Failed to acquire background worker lock");
await _cleanup();
await _backgroundHostApi.close();
} catch (error, stack) { } catch (error, stack) {
_logger.severe("Failed to initialize background worker", error, stack); _logger.severe("Failed to initialize background worker", error, stack);
_backgroundHostApi.close(); _backgroundHostApi.close();
@ -170,6 +157,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
_isCleanedUp = true; _isCleanedUp = true;
_logger.info("Cleaning up background worker"); _logger.info("Cleaning up background worker");
final cleanupFutures = [ final cleanupFutures = [
workerManager.dispose(),
_drift.close(), _drift.close(),
_driftLogger.close(), _driftLogger.close(),
_ref.read(backgroundSyncProvider).cancel(), _ref.read(backgroundSyncProvider).cancel(),
@ -180,8 +168,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
cleanupFutures.add(_isar.close()); cleanupFutures.add(_isar.close());
} }
_ref.dispose(); _ref.dispose();
_lockManager.releaseLock();
await Future.wait(cleanupFutures); await Future.wait(cleanupFutures);
_logger.info("Background worker resources cleaned up"); _logger.info("Background worker resources cleaned up");
} catch (error, stack) { } catch (error, stack) {
@ -191,22 +177,29 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
Future<void> _handleBackup({bool processBulk = true}) async { Future<void> _handleBackup({bool processBulk = true}) async {
if (!_isBackupEnabled) { if (!_isBackupEnabled) {
_logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine");
return; return;
} }
_logger.info("[_handleBackup 2] Enqueuing assets for backup from the background service");
final currentUser = _ref.read(currentUserProvider); final currentUser = _ref.read(currentUserProvider);
if (currentUser == null) { if (currentUser == null) {
_logger.warning("[_handleBackup 3] No current user found. Skipping backup from background");
return; return;
} }
if (processBulk) { if (processBulk) {
_logger.info("[_handleBackup 4] Resume backup from background");
return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id); return _ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
} }
final activeTask = await _ref.read(uploadServiceProvider).getActiveTasks(currentUser.id); final activeTask = await _ref.read(uploadServiceProvider).getActiveTasks(currentUser.id);
if (activeTask.isNotEmpty) { if (activeTask.isNotEmpty) {
_logger.info("[_handleBackup 5] Resuming backup for active tasks from background");
await _ref.read(uploadServiceProvider).resumeBackup(); await _ref.read(uploadServiceProvider).resumeBackup();
} else { } else {
_logger.info("[_handleBackup 6] Starting serial backup for new tasks from background");
await _ref.read(uploadServiceProvider).startBackupSerial(currentUser.id); await _ref.read(uploadServiceProvider).startBackupSerial(currentUser.id);
} }
} }

View File

@ -1,235 +0,0 @@
import 'dart:isolate';
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
const String kIsolateLockManagerPort = "immich://isolate_mutex";
enum _LockStatus { active, released }
class _IsolateRequest {
const _IsolateRequest();
}
class _HeartbeatRequest extends _IsolateRequest {
// Port for the receiver to send replies back
final SendPort sendPort;
const _HeartbeatRequest(this.sendPort);
Map<String, dynamic> toJson() {
return {'type': 'heartbeat', 'sendPort': sendPort};
}
}
class _CloseRequest extends _IsolateRequest {
const _CloseRequest();
Map<String, dynamic> toJson() {
return {'type': 'close'};
}
}
class _IsolateResponse {
const _IsolateResponse();
}
class _HeartbeatResponse extends _IsolateResponse {
final _LockStatus status;
const _HeartbeatResponse(this.status);
Map<String, dynamic> toJson() {
return {'type': 'heartbeat', 'status': status.index};
}
}
typedef OnCloseLockHolderRequest = void Function();
class IsolateLockManager {
final String _portName;
bool _hasLock = false;
ReceivePort? _receivePort;
final OnCloseLockHolderRequest? _onCloseRequest;
final Set<SendPort> _waitingIsolates = {};
// Token object - a new one is created for each acquisition attempt
Object? _currentAcquisitionToken;
IsolateLockManager({String? portName, OnCloseLockHolderRequest? onCloseRequest})
: _portName = portName ?? kIsolateLockManagerPort,
_onCloseRequest = onCloseRequest;
Future<bool> acquireLock() async {
if (_hasLock) {
Logger('BackgroundWorkerLockManager').warning("WARNING: [acquireLock] called more than once");
return true;
}
// Create a new token - this invalidates any previous attempt
final token = _currentAcquisitionToken = Object();
final ReceivePort rp = _receivePort = ReceivePort(_portName);
final SendPort sp = rp.sendPort;
while (!IsolateNameServer.registerPortWithName(sp, _portName)) {
// This attempt was superseded by a newer one in the same isolate
if (_currentAcquisitionToken != token) {
return false;
}
await _lockReleasedByHolder(token);
}
_hasLock = true;
rp.listen(_onRequest);
return true;
}
Future<void> _lockReleasedByHolder(Object token) async {
SendPort? holder = IsolateNameServer.lookupPortByName(_portName);
debugPrint("Found lock holder: $holder");
if (holder == null) {
// No holder, try and acquire lock
return;
}
final ReceivePort tempRp = ReceivePort();
final SendPort tempSp = tempRp.sendPort;
final bs = tempRp.asBroadcastStream();
try {
while (true) {
// Send a heartbeat request with the send port to receive reply from the holder
debugPrint("Sending heartbeat request to lock holder");
holder.send(_HeartbeatRequest(tempSp).toJson());
dynamic answer = await bs.first.timeout(const Duration(seconds: 3), onTimeout: () => null);
debugPrint("Received heartbeat response from lock holder: $answer");
// This attempt was superseded by a newer one in the same isolate
if (_currentAcquisitionToken != token) {
break;
}
if (answer == null) {
// Holder failed, most likely killed without calling releaseLock
// Check if a different waiting isolate took the lock
if (holder == IsolateNameServer.lookupPortByName(_portName)) {
// No, remove the stale lock
IsolateNameServer.removePortNameMapping(_portName);
}
break;
}
// Unknown message type received for heartbeat request. Try again
_IsolateResponse? response = _parseResponse(answer);
if (response == null || response is! _HeartbeatResponse) {
break;
}
if (response.status == _LockStatus.released) {
// Holder has released the lock
break;
}
// If the _LockStatus is active, we check again if the task completed
// by sending a released messaged again, if not, send a new heartbeat again
// Check if the holder completed its task after the heartbeat
answer = await bs.first.timeout(
const Duration(seconds: 3),
onTimeout: () => const _HeartbeatResponse(_LockStatus.active).toJson(),
);
response = _parseResponse(answer);
if (response is _HeartbeatResponse && response.status == _LockStatus.released) {
break;
}
}
} catch (e) {
// Timeout or error
} finally {
tempRp.close();
}
return;
}
_IsolateRequest? _parseRequest(dynamic msg) {
if (msg is! Map<String, dynamic>) {
return null;
}
return switch (msg['type']) {
'heartbeat' => _HeartbeatRequest(msg['sendPort']),
'close' => const _CloseRequest(),
_ => null,
};
}
_IsolateResponse? _parseResponse(dynamic msg) {
if (msg is! Map<String, dynamic>) {
return null;
}
return switch (msg['type']) {
'heartbeat' => _HeartbeatResponse(_LockStatus.values[msg['status']]),
_ => null,
};
}
// Executed in the isolate with the lock
void _onRequest(dynamic msg) {
final request = _parseRequest(msg);
if (request == null) {
return;
}
if (request is _HeartbeatRequest) {
// Add the send port to the list of waiting isolates
_waitingIsolates.add(request.sendPort);
request.sendPort.send(const _HeartbeatResponse(_LockStatus.active).toJson());
return;
}
if (request is _CloseRequest) {
_onCloseRequest?.call();
return;
}
}
void releaseLock() {
if (_hasLock) {
IsolateNameServer.removePortNameMapping(_portName);
// Notify waiting isolates
for (final port in _waitingIsolates) {
port.send(const _HeartbeatResponse(_LockStatus.released).toJson());
}
_waitingIsolates.clear();
_hasLock = false;
}
_receivePort?.close();
_receivePort = null;
}
void cancel() {
if (_hasLock) {
return;
}
debugPrint("Cancelling ongoing acquire lock attempts");
// Create a new token to invalidate ongoing acquire lock attempts
_currentAcquisitionToken = Object();
}
void requestHolderToClose() {
if (_hasLock) {
return;
}
IsolateNameServer.lookupPortByName(_portName)?.send(const _CloseRequest().toJson());
}
}

View File

@ -17,9 +17,9 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/generated/codegen_loader.g.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_life_cycle.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.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/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart';
@ -205,9 +205,9 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
// needs to be delayed so that EasyLocalization is working // needs to be delayed so that EasyLocalization is working
if (Store.isBetaTimelineEnabled) { if (Store.isBetaTimelineEnabled) {
ref.read(backgroundServiceProvider).disableService(); ref.read(backgroundServiceProvider).disableService();
ref.read(driftBackgroundUploadFgService).enable(); ref.read(backgroundWorkerFgServiceProvider).enable();
} else { } else {
ref.read(driftBackgroundUploadFgService).disable(); ref.read(backgroundWorkerFgServiceProvider).disable();
ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
} }
}); });

View File

@ -13,6 +13,7 @@ import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.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/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/background.service.dart';
@ -79,7 +80,7 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
ref.read(readonlyModeProvider.notifier).setReadonlyMode(false); ref.read(readonlyModeProvider.notifier).setReadonlyMode(false);
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider)); await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
await ref.read(backgroundServiceProvider).resumeServiceIfEnabled(); await ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
await ref.read(driftBackgroundUploadFgService).disable(); await ref.read(backgroundWorkerFgServiceProvider).disable();
} }
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta); await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);

View File

@ -2,10 +2,8 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.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.provider.dart';
import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
@ -23,23 +21,14 @@ class SplashScreenPage extends StatefulHookConsumerWidget {
class SplashScreenPageState extends ConsumerState<SplashScreenPage> { class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
final log = Logger("SplashScreenPage"); final log = Logger("SplashScreenPage");
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final lockManager = ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)); ref
lockManager.requestHolderToClose();
lockManager
.acquireLock()
.timeout(const Duration(seconds: 5))
.whenComplete(
() => ref
.read(authProvider.notifier) .read(authProvider.notifier)
.setOpenApiServiceEndpoint() .setOpenApiServiceEndpoint()
.then(logConnectionInfo) .then(logConnectionInfo)
.whenComplete(() => resumeSession()), .whenComplete(() => resumeSession());
);
} }
void logConnectionInfo(String? endpoint) { void logConnectionInfo(String? endpoint) {

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album.provider.dart';
@ -139,27 +138,10 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
Future<void> _handleBetaTimelineResume() async { Future<void> _handleBetaTimelineResume() async {
_ref.read(backupProvider.notifier).cancelBackup(); _ref.read(backupProvider.notifier).cancelBackup();
final lockManager = _ref.read(isolateLockManagerProvider(kIsolateLockManagerPort));
// Give isolates time to complete any ongoing database transactions // Give isolates time to complete any ongoing database transactions
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
lockManager.requestHolderToClose();
// Add timeout to prevent deadlock on lock acquisition
try {
await lockManager.acquireLock().timeout(
const Duration(seconds: 10),
onTimeout: () {
_log.warning("Lock acquisition timed out, proceeding without lock");
throw TimeoutException("Lock acquisition timed out", const Duration(seconds: 10));
},
);
} catch (e) {
_log.warning("Failed to acquire lock: $e");
return;
}
final backgroundManager = _ref.read(backgroundSyncProvider); final backgroundManager = _ref.read(backgroundSyncProvider);
final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup); final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
@ -186,7 +168,6 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
} finally { } finally {
// Ensure lock is released even if operations fail // Ensure lock is released even if operations fail
try { try {
lockManager.releaseLock();
_log.info("Lock released after background sync operations"); _log.info("Lock released after background sync operations");
} catch (lockError) { } catch (lockError) {
_log.warning("Failed to release lock after error: $lockError"); _log.warning("Failed to release lock after error: $lockError");
@ -241,28 +222,6 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) { if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) {
_ref.read(backupProvider.notifier).cancelBackup(); _ref.read(backupProvider.notifier).cancelBackup();
} }
} else {
final backgroundManager = _ref.read(backgroundSyncProvider);
// Cancel operations with extended timeout to allow database transactions to complete
try {
await Future.wait([
backgroundManager.cancel().timeout(const Duration(seconds: 10)),
backgroundManager.cancelLocal().timeout(const Duration(seconds: 10)),
]).timeout(const Duration(seconds: 15));
// Give additional time for isolates to clean up database connections
await Future.delayed(const Duration(milliseconds: 1000));
} catch (e) {
_log.warning("Timeout during background cancellation: $e");
}
// Always release the lock, even if cancellation failed
try {
_ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)).releaseLock();
} catch (e) {
_log.warning("Failed to release lock on pause: $e");
}
} }
_ref.read(websocketProvider.notifier).disconnect(); _ref.read(websocketProvider.notifier).disconnect();
@ -290,7 +249,6 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
} catch (_) {} } catch (_) {}
if (Store.isBetaTimelineEnabled) { if (Store.isBetaTimelineEnabled) {
_ref.read(isolateLockManagerProvider(kIsolateLockManagerPort)).releaseLock();
return; return;
} }

View File

@ -1,6 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/domain/utils/isolate_lock_manager.dart';
import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart';
final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) { final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
@ -19,7 +18,3 @@ final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
ref.onDispose(manager.cancel); ref.onDispose(manager.cancel);
return manager; return manager;
}); });
final isolateLockManagerProvider = Provider.family<IsolateLockManager, String>((ref, name) {
return IsolateLockManager(portName: name);
});

View File

@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.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/album.entity.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
@ -18,7 +17,6 @@ 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/error_upload_asset.model.dart';
import 'package:immich_mobile/models/backup/success_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/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/app_life_cycle.provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
@ -36,8 +34,6 @@ import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
final driftBackgroundUploadFgService = Provider((ref) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) { final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
return BackupNotifier( return BackupNotifier(
ref.watch(backupServiceProvider), ref.watch(backupServiceProvider),

View File

@ -1,7 +1,11 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/services/background_worker.service.dart';
import 'package:immich_mobile/platform/background_worker_api.g.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/platform/thumbnail_api.g.dart'; import 'package:immich_mobile/platform/thumbnail_api.g.dart';
final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi()); final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());
final thumbnailApi = ThumbnailApi(); final thumbnailApi = ThumbnailApi();