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 ## 1.4.3+2
Updates dependencies. Updates dependencies.

View File

@ -34,5 +34,6 @@ android {
dependencies { dependencies {
implementation 'androidx.core:core:1.0.2' implementation 'androidx.core:core:1.0.2'
implementation 'androidx.annotation:annotation:1.0.0' 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; package com.mr.flutter.plugin.filepicker;
import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.app.Application;
import android.content.pm.PackageManager; import android.os.Bundle;
import android.net.Uri; import android.os.Handler;
import android.os.Build; import android.os.Looper;
import android.os.Environment;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.util.Log; import android.util.Log;
import android.webkit.MimeTypeMap; 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 io.flutter.embedding.engine.plugins.FlutterPlugin;
import java.util.ArrayList; 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.MethodCall;
import io.flutter.plugin.common.MethodChannel; 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;
import io.flutter.plugin.common.PluginRegistry.Registrar; 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 String TAG = "FilePicker";
private static final int PERM_CODE = (FilePickerPlugin.class.hashCode() + 50) & 0x0000ffff; private static final String CHANNEL = "miguelruivo.flutter.plugins.file_picker";
private static final String TAG = "FilePicker";
private static final String permission = Manifest.permission.READ_EXTERNAL_STORAGE;
private static Result result; private class LifeCycleObserver
private static Registrar instance; implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver {
private static String fileType; private final Activity thisActivity;
private static boolean isMultipleSelection = false;
/** Plugin registration. */ LifeCycleObserver(final Activity activity) {
public static void registerWith(Registrar registrar) { this.thisActivity = activity;
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);
} }
return false;
}
});
instance.addRequestPermissionsResultListener(new PluginRegistry.RequestPermissionsResultListener() { @Override
@Override public void onCreate(@NonNull final LifecycleOwner owner) {
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;
} }
return false;
}
});
}
private static void runOnUiThread(final Result result, final Object o, final boolean success) { @Override
instance.activity().runOnUiThread(new Runnable() { public void onStart(@NonNull final LifecycleOwner owner) {
@Override }
public void run() {
if(success) { @Override
result.success(o); public void onResume(@NonNull final LifecycleOwner owner) {
} else if(o != null) { }
result.error(TAG,(String)o, null);
} else { @Override
result.notImplemented(); 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.io.InputStream;
import java.util.Random; import java.util.Random;
import io.flutter.plugin.common.MethodChannel;
public class FileUtils { public class FileUtils {
private static final String TAG = "FilePickerUtils"; 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; final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
if (isKitKat) { if (isKitKat) {
return getForApi19(context, uri); return getForApi19(context, uri);
@ -41,7 +39,7 @@ public class FileUtils {
@TargetApi(19) @TargetApi(19)
@SuppressWarnings("deprecation") @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); Log.e(TAG, "Getting for API 19 or above" + uri);
if (DocumentsContract.isDocumentUri(context, uri)) { if (DocumentsContract.isDocumentUri(context, uri)) {
Log.e(TAG, "Document URI"); Log.e(TAG, "Document URI");
@ -62,27 +60,27 @@ public class FileUtils {
if (id.startsWith("raw:")) { if (id.startsWith("raw:")) {
return id.replaceFirst("raw:", ""); return id.replaceFirst("raw:", "");
} }
String[] contentUriPrefixesToTry = new String[]{ final String[] contentUriPrefixesToTry = new String[]{
"content://downloads/public_downloads", "content://downloads/public_downloads",
"content://downloads/my_downloads", "content://downloads/my_downloads",
"content://downloads/all_downloads" "content://downloads/all_downloads"
}; };
if(id.contains(":")){ if (id.contains(":")) {
id = id.split(":")[1]; id = id.split(":")[1];
} }
for (String contentUriPrefix : contentUriPrefixesToTry) { for (final String contentUriPrefix : contentUriPrefixesToTry) {
Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.valueOf(id)); final Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.valueOf(id));
try { try {
String path = getDataColumn(context, contentUri, null, null); final String path = getDataColumn(context, contentUri, null, null);
if (path != null) { if (path != null) {
return path; return path;
}
} catch (Exception e) {
Log.e(TAG, "Something went wrong while retrieving document path: " + e.toString());
} }
} catch (final Exception e) {
Log.e(TAG, "Something went wrong while retrieving document path: " + e.toString());
} }
} }
}
} else if (isMediaDocument(uri)) { } else if (isMediaDocument(uri)) {
Log.e(TAG, "Media Document URI"); Log.e(TAG, "Media Document URI");
final String docId = DocumentsContract.getDocumentId(uri); final String docId = DocumentsContract.getDocumentId(uri);
@ -123,8 +121,8 @@ public class FileUtils {
return null; return null;
} }
private static String getDataColumn(Context context, Uri uri, String selection, private static String getDataColumn(final Context context, final Uri uri, final String selection,
String[] selectionArgs) { final String[] selectionArgs) {
Cursor cursor = null; Cursor cursor = null;
final String column = "_data"; final String column = "_data";
final String[] projection = { final String[] projection = {
@ -137,104 +135,109 @@ public class FileUtils {
final int index = cursor.getColumnIndexOrThrow(column); final int index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(index); return cursor.getString(index);
} }
} catch(Exception ex){ } catch (final Exception ex) {
} finally { } finally {
if (cursor != null) if (cursor != null) {
cursor.close(); cursor.close();
}
} }
return null; return null;
} }
public static String getFileName(Uri uri, Context context) { public static String getFileName(Uri uri, final Context context) {
String result = null; String result = null;
//if uri is content //if uri is content
if (uri.getScheme() != null && uri.getScheme().equals("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 { try {
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
//local filesystem //local filesystem
int index = cursor.getColumnIndex("_data"); int index = cursor.getColumnIndex("_data");
if (index == -1) if (index == -1)
//google drive //google drive
{
index = cursor.getColumnIndex("_display_name"); index = cursor.getColumnIndex("_display_name");
}
result = cursor.getString(index); result = cursor.getString(index);
if (result != null) if (result != null) {
uri = Uri.parse(result); uri = Uri.parse(result);
else } else {
return null; return null;
}
} }
} finally { } finally {
cursor.close(); cursor.close();
} }
} }
if(uri.getPath() != null) { if (uri.getPath() != null) {
result = uri.getPath(); result = uri.getPath();
int cut = result.lastIndexOf('/'); final int cut = result.lastIndexOf('/');
if (cut != -1) if (cut != -1) {
result = result.substring(cut + 1); result = result.substring(cut + 1);
}
} }
return result; 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"); Log.i(TAG, "Caching file from remote/external URI");
FileOutputStream fos = null; FileOutputStream fos = null;
final String fileName = FileUtils.getFileName(uri, context); 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 { try {
fos = new FileOutputStream(externalFile); final BufferedOutputStream out = new BufferedOutputStream(fos);
try { final InputStream in = context.getContentResolver().openInputStream(uri);
BufferedOutputStream out = new BufferedOutputStream(fos);
InputStream in = context.getContentResolver().openInputStream(uri);
byte[] buffer = new byte[8192]; final byte[] buffer = new byte[8192];
int len = 0; int len = 0;
while ((len = in.read(buffer)) >= 0) { while ((len = in.read(buffer)) >= 0) {
out.write(buffer, 0, len); out.write(buffer, 0, len);
}
out.flush();
} finally {
fos.getFD().sync();
} }
} catch (Exception e) {
try { out.flush();
fos.close(); } finally {
} catch(IOException | NullPointerException ex) { fos.getFD().sync();
Log.e(TAG, "Failed to close file streams: " + e.getMessage(),null); }
return null; } catch (final Exception e) {
} try {
Log.e(TAG, "Failed to retrieve path: " + e.getMessage(),null); fos.close();
} catch (final IOException | NullPointerException ex) {
Log.e(TAG, "Failed to close file streams: " + e.getMessage(), null);
return null; return null;
} }
Log.e(TAG, "Failed to retrieve path: " + e.getMessage(), null);
return null;
}
Log.i(TAG, "File loaded and cached at:" + externalFile); Log.i(TAG, "File loaded and cached at:" + externalFile);
return externalFile; return externalFile;
} }
private static boolean isDropBoxUri(Uri uri) { private static boolean isDropBoxUri(final Uri uri) {
return "com.dropbox.android.FileCache".equals(uri.getAuthority()); 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()); 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()); 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()); 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()); return "com.google.android.apps.photos.content".equals(uri.getAuthority());
} }

View File

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

View File

@ -4,18 +4,17 @@ import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
enum FileType { enum FileType {
ANY, any,
IMAGE, image,
VIDEO, video,
AUDIO, audio,
CUSTOM, custom,
} }
class FilePicker { class FilePicker {
static const MethodChannel _channel = const MethodChannel('file_picker');
static const String _tag = 'FilePicker';
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 /// 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.
@ -23,8 +22,7 @@ class FilePicker {
/// A [fileExtension] can be provided to filter the picking results. /// A [fileExtension] can be provided to filter the picking results.
/// If provided, it will be use the `FileType.CUSTOM` for that [fileExtension]. /// 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. /// If not, `FileType.ANY` will be used and any combination of files can be multi picked at once.
static Future<Map<String, String>> getMultiFilePath( static Future<Map<String, String>> getMultiFilePath({FileType type = FileType.any, String fileExtension}) async =>
{FileType type = FileType.ANY, String fileExtension}) async =>
await _getPath(_handleType(type, fileExtension), true); await _getPath(_handleType(type, fileExtension), true);
/// Returns an absolute file path from the calling platform. /// 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. /// 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.) /// 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. /// Defaults to `FileType.ANY` which will display all file types.
static Future<String> getFilePath( static Future<String> getFilePath({FileType type = FileType.any, String fileExtension}) async =>
{FileType type = FileType.ANY, String fileExtension}) async =>
await _getPath(_handleType(type, fileExtension), false); await _getPath(_handleType(type, fileExtension), false);
/// 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( static Future<File> getFile({FileType type = FileType.any, String fileExtension}) async {
{FileType type = FileType.ANY, String fileExtension}) async { final String filePath = await _getPath(_handleType(type, fileExtension), false);
final String filePath =
await _getPath(_handleType(type, fileExtension), false);
return filePath != null ? File(filePath) : null; 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 /// 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( static Future<List<File>> getMultiFile({FileType type = FileType.any, String fileExtension}) async {
{FileType type = FileType.ANY, String fileExtension}) async { final Map<String, String> paths = await _getPath(_handleType(type, fileExtension), true);
final Map<String, String> paths = return paths != null && paths.isNotEmpty ? paths.values.map((path) => File(path)).toList() : null;
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 { static Future<dynamic> _getPath(String type, bool multipleSelection) async {
@ -67,31 +58,29 @@ class FilePicker {
if (result is String) { if (result is String) {
result = [result]; result = [result];
} }
return Map<String, String>.fromIterable(result, return Map<String, String>.fromIterable(result, key: (path) => path.split('/').last, value: (path) => path);
key: (path) => path.split('/').last, value: (path) => path);
} }
return result; return result;
} on PlatformException catch (e) { } on PlatformException catch (e) {
print('[$_tag] Platform exception: $e'); print('[$_tag] Platform exception: $e');
rethrow; rethrow;
} catch (e) { } catch (e) {
print( print('[$_tag] Unsupported operation. Method not found. The exception thrown was: $e');
'[$_tag] Unsupported operation. Method not found. The exception thrown was: $e');
rethrow; rethrow;
} }
} }
static String _handleType(FileType type, String fileExtension) { static String _handleType(FileType type, String fileExtension) {
switch (type) { switch (type) {
case FileType.IMAGE: case FileType.image:
return 'IMAGE'; return 'IMAGE';
case FileType.AUDIO: case FileType.audio:
return 'AUDIO'; return 'AUDIO';
case FileType.VIDEO: case FileType.video:
return 'VIDEO'; return 'VIDEO';
case FileType.ANY: case FileType.any:
return 'ANY'; return 'ANY';
case FileType.CUSTOM: case FileType.custom:
return '__CUSTOM_' + (fileExtension ?? ''); return '__CUSTOM_' + (fileExtension ?? '');
default: default:
return 'ANY'; return 'ANY';

View File

@ -1,17 +1,23 @@
name: file_picker 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. 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 homepage: https://github.com/miguelpruivo/plugins_flutter_file_picker
version: 1.4.3+2 version: 1.5.0
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_plugin_android_lifecycle: ^1.0.6
environment: 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: flutter:
plugin: plugin:
androidPackage: com.mr.flutter.plugin.filepicker platforms:
pluginClass: FilePickerPlugin android:
package: com.mr.flutter.plugin.filepicker
pluginClass: FilePickerPlugin
ios:
pluginClass: FilePickerPlugin