Adds lastModified property and prevents caching if file already exists (Android)

This commit is contained in:
Miguel Ruivo 2020-09-11 14:53:18 +01:00
parent c4d80c5d7c
commit ea601246fd
8 changed files with 135 additions and 227 deletions

View File

@ -2,9 +2,7 @@ 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 {
@ -13,14 +11,16 @@ public class FileInfo {
final String name;
final int size;
final byte[] bytes;
final long lastModified;
final boolean isDirectory;
public FileInfo(Uri uri, String path, String name, int size, byte[] bytes, boolean isDirectory) {
public FileInfo(Uri uri, String path, String name, int size, byte[] bytes, boolean isDirectory, long lastModified) {
this.uri = uri;
this.path = path;
this.name = name;
this.size = size;
this.bytes = bytes;
this.lastModified = lastModified;
this.isDirectory = isDirectory;
}
@ -30,6 +30,7 @@ public class FileInfo {
private String path;
private String name;
private int size;
private long lastModified;
private byte[] bytes;
private boolean isDirectory;
@ -58,14 +59,19 @@ public class FileInfo {
return this;
}
public Builder withDirectory(String directory){
this.path = directory;
this.isDirectory = directory != null;
public Builder withDirectory(String path){
this.path = path;
this.isDirectory = path != null;
return this;
}
public Builder lastModifiedAt(long timeStamp){
this.lastModified = timeStamp;
return this;
}
public FileInfo build() {
return new FileInfo(this.uri, this.path, this.name, this.size, this.bytes, this.isDirectory);
return new FileInfo(this.uri, this.path, this.name, this.size, this.bytes, this.isDirectory, this.lastModified);
}
}
@ -78,6 +84,7 @@ public class FileInfo {
data.put("size", size);
data.put("bytes", bytes);
data.put("isDirectory", isDirectory);
data.put("lastModified", lastModified);
return data;
}
}

View File

@ -111,11 +111,20 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener
Log.d(FilePickerDelegate.TAG, "[SingleFilePick] File URI:" + uri.toString());
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
final FileInfo file = type.equals("dir") ? FileUtils.getFullPathFromTreeUri(uri, activity) : FileUtils.openFileStream(FilePickerDelegate.this.activity, uri);
if(file != null) {
files.add(file);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && type.equals("dir")) {
final String dirPath = FileUtils.getFullPathFromTreeUri(uri, activity);
if(dirPath != null) {
finishWithSuccess(dirPath);
} else {
finishWithError("unknown_path", "Failed to retrieve directory path.");
}
return;
}
final FileInfo file = FileUtils.openFileStream(FilePickerDelegate.this.activity, uri);
if(file != null) {
files.add(file);
}
if (!files.isEmpty()) {
@ -233,7 +242,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener
this.startFileExplorer();
}
private void finishWithSuccess(final ArrayList<FileInfo> files) {
private void finishWithSuccess(Object data) {
if (eventSink != null) {
this.dispatchEventStatus(false);
}
@ -241,10 +250,13 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener
// 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<>();
if(data != null && !(data instanceof String)) {
final ArrayList<HashMap<String, Object>> files = new ArrayList<>();
for(FileInfo file : files) {
data.add(file.toMap());
for (FileInfo file : (ArrayList<FileInfo>)data) {
files.add(file.toMap());
}
data = files;
}
this.pendingResult.success(data);

View File

@ -17,8 +17,11 @@ import android.webkit.MimeTypeMap;
import androidx.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@ -32,106 +35,6 @@ 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;
if (isKitKat) {
return getForApi19(context, uri);
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
if (isGooglePhotosUri(uri)) {
return uri.getLastPathSegment();
}
return getDataColumn(context, uri, null, null);
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
@TargetApi(19)
@SuppressWarnings("deprecation")
private static String getForApi19(final Context context, final Uri uri) {
Log.e(TAG, "Getting for API 19 or above" + uri);
if (DocumentsContract.isDocumentUri(context, uri)) {
Log.e(TAG, "Document URI");
if (isExternalStorageDocument(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");
return Environment.getExternalStorageDirectory() + (split.length > 1 ? ("/" + split[1]) : "");
}
} else if (isDownloadsDocument(uri)) {
Log.e(TAG, "Downloads External Document URI");
String id = DocumentsContract.getDocumentId(uri);
if (!TextUtils.isEmpty(id)) {
if (id.startsWith("raw:")) {
return id.replaceFirst("raw:", "");
}
final String[] contentUriPrefixesToTry = new String[]{
"content://downloads/public_downloads",
"content://downloads/my_downloads",
"content://downloads/all_downloads"
};
if (id.contains(":")) {
id = id.split(":")[1];
}
for (final String contentUriPrefix : contentUriPrefixesToTry) {
try {
final Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.valueOf(id));
final String path = getDataColumn(context, contentUri, null, null);
if (path != null) {
return path;
}
} catch (final Exception e) {
Log.e(TAG, "Something went wrong while retrieving document path: " + e.toString());
return null;
}
}
}
} else if (isMediaDocument(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.i(TAG, "Image Media Document URI");
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
Log.i(TAG, "Video Media Document URI");
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
Log.i(TAG, "Audio Media Document URI");
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = MediaStore.Images.Media._ID + "=?";
final String[] selectionArgs = new String[]{
split[1]
};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
} else if ("content".equalsIgnoreCase(uri.getScheme())) {
Log.e(TAG, "NO DOCUMENT URI - CONTENT: " + uri.getPath());
if (isGooglePhotosUri(uri)) {
return uri.getLastPathSegment();
} else if (isDropBoxUri(uri)) {
return null;
}
return getDataColumn(context, uri, null, null);
} else if ("file".equalsIgnoreCase(uri.getScheme())) {
Log.e(TAG, "No DOCUMENT URI - FILE: " + uri.getPath());
return uri.getPath();
}
return null;
}
public static String[] getMimeTypes(final ArrayList<String> allowedExtensions) {
if (allowedExtensions == null || allowedExtensions.isEmpty()) {
@ -153,29 +56,6 @@ public class FileUtils {
return mimes.toArray(new String[0]);
}
private static String getDataColumn(final Context context, final Uri uri, final String selection,
final String[] selectionArgs) {
Cursor cursor = null;
final String column = MediaStore.Images.Media.DATA;
final String[] projection = {
column
};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
null);
if (cursor != null && cursor.moveToFirst()) {
final int index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(index);
}
} catch (final Exception ex) {
} finally {
if (cursor != null) {
cursor.close();
}
}
return null;
}
public static String getFileName(Uri uri, final Context context) {
String result = null;
@ -245,41 +125,59 @@ public class FileUtils {
final String path = context.getCacheDir().getAbsolutePath() + "/file_picker/" + (fileName != null ? fileName : new Random().nextInt(100000));
final File file = new File(path);
file.getParentFile().mkdirs();
try {
fos = new FileOutputStream(path);
if(file.exists()) {
int size = (int) file.length();
byte[] bytes = new byte[size];
try {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final InputStream in = context.getContentResolver().openInputStream(uri);
final byte[] buffer = new byte[8192];
int len = 0;
while ((len = in.read(buffer)) >= 0) {
out.write(buffer, 0, len);
}
fileInfo.withData(out.toByteArray());
out.writeTo(fos);
out.flush();
} finally {
fos.getFD().sync();
}
} catch (final Exception e) {
try {
fos.close();
} catch (final IOException | NullPointerException ex) {
BufferedInputStream buf = new BufferedInputStream(new FileInputStream(file));
buf.read(bytes, 0, bytes.length);
buf.close();
} catch (FileNotFoundException e) {
Log.e(TAG, "File not found: " + e.getMessage(), null);
} catch (IOException e) {
Log.e(TAG, "Failed to close file streams: " + e.getMessage(), null);
}
fileInfo.withData(bytes);
} else {
file.getParentFile().mkdirs();
try {
fos = new FileOutputStream(path);
try {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final InputStream in = context.getContentResolver().openInputStream(uri);
final byte[] buffer = new byte[8192];
int len = 0;
while ((len = in.read(buffer)) >= 0) {
out.write(buffer, 0, len);
}
fileInfo.withData(out.toByteArray());
out.writeTo(fos);
out.flush();
} finally {
fos.getFD().sync();
}
} catch (final Exception e) {
try {
fos.close();
} catch (final IOException | NullPointerException 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.e(TAG, "Failed to retrieve path: " + e.getMessage(), null);
return null;
}
Log.d(TAG, "File loaded and cached at:" + path);
fileInfo
.lastModifiedAt(file.lastModified())
.withPath(path)
.withName(fileName)
.withSize(Integer.parseInt(String.valueOf(file.length()/1024)))
@ -289,7 +187,7 @@ public class FileUtils {
}
@Nullable
public static FileInfo getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) {
public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) {
if (treeUri == null) {
return null;
}
@ -300,7 +198,7 @@ public class FileUtils {
fileInfo.withUri(treeUri);
if (volumePath == null) {
return fileInfo.withDirectory(File.separator).build();
return File.separator;
}
if (volumePath.endsWith(File.separator))
@ -313,13 +211,13 @@ public class FileUtils {
if (documentPath.length() > 0) {
if (documentPath.startsWith(File.separator)) {
return fileInfo.withDirectory(volumePath + documentPath).build();
return volumePath + documentPath;
}
else {
return fileInfo.withDirectory(volumePath + File.separator + documentPath).build();
return volumePath + File.separator + documentPath;
}
} else {
return fileInfo.withDirectory(volumePath).build();
return volumePath;
}
}
@ -375,24 +273,4 @@ public class FileUtils {
else return File.separator;
}
private static boolean isDropBoxUri(final Uri uri) {
return "com.dropbox.android.FileCache".equals(uri.getAuthority());
}
private static boolean isExternalStorageDocument(final Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
private static boolean isDownloadsDocument(final Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
private static boolean isMediaDocument(final Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
private static boolean isGooglePhotosUri(final Uri uri) {
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
}
}

View File

@ -12,6 +12,7 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
String _fileName;
List<PlatformFile> _paths;
String _directoryPath;
String _extension;
bool _loadingPath = false;
bool _multiPick = false;
@ -27,7 +28,8 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
void _openFileExplorer() async {
setState(() => _loadingPath = true);
try {
_paths = (await FilePicker.instance.pickFiles(
_directoryPath = null;
_paths = (await FilePicker.platform.pickFiles(
type: _pickingType,
allowMultiple: _multiPick,
allowedExtensions: (_extension?.isNotEmpty ?? false) ? _extension?.replaceAll(' ', '')?.split(',') : null,
@ -46,7 +48,7 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
}
void _clearCachedFiles() {
FilePicker.instance.clearTemporaryFiles().then((result) {
FilePicker.platform.clearTemporaryFiles().then((result) {
_scaffoldKey.currentState.showSnackBar(
SnackBar(
backgroundColor: result ? Colors.green : Colors.red,
@ -57,8 +59,8 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
}
void _selectFolder() {
FilePicker.instance.getDirectoryPath().then((value) {
setState(() => _paths = [value]);
FilePicker.platform.getDirectoryPath().then((value) {
setState(() => _directoryPath = value);
});
}
@ -161,30 +163,35 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
padding: const EdgeInsets.only(bottom: 10.0),
child: const CircularProgressIndicator(),
)
: _paths != null
? Container(
padding: const EdgeInsets.only(bottom: 30.0),
height: MediaQuery.of(context).size.height * 0.50,
child: Scrollbar(
child: 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.map((e) => e.name).toList()[index] : _fileName ?? '...');
final path = _paths.map((e) => e.path).toList()[index].toString();
return ListTile(
title: Text(
name,
),
subtitle: Text(path),
);
},
separatorBuilder: (BuildContext context, int index) => const Divider(),
)),
: _directoryPath != null
? ListTile(
title: Text('Directory path'),
subtitle: Text(_directoryPath),
)
: const SizedBox(),
: _paths != null
? Container(
padding: const EdgeInsets.only(bottom: 30.0),
height: MediaQuery.of(context).size.height * 0.50,
child: Scrollbar(
child: 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.map((e) => e.name).toList()[index] : _fileName ?? '...');
final path = _paths.map((e) => e.path).toList()[index].toString();
return ListTile(
title: Text(
name,
),
subtitle: Text(path),
);
},
separatorBuilder: (BuildContext context, int index) => const Divider(),
)),
)
: const SizedBox(),
),
],
),

View File

@ -34,9 +34,9 @@ abstract class FilePicker extends PlatformInterface {
static FilePicker _instance = FilePickerIO();
static FilePicker get instance => _instance;
static FilePicker get platform => _instance;
static set instance(FilePicker instance) {
static set platform(FilePicker instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
@ -71,5 +71,5 @@ abstract class FilePicker extends PlatformInterface {
///
/// 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.
Future<PlatformFile> getDirectoryPath() async => throw UnimplementedError('getDirectoryPath() has not been implemented.');
Future<String> getDirectoryPath() async => throw UnimplementedError('getDirectoryPath() has not been implemented.');
}

View File

@ -29,12 +29,9 @@ class FilePickerIO extends FilePicker {
Future<bool> clearTemporaryFiles() async => _channel.invokeMethod<bool>('clear');
@override
Future<PlatformFile> getDirectoryPath() async {
Future<String> getDirectoryPath() async {
try {
String result = await _channel.invokeMethod('dir', {});
if (result != null) {
return PlatformFile(path: result, isDirectory: true);
}
return await _channel.invokeMethod('dir', {});
} on PlatformException catch (ex) {
if (ex.code == "unknown_path") {
print(

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:html' as html;
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
@ -13,7 +14,7 @@ class FilePickerWeb extends FilePicker {
static final FilePickerWeb platform = FilePickerWeb._();
static void registerWith(Registrar registrar) {
FilePicker.instance = platform;
FilePicker.platform = platform;
}
@override
@ -40,10 +41,14 @@ class FilePickerWeb extends FilePicker {
List<PlatformFile> pickedFiles = [];
reader.onLoadEnd.listen((e) {
final Uint8List bytes = Base64Decoder().convert(reader.result.toString().split(",").last);
pickedFiles.add(
PlatformFile(
name: uploadInput.value.replaceAll('\\', '/'),
bytes: Base64Decoder().convert(reader.result.toString().split(",").last),
path: uploadInput.value,
size: bytes.length ~/ 1024,
bytes: bytes,
),
);

View File

@ -1,7 +1,7 @@
import 'dart:typed_data';
class PlatformFile {
PlatformFile({
const PlatformFile({
this.path,
this.uri,
this.name,
@ -28,7 +28,9 @@ class PlatformFile {
/// manipulate the original file (read, write, delete).
///
/// 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;
@ -46,5 +48,5 @@ class PlatformFile {
final bool isDirectory;
/// File extension for this file.
String get extension => name.split('/').last;
String get extension => name?.split('/')?.last;
}