import 'dart:async'; import 'package:aves/model/covers.dart'; import 'package:aves/services/common/output_buffer.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves_model/aves_model.dart'; import 'package:flutter/services.dart'; import 'package:streams_channel/streams_channel.dart'; abstract class StorageService { Future> getDataUsage(); Future> getStorageVolumes(); Future getExternalCacheDirectory(); Future> getUntrackedTrashPaths(Iterable knownPaths); Future> getUntrackedVaultPaths(String vaultName, Iterable knownPaths); Future getVaultRoot(); Future getFreeSpace(StorageVolume volume); Future> getGrantedDirectories(); Future> getInaccessibleDirectories(Iterable dirPaths); // returns directories with restricted access, // with the relative part in lowercase, for case-insensitive comparison Future> getRestrictedDirectoriesLowerCase(); Future revokeDirectoryAccess(String path); // returns number of deleted directories Future deleteEmptyRegularDirectories(Set dirPaths); Future deleteTempDirectory(); Future deleteExternalCache(); // returns whether user granted access to a directory of his choosing Future requestDirectoryAccess(String path); Future canRequestMediaFileBulkAccess(); Future canInsertMedia(Set directories); // returns whether user granted access to URIs Future requestMediaFileAccess(List uris, List mimeTypes); // returns whether operation succeeded (`null` if user cancelled) Future createFile(String name, String mimeType, Uint8List bytes); Future openFile([String? mimeType]); // returns whether operation succeeded (`null` if user cancelled) Future copyFile(String name, String mimeType, String sourceUri); } class PlatformStorageService implements StorageService { static const _platform = MethodChannel('deckers.thibault/aves/storage'); static final _stream = StreamsChannel('deckers.thibault/aves/activity_result_stream'); @override Future> getDataUsage() async { try { final result = await _platform.invokeMethod('getDataUsage'); if (result != null) return (result as Map).cast(); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return {}; } @override Future> getStorageVolumes() async { try { final result = await _platform.invokeMethod('getStorageVolumes'); return (result as List).cast().map(StorageVolume.fromMap).toSet(); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return {}; } @override Future getExternalCacheDirectory() async { try { final result = await _platform.invokeMethod('getCacheDirectory', { 'external': true, }); return result as String; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return ''; } @override Future> getUntrackedTrashPaths(Iterable knownPaths) async { try { final result = await _platform.invokeMethod('getUntrackedTrashPaths', { 'knownPaths': knownPaths.toList(), }); return (result as List).cast().toSet(); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return {}; } @override Future> getUntrackedVaultPaths(String vaultName, Iterable knownPaths) async { try { final result = await _platform.invokeMethod('getUntrackedVaultPaths', { 'vault': vaultName, 'knownPaths': knownPaths.toList(), }); return (result as List).cast().toSet(); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return {}; } @override Future getVaultRoot() async { try { final result = await _platform.invokeMethod('getVaultRoot'); return result as String; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return ''; } @override Future getFreeSpace(StorageVolume volume) async { try { final result = await _platform.invokeMethod('getFreeSpace', { 'path': volume.path, }); return result as int?; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return null; } @override Future> getGrantedDirectories() async { try { final result = await _platform.invokeMethod('getGrantedDirectories'); return (result as List).cast(); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return []; } @override Future> getInaccessibleDirectories(Iterable dirPaths) async { try { final result = await _platform.invokeMethod('getInaccessibleDirectories', { 'dirPaths': dirPaths.toList(), }); if (result != null) { return (result as List).cast().map(VolumeRelativeDirectory.fromMap).toSet(); } } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return {}; } @override Future> getRestrictedDirectoriesLowerCase() async { try { final result = await _platform.invokeMethod('getRestrictedDirectories'); if (result != null) { return (result as List) .cast() .map(VolumeRelativeDirectory.fromMap) .map((dir) => dir.copyWith( relativeDir: dir.relativeDir.toLowerCase(), )) .toSet(); } } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return {}; } @override Future revokeDirectoryAccess(String path) async { try { await _platform.invokeMethod('revokeDirectoryAccess', { 'path': path, }); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } } // returns number of deleted directories @override Future deleteEmptyRegularDirectories(Set dirPaths) async { try { final result = await _platform.invokeMethod('deleteEmptyDirectories', { 'dirPaths': dirPaths.where((v) => covers.effectiveAlbumType(v) == AlbumType.regular).toList(), }); if (result != null) return result as int; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return 0; } @override Future deleteTempDirectory() async { try { final result = await _platform.invokeMethod('deleteTempDirectory'); if (result != null) return result as bool; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return false; } @override Future deleteExternalCache() async { try { final result = await _platform.invokeMethod('deleteExternalCache'); if (result != null) return result as bool; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return false; } @override Future canRequestMediaFileBulkAccess() async { try { final result = await _platform.invokeMethod('canRequestMediaFileBulkAccess'); if (result != null) return result as bool; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return false; } @override Future canInsertMedia(Set directories) async { try { final result = await _platform.invokeMethod('canInsertMedia', { 'directories': directories.map((v) => v.toMap()).toList(), }); if (result != null) return result as bool; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return false; } // returns whether user granted access to a directory of his choosing @override Future requestDirectoryAccess(String path) async { try { final opCompleter = Completer(); _stream.receiveBroadcastStream({ 'op': 'requestDirectoryAccess', 'path': path, }).listen( (data) => opCompleter.complete(data as bool), onError: opCompleter.completeError, onDone: () { if (!opCompleter.isCompleted) opCompleter.complete(false); }, cancelOnError: true, ); // `await` here, so that `completeError` will be caught below return await opCompleter.future; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return false; } // returns whether user granted access to URIs @override Future requestMediaFileAccess(List uris, List mimeTypes) async { try { final opCompleter = Completer(); _stream.receiveBroadcastStream({ 'op': 'requestMediaFileAccess', 'uris': uris, 'mimeTypes': mimeTypes, }).listen( (data) => opCompleter.complete(data as bool), onError: opCompleter.completeError, onDone: () { if (!opCompleter.isCompleted) opCompleter.complete(false); }, cancelOnError: true, ); // `await` here, so that `completeError` will be caught below return await opCompleter.future; } on PlatformException catch (e, stack) { final message = e.message; // mute issue in the specific case when an item: // 1) is a Media Store `file` content, // 2) has no `images` or `video` entry, // 3) is in a restricted directory if (message == null || !message.contains('/external/file/')) { await reportService.recordError(e, stack); } } return false; } @override Future createFile(String name, String mimeType, Uint8List bytes) async { try { final opCompleter = Completer(); _stream.receiveBroadcastStream({ 'op': 'createFile', 'name': name, 'mimeType': mimeType, 'bytes': bytes, }).listen( (data) => opCompleter.complete(data as bool?), onError: opCompleter.completeError, onDone: () { if (!opCompleter.isCompleted) opCompleter.complete(false); }, cancelOnError: true, ); // `await` here, so that `completeError` will be caught below return await opCompleter.future; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return false; } @override Future openFile([String? mimeType]) async { try { final opCompleter = Completer(); final sink = OutputBuffer(); _stream.receiveBroadcastStream({ 'op': 'openFile', 'mimeType': mimeType, }).listen( (data) { final chunk = data as Uint8List; sink.add(chunk); }, onError: opCompleter.completeError, onDone: () { sink.close(); opCompleter.complete(sink.bytes); }, cancelOnError: true, ); // `await` here, so that `completeError` will be caught below return await opCompleter.future; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return Uint8List(0); } @override Future copyFile(String name, String mimeType, String sourceUri) async { try { final opCompleter = Completer(); _stream.receiveBroadcastStream({ 'op': 'copyFile', 'name': name, 'mimeType': mimeType, 'sourceUri': sourceUri, }).listen( (data) => opCompleter.complete(data as bool?), onError: opCompleter.completeError, onDone: () { if (!opCompleter.isCompleted) opCompleter.complete(false); }, cancelOnError: true, ); // `await` here, so that `completeError` will be caught below return await opCompleter.future; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } return false; } }