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 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);

View File

@ -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<String>) 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;

View File

@ -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<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,
final String[] selectionArgs) {
Cursor cursor = null;
@ -167,7 +190,9 @@ public class FileUtils {
}
}
} finally {
cursor.close();
if (cursor != null) {
cursor.close();
}
}
}

View File

@ -15,7 +15,6 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
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<FilePickerDemo> {
}
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<FilePickerDemo> {
onChanged: (value) => setState(() {
_pickingType = value;
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'),
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(),
),

View File

@ -19,26 +19,26 @@ class FilePicker {
/// Returns an iterable `Map<String,String>` 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<Map<String, String>> 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<Map<String, String>> getMultiFilePath({FileType type = FileType.any, List<String> 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<String> 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<String> getFilePath({FileType type = FileType.any, List<String> 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<File> getFile({FileType type = FileType.any, String fileExtension}) async {
final String filePath = await _getPath(_handleType(type, fileExtension), false);
static Future<File> getFile({FileType type = FileType.any, List<String> 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<List<File>> getMultiFile({FileType type = FileType.any, String fileExtension}) async {
final Map<String, String> paths = await _getPath(_handleType(type, fileExtension), true);
static Future<List<File>> getMultiFile({FileType type = FileType.any, List<String> allowedExtensions}) async {
final Map<String, String> paths = await _getPath(_handleType(type), true, allowedExtensions);
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 {
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';
}