Sync only when WiFi is available
This commit is contained in:
parent
5629f8ae7a
commit
1947970b0a
8 changed files with 175 additions and 41 deletions
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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(() {});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
pubspec.lock
28
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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Add table
Reference in a new issue