From c4d80c5d7ceefd6efa14c4d8bcecfdd2a4ee9d01 Mon Sep 17 00:00:00 2001 From: Miguel Ruivo Date: Thu, 10 Sep 2020 00:52:45 +0100 Subject: [PATCH] Adds Android implementation --- .../flutter/plugin/filepicker/FileInfo.java | 83 +++++++++++++++++++ .../plugin/filepicker/FilePickerDelegate.java | 52 ++++++------ .../flutter/plugin/filepicker/FileUtils.java | 60 ++++++++++---- file_picker/example/test/widget_test.dart | 30 ------- file_picker/lib/src/file_picker_io.dart | 4 +- file_picker/lib/src/platform_file.dart | 30 ++++--- 6 files changed, 175 insertions(+), 84 deletions(-) create mode 100644 file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FileInfo.java delete mode 100644 file_picker/example/test/widget_test.dart diff --git a/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FileInfo.java b/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FileInfo.java new file mode 100644 index 0000000..830e35d --- /dev/null +++ b/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FileInfo.java @@ -0,0 +1,83 @@ +package com.mr.flutter.plugin.filepicker; + +import android.net.Uri; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class FileInfo { + + final Uri uri; + final String path; + final String name; + final int size; + final byte[] bytes; + final boolean isDirectory; + + public FileInfo(Uri uri, String path, String name, int size, byte[] bytes, boolean isDirectory) { + this.uri = uri; + this.path = path; + this.name = name; + this.size = size; + this.bytes = bytes; + this.isDirectory = isDirectory; + } + + public static class Builder { + + private Uri uri; + private String path; + private String name; + private int size; + private byte[] bytes; + private boolean isDirectory; + + public Builder withUri(Uri uri){ + this.uri = uri; + return this; + } + + public Builder withPath(String path){ + this.path = path; + return this; + } + + public Builder withName(String name){ + this.name = name; + return this; + } + + public Builder withSize(int size){ + this.size = size; + return this; + } + + public Builder withData(byte[] bytes){ + this.bytes = bytes; + return this; + } + + public Builder withDirectory(String directory){ + this.path = directory; + this.isDirectory = directory != null; + return this; + } + + public FileInfo build() { + return new FileInfo(this.uri, this.path, this.name, this.size, this.bytes, this.isDirectory); + } + } + + + public HashMap toMap() { + final HashMap data = new HashMap<>(); + data.put("uri", uri.toString()); + data.put("path", path); + data.put("name", name); + data.put("size", size); + data.put("bytes", bytes); + data.put("isDirectory", isDirectory); + return data; + } +} diff --git a/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java b/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java index 27b70fa..a097467 100644 --- a/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java +++ b/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java @@ -17,8 +17,11 @@ import androidx.annotation.VisibleForTesting; import androidx.core.app.ActivityCompat; import java.io.File; +import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodChannel; @@ -82,49 +85,42 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener @Override public void run() { if (data != null) { + final ArrayList files = new ArrayList<>(); + if (data.getClipData() != null) { final int count = data.getClipData().getItemCount(); int currentItem = 0; - final ArrayList paths = new ArrayList<>(); while (currentItem < count) { final Uri currentUri = data.getClipData().getItemAt(currentItem).getUri(); - String path; - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - path = FileUtils.getUriFromRemote(FilePickerDelegate.this.activity, currentUri); - } else { - path = FileUtils.getPath(currentUri, FilePickerDelegate.this.activity); - if (path == null) { - path = FileUtils.getUriFromRemote(FilePickerDelegate.this.activity, currentUri); - } + final FileInfo file = FileUtils.openFileStream(FilePickerDelegate.this.activity, currentUri); + + if(file != null) { + files.add(file); + Log.d(FilePickerDelegate.TAG, "[MultiFilePick] File #" + currentItem + " - URI: " + currentUri.getPath()); } - paths.add(path); - Log.i(FilePickerDelegate.TAG, "[MultiFilePick] File #" + currentItem + " - URI: " + currentUri.getPath()); currentItem++; } - finishWithSuccess(paths); - + finishWithSuccess(files); } else if (data.getData() != null) { 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()); + Log.d(FilePickerDelegate.TAG, "[SingleFilePick] File URI:" + uri.toString()); if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - fullPath = type.equals("dir") ? FileUtils.getFullPathFromTreeUri(uri, activity) : FileUtils.getUriFromRemote(FilePickerDelegate.this.activity, uri); - } else { - fullPath = FileUtils.getPath(uri, FilePickerDelegate.this.activity); - if (fullPath == null) { - fullPath = type.equals("dir") ? FileUtils.getFullPathFromTreeUri(uri, activity) : FileUtils.getUriFromRemote(FilePickerDelegate.this.activity, uri); + final FileInfo file = type.equals("dir") ? FileUtils.getFullPathFromTreeUri(uri, activity) : FileUtils.openFileStream(FilePickerDelegate.this.activity, uri); + if(file != null) { + files.add(file); } } - if (fullPath != null) { - Log.i(FilePickerDelegate.TAG, "Absolute file path:" + fullPath); - finishWithSuccess(Arrays.asList(fullPath)); + if (!files.isEmpty()) { + Log.d(FilePickerDelegate.TAG, "File path:" + files.toString()); + finishWithSuccess(files); } else { finishWithError("unknown_path", "Failed to retrieve path."); } @@ -137,7 +133,6 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener } } }).start(); - return true; } else if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_CANCELED) { @@ -238,13 +233,20 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener this.startFileExplorer(); } - private void finishWithSuccess(final Object data) { + private void finishWithSuccess(final ArrayList files) { if (eventSink != null) { this.dispatchEventStatus(false); } // Temporary fix, remove this null-check after Flutter Engine 1.14 has landed on stable if (this.pendingResult != null) { + + final ArrayList> data = new ArrayList<>(); + + for(FileInfo file : files) { + data.add(file.toMap()); + } + this.pendingResult.success(data); this.clearPendingResult(); } diff --git a/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java b/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java index 7ca9d8c..aa84526 100644 --- a/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java +++ b/file_picker/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java @@ -17,7 +17,7 @@ import android.webkit.MimeTypeMap; import androidx.annotation.Nullable; -import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -236,19 +236,21 @@ public class FileUtils { return true; } - public static String getUriFromRemote(final Context context, final Uri uri) { + public static FileInfo openFileStream(final Context context, final Uri uri) { - Log.i(TAG, "Caching file from remote/external URI"); + Log.i(TAG, "Caching from URI: " + uri.toString()); FileOutputStream fos = null; + final FileInfo.Builder fileInfo = new FileInfo.Builder(); final String fileName = FileUtils.getFileName(uri, context); - final String externalFile = context.getCacheDir().getAbsolutePath() + "/file_picker/" + (fileName != null ? fileName : new Random().nextInt(100000)); + final String path = context.getCacheDir().getAbsolutePath() + "/file_picker/" + (fileName != null ? fileName : new Random().nextInt(100000)); - new File(externalFile).getParentFile().mkdirs(); + final File file = new File(path); + file.getParentFile().mkdirs(); try { - fos = new FileOutputStream(externalFile); + fos = new FileOutputStream(path); try { - final BufferedOutputStream out = new BufferedOutputStream(fos); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); final InputStream in = context.getContentResolver().openInputStream(uri); final byte[] buffer = new byte[8192]; @@ -258,6 +260,8 @@ public class FileUtils { out.write(buffer, 0, len); } + fileInfo.withData(out.toByteArray()); + out.writeTo(fos); out.flush(); } finally { fos.getFD().sync(); @@ -273,28 +277,50 @@ public class FileUtils { return null; } - Log.i(TAG, "File loaded and cached at:" + externalFile); - return externalFile; + Log.d(TAG, "File loaded and cached at:" + path); + + fileInfo + .withPath(path) + .withName(fileName) + .withSize(Integer.parseInt(String.valueOf(file.length()/1024))) + .withUri(uri); + + return fileInfo.build(); } @Nullable - public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) { - if (treeUri == null) return null; + public static FileInfo getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) { + if (treeUri == null) { + return null; + } + String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri), con); - if (volumePath == null) return File.separator; + FileInfo.Builder fileInfo = new FileInfo.Builder(); + + fileInfo.withUri(treeUri); + + if (volumePath == null) { + return fileInfo.withDirectory(File.separator).build(); + } + 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; + if (documentPath.startsWith(File.separator)) { + return fileInfo.withDirectory(volumePath + documentPath).build(); + } + else { + return fileInfo.withDirectory(volumePath + File.separator + documentPath).build(); + } + } else { + return fileInfo.withDirectory(volumePath).build(); + } } diff --git a/file_picker/example/test/widget_test.dart b/file_picker/example/test/widget_test.dart deleted file mode 100644 index 747db1d..0000000 --- a/file_picker/example/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:example/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/file_picker/lib/src/file_picker_io.dart b/file_picker/lib/src/file_picker_io.dart index 64493ab..0084e23 100644 --- a/file_picker/lib/src/file_picker_io.dart +++ b/file_picker/lib/src/file_picker_io.dart @@ -64,7 +64,7 @@ class FilePickerIO extends FilePicker { ); } - final List result = await _channel.invokeListMethod(type, { + final List result = await _channel.invokeListMethod(type, { 'allowMultipleSelection': allowMultipleSelection, 'allowedExtensions': allowedExtensions, 'allowCompression': allowCompression, @@ -74,7 +74,7 @@ class FilePickerIO extends FilePicker { return null; } - return FilePickerResult(result.map((file) => PlatformFile(name: file.split('/').last, path: file)).toList()); + return FilePickerResult(result.map((file) => PlatformFile.fromMap(file)).toList()); } on PlatformException catch (e) { print('[$_tag] Platform exception: $e'); rethrow; diff --git a/file_picker/lib/src/platform_file.dart b/file_picker/lib/src/platform_file.dart index cbefcb6..4b25e19 100644 --- a/file_picker/lib/src/platform_file.dart +++ b/file_picker/lib/src/platform_file.dart @@ -6,32 +6,42 @@ class PlatformFile { this.uri, this.name, this.bytes, + this.size, this.isDirectory = false, }); - /// The absolute path for this file instance. - /// - /// Typically whis will reflect a copy cached file and not the original source, - /// also, it's not guaranteed that this path is always available as some files - /// can be protected by OS. - /// - /// Available on IO only. On Web is always `null`. + PlatformFile.fromMap(Map data) + : this.path = data['path'], + this.uri = data['uri'], + this.name = data['name'], + this.bytes = data['bytes'], + this.size = data['size'], + this.isDirectory = data['isDirectory']; + + /// The absolute path for a cached copy of this file. + /// If you want to access the original file identifier use [uri] property instead. final String path; /// The URI (Universal Resource Identifier) for this file. /// - /// This is the original file resource identifier and can be used to + /// This is the identifier of original resource and can be used to /// manipulate the original file (read, write, delete). /// - /// Available on IO only. On Web is always `null`. + /// Android: it can be either content:// or file:// url. + /// iOS: a file:// URL below a document provider (like iCloud). + /// Web: Not supported, will be always `null`. final String uri; /// File name including its extension. final String name; - /// Byte data for this file. + /// Byte data for this file. Particurlarly useful if you want to manipulate its data + /// or easily upload to somewhere else. final Uint8List bytes; + /// The file size in KB. + final int size; + /// Whether this file references a directory or not. final bool isDirectory;