feat(mobile): use shared native client (#25942)

* use shared client in dart

fix android

* websocket integration

platform-side headers

update comment

consistent platform check

tweak websocket handling

support streaming

* redundant logging

* fix proguard

* formatting

* handle onProgress

* support videos on ios

* inline return

* improved ios impl

* cleanup

* sync stopForegroundBackup

* voidify

* future already completed

* stream request on android

* outdated ios ws code

* use `choosePrivateKeyAlias`

* return result

* formatting

* update tests

* redundant check

* handle custom headers

* move completer outside of state

* persist auth

* dispose old socket

* use group id for cookies

* redundant headers

* cache global ref

* handle network switching

* handle basic auth

* apply custom headers immediately

* video player update

* fix

* persist url

* potential logout fix

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Mert
2026-03-05 12:04:45 -05:00
committed by GitHub
parent 35a521c6ec
commit a05c8c6087
57 changed files with 880 additions and 855 deletions
@@ -1,4 +1,5 @@
import 'package:cancellation_token_http/http.dart';
import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
/// Tracks per-asset upload progress.
@@ -30,4 +31,4 @@ final assetUploadProgressProvider = NotifierProvider<AssetUploadProgressNotifier
AssetUploadProgressNotifier.new,
);
final manualUploadCancelTokenProvider = StateProvider<CancellationToken?>((ref) => null);
final manualUploadCancelTokenProvider = StateProvider<Completer<void>?>((ref) => null);
@@ -1,6 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
@@ -68,7 +68,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
progressInFileSpeeds: const [],
progressInFileSpeedUpdateTime: DateTime.now(),
progressInFileSpeedUpdateSentBytes: 0,
cancelToken: CancellationToken(),
autoBackup: Store.get(StoreKey.autoBackup, false),
backgroundBackup: Store.get(StoreKey.backgroundBackup, false),
backupRequireWifi: Store.get(StoreKey.backupRequireWifi, true),
@@ -102,6 +101,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
final FileMediaRepository _fileMediaRepository;
final BackupAlbumService _backupAlbumService;
final Ref ref;
Completer<void>? _cancelToken;
///
/// UI INTERACTION
@@ -454,7 +454,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
}
// Perform Backup
state = state.copyWith(cancelToken: CancellationToken());
_cancelToken?.complete();
_cancelToken = Completer<void>();
final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null;
@@ -465,7 +466,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
await _backupService.backupAsset(
assetsWillBeBackup,
state.cancelToken,
_cancelToken!,
pmProgressHandler: pmProgressHandler,
onSuccess: _onAssetUploaded,
onProgress: _onUploadProgress,
@@ -494,7 +495,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
if (state.backupProgress != BackUpProgressEnum.inProgress) {
notifyBackgroundServiceCanRun();
}
state.cancelToken.cancel();
_cancelToken?.complete();
_cancelToken = null;
state = state.copyWith(
backupProgress: BackUpProgressEnum.idle,
progressInPercentage: 0.0,
@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
@@ -109,7 +108,6 @@ class DriftBackupState {
final BackupError error;
final Map<String, DriftUploadStatus> uploadItems;
final CancellationToken? cancelToken;
final Map<String, double> iCloudDownloadProgress;
@@ -121,7 +119,6 @@ class DriftBackupState {
required this.isSyncing,
this.error = BackupError.none,
required this.uploadItems,
this.cancelToken,
this.iCloudDownloadProgress = const {},
});
@@ -133,7 +130,6 @@ class DriftBackupState {
bool? isSyncing,
BackupError? error,
Map<String, DriftUploadStatus>? uploadItems,
CancellationToken? cancelToken,
Map<String, double>? iCloudDownloadProgress,
}) {
return DriftBackupState(
@@ -144,7 +140,6 @@ class DriftBackupState {
isSyncing: isSyncing ?? this.isSyncing,
error: error ?? this.error,
uploadItems: uploadItems ?? this.uploadItems,
cancelToken: cancelToken ?? this.cancelToken,
iCloudDownloadProgress: iCloudDownloadProgress ?? this.iCloudDownloadProgress,
);
}
@@ -153,7 +148,7 @@ class DriftBackupState {
@override
String toString() {
return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, processingCount: $processingCount, isSyncing: $isSyncing, error: $error, uploadItems: $uploadItems, cancelToken: $cancelToken, iCloudDownloadProgress: $iCloudDownloadProgress)';
return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, processingCount: $processingCount, isSyncing: $isSyncing, error: $error, uploadItems: $uploadItems, iCloudDownloadProgress: $iCloudDownloadProgress)';
}
@override
@@ -168,8 +163,7 @@ class DriftBackupState {
other.isSyncing == isSyncing &&
other.error == error &&
mapEquals(other.iCloudDownloadProgress, iCloudDownloadProgress) &&
mapEquals(other.uploadItems, uploadItems) &&
other.cancelToken == cancelToken;
mapEquals(other.uploadItems, uploadItems);
}
@override
@@ -181,7 +175,6 @@ class DriftBackupState {
isSyncing.hashCode ^
error.hashCode ^
uploadItems.hashCode ^
cancelToken.hashCode ^
iCloudDownloadProgress.hashCode;
}
}
@@ -211,6 +204,7 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
final ForegroundUploadService _foregroundUploadService;
final BackgroundUploadService _backgroundUploadService;
final UploadSpeedManager _uploadSpeedManager;
Completer<void>? _cancelToken;
final _logger = Logger("DriftBackupNotifier");
@@ -246,7 +240,7 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
);
}
void updateError(BackupError error) async {
void updateError(BackupError error) {
if (!mounted) {
_logger.warning("Skip updateError: notifier disposed");
return;
@@ -254,24 +248,23 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
state = state.copyWith(error: error);
}
void updateSyncing(bool isSyncing) async {
void updateSyncing(bool isSyncing) {
state = state.copyWith(isSyncing: isSyncing);
}
Future<void> startForegroundBackup(String userId) async {
Future<void> startForegroundBackup(String userId) {
// Cancel any existing backup before starting a new one
if (state.cancelToken != null) {
await stopForegroundBackup();
if (_cancelToken != null) {
stopForegroundBackup();
}
state = state.copyWith(error: BackupError.none);
final cancelToken = CancellationToken();
state = state.copyWith(cancelToken: cancelToken);
_cancelToken = Completer<void>();
return _foregroundUploadService.uploadCandidates(
userId,
cancelToken,
_cancelToken!,
callbacks: UploadCallbacks(
onProgress: _handleForegroundBackupProgress,
onSuccess: _handleForegroundBackupSuccess,
@@ -281,10 +274,11 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
);
}
Future<void> stopForegroundBackup() async {
state.cancelToken?.cancel();
void stopForegroundBackup() {
_cancelToken?.complete();
_cancelToken = null;
_uploadSpeedManager.clear();
state = state.copyWith(cancelToken: null, uploadItems: {}, iCloudDownloadProgress: {});
state = state.copyWith(uploadItems: {}, iCloudDownloadProgress: {});
}
void _handleICloudProgress(String localAssetId, double progress) {
@@ -300,7 +294,7 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
}
void _handleForegroundBackupProgress(String localAssetId, String filename, int bytes, int totalBytes) {
if (state.cancelToken == null) {
if (_cancelToken == null) {
return;
}
@@ -399,7 +393,7 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
}
}
final driftBackupCandidateProvider = FutureProvider.autoDispose<List<LocalAsset>>((ref) async {
final driftBackupCandidateProvider = FutureProvider.autoDispose<List<LocalAsset>>((ref) {
final user = ref.watch(currentUserProvider);
if (user == null) {
return [];
@@ -1,7 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/widgets.dart';
@@ -50,6 +49,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
final BackupService _backupService;
final BackupAlbumService _backupAlbumService;
final Ref ref;
Completer<void>? _cancelToken;
ManualUploadNotifier(
this._localNotificationService,
@@ -65,7 +65,6 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
progressInFileSpeeds: const [],
progressInFileSpeedUpdateTime: DateTime.now(),
progressInFileSpeedUpdateSentBytes: 0,
cancelToken: CancellationToken(),
currentUploadAsset: CurrentUploadAsset(
id: '...',
fileCreatedAt: DateTime.parse('2020-10-04'),
@@ -236,7 +235,6 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
fileName: '...',
fileType: '...',
),
cancelToken: CancellationToken(),
);
// Reset Error List
ref.watch(errorBackupListProvider.notifier).empty();
@@ -252,11 +250,13 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
state = state.copyWith(showDetailedNotification: showDetailedNotification);
final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null;
_cancelToken?.complete();
_cancelToken = Completer<void>();
final bool ok = await ref
.read(backupServiceProvider)
.backupAsset(
uploadAssets,
state.cancelToken,
_cancelToken!,
pmProgressHandler: pmProgressHandler,
onSuccess: _onAssetUploaded,
onProgress: _onProgress,
@@ -273,14 +273,14 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
);
// User cancelled upload
if (!ok && state.cancelToken.isCancelled) {
if (!ok && _cancelToken == null) {
await _localNotificationService.showOrUpdateManualUploadStatus(
"backup_manual_title".tr(),
"backup_manual_cancelled".tr(),
presentBanner: true,
);
hasErrors = true;
} else if (state.successfulUploads == 0 || (!ok && !state.cancelToken.isCancelled)) {
} else if (state.successfulUploads == 0 || (!ok && _cancelToken != null)) {
await _localNotificationService.showOrUpdateManualUploadStatus(
"backup_manual_title".tr(),
"failed".tr(),
@@ -324,7 +324,8 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) {
_backupProvider.notifyBackgroundServiceCanRun();
}
state.cancelToken.cancel();
_cancelToken?.complete();
_cancelToken = null;
if (_backupProvider.backupProgress != BackUpProgressEnum.manualInProgress) {
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
}