diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ef564df..0df339ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,21 @@ +## 1.3.0 + +**Breaking changes** + * `FileType.CAMERA` is no longer available, if you need it, you can use this package along with [image_picker](https://pub.dartlang.org/packages/image_picker). + +**New features** + * You can now pick multiple files by using the `getMultiFilePath()` method which will return a `Map` with all paths from selected files, where the key matches the file name and the value its path. Optionally, it also supports filtering by file extension, otherwise all files will be selectable. Nevertheless, you should keep using `getFilePath()` for single path picking. + * You can now use `FileType.AUDIO` to pick audio files. In iOS this will let you select from your music library. Paths from DRM protected files won't be loaded (see README for more details). + * Adds `getFile()` utility method that does the same of `getFilePath()` but returns a `File` object instead, for the returned path. + +**Bug fixes and updates** + * This package is no longer attached to the [image_picker](https://pub.dartlang.org/packages/image_picker), and because of that, camera permission is also no longer required. + * Fixes an issue where sometimes the _InputStream_ wasn't being properly closed. Also, its exception is now being forward to the plugin caller. + * Fixes an issue where the picker, when canceled, wasn't calling the result callback on the underlying platforms. + ## 1.2.0 -**Breaking change** - Migrate from the deprecated original Android Support Library to AndroidX. This shouldn't result in any functional changes, but it requires any Android apps using this plugin to [also migrate](https://developer.android.com/jetpack/androidx/migrate) if they're using the original support library. +**Breaking change**: Migrate from the deprecated original Android Support Library to AndroidX. This shouldn't result in any functional changes, but it requires any Android apps using this plugin to [also migrate](https://developer.android.com/jetpack/androidx/migrate) if they're using the original support library. ## 1.1.1 diff --git a/README.md b/README.md index 6abb4d1b..c356a890 100644 --- a/README.md +++ b/README.md @@ -3,96 +3,111 @@ # file_picker -File picker plugin alows you to use a native file explorer to load absolute file path from different file types. +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.2.0 +file_picker: ^1.3.0 ``` -## Android -Add `` to your app `AndroidManifest.xml` file. +### Android +Add `` to your app `AndroidManifest.xml` file. This is required due to file caching when a path is required from a remote file (eg. Google Drive). -## iOS -Since we are using *image_picker* as a dependency from this plugin to load paths from gallery and camera, we need the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: +### 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). -* `NSPhotoLibraryUsageDescription` - describe why your app needs permission for the photo library. This is called _Privacy - Photo Library Usage Description_ in the visual editor. -* `NSCameraUsageDescription` - describe why your app needs access to the camera. This is called _Privacy - Camera Usage Description_ in the visual editor. -* `NSMicrophoneUsageDescription` - describe why your app needs access to the microphone, if you intend to record videos. This is called _Privacy - Microphone Usage Description_ in the visual editor. -* `UIBackgroundModes` with the `fetch` and `remote-notifications` keys - describe why your app needs to access background taks, such downloading files (from cloud services) when not cached to locate path. 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). ## Usage -There's only one method within this package -`FilePicker.getFilePath()` -this receives 2 optional parameters, the `fileType` and a `fileExtension` to be used along with `FileType.CUSTOM`. -So, 2 basically usages may be: +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: ``` -await FilePicker.getFilePath(type: FileType.ANY); // will display all file types -await FilePicker.getFilePath(type: FileType.CUSTOM, fileExtension: 'svg'); // will filter and display only files with SVG extension. +// 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) ``` -**Note:** When using `FileType.CUSTOM`, unsupported extensions will throw a `MissingPluginException` that is handled by the plugin. +##### 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.) +* [X] Load path from **multiple files** optionally, supplying a file extension * [X] Load path from **gallery** -* [X] Load path from **camera** +* [X] Load path from **audio** * [X] Load path from **video** -* [X] Load path from **any** type of file (without filtering) -* [X] Load path from a **custom format** by providing a file extension (pdf, svg, zip, etc.) +* [X] Load path from **any** +* [X] Create a `File` object from **any** selected file ## Demo App ![Demo](https://github.com/miguelpruivo/plugins_flutter_file_picker/blob/master/example/example.gif) ## Example -``` -import 'package:file_picker/file_picker.dart'; - -class MyHomePage extends StatefulWidget { - @override - _MyHomePageState createState() => new _MyHomePageState(); -} - -class _MyHomePageState extends State { - String _filePath; - - void getFilePath() async { - try { - String filePath = await FilePicker.getFilePath(type: FileType.ANY); - if (filePath == '') { - return; - } - print("File path: " + filePath); - setState((){this._filePath = filePath;}); - } on PlatformException catch (e) { - print("Error while picking the file: " + e.toString()); - } - } - - @override - Widget build(BuildContext context) { - return new Scaffold( - appBar: new AppBar( - title: new Text('File Picker Example'), - ), - body: new Center( - child: _filePath == null - ? new Text('No file selected.') - : new Text('Path' + _filePath), - ), - floatingActionButton: new FloatingActionButton( - onPressed: getFilePath, - tooltip: 'Select file', - child: new Icon(Icons.sd_storage), - ), - ); - } -} - -``` +See example app. ## Getting Started @@ -100,3 +115,6 @@ For help getting started with Flutter, view our online [documentation](https://flutter.io/). For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). + + + 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 2c300e07..2aa1c0c7 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 @@ -12,10 +12,9 @@ import android.util.Log; import android.webkit.MimeTypeMap; -import java.io.BufferedOutputStream; + import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; +import java.util.ArrayList; import io.flutter.plugin.common.MethodCall; @@ -36,6 +35,7 @@ public class FilePickerPlugin implements MethodCallHandler { private static Result result; private static Registrar instance; private static String fileType; + private static boolean isMultipleSelection = false; /** Plugin registration. */ public static void registerWith(Registrar registrar) { @@ -49,48 +49,40 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) { - 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()); + paths.add(path); + Log.i(TAG, "[MultiFilePick] File #" + currentItem + " - URI: " +currentUri.getPath()); + currentItem++; + } + result.success(paths); + } else if (data != null) { Uri uri = data.getData(); - Log.i(TAG, "URI:" +data.getData().toString()); + Log.i(TAG, "[SingleFilePick] File URI:" +data.getData().toString()); String fullPath = FileUtils.getPath(uri, instance.context()); - String cloudFile = null; - - if(fullPath == null) - { - FileOutputStream fos = null; - cloudFile = instance.activeContext().getCacheDir().getAbsolutePath() + "/" + FileUtils.getFileName(uri, instance.activeContext()); - - try { - fos = new FileOutputStream(cloudFile); - try{ - BufferedOutputStream out = new BufferedOutputStream(fos); - InputStream in = instance.activeContext().getContentResolver().openInputStream(uri); - - 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(); - } - } catch (Exception e) { - e.printStackTrace(); - } - - Log.i(TAG, "Cloud file loaded and cached on:" + cloudFile); - fullPath = cloudFile; + if(fullPath == null) { + fullPath = FileUtils.getUriFromRemote(instance.activeContext(), uri, result); } - Log.i(TAG, "Absolute file path:" + fullPath); - result.success(fullPath); + 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; } + result.error(TAG, "Unknown activity error, please fill an issue." ,null); return false; } }); @@ -112,9 +104,12 @@ public boolean onRequestPermissionsResult(int requestCode, String[] strings, int public void onMethodCall(MethodCall call, Result result) { this.result = result; fileType = resolveType(call.method); + isMultipleSelection = (boolean)call.arguments; - if(fileType == null){ + 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); } @@ -128,7 +123,6 @@ private static boolean checkPermission() { } private static void requestPermission() { - Activity activity = instance.activity(); Log.i(TAG, "Requesting permission: " + permission); String[] perm = { permission }; @@ -142,13 +136,16 @@ private String resolveType(String type) { 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 "PDF": - return "application/pdf"; + case "AUDIO": + return "audio/*"; + case "IMAGE": + return "image/*"; case "VIDEO": return "video/*"; case "ANY": @@ -174,10 +171,9 @@ private static void startFileExplorer(String type) { 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); - Log.d(TAG, "Intent: " + intent.toString()); - instance.activity().startActivityForResult(intent, REQUEST_CODE); } else { requestPermission(); 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 9a6bf566..077653ef 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 @@ -11,7 +11,13 @@ import android.provider.MediaStore; import android.text.TextUtils; import android.util.Log; -import android.webkit.MimeTypeMap; + +import java.io.BufferedOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import io.flutter.plugin.common.MethodChannel; /** * Credits to NiRRaNjAN from utils extracted of in.gauriinfotech.commons;. @@ -19,7 +25,7 @@ public class FileUtils { - private static final String tag = "FilePathPicker"; + private static final String TAG = "FilePickerUtils"; public static String getPath(final Uri uri, Context context) { final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; @@ -38,27 +44,26 @@ public static String getPath(final Uri uri, Context context) { @TargetApi(19) private static String getForApi19(Context context, Uri uri) { - Log.e(tag, "+++ API 19 URI :: " + uri); + Log.e(TAG, "Getting for API 19 or above" + uri); if (DocumentsContract.isDocumentUri(context, uri)) { - Log.e(tag, "+++ Document URI"); + Log.e(TAG, "Document URI"); if (isExternalStorageDocument(uri)) { - Log.e(tag, "+++ External Document URI"); + Log.e(TAG, "External Document URI"); final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; if ("primary".equalsIgnoreCase(type)) { - Log.e(tag, "+++ Primary External Document URI"); + Log.e(TAG, "Primary External Document URI"); return Environment.getExternalStorageDirectory() + "/" + split[1]; } } else if (isDownloadsDocument(uri)) { - Log.e(tag, "+++ Downloads External Document URI"); + Log.e(TAG, "Downloads External Document URI"); final String id = DocumentsContract.getDocumentId(uri); if (!TextUtils.isEmpty(id)) { if (id.startsWith("raw:")) { return id.replaceFirst("raw:", ""); } - String[] contentUriPrefixesToTry = new String[]{ "content://downloads/public_downloads", "content://downloads/my_downloads", @@ -72,26 +77,26 @@ private static String getForApi19(Context context, Uri uri) { return path; } } catch (Exception e) { - Log.e(tag, "+++ Something went wrong while retrieving document path: " + e.toString()); + Log.e(TAG, "Something went wrong while retrieving document path: " + e.toString()); } } } } else if (isMediaDocument(uri)) { - Log.e(tag, "+++ Media Document URI"); + Log.e(TAG, "Media Document URI"); final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; Uri contentUri = null; if ("image".equals(type)) { - Log.e(tag, "+++ Image Media Document URI"); + Log.i(TAG, "Image Media Document URI"); contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; } else if ("video".equals(type)) { - Log.e(tag, "+++ Video Media Document URI"); + Log.i(TAG, "Video Media Document URI"); contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } else if ("audio".equals(type)) { - Log.e(tag, "+++ Audio Media Document URI"); + Log.i(TAG, "Audio Media Document URI"); contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } @@ -103,13 +108,13 @@ private static String getForApi19(Context context, Uri uri) { return getDataColumn(context, contentUri, selection, selectionArgs); } } else if ("content".equalsIgnoreCase(uri.getScheme())) { - Log.e(tag, "+++ No DOCUMENT URI :: CONTENT "); + Log.e(TAG, "NO DOCUMENT URI - CONTENT"); if (isGooglePhotosUri(uri)) return uri.getLastPathSegment(); return getDataColumn(context, uri, null, null); } else if ("file".equalsIgnoreCase(uri.getScheme())) { - Log.e(tag, "+++ No DOCUMENT URI :: FILE "); + Log.e(TAG, "No DOCUMENT URI - FILE"); return uri.getPath(); } return null; @@ -170,6 +175,43 @@ public static String getFileName(Uri uri, Context context) { return result; } + public static String getUriFromRemote(Context context, Uri uri, MethodChannel.Result result) { + + FileOutputStream fos = null; + String cloudFile = context.getCacheDir().getAbsolutePath() + "/" + FileUtils.getFileName(uri, context); + + try { + fos = new FileOutputStream(cloudFile); + try { + BufferedOutputStream out = new BufferedOutputStream(fos); + InputStream in = context.getContentResolver().openInputStream(uri); + + 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(); + } + } catch (Exception e) { + try { + fos.close(); + } catch(IOException 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, "Remote file loaded and cached at:" + cloudFile); + return cloudFile; + } + private static boolean isExternalStorageDocument(Uri uri) { return "com.android.externalstorage.documents".equals(uri.getAuthority()); diff --git a/example/example.gif b/example/example.gif index 328cc1c1..a80843d8 100644 Binary files a/example/example.gif and b/example/example.gif differ diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 652a1592..f626a5c5 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2,26 +2,20 @@ PODS: - file_picker (0.0.1): - Flutter - Flutter (1.0.0) - - image_picker (0.0.1): - - Flutter DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `.symlinks/flutter/ios`) - - image_picker (from `.symlinks/plugins/image_picker/ios`) EXTERNAL SOURCES: file_picker: :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: ".symlinks/flutter/ios" - image_picker: - :path: ".symlinks/plugins/image_picker/ios" SPEC CHECKSUMS: file_picker: 78c3344d9b2c343bb3090c2f032b796242ebaea7 Flutter: 9d0fac939486c9aba2809b7982dfdbb47a7b0296 - image_picker: ee00aab0487cedc80a304085219503cc6d0f2e22 PODFILE CHECKSUM: 1e5af4103afd21ca5ead147d7b81d06f494f51a2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 4190d802..7c79d1d6 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,15 +8,12 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */ = {isa = PBXBuildFile; fileRef = 2D5378251FAA1A9400D5DBA9 /* flutter_assets */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 94EE95F5D222CC3C902F7AA8 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9E29C2B321AA1B6738D05DCC /* libPods-Runner.a */; }; 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; @@ -42,7 +39,6 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = flutter_assets; path = Flutter/flutter_assets; sourceTree = SOURCE_ROOT; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -77,7 +73,6 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 2D5378251FAA1A9400D5DBA9 /* flutter_assets */, 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEBA1CF902C7004384FC /* Flutter.framework */, @@ -177,7 +172,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 0910; + LastUpgradeCheck = 1010; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -210,11 +205,8 @@ buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 2D5378261FAA1A9400D5DBA9 /* flutter_assets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -335,12 +327,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; @@ -389,12 +383,14 @@ CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index f5a8db1a..e4f93ede 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ $(FLUTTER_BUILD_NAME) CFBundleSignature ???? + NSAppleMusicUsageDescription + Used to demonstrate file picker plugin CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS - NSCameraUsageDescription - Used to demonstrate image picker plugin - NSMicrophoneUsageDescription - Used to capture audio for image picker plugin NSPhotoLibraryUsageDescription - Used to demonstrate image picker plugin + Used to demonstrate file picker plugin UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -38,11 +36,11 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIBackgroundModes - - fetch - remote-notification - + UIBackgroundModes + + fetch + remote-notification + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait diff --git a/example/lib/main.dart b/example/lib/main.dart index 5e27ef03..65197d2f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,17 +3,19 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:file_picker/file_picker.dart'; -void main() => runApp(new MyApp()); +void main() => runApp(new FilePickerDemo()); -class MyApp extends StatefulWidget { +class FilePickerDemo extends StatefulWidget { @override - _MyAppState createState() => new _MyAppState(); + _FilePickerDemoState createState() => new _FilePickerDemoState(); } -class _MyAppState extends State { - String _fileName = '...'; - String _path = '...'; +class _FilePickerDemoState extends State { + String _fileName; + String _path; + Map _paths; String _extension; + bool _multiPick = false; bool _hasValidMime = false; FileType _pickingType; TextEditingController _controller = new TextEditingController(); @@ -27,15 +29,20 @@ class _MyAppState extends State { void _openFileExplorer() async { if (_pickingType != FileType.CUSTOM || _hasValidMime) { try { - _path = await FilePicker.getFilePath(type: _pickingType, fileExtension: _extension); + if (_multiPick) { + _path = null; + _paths = await FilePicker.getMultiFilePath(type: _pickingType, fileExtension: _extension); + } else { + _paths = null; + _path = await FilePicker.getFilePath(type: _pickingType, fileExtension: _extension); + } } on PlatformException catch (e) { print("Unsupported operation" + e.toString()); } - if (!mounted) return; setState(() { - _fileName = _path != null ? _path.split('/').last : '...'; + _fileName = _path != null ? _path.split('/').last : _paths != null ? _paths.keys.toString() : '...'; }); } } @@ -45,12 +52,12 @@ class _MyAppState extends State { return new MaterialApp( home: new Scaffold( appBar: new AppBar( - title: const Text('Plugin example app'), + title: const Text('File Picker example app'), ), - body: SingleChildScrollView( - child: new Center( - child: new Padding( - padding: const EdgeInsets.only(top: 50.0, left: 10.0, right: 10.0), + body: new Center( + child: new Padding( + padding: const EdgeInsets.only(top: 50.0, left: 10.0, right: 10.0), + child: new SingleChildScrollView( child: new Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -61,11 +68,11 @@ class _MyAppState extends State { value: _pickingType, items: [ new DropdownMenuItem( - child: new Text('FROM CAMERA'), - value: FileType.CAMERA, + child: new Text('FROM AUDIO'), + value: FileType.AUDIO, ), new DropdownMenuItem( - child: new Text('FROM GALLERY'), + child: new Text('FROM IMAGE'), value: FileType.IMAGE, ), new DropdownMenuItem( @@ -81,16 +88,21 @@ class _MyAppState extends State { value: FileType.CUSTOM, ), ], - onChanged: (value) => setState(() => _pickingType = value)), + onChanged: (value) => setState(() { + _pickingType = value; + if (_pickingType != FileType.CUSTOM) { + _controller.text = _extension = ''; + } + })), ), - new ConstrainedBox( - constraints: new BoxConstraints(maxWidth: 150.0), + ConstrainedBox( + constraints: BoxConstraints.tightFor(width: 100.0), child: _pickingType == FileType.CUSTOM ? new TextFormField( - maxLength: 20, + maxLength: 15, autovalidate: true, controller: _controller, - decoration: InputDecoration(labelText: 'File type'), + decoration: InputDecoration(labelText: 'File extension'), keyboardType: TextInputType.text, textCapitalization: TextCapitalization.none, validator: (value) { @@ -104,6 +116,14 @@ class _MyAppState extends State { ) : new Container(), ), + 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), + value: _multiPick, + ), + ), new Padding( padding: const EdgeInsets.only(top: 50.0, bottom: 20.0), child: new RaisedButton( @@ -111,33 +131,36 @@ class _MyAppState extends State { child: new Text("Open file picker"), ), ), - new Text( - 'URI PATH ', - textAlign: TextAlign.center, - style: new TextStyle(fontWeight: FontWeight.bold), - ), - new Text( - _path ?? '...', - textAlign: TextAlign.center, - softWrap: true, - textScaleFactor: 0.85, - ), - new Padding( - padding: const EdgeInsets.only(top: 10.0), - child: new Text( - 'FILE NAME ', - textAlign: TextAlign.center, - style: new TextStyle(fontWeight: FontWeight.bold), - ), - ), - new Text( - _fileName, - textAlign: TextAlign.center, + new Builder( + builder: (BuildContext context) => new Container( + padding: const EdgeInsets.only(bottom: 30.0), + height: MediaQuery.of(context).size.height * 0.50, + child: new Scrollbar( + child: _path != null || _paths != null + ? new ListView.separated( + 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; + + return new ListTile( + title: new Text( + name, + ), + subtitle: new Text(path), + ); + }, + separatorBuilder: (BuildContext context, int index) => new Divider(), + ) + : new Container(), + ), + ), ), ], ), - )), - ), + ), + )), ), ); } diff --git a/ios/Classes/FilePickerPlugin.h b/ios/Classes/FilePickerPlugin.h index 145a3228..fecbd3f7 100644 --- a/ios/Classes/FilePickerPlugin.h +++ b/ios/Classes/FilePickerPlugin.h @@ -1,5 +1,7 @@ #import #import +#import +#import #import @interface FilePickerPlugin : NSObject diff --git a/ios/Classes/FilePickerPlugin.m b/ios/Classes/FilePickerPlugin.m index f9bc5d39..d2302601 100644 --- a/ios/Classes/FilePickerPlugin.m +++ b/ios/Classes/FilePickerPlugin.m @@ -1,11 +1,14 @@ #import "FilePickerPlugin.h" #import "FileUtils.h" +#import "ImageUtils.h" -@interface FilePickerPlugin() +@interface FilePickerPlugin() @property (nonatomic) FlutterResult result; @property (nonatomic) UIViewController *viewController; -@property (nonatomic) UIDocumentPickerViewController *pickerController; +@property (nonatomic) UIImagePickerController *galleryPickerController; +@property (nonatomic) UIDocumentPickerViewController *documentPickerController; @property (nonatomic) UIDocumentInteractionController *interactionController; +@property (nonatomic) MPMediaPickerController *audioPickerController; @property (nonatomic) NSString * fileType; @end @@ -32,81 +35,182 @@ - (instancetype)initWithViewController:(UIViewController *)viewController { return self; } -- (void)initPicker { - - self.pickerController = [[UIDocumentPickerViewController alloc] - initWithDocumentTypes:@[self.fileType] - inMode:UIDocumentPickerModeImport]; - - self.pickerController.modalPresentationStyle = UIModalPresentationCurrentContext; - self.pickerController.delegate = self; -} - - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if (_result) { - _result([FlutterError errorWithCode:@"multiple_request" + result([FlutterError errorWithCode:@"multiple_request" message:@"Cancelled by a second request" details:nil]); _result = nil; + return; } _result = result; + BOOL isMultiplePick = [call.arguments boolValue]; + if(isMultiplePick || [call.method isEqualToString:@"ANY"] || [call.method containsString:@"__CUSTOM"]) { + self.fileType = [FileUtils resolveType:call.method]; + if(self.fileType == nil) { + _result([FlutterError errorWithCode:@"Unsupported file extension" + message:@"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." + details:nil]); + _result = nil; + } else if(self.fileType != nil) { + [self resolvePickDocumentWithMultipleSelection:isMultiplePick]; + } + } else if([call.method isEqualToString:@"VIDEO"]) { + [self resolvePickVideo]; + } else if([call.method isEqualToString:@"AUDIO"]) { + [self resolvePickAudio]; + } else if([call.method isEqualToString:@"IMAGE"]) { + [self resolvePickImage]; + } else { + result(FlutterMethodNotImplemented); + _result = nil; + } +} + +#pragma mark - Resolvers + +- (void)resolvePickDocumentWithMultipleSelection:(BOOL)allowsMultipleSelection { - if([call.method isEqualToString:@"VIDEO"]) { - [self resolvePickVideo]; + @try{ + self.documentPickerController = [[UIDocumentPickerViewController alloc] + initWithDocumentTypes:@[self.fileType] + inMode:UIDocumentPickerModeImport]; + } @catch (NSException * e) { + Log(@"Couldn't launch documents file picker. Probably due to iOS version being below 11.0 and not having the iCloud entitlement. If so, just make sure to enable it for your app in Xcode. Exception was: %@", e); + _result = nil; + return; } - else { - self.fileType = [FileUtils resolveType:call.method]; - - if(self.fileType == nil){ - result(FlutterMethodNotImplemented); - } else { - [self initPicker]; - [_viewController presentViewController:self.pickerController animated:YES completion:^{ - if (@available(iOS 11.0, *)) { - self.pickerController.allowsMultipleSelection = NO; - } - }]; - - } + + if (@available(iOS 11.0, *)) { + self.documentPickerController.allowsMultipleSelection = allowsMultipleSelection; + } else if(allowsMultipleSelection) { + Log(@"Multiple file selection is only supported on iOS 11 and above. Single selection will be used."); } + self.documentPickerController.delegate = self; + self.documentPickerController.modalPresentationStyle = UIModalPresentationCurrentContext; + self.galleryPickerController.allowsEditing = NO; + + [_viewController presentViewController:self.documentPickerController animated:YES completion:nil]; } -- (void)documentPicker:(UIDocumentPickerViewController *)controller -didPickDocumentsAtURLs:(NSArray *)urls{ +- (void) resolvePickImage { + + self.galleryPickerController = [[UIImagePickerController alloc] init]; + self.galleryPickerController.delegate = self; + self.galleryPickerController.modalPresentationStyle = UIModalPresentationCurrentContext; + self.galleryPickerController.mediaTypes = @[(NSString *)kUTTypeImage]; + self.galleryPickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; - [self.pickerController dismissViewControllerAnimated:YES completion:nil]; - _result([FileUtils resolvePath:urls]); + [_viewController presentViewController:self.galleryPickerController animated:YES completion:nil]; } +- (void) resolvePickAudio { + + self.audioPickerController = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeAnyAudio]; + self.audioPickerController.delegate = self; + self.audioPickerController.showsCloudItems = NO; + self.audioPickerController.allowsPickingMultipleItems = NO; + self.audioPickerController.modalPresentationStyle = UIModalPresentationCurrentContext; + + [self.viewController presentViewController:self.audioPickerController animated:YES completion:nil]; +} -// VideoPicker delegate -- (void) resolvePickVideo{ +- (void) resolvePickVideo { - UIImagePickerController *videoPicker = [[UIImagePickerController alloc] init]; - videoPicker.delegate = self; - videoPicker.modalPresentationStyle = UIModalPresentationCurrentContext; - videoPicker.mediaTypes = @[(NSString*)kUTTypeMovie, (NSString*)kUTTypeAVIMovie, (NSString*)kUTTypeVideo, (NSString*)kUTTypeMPEG4]; - videoPicker.videoQuality = UIImagePickerControllerQualityTypeHigh; + self.galleryPickerController = [[UIImagePickerController alloc] init]; + self.galleryPickerController.delegate = self; + self.galleryPickerController.modalPresentationStyle = UIModalPresentationCurrentContext; + self.galleryPickerController.mediaTypes = @[(NSString*)kUTTypeMovie, (NSString*)kUTTypeAVIMovie, (NSString*)kUTTypeVideo, (NSString*)kUTTypeMPEG4]; + self.galleryPickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; - [self.viewController presentViewController:videoPicker animated:YES completion:nil]; + [self.viewController presentViewController:self.galleryPickerController animated:YES completion:nil]; } +#pragma mark - Delegates + +// DocumentPicker delegate +- (void)documentPicker:(UIDocumentPickerViewController *)controller +didPickDocumentsAtURLs:(NSArray *)urls{ + + [self.documentPickerController dismissViewControllerAnimated:YES completion:nil]; + NSArray * result = [FileUtils resolvePath:urls]; + + if([result count] > 1) { + _result(result); + } else { + _result([result objectAtIndex:0]); + } + _result = nil; + +} + + +// ImagePicker delegate - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { - NSURL *videoURL = [info objectForKey:UIImagePickerControllerMediaURL]; + NSURL *pickedVideoUrl = [info objectForKey:UIImagePickerControllerMediaURL]; + NSURL *pickedImageUrl; + + if (@available(iOS 11.0, *)) { + pickedImageUrl = [info objectForKey:UIImagePickerControllerImageURL]; + } else { + UIImage *pickedImage = [info objectForKey:UIImagePickerControllerEditedImage]; + + if(pickedImage == nil) { + pickedImage = [info objectForKey:UIImagePickerControllerOriginalImage]; + } + pickedImageUrl = [ImageUtils saveTmpImage:pickedImage]; + } + [picker dismissViewControllerAnimated:YES completion:NULL]; - _result([videoURL path]); + + if(pickedImageUrl == nil && pickedVideoUrl == nil) { + _result([FlutterError errorWithCode:@"file_picker_error" + message:@"Temporary file could not be created" + details:nil]); + _result = nil; + return; + } + + _result([pickedVideoUrl != nil ? pickedVideoUrl : pickedImageUrl path]); + _result = nil; +} + + +// AudioPicker delegate +- (void)mediaPicker: (MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection +{ + [mediaPicker dismissViewControllerAnimated:YES completion:NULL]; + NSURL *url = [[[mediaItemCollection items] objectAtIndex:0] valueForKey:MPMediaItemPropertyAssetURL]; + if(url == nil) { + Log(@"Couldn't retrieve the audio file path, either is not locally downloaded or the file is DRM protected."); + } + _result([url absoluteString]); + _result = nil; +} + +#pragma mark - Actions canceled + +- (void)mediaPickerDidCancel:(MPMediaPickerController *)controller { + Log(@"FilePicker canceled"); + _result(nil); + _result = nil; + [controller dismissViewControllerAnimated:YES completion:NULL]; } - (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller { + Log(@"FilePicker canceled"); + _result(nil); _result = nil; [controller dismissViewControllerAnimated:YES completion:NULL]; } - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { + Log(@"FilePicker canceled"); + _result(nil); _result = nil; [picker dismissViewControllerAnimated:YES completion:NULL]; } diff --git a/ios/Classes/FileUtils.h b/ios/Classes/FileUtils.h index a02ba06a..6c04ba60 100644 --- a/ios/Classes/FileUtils.h +++ b/ios/Classes/FileUtils.h @@ -5,9 +5,16 @@ // Created by Miguel Ruivo on 05/12/2018. // #import + +#ifdef DEBUG +#define Log(fmt, ...) NSLog((@"\n\n***** " fmt @"\n* %s [Line %d]\n\n\n"), ##__VA_ARGS__, __PRETTY_FUNCTION__, __LINE__) +#else +#define Log(fmt, ...) +#endif + @interface FileUtils : NSObject + (NSString*) resolveType:(NSString*)type; -+ (NSString*) resolvePath:(NSArray *)urls; ++ (NSArray*) resolvePath:(NSArray *)urls; @end diff --git a/ios/Classes/FileUtils.m b/ios/Classes/FileUtils.m index ae245246..5f941bae 100644 --- a/ios/Classes/FileUtils.m +++ b/ios/Classes/FileUtils.m @@ -19,29 +19,33 @@ + (NSString*) resolveType:(NSString*)type { CFStringRef UTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)[format pathExtension], NULL); NSString * UTIString = (__bridge NSString *)(UTI); CFRelease(UTI); - NSLog(@"Custom file type: %@", UTIString); + Log(@"Custom file type: %@", UTIString); return [UTIString containsString:@"dyn."] ? nil : UTIString; } - if ([type isEqualToString:@"PDF"]) { - return @"com.adobe.pdf"; - } - else if ([type isEqualToString:@"ANY"]) { + if ([type isEqualToString:@"ANY"]) { return @"public.item"; + } else if ([type isEqualToString:@"IMAGE"]) { + return @"public.image"; + } else if ([type isEqualToString:@"VIDEO"]) { + return @"public.movie"; + } else if ([type isEqualToString:@"AUDIO"]) { + return @"public.audio"; } else { return nil; } } - -+ (NSString*) resolvePath:(NSArray *)urls{ ++ (NSMutableArray*) resolvePath:(NSArray *)urls{ NSString * uri; + NSMutableArray * paths = [[NSMutableArray alloc] init]; for (NSURL *url in urls) { uri = (NSString *)[url path]; + [paths addObject:uri]; } - return uri; + return paths; } @end diff --git a/ios/Classes/ImageUtils.h b/ios/Classes/ImageUtils.h new file mode 100644 index 00000000..53aefad2 --- /dev/null +++ b/ios/Classes/ImageUtils.h @@ -0,0 +1,11 @@ +// +// ImageUtils.h +// Pods +// +// Created by Miguel Ruivo on 05/03/2019. +// + +@interface ImageUtils : NSObject ++ (BOOL)hasAlpha:(UIImage *)image; ++ (NSURL*)saveTmpImage:(UIImage *)image; +@end diff --git a/ios/Classes/ImageUtils.m b/ios/Classes/ImageUtils.m new file mode 100644 index 00000000..df5b6810 --- /dev/null +++ b/ios/Classes/ImageUtils.m @@ -0,0 +1,35 @@ +// +// ImageUtils.m +// file_picker +// +// Created by Miguel Ruivo on 05/03/2019. +// + +#import "ImageUtils.h" + +@implementation ImageUtils + +// Returns true if the image has an alpha layer ++ (BOOL)hasAlpha:(UIImage *)image { + CGImageAlphaInfo alpha = CGImageGetAlphaInfo(image.CGImage); + return (alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || + alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast); +} + +// Save the image temporarly in the app's tmp directory ++ (NSURL *)saveTmpImage:(UIImage *)image { + BOOL hasAlpha = [ImageUtils hasAlpha:image]; + NSData *data = hasAlpha ? UIImagePNGRepresentation(image) : UIImageJPEGRepresentation(image, 1.0); + NSString *fileExtension = hasAlpha ? @"tmp_%@.png" : @"tmp_%@.jpg"; + NSString *guid = [[NSProcessInfo processInfo] globallyUniqueString]; + NSString *tmpFile = [NSString stringWithFormat:fileExtension, guid]; + NSString *tmpDirectory = NSTemporaryDirectory(); + NSString *tmpPath = [tmpDirectory stringByAppendingPathComponent:tmpFile]; + + if ([[NSFileManager defaultManager] createFileAtPath:tmpPath contents:data attributes:nil]) { + return [NSURL URLWithString: tmpPath]; + } + return nil; +} + +@end diff --git a/lib/file_picker.dart b/lib/file_picker.dart index e1f60f28..a81fa5bb 100644 --- a/lib/file_picker.dart +++ b/lib/file_picker.dart @@ -1,14 +1,13 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/services.dart'; -import 'package:image_picker/image_picker.dart'; -/// Supported file types, [ANY] should be used if the file you need isn't listed enum FileType { ANY, IMAGE, VIDEO, - CAMERA, + AUDIO, CUSTOM, } @@ -16,47 +15,66 @@ class FilePicker { static const MethodChannel _channel = const MethodChannel('file_picker'); static const String _tag = 'FilePicker'; - static Future _getPath(String type) async { - try { - return await _channel.invokeMethod(type); - } on PlatformException catch (e) { - print("[$_tag] Platform exception: " + e.toString()); - } catch (e) { - print( - "[$_tag] Unsupported operation. This probably have happened because [${type.split('_').last}] is an unsupported file type. You may want to try FileType.ALL instead."); - } - return null; + FilePicker._(); + + /// Returns an iterable `Map` where the `key` is the name of the file + /// and the `value` the path. + /// + /// 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 => + await _getPath(_handleType(type, fileExtension), true); + + /// Returns an absolute file path from the calling platform. + /// + /// 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 => + 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); + return File(filePath); } - static Future _getImage(ImageSource type) async { + static Future _getPath(String type, bool multipleSelection) async { try { - var image = await ImagePicker.pickImage(source: type); - return image?.path; + dynamic result = await _channel.invokeMethod(type, multipleSelection); + if (result != null && multipleSelection) { + if (result is String) { + result = [result]; + } + return Map.fromIterable(result, key: (path) => path.split('/').last, value: (path) => path); + } + return result; } on PlatformException catch (e) { - print("[$_tag] Platform exception: " + e.toString()); + print('[$_tag] Platform exception: ' + e.toString()); + } catch (e) { + print('[$_tag] Unsupported operation. Method not found. The exception thrown was: ' + e.toString()); } return null; } - /// Returns an absolute file path from the calling platform - /// - /// 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 String _handleType(FileType type, String fileExtension) { switch (type) { case FileType.IMAGE: - return _getImage(ImageSource.gallery); - case FileType.CAMERA: - return _getImage(ImageSource.camera); + return 'IMAGE'; + case FileType.AUDIO: + return 'AUDIO'; case FileType.VIDEO: - return _getPath('VIDEO'); + return 'VIDEO'; case FileType.ANY: - return _getPath('ANY'); + return 'ANY'; case FileType.CUSTOM: - return _getPath('__CUSTOM_' + (fileExtension ?? '')); + return '__CUSTOM_' + (fileExtension ?? ''); default: - return _getPath('ANY'); + return 'ANY'; } } } diff --git a/pubspec.yaml b/pubspec.yaml index 694a6336..b5448416 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,16 +1,14 @@ name: file_picker -description: A plugin that allows you to pick absolute paths from diferent file types. -version: 1.2.0 +description: A package that allows you to use a native file explorer to pick single or multiple absolute file paths, with extensions filtering support. author: Miguel Ruivo homepage: https://github.com/miguelpruivo/plugins_flutter_file_picker +version: 1.3.0 dependencies: flutter: sdk: flutter - image_picker: ^0.5.0 - meta: ^1.1.5 - + environment: sdk: ">=2.0.0 <3.0.0"