From 6d830d9da628e2c1859a964e80adbf662bddefad Mon Sep 17 00:00:00 2001 From: Samoy Date: Wed, 7 Feb 2024 11:17:42 +0800 Subject: [PATCH 1/7] feat: save file to android. --- README.md | 2 +- .../plugin/filepicker/FilePickerDelegate.java | 45 +++++++++++++++++++ .../plugin/filepicker/FilePickerPlugin.java | 9 ++++ lib/src/file_picker.dart | 9 ++-- lib/src/file_picker_io.dart | 25 +++++++++++ 5 files changed, 84 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6dd18f55..3f4cd9fd 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ If you have any feature that you want to see in this package, please feel free t | clearTemporaryFiles() | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | :x: | :x: | | getDirectoryPath() | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | | pickFiles() | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| saveFile() | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | +| saveFile() | :heavy_check_mark: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | See the [API section of the File Picker Wiki](https://github.com/miguelpruivo/flutter_file_picker/wiki/api) or the [official API reference on pub.dev](https://pub.dev/documentation/file_picker/latest/file_picker/FilePicker-class.html) for further details. 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..ae29a1c4 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 @@ -30,6 +30,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener private static final String TAG = "FilePickerDelegate"; private static final int REQUEST_CODE = (FilePickerPlugin.class.hashCode() + 43) & 0x0000ffff; + private static final int SAVE_FILE_CODE = (FilePickerPlugin.class.hashCode() + 83) & 0x0000ffff; private final Activity activity; private final PermissionManager permissionManager; @@ -74,7 +75,22 @@ public void setEventHandler(final EventChannel.EventSink eventSink) { @Override public boolean onActivityResult(final int requestCode, final int resultCode, final Intent data) { + // Save file + if (requestCode == SAVE_FILE_CODE) { + if (resultCode == Activity.RESULT_OK) { + this.dispatchEventStatus(true); + final Uri uri = data.getData(); + finishWithSuccess(uri.toString()); + return true; + } + if (resultCode == Activity.RESULT_CANCELED) { + Log.i(TAG, "User cancelled the save request"); + finishWithSuccess(null); + } + return false; + } + // Pick files if(type == null) { return false; } @@ -284,6 +300,35 @@ public void startFileExplorer(final String type, final boolean isMultipleSelecti this.startFileExplorer(); } + public void saveFile(String fileName, String type, String initialDirectory, String[] allowedExtensions, MethodChannel.Result result) { + if (!this.setPendingMethodCallAndResult(result)) { + finishWithAlreadyActiveError(result); + return; + } + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + if (fileName != null && !fileName.isEmpty()) { + intent.putExtra(Intent.EXTRA_TITLE, fileName); + } + if (type != null && !"dir".equals(type) && type.split(",").length == 1) { + intent.setType(type); + } else { + intent.setType("*/*"); + } + if (initialDirectory != null && !initialDirectory.isEmpty()) { + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.parse(initialDirectory)); + } + if (allowedExtensions != null && allowedExtensions.length > 0) { + intent.putExtra(Intent.EXTRA_MIME_TYPES, allowedExtensions); + } + if (intent.resolveActivity(this.activity.getPackageManager()) != null) { + this.activity.startActivityForResult(intent, SAVE_FILE_CODE); + } else { + Log.e(TAG, "Can't find a valid activity to handle the request. Make sure you've a file explorer installed."); + finishWithError("invalid_format_type", "Can't handle the provided file type."); + } + } + @SuppressWarnings("unchecked") private void finishWithSuccess(Object data) { 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..0bddc67d 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 @@ -154,6 +154,15 @@ public void onMethodCall(final MethodCall call, final MethodChannel.Result rawRe return; } + if (call.method != null && call.method.equals("save")) { + String fileName = (String) arguments.get("fileName"); + String type = resolveType((String) arguments.get("fileType")); + String initialDirectory = (String) arguments.get("initialDirectory"); + String[] allowedExtensions = FileUtils.getMimeTypes((ArrayList) arguments.get("allowedExtensions")); + this.delegate.saveFile(fileName, type, initialDirectory, allowedExtensions, result); + return; + } + fileType = FilePickerPlugin.resolveType(call.method); String[] allowedExtensions = null; diff --git a/lib/src/file_picker.dart b/lib/src/file_picker.dart index e3d32dca..7b1802ec 100644 --- a/lib/src/file_picker.dart +++ b/lib/src/file_picker.dart @@ -158,12 +158,11 @@ abstract class FilePicker extends PlatformInterface { /// Opens a save file dialog which lets the user select a file path and a file /// name to save a file. /// - /// This function does not actually save a file. It only opens the dialog to - /// let the user choose a location and file name. This function only returns - /// the **path** to this (non-existing) file. + /// For mobile platforms, this function will save an empty file to return a path. /// - /// This method is only available on desktop platforms (Linux, macOS & - /// Windows). + /// For desktop platforms (Linux, macOS & Windows),This function does not actually + /// save a file. It only opens the dialog to let the user choose a location and + /// file name. This function only returns the **path** to this (non-existing) file. /// /// [dialogTitle] can be set to display a custom title on desktop platforms. /// diff --git a/lib/src/file_picker_io.dart b/lib/src/file_picker_io.dart index 3560477c..5924db32 100644 --- a/lib/src/file_picker_io.dart +++ b/lib/src/file_picker_io.dart @@ -123,4 +123,29 @@ class FilePickerIO extends FilePicker { rethrow; } } + + @override + Future saveFile( + {String? dialogTitle, + String? fileName, + String? initialDirectory, + FileType type = FileType.any, + List? allowedExtensions, + bool lockParentWindow = false}) { + if (Platform.isIOS || Platform.isAndroid) { + return _channel.invokeMethod("save", { + "fileName": fileName, + "fileType": type.name, + "allowedExtensions": allowedExtensions, + }); + } + return super.saveFile( + dialogTitle: dialogTitle, + fileName: fileName, + initialDirectory: initialDirectory, + type: type, + allowedExtensions: allowedExtensions, + lockParentWindow: lockParentWindow, + ); + } } From c8f5b61128e9da43ab0a20a599f4aca49bea256c Mon Sep 17 00:00:00 2001 From: Samoy Date: Thu, 8 Feb 2024 10:32:39 +0800 Subject: [PATCH 2/7] fix: extract real path for android. --- .../mr/flutter/plugin/filepicker/FilePickerDelegate.java | 7 ++++++- lib/src/file_picker_io.dart | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) 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 ae29a1c4..84141a72 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 @@ -80,7 +80,12 @@ public boolean onActivityResult(final int requestCode, final int resultCode, fin if (resultCode == Activity.RESULT_OK) { this.dispatchEventStatus(true); final Uri uri = data.getData(); - finishWithSuccess(uri.toString()); + String path = null; + if (uri != null) { + path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + .getAbsolutePath() + File.separator + FileUtils.getFileName(uri, this.activity); + } + finishWithSuccess(path); return true; } if (resultCode == Activity.RESULT_CANCELED) { diff --git a/lib/src/file_picker_io.dart b/lib/src/file_picker_io.dart index 5924db32..7a2747b2 100644 --- a/lib/src/file_picker_io.dart +++ b/lib/src/file_picker_io.dart @@ -136,6 +136,7 @@ class FilePickerIO extends FilePicker { return _channel.invokeMethod("save", { "fileName": fileName, "fileType": type.name, + "initialDirectory": initialDirectory, "allowedExtensions": allowedExtensions, }); } From 0c38bbdcf9ae7cea6d2bfd70875084b734538416 Mon Sep 17 00:00:00 2001 From: samoy Date: Sun, 3 Mar 2024 11:15:51 +0800 Subject: [PATCH 3/7] feat: save file to ios. --- README.md | 2 +- ios/Classes/FilePickerPlugin.m | 43 +++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3f4cd9fd..6ae502d2 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ If you have any feature that you want to see in this package, please feel free t | clearTemporaryFiles() | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | :x: | :x: | | getDirectoryPath() | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | | pickFiles() | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| saveFile() | :heavy_check_mark: | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | +| saveFile() | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | See the [API section of the File Picker Wiki](https://github.com/miguelpruivo/flutter_file_picker/wiki/api) or the [official API reference on pub.dev](https://pub.dev/documentation/file_picker/latest/file_picker/FilePicker-class.html) for further details. diff --git a/ios/Classes/FilePickerPlugin.m b/ios/Classes/FilePickerPlugin.m index be6bc423..fa545d89 100644 --- a/ios/Classes/FilePickerPlugin.m +++ b/ios/Classes/FilePickerPlugin.m @@ -23,6 +23,7 @@ @interface FilePickerPlugin() @property (nonatomic) BOOL loadDataToMemory; @property (nonatomic) BOOL allowCompression; @property (nonatomic) dispatch_group_t group; +@property (nonatomic) BOOL isSaveFile; @end @implementation FilePickerPlugin @@ -147,6 +148,12 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { message:@"Support for the Audio picker is not compiled in. Remove the Pod::PICKER_AUDIO=false statement from your Podfile." details:nil]); #endif + } else if([call.method isEqualToString:@"save"]) { + NSString *fileName = [arguments valueForKey:@"fileName"]; + NSString *fileType = [arguments valueForKey:@"fileType"]; + NSString *initialDirectory = [arguments valueForKey:@"initialDirectory"]; + NSArray *allowedExtensions = [arguments valueForKey:@"allowedExtensions"]; + [self saveFileWithName:fileName fileType:fileType initialDirectory:initialDirectory allowedExtensions:allowedExtensions]; } else { result(FlutterMethodNotImplemented); _result = nil; @@ -160,9 +167,38 @@ - (NSString*)getDocumentDirectory { #pragma mark - Resolvers +- (void)saveFileWithName:(NSString*)fileName fileType:(NSString *)fileType initialDirectory:(NSString*)initialDirectory allowedExtensions:(NSArray*)allowedExtensions{ + self.isSaveFile = YES; + NSFileManager*fm = [NSFileManager defaultManager]; + NSURL *documentsDirectory = [fm URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0]; + NSURL* destinationPath = [documentsDirectory URLByAppendingPathComponent:fileName]; + NSError *error; + if ([fm fileExistsAtPath:destinationPath.path]) { + [fm removeItemAtURL:destinationPath error:&error]; + if (error != nil) { + _result([FlutterError errorWithCode:@"Failed to remove file" message:[error debugDescription] details:nil]); + error = nil; + } + } + [[NSData data] writeToURL:destinationPath options:NSDataWritingAtomic error:&error]; + if (error!=nil) { + _result([FlutterError errorWithCode:@"Failed to write file" message:[error debugDescription] details:nil]); + error = nil; + } + self.documentPickerController = [[UIDocumentPickerViewController alloc] initWithURL:destinationPath inMode:UIDocumentPickerModeExportToService]; + self.documentPickerController.delegate = self; + self.documentPickerController.presentationController.delegate = self; + if(@available(iOS 13, *)){ + if(![initialDirectory isEqualToString:@""]){ + self.documentPickerController.directoryURL = [NSURL URLWithString:initialDirectory]; + } + } + [[self viewControllerWithWindow:nil] presentViewController:self.documentPickerController animated:YES completion:nil]; +} + #ifdef PICKER_DOCUMENT - (void)resolvePickDocumentWithMultiPick:(BOOL)allowsMultipleSelection pickDirectory:(BOOL)isDirectory { - + self.isSaveFile = NO; @try{ self.documentPickerController = [[UIDocumentPickerViewController alloc] initWithDocumentTypes: isDirectory ? @[@"public.folder"] : self.allowedExtensions @@ -349,6 +385,11 @@ - (void)documentPicker:(UIDocumentPickerViewController *)controller if(_result == nil) { return; } + if(self.isSaveFile){ + _result(urls[0].path); + _result = nil; + return; + } NSMutableArray *newUrls = [NSMutableArray new]; for (NSURL *url in urls) { // Create file URL to temporary folder From 088303a82f960f6d232a8f3be3c009d3b1bfbe06 Mon Sep 17 00:00:00 2001 From: samoy Date: Sun, 3 Mar 2024 14:47:58 +0800 Subject: [PATCH 4/7] fix: save file to ios with initialDirectory crash. --- ios/Classes/FilePickerPlugin.m | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ios/Classes/FilePickerPlugin.m b/ios/Classes/FilePickerPlugin.m index fa545d89..7c8a179a 100644 --- a/ios/Classes/FilePickerPlugin.m +++ b/ios/Classes/FilePickerPlugin.m @@ -169,10 +169,10 @@ - (NSString*)getDocumentDirectory { - (void)saveFileWithName:(NSString*)fileName fileType:(NSString *)fileType initialDirectory:(NSString*)initialDirectory allowedExtensions:(NSArray*)allowedExtensions{ self.isSaveFile = YES; - NSFileManager*fm = [NSFileManager defaultManager]; - NSURL *documentsDirectory = [fm URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0]; + NSFileManager* fm = [NSFileManager defaultManager]; + NSURL* documentsDirectory = [fm URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0]; NSURL* destinationPath = [documentsDirectory URLByAppendingPathComponent:fileName]; - NSError *error; + NSError* error; if ([fm fileExistsAtPath:destinationPath.path]) { [fm removeItemAtURL:destinationPath error:&error]; if (error != nil) { @@ -181,7 +181,7 @@ - (void)saveFileWithName:(NSString*)fileName fileType:(NSString *)fileType initi } } [[NSData data] writeToURL:destinationPath options:NSDataWritingAtomic error:&error]; - if (error!=nil) { + if (error != nil) { _result([FlutterError errorWithCode:@"Failed to write file" message:[error debugDescription] details:nil]); error = nil; } @@ -189,7 +189,7 @@ - (void)saveFileWithName:(NSString*)fileName fileType:(NSString *)fileType initi self.documentPickerController.delegate = self; self.documentPickerController.presentationController.delegate = self; if(@available(iOS 13, *)){ - if(![initialDirectory isEqualToString:@""]){ + if(![[NSNull null] isEqual:initialDirectory] && ![@"" isEqualToString:initialDirectory]){ self.documentPickerController.directoryURL = [NSURL URLWithString:initialDirectory]; } } From 25c9b0d61147375ef584d161b7c02a3ba6b2f509 Mon Sep 17 00:00:00 2001 From: samoy Date: Sun, 3 Mar 2024 19:43:22 +0800 Subject: [PATCH 5/7] feat: save file to mobile with bytes. --- .../plugin/filepicker/FilePickerDelegate.java | 47 +++++++++++++------ .../plugin/filepicker/FilePickerPlugin.java | 3 +- ios/Classes/FilePickerPlugin.m | 16 ++++--- lib/src/file_picker.dart | 4 +- lib/src/file_picker_io.dart | 3 ++ lib/src/file_picker_macos.dart | 4 ++ lib/src/linux/file_picker_linux.dart | 2 + lib/src/windows/file_picker_windows.dart | 1 + 8 files changed, 56 insertions(+), 24 deletions(-) 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 84141a72..20e2815c 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 @@ -18,7 +18,10 @@ import androidx.annotation.VisibleForTesting; import androidx.core.app.ActivityCompat; + import java.io.File; +import java.io.IOException; +import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; @@ -41,6 +44,8 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener private String[] allowedExtensions; private EventChannel.EventSink eventSink; + private byte[] bytes; + public FilePickerDelegate(final Activity activity) { this( activity, @@ -80,13 +85,24 @@ public boolean onActivityResult(final int requestCode, final int resultCode, fin if (resultCode == Activity.RESULT_OK) { this.dispatchEventStatus(true); final Uri uri = data.getData(); - String path = null; if (uri != null) { - path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + String path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) .getAbsolutePath() + File.separator + FileUtils.getFileName(uri, this.activity); + try { + OutputStream outputStream = this.activity.getContentResolver().openOutputStream(uri); + if(outputStream != null){ + outputStream.write(bytes); + outputStream.flush(); + outputStream.close(); + } + finishWithSuccess(path); + return true; + } catch (IOException e) { + Log.i(TAG, "Error while saving file", e); + finishWithError("Error while saving file", e.getMessage()); + } } - finishWithSuccess(path); - return true; + } if (resultCode == Activity.RESULT_CANCELED) { Log.i(TAG, "User cancelled the save request"); @@ -96,7 +112,7 @@ public boolean onActivityResult(final int requestCode, final int resultCode, fin } // Pick files - if(type == null) { + if (type == null) { return false; } @@ -117,7 +133,7 @@ public void run() { final Uri currentUri = data.getClipData().getItemAt(currentItem).getUri(); final FileInfo file = FileUtils.openFileStream(FilePickerDelegate.this.activity, currentUri, loadDataToMemory); - if(file != null) { + if (file != null) { files.add(file); Log.d(FilePickerDelegate.TAG, "[MultiFilePick] File #" + currentItem + " - URI: " + currentUri.getPath()); } @@ -134,7 +150,7 @@ public void run() { Log.d(FilePickerDelegate.TAG, "[SingleFilePick] File URI:" + uri.toString()); final String dirPath = FileUtils.getFullPathFromTreeUri(uri, activity); - if(dirPath != null) { + if (dirPath != null) { finishWithSuccess(dirPath); } else { finishWithError("unknown_path", "Failed to retrieve directory path."); @@ -144,7 +160,7 @@ public void run() { final FileInfo file = FileUtils.openFileStream(FilePickerDelegate.this.activity, uri, loadDataToMemory); - if(file != null) { + if (file != null) { files.add(file); } @@ -155,7 +171,7 @@ public void run() { finishWithError("unknown_path", "Failed to retrieve path."); } - } else if (data.getExtras() != null){ + } else if (data.getExtras() != null) { Bundle bundle = data.getExtras(); if (bundle.keySet().contains("selectedItems")) { ArrayList fileUris = getSelectedItems(bundle); @@ -231,8 +247,8 @@ private static void finishWithAlreadyActiveError(final MethodChannel.Result resu } @SuppressWarnings("deprecation") - private ArrayList getSelectedItems(Bundle bundle){ - if(Build.VERSION.SDK_INT >= 33){ + private ArrayList getSelectedItems(Bundle bundle) { + if (Build.VERSION.SDK_INT >= 33) { return bundle.getParcelableArrayList("selectedItems", Parcelable.class); } @@ -305,7 +321,7 @@ public void startFileExplorer(final String type, final boolean isMultipleSelecti this.startFileExplorer(); } - public void saveFile(String fileName, String type, String initialDirectory, String[] allowedExtensions, MethodChannel.Result result) { + public void saveFile(String fileName, String type, String initialDirectory, String[] allowedExtensions, byte[] bytes, MethodChannel.Result result) { if (!this.setPendingMethodCallAndResult(result)) { finishWithAlreadyActiveError(result); return; @@ -315,6 +331,7 @@ public void saveFile(String fileName, String type, String initialDirectory, Stri if (fileName != null && !fileName.isEmpty()) { intent.putExtra(Intent.EXTRA_TITLE, fileName); } + this.bytes = bytes; if (type != null && !"dir".equals(type) && type.split(",").length == 1) { intent.setType(type); } else { @@ -342,10 +359,10 @@ private void finishWithSuccess(Object data) { // Temporary fix, remove this null-check after Flutter Engine 1.14 has landed on stable if (this.pendingResult != null) { - if(data != null && !(data instanceof String)) { + if (data != null && !(data instanceof String)) { final ArrayList> files = new ArrayList<>(); - for (FileInfo file : (ArrayList)data) { + for (FileInfo file : (ArrayList) data) { files.add(file.toMap()); } data = files; @@ -368,7 +385,7 @@ private void finishWithError(final String errorCode, final String errorMessage) private void dispatchEventStatus(final boolean status) { - if(eventSink == null || type.equals("dir")) { + if (eventSink == null || type.equals("dir")) { return; } 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 0bddc67d..a56ac1d8 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 @@ -159,7 +159,8 @@ public void onMethodCall(final MethodCall call, final MethodChannel.Result rawRe String type = resolveType((String) arguments.get("fileType")); String initialDirectory = (String) arguments.get("initialDirectory"); String[] allowedExtensions = FileUtils.getMimeTypes((ArrayList) arguments.get("allowedExtensions")); - this.delegate.saveFile(fileName, type, initialDirectory, allowedExtensions, result); + byte[] bytes = (byte[]) arguments.get("bytes"); + this.delegate.saveFile(fileName, type, initialDirectory, allowedExtensions, bytes,result); return; } diff --git a/ios/Classes/FilePickerPlugin.m b/ios/Classes/FilePickerPlugin.m index 7c8a179a..3f9c7d1a 100644 --- a/ios/Classes/FilePickerPlugin.m +++ b/ios/Classes/FilePickerPlugin.m @@ -152,8 +152,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { NSString *fileName = [arguments valueForKey:@"fileName"]; NSString *fileType = [arguments valueForKey:@"fileType"]; NSString *initialDirectory = [arguments valueForKey:@"initialDirectory"]; - NSArray *allowedExtensions = [arguments valueForKey:@"allowedExtensions"]; - [self saveFileWithName:fileName fileType:fileType initialDirectory:initialDirectory allowedExtensions:allowedExtensions]; + FlutterStandardTypedData *bytes = [arguments valueForKey:@"bytes"]; + [self saveFileWithName:fileName fileType:fileType initialDirectory:initialDirectory bytes: bytes]; } else { result(FlutterMethodNotImplemented); _result = nil; @@ -167,7 +167,7 @@ - (NSString*)getDocumentDirectory { #pragma mark - Resolvers -- (void)saveFileWithName:(NSString*)fileName fileType:(NSString *)fileType initialDirectory:(NSString*)initialDirectory allowedExtensions:(NSArray*)allowedExtensions{ +- (void)saveFileWithName:(NSString*)fileName fileType:(NSString *)fileType initialDirectory:(NSString*)initialDirectory bytes:(FlutterStandardTypedData*)bytes{ self.isSaveFile = YES; NSFileManager* fm = [NSFileManager defaultManager]; NSURL* documentsDirectory = [fm URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0]; @@ -180,10 +180,12 @@ - (void)saveFileWithName:(NSString*)fileName fileType:(NSString *)fileType initi error = nil; } } - [[NSData data] writeToURL:destinationPath options:NSDataWritingAtomic error:&error]; - if (error != nil) { - _result([FlutterError errorWithCode:@"Failed to write file" message:[error debugDescription] details:nil]); - error = nil; + if(bytes != nil){ + [bytes.data writeToURL:destinationPath options:NSDataWritingAtomic error:&error]; + if (error != nil) { + _result([FlutterError errorWithCode:@"Failed to write file" message:[error debugDescription] details:nil]); + error = nil; + } } self.documentPickerController = [[UIDocumentPickerViewController alloc] initWithURL:destinationPath inMode:UIDocumentPickerModeExportToService]; self.documentPickerController.delegate = self; diff --git a/lib/src/file_picker.dart b/lib/src/file_picker.dart index 7b1802ec..e1067180 100644 --- a/lib/src/file_picker.dart +++ b/lib/src/file_picker.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; import 'package:file_picker/src/file_picker_io.dart'; import 'package:file_picker/src/file_picker_macos.dart'; @@ -158,7 +159,7 @@ abstract class FilePicker extends PlatformInterface { /// Opens a save file dialog which lets the user select a file path and a file /// name to save a file. /// - /// For mobile platforms, this function will save an empty file to return a path. + /// For mobile platforms, this function will save file with [bytes] to return a path. /// /// For desktop platforms (Linux, macOS & Windows),This function does not actually /// save a file. It only opens the dialog to let the user choose a location and @@ -190,6 +191,7 @@ abstract class FilePicker extends PlatformInterface { String? initialDirectory, FileType type = FileType.any, List? allowedExtensions, + Uint8List? bytes, bool lockParentWindow = false, }) async => throw UnimplementedError('saveFile() has not been implemented.'); diff --git a/lib/src/file_picker_io.dart b/lib/src/file_picker_io.dart index 7a2747b2..a9a7c77e 100644 --- a/lib/src/file_picker_io.dart +++ b/lib/src/file_picker_io.dart @@ -131,6 +131,7 @@ class FilePickerIO extends FilePicker { String? initialDirectory, FileType type = FileType.any, List? allowedExtensions, + Uint8List? bytes, bool lockParentWindow = false}) { if (Platform.isIOS || Platform.isAndroid) { return _channel.invokeMethod("save", { @@ -138,6 +139,7 @@ class FilePickerIO extends FilePicker { "fileType": type.name, "initialDirectory": initialDirectory, "allowedExtensions": allowedExtensions, + "bytes": bytes, }); } return super.saveFile( @@ -146,6 +148,7 @@ class FilePickerIO extends FilePicker { initialDirectory: initialDirectory, type: type, allowedExtensions: allowedExtensions, + bytes: bytes, lockParentWindow: lockParentWindow, ); } diff --git a/lib/src/file_picker_macos.dart b/lib/src/file_picker_macos.dart index bbcbd41e..fe6f70a4 100644 --- a/lib/src/file_picker_macos.dart +++ b/lib/src/file_picker_macos.dart @@ -1,3 +1,6 @@ +import 'dart:ffi'; +import 'dart:typed_data'; + import 'package:file_picker/file_picker.dart'; import 'package:file_picker/src/utils.dart'; @@ -80,6 +83,7 @@ class FilePickerMacOS extends FilePicker { String? initialDirectory, FileType type = FileType.any, List? allowedExtensions, + Uint8List? bytes, bool lockParentWindow = false, }) async { final String executable = await isExecutableOnPath('osascript'); diff --git a/lib/src/linux/file_picker_linux.dart b/lib/src/linux/file_picker_linux.dart index 0892aa0a..c73190b8 100644 --- a/lib/src/linux/file_picker_linux.dart +++ b/lib/src/linux/file_picker_linux.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:file_picker/src/file_picker.dart'; import 'package:file_picker/src/file_picker_result.dart'; import 'package:file_picker/src/linux/dialog_handler.dart'; @@ -79,6 +80,7 @@ class FilePickerLinux extends FilePicker { String? initialDirectory, FileType type = FileType.any, List? allowedExtensions, + Uint8List? bytes, bool lockParentWindow = false, }) async { final executable = await _getPathToExecutable(); diff --git a/lib/src/windows/file_picker_windows.dart b/lib/src/windows/file_picker_windows.dart index 12969e81..c1e39150 100644 --- a/lib/src/windows/file_picker_windows.dart +++ b/lib/src/windows/file_picker_windows.dart @@ -166,6 +166,7 @@ class FilePickerWindows extends FilePicker { String? initialDirectory, FileType type = FileType.any, List? allowedExtensions, + Uint8List? bytes, bool lockParentWindow = false, }) async { final port = ReceivePort(); From 39eac4224ce4f579cd32af6e3af2c51909492e75 Mon Sep 17 00:00:00 2001 From: samoy Date: Tue, 5 Mar 2024 09:18:14 +0800 Subject: [PATCH 6/7] fix: resolve conflict. --- .../plugin/filepicker/FilePickerDelegate.java | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) 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 8391cfa7..9ee50c5b 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 @@ -15,15 +15,13 @@ import android.provider.DocumentsContract; import android.util.Log; +import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import androidx.core.app.ActivityCompat; - import java.io.File; import java.io.IOException; import java.io.OutputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; @@ -316,7 +314,6 @@ public void startFileExplorer(final String type, final boolean isMultipleSelecti finishWithAlreadyActiveError(result); return; } - this.type = type; this.isMultipleSelection = isMultipleSelection; this.loadDataToMemory = withData; @@ -334,6 +331,7 @@ public void startFileExplorer(final String type, final boolean isMultipleSelecti this.startFileExplorer(); } + @RequiresApi(api = Build.VERSION_CODES.KITKAT) public void saveFile(String fileName, String type, String initialDirectory, String[] allowedExtensions, byte[] bytes, MethodChannel.Result result) { if (!this.setPendingMethodCallAndResult(result)) { finishWithAlreadyActiveError(result); @@ -351,7 +349,9 @@ public void saveFile(String fileName, String type, String initialDirectory, Stri intent.setType("*/*"); } if (initialDirectory != null && !initialDirectory.isEmpty()) { - intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.parse(initialDirectory)); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.parse(initialDirectory)); + } } if (allowedExtensions != null && allowedExtensions.length > 0) { intent.putExtra(Intent.EXTRA_MIME_TYPES, allowedExtensions); @@ -366,7 +366,6 @@ public void saveFile(String fileName, String type, String initialDirectory, Stri @SuppressWarnings("unchecked") private void finishWithSuccess(Object data) { - this.dispatchEventStatus(false); // Temporary fix, remove this null-check after Flutter Engine 1.14 has landed on stable @@ -397,7 +396,7 @@ private void finishWithError(final String errorCode, final String errorMessage) private void dispatchEventStatus(final boolean status) { - if (eventSink == null || type.equals("dir")) { + if(eventSink == null || type.equals("dir")) { return; } From 21004d057fd2baaa7544994c6d3c7c43e63dc4b5 Mon Sep 17 00:00:00 2001 From: samoy Date: Tue, 5 Mar 2024 09:23:39 +0800 Subject: [PATCH 7/7] release: update major version and changelog. --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94d64a83..dff80862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 7.0.0 +### Mobile (Android, iOS) +Save file to mobile platforms with `bytes`. + ## 6.2.0 ### Android Add ability to compress images on android by specifying a compression quality value ([#735] diff --git a/pubspec.yaml b/pubspec.yaml index 4c9d6537..3c1e114c 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.2.0 +version: 7.0.0 dependencies: flutter: