diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileApi.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileApi.kt index af90e89..2d7a674 100644 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileApi.kt +++ b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/DocumentFileApi.kt @@ -74,6 +74,13 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : call.argument("content")!! ) } + WRITE_TO_FILE -> + writeToFile( + result, + call.argument("uri")!!, + call.argument("content")!!, + call.argument("mode")!! + ) PERSISTED_URI_PERMISSIONS -> persistedUriPermissions(result) RELEASE_PERSISTABLE_URI_PERMISSION -> @@ -311,6 +318,25 @@ internal class DocumentFileApi(private val plugin: SharedStoragePlugin) : } } + private fun writeToFile( + result: MethodChannel.Result, + uri: String, + content: ByteArray, + mode: String + ) { + try { + plugin.context.contentResolver.openOutputStream(Uri.parse(uri), mode)?.apply { + write(content) + flush() + close() + + result.success(true) + } + } catch (e: Exception) { + result.success(false) + } + } + @RequiresApi(API_19) private fun persistedUriPermissions(result: MethodChannel.Result) { val persistedUriPermissions = plugin.context.contentResolver.persistedUriPermissions diff --git a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt index d1ed988..e9f9d52 100644 --- a/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt +++ b/android/src/main/kotlin/io/lakscastro/sharedstorage/storageaccessframework/lib/StorageAccessFrameworkConstant.kt @@ -23,6 +23,7 @@ const val OPEN_DOCUMENT_TREE = "openDocumentTree" const val PERSISTED_URI_PERMISSIONS = "persistedUriPermissions" const val RELEASE_PERSISTABLE_URI_PERMISSION = "releasePersistableUriPermission" const val CREATE_FILE = "createFile" +const val WRITE_TO_FILE = "writeToFile" const val FROM_TREE_URI = "fromTreeUri" const val CAN_WRITE = "canWrite" const val CAN_READ = "canRead" diff --git a/docs/Usage/Storage Access Framework.md b/docs/Usage/Storage Access Framework.md index 34f2fdd..506cbb7 100644 --- a/docs/Usage/Storage Access Framework.md +++ b/docs/Usage/Storage Access Framework.md @@ -217,6 +217,33 @@ final DocumentFile? createdFile = createFileAsBytes( ); ``` +### writeToFileAsBytes + +Write to a file using raw bytes `Uint8List`. + +Given the document uri, opens the file in the specified `mode` and writes the `bytes` to it. + +`mode` represents the mode in which the file will be opened for writing. Use `FileMode.write` for truncating (overwrite) and `FileMode.append` for appending to the file. + +```dart +final Uri documentUri = ... +final String fileContent = 'My File Content'; + +/// Write to a file using a [Uint8List] as file contents [bytes] +final bool? success = writeToFileAsBytes( + documentUri, + bytes: Uint8List.fromList(fileContent.codeUnits), + mode: FileMode.write, +); + +/// Append to a file using a [Uint8List] as file contents [bytes] +final bool? success = writeToFileAsBytes( + documentUri, + bytes: Uint8List.fromList(fileContent.codeUnits), + mode: FileMode.write, +); +``` + ### canRead Mirror of [`DocumentFile.canRead`]() @@ -485,6 +512,31 @@ final DocumentFile? createdFile = createFileAsString( ); ``` +### writeToFileAsString + +Alias for `writeToFileAsBytes` + +Convenient method to write to a file using `content` as `String` instead `Uint8List`. + +```dart +final Uri documentUri = ... +final String fileContent = 'My File Content'; + +/// Write to a file using a [Uint8List] as file contents [bytes] +final bool? success = writeToFileAsString( + documentUri, + content: fileContent, + mode: FileMode.write, +); + +/// Append to a file using a [Uint8List] as file contents [bytes] +final bool? success = writeToFileAsBytes( + documentUri, + content: fileContent, + mode: FileMode.write, +); +``` + ### createFile Alias for `createFileAsBytes` and `createFileAsString` @@ -514,6 +566,49 @@ final DocumentFile? createdFile = createFile( ); ``` +### writeToFile + +Alias for `writeToFileAsBytes` and `writeToFileAsString` + +Convenient method to write to a file using `content` as `String` **or** `bytes` as `Uint8List`. + +You should provide either `content` or `bytes`, if both `bytes` will be used. + +`mode` represents the mode in which the file will be opened for writing. Use `FileMode.write` for truncating and `FileMode.append` for appending to the file. + +```dart +final Uri documentUri = ... +final String fileContent = 'My File Content'; + +/// Write to a file using a [String] as file contents [content] +final bool? success = writeToFile( + documentUri, + content: fileContent, + mode: FileMode.write, +); + +/// Append to a file using a [String] as file contents [content] +final bool? success = writeToFile( + documentUri, + content: fileContent, + mode: FileMode.append, +); + +/// Write to a file using a [Uint8List] as file contents [bytes] +final bool? success = writeToFile( + documentUri, + content: Uint8List.fromList(fileContent.codeUnits), + mode: FileMode.write, +); + +/// Append to a file using a [Uint8List] as file contents [bytes] +final bool? success = writeToFile( + documentUri, + content: Uint8List.fromList(fileContent.codeUnits), + mode: FileMode.append, +); +``` + ## External APIs (deprecated) These APIs are from external Android libraries. diff --git a/example/lib/screens/file_explorer/file_explorer_card.dart b/example/lib/screens/file_explorer/file_explorer_card.dart index 874762e..07d40bb 100644 --- a/example/lib/screens/file_explorer/file_explorer_card.dart +++ b/example/lib/screens/file_explorer/file_explorer_card.dart @@ -198,6 +198,13 @@ class _FileExplorerCardState extends State { } }, ), + if (!_isDirectory) + DangerButton( + 'Write to File', + onTap: () async { + await writeToFile(widget.partialFile.metadata!.uri!, content: 'Hello World!'); + }, + ), ], ), ], diff --git a/lib/src/saf/document_file.dart b/lib/src/saf/document_file.dart index e072d20..2e87f81 100644 --- a/lib/src/saf/document_file.dart +++ b/lib/src/saf/document_file.dart @@ -1,3 +1,4 @@ +import 'dart:io'; import 'dart:typed_data'; import '../common/functional_extender.dart'; @@ -155,6 +156,41 @@ class DocumentFile { displayName: displayName, content: content, ); + + /// {@macro sharedstorage.saf.writeToFileAsBytes} + Future writeToFileAsBytes({ + required Uint8List bytes, + FileMode? mode, + }) => + saf.writeToFileAsBytes( + uri, + bytes: bytes, + mode: mode, + ); + + /// {@macro sharedstorage.saf.writeToFile} + Future writeToFile({ + String? content, + Uint8List? bytes, + FileMode? mode, + }) => + saf.writeToFile( + uri, + content: content, + bytes: bytes, + mode: mode, + ); + + /// Alias for [writeToFile] with [content] param + Future writeToFileAsString({ + required String content, + FileMode? mode, + }) => + saf.writeToFile( + uri, + content: content, + mode: mode, + ); /// {@macro sharedstorage.saf.length} Future get length => saf.documentLength(uri); diff --git a/lib/src/saf/saf.dart b/lib/src/saf/saf.dart index 3b79293..cea8c55 100644 --- a/lib/src/saf/saf.dart +++ b/lib/src/saf/saf.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'dart:typed_data'; import '../../saf.dart'; @@ -201,7 +202,7 @@ Future createDirectory(Uri parentUri, String displayName) async { } /// {@template sharedstorage.saf.createFile} -/// Convenient method to create files using either String or raw bytes. +/// Convenient method to create files using either [String] or raw bytes [Uint8List]. /// /// Under the hood this method calls `createFileAsString` or `createFileAsBytes` /// depending on which argument is passed. @@ -281,6 +282,79 @@ Future createFileAsString( ); } +/// {@template sharedstorage.saf.writeToFile} +/// Convenient method to write to a file using either [String] or raw bytes [Uint8List]. +/// +/// Under the hood this method calls `writeToFileAsString` or `writeToFileAsBytes` +/// depending on which argument is passed. +/// +/// If both (bytes and content) are passed, the bytes will be used and the content will be ignored. +/// {@endtemplate} +Future writeToFile( + Uri uri, { + Uint8List? bytes, + String? content, + FileMode? mode, +}) { + assert( + bytes != null || content != null, + '''Either [bytes] or [content] should be provided''', + ); + + return bytes != null + ? writeToFileAsBytes( + uri, + bytes: bytes, + mode: mode, + ) + : writeToFileAsString( + uri, + content: content!, + mode: mode, + ); +} + +/// {@template sharedstorage.saf.writeToFileAsBytes} +/// Write to a file. +/// - `uri` is the URI of the file. +/// - `bytes` is the content of the document as a list of bytes `Uint8List`. +/// - `mode` is the mode in which the file will be opened for writing. Use `FileMode.write` for truncating and `FileMode.append` for appending to the file. +/// +/// Returns `true` if the file was successfully written to. +/// {@endtemplate} +Future writeToFileAsBytes( + Uri uri, { + required Uint8List bytes, + FileMode? mode, +}) async { + final writeMode = + mode == FileMode.append || mode == FileMode.writeOnlyAppend ? 'wa' : 'wt'; + + final args = { + 'uri': '$uri', + 'content': bytes, + 'mode': writeMode, + }; + + return kDocumentFileChannel.invokeMethod('writeToFile', args); +} + +/// {@template sharedstorage.saf.writeToFileAsString} +/// Convenient method to write to a file. +/// using `content` as [String] instead [Uint8List]. +/// {@endtemplate} +Future writeToFileAsString( + Uri uri, { + required String content, + FileMode? mode, +}) { + return writeToFileAsBytes( + uri, + bytes: Uint8List.fromList(content.codeUnits), + mode: mode, + ); +} + /// {@template sharedstorage.saf.length} /// Equivalent to `DocumentFile.length`. ///