diff --git a/CHANGELOG.md b/CHANGELOG.md index bb2482e0..13f481af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 6.1.0 +### Android +- saveFile() method implemented for the android platform ## 6.0.0 Update minimum Flutter version to 3.7.0. diff --git a/android/src/main/java/com/mr/flutter/plugin/filepicker/FileInfo.java b/android/src/main/java/com/mr/flutter/plugin/filepicker/FileInfo.java index 91089691..01fa0b4e 100644 --- a/android/src/main/java/com/mr/flutter/plugin/filepicker/FileInfo.java +++ b/android/src/main/java/com/mr/flutter/plugin/filepicker/FileInfo.java @@ -20,6 +20,10 @@ public FileInfo(String path, String name, Uri uri, long size, byte[] bytes) { this.uri = uri; } + public String toString() { + return "path: " + this.path + ", name: " + this.name; + } + public static class Builder { private String path; diff --git a/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java b/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java index 7b938a64..a0d3be19 100644 --- a/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java +++ b/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java @@ -37,6 +37,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener private boolean isMultipleSelection = false; private boolean loadDataToMemory = false; private String type; + private String input; private String[] allowedExtensions; private EventChannel.EventSink eventSink; @@ -119,16 +120,26 @@ public void run() { finishWithError("unknown_path", "Failed to retrieve directory path."); } return; + } else if (type.equals("save") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Log.d(FilePickerDelegate.TAG, "[SaveFile] File URI:" + uri.toString()); + final String filePath = FileUtils.getAbsolutePathFromUri(uri, input, activity); + if(filePath != null) { + Log.d(FilePickerDelegate.TAG, "[SaveFile] File Path:" + filePath); + finishWithSuccess(filePath); + } else { + finishWithError("unknown_path", "Failed to retrieve file path."); + } + return; } final FileInfo file = FileUtils.openFileStream(FilePickerDelegate.this.activity, uri, loadDataToMemory); if(file != null) { + Log.d(FilePickerDelegate.TAG, file.toString()); files.add(file); } if (!files.isEmpty()) { - Log.d(FilePickerDelegate.TAG, "File path:" + files.toString()); finishWithSuccess(files); } else { finishWithError("unknown_path", "Failed to retrieve path."); @@ -229,6 +240,14 @@ private void startFileExplorer() { if (type.equals("dir")) { intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + } else if (type.equals("save")) { + intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + Log.d(TAG, "Selected type " + type); + intent.setType("*/*"); + if (allowedExtensions != null) { + intent.putExtra(Intent.EXTRA_MIME_TYPES, allowedExtensions); + } } else { if (type.equals("image/*")) { intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); @@ -261,7 +280,7 @@ private void startFileExplorer() { } @SuppressWarnings("deprecation") - public void startFileExplorer(final String type, final boolean isMultipleSelection, final boolean withData, final String[] allowedExtensions, final MethodChannel.Result result) { + public void startFileExplorer(final String type, final String input, final boolean isMultipleSelection, final boolean withData, final String[] allowedExtensions, final MethodChannel.Result result) { if (!this.setPendingMethodCallAndResult(result)) { finishWithAlreadyActiveError(result); @@ -269,6 +288,7 @@ public void startFileExplorer(final String type, final boolean isMultipleSelecti } this.type = type; + this.input = input; this.isMultipleSelection = isMultipleSelection; this.loadDataToMemory = withData; this.allowedExtensions = allowedExtensions; diff --git a/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java b/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java index 585b2459..abe28873 100644 --- a/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java +++ b/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java @@ -111,6 +111,7 @@ public void onActivityStopped(final Activity activity) { private Activity activity; private MethodChannel channel; private static String fileType; + private static String inputFile; private static boolean isMultipleSelection = false; private static boolean withData = false; @@ -159,6 +160,9 @@ public void onMethodCall(final MethodCall call, final MethodChannel.Result rawRe if (fileType == null) { result.notImplemented(); + } else if (fileType == "save") { + inputFile = (String) arguments.get("inputFile"); + allowedExtensions = FileUtils.getMimeTypes((ArrayList) arguments.get("allowedExtensions")); } else if (fileType != "dir") { isMultipleSelection = (boolean) arguments.get("allowMultipleSelection"); withData = (boolean) arguments.get("withData"); @@ -168,7 +172,7 @@ public void onMethodCall(final MethodCall call, final MethodChannel.Result rawRe if (call.method != null && call.method.equals("custom") && (allowedExtensions == null || allowedExtensions.length == 0)) { result.error(TAG, "Unsupported filter. Make sure that you are only using the extension without the dot, (ie., jpg instead of .jpg). This could also have happened because you are using an unsupported file extension. If the problem persists, you may want to consider using FileType.all instead.", null); } else { - this.delegate.startFileExplorer(fileType, isMultipleSelection, withData, allowedExtensions, result); + this.delegate.startFileExplorer(fileType, inputFile, isMultipleSelection, withData, allowedExtensions, result); } } @@ -189,6 +193,9 @@ private static String resolveType(final String type) { return "*/*"; case "dir": return "dir"; + case "save": + return "save"; + default: return null; } diff --git a/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java b/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java index 91eea20f..b674145c 100644 --- a/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java +++ b/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java @@ -7,11 +7,13 @@ import android.net.Uri; import android.os.Build; import android.os.Environment; +import android.os.storage.StorageVolume; import android.os.storage.StorageManager; import android.provider.DocumentsContract; import android.provider.OpenableColumns; import android.util.Log; import android.webkit.MimeTypeMap; +import java.util.List; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -24,6 +26,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.lang.reflect.Array; import java.lang.reflect.Method; import java.util.ArrayList; @@ -178,6 +181,60 @@ public static FileInfo openFileStream(final Context context, final Uri uri, bool return fileInfo.build(); } + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + @Nullable + @SuppressWarnings("deprecation") + public static String getAbsolutePathFromUri(Uri uri, String input, Context context) { + String absolutePath = null; + if (DocumentsContract.isDocumentUri(context, uri)) { + + // DocumentProvider + String documentId = DocumentsContract.getDocumentId(uri); + String[] split = documentId.split(":"); + String authority = split[0]; + String path = split[1]; + + try { + StorageManager storageManager = + (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); + StorageVolume storageVolume = null; + List storageVolumes = storageManager.getStorageVolumes(); + for (StorageVolume volume : storageVolumes) { + if ( + PRIMARY_VOLUME_NAME.equals(authority) || + (volume.getUuid() != null && volume.getUuid().equals(authority)) + ) { + storageVolume = volume; + break; + } + } + + if (storageVolume != null) { + String rootPath = storageVolume.getDirectory().getAbsolutePath(); + absolutePath = rootPath + "/" + path; + try { + File iFile = new File(input); + if (iFile.exists() && iFile.isFile()) { + OutputStream outputStream = context.getContentResolver().openOutputStream(uri); + InputStream inputStream = new FileInputStream(input); + byte[] buffer = new byte[1024]; + int bytesRead = 0; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + return absolutePath; + } + @RequiresApi(api = Build.VERSION_CODES.KITKAT) @Nullable @SuppressWarnings("deprecation") @@ -307,4 +364,4 @@ private static String getDocumentPathFromTreeUri(final Uri treeUri) { else return File.separator; } -} \ No newline at end of file +} diff --git a/lib/src/file_picker_io.dart b/lib/src/file_picker_io.dart index 2dc5df50..0d235943 100644 --- a/lib/src/file_picker_io.dart +++ b/lib/src/file_picker_io.dart @@ -42,6 +42,21 @@ class FilePickerIO extends FilePicker { withReadStream, ); + @override + Future saveFile({ + String? dialogTitle, + String? fileName, + String? initialDirectory, + FileType type = FileType.any, + List? allowedExtensions, + bool lockParentWindow = false, + }) => + _getSavePath( + fileName!, + type, + allowedExtensions, + ); + @override Future clearTemporaryFiles() async => _channel.invokeMethod('clear'); @@ -122,4 +137,32 @@ class FilePickerIO extends FilePicker { rethrow; } } + + Future _getSavePath( + String inputFile, + FileType fileType, + List? allowedExtensions, + ) async { + final String type = fileType.name; + if (type != 'custom' && (allowedExtensions?.isNotEmpty ?? false)) { + throw Exception( + 'You are setting a type [$fileType]. Custom extension filters are only allowed with FileType.custom, please change it or remove filters.'); + } + try { + return await _channel.invokeMethod('save', { + 'inputFile': inputFile, + 'allowedExtensions': allowedExtensions, + }); + } on PlatformException catch (e) { + if (e.code == "unknown_path") { + print( + '[$_tag] Could not resolve directory path. Maybe it\'s a protected one or unsupported (such as Downloads folder). If you are on Android, make sure that you are on SDK 21 or above.'); + } + print('[$_tag] Platform exception: $e'); + } catch (e) { + print( + '[$_tag] Unsupported operation. Method not found. The exception thrown was: $e'); + } + return null; + } } diff --git a/pubspec.yaml b/pubspec.yaml index 3e297bdd..8f38dd9f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A package that allows you to use a native file explorer to pick sin homepage: https://github.com/miguelpruivo/plugins_flutter_file_picker repository: https://github.com/miguelpruivo/flutter_file_picker issue_tracker: https://github.com/miguelpruivo/flutter_file_picker/issues -version: 6.0.0 +version: 6.1.0 dependencies: flutter: