diff --git a/CHANGELOG.md b/CHANGELOG.md index 1adefc7..e17a5fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 1.4.0 + +**New features** + * Adds Desktop support throught **[go-flutter](https://github.com/go-flutter-desktop/go-flutter)**, you can see detailed instructions on how to get in runing [here](https://github.com/go-flutter-desktop/hover). + * Adds Desktop example, to run it just do `hover init` and then `hover run` within the plugin's example folder (you must have go and hover installed, check the previous point). + * Similar to `getFile`, now there is also a `getMultiFile` which behaves the same way, but returning a list of files instead. + +**Improvements:** + * Updates Android SDK deprecated code. + * Sometimes when a big file was being picked from a remote directory (GDrive for example), the UI could be blocked. Now this shouldn't happen anymore. + ## 1.3.8 **Bug fix:** Fixes an issue that could cause a crash when picking files with very long names. diff --git a/README.md b/README.md index c902d08..a8e3709 100644 --- a/README.md +++ b/README.md @@ -2,103 +2,10 @@ [![Awesome Flutter](https://img.shields.io/badge/Awesome-Flutter-blue.svg?longCache=true&style=flat-square)](https://github.com/Solido/awesome-flutter) [![Codemagic build status](https://api.codemagic.io/apps/5ce89f4a9b46f5000ca89638/5ce89f4a9b46f5000ca89637/status_badge.svg)](https://codemagic.io/apps/5ce89f4a9b46f5000ca89638/5ce89f4a9b46f5000ca89637/latest_build) -# file_picker - +![fluter_file_picker](https://user-images.githubusercontent.com/27860743/64064695-b88dab00-cbfc-11e9-814f-30921b66035f.png) +# File Picker A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extensions filtering support. -## Installation - -First, add *file_picker* as a dependency in [your pubspec.yaml file](https://flutter.io/platform-plugins/). - -``` -file_picker: ^1.3.8 -``` -### Android - -Add -``` - -``` -before `` to your app's `AndroidManifest.xml` file. This is required to access files from external storage. - - -### iOS -Based on the location of the files that you are willing to pick paths, you may need to add some keys to your iOS app's _Info.plist_ file, located in `/ios/Runner/Info.plist`: - -* **_UIBackgroundModes_** with the **_fetch_** and **_remote-notifications_** keys - Required if you'll be using the `FileType.ANY` or `FileType.CUSTOM`. Describe why your app needs to access background taks, such downloading files (from cloud services). This is called _Required background modes_, with the keys _App download content from network_ and _App downloads content in response to push notifications_ respectively in the visual editor (since both methods aren't actually overriden, not adding this property/keys may only display a warning, but shouldn't prevent its correct usage). - - ``` - UIBackgroundModes - - fetch - remote-notification - - ``` - -* **_NSAppleMusicUsageDescription_** - Required if you'll be using the `FileType.AUDIO`. Describe why your app needs permission to access music library. This is called _Privacy - Media Library Usage Description_ in the visual editor. - - ``` - NSAppleMusicUsageDescription - Explain why your app uses music - ``` - - -* **_NSPhotoLibraryUsageDescription_** - Required if you'll be using the `FileType.IMAGE` or `FileType.VIDEO`. Describe why your app needs permission for the photo library. This is called _Privacy - Photo Library Usage Description_ in the visual editor. - - ``` - NSPhotoLibraryUsageDescription - Explain why your app uses photo library - ``` - -**Note:** Any iOS version below 11.0, will require an Apple Developer Program account to enable _CloudKit_ and make it possible to use the document picker (which happens when you select `FileType.ALL`, `FileType.CUSTOM` or any other option with `getMultiFilePath()`). You can read more about it [here]( https://developer.apple.com/library/archive/documentation/DataManagement/Conceptual/CloudKitQuickStart/EnablingiCloudandConfiguringCloudKit/EnablingiCloudandConfiguringCloudKit.html). - - -## Usage -There are only two methods that should be used with this package: - -#### `FilePicker.getFilePath()` - -Will let you pick a **single** file. This receives two optional parameters: the `fileType` for specifying the type of the picker and a `fileExtension` parameter to filter selectable files. The available filters are: - * `FileType.ANY` - Will let you pick all available files. - * `FileType.CUSTOM` - Will let you pick a single path for the extension matching the `fileExtension` provided. - * `FileType.IMAGE` - Will let you pick a single image file. Opens gallery on iOS. - * `FileType.VIDEO` - WIll let you pick a single video file. Opens gallery on iOS. - * `FileType.AUDIO` - Will let you pick a single audio file. Opens music on iOS. Note that DRM protected files won't provide a path, `null` will be returned instead. - -#### `FilePicker.getMultiFilePath()` - -Will let you select **multiple** files and retrieve its path at once. Optionally you can provide a `fileExtension` parameter to filter the allowed selectable files. -Will return a `Map` with the files name (`key`) and corresponding path (`value`) of all selected files. -Picking multiple paths from iOS gallery (image and video) aren't currently supported. - -#### Usages - -So, a few example usages can be as follow: -``` -// Single file path -String filePath; -filePath = await FilePicker.getFilePath(type: FileType.ANY); // will let you pick one file path, from all extensions -filePath = await FilePicker.getFilePath(type: FileType.CUSTOM, fileExtension: 'svg'); // will filter and only let you pick files with svg extension - -// Pick a single file directly -File file = await FilePicker.getFile(type: FileType.ANY); // will return a File object directly from the selected file - -// Multi file path -Map filesPaths; -filePaths = await FilePicker.getMultiFilePath(); // will let you pick multiple files of any format at once -filePaths = await FilePicker.getMultiFilePath(fileExtension: 'pdf'); // will let you pick multiple pdf files at once -filePaths = await FilePicker.getMultiFilePath(type: FileType.IMAGE); // will let you pick multiple image files at once - -List allNames = filePaths.keys; // List of all file names -List allPaths = filePaths.values; // List of all paths -String someFilePath = filePaths['fileName']; // Access a file path directly by its name (matching a key) -``` - -##### A few side notes -* Using `getMultiFilePath()` on iOS will always use the document picker (aka Files app). This means that multi picks are not currently supported for photo library images/videos or music library files. -* When using `FileType.CUSTOM`, unsupported extensions will throw a `MissingPluginException` that is handled by the plugin. -* On Android, when available, you should avoid using third-party file explorers as those may prevent file extension filtering (behaving as `FileType.ANY`). In this scenario, you will need to validate it on return. - ## Currently supported features * [X] Load paths from **cloud files** (GDrive, Dropbox, iCloud) * [X] Load path from a **custom format** by providing a file extension (pdf, svg, zip, etc.) @@ -107,17 +14,27 @@ String someFilePath = filePaths['fileName']; // Access a file path directly by i * [X] Load path from **audio** * [X] Load path from **video** * [X] Load path from **any** -* [X] Create a `File` object from **any** selected file +* [X] Create a `File` or `List` objects from **any** selected file(s) +* [X] Supports desktop through **go-flutter** (MacOS, Windows, Linux) If you have any feature that you want to see in this package, please add it [here](https://github.com/miguelpruivo/plugins_flutter_file_picker/issues/99). 🎉 -## Demo App +## Documentation +See the **[File Picker Wiki](https://github.com/miguelpruivo/flutter_file_picker/wiki)** for every detail on about how to install, setup and use it. +1. [Installation](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/Installation) +2. [Setup](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/Setup) + * [Android](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/Setup#android) + * [iOS](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/Setup#ios) + * [Desktop (go-flutter)](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/Setup/_edit#desktop-go-flutter) +3. [API](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/api) + * [Filters](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/API#filters) + * [Methods](https://github.com/miguelpruivo/plugins_flutter_file_picker/wiki/API#methods) +4. [Example App](https://github.com/miguelpruivo/flutter_file_picker/blob/master/example/lib/main.dart) + +## Example App ![Demo](https://github.com/miguelpruivo/plugins_flutter_file_picker/blob/master/example/example.gif) -## Example -See example app. - ## Getting Started For help getting started with Flutter, view our online diff --git a/android/.idea/codeStyles/Project.xml b/android/.idea/codeStyles/Project.xml deleted file mode 100644 index 30aa626..0000000 --- a/android/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 79f7c64..51cc429 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,4 @@ + 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 ced49cd..4664a65 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 @@ -52,46 +52,58 @@ public class FilePickerPlugin implements MethodCallHandler { instance = registrar; instance.addActivityResultListener(new PluginRegistry.ActivityResultListener() { @Override - public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + public boolean onActivityResult(int requestCode, int resultCode, final Intent data) { if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) { - 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){ - result.success(paths); - } else { - result.success(paths.get(0)); - } - } else if (data != null) { - Uri uri = data.getData(); - Log.i(TAG, "[SingleFilePick] File URI:" +data.getData().toString()); - String fullPath = FileUtils.getPath(uri, instance.context()); + 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) { + 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; - if(fullPath != null) { - Log.i(TAG, "Absolute file path:" + fullPath); - result.success(fullPath); - } else { - result.error(TAG, "Failed to retrieve path." ,null); - } - } - return true; } else if(requestCode == REQUEST_CODE && resultCode == Activity.RESULT_CANCELED) { result.success(null); return true; @@ -115,9 +127,24 @@ public class FilePickerPlugin implements MethodCallHandler { }); } + 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 onMethodCall(MethodCall call, Result result) { - this.result = result; + FilePickerPlugin.result = result; fileType = resolveType(call.method); isMultipleSelection = (boolean)call.arguments; @@ -177,13 +204,9 @@ public class FilePickerPlugin implements MethodCallHandler { Intent intent; if (checkPermission()) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - intent = new Intent(Intent.ACTION_PICK); - } else { - intent = new Intent(Intent.ACTION_GET_CONTENT); - } - Uri uri = Uri.parse(Environment.getExternalStorageDirectory().getPath() + File.separator); + intent = new Intent(Intent.ACTION_GET_CONTENT); + Uri uri = Uri.parse(FileUtils.getExternalPath(instance.activeContext()) + File.separator); intent.setDataAndType(uri, type); intent.setType(type); intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, isMultipleSelection); 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 dc91416..bf66f09 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 @@ -38,6 +38,13 @@ public class FileUtils { return null; } + public static String getExternalPath(Context context) { + if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { + return context.getExternalFilesDir(null).getAbsolutePath(); + } + return context.getFilesDir().getAbsolutePath(); + } + @TargetApi(19) private static String getForApi19(Context context, Uri uri) { Log.e(TAG, "Getting for API 19 or above" + uri); @@ -50,11 +57,11 @@ public class FileUtils { final String type = split[0]; if ("primary".equalsIgnoreCase(type)) { Log.e(TAG, "Primary External Document URI"); - return Environment.getExternalStorageDirectory() + "/" + split[1]; + return getExternalPath(context) + "/" + split[1]; } } else if (isDownloadsDocument(uri)) { Log.e(TAG, "Downloads External Document URI"); - final String id = DocumentsContract.getDocumentId(uri); + String id = DocumentsContract.getDocumentId(uri); if (!TextUtils.isEmpty(id)) { if (id.startsWith("raw:")) { @@ -65,6 +72,9 @@ public class FileUtils { "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 { @@ -132,6 +142,7 @@ public class FileUtils { final int index = cursor.getColumnIndexOrThrow(column); return cursor.getString(index); } + } catch(Exception ex){ } finally { if (cursor != null) cursor.close(); diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 3f815b3..ae8e6ae 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -15,7 +15,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' @@ -24,7 +24,7 @@ android { defaultConfig { applicationId "com.mr.flutter.plugin.filepickerexample" minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 94e4412..91966c1 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -7,8 +7,7 @@ to allow setting breakpoints, to provide hot reload, etc. --> - - +