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

View File

@ -111,11 +111,20 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener
Log.d(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) { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && type.equals("dir")) {
final FileInfo file = type.equals("dir") ? FileUtils.getFullPathFromTreeUri(uri, activity) : FileUtils.openFileStream(FilePickerDelegate.this.activity, uri); final String dirPath = FileUtils.getFullPathFromTreeUri(uri, activity);
if(file != null) { if(dirPath != null) {
files.add(file); 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()) { if (!files.isEmpty()) {
@ -233,7 +242,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener
this.startFileExplorer(); this.startFileExplorer();
} }
private void finishWithSuccess(final ArrayList<FileInfo> files) { private void finishWithSuccess(Object data) {
if (eventSink != null) { if (eventSink != null) {
this.dispatchEventStatus(false); 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 // Temporary fix, remove this null-check after Flutter Engine 1.14 has landed on stable
if (this.pendingResult != null) { 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) { for (FileInfo file : (ArrayList<FileInfo>)data) {
data.add(file.toMap()); files.add(file.toMap());
}
data = files;
} }
this.pendingResult.success(data); this.pendingResult.success(data);

View File

@ -17,8 +17,11 @@ import android.webkit.MimeTypeMap;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -32,106 +35,6 @@ public class FileUtils {
private static final String TAG = "FilePickerUtils"; private static final String TAG = "FilePickerUtils";
private static final String PRIMARY_VOLUME_NAME = "primary"; 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) { public static String[] getMimeTypes(final ArrayList<String> allowedExtensions) {
if (allowedExtensions == null || allowedExtensions.isEmpty()) { if (allowedExtensions == null || allowedExtensions.isEmpty()) {
@ -153,29 +56,6 @@ public class FileUtils {
return mimes.toArray(new String[0]); 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) { public static String getFileName(Uri uri, final Context context) {
String result = null; 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 String path = context.getCacheDir().getAbsolutePath() + "/file_picker/" + (fileName != null ? fileName : new Random().nextInt(100000));
final File file = new File(path); final File file = new File(path);
file.getParentFile().mkdirs();
try { if(file.exists()) {
fos = new FileOutputStream(path); int size = (int) file.length();
byte[] bytes = new byte[size];
try { try {
final ByteArrayOutputStream out = new ByteArrayOutputStream(); BufferedInputStream buf = new BufferedInputStream(new FileInputStream(file));
final InputStream in = context.getContentResolver().openInputStream(uri); buf.read(bytes, 0, bytes.length);
buf.close();
final byte[] buffer = new byte[8192]; } catch (FileNotFoundException e) {
int len = 0; Log.e(TAG, "File not found: " + e.getMessage(), null);
} catch (IOException e) {
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); 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; return null;
} }
Log.e(TAG, "Failed to retrieve path: " + e.getMessage(), null);
return null;
} }
Log.d(TAG, "File loaded and cached at:" + path); Log.d(TAG, "File loaded and cached at:" + path);
fileInfo fileInfo
.lastModifiedAt(file.lastModified())
.withPath(path) .withPath(path)
.withName(fileName) .withName(fileName)
.withSize(Integer.parseInt(String.valueOf(file.length()/1024))) .withSize(Integer.parseInt(String.valueOf(file.length()/1024)))
@ -289,7 +187,7 @@ public class FileUtils {
} }
@Nullable @Nullable
public static FileInfo getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) { public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) {
if (treeUri == null) { if (treeUri == null) {
return null; return null;
} }
@ -300,7 +198,7 @@ public class FileUtils {
fileInfo.withUri(treeUri); fileInfo.withUri(treeUri);
if (volumePath == null) { if (volumePath == null) {
return fileInfo.withDirectory(File.separator).build(); return File.separator;
} }
if (volumePath.endsWith(File.separator)) if (volumePath.endsWith(File.separator))
@ -313,13 +211,13 @@ public class FileUtils {
if (documentPath.length() > 0) { if (documentPath.length() > 0) {
if (documentPath.startsWith(File.separator)) { if (documentPath.startsWith(File.separator)) {
return fileInfo.withDirectory(volumePath + documentPath).build(); return volumePath + documentPath;
} }
else { else {
return fileInfo.withDirectory(volumePath + File.separator + documentPath).build(); return volumePath + File.separator + documentPath;
} }
} else { } else {
return fileInfo.withDirectory(volumePath).build(); return volumePath;
} }
} }
@ -375,24 +273,4 @@ public class FileUtils {
else return File.separator; 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>(); final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
String _fileName; String _fileName;
List<PlatformFile> _paths; List<PlatformFile> _paths;
String _directoryPath;
String _extension; String _extension;
bool _loadingPath = false; bool _loadingPath = false;
bool _multiPick = false; bool _multiPick = false;
@ -27,7 +28,8 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
void _openFileExplorer() async { void _openFileExplorer() async {
setState(() => _loadingPath = true); setState(() => _loadingPath = true);
try { try {
_paths = (await FilePicker.instance.pickFiles( _directoryPath = null;
_paths = (await FilePicker.platform.pickFiles(
type: _pickingType, type: _pickingType,
allowMultiple: _multiPick, allowMultiple: _multiPick,
allowedExtensions: (_extension?.isNotEmpty ?? false) ? _extension?.replaceAll(' ', '')?.split(',') : null, allowedExtensions: (_extension?.isNotEmpty ?? false) ? _extension?.replaceAll(' ', '')?.split(',') : null,
@ -46,7 +48,7 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
} }
void _clearCachedFiles() { void _clearCachedFiles() {
FilePicker.instance.clearTemporaryFiles().then((result) { FilePicker.platform.clearTemporaryFiles().then((result) {
_scaffoldKey.currentState.showSnackBar( _scaffoldKey.currentState.showSnackBar(
SnackBar( SnackBar(
backgroundColor: result ? Colors.green : Colors.red, backgroundColor: result ? Colors.green : Colors.red,
@ -57,8 +59,8 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
} }
void _selectFolder() { void _selectFolder() {
FilePicker.instance.getDirectoryPath().then((value) { FilePicker.platform.getDirectoryPath().then((value) {
setState(() => _paths = [value]); setState(() => _directoryPath = value);
}); });
} }
@ -161,30 +163,35 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
padding: const EdgeInsets.only(bottom: 10.0), padding: const EdgeInsets.only(bottom: 10.0),
child: const CircularProgressIndicator(), child: const CircularProgressIndicator(),
) )
: _paths != null : _directoryPath != null
? Container( ? ListTile(
padding: const EdgeInsets.only(bottom: 30.0), title: Text('Directory path'),
height: MediaQuery.of(context).size.height * 0.50, subtitle: Text(_directoryPath),
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(), : _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 _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); PlatformInterface.verifyToken(instance, _token);
_instance = instance; _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. /// 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. /// 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'); Future<bool> clearTemporaryFiles() async => _channel.invokeMethod<bool>('clear');
@override @override
Future<PlatformFile> getDirectoryPath() async { Future<String> getDirectoryPath() async {
try { try {
String result = await _channel.invokeMethod('dir', {}); return await _channel.invokeMethod('dir', {});
if (result != null) {
return PlatformFile(path: result, isDirectory: true);
}
} on PlatformException catch (ex) { } on PlatformException catch (ex) {
if (ex.code == "unknown_path") { if (ex.code == "unknown_path") {
print( print(

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:html' as html; import 'dart:html' as html;
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart';
@ -13,7 +14,7 @@ class FilePickerWeb extends FilePicker {
static final FilePickerWeb platform = FilePickerWeb._(); static final FilePickerWeb platform = FilePickerWeb._();
static void registerWith(Registrar registrar) { static void registerWith(Registrar registrar) {
FilePicker.instance = platform; FilePicker.platform = platform;
} }
@override @override
@ -40,10 +41,14 @@ class FilePickerWeb extends FilePicker {
List<PlatformFile> pickedFiles = []; List<PlatformFile> pickedFiles = [];
reader.onLoadEnd.listen((e) { reader.onLoadEnd.listen((e) {
final Uint8List bytes = Base64Decoder().convert(reader.result.toString().split(",").last);
pickedFiles.add( pickedFiles.add(
PlatformFile( PlatformFile(
name: uploadInput.value.replaceAll('\\', '/'), 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'; import 'dart:typed_data';
class PlatformFile { class PlatformFile {
PlatformFile({ const PlatformFile({
this.path, this.path,
this.uri, this.uri,
this.name, this.name,
@ -28,7 +28,9 @@ class PlatformFile {
/// manipulate the original file (read, write, delete). /// manipulate the original file (read, write, delete).
/// ///
/// Android: it can be either content:// or file:// url. /// Android: it can be either content:// or file:// url.
///
/// iOS: a file:// URL below a document provider (like iCloud). /// iOS: a file:// URL below a document provider (like iCloud).
///
/// Web: Not supported, will be always `null`. /// Web: Not supported, will be always `null`.
final String uri; final String uri;
@ -46,5 +48,5 @@ class PlatformFile {
final bool isDirectory; final bool isDirectory;
/// File extension for this file. /// File extension for this file.
String get extension => name.split('/').last; String get extension => name?.split('/')?.last;
} }