Adds multiple file extensions support (Android)

This commit is contained in:
Miguel Ruivo 2020-04-05 17:01:31 +01:00
parent 8eb61af343
commit 2378cd5422
5 changed files with 88 additions and 67 deletions

View File

@ -27,6 +27,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener
private MethodChannel.Result pendingResult; private MethodChannel.Result pendingResult;
private boolean isMultipleSelection = false; private boolean isMultipleSelection = false;
private String type; private String type;
private String[] allowedExtensions;
public FilePickerDelegate(final Activity activity) { public FilePickerDelegate(final Activity activity) {
this( this(
@ -150,11 +151,16 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener
intent = new Intent(Intent.ACTION_GET_CONTENT); intent = new Intent(Intent.ACTION_GET_CONTENT);
final Uri uri = Uri.parse(Environment.getExternalStorageDirectory().getPath() + File.separator); final Uri uri = Uri.parse(Environment.getExternalStorageDirectory().getPath() + File.separator);
intent.setDataAndType(uri, this.type); intent.setDataAndType(uri, this.type);
intent.setType(this.type); intent.setType(this.type);
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, this.isMultipleSelection); intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, this.isMultipleSelection);
intent.addCategory(Intent.CATEGORY_OPENABLE); intent.addCategory(Intent.CATEGORY_OPENABLE);
if (allowedExtensions != null) {
intent.putExtra(Intent.EXTRA_MIME_TYPES, allowedExtensions);
}
if (intent.resolveActivity(this.activity.getPackageManager()) != null) { if (intent.resolveActivity(this.activity.getPackageManager()) != null) {
this.activity.startActivityForResult(intent, REQUEST_CODE); this.activity.startActivityForResult(intent, REQUEST_CODE);
} else { } else {
@ -164,7 +170,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener
} }
@SuppressWarnings("deprecation") @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)) { if (!this.setPendingMethodCallAndResult(result)) {
finishWithAlreadyActiveError(result); finishWithAlreadyActiveError(result);
@ -173,6 +179,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener
this.type = type; this.type = type;
this.isMultipleSelection = isMultipleSelection; this.isMultipleSelection = isMultipleSelection;
this.allowedExtensions = allowedExtensions;
if (!this.permissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) { if (!this.permissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) {
this.permissionManager.askForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_CODE); this.permissionManager.askForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_CODE);

View File

@ -5,14 +5,15 @@ import android.app.Application;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Log;
import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner; 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.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
@ -133,6 +134,7 @@ public class FilePickerPlugin implements MethodChannel.MethodCallHandler, Flutte
} }
@SuppressWarnings("unchecked")
@Override @Override
public void onMethodCall(final MethodCall call, final MethodChannel.Result rawResult) { 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 MethodChannel.Result result = new MethodResultWrapper(rawResult);
final HashMap arguments = (HashMap) call.arguments;
fileType = FilePickerPlugin.resolveType(call.method); fileType = FilePickerPlugin.resolveType(call.method);
isMultipleSelection = (boolean) call.arguments; isMultipleSelection = (boolean) arguments.get("allowMultipleSelection");
final String[] allowedExtensions = FileUtils.getMimeTypes((ArrayList<String>) arguments.get("allowedExtensions"));
if (fileType == null) { if (fileType == null) {
result.notImplemented(); result.notImplemented();
} else if (fileType.equals("unsupported")) { } 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); 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 { } else {
this.delegate.startFileExplorer(fileType, isMultipleSelection, result); this.delegate.startFileExplorer(fileType, isMultipleSelection, allowedExtensions, result);
} }
} }
private static String resolveType(final String type) { 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) { switch (type) {
case "AUDIO": case "AUDIO":
@ -175,6 +172,7 @@ public class FilePickerPlugin implements MethodChannel.MethodCallHandler, Flutte
case "VIDEO": case "VIDEO":
return "video/*"; return "video/*";
case "ANY": case "ANY":
case "CUSTOM":
return "*/*"; return "*/*";
default: default:
return null; return null;

View File

@ -11,11 +11,13 @@ import android.provider.DocumentsContract;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.webkit.MimeTypeMap;
import java.io.BufferedOutputStream; import java.io.BufferedOutputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList;
import java.util.Random; import java.util.Random;
public class FileUtils { public class FileUtils {
@ -121,6 +123,27 @@ public class FileUtils {
return null; return null;
} }
public static String[] getMimeTypes(final ArrayList<String> allowedExtensions) {
if (allowedExtensions == null || allowedExtensions.isEmpty()) {
return null;
}
final ArrayList<String> 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, private static String getDataColumn(final Context context, final Uri uri, final String selection,
final String[] selectionArgs) { final String[] selectionArgs) {
Cursor cursor = null; Cursor cursor = null;
@ -167,7 +190,9 @@ public class FileUtils {
} }
} }
} finally { } finally {
cursor.close(); if (cursor != null) {
cursor.close();
}
} }
} }

View File

@ -15,7 +15,6 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
String _extension; String _extension;
bool _loadingPath = false; bool _loadingPath = false;
bool _multiPick = false; bool _multiPick = false;
bool _hasValidMime = false;
FileType _pickingType; FileType _pickingType;
TextEditingController _controller = new TextEditingController(); TextEditingController _controller = new TextEditingController();
@ -26,25 +25,23 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
} }
void _openFileExplorer() async { void _openFileExplorer() async {
if (_pickingType != FileType.custom || _hasValidMime) { setState(() => _loadingPath = true);
setState(() => _loadingPath = true); try {
try { if (_multiPick) {
if (_multiPick) { _path = null;
_path = null; _paths = await FilePicker.getMultiFilePath(type: _pickingType, allowedExtensions: _extension?.replaceAll(' ', '')?.split(','));
_paths = await FilePicker.getMultiFilePath(type: _pickingType, fileExtension: _extension); } else {
} else { _paths = null;
_paths = null; _path = await FilePicker.getFilePath(type: _pickingType, allowedExtensions: _extension?.replaceAll(' ', '')?.split(','));
_path = await FilePicker.getFilePath(type: _pickingType, fileExtension: _extension);
}
} on PlatformException catch (e) {
print("Unsupported operation" + e.toString());
} }
if (!mounted) return; } on PlatformException catch (e) {
setState(() { print("Unsupported operation" + e.toString());
_loadingPath = false;
_fileName = _path != null ? _path.split('/').last : _paths != null ? _paths.keys.toString() : '...';
});
} }
if (!mounted) return;
setState(() {
_loadingPath = false;
_fileName = _path != null ? _path.split('/').last : _paths != null ? _paths.keys.toString() : '...';
});
} }
@override @override
@ -91,7 +88,7 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
onChanged: (value) => setState(() { onChanged: (value) => setState(() {
_pickingType = value; _pickingType = value;
if (_pickingType != FileType.custom) { if (_pickingType != FileType.custom) {
_controller.text = _extension = ''; _controller.text = _extension = null;
} }
})), })),
), ),
@ -105,15 +102,6 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
decoration: InputDecoration(labelText: 'File extension'), decoration: InputDecoration(labelText: 'File extension'),
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.none, 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(), : new Container(),
), ),

View File

@ -19,26 +19,26 @@ class FilePicker {
/// Returns an iterable `Map<String,String>` where the `key` is the name of the file /// Returns an iterable `Map<String,String>` where the `key` is the name of the file
/// and the `value` the path. /// and the `value` the path.
/// ///
/// A [fileExtension] can be provided to filter the picking results. /// A `List` with [allowedExtensions] can be provided to filter the allowed files to picked.
/// If provided, it will be use the `FileType.CUSTOM` for that [fileExtension]. /// If provided, make sure you select `FileType.custom` as type.
/// If not, `FileType.ANY` will be used and any combination of files can be multi picked at once. /// Defaults to `FileType.any`, which allows any combination of files to be multi selected at once.
static Future<Map<String, String>> getMultiFilePath({FileType type = FileType.any, String fileExtension}) async => static Future<Map<String, String>> getMultiFilePath({FileType type = FileType.any, List<String> allowedExtensions}) async =>
await _getPath(_handleType(type, fileExtension), true); await _getPath(_handleType(type), true, allowedExtensions);
/// Returns an absolute file path from the calling platform. /// Returns an absolute file path from the calling platform.
/// ///
/// A [type] must be provided to filter the picking results. /// Extension filters are allowed with `FileType.custom`, when used, make sure to provide a `List`
/// Can be used a custom file type with `FileType.CUSTOM`. A [fileExtension] must be provided (e.g. PDF, SVG, etc.) /// of [allowedExtensions] (e.g. [`pdf`, `svg`, `jpg`].).
/// Defaults to `FileType.ANY` which will display all file types. /// Defaults to `FileType.any` which will display all file types.
static Future<String> getFilePath({FileType type = FileType.any, String fileExtension}) async => static Future<String> getFilePath({FileType type = FileType.any, List<String> allowedExtensions}) async =>
await _getPath(_handleType(type, fileExtension), false); await _getPath(_handleType(type), false, allowedExtensions);
/// Returns a `File` object from the selected file path. /// 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 /// 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. /// you are planing to create a `File` for the returned path.
static Future<File> getFile({FileType type = FileType.any, String fileExtension}) async { static Future<File> getFile({FileType type = FileType.any, List<String> allowedExtensions}) async {
final String filePath = await _getPath(_handleType(type, fileExtension), false); final String filePath = await _getPath(_handleType(type), false, allowedExtensions);
return filePath != null ? File(filePath) : null; 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 /// 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. /// you are planing to create a list of `File`s for the returned paths.
static Future<List<File>> getMultiFile({FileType type = FileType.any, String fileExtension}) async { static Future<List<File>> getMultiFile({FileType type = FileType.any, List<String> allowedExtensions}) async {
final Map<String, String> paths = await _getPath(_handleType(type, fileExtension), true); final Map<String, String> paths = await _getPath(_handleType(type), true, allowedExtensions);
return paths != null && paths.isNotEmpty ? paths.values.map((path) => File(path)).toList() : null; return paths != null && paths.isNotEmpty ? paths.values.map((path) => File(path)).toList() : null;
} }
static Future<dynamic> _getPath(String type, bool multipleSelection) async { static Future<dynamic> _getPath(String type, bool allowMultipleSelection, List<String> 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 { try {
dynamic result = await _channel.invokeMethod(type, multipleSelection); dynamic result = await _channel.invokeMethod(type, {
if (result != null && multipleSelection) { 'allowMultipleSelection': allowMultipleSelection,
'allowedExtensions': allowedExtensions,
});
if (result != null && allowMultipleSelection) {
if (result is String) { if (result is String) {
result = [result]; result = [result];
} }
@ -70,10 +76,7 @@ class FilePicker {
} }
} }
static String _handleType(FileType type, String fileExtension) { static String _handleType(FileType type) {
if (type != FileType.custom && (fileExtension?.isNotEmpty ?? false)) {
throw Exception('If you are using a custom extension filter, please use the FileType.custom instead.');
}
switch (type) { switch (type) {
case FileType.image: case FileType.image:
return 'IMAGE'; return 'IMAGE';
@ -84,7 +87,7 @@ class FilePicker {
case FileType.any: case FileType.any:
return 'ANY'; return 'ANY';
case FileType.custom: case FileType.custom:
return '__CUSTOM_' + (fileExtension ?? ''); return 'CUSTOM';
default: default:
return 'ANY'; return 'ANY';
} }