remote_sync_service.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'dart:math';
  4. import 'package:flutter/foundation.dart';
  5. import 'package:logging/logging.dart';
  6. import 'package:photos/core/configuration.dart';
  7. import 'package:photos/core/errors.dart';
  8. import 'package:photos/core/event_bus.dart';
  9. import 'package:photos/db/device_files_db.dart';
  10. import 'package:photos/db/file_updation_db.dart';
  11. import 'package:photos/db/files_db.dart';
  12. import 'package:photos/events/collection_updated_event.dart';
  13. import 'package:photos/events/files_updated_event.dart';
  14. import 'package:photos/events/force_reload_home_gallery_event.dart';
  15. import 'package:photos/events/local_photos_updated_event.dart';
  16. import 'package:photos/events/sync_status_update_event.dart';
  17. import 'package:photos/models/device_collection.dart';
  18. import 'package:photos/models/file.dart';
  19. import 'package:photos/models/file_type.dart';
  20. import 'package:photos/services/app_lifecycle_service.dart';
  21. import 'package:photos/services/collections_service.dart';
  22. import 'package:photos/services/ignored_files_service.dart';
  23. import 'package:photos/services/local_file_update_service.dart';
  24. import 'package:photos/services/local_sync_service.dart';
  25. import 'package:photos/services/trash_sync_service.dart';
  26. import 'package:photos/utils/diff_fetcher.dart';
  27. import 'package:photos/utils/file_uploader.dart';
  28. import 'package:photos/utils/file_util.dart';
  29. import 'package:shared_preferences/shared_preferences.dart';
  30. class RemoteSyncService {
  31. final _logger = Logger("RemoteSyncService");
  32. final _db = FilesDB.instance;
  33. final _uploader = FileUploader.instance;
  34. final _collectionsService = CollectionsService.instance;
  35. final _diffFetcher = DiffFetcher();
  36. final LocalFileUpdateService _localFileUpdateService =
  37. LocalFileUpdateService.instance;
  38. int _completedUploads = 0;
  39. SharedPreferences _prefs;
  40. Completer<void> _existingSync;
  41. bool _existingSyncSilent = false;
  42. static const kHasSyncedArchiveKey = "has_synced_archive";
  43. // 28 Sept, 2021 9:03:20 AM IST
  44. static const kArchiveFeatureReleaseTime = 1632800000000000;
  45. static const kHasSyncedEditTime = "has_synced_edit_time";
  46. // 29 October, 2021 3:56:40 AM IST
  47. static const kEditTimeFeatureReleaseTime = 1635460000000000;
  48. static const kMaximumPermissibleUploadsInThrottledMode = 4;
  49. static final RemoteSyncService instance =
  50. RemoteSyncService._privateConstructor();
  51. RemoteSyncService._privateConstructor();
  52. Future<void> init() async {
  53. _prefs = await SharedPreferences.getInstance();
  54. Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) async {
  55. if (event.type == EventType.addedOrUpdated) {
  56. if (_existingSync == null) {
  57. sync();
  58. }
  59. }
  60. });
  61. }
  62. Future<void> sync({bool silently = false}) async {
  63. if (!Configuration.instance.hasConfiguredAccount()) {
  64. _logger.info("Skipping remote sync since account is not configured");
  65. return;
  66. }
  67. if (_existingSync != null) {
  68. _logger.info("Remote sync already in progress, skipping");
  69. // if current sync is silent but request sync is non-silent (demands UI
  70. // updates), update the syncSilently flag
  71. if (_existingSyncSilent == true && silently == false) {
  72. _existingSyncSilent = false;
  73. }
  74. return _existingSync.future;
  75. }
  76. _existingSync = Completer<void>();
  77. _existingSyncSilent = silently;
  78. try {
  79. await _pullDiff();
  80. // sync trash but consume error during initial launch.
  81. // this is to ensure that we don't pause upload due to any error during
  82. // the trash sync. Impact: We may end up re-uploading a file which was
  83. // recently trashed.
  84. await TrashSyncService.instance
  85. .syncTrash()
  86. .onError((e, s) => _logger.severe('trash sync failed', e, s));
  87. await _syncDeviceCollectionFilesForUpload();
  88. final filesToBeUploaded = await _getFilesToBeUploaded();
  89. if (kDebugMode) {
  90. debugPrint("Skip upload for testing");
  91. filesToBeUploaded.clear();
  92. }
  93. final hasUploadedFiles = await _uploadFiles(filesToBeUploaded);
  94. if (hasUploadedFiles) {
  95. await _pullDiff();
  96. _existingSync.complete();
  97. _existingSync = null;
  98. final hasMoreFilesToBackup = (await _getFilesToBeUploaded()).isNotEmpty;
  99. if (hasMoreFilesToBackup && !_shouldThrottleSync()) {
  100. // Skipping a resync to ensure that files that were ignored in this
  101. // session are not processed now
  102. sync();
  103. } else {
  104. Bus.instance.fire(SyncStatusUpdate(SyncStatus.completedBackup));
  105. }
  106. } else {
  107. _existingSync.complete();
  108. _existingSync = null;
  109. }
  110. } catch (e, s) {
  111. _existingSync.complete();
  112. _existingSync = null;
  113. // rethrow whitelisted error so that UI status can be updated correctly.
  114. if (e is UnauthorizedError ||
  115. e is NoActiveSubscriptionError ||
  116. e is WiFiUnavailableError ||
  117. e is StorageLimitExceededError ||
  118. e is SyncStopRequestedError) {
  119. _logger.warning("Error executing remote sync", e);
  120. rethrow;
  121. } else {
  122. _logger.severe("Error executing remote sync ", e, s);
  123. }
  124. } finally {
  125. _existingSyncSilent = false;
  126. }
  127. }
  128. Future<void> _pullDiff() async {
  129. final isFirstSync = !_collectionsService.hasSyncedCollections();
  130. await _collectionsService.sync();
  131. // check and reset user's collection syncTime in past for older clients
  132. if (isFirstSync) {
  133. // not need reset syncTime, mark all flags as done if firstSync
  134. await _markResetSyncTimeAsDone();
  135. } else if (_shouldResetSyncTime()) {
  136. _logger.warning('Resetting syncTime for for the client');
  137. await _resetAllCollectionsSyncTime();
  138. await _markResetSyncTimeAsDone();
  139. }
  140. await _syncUpdatedCollections();
  141. unawaited(_localFileUpdateService.markUpdatedFilesForReUpload());
  142. }
  143. Future<void> _syncUpdatedCollections() async {
  144. final updatedCollections =
  145. await _collectionsService.getCollectionsToBeSynced();
  146. for (final c in updatedCollections) {
  147. await _syncCollectionDiff(
  148. c.id,
  149. _collectionsService.getCollectionSyncTime(c.id),
  150. );
  151. await _collectionsService.setCollectionSyncTime(c.id, c.updationTime);
  152. }
  153. }
  154. Future<void> _resetAllCollectionsSyncTime() async {
  155. final resetSyncTime = _getSinceTimeForReSync();
  156. _logger.info('re-setting all collections syncTime to: $resetSyncTime');
  157. final collections = _collectionsService.getActiveCollections();
  158. for (final c in collections) {
  159. final int newSyncTime =
  160. min(_collectionsService.getCollectionSyncTime(c.id), resetSyncTime);
  161. await _collectionsService.setCollectionSyncTime(c.id, newSyncTime);
  162. }
  163. }
  164. Future<void> _syncCollectionDiff(int collectionID, int sinceTime) async {
  165. if (!_existingSyncSilent) {
  166. Bus.instance.fire(SyncStatusUpdate(SyncStatus.applyingRemoteDiff));
  167. }
  168. final diff =
  169. await _diffFetcher.getEncryptedFilesDiff(collectionID, sinceTime);
  170. if (diff.deletedFiles.isNotEmpty) {
  171. final fileIDs = diff.deletedFiles.map((f) => f.uploadedFileID).toList();
  172. final deletedFiles =
  173. (await FilesDB.instance.getFilesFromIDs(fileIDs)).values.toList();
  174. await FilesDB.instance.deleteFilesFromCollection(collectionID, fileIDs);
  175. Bus.instance.fire(
  176. CollectionUpdatedEvent(
  177. collectionID,
  178. deletedFiles,
  179. type: EventType.deletedFromRemote,
  180. ),
  181. );
  182. Bus.instance.fire(
  183. LocalPhotosUpdatedEvent(
  184. deletedFiles,
  185. type: EventType.deletedFromRemote,
  186. ),
  187. );
  188. }
  189. if (diff.updatedFiles.isNotEmpty) {
  190. await _storeDiff(diff.updatedFiles, collectionID);
  191. _logger.info(
  192. "Updated " +
  193. diff.updatedFiles.length.toString() +
  194. " files in collection " +
  195. collectionID.toString(),
  196. );
  197. Bus.instance.fire(LocalPhotosUpdatedEvent(diff.updatedFiles));
  198. Bus.instance
  199. .fire(CollectionUpdatedEvent(collectionID, diff.updatedFiles));
  200. }
  201. if (diff.latestUpdatedAtTime > 0) {
  202. await _collectionsService.setCollectionSyncTime(
  203. collectionID,
  204. diff.latestUpdatedAtTime,
  205. );
  206. }
  207. if (diff.hasMore) {
  208. return await _syncCollectionDiff(
  209. collectionID,
  210. _collectionsService.getCollectionSyncTime(collectionID),
  211. );
  212. }
  213. }
  214. Future<void> _syncDeviceCollectionFilesForUpload() async {
  215. final deviceCollections = await FilesDB.instance.getDeviceCollections();
  216. deviceCollections.removeWhere((element) => !element.shouldBackup);
  217. await _createCollectionsForDevicePath(deviceCollections);
  218. }
  219. Future<void> _createCollectionsForDevicePath(
  220. List<DeviceCollection> deviceCollections,
  221. ) async {
  222. for (var deviceCollection in deviceCollections) {
  223. int deviceCollectionID = deviceCollection.collectionID;
  224. if (deviceCollectionID != -1) {
  225. final collectionByID =
  226. CollectionsService.instance.getCollectionByID(deviceCollectionID);
  227. if (collectionByID == null || collectionByID.isDeleted) {
  228. _logger.info(
  229. "Collection $deviceCollectionID either deleted or missing "
  230. "for path ${deviceCollection.name}",
  231. );
  232. deviceCollectionID = -1;
  233. }
  234. }
  235. if (deviceCollectionID == -1) {
  236. final collection = await CollectionsService.instance
  237. .getOrCreateForPath(deviceCollection.name);
  238. await FilesDB.instance
  239. .updateDeviceCollection(deviceCollection.id, collection.id);
  240. deviceCollection.collectionID = collection.id;
  241. }
  242. }
  243. }
  244. Future<List<File>> _getFilesToBeUploaded() async {
  245. final deviceCollections = await FilesDB.instance.getDeviceCollections();
  246. deviceCollections.removeWhere((element) => !element.shouldBackup);
  247. final foldersToBackUp = Configuration.instance.getPathsToBackUp();
  248. List<File> filesToBeUploaded;
  249. if (LocalSyncService.instance.hasGrantedLimitedPermissions() &&
  250. foldersToBackUp.isEmpty) {
  251. filesToBeUploaded = await _db.getUnUploadedLocalFiles();
  252. } else {
  253. filesToBeUploaded =
  254. await _db.getFilesToBeUploadedWithinFolders(foldersToBackUp);
  255. }
  256. if (!Configuration.instance.shouldBackupVideos() || _shouldThrottleSync()) {
  257. filesToBeUploaded
  258. .removeWhere((element) => element.fileType == FileType.video);
  259. }
  260. if (filesToBeUploaded.isNotEmpty) {
  261. final int prevCount = filesToBeUploaded.length;
  262. final ignoredIDs = await IgnoredFilesService.instance.ignoredIDs;
  263. filesToBeUploaded.removeWhere(
  264. (file) =>
  265. IgnoredFilesService.instance.shouldSkipUpload(ignoredIDs, file),
  266. );
  267. if (prevCount != filesToBeUploaded.length) {
  268. _logger.info(
  269. (prevCount - filesToBeUploaded.length).toString() +
  270. " files were ignored for upload",
  271. );
  272. }
  273. }
  274. if (filesToBeUploaded.isEmpty) {
  275. // look for files which user manually tried to back up but they are not
  276. // uploaded yet. These files should ignore video backup & ignored files filter
  277. filesToBeUploaded = await _db.getPendingManualUploads();
  278. }
  279. _sortByTimeAndType(filesToBeUploaded);
  280. _logger.info(
  281. filesToBeUploaded.length.toString() + " new files to be uploaded.",
  282. );
  283. return filesToBeUploaded;
  284. }
  285. Future<bool> _uploadFiles(List<File> filesToBeUploaded) async {
  286. final updatedFileIDs = await _db.getUploadedFileIDsToBeUpdated();
  287. _logger.info(updatedFileIDs.length.toString() + " files updated.");
  288. final editedFiles = await _db.getEditedRemoteFiles();
  289. _logger.info(editedFiles.length.toString() + " files edited.");
  290. _completedUploads = 0;
  291. final int toBeUploaded =
  292. filesToBeUploaded.length + updatedFileIDs.length + editedFiles.length;
  293. if (toBeUploaded > 0) {
  294. Bus.instance.fire(SyncStatusUpdate(SyncStatus.preparingForUpload));
  295. // verify if files upload is allowed based on their subscription plan and
  296. // storage limit. To avoid creating new endpoint, we are using
  297. // fetchUploadUrls as alternative method.
  298. await _uploader.fetchUploadURLs(toBeUploaded);
  299. }
  300. final List<Future> futures = [];
  301. for (final uploadedFileID in updatedFileIDs) {
  302. if (_shouldThrottleSync() &&
  303. futures.length >= kMaximumPermissibleUploadsInThrottledMode) {
  304. _logger
  305. .info("Skipping some updated files as we are throttling uploads");
  306. break;
  307. }
  308. final file = await _db.getUploadedFileInAnyCollection(uploadedFileID);
  309. _uploadFile(file, file.collectionID, futures);
  310. }
  311. for (final file in filesToBeUploaded) {
  312. if (_shouldThrottleSync() &&
  313. futures.length >= kMaximumPermissibleUploadsInThrottledMode) {
  314. _logger.info("Skipping some new files as we are throttling uploads");
  315. break;
  316. }
  317. // prefer existing collection ID for manually uploaded files.
  318. // See https://github.com/ente-io/frame/pull/187
  319. final collectionID = file.collectionID ??
  320. (await CollectionsService.instance
  321. .getOrCreateForPath(file.deviceFolder))
  322. .id;
  323. _uploadFile(file, collectionID, futures);
  324. }
  325. for (final file in editedFiles) {
  326. if (_shouldThrottleSync() &&
  327. futures.length >= kMaximumPermissibleUploadsInThrottledMode) {
  328. _logger.info("Skipping some edited files as we are throttling uploads");
  329. break;
  330. }
  331. _uploadFile(file, file.collectionID, futures);
  332. }
  333. try {
  334. await Future.wait(futures);
  335. } on InvalidFileError {
  336. // Do nothing
  337. } on FileSystemException {
  338. // Do nothing since it's caused mostly due to concurrency issues
  339. // when the foreground app deletes temporary files, interrupting a background
  340. // upload
  341. } on LockAlreadyAcquiredError {
  342. // Do nothing
  343. } on SilentlyCancelUploadsError {
  344. // Do nothing
  345. } on UserCancelledUploadError {
  346. // Do nothing
  347. } catch (e) {
  348. rethrow;
  349. }
  350. return _completedUploads > 0;
  351. }
  352. void _uploadFile(File file, int collectionID, List<Future> futures) {
  353. final future = _uploader
  354. .upload(file, collectionID)
  355. .then((uploadedFile) => _onFileUploaded(uploadedFile));
  356. futures.add(future);
  357. }
  358. Future<void> _onFileUploaded(File file) async {
  359. Bus.instance.fire(CollectionUpdatedEvent(file.collectionID, [file]));
  360. _completedUploads++;
  361. final toBeUploadedInThisSession =
  362. FileUploader.instance.getCurrentSessionUploadCount();
  363. if (toBeUploadedInThisSession == 0) {
  364. return;
  365. }
  366. if (_completedUploads > toBeUploadedInThisSession ||
  367. _completedUploads < 0 ||
  368. toBeUploadedInThisSession < 0) {
  369. _logger.info(
  370. "Incorrect sync status",
  371. InvalidSyncStatusError(
  372. "Tried to report $_completedUploads as "
  373. "uploaded out of $toBeUploadedInThisSession",
  374. ),
  375. );
  376. return;
  377. }
  378. Bus.instance.fire(
  379. SyncStatusUpdate(
  380. SyncStatus.inProgress,
  381. completed: _completedUploads,
  382. total: toBeUploadedInThisSession,
  383. ),
  384. );
  385. }
  386. /* _storeDiff maps each remoteDiff file to existing
  387. entries in files table. When match is found, it compares both file to
  388. perform relevant actions like
  389. [1] Clear local cache when required (Both Shared and Owned files)
  390. [2] Retain localID of remote file based on matching logic [Owned files]
  391. [3] Refresh UI if visibility or creationTime has changed [Owned files]
  392. [4] Schedule file update if the local file has changed since last time
  393. [Owned files]
  394. [Important Note: If given uploadedFileID and collectionID is already present
  395. in files db, the generateID should already point to existing entry.
  396. Known Issues:
  397. [K1] Cached entry will not be cleared when if a file was edited and
  398. moved to different collection as Vid/Image cache key is uploadedID.
  399. [Existing]
  400. ]
  401. */
  402. Future _storeDiff(List<File> diff, int collectionID) async {
  403. int sharedFileNew = 0,
  404. sharedFileUpdated = 0,
  405. localUploadedFromDevice = 0,
  406. localButUpdatedOnDevice = 0,
  407. remoteNewFile = 0;
  408. final int userID = Configuration.instance.getUserID();
  409. bool needsGalleryReload = false;
  410. // this is required when same file is uploaded twice in the same
  411. // collection. Without this check, if both remote files are part of same
  412. // diff response, then we end up inserting one entry instead of two
  413. // as we update the generatedID for remoteDiff to local file's genID
  414. final Set<int> alreadyClaimedLocalFilesGenID = {};
  415. final List<File> toBeInserted = [];
  416. for (File remoteDiff in diff) {
  417. // existingFile will be either set to existing collectionID+localID or
  418. // to the unclaimed aka not already linked to any uploaded file.
  419. File existingFile;
  420. if (remoteDiff.generatedID != null) {
  421. // Case [1] Check and clear local cache when uploadedFile already exist
  422. existingFile = await _db.getFile(remoteDiff.generatedID);
  423. if (_shouldClearCache(remoteDiff, existingFile)) {
  424. await clearCache(remoteDiff);
  425. }
  426. }
  427. /* If file is not owned by the user, no further processing is required
  428. as Case [2,3,4] are only relevant to files owned by user
  429. */
  430. if (userID != remoteDiff.ownerID) {
  431. if (existingFile == null) {
  432. sharedFileNew++;
  433. remoteDiff.localID = null;
  434. } else {
  435. sharedFileUpdated++;
  436. // if user has downloaded the file on the device, avoid removing the
  437. // localID reference.
  438. // [Todo-fix: Excluded shared file's localIDs during syncALL]
  439. remoteDiff.localID = existingFile.localID;
  440. }
  441. toBeInserted.add(remoteDiff);
  442. // end processing for file here, move to next file now
  443. break;
  444. }
  445. // If remoteDiff is not already synced (i.e. existingFile is null), check
  446. // if the remoteFile was uploaded from this device.
  447. // Note: DeviceFolder is ignored for iOS during matching
  448. if (existingFile == null && remoteDiff.localID != null) {
  449. final localFileEntries = await _db.getUnlinkedLocalMatchesForRemoteFile(
  450. userID,
  451. remoteDiff.localID,
  452. remoteDiff.fileType,
  453. title: remoteDiff.title,
  454. deviceFolder: remoteDiff.deviceFolder,
  455. );
  456. if (localFileEntries.isEmpty) {
  457. // set remote file's localID as null because corresponding local file
  458. // does not exist [Case 2, do not retain localID of the remote file]
  459. remoteDiff.localID = null;
  460. } else {
  461. // case 4: Check and schedule the file for update
  462. final int maxModificationTime = localFileEntries
  463. .map(
  464. (e) => e.modificationTime ?? 0,
  465. )
  466. .reduce(max);
  467. if (maxModificationTime > remoteDiff.modificationTime) {
  468. localButUpdatedOnDevice++;
  469. await FileUpdationDB.instance.insertMultiple(
  470. [remoteDiff.localID],
  471. FileUpdationDB.modificationTimeUpdated,
  472. );
  473. }
  474. localFileEntries.removeWhere(
  475. (e) =>
  476. e.uploadedFileID != null ||
  477. alreadyClaimedLocalFilesGenID.contains(e.generatedID),
  478. );
  479. if (localFileEntries.isNotEmpty) {
  480. // file uploaded from same device, replace the local file row by
  481. // setting the generated ID of remoteFile to localFile generatedID
  482. existingFile = localFileEntries.first;
  483. localUploadedFromDevice++;
  484. alreadyClaimedLocalFilesGenID.add(existingFile.generatedID);
  485. remoteDiff.generatedID = existingFile.generatedID;
  486. }
  487. }
  488. }
  489. if (existingFile != null &&
  490. _shouldReloadHomeGallery(remoteDiff, existingFile)) {
  491. needsGalleryReload = true;
  492. } else {
  493. remoteNewFile++;
  494. }
  495. toBeInserted.add(remoteDiff);
  496. }
  497. await _db.insertMultiple(toBeInserted);
  498. _logger.info(
  499. "Diff to be deduplicated was: " +
  500. diff.length.toString() +
  501. " out of which \n" +
  502. localUploadedFromDevice.toString() +
  503. " was uploaded from device, \n" +
  504. localButUpdatedOnDevice.toString() +
  505. " was uploaded from device, but has been updated since and should be reuploaded, \n" +
  506. sharedFileNew.toString() +
  507. " new sharedFiles, \n" +
  508. sharedFileUpdated.toString() +
  509. " updatedSharedFiles, and \n" +
  510. remoteNewFile.toString() +
  511. " remoteFiles seen first time",
  512. );
  513. if (needsGalleryReload) {
  514. Bus.instance.fire(ForceReloadHomeGalleryEvent());
  515. }
  516. }
  517. bool _shouldClearCache(File remoteFile, File existingFile) {
  518. if (remoteFile.hash != null && existingFile.hash != null) {
  519. return remoteFile.hash != existingFile.hash;
  520. }
  521. return remoteFile.updationTime != (existingFile.updationTime ?? 0);
  522. }
  523. bool _shouldReloadHomeGallery(File remoteFile, File existingFile) {
  524. int remoteCreationTime = remoteFile.creationTime;
  525. if (remoteFile.pubMmdVersion > 0 &&
  526. (remoteFile.pubMagicMetadata.editedTime ?? 0) != 0) {
  527. remoteCreationTime = remoteFile.pubMagicMetadata.editedTime;
  528. }
  529. if (remoteCreationTime != existingFile.creationTime) {
  530. return true;
  531. }
  532. if (existingFile.mMdVersion > 0 &&
  533. remoteFile.mMdVersion != existingFile.mMdVersion &&
  534. remoteFile.magicMetadata.visibility !=
  535. existingFile.magicMetadata.visibility) {
  536. return false;
  537. }
  538. return false;
  539. }
  540. // return true if the client needs to re-sync the collections from previous
  541. // version
  542. bool _shouldResetSyncTime() {
  543. return !_prefs.containsKey(kHasSyncedEditTime) ||
  544. !_prefs.containsKey(kHasSyncedArchiveKey);
  545. }
  546. Future<void> _markResetSyncTimeAsDone() async {
  547. await _prefs.setBool(kHasSyncedArchiveKey, true);
  548. await _prefs.setBool(kHasSyncedEditTime, true);
  549. // Check to avoid regression because of change or additions of keys
  550. if (_shouldResetSyncTime()) {
  551. throw Exception("_shouldResetSyncTime should return false now");
  552. }
  553. }
  554. int _getSinceTimeForReSync() {
  555. // re-sync from archive feature time if the client still hasn't synced
  556. // since the feature release.
  557. if (!_prefs.containsKey(kHasSyncedArchiveKey)) {
  558. return kArchiveFeatureReleaseTime;
  559. }
  560. return kEditTimeFeatureReleaseTime;
  561. }
  562. bool _shouldThrottleSync() {
  563. return Platform.isIOS && !AppLifecycleService.instance.isForeground;
  564. }
  565. // _sortByTimeAndType moves videos to end and sort by creation time (desc).
  566. // This is done to upload most recent photo first.
  567. void _sortByTimeAndType(List<File> file) {
  568. file.sort((first, second) {
  569. if (first.fileType == second.fileType) {
  570. return second.creationTime.compareTo(first.creationTime);
  571. } else if (first.fileType == FileType.video) {
  572. return 1;
  573. } else {
  574. return -1;
  575. }
  576. });
  577. // move updated files towards the end
  578. file.sort((first, second) {
  579. if (first.updationTime == second.updationTime) {
  580. return 0;
  581. }
  582. if (first.updationTime == -1) {
  583. return 1;
  584. } else {
  585. return -1;
  586. }
  587. });
  588. }
  589. }