Browse Source

Sync only when WiFi is available

Vishnu Mohandas 4 years ago
parent
commit
1947970b0a

+ 20 - 1
lib/core/configuration.dart

@@ -9,6 +9,7 @@ import 'package:path_provider/path_provider.dart';
 import 'package:photos/models/key_attributes.dart';
 import 'package:photos/models/key_gen_result.dart';
 import 'package:photos/models/private_key_attributes.dart';
+import 'package:photos/services/sync_service.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 
 import 'package:photos/utils/crypto_util.dart';
@@ -27,6 +28,7 @@ class Configuration {
   static const keyKey = "key";
   static const secretKeyKey = "secret_key";
   static const keyAttributesKey = "key_attributes";
+  static const keyShouldBackupOverMobileData = "should_backup_over_mobile_data";
 
   SharedPreferences _preferences;
   FlutterSecureStorage _secureStorage;
@@ -42,7 +44,9 @@ class Configuration {
     _documentsDirectory = (await getApplicationDocumentsDirectory()).path;
     _tempDirectory = _documentsDirectory + "/temp/";
     final tempDirectory = new io.Directory(_tempDirectory);
-    tempDirectory.deleteSync(recursive: true);
+    if (tempDirectory.existsSync()) {
+      tempDirectory.deleteSync(recursive: true);
+    }
     tempDirectory.createSync(recursive: true);
     _key = await _secureStorage.read(key: keyKey);
     _secretKey = await _secureStorage.read(key: secretKeyKey);
@@ -234,4 +238,19 @@ class Configuration {
   bool hasConfiguredAccount() {
     return getToken() != null && _key != null;
   }
+
+  bool shouldBackupOverMobileData() {
+    if (_preferences.containsKey(keyShouldBackupOverMobileData)) {
+      return _preferences.getBool(keyShouldBackupOverMobileData);
+    } else {
+      return false;
+    }
+  }
+
+  Future<void> setBackupOverMobileData(bool value) async {
+    await _preferences.setBool(keyShouldBackupOverMobileData, value);
+    if (value) {
+      SyncService.instance.sync();
+    }
+  }
 }

+ 3 - 0
lib/events/photo_upload_event.dart → lib/events/sync_status_update_event.dart

@@ -5,18 +5,21 @@ class SyncStatusUpdate extends Event {
   final int total;
   final bool wasStopped;
   final SyncStatus status;
+  final String reason;
 
   SyncStatusUpdate(
     this.status, {
     this.completed,
     this.total,
     this.wasStopped = false,
+    this.reason = "",
   });
 }
 
 enum SyncStatus {
   not_started,
   in_progress,
+  paused,
   completed,
   error,
 }

+ 36 - 17
lib/services/sync_service.dart

@@ -1,10 +1,11 @@
 import 'dart:async';
+import 'package:connectivity/connectivity.dart';
 import 'package:flutter/foundation.dart';
 import 'package:logging/logging.dart';
 import 'package:photos/core/event_bus.dart';
 import 'package:photos/db/files_db.dart';
 import 'package:photos/events/collection_updated_event.dart';
-import 'package:photos/events/photo_upload_event.dart';
+import 'package:photos/events/sync_status_update_event.dart';
 import 'package:photos/events/user_authenticated_event.dart';
 import 'package:photos/models/file_type.dart';
 import 'package:photos/services/collections_service.dart';
@@ -32,6 +33,7 @@ class SyncService {
   bool _syncStopRequested = false;
   Future<void> _existingSync;
   SharedPreferences _prefs;
+  SyncStatusUpdate _lastSyncStatusEvent;
 
   static final _collectionSyncTimeKeyPrefix = "collection_sync_time_";
   static final _dbUpdationTimeKey = "db_updation_time";
@@ -41,6 +43,15 @@ class SyncService {
     Bus.instance.on<UserAuthenticatedEvent>().listen((event) {
       sync();
     });
+
+    Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
+      _logger.info("Connectivity change detected " + result.toString());
+      sync();
+    });
+
+    Bus.instance.on<SyncStatusUpdate>().listen((event) {
+      _lastSyncStatusEvent = event;
+    });
   }
 
   static final SyncService instance = SyncService._privateConstructor();
@@ -61,11 +72,16 @@ class SyncService {
       _logger.info("Syncing...");
       try {
         await _doSync();
+        Bus.instance.fire(SyncStatusUpdate(SyncStatus.completed));
+      } on WiFiUnavailableError {
+        _logger.warning("Not uploading over mobile data");
+        Bus.instance.fire(
+            SyncStatusUpdate(SyncStatus.paused, reason: "Waiting for WiFi..."));
       } catch (e, s) {
         _logger.severe(e, s);
+        Bus.instance.fire(SyncStatusUpdate(SyncStatus.error));
       } finally {
         _isSyncInProgress = false;
-        Bus.instance.fire(SyncStatusUpdate(SyncStatus.error));
       }
     });
     return _existingSync;
@@ -88,6 +104,10 @@ class SyncService {
     return _isSyncInProgress;
   }
 
+  SyncStatusUpdate getLastSyncStatusEvent() {
+    return _lastSyncStatusEvent;
+  }
+
   Future<void> _doSync() async {
     final result = await PhotoManager.requestPermission();
     if (!result) {
@@ -189,24 +209,23 @@ class SyncService {
         return;
       }
       File file = filesToBeUploaded[i];
-      try {
-        final collectionID = (await CollectionsService.instance
-                .getOrCreateForPath(file.deviceFolder))
-            .id;
-        final future = _uploader.upload(file, collectionID).then((value) {
-          Bus.instance
-              .fire(CollectionUpdatedEvent(collectionID: file.collectionID));
-          Bus.instance.fire(SyncStatusUpdate(SyncStatus.in_progress,
-              completed: i + 1, total: filesToBeUploaded.length));
-        });
-        futures.add(future);
-      } catch (e, s) {
-        Bus.instance.fire(SyncStatusUpdate(SyncStatus.error));
-        _logger.severe(e, s);
-      }
+      final collectionID = (await CollectionsService.instance
+              .getOrCreateForPath(file.deviceFolder))
+          .id;
+      final future = _uploader.upload(file, collectionID).then((value) {
+        Bus.instance
+            .fire(CollectionUpdatedEvent(collectionID: file.collectionID));
+        Bus.instance.fire(SyncStatusUpdate(SyncStatus.in_progress,
+            completed: i + 1, total: filesToBeUploaded.length));
+      });
+      futures.add(future);
     }
     try {
       await Future.wait(futures);
+    } on InvalidFileError {
+      // Do nothing
+    } on WiFiUnavailableError {
+      throw WiFiUnavailableError();
     } catch (e, s) {
       _isSyncInProgress = false;
       Bus.instance.fire(SyncStatusUpdate(SyncStatus.error));

+ 15 - 0
lib/ui/settings_page.dart

@@ -89,6 +89,21 @@ class UsageWidgetState extends State<UsageWidget> {
                     ),
             ],
           ),
+          Divider(height: 4),
+          Padding(padding: EdgeInsets.all(4)),
+          Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              Text("Backup over mobile data"),
+              Switch(
+                value: Configuration.instance.shouldBackupOverMobileData(),
+                onChanged: (value) async {
+                  Configuration.instance.setBackupOverMobileData(value);
+                  setState(() {});
+                },
+              ),
+            ],
+          ),
         ],
       ),
     );

+ 46 - 15
lib/ui/sync_indicator.dart

@@ -3,7 +3,7 @@ import 'dart:async';
 import 'package:flutter/material.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/event_bus.dart';
-import 'package:photos/events/photo_upload_event.dart';
+import 'package:photos/events/sync_status_update_event.dart';
 import 'package:photos/services/sync_service.dart';
 
 class SyncIndicator extends StatefulWidget {
@@ -15,6 +15,7 @@ class SyncIndicator extends StatefulWidget {
 
 class _SyncIndicatorState extends State<SyncIndicator> {
   SyncStatusUpdate _event;
+  double _containerHeight = 48;
   int _latestCompletedCount = 0;
   StreamSubscription<SyncStatusUpdate> _subscription;
 
@@ -29,6 +30,7 @@ class _SyncIndicatorState extends State<SyncIndicator> {
         }
       });
     });
+    _event = SyncService.instance.getLastSyncStatusEvent();
     super.initState();
   }
 
@@ -40,13 +42,38 @@ class _SyncIndicatorState extends State<SyncIndicator> {
 
   @override
   Widget build(BuildContext context) {
-    if (Configuration.instance.hasConfiguredAccount()) {
-      if (SyncService.instance.isSyncInProgress()) {
-        return Container(
-          height: 48,
-          width: double.infinity,
-          margin: EdgeInsets.all(8),
-          alignment: Alignment.center,
+    if (Configuration.instance.hasConfiguredAccount() && _event != null) {
+      if (_event.status == SyncStatus.completed) {
+        Future.delayed(Duration(milliseconds: 5000), () {
+          setState(() {
+            _containerHeight = 0;
+          });
+        });
+      } else {
+        _containerHeight = 48;
+      }
+      var icon;
+      if (_event.status == SyncStatus.completed) {
+        icon = Icon(
+          Icons.cloud_done_outlined,
+          color: Theme.of(context).accentColor,
+        );
+      } else if (_event.status == SyncStatus.error) {
+        icon = Icon(
+          Icons.error_outline,
+          color: Theme.of(context).accentColor,
+        );
+      } else {
+        icon = CircularProgressIndicator(strokeWidth: 2);
+      }
+      return AnimatedContainer(
+        duration: Duration(milliseconds: 300),
+        height: _containerHeight,
+        width: double.infinity,
+        margin: EdgeInsets.all(8),
+        alignment: Alignment.center,
+        child: SingleChildScrollView(
+          physics: NeverScrollableScrollPhysics(),
           child: Column(
             mainAxisAlignment: MainAxisAlignment.center,
             crossAxisAlignment: CrossAxisAlignment.center,
@@ -58,7 +85,7 @@ class _SyncIndicatorState extends State<SyncIndicator> {
                   Container(
                     width: 24,
                     height: 24,
-                    child: CircularProgressIndicator(strokeWidth: 2),
+                    child: icon,
                   ),
                   Padding(
                     padding: const EdgeInsets.fromLTRB(8, 4, 0, 0),
@@ -70,8 +97,8 @@ class _SyncIndicatorState extends State<SyncIndicator> {
               Divider(),
             ],
           ),
-        );
-      }
+        ),
+      );
     }
     return Container();
   }
@@ -81,18 +108,22 @@ class _SyncIndicatorState extends State<SyncIndicator> {
       return "Syncing...";
     } else {
       var s;
-      // TODO: Display errors softly
       if (_event.status == SyncStatus.error) {
         s = "Upload failed.";
-      } else if (_event.status == SyncStatus.completed && _event.wasStopped) {
-        s = "Sync stopped.";
+      } else if (_event.status == SyncStatus.completed) {
+        if (_event.wasStopped) {
+          s = "Sync stopped.";
+        } else {
+          s = "All memories preserved.";
+        }
+      } else if (_event.status == SyncStatus.paused) {
+        s = _event.reason;
       } else {
         s = _latestCompletedCount.toString() +
             "/" +
             _event.total.toString() +
             " memories preserved";
       }
-      _event = null;
       return s;
     }
   }

+ 26 - 8
lib/utils/file_uploader.dart

@@ -2,6 +2,7 @@ import 'dart:async';
 import 'dart:collection';
 import 'dart:convert';
 import 'dart:io' as io;
+import 'package:connectivity/connectivity.dart';
 import 'package:dio/dio.dart';
 import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:logging/logging.dart';
@@ -97,11 +98,14 @@ class FileUploader {
   void _pollQueue() {
     if (_queue.length > 0 && _currentlyUploading < _maximumConcurrentUploads) {
       final firstPendingEntry = _queue.entries
-          .firstWhere((entry) => entry.value.status == UploadStatus.not_started)
-          .value;
-      firstPendingEntry.status = UploadStatus.in_progress;
-      _encryptAndUploadFileToCollection(
-          firstPendingEntry.file, firstPendingEntry.collectionID);
+          .firstWhere((entry) => entry.value.status == UploadStatus.not_started,
+              orElse: () => null)
+          ?.value;
+      if (firstPendingEntry != null) {
+        firstPendingEntry.status = UploadStatus.in_progress;
+        _encryptAndUploadFileToCollection(
+            firstPendingEntry.file, firstPendingEntry.collectionID);
+      }
     }
   }
 
@@ -132,6 +136,12 @@ class FileUploader {
 
   Future<File> _tryToUpload(
       File file, int collectionID, bool forcedUpload) async {
+    final connectivityResult = await (Connectivity().checkConnectivity());
+    if (connectivityResult != ConnectivityResult.wifi &&
+        !Configuration.instance.shouldBackupOverMobileData()) {
+      throw WiFiUnavailableError();
+    }
+
     final encryptedFileName = file.generatedID.toString() + ".encrypted";
     final tempDirectory = Configuration.instance.getTempDirectory();
     final encryptedFilePath = tempDirectory + encryptedFileName;
@@ -141,14 +151,15 @@ class FileUploader {
     final fileAttributes =
         await CryptoUtil.encryptFile(sourceFile.path, encryptedFilePath);
 
-    final fileUploadURL = await _getUploadURL();
-    String fileObjectKey = await _putFile(fileUploadURL, encryptedFile);
-
     final thumbnailData = (await (await file.getAsset()).thumbDataWithSize(
       THUMBNAIL_LARGE_SIZE,
       THUMBNAIL_LARGE_SIZE,
       quality: 50,
     ));
+    if (thumbnailData == null) {
+      _logger.severe("Could not generate thumbnail for " + file.toString());
+      throw InvalidFileError();
+    }
     final encryptedThumbnailName =
         file.generatedID.toString() + "_thumbnail.encrypted";
     final encryptedThumbnailPath = tempDirectory + encryptedThumbnailName;
@@ -157,6 +168,9 @@ class FileUploader {
     final encryptedThumbnail = io.File(encryptedThumbnailPath);
     encryptedThumbnail.writeAsBytesSync(encryptedThumbnailData.encryptedData);
 
+    final fileUploadURL = await _getUploadURL();
+    String fileObjectKey = await _putFile(fileUploadURL, encryptedFile);
+
     final thumbnailUploadURL = await _getUploadURL();
     String thumbnailObjectKey =
         await _putFile(thumbnailUploadURL, encryptedThumbnail);
@@ -306,3 +320,7 @@ enum UploadStatus {
   in_progress,
   completed,
 }
+
+class InvalidFileError extends Error {}
+
+class WiFiUnavailableError extends Error {}

+ 28 - 0
pubspec.lock

@@ -85,6 +85,34 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.0.2"
+  connectivity:
+    dependency: "direct main"
+    description:
+      name: connectivity
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.1"
+  connectivity_for_web:
+    dependency: transitive
+    description:
+      name: connectivity_for_web
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.3.1+4"
+  connectivity_macos:
+    dependency: transitive
+    description:
+      name: connectivity_macos
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.1.0+7"
+  connectivity_platform_interface:
+    dependency: transitive
+    description:
+      name: connectivity_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.6"
   convert:
     dependency: transitive
     description:

+ 1 - 0
pubspec.yaml

@@ -60,6 +60,7 @@ dependencies:
   page_transition: "^1.1.7+2"
   convex_bottom_bar: ^2.6.0
   scrollable_positioned_list: ^0.1.8
+  connectivity: ^2.0.1
 
 dev_dependencies:
   flutter_test: