sync_service.dart 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'package:connectivity/connectivity.dart';
  4. import 'package:dio/dio.dart';
  5. import 'package:logging/logging.dart';
  6. import 'package:photo_manager/photo_manager.dart';
  7. import 'package:photos/core/configuration.dart';
  8. import 'package:photos/core/constants.dart';
  9. import 'package:photos/core/errors.dart';
  10. import 'package:photos/core/event_bus.dart';
  11. import 'package:photos/core/network.dart';
  12. import 'package:photos/db/files_db.dart';
  13. import 'package:photos/events/permission_granted_event.dart';
  14. import 'package:photos/events/subscription_purchased_event.dart';
  15. import 'package:photos/events/sync_status_update_event.dart';
  16. import 'package:photos/events/trigger_logout_event.dart';
  17. import 'package:photos/models/backup_status.dart';
  18. import 'package:photos/models/file_type.dart';
  19. import 'package:photos/services/local_sync_service.dart';
  20. import 'package:photos/services/notification_service.dart';
  21. import 'package:photos/services/remote_sync_service.dart';
  22. import 'package:photos/utils/file_uploader.dart';
  23. import 'package:shared_preferences/shared_preferences.dart';
  24. class SyncService {
  25. final _logger = Logger("SyncService");
  26. final _localSyncService = LocalSyncService.instance;
  27. final _remoteSyncService = RemoteSyncService.instance;
  28. final _dio = Network.instance.getDio();
  29. final _uploader = FileUploader.instance;
  30. bool _syncStopRequested = false;
  31. Completer<bool> _existingSync;
  32. SharedPreferences _prefs;
  33. SyncStatusUpdate _lastSyncStatusEvent;
  34. static const kLastStorageLimitExceededNotificationPushTime =
  35. "last_storage_limit_exceeded_notification_push_time";
  36. SyncService._privateConstructor() {
  37. Bus.instance.on<SubscriptionPurchasedEvent>().listen((event) {
  38. _uploader.clearQueue(SilentlyCancelUploadsError());
  39. sync();
  40. });
  41. Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
  42. _logger.info("Connectivity change detected " + result.toString());
  43. if (Configuration.instance.hasConfiguredAccount()) {
  44. sync();
  45. }
  46. });
  47. Bus.instance.on<SyncStatusUpdate>().listen((event) {
  48. _logger.info("Sync status received " + event.toString());
  49. _lastSyncStatusEvent = event;
  50. });
  51. }
  52. static final SyncService instance = SyncService._privateConstructor();
  53. Future<void> init() async {
  54. _prefs = await SharedPreferences.getInstance();
  55. if (Platform.isIOS) {
  56. _logger.info("Clearing file cache");
  57. await PhotoManager.clearFileCache();
  58. _logger.info("Cleared file cache");
  59. }
  60. if (LocalSyncService.instance.hasGrantedPermissions()) {
  61. LocalSyncService.instance.addChangeCallback(() => sync());
  62. }
  63. }
  64. Future<bool> existingSync() async {
  65. return _existingSync.future;
  66. }
  67. Future<bool> sync() async {
  68. _syncStopRequested = false;
  69. if (_existingSync != null) {
  70. _logger.warning("Sync already in progress, skipping.");
  71. return _existingSync.future;
  72. }
  73. _existingSync = Completer<bool>();
  74. bool successful = false;
  75. try {
  76. await _doSync();
  77. if (_lastSyncStatusEvent != null &&
  78. _lastSyncStatusEvent.status !=
  79. SyncStatus.completed_first_gallery_import &&
  80. _lastSyncStatusEvent.status != SyncStatus.completed_backup) {
  81. Bus.instance.fire(SyncStatusUpdate(SyncStatus.completed_backup));
  82. }
  83. successful = true;
  84. } on WiFiUnavailableError {
  85. _logger.warning("Not uploading over mobile data");
  86. Bus.instance.fire(
  87. SyncStatusUpdate(SyncStatus.paused, reason: "waiting for WiFi..."));
  88. } on SyncStopRequestedError {
  89. _syncStopRequested = false;
  90. Bus.instance.fire(
  91. SyncStatusUpdate(SyncStatus.completed_backup, wasStopped: true));
  92. } on NoActiveSubscriptionError {
  93. Bus.instance.fire(SyncStatusUpdate(SyncStatus.error,
  94. error: NoActiveSubscriptionError()));
  95. } on StorageLimitExceededError {
  96. _showStorageLimitExceededNotification();
  97. Bus.instance.fire(SyncStatusUpdate(SyncStatus.error,
  98. error: StorageLimitExceededError()));
  99. } on UnauthorizedError {
  100. _logger.info("Logging user out");
  101. Bus.instance.fire(TriggerLogoutEvent());
  102. } catch (e, s) {
  103. if (e is DioError) {
  104. if (e.type == DioErrorType.connectTimeout ||
  105. e.type == DioErrorType.sendTimeout ||
  106. e.type == DioErrorType.receiveTimeout ||
  107. e.type == DioErrorType.other) {
  108. Bus.instance.fire(SyncStatusUpdate(SyncStatus.paused,
  109. reason: "waiting for network..."));
  110. return false;
  111. }
  112. }
  113. _logger.severe("backup failed", e, s);
  114. Bus.instance.fire(SyncStatusUpdate(SyncStatus.error));
  115. rethrow;
  116. } finally {
  117. _existingSync.complete(successful);
  118. _existingSync = null;
  119. _lastSyncStatusEvent = null;
  120. _logger.info("Syncing completed");
  121. }
  122. return successful;
  123. }
  124. void stopSync() {
  125. _logger.info("Sync stop requested");
  126. _syncStopRequested = true;
  127. }
  128. bool shouldStopSync() {
  129. return _syncStopRequested;
  130. }
  131. bool isSyncInProgress() {
  132. return _existingSync != null;
  133. }
  134. SyncStatusUpdate getLastSyncStatusEvent() {
  135. return _lastSyncStatusEvent;
  136. }
  137. Future<void> onPermissionGranted(PermissionState state) async {
  138. _logger.info("Permission granted " + state.toString());
  139. await _localSyncService.onPermissionGranted(state);
  140. Bus.instance.fire(PermissionGrantedEvent());
  141. _doSync();
  142. LocalSyncService.instance.addChangeCallback(() => sync());
  143. }
  144. void onFoldersSet(Set<String> paths) {
  145. _uploader.removeFromQueueWhere((file) {
  146. return !paths.contains(file.deviceFolder);
  147. }, UserCancelledUploadError());
  148. }
  149. void onVideoBackupPaused() {
  150. _uploader.removeFromQueueWhere((file) {
  151. return file.fileType == FileType.video;
  152. }, UserCancelledUploadError());
  153. }
  154. Future<void> deleteFilesOnServer(List<int> fileIDs) async {
  155. return await _dio.post(
  156. Configuration.instance.getHttpEndpoint() + "/files/delete",
  157. options: Options(
  158. headers: {
  159. "X-Auth-Token": Configuration.instance.getToken(),
  160. },
  161. ),
  162. data: {
  163. "fileIDs": fileIDs,
  164. },
  165. );
  166. }
  167. Future<BackupStatus> getBackupStatus() async {
  168. final ids = await FilesDB.instance.getBackedUpIDs();
  169. final size = await _getFileSize(ids.uploadedIDs);
  170. return BackupStatus(ids.localIDs, size);
  171. }
  172. Future<int> _getFileSize(List<int> fileIDs) async {
  173. try {
  174. final response = await _dio.post(
  175. Configuration.instance.getHttpEndpoint() + "/files/size",
  176. options: Options(
  177. headers: {
  178. "X-Auth-Token": Configuration.instance.getToken(),
  179. },
  180. ),
  181. data: {
  182. "fileIDs": fileIDs,
  183. },
  184. );
  185. return response.data["size"];
  186. } catch (e) {
  187. _logger.severe(e);
  188. rethrow;
  189. }
  190. }
  191. Future<void> _doSync() async {
  192. await _localSyncService.sync();
  193. if (_localSyncService.hasCompletedFirstImport()) {
  194. await _remoteSyncService.sync();
  195. final shouldSync = await _localSyncService.syncAll();
  196. if (shouldSync) {
  197. await _remoteSyncService.sync();
  198. }
  199. }
  200. }
  201. void _showStorageLimitExceededNotification() async {
  202. final lastNotificationShownTime =
  203. _prefs.getInt(kLastStorageLimitExceededNotificationPushTime) ?? 0;
  204. final now = DateTime.now().microsecondsSinceEpoch;
  205. if ((now - lastNotificationShownTime) > kMicroSecondsInDay) {
  206. await _prefs.setInt(kLastStorageLimitExceededNotificationPushTime, now);
  207. NotificationService.instance.showNotification(
  208. "storage limit exceeded", "sorry, we had to pause your backups");
  209. }
  210. }
  211. }