From b1f8cd60649ce8d34f50d2ad47e7333f0a782156 Mon Sep 17 00:00:00 2001 From: Miguel Ruivo Date: Thu, 4 Jun 2020 22:49:39 +0100 Subject: [PATCH] Allow picking directory path Adds support for picking directory paths on both iOS & Android through getDirectoryPath() method. --- CHANGELOG.md | 3 + README.md | 1 + .../plugin/filepicker/FilePickerDelegate.java | 42 ++++++---- .../plugin/filepicker/FilePickerPlugin.java | 14 ++-- .../flutter/plugin/filepicker/FileUtils.java | 81 ++++++++++++++++++- example/ios/Podfile.lock | 2 +- example/ios/Runner.xcodeproj/project.pbxproj | 4 - example/lib/src/file_picker_demo.dart | 61 ++++++-------- .../xcshareddata/IDEWorkspaceChecks.plist | 8 ++ ios/Classes/FilePickerPlugin.m | 14 ++-- lib/file_picker.dart | 55 ++++++------- pubspec.yaml | 2 +- 12 files changed, 190 insertions(+), 97 deletions(-) create mode 100644 file_picker/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/CHANGELOG.md b/CHANGELOG.md index 956af05..e02e0d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 1.10.0 +**Android & iOS:** Adds `getDirectoryPath()` method that allows you to select and pick directory paths. Android, requires SDK 21 or above for this to work, and iOS requires iOS 11 or above. + ## 1.9.0+1 Adds a temporary workaround on Android where it can trigger `onRequestPermissionsResult` twice, related to Flutter issue [49365](https://github.com/flutter/flutter/issues/49365) for anyone affected in Flutter versions below 1.14.6. diff --git a/README.md b/README.md index ebd7c4b..b405a9e 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ A package that allows you to use a native file explorer to pick single or multip * Load path from **audio** only * Load path from **image** only * Load path from **video** only +* Load path from **directory** * Load path from **any** * Create a `File` or `List` objects from **any** selected file(s) * Supports desktop through **go-flutter** (MacOS, Windows, Linux) 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 index 16ed4eb..dcaaffc 100644 --- a/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java +++ b/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java @@ -5,7 +5,9 @@ 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 android.provider.DocumentsContract; import android.util.Log; import androidx.annotation.VisibleForTesting; @@ -85,12 +87,17 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener finishWithSuccess(paths.get(0)); } } else if (data.getData() != null) { - final Uri uri = data.getData(); + Uri uri = data.getData(); + String fullPath; + if (type.equals("dir") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + uri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)); + } + Log.i(FilePickerDelegate.TAG, "[SingleFilePick] File URI:" + uri.toString()); - String fullPath = FileUtils.getPath(uri, FilePickerDelegate.this.activity); + fullPath = FileUtils.getPath(uri, FilePickerDelegate.this.activity); if (fullPath == null) { - fullPath = FileUtils.getUriFromRemote(FilePickerDelegate.this.activity, uri); + fullPath = type.equals("dir") ? FileUtils.getFullPathFromTreeUri(uri, activity) : FileUtils.getUriFromRemote(FilePickerDelegate.this.activity, uri); } if (fullPath != null) { @@ -99,6 +106,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener } else { finishWithError("unknown_path", "Failed to retrieve path."); } + } else { finishWithError("unknown_activity", "Unknown activity error, please fill an issue."); } @@ -160,20 +168,24 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener return; } - intent = new Intent(Intent.ACTION_GET_CONTENT); - final Uri uri = Uri.parse(Environment.getExternalStorageDirectory().getPath() + File.separator); - Log.d(TAG, "Selected type " + type); - intent.setDataAndType(uri, this.type); - intent.setType(this.type); - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, this.isMultipleSelection); - intent.addCategory(Intent.CATEGORY_OPENABLE); + if (type.equals("dir")) { + intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + } else { + intent = new Intent(Intent.ACTION_GET_CONTENT); + final Uri uri = Uri.parse(Environment.getExternalStorageDirectory().getPath() + File.separator); + Log.d(TAG, "Selected type " + type); + intent.setDataAndType(uri, this.type); + intent.setType(this.type); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, this.isMultipleSelection); + intent.addCategory(Intent.CATEGORY_OPENABLE); - if (type.contains(",")) { - allowedExtensions = type.split(","); - } + if (type.contains(",")) { + allowedExtensions = type.split(","); + } - if (allowedExtensions != null) { - intent.putExtra(Intent.EXTRA_MIME_TYPES, allowedExtensions); + if (allowedExtensions != null) { + intent.putExtra(Intent.EXTRA_MIME_TYPES, allowedExtensions); + } } if (intent.resolveActivity(this.activity.getPackageManager()) != null) { 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 8ad6d8e..cfad235 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 @@ -152,13 +152,16 @@ public class FilePickerPlugin implements MethodChannel.MethodCallHandler, Flutte } fileType = FilePickerPlugin.resolveType(call.method); - isMultipleSelection = (boolean) arguments.get("allowMultipleSelection"); - - final String[] allowedExtensions = FileUtils.getMimeTypes((ArrayList) arguments.get("allowedExtensions")); + String[] allowedExtensions = null; if (fileType == null) { result.notImplemented(); - } else if (fileType == "custom" && (allowedExtensions == null || allowedExtensions.length == 0)) { + } else if (fileType != "dir") { + isMultipleSelection = (boolean) arguments.get("allowMultipleSelection"); + allowedExtensions = FileUtils.getMimeTypes((ArrayList) arguments.get("allowedExtensions")); + } + + if (fileType == "custom" && (allowedExtensions == null || allowedExtensions.length == 0)) { 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, allowedExtensions, result); @@ -168,7 +171,6 @@ public class FilePickerPlugin implements MethodChannel.MethodCallHandler, Flutte private static String resolveType(final String type) { - switch (type) { case "audio": return "audio/*"; @@ -181,6 +183,8 @@ public class FilePickerPlugin implements MethodChannel.MethodCallHandler, Flutte case "any": case "custom": return "*/*"; + case "dir": + return "dir"; default: return null; } 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 5449f19..a932429 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 @@ -1,5 +1,6 @@ package com.mr.flutter.plugin.filepicker; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.ContentUris; import android.content.Context; @@ -7,23 +8,29 @@ import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Environment; +import android.os.storage.StorageManager; import android.provider.DocumentsContract; import android.provider.MediaStore; import android.text.TextUtils; import android.util.Log; import android.webkit.MimeTypeMap; +import androidx.annotation.Nullable; + import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Array; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Random; public class FileUtils { private static final String TAG = "FilePickerUtils"; + private static final String PRIMARY_VOLUME_NAME = "primary"; public static String getPath(final Uri uri, final Context context) { final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; @@ -53,7 +60,7 @@ 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 Environment.getExternalStorageDirectory() + (split.length > 1 ? ("/" + split[1]) : ""); } } else if (isDownloadsDocument(uri)) { Log.e(TAG, "Downloads External Document URI"); @@ -270,6 +277,78 @@ public class FileUtils { return externalFile; } + @Nullable + public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) { + if (treeUri == null) return null; + String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri), con); + if (volumePath == null) return File.separator; + if (volumePath.endsWith(File.separator)) + volumePath = volumePath.substring(0, volumePath.length() - 1); + + String documentPath = getDocumentPathFromTreeUri(treeUri); + if (documentPath.endsWith(File.separator)) + documentPath = documentPath.substring(0, documentPath.length() - 1); + + if (documentPath.length() > 0) { + if (documentPath.startsWith(File.separator)) + return volumePath + documentPath; + else + return volumePath + File.separator + documentPath; + } else return volumePath; + } + + + @SuppressLint("ObsoleteSdkInt") + private static String getVolumePath(final String volumeId, Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null; + try { + StorageManager mStorageManager = + (StorageManager) context.getSystemService(Context.STORAGE_SERVICE); + Class storageVolumeClazz = Class.forName("android.os.storage.StorageVolume"); + Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList"); + Method getUuid = storageVolumeClazz.getMethod("getUuid"); + Method getPath = storageVolumeClazz.getMethod("getPath"); + Method isPrimary = storageVolumeClazz.getMethod("isPrimary"); + Object result = getVolumeList.invoke(mStorageManager); + + final int length = Array.getLength(result); + for (int i = 0; i < length; i++) { + Object storageVolumeElement = Array.get(result, i); + String uuid = (String) getUuid.invoke(storageVolumeElement); + Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement); + + // primary volume? + if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) + return (String) getPath.invoke(storageVolumeElement); + + // other volumes? + if (uuid != null && uuid.equals(volumeId)) + return (String) getPath.invoke(storageVolumeElement); + } + // not found. + return null; + } catch (Exception ex) { + return null; + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static String getVolumeIdFromTreeUri(final Uri treeUri) { + final String docId = DocumentsContract.getTreeDocumentId(treeUri); + final String[] split = docId.split(":"); + if (split.length > 0) return split[0]; + else return null; + } + + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static String getDocumentPathFromTreeUri(final Uri treeUri) { + final String docId = DocumentsContract.getTreeDocumentId(treeUri); + final String[] split = docId.split(":"); + if ((split.length >= 2) && (split[1] != null)) return split[1]; + else return File.separator; + } + private static boolean isDropBoxUri(final Uri uri) { return "com.dropbox.android.FileCache".equals(uri.getAuthority()); } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 29a3527..6ccd59a 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -71,7 +71,7 @@ SPEC CHECKSUMS: file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31 Flutter: 0e3d915762c693b495b44d77113d4970485de6ec - flutter_plugin_android_lifecycle: 47de533a02850f070f5696a623995e93eddcdb9b + flutter_plugin_android_lifecycle: dc0b544e129eebb77a6bfb1239d4d1c673a60a35 SDWebImage: 97351f6582ceca541ea294ba66a1fcb342a331c2 SDWebImageFLPlugin: 6c2295fb1242d44467c6c87dc5db6b0a13228fd8 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index e7ce601..97a44e0 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -35,7 +35,6 @@ 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 = ""; }; 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 = ""; }; 4FE42FA345519DC2CF8F7CAB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 620D3249E16C66CF31F39A1D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -43,7 +42,6 @@ 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -67,9 +65,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, diff --git a/example/lib/src/file_picker_demo.dart b/example/lib/src/file_picker_demo.dart index 94397a3..ad3d13d 100644 --- a/example/lib/src/file_picker_demo.dart +++ b/example/lib/src/file_picker_demo.dart @@ -31,17 +31,11 @@ class _FilePickerDemoState extends State { if (_multiPick) { _path = null; _paths = await FilePicker.getMultiFilePath( - type: _pickingType, - allowedExtensions: (_extension?.isNotEmpty ?? false) - ? _extension?.replaceAll(' ', '')?.split(',') - : null); + type: _pickingType, allowedExtensions: (_extension?.isNotEmpty ?? false) ? _extension?.replaceAll(' ', '')?.split(',') : null); } else { _paths = null; _path = await FilePicker.getFilePath( - type: _pickingType, - allowedExtensions: (_extension?.isNotEmpty ?? false) - ? _extension?.replaceAll(' ', '')?.split(',') - : null); + type: _pickingType, allowedExtensions: (_extension?.isNotEmpty ?? false) ? _extension?.replaceAll(' ', '')?.split(',') : null); } } on PlatformException catch (e) { print("Unsupported operation" + e.toString()); @@ -49,9 +43,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() : '...'; }); } @@ -60,14 +52,18 @@ class _FilePickerDemoState extends State { _scaffoldKey.currentState.showSnackBar( SnackBar( backgroundColor: result ? Colors.green : Colors.red, - content: Text((result - ? 'Temporary files removed with success.' - : 'Failed to clean temporary files')), + content: Text((result ? 'Temporary files removed with success.' : 'Failed to clean temporary files')), ), ); }); } + void _selectFolder() { + FilePicker.getDirectoryPath().then((value) { + setState(() => _path = value); + }); + } + @override Widget build(BuildContext context) { return new MaterialApp( @@ -128,8 +124,7 @@ class _FilePickerDemoState extends State { maxLength: 15, autovalidate: true, controller: _controller, - decoration: - InputDecoration(labelText: 'File extension'), + decoration: InputDecoration(labelText: 'File extension'), keyboardType: TextInputType.text, textCapitalization: TextCapitalization.none, ) @@ -138,10 +133,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, ), ), @@ -153,6 +146,10 @@ class _FilePickerDemoState extends State { onPressed: () => _openFileExplorer(), child: new Text("Open file picker"), ), + new RaisedButton( + onPressed: () => _selectFolder(), + child: new Text("Pick folder"), + ), new RaisedButton( onPressed: () => _clearCachedFiles(), child: new Text("Clear temporary files"), @@ -162,28 +159,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( @@ -192,9 +179,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/file_picker/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/file_picker/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/file_picker/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Classes/FilePickerPlugin.m b/ios/Classes/FilePickerPlugin.m index 7cf927b..c87c305 100644 --- a/ios/Classes/FilePickerPlugin.m +++ b/ios/Classes/FilePickerPlugin.m @@ -54,6 +54,11 @@ return; } + if([call.method isEqualToString:@"dir"]) { + [self resolvePickDocumentWithMultiPick:NO pickDirectory:YES]; + return; + } + NSDictionary * arguments = call.arguments; BOOL isMultiplePick = ((NSNumber*)[arguments valueForKey:@"allowMultipleSelection"]).boolValue; if([call.method isEqualToString:@"any"] || [call.method containsString:@"custom"]) { @@ -64,7 +69,7 @@ details:nil]); _result = nil; } else if(self.allowedExtensions != nil) { - [self resolvePickDocumentWithMultipleSelection:isMultiplePick]; + [self resolvePickDocumentWithMultiPick:isMultiplePick pickDirectory:NO]; } } else if([call.method isEqualToString:@"video"] || [call.method isEqualToString:@"image"] || [call.method isEqualToString:@"media"]) { [self resolvePickMedia:[FileUtils resolveMediaType:call.method] withMultiPick:isMultiplePick]; @@ -78,13 +83,12 @@ } #pragma mark - Resolvers - -- (void)resolvePickDocumentWithMultipleSelection:(BOOL)allowsMultipleSelection { +- (void)resolvePickDocumentWithMultiPick:(BOOL)allowsMultipleSelection pickDirectory:(BOOL)isDirectory { @try{ self.documentPickerController = [[UIDocumentPickerViewController alloc] - initWithDocumentTypes: self.allowedExtensions - inMode:UIDocumentPickerModeImport]; + initWithDocumentTypes: isDirectory ? @[@"public.folder"] : self.allowedExtensions + inMode: isDirectory ? UIDocumentPickerModeOpen : 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; diff --git a/lib/file_picker.dart b/lib/file_picker.dart index 7e509a0..0ba1df2 100644 --- a/lib/file_picker.dart +++ b/lib/file_picker.dart @@ -15,8 +15,7 @@ enum FileType { class FilePicker { FilePicker._(); - static const MethodChannel _channel = - const MethodChannel('miguelruivo.flutter.plugins.filepicker'); + static const MethodChannel _channel = const MethodChannel('miguelruivo.flutter.plugins.filepicker'); static const String _tag = 'FilePicker'; /// Returns an iterable `Map` where the `key` is the name of the file @@ -25,9 +24,7 @@ class FilePicker { /// A `List` with [allowedExtensions] can be provided to filter the allowed files to picked. /// If provided, make sure you select `FileType.custom` as type. /// Defaults to `FileType.any`, which allows any combination of files to be multi selected at once. - static Future> getMultiFilePath( - {FileType type = FileType.any, - List allowedExtensions}) async => + static Future> getMultiFilePath({FileType type = FileType.any, List allowedExtensions}) async => await _getPath(describeEnum(type), true, allowedExtensions); /// Returns an absolute file path from the calling platform. @@ -35,19 +32,15 @@ class FilePicker { /// Extension filters are allowed with `FileType.custom`, when used, make sure to provide a `List` /// of [allowedExtensions] (e.g. [`pdf`, `svg`, `jpg`].). /// Defaults to `FileType.any` which will display all file types. - static Future getFilePath( - {FileType type = FileType.any, - List allowedExtensions}) async => + static Future getFilePath({FileType type = FileType.any, List allowedExtensions}) async => await _getPath(describeEnum(type), false, allowedExtensions); /// 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, List allowedExtensions}) async { - final String filePath = - await _getPath(describeEnum(type), false, allowedExtensions); + static Future getFile({FileType type = FileType.any, List allowedExtensions}) async { + final String filePath = await _getPath(describeEnum(type), false, allowedExtensions); return filePath != null ? File(filePath) : null; } @@ -66,20 +59,30 @@ 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, List allowedExtensions}) async { - final Map paths = - await _getPath(describeEnum(type), true, allowedExtensions); - return paths != null && paths.isNotEmpty - ? paths.values.map((path) => File(path)).toList() - : null; + static Future> getMultiFile({FileType type = FileType.any, List allowedExtensions}) async { + final Map paths = await _getPath(describeEnum(type), true, allowedExtensions); + return paths != null && paths.isNotEmpty ? paths.values.map((path) => File(path)).toList() : null; } - static Future _getPath(String type, bool allowMultipleSelection, - List allowedExtensions) async { + /// Selects a directory and returns its absolute path. + /// + /// On Android, this requires to be running on SDK 21 or above, else won't work. + /// Returns `null` if folder path couldn't be resolved. + static Future getDirectoryPath() async { + try { + return await _channel.invokeMethod('dir'); + } on PlatformException catch (ex) { + if (ex.code == "unknown_path") { + print( + '[$_tag] Could not resolve directory path. Maybe it\'s a protected one or unsupported (such as Downloads folder). If you are on Android, make sure that you are on SDK 21 or above.'); + } + return null; + } + } + + static Future _getPath(String type, bool allowMultipleSelection, List allowedExtensions) async { if (type != 'custom' && (allowedExtensions?.isNotEmpty ?? false)) { - throw Exception( - 'If you are using a custom extension filter, please use the FileType.custom instead.'); + throw Exception('If you are using a custom extension filter, please use the FileType.custom instead.'); } try { dynamic result = await _channel.invokeMethod(type, { @@ -90,16 +93,14 @@ 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; } } diff --git a/pubspec.yaml b/pubspec.yaml index 2019022..0c5b600 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ 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.9.0+1 +version: 1.10.0 dependencies: flutter: