Skip to content

Commit

Permalink
Merge pull request #1452 from Samoy/master
Browse files Browse the repository at this point in the history
Save file to mobile platforms.
  • Loading branch information
Miguel Ruivo authored Mar 20, 2024
2 parents e777abe + f28bde1 commit a84f497
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 7 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>) 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;

Expand Down
45 changes: 44 additions & 1 deletion ios/Classes/FilePickerPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -349,6 +387,11 @@ - (void)documentPicker:(UIDocumentPickerViewController *)controller
if(_result == nil) {
return;
}
if(self.isSaveFile){
_result(urls[0].path);
_result = nil;
return;
}
NSMutableArray<NSURL *> *newUrls = [NSMutableArray new];
for (NSURL *url in urls) {
// Create file URL to temporary folder
Expand Down
11 changes: 6 additions & 5 deletions lib/src/file_picker.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -192,6 +192,7 @@ abstract class FilePicker extends PlatformInterface {
String? initialDirectory,
FileType type = FileType.any,
List<String>? allowedExtensions,
Uint8List? bytes,
bool lockParentWindow = false,
}) async =>
throw UnimplementedError('saveFile() has not been implemented.');
Expand Down
29 changes: 29 additions & 0 deletions lib/src/file_picker_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,33 @@ class FilePickerIO extends FilePicker {
rethrow;
}
}

@override
Future<String?> saveFile(
{String? dialogTitle,
String? fileName,
String? initialDirectory,
FileType type = FileType.any,
List<String>? 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,
);
}
}
4 changes: 4 additions & 0 deletions lib/src/file_picker_macos.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -81,6 +84,7 @@ class FilePickerMacOS extends FilePicker {
String? initialDirectory,
FileType type = FileType.any,
List<String>? allowedExtensions,
Uint8List? bytes,
bool lockParentWindow = false,
}) async {
final String executable = await isExecutableOnPath('osascript');
Expand Down
2 changes: 2 additions & 0 deletions lib/src/linux/file_picker_linux.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -80,6 +81,7 @@ class FilePickerLinux extends FilePicker {
String? initialDirectory,
FileType type = FileType.any,
List<String>? allowedExtensions,
Uint8List? bytes,
bool lockParentWindow = false,
}) async {
final executable = await _getPathToExecutable();
Expand Down
1 change: 1 addition & 0 deletions lib/src/windows/file_picker_windows.dart
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ class FilePickerWindows extends FilePicker {
String? initialDirectory,
FileType type = FileType.any,
List<String>? allowedExtensions,
Uint8List? bytes,
bool lockParentWindow = false,
}) async {
final port = ReceivePort();
Expand Down

0 comments on commit a84f497

Please sign in to comment.