Skip to content

Commit

Permalink
see changelog (v1.4.0)
Browse files Browse the repository at this point in the history
* Better handling on Android of buggy file managers that return no intent when canceling the file selection even though it returns Activity.RESULT_OK (#111)

* Add file_picker Go support. (#132)

* Add file_picker Go support.

Originally written by @chunhunghan, cleaned up and fixed for MacOS by Geert-Johan Riemer.

Co-authored-by: chunhunghan <[email protected]>

* Add improved instructions to go/README.md

* removes deprecated Android SDK code and fixes an issue that could prevent some downloaded files from being picked

* adds getMultiFile and prevents UI blocking when picking large remote files

* updates readme file
  • Loading branch information
Miguel Ruivo committed Aug 31, 2019
1 parent 3f67653 commit f502423
Show file tree
Hide file tree
Showing 26 changed files with 593 additions and 383 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
117 changes: 17 additions & 100 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
```
before `<application>` 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 `<project root>/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).

```
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
```

* **_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.

```
<key>NSAppleMusicUsageDescription</key>
<string>Explain why your app uses music</string>
```


* **_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.

```
<key>NSPhotoLibraryUsageDescription</key>
<string>Explain why your app uses photo library</string>
```

**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<String,String>` 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<String,String> 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<String> allNames = filePaths.keys; // List of all file names
List<String> 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.)
Expand All @@ -107,16 +14,26 @@ 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<File>` 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.

![Demo](https://github.com/miguelpruivo/plugins_flutter_file_picker/blob/master/example/example.gif)
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
See example app.
## Example App
![Demo](https://github.com/miguelpruivo/plugins_flutter_file_picker/blob/master/example/example.gif)

## Getting Started

Expand Down
29 changes: 0 additions & 29 deletions android/.idea/codeStyles/Project.xml

This file was deleted.

1 change: 1 addition & 0 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.mr.flutter.plugin.filepicker">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -52,46 +52,58 @@ public static void registerWith(Registrar registrar) {
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<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){
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());

if(fullPath == null) {
fullPath = FileUtils.getUriFromRemote(instance.activeContext(), uri, result);
}
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;

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;
Expand All @@ -115,9 +127,24 @@ public boolean onRequestPermissionsResult(int requestCode, String[] strings, int
});
}

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;

Expand Down Expand Up @@ -177,13 +204,9 @@ private static void startFileExplorer(String type) {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ public static String getPath(final Uri uri, Context context) {
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);
Expand All @@ -50,11 +57,11 @@ private static String getForApi19(Context context, Uri uri) {
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:")) {
Expand All @@ -65,6 +72,9 @@ private static String getForApi19(Context context, Uri uri) {
"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 {
Expand Down Expand Up @@ -132,6 +142,7 @@ private static String getDataColumn(Context context, Uri uri, String selection,
final int index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(index);
}
} catch(Exception ex){
} finally {
if (cursor != null)
cursor.close();
Expand Down
Loading

0 comments on commit f502423

Please sign in to comment.