import 'dart:math'; import 'package:computer/computer.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:photos/models/file.dart'; import 'package:tuple/tuple.dart'; final _logger = Logger("FileSyncUtil"); const ignoreSizeConstraint = SizeConstraint(ignoreSize: true); const assetFetchPageSize = 2000; Future, List>> getDeviceFiles( int fromTime, int toTime, Computer computer, ) async { final pathEntities = await _getGalleryList(fromTime, toTime); List files = []; for (AssetPathEntity pathEntity in pathEntities) { files = await _computeFiles(pathEntity, fromTime, files, computer); } // todo: Check if sort is needed and document the reason. files.sort( (first, second) => first.creationTime.compareTo(second.creationTime), ); return Tuple2(pathEntities, files); } Future> getAllLocalAssets() async { final filterOptionGroup = FilterOptionGroup(); filterOptionGroup.setOption( AssetType.image, const FilterOption(sizeConstraint: ignoreSizeConstraint), ); filterOptionGroup.setOption( AssetType.video, const FilterOption(sizeConstraint: ignoreSizeConstraint), ); filterOptionGroup.createTimeCond = DateTimeCond.def().copyWith(ignore: true); final assetPaths = await PhotoManager.getAssetPathList( hasAll: true, type: RequestType.common, filterOption: filterOptionGroup, ); final List assets = []; for (final assetPath in assetPaths) { for (final asset in await _getAllAssetLists(assetPath)) { assets.add( LocalAsset( id: asset.id, pathName: assetPath.name, pathID: assetPath.id, ), ); } } return assets; } Future> getUnsyncedFiles( List assets, Set existingIDs, Set invalidIDs, Computer computer, ) async { final Map args = {}; args['assets'] = assets; args['existingIDs'] = existingIDs; args['invalidIDs'] = invalidIDs; final unsyncedAssets = await computer.compute(_getUnsyncedAssets, param: args); if (unsyncedAssets.isEmpty) { return []; } return _convertToFiles(unsyncedAssets, computer); } List _getUnsyncedAssets(Map args) { final List assets = args['assets']; final Set existingIDs = args['existingIDs']; final Set invalidIDs = args['invalidIDs']; final List unsyncedAssets = []; for (final asset in assets) { if (!existingIDs.contains(asset.id) && !invalidIDs.contains(asset.id)) { unsyncedAssets.add(asset); } } return unsyncedAssets; } Future> _convertToFiles( List assets, Computer computer, ) async { final Map assetIDToEntityMap = {}; final List files = []; for (final localAsset in assets) { if (!assetIDToEntityMap.containsKey(localAsset.id)) { assetIDToEntityMap[localAsset.id] = await AssetEntity.fromId(localAsset.id); } files.add( File.fromAsset( localAsset.pathName, assetIDToEntityMap[localAsset.id], devicePathID: localAsset.pathID, ), ); } return files; } Future> _getGalleryList( final int fromTime, final int toTime, ) async { final filterOptionGroup = FilterOptionGroup(); filterOptionGroup.setOption( AssetType.image, const FilterOption(needTitle: true, sizeConstraint: ignoreSizeConstraint), ); filterOptionGroup.setOption( AssetType.video, const FilterOption(needTitle: true, sizeConstraint: ignoreSizeConstraint), ); filterOptionGroup.updateTimeCond = DateTimeCond( min: DateTime.fromMicrosecondsSinceEpoch(fromTime), max: DateTime.fromMicrosecondsSinceEpoch(toTime), ); final galleryList = await PhotoManager.getAssetPathList( hasAll: true, type: RequestType.common, filterOption: filterOptionGroup, ); galleryList.sort((s1, s2) { return s2.assetCount.compareTo(s1.assetCount); }); return galleryList; } Future> _computeFiles( AssetPathEntity pathEntity, int fromTime, List files, Computer computer, ) async { final Map args = {}; args["pathEntity"] = pathEntity; args["assetList"] = await _getAllAssetLists(pathEntity); args["fromTime"] = fromTime; args["files"] = files; return await computer.compute(_getFiles, param: args); } Future> _getAllAssetLists(AssetPathEntity pathEntity) async { List result = []; int currentPage = 0; List currentPageResult = []; do { currentPageResult = await pathEntity.getAssetListPaged( page: currentPage, size: assetFetchPageSize, ); result.addAll(currentPageResult); currentPage = currentPage + 1; } while (currentPageResult.length >= assetFetchPageSize); return result; } // review: do we need to run this inside compute, after making File.FromAsset // sync. If yes, update the method documentation with reason. Future> _getFiles(Map args) async { final pathEntity = args["pathEntity"] as AssetPathEntity; final assetList = args["assetList"]; final fromTime = args["fromTime"]; final files = args["files"]; for (AssetEntity entity in assetList) { if (max( entity.createDateTime.microsecondsSinceEpoch, entity.modifiedDateTime.microsecondsSinceEpoch, ) > fromTime) { try { final file = File.fromAsset( pathEntity.name, entity, devicePathID: pathEntity.id, ); files.add(file); } catch (e) { _logger.severe(e); } } } return files; } class LocalAsset { final String id; final String pathID; final String pathName; LocalAsset({ @required this.id, @required this.pathName, @required this.pathID, }); }