Adds Android implementation

This commit is contained in:
Miguel Ruivo 2020-09-10 00:52:45 +01:00
parent 1916a9acce
commit c4d80c5d7c
6 changed files with 175 additions and 84 deletions

View File

@ -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<String, Object> toMap() {
final HashMap<String, Object> 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;
}
}

View File

@ -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<FileInfo> files = new ArrayList<>();
if (data.getClipData() != null) {
final int count = data.getClipData().getItemCount();
int currentItem = 0;
final ArrayList<String> 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<FileInfo> 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<HashMap<String, Object>> data = new ArrayList<>();
for(FileInfo file : files) {
data.add(file.toMap());
}
this.pendingResult.success(data);
this.clearPendingResult();
}

View File

@ -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();
}
}

View File

@ -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);
});
}

View File

@ -64,7 +64,7 @@ class FilePickerIO extends FilePicker {
);
}
final List<String> result = await _channel.invokeListMethod(type, {
final List<Map> 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;

View File

@ -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;