123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319 |
- import 'dart:async';
- import 'dart:io';
- import 'package:computer/computer.dart';
- import 'package:logging/logging.dart';
- import 'package:photo_manager/photo_manager.dart';
- import 'package:photos/core/configuration.dart';
- import 'package:photos/core/event_bus.dart';
- import 'package:photos/db/files_db.dart';
- import 'package:photos/events/local_photos_updated_event.dart';
- import 'package:photos/events/sync_status_update_event.dart';
- import 'package:photos/models/file.dart';
- import 'package:photos/services/app_lifecycle_service.dart';
- import 'package:photos/utils/file_sync_util.dart';
- import 'package:shared_preferences/shared_preferences.dart';
- class LocalSyncService {
- final _logger = Logger("LocalSyncService");
- final _db = FilesDB.instance;
- final Computer _computer = Computer();
- SharedPreferences _prefs;
- Completer<void> _existingSync;
- static const kDbUpdationTimeKey = "db_updation_time";
- static const kHasCompletedFirstImportKey = "has_completed_firstImport";
- static const kHasGrantedPermissionsKey = "has_granted_permissions";
- static const kPermissionStateKey = "permission_state";
- static const kEditedFileIDsKey = "edited_file_ids";
- static const kDownloadedFileIDsKey = "downloaded_file_ids";
- // Adding `_2` as a suffic to pull files that were earlier ignored due to permission errors
- // See https://github.com/CaiJingLong/flutter_photo_manager/issues/589
- static const kInvalidFileIDsKey = "invalid_file_ids_2";
- LocalSyncService._privateConstructor();
- static final LocalSyncService instance =
- LocalSyncService._privateConstructor();
- Future<void> init() async {
- _prefs = await SharedPreferences.getInstance();
- if (!AppLifecycleService.instance.isForeground) {
- await PhotoManager.setIgnorePermissionCheck(true);
- }
- await _computer.turnOn(workersCount: 1);
- if (hasGrantedPermissions()) {
- _registerChangeCallback();
- }
- }
- Future<void> sync() async {
- if (!_prefs.containsKey(kHasGrantedPermissionsKey)) {
- _logger.info("Skipping local sync since permission has not been granted");
- return;
- }
- if (Platform.isAndroid && AppLifecycleService.instance.isForeground) {
- final permissionState = await PhotoManager.requestPermissionExtend();
- if (permissionState != PermissionState.authorized) {
- _logger.severe(
- "sync requested with invalid permission",
- permissionState.toString(),
- );
- return;
- }
- }
- if (_existingSync != null) {
- _logger.warning("Sync already in progress, skipping.");
- return _existingSync.future;
- }
- _existingSync = Completer<void>();
- final existingLocalFileIDs = await _db.getExistingLocalFileIDs();
- _logger.info(
- existingLocalFileIDs.length.toString() + " localIDs were discovered",
- );
- final editedFileIDs = getEditedFileIDs().toSet();
- final downloadedFileIDs = getDownloadedFileIDs().toSet();
- final syncStartTime = DateTime.now().microsecondsSinceEpoch;
- final lastDBUpdationTime = _prefs.getInt(kDbUpdationTimeKey) ?? 0;
- final startTime = DateTime.now().microsecondsSinceEpoch;
- if (lastDBUpdationTime != 0) {
- await _loadAndStorePhotos(
- lastDBUpdationTime,
- syncStartTime,
- existingLocalFileIDs,
- editedFileIDs,
- downloadedFileIDs,
- );
- } else {
- // Load from 0 - 01.01.2010
- Bus.instance.fire(SyncStatusUpdate(SyncStatus.startedFirstGalleryImport));
- var startTime = 0;
- var toYear = 2010;
- var toTime = DateTime(toYear).microsecondsSinceEpoch;
- while (toTime < syncStartTime) {
- await _loadAndStorePhotos(
- startTime,
- toTime,
- existingLocalFileIDs,
- editedFileIDs,
- downloadedFileIDs,
- );
- startTime = toTime;
- toYear++;
- toTime = DateTime(toYear).microsecondsSinceEpoch;
- }
- await _loadAndStorePhotos(
- startTime,
- syncStartTime,
- existingLocalFileIDs,
- editedFileIDs,
- downloadedFileIDs,
- );
- }
- if (!_prefs.containsKey(kHasCompletedFirstImportKey) ||
- !_prefs.getBool(kHasCompletedFirstImportKey)) {
- await _prefs.setBool(kHasCompletedFirstImportKey, true);
- _logger.fine("first gallery import finished");
- Bus.instance
- .fire(SyncStatusUpdate(SyncStatus.completedFirstGalleryImport));
- }
- final endTime = DateTime.now().microsecondsSinceEpoch;
- final duration = Duration(microseconds: endTime - startTime);
- _logger.info("Load took " + duration.inMilliseconds.toString() + "ms");
- _existingSync.complete();
- _existingSync = null;
- }
- Future<bool> syncAll() async {
- final sTime = DateTime.now().microsecondsSinceEpoch;
- final localAssets = await getAllLocalAssets();
- final eTime = DateTime.now().microsecondsSinceEpoch;
- final d = Duration(microseconds: eTime - sTime);
- _logger.info(
- "Loading from the beginning returned " +
- localAssets.length.toString() +
- " assets and took " +
- d.inMilliseconds.toString() +
- "ms",
- );
- final existingIDs = await _db.getExistingLocalFileIDs();
- final invalidIDs = getInvalidFileIDs().toSet();
- final unsyncedFiles =
- await getUnsyncedFiles(localAssets, existingIDs, invalidIDs, _computer);
- if (unsyncedFiles.isNotEmpty) {
- await _db.insertMultiple(unsyncedFiles);
- _logger.info(
- "Inserted " + unsyncedFiles.length.toString() + " unsynced files.",
- );
- _updatePathsToBackup(unsyncedFiles);
- Bus.instance.fire(LocalPhotosUpdatedEvent(unsyncedFiles));
- return true;
- }
- return false;
- }
- Future<void> trackEditedFile(File file) async {
- final editedIDs = getEditedFileIDs();
- editedIDs.add(file.localID);
- await _prefs.setStringList(kEditedFileIDsKey, editedIDs);
- }
- List<String> getEditedFileIDs() {
- if (_prefs.containsKey(kEditedFileIDsKey)) {
- return _prefs.getStringList(kEditedFileIDsKey);
- } else {
- final List<String> editedIDs = [];
- return editedIDs;
- }
- }
- Future<void> trackDownloadedFile(String localID) async {
- final downloadedIDs = getDownloadedFileIDs();
- downloadedIDs.add(localID);
- await _prefs.setStringList(kDownloadedFileIDsKey, downloadedIDs);
- }
- List<String> getDownloadedFileIDs() {
- if (_prefs.containsKey(kDownloadedFileIDsKey)) {
- return _prefs.getStringList(kDownloadedFileIDsKey);
- } else {
- final List<String> downloadedIDs = [];
- return downloadedIDs;
- }
- }
- Future<void> trackInvalidFile(File file) async {
- final invalidIDs = getInvalidFileIDs();
- invalidIDs.add(file.localID);
- await _prefs.setStringList(kInvalidFileIDsKey, invalidIDs);
- }
- List<String> getInvalidFileIDs() {
- if (_prefs.containsKey(kInvalidFileIDsKey)) {
- return _prefs.getStringList(kInvalidFileIDsKey);
- } else {
- final List<String> invalidIDs = [];
- return invalidIDs;
- }
- }
- bool hasGrantedPermissions() {
- return _prefs.getBool(kHasGrantedPermissionsKey) ?? false;
- }
- bool hasGrantedLimitedPermissions() {
- return _prefs.getString(kPermissionStateKey) ==
- PermissionState.limited.toString();
- }
- Future<void> onPermissionGranted(PermissionState state) async {
- await _prefs.setBool(kHasGrantedPermissionsKey, true);
- await _prefs.setString(kPermissionStateKey, state.toString());
- _registerChangeCallback();
- }
- bool hasCompletedFirstImport() {
- return _prefs.getBool(kHasCompletedFirstImportKey) ?? false;
- }
- Future<void> _loadAndStorePhotos(
- int fromTime,
- int toTime,
- Set<String> existingLocalFileIDs,
- Set<String> editedFileIDs,
- Set<String> downloadedFileIDs,
- ) async {
- _logger.info(
- "Loading photos from " +
- DateTime.fromMicrosecondsSinceEpoch(fromTime).toString() +
- " to " +
- DateTime.fromMicrosecondsSinceEpoch(toTime).toString(),
- );
- final files = await getDeviceFiles(fromTime, toTime, _computer);
- if (files.isNotEmpty) {
- _logger.info("Fetched " + files.length.toString() + " files.");
- final updatedFiles = files
- .where((file) => existingLocalFileIDs.contains(file.localID))
- .toList();
- updatedFiles.removeWhere((file) => editedFileIDs.contains(file.localID));
- updatedFiles
- .removeWhere((file) => downloadedFileIDs.contains(file.localID));
- if (updatedFiles.isNotEmpty) {
- _logger.info(
- updatedFiles.length.toString() + " local files were updated.",
- );
- }
- for (final file in updatedFiles) {
- await captureUpdateLogs(file);
- await _db.updateUploadedFile(
- file.localID,
- file.title,
- file.location,
- file.creationTime,
- file.modificationTime,
- null,
- );
- }
- final List<File> allFiles = [];
- allFiles.addAll(files);
- files.removeWhere((file) => existingLocalFileIDs.contains(file.localID));
- await _db.insertMultiple(files);
- _logger.info("Inserted " + files.length.toString() + " files.");
- _updatePathsToBackup(files);
- Bus.instance.fire(LocalPhotosUpdatedEvent(allFiles));
- }
- await _prefs.setInt(kDbUpdationTimeKey, toTime);
- }
- // _captureUpdateLogs is a helper method to log details
- // about the file which is being marked for re-upload
- Future<void> captureUpdateLogs(File file) async {
- _logger.info(
- 're-upload locally updated file ${file.toString()}',
- );
- try {
- if (Platform.isIOS) {
- final assetEntity = await AssetEntity.fromId(file.localID);
- if (assetEntity != null) {
- final isLocallyAvailable =
- await assetEntity.isLocallyAvailable(isOrigin: true);
- _logger.info(
- 're-upload asset ${file.toString()} with localAvailableFlag '
- '$isLocallyAvailable and fav ${assetEntity.isFavorite}',
- );
- } else {
- _logger
- .info('re-upload failed to fetch assetInfo ${file.toString()}');
- }
- }
- } catch (ignore) {
- //ignore
- }
- }
- void _updatePathsToBackup(List<File> files) {
- if (Configuration.instance.hasSelectedAllFoldersForBackup()) {
- final pathsToBackup = Configuration.instance.getPathsToBackUp();
- final newFilePaths = files.map((file) => file.deviceFolder).toList();
- pathsToBackup.addAll(newFilePaths);
- Configuration.instance.setPathsToBackUp(pathsToBackup);
- }
- }
- void _registerChangeCallback() {
- // In case of iOS limit permission, this call back is fired immediately
- // after file selection dialog is dismissed.
- PhotoManager.addChangeCallback((value) async {
- _logger.info("Something changed on disk");
- if (_existingSync != null) {
- await _existingSync.future;
- }
- if (hasGrantedLimitedPermissions()) {
- syncAll();
- } else {
- sync();
- }
- });
- PhotoManager.startChangeNotify();
- }
- }
|