diff --git a/CHANGELOG.md b/CHANGELOG.md index 74043dca..52319853 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ + ## 7.0.2 ### Desktop (Linux) File picker extensions for Linux Zenity are case insensitive now Fixes [#1322](https://github.com/miguelpruivo/flutter_file_picker/issues/1322) +## 7.0.0 +### Mobile (Android, iOS) +Save file to mobile platforms with `bytes`. + ## 6.2.1 ### Desktop (Windows) The `initialDirectory` parameter of `getDirectoryPath()` now works ([#970](https://github.com/miguelpruivo/flutter_file_picker/issues/970)). diff --git a/README.md b/README.md index 6dd18f55..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() | :x: | :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/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java b/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java index 5b8efb91..5e5eab17 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,10 +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.util.ArrayList; import java.util.HashMap; import java.util.Objects; @@ -31,6 +34,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; @@ -42,6 +46,8 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener private String[] allowedExtensions; private EventChannel.EventSink eventSink; + private byte[] bytes; + public FilePickerDelegate(final Activity activity) { this( activity, @@ -76,6 +82,38 @@ 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(); + if (uri != null) { + 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()); + } + } + + } + if (resultCode == Activity.RESULT_CANCELED) { + Log.i(TAG, "User cancelled the save request"); + finishWithSuccess(null); + } + return false; + } + + // Pick files if (type == null) { return false; } @@ -290,6 +328,39 @@ 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); + 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); + } + this.bytes = bytes; + if (type != null && !"dir".equals(type) && type.split(",").length == 1) { + intent.setType(type); + } else { + intent.setType("*/*"); + } + if (initialDirectory != null && !initialDirectory.isEmpty()) { + 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); + } + 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) { this.dispatchEventStatus(false); 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 235e1146..86e383a4 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 @@ -156,6 +156,16 @@ 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")); + byte[] bytes = (byte[]) arguments.get("bytes"); + this.delegate.saveFile(fileName, type, initialDirectory, allowedExtensions, bytes,result); + return; + } + fileType = FilePickerPlugin.resolveType(call.method); String[] allowedExtensions = null; diff --git a/ios/Classes/FilePickerPlugin.m b/ios/Classes/FilePickerPlugin.m index be6bc423..3f9c7d1a 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"]; + FlutterStandardTypedData *bytes = [arguments valueForKey:@"bytes"]; + [self saveFileWithName:fileName fileType:fileType initialDirectory:initialDirectory bytes: bytes]; } else { result(FlutterMethodNotImplemented); _result = nil; @@ -160,9 +167,40 @@ - (NSString*)getDocumentDirectory { #pragma mark - Resolvers +- (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]; + 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; + } + } + 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; + self.documentPickerController.presentationController.delegate = self; + if(@available(iOS 13, *)){ + if(![[NSNull null] isEqual:initialDirectory] && ![@"" isEqualToString:initialDirectory]){ + 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 +387,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 diff --git a/lib/src/file_picker.dart b/lib/src/file_picker.dart index 63146cb4..4d16fb21 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'; @@ -159,12 +160,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 file with [bytes] 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. /// @@ -192,6 +192,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 71d5d73b..cc9b4885 100644 --- a/lib/src/file_picker_io.dart +++ b/lib/src/file_picker_io.dart @@ -131,4 +131,33 @@ class FilePickerIO extends FilePicker { rethrow; } } + + @override + Future saveFile( + {String? dialogTitle, + String? fileName, + String? initialDirectory, + FileType type = FileType.any, + List? allowedExtensions, + Uint8List? bytes, + bool lockParentWindow = false}) { + if (Platform.isIOS || Platform.isAndroid) { + return _channel.invokeMethod("save", { + "fileName": fileName, + "fileType": type.name, + "initialDirectory": initialDirectory, + "allowedExtensions": allowedExtensions, + "bytes": bytes, + }); + } + return super.saveFile( + dialogTitle: dialogTitle, + fileName: fileName, + 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 8bb9a7ab..2dc535b4 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'; @@ -81,6 +84,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 36b278a1..726c7a72 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'; @@ -80,6 +81,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 b854161e..8e792b00 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();