backup.provider.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. import 'dart:io';
  2. import 'package:cancellation_token_http/http.dart';
  3. import 'package:flutter/widgets.dart';
  4. import 'package:hive_flutter/hive_flutter.dart';
  5. import 'package:hooks_riverpod/hooks_riverpod.dart';
  6. import 'package:immich_mobile/constants/hive_box.dart';
  7. import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
  8. import 'package:immich_mobile/modules/backup/models/backup_state.model.dart';
  9. import 'package:immich_mobile/modules/backup/models/current_upload_asset.model.dart';
  10. import 'package:immich_mobile/modules/backup/models/error_upload_asset.model.dart';
  11. import 'package:immich_mobile/modules/backup/models/hive_backup_albums.model.dart';
  12. import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model.dart';
  13. import 'package:immich_mobile/modules/backup/providers/error_backup_list.provider.dart';
  14. import 'package:immich_mobile/modules/backup/background_service/background.service.dart';
  15. import 'package:immich_mobile/modules/backup/services/backup.service.dart';
  16. import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
  17. import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
  18. import 'package:immich_mobile/shared/providers/app_state.provider.dart';
  19. import 'package:immich_mobile/shared/services/server_info.service.dart';
  20. import 'package:logging/logging.dart';
  21. import 'package:openapi/api.dart';
  22. import 'package:photo_manager/photo_manager.dart';
  23. class BackupNotifier extends StateNotifier<BackUpState> {
  24. BackupNotifier(
  25. this._backupService,
  26. this._serverInfoService,
  27. this._authState,
  28. this._backgroundService,
  29. this.ref,
  30. ) : super(
  31. BackUpState(
  32. backupProgress: BackUpProgressEnum.idle,
  33. allAssetsInDatabase: const [],
  34. progressInPercentage: 0,
  35. cancelToken: CancellationToken(),
  36. backgroundBackup: false,
  37. backupRequireWifi: true,
  38. backupRequireCharging: false,
  39. backupTriggerDelay: 5000,
  40. serverInfo: ServerInfoResponseDto(
  41. diskAvailable: "0",
  42. diskAvailableRaw: 0,
  43. diskSize: "0",
  44. diskSizeRaw: 0,
  45. diskUsagePercentage: 0,
  46. diskUse: "0",
  47. diskUseRaw: 0,
  48. ),
  49. availableAlbums: const [],
  50. selectedBackupAlbums: const {},
  51. excludedBackupAlbums: const {},
  52. allUniqueAssets: const {},
  53. selectedAlbumsBackupAssetsIds: const {},
  54. currentUploadAsset: CurrentUploadAsset(
  55. id: '...',
  56. createdAt: DateTime.parse('2020-10-04'),
  57. fileName: '...',
  58. fileType: '...',
  59. ),
  60. ),
  61. ) {
  62. getBackupInfo();
  63. }
  64. final log = Logger('BackupNotifier');
  65. final BackupService _backupService;
  66. final ServerInfoService _serverInfoService;
  67. final AuthenticationState _authState;
  68. final BackgroundService _backgroundService;
  69. final Ref ref;
  70. ///
  71. /// UI INTERACTION
  72. ///
  73. /// Album selection
  74. /// Due to the overlapping assets across multiple albums on the device
  75. /// We have method to include and exclude albums
  76. /// The total unique assets will be used for backing mechanism
  77. ///
  78. void addAlbumForBackup(AvailableAlbum album) {
  79. if (state.excludedBackupAlbums.contains(album)) {
  80. removeExcludedAlbumForBackup(album);
  81. }
  82. state = state
  83. .copyWith(selectedBackupAlbums: {...state.selectedBackupAlbums, album});
  84. _updateBackupAssetCount();
  85. }
  86. void addExcludedAlbumForBackup(AvailableAlbum album) {
  87. if (state.selectedBackupAlbums.contains(album)) {
  88. removeAlbumForBackup(album);
  89. }
  90. state = state
  91. .copyWith(excludedBackupAlbums: {...state.excludedBackupAlbums, album});
  92. _updateBackupAssetCount();
  93. }
  94. void removeAlbumForBackup(AvailableAlbum album) {
  95. Set<AvailableAlbum> currentSelectedAlbums = state.selectedBackupAlbums;
  96. currentSelectedAlbums.removeWhere((a) => a == album);
  97. state = state.copyWith(selectedBackupAlbums: currentSelectedAlbums);
  98. _updateBackupAssetCount();
  99. }
  100. void removeExcludedAlbumForBackup(AvailableAlbum album) {
  101. Set<AvailableAlbum> currentExcludedAlbums = state.excludedBackupAlbums;
  102. currentExcludedAlbums.removeWhere((a) => a == album);
  103. state = state.copyWith(excludedBackupAlbums: currentExcludedAlbums);
  104. _updateBackupAssetCount();
  105. }
  106. void configureBackgroundBackup({
  107. bool? enabled,
  108. bool? requireWifi,
  109. bool? requireCharging,
  110. int? triggerDelay,
  111. required void Function(String msg) onError,
  112. required void Function() onBatteryInfo,
  113. }) async {
  114. assert(
  115. enabled != null ||
  116. requireWifi != null ||
  117. requireCharging != null ||
  118. triggerDelay != null,
  119. );
  120. if (Platform.isAndroid) {
  121. final bool wasEnabled = state.backgroundBackup;
  122. final bool wasWifi = state.backupRequireWifi;
  123. final bool wasCharging = state.backupRequireCharging;
  124. final int oldTriggerDelay = state.backupTriggerDelay;
  125. state = state.copyWith(
  126. backgroundBackup: enabled,
  127. backupRequireWifi: requireWifi,
  128. backupRequireCharging: requireCharging,
  129. backupTriggerDelay: triggerDelay,
  130. );
  131. if (state.backgroundBackup) {
  132. bool success = true;
  133. if (!wasEnabled) {
  134. if (!await _backgroundService.isIgnoringBatteryOptimizations()) {
  135. onBatteryInfo();
  136. }
  137. success &= await _backgroundService.enableService(immediate: true);
  138. }
  139. success &= success &&
  140. await _backgroundService.configureService(
  141. requireUnmetered: state.backupRequireWifi,
  142. requireCharging: state.backupRequireCharging,
  143. triggerUpdateDelay: state.backupTriggerDelay,
  144. triggerMaxDelay: state.backupTriggerDelay * 10,
  145. );
  146. if (success) {
  147. final box = Hive.box(backgroundBackupInfoBox);
  148. await Future.wait([
  149. box.put(backupRequireWifi, state.backupRequireWifi),
  150. box.put(backupRequireCharging, state.backupRequireCharging),
  151. box.put(backupTriggerDelay, state.backupTriggerDelay),
  152. ]);
  153. } else {
  154. state = state.copyWith(
  155. backgroundBackup: wasEnabled,
  156. backupRequireWifi: wasWifi,
  157. backupRequireCharging: wasCharging,
  158. backupTriggerDelay: oldTriggerDelay,
  159. );
  160. onError("backup_controller_page_background_configure_error");
  161. }
  162. } else {
  163. final bool success = await _backgroundService.disableService();
  164. if (!success) {
  165. state = state.copyWith(backgroundBackup: wasEnabled);
  166. onError("backup_controller_page_background_configure_error");
  167. }
  168. }
  169. }
  170. }
  171. ///
  172. /// Get all album on the device
  173. /// Get all selected and excluded album from the user's persistent storage
  174. /// If this is the first time performing backup - set the default selected album to be
  175. /// the one that has all assets (`Recent` on Android, `Recents` on iOS)
  176. ///
  177. Future<void> _getBackupAlbumsInfo() async {
  178. Stopwatch stopwatch = Stopwatch()..start();
  179. // Get all albums on the device
  180. List<AvailableAlbum> availableAlbums = [];
  181. List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
  182. hasAll: true,
  183. type: RequestType.common,
  184. );
  185. log.info('Found ${albums.length} local albums');
  186. for (AssetPathEntity album in albums) {
  187. AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
  188. var assetCountInAlbum = await album.assetCountAsync;
  189. if (assetCountInAlbum > 0) {
  190. var assetList =
  191. await album.getAssetListRange(start: 0, end: assetCountInAlbum);
  192. if (assetList.isNotEmpty) {
  193. var thumbnailAsset = assetList.first;
  194. var thumbnailData = await thumbnailAsset
  195. .thumbnailDataWithSize(const ThumbnailSize(512, 512));
  196. availableAlbum =
  197. availableAlbum.copyWith(thumbnailData: thumbnailData);
  198. }
  199. availableAlbums.add(availableAlbum);
  200. }
  201. }
  202. state = state.copyWith(availableAlbums: availableAlbums);
  203. // Put persistent storage info into local state of the app
  204. // Get local storage on selected backup album
  205. Box<HiveBackupAlbums> backupAlbumInfoBox =
  206. Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
  207. HiveBackupAlbums? backupAlbumInfo = backupAlbumInfoBox.get(
  208. backupInfoKey,
  209. defaultValue: HiveBackupAlbums(
  210. selectedAlbumIds: [],
  211. excludedAlbumsIds: [],
  212. lastSelectedBackupTime: [],
  213. lastExcludedBackupTime: [],
  214. ),
  215. );
  216. if (backupAlbumInfo == null) {
  217. log.severe(
  218. "backupAlbumInfo == null",
  219. "Failed to get Hive backup album information",
  220. );
  221. return;
  222. }
  223. // First time backup - set isAll album is the default one for backup.
  224. if (backupAlbumInfo.selectedAlbumIds.isEmpty) {
  225. log.info("First time backup; setup 'Recent(s)' album as default");
  226. // Get album that contains all assets
  227. var list = await PhotoManager.getAssetPathList(
  228. hasAll: true,
  229. onlyAll: true,
  230. type: RequestType.common,
  231. );
  232. if (list.isEmpty) {
  233. return;
  234. }
  235. AssetPathEntity albumHasAllAssets = list.first;
  236. backupAlbumInfoBox.put(
  237. backupInfoKey,
  238. HiveBackupAlbums(
  239. selectedAlbumIds: [albumHasAllAssets.id],
  240. excludedAlbumsIds: [],
  241. lastSelectedBackupTime: [
  242. DateTime.fromMillisecondsSinceEpoch(0, isUtc: true)
  243. ],
  244. lastExcludedBackupTime: [],
  245. ),
  246. );
  247. backupAlbumInfo = backupAlbumInfoBox.get(backupInfoKey);
  248. }
  249. // Generate AssetPathEntity from id to add to local state
  250. try {
  251. Set<AvailableAlbum> selectedAlbums = {};
  252. for (var i = 0; i < backupAlbumInfo!.selectedAlbumIds.length; i++) {
  253. var albumAsset =
  254. await AssetPathEntity.fromId(backupAlbumInfo.selectedAlbumIds[i]);
  255. selectedAlbums.add(
  256. AvailableAlbum(
  257. albumEntity: albumAsset,
  258. lastBackup: backupAlbumInfo.lastSelectedBackupTime.length > i
  259. ? backupAlbumInfo.lastSelectedBackupTime[i]
  260. : DateTime.fromMillisecondsSinceEpoch(0, isUtc: true),
  261. ),
  262. );
  263. }
  264. Set<AvailableAlbum> excludedAlbums = {};
  265. for (var i = 0; i < backupAlbumInfo.excludedAlbumsIds.length; i++) {
  266. var albumAsset =
  267. await AssetPathEntity.fromId(backupAlbumInfo.excludedAlbumsIds[i]);
  268. excludedAlbums.add(
  269. AvailableAlbum(
  270. albumEntity: albumAsset,
  271. lastBackup: backupAlbumInfo.lastExcludedBackupTime.length > i
  272. ? backupAlbumInfo.lastExcludedBackupTime[i]
  273. : DateTime.fromMillisecondsSinceEpoch(0, isUtc: true),
  274. ),
  275. );
  276. }
  277. state = state.copyWith(
  278. selectedBackupAlbums: selectedAlbums,
  279. excludedBackupAlbums: excludedAlbums,
  280. );
  281. } catch (e, stackTrace) {
  282. log.severe("Failed to generate album from id", e, stackTrace);
  283. }
  284. debugPrint("_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms");
  285. }
  286. ///
  287. /// From all the selected and albums assets
  288. /// Find the assets that are not overlapping between the two sets
  289. /// Those assets are unique and are used as the total assets
  290. ///
  291. Future<void> _updateBackupAssetCount() async {
  292. Set<String> duplicatedAssetIds = _backupService.getDuplicatedAssetIds();
  293. Set<AssetEntity> assetsFromSelectedAlbums = {};
  294. Set<AssetEntity> assetsFromExcludedAlbums = {};
  295. for (var album in state.selectedBackupAlbums) {
  296. var assets = await album.albumEntity.getAssetListRange(
  297. start: 0,
  298. end: await album.albumEntity.assetCountAsync,
  299. );
  300. assetsFromSelectedAlbums.addAll(assets);
  301. }
  302. for (var album in state.excludedBackupAlbums) {
  303. var assets = await album.albumEntity.getAssetListRange(
  304. start: 0,
  305. end: await album.albumEntity.assetCountAsync,
  306. );
  307. assetsFromExcludedAlbums.addAll(assets);
  308. }
  309. Set<AssetEntity> allUniqueAssets =
  310. assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
  311. var allAssetsInDatabase = await _backupService.getDeviceBackupAsset();
  312. if (allAssetsInDatabase == null) {
  313. return;
  314. }
  315. // Find asset that were backup from selected albums
  316. Set<String> selectedAlbumsBackupAssets =
  317. Set.from(allUniqueAssets.map((e) => e.id));
  318. selectedAlbumsBackupAssets
  319. .removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
  320. // Remove duplicated asset from all unique assets
  321. allUniqueAssets.removeWhere(
  322. (asset) => duplicatedAssetIds.contains(asset.id),
  323. );
  324. if (allUniqueAssets.isEmpty) {
  325. log.info("Not found albums or assets on the device to backup");
  326. state = state.copyWith(
  327. backupProgress: BackUpProgressEnum.idle,
  328. allAssetsInDatabase: allAssetsInDatabase,
  329. allUniqueAssets: {},
  330. selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
  331. );
  332. return;
  333. } else {
  334. state = state.copyWith(
  335. allAssetsInDatabase: allAssetsInDatabase,
  336. allUniqueAssets: allUniqueAssets,
  337. selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
  338. );
  339. }
  340. // Save to persistent storage
  341. _updatePersistentAlbumsSelection();
  342. return;
  343. }
  344. /// Get all necessary information for calculating the available albums,
  345. /// which albums are selected or excluded
  346. /// and then update the UI according to those information
  347. Future<void> getBackupInfo() async {
  348. var isEnabled = await _backgroundService.isBackgroundBackupEnabled();
  349. state = state.copyWith(backgroundBackup: isEnabled);
  350. if (state.backupProgress != BackUpProgressEnum.inBackground) {
  351. await _getBackupAlbumsInfo();
  352. await _updateServerInfo();
  353. await _updateBackupAssetCount();
  354. }
  355. }
  356. /// Save user selection of selected albums and excluded albums to
  357. /// Hive database
  358. void _updatePersistentAlbumsSelection() {
  359. final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
  360. Box<HiveBackupAlbums> backupAlbumInfoBox =
  361. Hive.box<HiveBackupAlbums>(hiveBackupInfoBox);
  362. backupAlbumInfoBox.put(
  363. backupInfoKey,
  364. HiveBackupAlbums(
  365. selectedAlbumIds: state.selectedBackupAlbums.map((e) => e.id).toList(),
  366. excludedAlbumsIds: state.excludedBackupAlbums.map((e) => e.id).toList(),
  367. lastSelectedBackupTime: state.selectedBackupAlbums
  368. .map((e) => e.lastBackup ?? epoch)
  369. .toList(),
  370. lastExcludedBackupTime: state.excludedBackupAlbums
  371. .map((e) => e.lastBackup ?? epoch)
  372. .toList(),
  373. ),
  374. );
  375. }
  376. /// Invoke backup process
  377. Future<void> startBackupProcess() async {
  378. debugPrint("Start backup process");
  379. assert(state.backupProgress == BackUpProgressEnum.idle);
  380. state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
  381. await getBackupInfo();
  382. var authResult = await PhotoManager.requestPermissionExtend();
  383. if (authResult.isAuth) {
  384. await PhotoManager.clearFileCache();
  385. if (state.allUniqueAssets.isEmpty) {
  386. log.info("No Asset On Device - Abort Backup Process");
  387. state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
  388. return;
  389. }
  390. Set<AssetEntity> assetsWillBeBackup = Set.from(state.allUniqueAssets);
  391. // Remove item that has already been backed up
  392. for (var assetId in state.allAssetsInDatabase) {
  393. assetsWillBeBackup.removeWhere((e) => e.id == assetId);
  394. }
  395. if (assetsWillBeBackup.isEmpty) {
  396. state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
  397. }
  398. // Perform Backup
  399. state = state.copyWith(cancelToken: CancellationToken());
  400. await _backupService.backupAsset(
  401. assetsWillBeBackup,
  402. state.cancelToken,
  403. _onAssetUploaded,
  404. _onUploadProgress,
  405. _onSetCurrentBackupAsset,
  406. _onBackupError,
  407. );
  408. await _notifyBackgroundServiceCanRun();
  409. } else {
  410. PhotoManager.openSetting();
  411. }
  412. }
  413. void _onBackupError(ErrorUploadAsset errorAssetInfo) {
  414. ref.watch(errorBackupListProvider.notifier).add(errorAssetInfo);
  415. }
  416. void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
  417. state = state.copyWith(currentUploadAsset: currentUploadAsset);
  418. }
  419. void cancelBackup() {
  420. if (state.backupProgress != BackUpProgressEnum.inProgress) {
  421. _notifyBackgroundServiceCanRun();
  422. }
  423. state.cancelToken.cancel();
  424. state = state.copyWith(
  425. backupProgress: BackUpProgressEnum.idle,
  426. progressInPercentage: 0.0,
  427. );
  428. }
  429. void _onAssetUploaded(
  430. String deviceAssetId,
  431. String deviceId,
  432. bool isDuplicated,
  433. ) {
  434. if (isDuplicated) {
  435. state = state.copyWith(
  436. allUniqueAssets: state.allUniqueAssets
  437. .where((asset) => asset.id != deviceAssetId)
  438. .toSet(),
  439. );
  440. } else {
  441. state = state.copyWith(
  442. selectedAlbumsBackupAssetsIds: {
  443. ...state.selectedAlbumsBackupAssetsIds,
  444. deviceAssetId
  445. },
  446. allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId],
  447. );
  448. }
  449. if (state.allUniqueAssets.length -
  450. state.selectedAlbumsBackupAssetsIds.length ==
  451. 0) {
  452. final latestAssetBackup =
  453. state.allUniqueAssets.map((e) => e.modifiedDateTime).reduce(
  454. (v, e) => e.isAfter(v) ? e : v,
  455. );
  456. state = state.copyWith(
  457. selectedBackupAlbums: state.selectedBackupAlbums
  458. .map((e) => e.copyWith(lastBackup: latestAssetBackup))
  459. .toSet(),
  460. excludedBackupAlbums: state.excludedBackupAlbums
  461. .map((e) => e.copyWith(lastBackup: latestAssetBackup))
  462. .toSet(),
  463. backupProgress: BackUpProgressEnum.done,
  464. progressInPercentage: 0.0,
  465. );
  466. _updatePersistentAlbumsSelection();
  467. }
  468. _updateServerInfo();
  469. }
  470. void _onUploadProgress(int sent, int total) {
  471. state = state.copyWith(
  472. progressInPercentage: (sent.toDouble() / total.toDouble() * 100),
  473. );
  474. }
  475. Future<void> _updateServerInfo() async {
  476. var serverInfo = await _serverInfoService.getServerInfo();
  477. // Update server info
  478. if (serverInfo != null) {
  479. state = state.copyWith(
  480. serverInfo: serverInfo,
  481. );
  482. }
  483. }
  484. Future<void> _resumeBackup() async {
  485. // Check if user is login
  486. var accessKey = Hive.box(userInfoBox).get(accessTokenKey);
  487. // User has been logged out return
  488. if (accessKey == null || !_authState.isAuthenticated) {
  489. log.info("[_resumeBackup] not authenticated - abort");
  490. return;
  491. }
  492. // Check if this device is enable backup by the user
  493. if ((_authState.deviceInfo.deviceId == _authState.deviceId) &&
  494. _authState.deviceInfo.isAutoBackup) {
  495. // check if backup is alreayd in process - then return
  496. if (state.backupProgress == BackUpProgressEnum.inProgress) {
  497. log.info("[_resumeBackup] Backup is already in progress - abort");
  498. return;
  499. }
  500. if (state.backupProgress == BackUpProgressEnum.inBackground) {
  501. log.info("[_resumeBackup] Background backup is running - abort");
  502. return;
  503. }
  504. // Run backup
  505. log.info("[_resumeBackup] Start back up");
  506. await startBackupProcess();
  507. }
  508. return;
  509. }
  510. Future<void> resumeBackup() async {
  511. if (Platform.isAndroid) {
  512. // assumes the background service is currently running
  513. // if true, waits until it has stopped to update the app state from HiveDB
  514. // before actually resuming backup by calling the internal `_resumeBackup`
  515. final BackUpProgressEnum previous = state.backupProgress;
  516. state = state.copyWith(backupProgress: BackUpProgressEnum.inBackground);
  517. final bool hasLock = await _backgroundService.acquireLock();
  518. if (!hasLock) {
  519. log.warning("WARNING [resumeBackup] failed to acquireLock");
  520. return;
  521. }
  522. await Future.wait([
  523. Hive.openBox<HiveBackupAlbums>(hiveBackupInfoBox),
  524. Hive.openBox<HiveDuplicatedAssets>(duplicatedAssetsBox),
  525. Hive.openBox(backgroundBackupInfoBox),
  526. ]);
  527. final HiveBackupAlbums? albums =
  528. Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).get(backupInfoKey);
  529. Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
  530. Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
  531. if (albums != null) {
  532. selectedAlbums = _updateAlbumsBackupTime(
  533. selectedAlbums,
  534. albums.selectedAlbumIds,
  535. albums.lastSelectedBackupTime,
  536. );
  537. excludedAlbums = _updateAlbumsBackupTime(
  538. excludedAlbums,
  539. albums.excludedAlbumsIds,
  540. albums.lastExcludedBackupTime,
  541. );
  542. }
  543. final Box backgroundBox = Hive.box(backgroundBackupInfoBox);
  544. state = state.copyWith(
  545. backupProgress: previous,
  546. selectedBackupAlbums: selectedAlbums,
  547. excludedBackupAlbums: excludedAlbums,
  548. backupRequireWifi: backgroundBox.get(backupRequireWifi),
  549. backupRequireCharging: backgroundBox.get(backupRequireCharging),
  550. backupTriggerDelay: backgroundBox.get(backupTriggerDelay),
  551. );
  552. }
  553. return _resumeBackup();
  554. }
  555. Set<AvailableAlbum> _updateAlbumsBackupTime(
  556. Set<AvailableAlbum> albums,
  557. List<String> ids,
  558. List<DateTime> times,
  559. ) {
  560. Set<AvailableAlbum> result = {};
  561. for (int i = 0; i < ids.length; i++) {
  562. try {
  563. AvailableAlbum a = albums.firstWhere((e) => e.id == ids[i]);
  564. result.add(a.copyWith(lastBackup: times[i]));
  565. } on StateError {
  566. log.severe(
  567. "[_updateAlbumBackupTime] failed to find album in state",
  568. "State Error",
  569. StackTrace.current,
  570. );
  571. }
  572. }
  573. return result;
  574. }
  575. Future<void> _notifyBackgroundServiceCanRun() async {
  576. const allowedStates = [
  577. AppStateEnum.inactive,
  578. AppStateEnum.paused,
  579. AppStateEnum.detached,
  580. ];
  581. if (Platform.isAndroid &&
  582. allowedStates.contains(ref.read(appStateProvider.notifier).state)) {
  583. try {
  584. if (Hive.isBoxOpen(hiveBackupInfoBox)) {
  585. await Hive.box<HiveBackupAlbums>(hiveBackupInfoBox).close();
  586. }
  587. } catch (error) {
  588. log.info("[_notifyBackgroundServiceCanRun] failed to close box");
  589. }
  590. try {
  591. if (Hive.isBoxOpen(duplicatedAssetsBox)) {
  592. await Hive.box<HiveDuplicatedAssets>(duplicatedAssetsBox).close();
  593. }
  594. } catch (error, stackTrace) {
  595. log.severe(
  596. "[_notifyBackgroundServiceCanRun] failed to close box",
  597. error,
  598. stackTrace,
  599. );
  600. }
  601. try {
  602. if (Hive.isBoxOpen(backgroundBackupInfoBox)) {
  603. await Hive.box(backgroundBackupInfoBox).close();
  604. }
  605. } catch (error, stackTrace) {
  606. log.severe(
  607. "[_notifyBackgroundServiceCanRun] failed to close box",
  608. error,
  609. stackTrace,
  610. );
  611. }
  612. _backgroundService.releaseLock();
  613. }
  614. }
  615. }
  616. final backupProvider =
  617. StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
  618. return BackupNotifier(
  619. ref.watch(backupServiceProvider),
  620. ref.watch(serverInfoServiceProvider),
  621. ref.watch(authenticationProvider),
  622. ref.watch(backgroundServiceProvider),
  623. ref,
  624. );
  625. });