Sync only when WiFi is available

This commit is contained in:
Vishnu Mohandas 2020-11-16 22:05:16 +05:30
parent 5629f8ae7a
commit 1947970b0a
8 changed files with 175 additions and 41 deletions

View file

@ -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();
}
}
}

View file

@ -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,
}

View file

@ -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));

View file

@ -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(() {});
},
),
],
),
],
),
);

View file

@ -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;
}
}

View file

@ -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 {}

View file

@ -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:

View file

@ -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: