From 2378cd542237a1f8666387c93d66b16bfccc6ac1 Mon Sep 17 00:00:00 2001 From: Miguel Ruivo Date: Sun, 5 Apr 2020 17:01:31 +0100 Subject: [PATCH] Adds multiple file extensions support (Android) --- .../plugin/filepicker/FilePickerDelegate.java | 9 +++- .../plugin/filepicker/FilePickerPlugin.java | 28 +++++------ .../flutter/plugin/filepicker/FileUtils.java | 27 ++++++++++- example/lib/src/file_picker_demo.dart | 44 +++++++---------- lib/file_picker.dart | 47 ++++++++++--------- 5 files changed, 88 insertions(+), 67 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 b8adfc1..907ad7a 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 @@ -27,6 +27,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener private MethodChannel.Result pendingResult; private boolean isMultipleSelection = false; private String type; + private String[] allowedExtensions; public FilePickerDelegate(final Activity activity) { this( @@ -150,11 +151,16 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener intent = new Intent(Intent.ACTION_GET_CONTENT); final Uri uri = Uri.parse(Environment.getExternalStorageDirectory().getPath() + File.separator); + intent.setDataAndType(uri, this.type); intent.setType(this.type); intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, this.isMultipleSelection); intent.addCategory(Intent.CATEGORY_OPENABLE); + if (allowedExtensions != null) { + intent.putExtra(Intent.EXTRA_MIME_TYPES, allowedExtensions); + } + if (intent.resolveActivity(this.activity.getPackageManager()) != null) { this.activity.startActivityForResult(intent, REQUEST_CODE); } else { @@ -164,7 +170,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener } @SuppressWarnings("deprecation") - public void startFileExplorer(final String type, final boolean isMultipleSelection, final MethodChannel.Result result) { + public void startFileExplorer(final String type, final boolean isMultipleSelection, final String[] allowedExtensions, final MethodChannel.Result result) { if (!this.setPendingMethodCallAndResult(result)) { finishWithAlreadyActiveError(result); @@ -173,6 +179,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener this.type = type; this.isMultipleSelection = isMultipleSelection; + this.allowedExtensions = allowedExtensions; if (!this.permissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) { this.permissionManager.askForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_CODE); 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 5b14950..9e79e0e 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 @@ -5,14 +5,15 @@ import android.app.Application; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.util.Log; -import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; +import java.util.ArrayList; +import java.util.HashMap; + import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; @@ -133,6 +134,7 @@ public class FilePickerPlugin implements MethodChannel.MethodCallHandler, Flutte } + @SuppressWarnings("unchecked") @Override public void onMethodCall(final MethodCall call, final MethodChannel.Result rawResult) { @@ -142,30 +144,25 @@ public class FilePickerPlugin implements MethodChannel.MethodCallHandler, Flutte } final MethodChannel.Result result = new MethodResultWrapper(rawResult); + final HashMap arguments = (HashMap) call.arguments; + fileType = FilePickerPlugin.resolveType(call.method); - isMultipleSelection = (boolean) call.arguments; + isMultipleSelection = (boolean) arguments.get("allowMultipleSelection"); + + final String[] allowedExtensions = FileUtils.getMimeTypes((ArrayList) arguments.get("allowedExtensions")); if (fileType == null) { result.notImplemented(); - } else if (fileType.equals("unsupported")) { - 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 if (fileType == "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, result); + this.delegate.startFileExplorer(fileType, isMultipleSelection, allowedExtensions, result); } } private static String resolveType(final String type) { - final boolean isCustom = type.contains("__CUSTOM_"); - - if (isCustom) { - final String extension = type.split("__CUSTOM_")[1].toLowerCase(); - String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - mime = mime == null ? "unsupported" : mime; - Log.i(TAG, "Custom file type: " + mime); - return mime; - } switch (type) { case "AUDIO": @@ -175,6 +172,7 @@ public class FilePickerPlugin implements MethodChannel.MethodCallHandler, Flutte case "VIDEO": return "video/*"; case "ANY": + case "CUSTOM": return "*/*"; 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 ce310da..a03ad87 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 @@ -11,11 +11,13 @@ import android.provider.DocumentsContract; import android.provider.MediaStore; import android.text.TextUtils; import android.util.Log; +import android.webkit.MimeTypeMap; import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Random; public class FileUtils { @@ -121,6 +123,27 @@ public class FileUtils { return null; } + public static String[] getMimeTypes(final ArrayList allowedExtensions) { + + if (allowedExtensions == null || allowedExtensions.isEmpty()) { + return null; + } + + final ArrayList mimes = new ArrayList<>(); + + for (int i = 0; i < allowedExtensions.size(); i++) { + final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(allowedExtensions.get(i)); + if (mime == null) { + Log.w(TAG, "Custom file type " + allowedExtensions.get(i) + " is unsupported and will be ignored."); + continue; + } + + mimes.add(mime); + } + Log.d(TAG, "Allowed file extensions mimes: " + mimes); + return mimes.toArray(new String[0]); + } + private static String getDataColumn(final Context context, final Uri uri, final String selection, final String[] selectionArgs) { Cursor cursor = null; @@ -167,7 +190,9 @@ public class FileUtils { } } } finally { - cursor.close(); + if (cursor != null) { + cursor.close(); + } } } diff --git a/example/lib/src/file_picker_demo.dart b/example/lib/src/file_picker_demo.dart index 3aa2f09..051c29f 100644 --- a/example/lib/src/file_picker_demo.dart +++ b/example/lib/src/file_picker_demo.dart @@ -15,7 +15,6 @@ class _FilePickerDemoState extends State { String _extension; bool _loadingPath = false; bool _multiPick = false; - bool _hasValidMime = false; FileType _pickingType; TextEditingController _controller = new TextEditingController(); @@ -26,25 +25,23 @@ class _FilePickerDemoState extends State { } void _openFileExplorer() async { - if (_pickingType != FileType.custom || _hasValidMime) { - setState(() => _loadingPath = true); - try { - if (_multiPick) { - _path = null; - _paths = await FilePicker.getMultiFilePath(type: _pickingType, fileExtension: _extension); - } else { - _paths = null; - _path = await FilePicker.getFilePath(type: _pickingType, fileExtension: _extension); - } - } on PlatformException catch (e) { - print("Unsupported operation" + e.toString()); + setState(() => _loadingPath = true); + try { + if (_multiPick) { + _path = null; + _paths = await FilePicker.getMultiFilePath(type: _pickingType, allowedExtensions: _extension?.replaceAll(' ', '')?.split(',')); + } else { + _paths = null; + _path = await FilePicker.getFilePath(type: _pickingType, allowedExtensions: _extension?.replaceAll(' ', '')?.split(',')); } - if (!mounted) return; - setState(() { - _loadingPath = false; - _fileName = _path != null ? _path.split('/').last : _paths != null ? _paths.keys.toString() : '...'; - }); + } on PlatformException catch (e) { + print("Unsupported operation" + e.toString()); } + if (!mounted) return; + setState(() { + _loadingPath = false; + _fileName = _path != null ? _path.split('/').last : _paths != null ? _paths.keys.toString() : '...'; + }); } @override @@ -91,7 +88,7 @@ class _FilePickerDemoState extends State { onChanged: (value) => setState(() { _pickingType = value; if (_pickingType != FileType.custom) { - _controller.text = _extension = ''; + _controller.text = _extension = null; } })), ), @@ -105,15 +102,6 @@ class _FilePickerDemoState extends State { decoration: InputDecoration(labelText: 'File extension'), keyboardType: TextInputType.text, textCapitalization: TextCapitalization.none, - validator: (value) { - RegExp reg = new RegExp(r'[^a-zA-Z0-9]'); - if (reg.hasMatch(value)) { - _hasValidMime = false; - return 'Invalid format'; - } - _hasValidMime = true; - return null; - }, ) : new Container(), ), diff --git a/lib/file_picker.dart b/lib/file_picker.dart index 1f622e3..81fe85a 100644 --- a/lib/file_picker.dart +++ b/lib/file_picker.dart @@ -19,26 +19,26 @@ class FilePicker { /// Returns an iterable `Map` where the `key` is the name of the file /// and the `value` the path. /// - /// A [fileExtension] can be provided to filter the picking results. - /// If provided, it will be use the `FileType.CUSTOM` for that [fileExtension]. - /// If not, `FileType.ANY` will be used and any combination of files can be multi picked at once. - static Future> getMultiFilePath({FileType type = FileType.any, String fileExtension}) async => - await _getPath(_handleType(type, fileExtension), true); + /// A `List` with [allowedExtensions] can be provided to filter the allowed files to picked. + /// If provided, make sure you select `FileType.custom` as type. + /// Defaults to `FileType.any`, which allows any combination of files to be multi selected at once. + static Future> getMultiFilePath({FileType type = FileType.any, List allowedExtensions}) async => + await _getPath(_handleType(type), true, allowedExtensions); /// Returns an absolute file path from the calling platform. /// - /// A [type] must be provided to filter the picking results. - /// Can be used a custom file type with `FileType.CUSTOM`. A [fileExtension] must be provided (e.g. PDF, SVG, etc.) - /// Defaults to `FileType.ANY` which will display all file types. - static Future getFilePath({FileType type = FileType.any, String fileExtension}) async => - await _getPath(_handleType(type, fileExtension), false); + /// Extension filters are allowed with `FileType.custom`, when used, make sure to provide a `List` + /// of [allowedExtensions] (e.g. [`pdf`, `svg`, `jpg`].). + /// Defaults to `FileType.any` which will display all file types. + static Future getFilePath({FileType type = FileType.any, List allowedExtensions}) async => + await _getPath(_handleType(type), false, allowedExtensions); /// Returns a `File` object from the selected file path. /// /// This is an utility method that does the same of `getFilePath()` but saving some boilerplate if /// you are planing to create a `File` for the returned path. - static Future getFile({FileType type = FileType.any, String fileExtension}) async { - final String filePath = await _getPath(_handleType(type, fileExtension), false); + static Future getFile({FileType type = FileType.any, List allowedExtensions}) async { + final String filePath = await _getPath(_handleType(type), false, allowedExtensions); return filePath != null ? File(filePath) : null; } @@ -46,15 +46,21 @@ class FilePicker { /// /// This is an utility method that does the same of `getMultiFilePath()` but saving some boilerplate if /// you are planing to create a list of `File`s for the returned paths. - static Future> getMultiFile({FileType type = FileType.any, String fileExtension}) async { - final Map paths = await _getPath(_handleType(type, fileExtension), true); + static Future> getMultiFile({FileType type = FileType.any, List allowedExtensions}) async { + final Map paths = await _getPath(_handleType(type), true, allowedExtensions); return paths != null && paths.isNotEmpty ? paths.values.map((path) => File(path)).toList() : null; } - static Future _getPath(String type, bool multipleSelection) async { + static Future _getPath(String type, bool allowMultipleSelection, List allowedExtensions) async { + if (type != 'CUSTOM' && (allowedExtensions?.isNotEmpty ?? false)) { + throw Exception('If you are using a custom extension filter, please use the FileType.custom instead.'); + } try { - dynamic result = await _channel.invokeMethod(type, multipleSelection); - if (result != null && multipleSelection) { + dynamic result = await _channel.invokeMethod(type, { + 'allowMultipleSelection': allowMultipleSelection, + 'allowedExtensions': allowedExtensions, + }); + if (result != null && allowMultipleSelection) { if (result is String) { result = [result]; } @@ -70,10 +76,7 @@ class FilePicker { } } - static String _handleType(FileType type, String fileExtension) { - if (type != FileType.custom && (fileExtension?.isNotEmpty ?? false)) { - throw Exception('If you are using a custom extension filter, please use the FileType.custom instead.'); - } + static String _handleType(FileType type) { switch (type) { case FileType.image: return 'IMAGE'; @@ -84,7 +87,7 @@ class FilePicker { case FileType.any: return 'ANY'; case FileType.custom: - return '__CUSTOM_' + (fileExtension ?? ''); + return 'CUSTOM'; default: return 'ANY'; }