import 'dart:async'; import 'dart:typed_data'; import 'package:aves/services/output_buffer.dart'; import 'package:aves/services/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:streams_channel/streams_channel.dart'; abstract class StorageService { Future> getStorageVolumes(); Future getFreeSpace(StorageVolume volume); Future> getGrantedDirectories(); Future revokeDirectoryAccess(String path); Future> getInaccessibleDirectories(Iterable dirPaths); Future> getRestrictedDirectories(); // returns whether user granted access to volume root at `volumePath` Future requestVolumeAccess(String volumePath); // returns number of deleted directories Future deleteEmptyDirectories(Iterable dirPaths); // returns media URI Future scanFile(String path, String mimeType); // return whether operation succeeded (`null` if user cancelled) Future createFile(String name, String mimeType, Uint8List bytes); Future openFile(String mimeType); Future selectDirectory(); } class PlatformStorageService implements StorageService { static const platform = MethodChannel('deckers.thibault/aves/storage'); static final StreamsChannel storageAccessChannel = StreamsChannel('deckers.thibault/aves/storage_access_stream'); @override Future> getStorageVolumes() async { try { final result = await platform.invokeMethod('getStorageVolumes'); return (result as List).cast().map((map) => StorageVolume.fromMap(map)).toSet(); } on PlatformException catch (e) { await reportService.recordChannelError('getStorageVolumes', e); } 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) { await reportService.recordChannelError('getFreeSpace', e); } return null; } @override Future> getGrantedDirectories() async { try { final result = await platform.invokeMethod('getGrantedDirectories'); return (result as List).cast(); } on PlatformException catch (e) { await reportService.recordChannelError('getGrantedDirectories', e); } return []; } @override Future revokeDirectoryAccess(String path) async { try { await platform.invokeMethod('revokeDirectoryAccess', { 'path': path, }); } on PlatformException catch (e) { await reportService.recordChannelError('revokeDirectoryAccess', e); } 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) { await reportService.recordChannelError('getInaccessibleDirectories', e); } return {}; } @override Future> getRestrictedDirectories() async { try { final result = await platform.invokeMethod('getRestrictedDirectories'); if (result != null) { return (result as List).cast().map(VolumeRelativeDirectory.fromMap).toSet(); } } on PlatformException catch (e) { await reportService.recordChannelError('getRestrictedDirectories', e); } return {}; } // returns whether user granted access to volume root at `volumePath` @override Future requestVolumeAccess(String volumePath) async { try { final completer = Completer(); storageAccessChannel.receiveBroadcastStream({ 'op': 'requestVolumeAccess', 'path': volumePath, }).listen( (data) => completer.complete(data as bool), onError: completer.completeError, onDone: () { if (!completer.isCompleted) completer.complete(false); }, cancelOnError: true, ); return completer.future; } on PlatformException catch (e) { await reportService.recordChannelError('requestVolumeAccess', e); } return false; } // returns number of deleted directories @override Future deleteEmptyDirectories(Iterable dirPaths) async { try { final result = await platform.invokeMethod('deleteEmptyDirectories', { 'dirPaths': dirPaths.toList(), }); if (result != null) return result as int; } on PlatformException catch (e) { await reportService.recordChannelError('deleteEmptyDirectories', e); } return 0; } // returns media URI @override Future scanFile(String path, String mimeType) async { debugPrint('scanFile with path=$path, mimeType=$mimeType'); try { final result = await platform.invokeMethod('scanFile', { 'path': path, 'mimeType': mimeType, }); if (result != null) return Uri.tryParse(result); } on PlatformException catch (e) { await reportService.recordChannelError('scanFile', e); } return null; } @override Future createFile(String name, String mimeType, Uint8List bytes) async { try { final completer = Completer(); storageAccessChannel.receiveBroadcastStream({ 'op': 'createFile', 'name': name, 'mimeType': mimeType, 'bytes': bytes, }).listen( (data) => completer.complete(data as bool?), onError: completer.completeError, onDone: () { if (!completer.isCompleted) completer.complete(false); }, cancelOnError: true, ); return completer.future; } on PlatformException catch (e) { await reportService.recordChannelError('createFile', e); } return false; } @override Future openFile(String mimeType) async { try { final completer = Completer.sync(); final sink = OutputBuffer(); storageAccessChannel.receiveBroadcastStream({ 'op': 'openFile', 'mimeType': mimeType, }).listen( (data) { final chunk = data as Uint8List; sink.add(chunk); }, onError: completer.completeError, onDone: () { sink.close(); completer.complete(sink.bytes); }, cancelOnError: true, ); return completer.future; } on PlatformException catch (e) { await reportService.recordChannelError('openFile', e); } return Uint8List(0); } @override Future selectDirectory() async { try { final completer = Completer(); storageAccessChannel.receiveBroadcastStream({ 'op': 'selectDirectory', }).listen( (data) => completer.complete(data as String?), onError: completer.completeError, onDone: () { if (!completer.isCompleted) completer.complete(null); }, cancelOnError: true, ); return completer.future; } on PlatformException catch (e) { await reportService.recordChannelError('selectDirectory', e); } return null; } }