import 'package:collection/collection.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/local_file.dart'; import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/mobile/android/android_info.dart'; import 'package:nc_photos/mobile/android/k.dart' as android; import 'package:nc_photos/mobile/share.dart'; import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/stream_extension.dart'; import 'package:nc_photos_plugin/nc_photos_plugin.dart'; class LocalFileMediaStoreDataSource implements LocalFileDataSource { const LocalFileMediaStoreDataSource(); @override listDir(String path) async { _log.info("[listDir] $path"); final results = await MediaStore.queryFiles(path); return results .where((r) => file_util.isSupportedMime(r.mimeType ?? "")) .map(_toLocalFile) .toList(); } @override deleteFiles( List files, { LocalFileOnFailureListener? onFailure, }) async { _log.info("[deleteFiles] ${files.map((f) => f.logTag).toReadableString()}"); final uriFiles = _filterUriFiles(files, (f) { onFailure?.call(f, ArgumentError("File not supported"), null); }); if (AndroidInfo().sdkInt >= AndroidVersion.R) { await _deleteFiles30(uriFiles, onFailure); } else { await _deleteFiles0(uriFiles, onFailure); } } @override shareFiles( List files, { LocalFileOnFailureListener? onFailure, }) async { _log.info("[shareFiles] ${files.map((f) => f.logTag).toReadableString()}"); final uriFiles = _filterUriFiles(files, (f) { onFailure?.call(f, ArgumentError("File not supported"), null); }); final share = AndroidFileShare(uriFiles.map((e) => e.uri).toList(), uriFiles.map((e) => e.mime).toList()); try { await share.share(); } catch (e, stackTrace) { for (final f in uriFiles) { onFailure?.call(f, e, stackTrace); } } } Future _deleteFiles30( List files, LocalFileOnFailureListener? onFailure) async { assert(AndroidInfo().sdkInt >= AndroidVersion.R); int? resultCode; final resultFuture = MediaStore.stream .whereType() .first .then((ev) => resultCode = ev.resultCode); await MediaStore.deleteFiles(files.map((f) => f.uri).toList()); await resultFuture; if (resultCode != android.resultOk) { _log.warning("[_deleteFiles30] result != OK: $resultCode"); for (final f in files) { onFailure?.call(f, null, null); } } } Future _deleteFiles0( List files, LocalFileOnFailureListener? onFailure) async { assert(AndroidInfo().sdkInt < AndroidVersion.R); final failedUris = await MediaStore.deleteFiles(files.map((f) => f.uri).toList()); final failedFilesIt = failedUris! .map((uri) => files.firstWhereOrNull((f) => f.uri == uri)) .whereNotNull(); for (final f in failedFilesIt) { onFailure?.call(f, null, null); } } List _filterUriFiles( List files, [ void Function(LocalFile)? nonUriFileCallback, ]) { return files .where((f) { if (f is! LocalUriFile) { _log.warning( "[deleteFiles] Can't remove file not returned by this data source: $f"); nonUriFileCallback?.call(f); return false; } else { return true; } }) .cast() .toList(); } static LocalFile _toLocalFile(MediaStoreQueryResult r) => LocalUriFile( uri: r.uri, displayName: r.displayName, path: r.path, lastModified: DateTime.fromMillisecondsSinceEpoch(r.dateModified), mime: r.mimeType, dateTaken: r.dateTaken?.run(DateTime.fromMillisecondsSinceEpoch), ); static final _log = Logger("entity.local_file.data_source.LocalFileMediaStoreDataSource"); }