From a9cc55f5f54880a6549841041b0bafd33967422a Mon Sep 17 00:00:00 2001 From: Miguel Ruivo Date: Sat, 14 Mar 2020 17:32:05 +0000 Subject: [PATCH] Android V2 embedding - Adds support for Android V2 embedding; - Refactors FileType to match camelCase enum Dart guideline; --- CHANGELOG.md | 5 + android/build.gradle | 1 + .../plugin/filepicker/FilePickerDelegate.java | 209 ++++++++ .../plugin/filepicker/FilePickerPlugin.java | 486 ++++++++++-------- .../flutter/plugin/filepicker/FileUtils.java | 127 ++--- example/lib/src/file_picker_demo.dart | 59 +-- lib/file_picker.dart | 53 +- pubspec.yaml | 14 +- 8 files changed, 614 insertions(+), 340 deletions(-) create mode 100644 android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a210ba..244965c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/android/build.gradle b/android/build.gradle index b67eb14..0c829aa 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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" } } diff --git a/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java b/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java new file mode 100644 index 0000000..0a356fb --- /dev/null +++ b/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java @@ -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 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); + } + +} diff --git a/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java b/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java index 197229b..5b14950 100644 --- a/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java +++ b/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java @@ -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 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(); + } } diff --git a/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java b/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java index 4daa659..ce310da 100644 --- a/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java +++ b/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java @@ -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()); } diff --git a/example/lib/src/file_picker_demo.dart b/example/lib/src/file_picker_demo.dart index 582ebdd..3aa2f09 100644 --- a/example/lib/src/file_picker_demo.dart +++ b/example/lib/src/file_picker_demo.dart @@ -26,17 +26,15 @@ class _FilePickerDemoState extends State { } 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 { 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 { items: [ 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 { 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 { ), 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 { subtitle: new Text(path), ); }, - separatorBuilder: - (BuildContext context, int index) => - new Divider(), + separatorBuilder: (BuildContext context, int index) => new Divider(), )), ) : new Container(), diff --git a/lib/file_picker.dart b/lib/file_picker.dart index 980c197..b328f39 100644 --- a/lib/file_picker.dart +++ b/lib/file_picker.dart @@ -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` 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> getMultiFilePath( - {FileType type = FileType.ANY, String fileExtension}) async => + static Future> 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 getFilePath( - {FileType type = FileType.ANY, String fileExtension}) async => + static Future 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 getFile( - {FileType type = FileType.ANY, String fileExtension}) async { - final String filePath = - await _getPath(_handleType(type, fileExtension), false); + static Future 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> getMultiFile( - {FileType type = FileType.ANY, String fileExtension}) async { - final Map paths = - await _getPath(_handleType(type, fileExtension), true); - return paths != null && paths.isNotEmpty - ? paths.values.map((path) => File(path)).toList() - : null; + static Future> getMultiFile({FileType type = FileType.any, String fileExtension}) async { + final Map paths = await _getPath(_handleType(type, fileExtension), true); + return paths != null && paths.isNotEmpty ? paths.values.map((path) => File(path)).toList() : null; } static Future _getPath(String type, bool multipleSelection) async { @@ -67,31 +58,29 @@ class FilePicker { if (result is String) { result = [result]; } - return Map.fromIterable(result, - key: (path) => path.split('/').last, value: (path) => path); + return Map.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'; diff --git a/pubspec.yaml b/pubspec.yaml index d2287ff..0eff72d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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