Android V2 embedding

- Adds support for Android V2 embedding;
- Refactors FileType to match camelCase enum Dart guideline;
This commit is contained in:
Miguel Ruivo 2020-03-14 17:32:05 +00:00
parent b68ee11c6a
commit a9cc55f5f5
8 changed files with 614 additions and 340 deletions

View File

@ -1,3 +1,8 @@
## 1.5.0
* **Breaking change:** Refactored `FileType` to match lower camelCase Dart guideline (eg. `FileType.ALL` now is `FileType.all`);
* Added support for new [Android plugins APIs](https://flutter.dev/docs/development/packages-and-plugins/plugin-api-migration) (Android V2 embedding);
## 1.4.3+2
Updates dependencies.

View File

@ -34,5 +34,6 @@ android {
dependencies {
implementation 'androidx.core:core:1.0.2'
implementation 'androidx.annotation:annotation:1.0.0'
implementation "androidx.lifecycle:lifecycle-runtime:2.1.0"
}
}

View File

@ -0,0 +1,209 @@
package com.mr.flutter.plugin.filepicker;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Environment;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.ActivityCompat;
import java.io.File;
import java.util.ArrayList;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.PluginRegistry;
public class FilePickerDelegate implements PluginRegistry.ActivityResultListener, PluginRegistry.RequestPermissionsResultListener {
private static final String TAG = "FilePickerDelegate";
private static final int REQUEST_CODE = (FilePickerPlugin.class.hashCode() + 43) & 0x0000ffff;
private final Activity activity;
private final PermissionManager permissionManager;
private MethodChannel.Result pendingResult;
private boolean isMultipleSelection = false;
private String type;
public FilePickerDelegate(final Activity activity) {
this(
activity,
null,
new PermissionManager() {
@Override
public boolean isPermissionGranted(final String permissionName) {
return ActivityCompat.checkSelfPermission(activity, permissionName)
== PackageManager.PERMISSION_GRANTED;
}
@Override
public void askForPermission(final String permissionName, final int requestCode) {
ActivityCompat.requestPermissions(activity, new String[]{permissionName}, requestCode);
}
}
);
}
@VisibleForTesting
FilePickerDelegate(final Activity activity, final MethodChannel.Result result, final PermissionManager permissionManager) {
this.activity = activity;
this.pendingResult = result;
this.permissionManager = permissionManager;
}
@Override
public boolean onActivityResult(final int requestCode, final int resultCode, final Intent data) {
if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
new Thread(new Runnable() {
@Override
public void run() {
if (data != null) {
if (data.getClipData() != null) {
final int count = data.getClipData().getItemCount();
int currentItem = 0;
final ArrayList<String> paths = new ArrayList<>();
while (currentItem < count) {
final Uri currentUri = data.getClipData().getItemAt(currentItem).getUri();
String path = FileUtils.getPath(currentUri, FilePickerDelegate.this.activity);
if (path == null) {
path = FileUtils.getUriFromRemote(FilePickerDelegate.this.activity, currentUri);
}
paths.add(path);
Log.i(FilePickerDelegate.TAG, "[MultiFilePick] File #" + currentItem + " - URI: " + currentUri.getPath());
currentItem++;
}
if (paths.size() > 1) {
FilePickerDelegate.this.finishWithSuccess(paths);
} else {
FilePickerDelegate.this.finishWithSuccess(paths.get(0));
}
} else if (data.getData() != null) {
final Uri uri = data.getData();
Log.i(FilePickerDelegate.TAG, "[SingleFilePick] File URI:" + uri.toString());
String fullPath = FileUtils.getPath(uri, FilePickerDelegate.this.activity);
if (fullPath == null) {
fullPath = FileUtils.getUriFromRemote(FilePickerDelegate.this.activity, uri);
}
if (fullPath != null) {
Log.i(FilePickerDelegate.TAG, "Absolute file path:" + fullPath);
FilePickerDelegate.this.finishWithSuccess(fullPath);
} else {
FilePickerDelegate.this.finishWithError("unknown_path", "Failed to retrieve path.");
}
} else {
FilePickerDelegate.this.finishWithError("unknown_activity", "Unknown activity error, please fill an issue.");
}
} else {
FilePickerDelegate.this.finishWithError("unknown_activity", "Unknown activity error, please fill an issue.");
}
}
}).start();
return true;
} else if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_CANCELED) {
Log.i(TAG, "User cancelled the picker request");
this.finishWithSuccess(null);
return true;
} else if (requestCode == REQUEST_CODE) {
this.finishWithError("unknown_activity", "Unknown activity error, please fill an issue.");
}
return false;
}
@Override
public boolean onRequestPermissionsResult(final int requestCode, final String[] permissions, final int[] grantResults) {
final boolean permissionGranted =
grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
if (permissionGranted) {
this.startFileExplorer();
} else {
this.finishWithError("read_external_storage_denied", "User did not allowed reading external storage");
}
return true;
}
private boolean setPendingMethodCallAndResult(final MethodChannel.Result result) {
if (this.pendingResult != null) {
return false;
}
this.pendingResult = result;
return true;
}
private static void finishWithAlreadyActiveError(final MethodChannel.Result result) {
result.error("already_active", "File picker is already active", null);
}
private void startFileExplorer() {
final Intent intent;
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 (intent.resolveActivity(this.activity.getPackageManager()) != null) {
this.activity.startActivityForResult(intent, REQUEST_CODE);
} else {
Log.e(TAG, "Can't find a valid activity to handle the request. Make sure you've a file explorer installed.");
this.finishWithError("invalid_format_type", "Can't handle the provided file type.");
}
}
@SuppressWarnings("deprecation")
public void startFileExplorer(final String type, final boolean isMultipleSelection, final MethodChannel.Result result) {
if (!this.setPendingMethodCallAndResult(result)) {
FilePickerDelegate.finishWithAlreadyActiveError(result);
return;
}
this.type = type;
this.isMultipleSelection = isMultipleSelection;
if (!this.permissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) {
this.permissionManager.askForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_CODE);
return;
}
this.startFileExplorer();
}
private void finishWithSuccess(final Object data) {
this.pendingResult.success(data);
this.clearPendingResult();
}
private void finishWithError(final String errorCode, final String errorMessage) {
if (this.pendingResult == null) {
return;
}
this.pendingResult.error(errorCode, errorMessage, null);
this.clearPendingResult();
}
private void clearPendingResult() {
this.pendingResult = null;
}
interface PermissionManager {
boolean isPermissionGranted(String permissionName);
void askForPermission(String permissionName, int requestCode);
}
}

View File

@ -1,226 +1,306 @@
package com.mr.flutter.plugin.filepicker;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
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.io.File;
import java.util.ArrayList;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.common.PluginRegistry.Registrar;
/** FilePickerPlugin */
public class FilePickerPlugin implements MethodCallHandler {
/**
* FilePickerPlugin
*/
public class FilePickerPlugin implements MethodChannel.MethodCallHandler, FlutterPlugin, ActivityAware {
private static final int REQUEST_CODE = (FilePickerPlugin.class.hashCode() + 43) & 0x0000ffff;
private static final int PERM_CODE = (FilePickerPlugin.class.hashCode() + 50) & 0x0000ffff;
private static final String TAG = "FilePicker";
private static final String permission = Manifest.permission.READ_EXTERNAL_STORAGE;
private static final String TAG = "FilePicker";
private static final String CHANNEL = "miguelruivo.flutter.plugins.file_picker";
private static Result result;
private static Registrar instance;
private static String fileType;
private static boolean isMultipleSelection = false;
private class LifeCycleObserver
implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver {
private final Activity thisActivity;
/** Plugin registration. */
public static void registerWith(Registrar registrar) {
if (registrar.activity() == null) {
// If a background flutter view tries to register the plugin, there will be no activity from the registrar,
// we stop the registering process immediately because the ImagePicker requires an activity.
return;
}
final MethodChannel channel = new MethodChannel(registrar.messenger(), "file_picker");
channel.setMethodCallHandler(new FilePickerPlugin());
instance = registrar;
instance.addActivityResultListener(new PluginRegistry.ActivityResultListener() {
@Override
public boolean onActivityResult(int requestCode, int resultCode, final Intent data) {
if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
new Thread(new Runnable() {
@Override
public void run() {
if (data != null) {
if(data.getClipData() != null) {
int count = data.getClipData().getItemCount();
int currentItem = 0;
ArrayList<String> paths = new ArrayList<>();
while(currentItem < count) {
final Uri currentUri = data.getClipData().getItemAt(currentItem).getUri();
String path = FileUtils.getPath(currentUri, instance.context());
if(path == null) {
path = FileUtils.getUriFromRemote(instance.activeContext(), currentUri, result);
}
paths.add(path);
Log.i(TAG, "[MultiFilePick] File #" + currentItem + " - URI: " +currentUri.getPath());
currentItem++;
}
if(paths.size() > 1){
runOnUiThread(result, paths, true);
} else {
runOnUiThread(result, paths.get(0), true);
}
} else if (data.getData() != null) {
Uri uri = data.getData();
Log.i(TAG, "[SingleFilePick] File URI:" + uri.toString());
String fullPath = FileUtils.getPath(uri, instance.context());
if(fullPath == null) {
fullPath = FileUtils.getUriFromRemote(instance.activeContext(), uri, result);
}
if(fullPath != null) {
Log.i(TAG, "Absolute file path:" + fullPath);
runOnUiThread(result, fullPath, true);
} else {
runOnUiThread(result, "Failed to retrieve path.", false);
}
} else {
runOnUiThread(result, "Unknown activity error, please fill an issue.", false);
}
} else {
runOnUiThread(result, "Unknown activity error, please fill an issue.", false);
}
}
}).start();
return true;
} else if(requestCode == REQUEST_CODE && resultCode == Activity.RESULT_CANCELED) {
result.success(null);
return true;
} else if (requestCode == REQUEST_CODE) {
result.error(TAG, "Unknown activity error, please fill an issue." ,null);
LifeCycleObserver(final Activity activity) {
this.thisActivity = activity;
}
return false;
}
});
instance.addRequestPermissionsResultListener(new PluginRegistry.RequestPermissionsResultListener() {
@Override
public boolean onRequestPermissionsResult(int requestCode, String[] strings, int[] grantResults) {
if (requestCode == PERM_CODE && grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startFileExplorer(fileType);
return true;
@Override
public void onCreate(@NonNull final LifecycleOwner owner) {
}
return false;
}
});
}
private static void runOnUiThread(final Result result, final Object o, final boolean success) {
instance.activity().runOnUiThread(new Runnable() {
@Override
public void run() {
if(success) {
result.success(o);
} else if(o != null) {
result.error(TAG,(String)o, null);
} else {
result.notImplemented();
}
@Override
public void onStart(@NonNull final LifecycleOwner owner) {
}
@Override
public void onResume(@NonNull final LifecycleOwner owner) {
}
@Override
public void onPause(@NonNull final LifecycleOwner owner) {
}
@Override
public void onStop(@NonNull final LifecycleOwner owner) {
this.onActivityStopped(this.thisActivity);
}
@Override
public void onDestroy(@NonNull final LifecycleOwner owner) {
this.onActivityDestroyed(this.thisActivity);
}
@Override
public void onActivityCreated(final Activity activity, final Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(final Activity activity) {
}
@Override
public void onActivityResumed(final Activity activity) {
}
@Override
public void onActivityPaused(final Activity activity) {
}
@Override
public void onActivitySaveInstanceState(final Activity activity, final Bundle outState) {
}
@Override
public void onActivityDestroyed(final Activity activity) {
if (this.thisActivity == activity && activity.getApplicationContext() != null) {
((Application) activity.getApplicationContext()).unregisterActivityLifecycleCallbacks(this); // Use getApplicationContext() to avoid casting failures
}
});
}
@Override
public void onMethodCall(MethodCall call, Result result) {
FilePickerPlugin.result = result;
fileType = resolveType(call.method);
isMultipleSelection = (boolean)call.arguments;
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 {
startFileExplorer(fileType);
}
}
private static boolean checkPermission() {
Activity activity = instance.activity();
Log.i(TAG, "Checking permission: " + permission);
return PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(activity, permission);
}
private static void requestPermission() {
Activity activity = instance.activity();
Log.i(TAG, "Requesting permission: " + permission);
String[] perm = { permission };
ActivityCompat.requestPermissions(activity, perm, PERM_CODE);
}
private String resolveType(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":
return "audio/*";
case "IMAGE":
return "image/*";
case "VIDEO":
return "video/*";
case "ANY":
return "*/*";
default:
return null;
}
}
@SuppressWarnings("deprecation")
private static void startFileExplorer(String type) {
Intent intent;
if (checkPermission()) {
intent = new Intent(Intent.ACTION_GET_CONTENT);
Uri uri = Uri.parse(Environment.getExternalStorageDirectory().getPath() + File.separator);
intent.setDataAndType(uri, type);
intent.setType(type);
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, isMultipleSelection);
intent.addCategory(Intent.CATEGORY_OPENABLE);
if (intent.resolveActivity(instance.activity().getPackageManager()) != null) {
instance.activity().startActivityForResult(intent, REQUEST_CODE);
} else {
Log.e(TAG, "Can't find a valid activity to handle the request. Make sure you've a file explorer installed.");
result.error(TAG, "Can't handle the provided file type.", null);
}
} else {
requestPermission();
}
}
@Override
public void onActivityStopped(final Activity activity) {
}
}
private ActivityPluginBinding activityBinding;
private FilePickerDelegate delegate;
private Application application;
private FlutterPluginBinding pluginBinding;
// This is null when not using v2 embedding;
private Lifecycle lifecycle;
private LifeCycleObserver observer;
private Activity activity;
private MethodChannel channel;
private static String fileType;
private static boolean isMultipleSelection = false;
/**
* Plugin registration.
*/
public static void registerWith(final Registrar registrar) {
if (registrar.activity() == null) {
// If a background flutter view tries to register the plugin, there will be no activity from the registrar,
// we stop the registering process immediately because the ImagePicker requires an activity.
return;
}
final Activity activity = registrar.activity();
Application application = null;
if (registrar.context() != null) {
application = (Application) (registrar.context().getApplicationContext());
}
final FilePickerPlugin plugin = new FilePickerPlugin();
plugin.setup(registrar.messenger(), application, activity, registrar, null);
}
@Override
public void onMethodCall(final MethodCall call, final MethodChannel.Result rawResult) {
if (this.activity == null) {
rawResult.error("no_activity", "file picker plugin requires a foreground activity", null);
return;
}
final MethodChannel.Result result = new MethodResultWrapper(rawResult);
fileType = FilePickerPlugin.resolveType(call.method);
isMultipleSelection = (boolean) call.arguments;
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 {
this.delegate.startFileExplorer(fileType, isMultipleSelection, 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":
return "audio/*";
case "IMAGE":
return "image/*";
case "VIDEO":
return "video/*";
case "ANY":
return "*/*";
default:
return null;
}
}
// MethodChannel.Result wrapper that responds on the platform thread.
private static class MethodResultWrapper implements MethodChannel.Result {
private final MethodChannel.Result methodResult;
private final Handler handler;
MethodResultWrapper(final MethodChannel.Result result) {
this.methodResult = result;
this.handler = new Handler(Looper.getMainLooper());
}
@Override
public void success(final Object result) {
this.handler.post(
new Runnable() {
@Override
public void run() {
MethodResultWrapper.this.methodResult.success(result);
}
});
}
@Override
public void error(
final String errorCode, final String errorMessage, final Object errorDetails) {
this.handler.post(
new Runnable() {
@Override
public void run() {
MethodResultWrapper.this.methodResult.error(errorCode, errorMessage, errorDetails);
}
});
}
@Override
public void notImplemented() {
this.handler.post(
new Runnable() {
@Override
public void run() {
MethodResultWrapper.this.methodResult.notImplemented();
}
});
}
}
private void setup(
final BinaryMessenger messenger,
final Application application,
final Activity activity,
final PluginRegistry.Registrar registrar,
final ActivityPluginBinding activityBinding) {
this.activity = activity;
this.application = application;
this.delegate = new FilePickerDelegate(activity);
this.channel = new MethodChannel(messenger, CHANNEL);
this.channel.setMethodCallHandler(this);
this.observer = new LifeCycleObserver(activity);
if (registrar != null) {
// V1 embedding setup for activity listeners.
application.registerActivityLifecycleCallbacks(this.observer);
registrar.addActivityResultListener(this.delegate);
registrar.addRequestPermissionsResultListener(this.delegate);
} else {
// V2 embedding setup for activity listeners.
activityBinding.addActivityResultListener(this.delegate);
activityBinding.addRequestPermissionsResultListener(this.delegate);
this.lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding);
this.lifecycle.addObserver(this.observer);
}
}
private void tearDown() {
this.activityBinding.removeActivityResultListener(this.delegate);
this.activityBinding.removeRequestPermissionsResultListener(this.delegate);
this.activityBinding = null;
this.lifecycle.removeObserver(this.observer);
this.lifecycle = null;
this.delegate = null;
this.channel.setMethodCallHandler(null);
this.channel = null;
this.application.unregisterActivityLifecycleCallbacks(this.observer);
this.application = null;
}
@Override
public void onAttachedToEngine(final FlutterPluginBinding binding) {
this.pluginBinding = binding;
}
@Override
public void onDetachedFromEngine(final FlutterPluginBinding binding) {
this.pluginBinding = null;
}
@Override
public void onAttachedToActivity(final ActivityPluginBinding binding) {
this.activityBinding = binding;
this.setup(
this.pluginBinding.getBinaryMessenger(),
(Application) this.pluginBinding.getApplicationContext(),
this.activityBinding.getActivity(),
null,
this.activityBinding);
}
@Override
public void onDetachedFromActivityForConfigChanges() {
this.onDetachedFromActivity();
}
@Override
public void onReattachedToActivityForConfigChanges(final ActivityPluginBinding binding) {
this.onAttachedToActivity(binding);
}
@Override
public void onDetachedFromActivity() {
this.tearDown();
}
}

View File

@ -18,13 +18,11 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.Random;
import io.flutter.plugin.common.MethodChannel;
public class FileUtils {
private static final String TAG = "FilePickerUtils";
public static String getPath(final Uri uri, Context context) {
public static String getPath(final Uri uri, final Context context) {
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
if (isKitKat) {
return getForApi19(context, uri);
@ -41,7 +39,7 @@ public class FileUtils {
@TargetApi(19)
@SuppressWarnings("deprecation")
private static String getForApi19(Context context, Uri uri) {
private static String getForApi19(final Context context, final Uri uri) {
Log.e(TAG, "Getting for API 19 or above" + uri);
if (DocumentsContract.isDocumentUri(context, uri)) {
Log.e(TAG, "Document URI");
@ -62,27 +60,27 @@ public class FileUtils {
if (id.startsWith("raw:")) {
return id.replaceFirst("raw:", "");
}
String[] contentUriPrefixesToTry = new String[]{
"content://downloads/public_downloads",
"content://downloads/my_downloads",
"content://downloads/all_downloads"
};
if(id.contains(":")){
final String[] contentUriPrefixesToTry = new String[]{
"content://downloads/public_downloads",
"content://downloads/my_downloads",
"content://downloads/all_downloads"
};
if (id.contains(":")) {
id = id.split(":")[1];
}
for (String contentUriPrefix : contentUriPrefixesToTry) {
Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.valueOf(id));
try {
String path = getDataColumn(context, contentUri, null, null);
if (path != null) {
return path;
}
} catch (Exception e) {
Log.e(TAG, "Something went wrong while retrieving document path: " + e.toString());
for (final String contentUriPrefix : contentUriPrefixesToTry) {
final Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.valueOf(id));
try {
final String path = getDataColumn(context, contentUri, null, null);
if (path != null) {
return path;
}
} catch (final Exception e) {
Log.e(TAG, "Something went wrong while retrieving document path: " + e.toString());
}
}
}
} else if (isMediaDocument(uri)) {
Log.e(TAG, "Media Document URI");
final String docId = DocumentsContract.getDocumentId(uri);
@ -123,8 +121,8 @@ public class FileUtils {
return null;
}
private static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
private static String getDataColumn(final Context context, final Uri uri, final String selection,
final String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = {
@ -137,104 +135,109 @@ public class FileUtils {
final int index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(index);
}
} catch(Exception ex){
} catch (final Exception ex) {
} finally {
if (cursor != null)
if (cursor != null) {
cursor.close();
}
}
return null;
}
public static String getFileName(Uri uri, Context context) {
public static String getFileName(Uri uri, final Context context) {
String result = null;
//if uri is content
if (uri.getScheme() != null && uri.getScheme().equals("content")) {
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
final Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
try {
if (cursor != null && cursor.moveToFirst()) {
//local filesystem
int index = cursor.getColumnIndex("_data");
if (index == -1)
//google drive
//google drive
{
index = cursor.getColumnIndex("_display_name");
}
result = cursor.getString(index);
if (result != null)
if (result != null) {
uri = Uri.parse(result);
else
} else {
return null;
}
}
} finally {
cursor.close();
}
}
if(uri.getPath() != null) {
if (uri.getPath() != null) {
result = uri.getPath();
int cut = result.lastIndexOf('/');
if (cut != -1)
final int cut = result.lastIndexOf('/');
if (cut != -1) {
result = result.substring(cut + 1);
}
}
return result;
}
public static String getUriFromRemote(Context context, Uri uri, MethodChannel.Result result) {
public static String getUriFromRemote(final Context context, final Uri uri) {
Log.i(TAG, "Caching file from remote/external URI");
FileOutputStream fos = null;
final String fileName = FileUtils.getFileName(uri, context);
String externalFile = context.getCacheDir().getAbsolutePath() + "/" + (fileName != null ? fileName : new Random().nextInt(100000));
final String externalFile = context.getCacheDir().getAbsolutePath() + "/" + (fileName != null ? fileName : new Random().nextInt(100000));
try {
fos = new FileOutputStream(externalFile);
try {
fos = new FileOutputStream(externalFile);
try {
BufferedOutputStream out = new BufferedOutputStream(fos);
InputStream in = context.getContentResolver().openInputStream(uri);
final BufferedOutputStream out = new BufferedOutputStream(fos);
final InputStream in = context.getContentResolver().openInputStream(uri);
byte[] buffer = new byte[8192];
int len = 0;
final byte[] buffer = new byte[8192];
int len = 0;
while ((len = in.read(buffer)) >= 0) {
out.write(buffer, 0, len);
}
out.flush();
} finally {
fos.getFD().sync();
while ((len = in.read(buffer)) >= 0) {
out.write(buffer, 0, len);
}
} catch (Exception e) {
try {
fos.close();
} catch(IOException | NullPointerException ex) {
Log.e(TAG, "Failed to close file streams: " + e.getMessage(),null);
return null;
}
Log.e(TAG, "Failed to retrieve path: " + e.getMessage(),null);
out.flush();
} finally {
fos.getFD().sync();
}
} catch (final Exception e) {
try {
fos.close();
} catch (final IOException | NullPointerException ex) {
Log.e(TAG, "Failed to close file streams: " + e.getMessage(), null);
return null;
}
Log.e(TAG, "Failed to retrieve path: " + e.getMessage(), null);
return null;
}
Log.i(TAG, "File loaded and cached at:" + externalFile);
return externalFile;
Log.i(TAG, "File loaded and cached at:" + externalFile);
return externalFile;
}
private static boolean isDropBoxUri(Uri uri) {
private static boolean isDropBoxUri(final Uri uri) {
return "com.dropbox.android.FileCache".equals(uri.getAuthority());
}
private static boolean isExternalStorageDocument(Uri uri) {
private static boolean isExternalStorageDocument(final Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
private static boolean isDownloadsDocument(Uri uri) {
private static boolean isDownloadsDocument(final Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
private static boolean isMediaDocument(Uri uri) {
private static boolean isMediaDocument(final Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
private static boolean isGooglePhotosUri(Uri uri) {
private static boolean isGooglePhotosUri(final Uri uri) {
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
}

View File

@ -26,17 +26,15 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
}
void _openFileExplorer() async {
if (_pickingType != FileType.CUSTOM || _hasValidMime) {
if (_pickingType != FileType.custom || _hasValidMime) {
setState(() => _loadingPath = true);
try {
if (_multiPick) {
_path = null;
_paths = await FilePicker.getMultiFilePath(
type: _pickingType, fileExtension: _extension);
_paths = await FilePicker.getMultiFilePath(type: _pickingType, fileExtension: _extension);
} else {
_paths = null;
_path = await FilePicker.getFilePath(
type: _pickingType, fileExtension: _extension);
_path = await FilePicker.getFilePath(type: _pickingType, fileExtension: _extension);
}
} on PlatformException catch (e) {
print("Unsupported operation" + e.toString());
@ -44,9 +42,7 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
if (!mounted) return;
setState(() {
_loadingPath = false;
_fileName = _path != null
? _path.split('/').last
: _paths != null ? _paths.keys.toString() : '...';
_fileName = _path != null ? _path.split('/').last : _paths != null ? _paths.keys.toString() : '...';
});
}
}
@ -73,41 +69,40 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
items: <DropdownMenuItem>[
new DropdownMenuItem(
child: new Text('FROM AUDIO'),
value: FileType.AUDIO,
value: FileType.audio,
),
new DropdownMenuItem(
child: new Text('FROM IMAGE'),
value: FileType.IMAGE,
value: FileType.image,
),
new DropdownMenuItem(
child: new Text('FROM VIDEO'),
value: FileType.VIDEO,
value: FileType.video,
),
new DropdownMenuItem(
child: new Text('FROM ANY'),
value: FileType.ANY,
value: FileType.any,
),
new DropdownMenuItem(
child: new Text('CUSTOM FORMAT'),
value: FileType.CUSTOM,
value: FileType.custom,
),
],
onChanged: (value) => setState(() {
_pickingType = value;
if (_pickingType != FileType.CUSTOM) {
if (_pickingType != FileType.custom) {
_controller.text = _extension = '';
}
})),
),
new ConstrainedBox(
constraints: BoxConstraints.tightFor(width: 100.0),
child: _pickingType == FileType.CUSTOM
child: _pickingType == FileType.custom
? new TextFormField(
maxLength: 15,
autovalidate: true,
controller: _controller,
decoration:
InputDecoration(labelText: 'File extension'),
decoration: InputDecoration(labelText: 'File extension'),
keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.none,
validator: (value) {
@ -125,10 +120,8 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
new ConstrainedBox(
constraints: BoxConstraints.tightFor(width: 200.0),
child: new SwitchListTile.adaptive(
title: new Text('Pick multiple files',
textAlign: TextAlign.right),
onChanged: (bool value) =>
setState(() => _multiPick = value),
title: new Text('Pick multiple files', textAlign: TextAlign.right),
onChanged: (bool value) => setState(() => _multiPick = value),
value: _multiPick,
),
),
@ -141,28 +134,18 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
),
new Builder(
builder: (BuildContext context) => _loadingPath
? Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: const CircularProgressIndicator())
? Padding(padding: const EdgeInsets.only(bottom: 10.0), child: const CircularProgressIndicator())
: _path != null || _paths != null
? new Container(
padding: const EdgeInsets.only(bottom: 30.0),
height: MediaQuery.of(context).size.height * 0.50,
child: new Scrollbar(
child: new ListView.separated(
itemCount: _paths != null && _paths.isNotEmpty
? _paths.length
: 1,
itemCount: _paths != null && _paths.isNotEmpty ? _paths.length : 1,
itemBuilder: (BuildContext context, int index) {
final bool isMultiPath =
_paths != null && _paths.isNotEmpty;
final String name = 'File $index: ' +
(isMultiPath
? _paths.keys.toList()[index]
: _fileName ?? '...');
final path = isMultiPath
? _paths.values.toList()[index].toString()
: _path;
final bool isMultiPath = _paths != null && _paths.isNotEmpty;
final String name = 'File $index: ' + (isMultiPath ? _paths.keys.toList()[index] : _fileName ?? '...');
final path = isMultiPath ? _paths.values.toList()[index].toString() : _path;
return new ListTile(
title: new Text(
@ -171,9 +154,7 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
subtitle: new Text(path),
);
},
separatorBuilder:
(BuildContext context, int index) =>
new Divider(),
separatorBuilder: (BuildContext context, int index) => new Divider(),
)),
)
: new Container(),

View File

@ -4,18 +4,17 @@ import 'dart:io';
import 'package:flutter/services.dart';
enum FileType {
ANY,
IMAGE,
VIDEO,
AUDIO,
CUSTOM,
any,
image,
video,
audio,
custom,
}
class FilePicker {
static const MethodChannel _channel = const MethodChannel('file_picker');
static const String _tag = 'FilePicker';
FilePicker._();
static const MethodChannel _channel = const MethodChannel('miguelruivo.flutter.plugins.file_picker');
static const String _tag = 'FilePicker';
/// Returns an iterable `Map<String,String>` where the `key` is the name of the file
/// and the `value` the path.
@ -23,8 +22,7 @@ class FilePicker {
/// 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 =>
static Future<Map<String, String>> getMultiFilePath({FileType type = FileType.any, String fileExtension}) async =>
await _getPath(_handleType(type, fileExtension), true);
/// Returns an absolute file path from the calling platform.
@ -32,18 +30,15 @@ class FilePicker {
/// 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 =>
static Future<String> getFilePath({FileType type = FileType.any, String fileExtension}) async =>
await _getPath(_handleType(type, fileExtension), false);
/// 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, String fileExtension}) async {
final String filePath = await _getPath(_handleType(type, fileExtension), false);
return filePath != null ? File(filePath) : null;
}
@ -51,13 +46,9 @@ 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);
return paths != null && paths.isNotEmpty
? paths.values.map((path) => File(path)).toList()
: null;
static Future<List<File>> getMultiFile({FileType type = FileType.any, String fileExtension}) async {
final Map<String, String> paths = await _getPath(_handleType(type, fileExtension), true);
return paths != null && paths.isNotEmpty ? paths.values.map((path) => File(path)).toList() : null;
}
static Future<dynamic> _getPath(String type, bool multipleSelection) async {
@ -67,31 +58,29 @@ class FilePicker {
if (result is String) {
result = [result];
}
return Map<String, String>.fromIterable(result,
key: (path) => path.split('/').last, value: (path) => path);
return Map<String, String>.fromIterable(result, key: (path) => path.split('/').last, value: (path) => path);
}
return result;
} on PlatformException catch (e) {
print('[$_tag] Platform exception: $e');
rethrow;
} catch (e) {
print(
'[$_tag] Unsupported operation. Method not found. The exception thrown was: $e');
print('[$_tag] Unsupported operation. Method not found. The exception thrown was: $e');
rethrow;
}
}
static String _handleType(FileType type, String fileExtension) {
switch (type) {
case FileType.IMAGE:
case FileType.image:
return 'IMAGE';
case FileType.AUDIO:
case FileType.audio:
return 'AUDIO';
case FileType.VIDEO:
case FileType.video:
return 'VIDEO';
case FileType.ANY:
case FileType.any:
return 'ANY';
case FileType.CUSTOM:
case FileType.custom:
return '__CUSTOM_' + (fileExtension ?? '');
default:
return 'ANY';

View File

@ -1,17 +1,23 @@
name: file_picker
description: A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extension filtering support.
homepage: https://github.com/miguelpruivo/plugins_flutter_file_picker
version: 1.4.3+2
version: 1.5.0
dependencies:
flutter:
sdk: flutter
flutter_plugin_android_lifecycle: ^1.0.6
environment:
sdk: ">=2.0.0 <3.0.0"
sdk: ">=2.0.0-dev.28.0 <3.0.0"
flutter: ">=1.10.0 <2.0.0"
flutter:
plugin:
androidPackage: com.mr.flutter.plugin.filepicker
pluginClass: FilePickerPlugin
platforms:
android:
package: com.mr.flutter.plugin.filepicker
pluginClass: FilePickerPlugin
ios:
pluginClass: FilePickerPlugin