From ea601246fd80e7205dbcbe061cbe52527c02046e Mon Sep 17 00:00:00 2001 From: Miguel Ruivo Date: Fri, 11 Sep 2020 14:53:18 +0100 Subject: [PATCH] Adds lastModified property and prevents caching if file already exists (Android) --- .../flutter/plugin/filepicker/FileInfo.java | 21 +- .../plugin/filepicker/FilePickerDelegate.java | 28 ++- .../flutter/plugin/filepicker/FileUtils.java | 224 ++++-------------- .../example/lib/src/file_picker_demo.dart | 61 ++--- file_picker/lib/src/file_picker.dart | 6 +- file_picker/lib/src/file_picker_io.dart | 7 +- file_picker/lib/src/file_picker_web.dart | 9 +- file_picker/lib/src/platform_file.dart | 6 +- 8 files changed, 135 insertions(+), 227 deletions(-) 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 index 830e35d..13ca187 100644 --- 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 @@ -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; } } 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 a097467..fd8705f 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 @@ -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 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> data = new ArrayList<>(); + if(data != null && !(data instanceof String)) { + final ArrayList> files = new ArrayList<>(); - for(FileInfo file : files) { - data.add(file.toMap()); + for (FileInfo file : (ArrayList)data) { + files.add(file.toMap()); + } + data = files; } this.pendingResult.success(data); 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 aa84526..17643ea 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,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 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()); - } - } \ No newline at end of file diff --git a/file_picker/example/lib/src/file_picker_demo.dart b/file_picker/example/lib/src/file_picker_demo.dart index 99f61ee..2b114e4 100644 --- a/file_picker/example/lib/src/file_picker_demo.dart +++ b/file_picker/example/lib/src/file_picker_demo.dart @@ -12,6 +12,7 @@ class _FilePickerDemoState extends State { final GlobalKey _scaffoldKey = GlobalKey(); String _fileName; List _paths; + String _directoryPath; String _extension; bool _loadingPath = false; bool _multiPick = false; @@ -27,7 +28,8 @@ class _FilePickerDemoState extends State { 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 { } 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 { } 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 { 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(), ), ], ), diff --git a/file_picker/lib/src/file_picker.dart b/file_picker/lib/src/file_picker.dart index 4876dec..1621ed9 100644 --- a/file_picker/lib/src/file_picker.dart +++ b/file_picker/lib/src/file_picker.dart @@ -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 getDirectoryPath() async => throw UnimplementedError('getDirectoryPath() has not been implemented.'); + Future getDirectoryPath() async => throw UnimplementedError('getDirectoryPath() has not been implemented.'); } diff --git a/file_picker/lib/src/file_picker_io.dart b/file_picker/lib/src/file_picker_io.dart index 0084e23..c33e1eb 100644 --- a/file_picker/lib/src/file_picker_io.dart +++ b/file_picker/lib/src/file_picker_io.dart @@ -29,12 +29,9 @@ class FilePickerIO extends FilePicker { Future clearTemporaryFiles() async => _channel.invokeMethod('clear'); @override - Future getDirectoryPath() async { + Future 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( diff --git a/file_picker/lib/src/file_picker_web.dart b/file_picker/lib/src/file_picker_web.dart index c4a336e..013f0df 100644 --- a/file_picker/lib/src/file_picker_web.dart +++ b/file_picker/lib/src/file_picker_web.dart @@ -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 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, ), ); diff --git a/file_picker/lib/src/platform_file.dart b/file_picker/lib/src/platform_file.dart index 4b25e19..fa82f81 100644 --- a/file_picker/lib/src/platform_file.dart +++ b/file_picker/lib/src/platform_file.dart @@ -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; }