diff --git a/CHANGELOG.md b/CHANGELOG.md index ddf8753b..94d64a83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 6.2.0 +### Android +Add ability to compress images on android by specifying a compression quality value ([#735] +(https://github.com/miguelpruivo/flutter_file_picker/issues/735)). + + ## 6.1.1 ### Android Android's CSV mime type is `text/comma-separated-values`. Added standard `text/csv` when the diff --git a/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java b/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java index 7b938a64..ed43c72f 100644 --- a/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java +++ b/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerDelegate.java @@ -19,6 +19,8 @@ import androidx.core.app.ActivityCompat; import java.io.File; +import java.io.FileNotFoundException; +import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; @@ -37,6 +39,7 @@ public class FilePickerDelegate implements PluginRegistry.ActivityResultListener private boolean isMultipleSelection = false; private boolean loadDataToMemory = false; private String type; + private int compressionQuality=20; private String[] allowedExtensions; private EventChannel.EventSink eventSink; @@ -89,13 +92,18 @@ public void run() { if (data != null) { final ArrayList files = new ArrayList<>(); + if (data.getClipData() != null) { final int count = data.getClipData().getItemCount(); int currentItem = 0; while (currentItem < count) { - final Uri currentUri = data.getClipData().getItemAt(currentItem).getUri(); - final FileInfo file = FileUtils.openFileStream(FilePickerDelegate.this.activity, currentUri, loadDataToMemory); + Uri currentUri = data.getClipData().getItemAt(currentItem).getUri(); + + if(type=="image/*" && compressionQuality>0) { + currentUri=FileUtils.compressImage(currentUri,compressionQuality,activity.getApplicationContext()); + } + final FileInfo file = FileUtils.openFileStream(FilePickerDelegate.this.activity, currentUri, loadDataToMemory); if(file != null) { files.add(file); Log.d(FilePickerDelegate.TAG, "[MultiFilePick] File #" + currentItem + " - URI: " + currentUri.getPath()); @@ -107,6 +115,10 @@ public void run() { } else if (data.getData() != null) { Uri uri = data.getData(); + if(type=="image/*" && compressionQuality>0) { + uri=FileUtils.compressImage(uri,compressionQuality,activity.getApplicationContext()); + } + if (type.equals("dir") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { uri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri)); @@ -261,17 +273,17 @@ private void startFileExplorer() { } @SuppressWarnings("deprecation") - public void startFileExplorer(final String type, final boolean isMultipleSelection, final boolean withData, final String[] allowedExtensions, final MethodChannel.Result result) { + public void startFileExplorer(final String type, final boolean isMultipleSelection, final boolean withData, final String[] allowedExtensions, final int compressionQuality, final MethodChannel.Result result) { if (!this.setPendingMethodCallAndResult(result)) { finishWithAlreadyActiveError(result); return; } - this.type = type; this.isMultipleSelection = isMultipleSelection; this.loadDataToMemory = withData; this.allowedExtensions = allowedExtensions; + this.compressionQuality=compressionQuality; // `READ_EXTERNAL_STORAGE` permission is not needed since SDK 33 (Android 13 or higher). // `READ_EXTERNAL_STORAGE` & `WRITE_EXTERNAL_STORAGE` are no longer meant to be used, but classified into granular types. // Reference: https://developer.android.com/about/versions/13/behavior-changes-13 @@ -286,13 +298,11 @@ public void startFileExplorer(final String type, final boolean isMultipleSelecti @SuppressWarnings("unchecked") private void finishWithSuccess(Object data) { - this.dispatchEventStatus(false); // Temporary fix, remove this null-check after Flutter Engine 1.14 has landed on stable if (this.pendingResult != null) { - - if(data != null && !(data instanceof String)) { + if (data != null && !(data instanceof String)) { final ArrayList> files = new ArrayList<>(); for (FileInfo file : (ArrayList)data) { diff --git a/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java b/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java index 585b2459..235e1146 100644 --- a/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java +++ b/android/src/main/java/com/mr/flutter/plugin/filepicker/FilePickerPlugin.java @@ -5,6 +5,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.util.Log; import androidx.annotation.NonNull; import androidx.lifecycle.DefaultLifecycleObserver; @@ -113,6 +114,7 @@ public void onActivityStopped(final Activity activity) { private static String fileType; private static boolean isMultipleSelection = false; private static boolean withData = false; + private static int compressionQuality; /** * Plugin registration. @@ -162,13 +164,14 @@ public void onMethodCall(final MethodCall call, final MethodChannel.Result rawRe } else if (fileType != "dir") { isMultipleSelection = (boolean) arguments.get("allowMultipleSelection"); withData = (boolean) arguments.get("withData"); + compressionQuality=(int) arguments.get("compressionQuality"); allowedExtensions = FileUtils.getMimeTypes((ArrayList) arguments.get("allowedExtensions")); } if (call.method != null && call.method.equals("custom") && (allowedExtensions == null || allowedExtensions.length == 0)) { result.error(TAG, "Unsupported filter. Make sure that you are only using the extension without the dot, (ie., jpg instead of .jpg). This could also have happened because you are using an unsupported file extension. If the problem persists, you may want to consider using FileType.all instead.", null); } else { - this.delegate.startFileExplorer(fileType, isMultipleSelection, withData, allowedExtensions, result); + this.delegate.startFileExplorer(fileType, isMultipleSelection, withData, allowedExtensions, compressionQuality,result); } } diff --git a/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java b/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java index 76266dde..46ae7e26 100644 --- a/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java +++ b/android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java @@ -2,13 +2,17 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.content.ContentUris; import android.content.Context; import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.os.storage.StorageManager; import android.provider.DocumentsContract; +import android.provider.MediaStore; import android.provider.OpenableColumns; import android.util.Log; import android.webkit.MimeTypeMap; @@ -26,7 +30,10 @@ import java.io.InputStream; import java.lang.reflect.Array; import java.lang.reflect.Method; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; import java.util.Random; public class FileUtils { @@ -34,14 +41,6 @@ public class FileUtils { private static final String TAG = "FilePickerUtils"; private static final String PRIMARY_VOLUME_NAME = "primary"; - // On Android, the CSV mime type from getMimeTypeFromExtension() returns - // "text/comma-separated-values" which is non-standard and doesn't filter - // CSV files in Google Drive. - // (see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) - // (see https://android.googlesource.com/platform/frameworks/base/+/61ae88e/core/java/android/webkit/MimeTypeMap.java#439) - private static final String CSV_EXTENSION = "csv"; - private static final String CSV_MIME_TYPE = "text/csv"; - public static String[] getMimeTypes(final ArrayList allowedExtensions) { if (allowedExtensions == null || allowedExtensions.isEmpty()) { @@ -51,18 +50,13 @@ public static String[] getMimeTypes(final ArrayList allowedExtensions) { final ArrayList mimes = new ArrayList<>(); for (int i = 0; i < allowedExtensions.size(); i++) { - final String extension = allowedExtensions.get(i); - final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(allowedExtensions.get(i)); if (mime == null) { Log.w(TAG, "Custom file type " + allowedExtensions.get(i) + " is unsupported and will be ignored."); continue; } mimes.add(mime); - if(extension.equals(CSV_EXTENSION)) { - // Add the standard CSV mime type. - mimes.add(CSV_MIME_TYPE); - } } Log.d(TAG, "Allowed file extensions mimes: " + mimes); return mimes.toArray(new String[0]); @@ -97,6 +91,168 @@ public static String getFileName(Uri uri, final Context context) { return result; } + + public static Uri compressImage(Uri originalImageUri, int compressionQuality,Context context) { + String originalImagePath = getRealPathFromURI(context,originalImageUri); + Uri compressedUri=null; + File compressedFile=null; + try { + compressedFile=createImageFile(); + Bitmap originalBitmap = BitmapFactory.decodeFile(originalImagePath); + String file_path = Environment.getExternalStorageDirectory().getAbsolutePath() + + "/FilePicker"; + // Compress and save the image + FileOutputStream fos = new FileOutputStream(compressedFile); + originalBitmap.compress(Bitmap.CompressFormat.JPEG, compressionQuality, fos); + fos.flush(); + fos.close(); + compressedUri=Uri.fromFile(compressedFile); + }catch (FileNotFoundException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + return compressedUri; + } + private static File createImageFile() throws IOException { + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); + String imageFileName = "JPEG_" + timeStamp + "_"; + File storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); + return File.createTempFile(imageFileName, ".jpg", storageDir); + } + + public static String getRealPathFromURI(final Context context, final Uri uri) { + + final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type)) { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } + + // TODO handle non-primary volumes + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + + final String id = DocumentsContract.getDocumentId(uri); + final Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + + return getDataColumn(context, contentUri, null, null); + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[] { + split[1] + }; + + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + + // Return the remote address + if (isGooglePhotosUri(uri)) + return uri.getLastPathSegment(); + + return getDataColumn(context, uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } + public static String getDataColumn(Context context, Uri uri, String selection, + String[] selectionArgs) { + + Cursor cursor = null; + final String column = "_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); + } + } finally { + if (cursor != null) + cursor.close(); + } + return null; + } + + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + public static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + public static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + public static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + public static boolean isGooglePhotosUri(Uri uri) { + return "com.google.android.apps.photos.content".equals(uri.getAuthority()); + } + + + // Create a HashMap for a FileInfo object representing the compressed image + public static HashMap createFileInfoMap(File compressedImageFile) { + HashMap fileInfoMap = new HashMap<>(); + fileInfoMap.put("filePath", compressedImageFile.getAbsolutePath()); + fileInfoMap.put("fileName", compressedImageFile.getName()); + // Add other file information as needed + return fileInfoMap; + } + public static boolean clearCache(final Context context) { try { final File cacheDir = new File(context.getCacheDir() + "/file_picker/"); @@ -299,10 +455,6 @@ private static String getVolumePath(final String volumeId, Context context) { } } - private static boolean isDownloadsDocument(Uri uri) { - return "com.android.providers.downloads.documents".equals(uri.getAuthority()); - } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static String getVolumeIdFromTreeUri(final Uri treeUri) { final String docId = DocumentsContract.getTreeDocumentId(treeUri); @@ -320,4 +472,4 @@ private static String getDocumentPathFromTreeUri(final Uri treeUri) { else return File.separator; } -} +} \ No newline at end of file diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index fd9aa0e1..e7e79559 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -23,7 +23,7 @@ android { defaultConfig { applicationId "com.mr.flutter.plugin.filepicker.example" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 33 versionCode 1 versionName "1.0" diff --git a/example/lib/src/file_picker_demo.dart b/example/lib/src/file_picker_demo.dart index 996d9f94..b34b1a78 100644 --- a/example/lib/src/file_picker_demo.dart +++ b/example/lib/src/file_picker_demo.dart @@ -38,6 +38,7 @@ class _FilePickerDemoState extends State { try { _directoryPath = null; _paths = (await FilePicker.platform.pickFiles( + compressionQuality: 30, type: _pickingType, allowMultiple: _multiPick, onFileLoading: (FilePickerStatus status) => print(status), diff --git a/lib/_internal/file_picker_web.dart b/lib/_internal/file_picker_web.dart index a53e2c9b..3e415eca 100644 --- a/lib/_internal/file_picker_web.dart +++ b/lib/_internal/file_picker_web.dart @@ -47,6 +47,7 @@ class FilePickerWeb extends FilePicker { bool withReadStream = false, bool lockParentWindow = false, bool readSequential = false, + int compressionQuality = 20, }) async { if (type != FileType.custom && (allowedExtensions?.isNotEmpty ?? false)) { throw Exception( diff --git a/lib/src/file_picker.dart b/lib/src/file_picker.dart index e3d32dca..bd8faacf 100644 --- a/lib/src/file_picker.dart +++ b/lib/src/file_picker.dart @@ -107,6 +107,7 @@ abstract class FilePicker extends PlatformInterface { List? allowedExtensions, Function(FilePickerStatus)? onFileLoading, bool allowCompression = true, + int compressionQuality = 30, bool allowMultiple = false, bool withData = false, bool withReadStream = false, diff --git a/lib/src/file_picker_io.dart b/lib/src/file_picker_io.dart index 3560477c..6e0499f5 100644 --- a/lib/src/file_picker_io.dart +++ b/lib/src/file_picker_io.dart @@ -29,6 +29,7 @@ class FilePickerIO extends FilePicker { bool? allowCompression = true, bool allowMultiple = false, bool? withData = false, + int compressionQuality = 30, bool? withReadStream = false, bool lockParentWindow = false, bool readSequential = false, @@ -41,6 +42,7 @@ class FilePickerIO extends FilePicker { onFileLoading, withData, withReadStream, + compressionQuality, ); @override @@ -72,6 +74,7 @@ class FilePickerIO extends FilePicker { Function(FilePickerStatus)? onFileLoading, bool? withData, bool? withReadStream, + int? compressionQuality, ) async { final String type = fileType.name; if (type != 'custom' && (allowedExtensions?.isNotEmpty ?? false)) { @@ -94,6 +97,7 @@ class FilePickerIO extends FilePicker { 'allowedExtensions': allowedExtensions, 'allowCompression': allowCompression, 'withData': withData, + 'compressionQuality': compressionQuality, }); if (result == null) { diff --git a/lib/src/file_picker_macos.dart b/lib/src/file_picker_macos.dart index bbcbd41e..cc3725c7 100644 --- a/lib/src/file_picker_macos.dart +++ b/lib/src/file_picker_macos.dart @@ -10,6 +10,7 @@ class FilePickerMacOS extends FilePicker { List? allowedExtensions, Function(FilePickerStatus)? onFileLoading, bool allowCompression = true, + int compressionQuality = 30, bool allowMultiple = false, bool withData = false, bool withReadStream = false, diff --git a/lib/src/linux/file_picker_linux.dart b/lib/src/linux/file_picker_linux.dart index 0892aa0a..36b278a1 100644 --- a/lib/src/linux/file_picker_linux.dart +++ b/lib/src/linux/file_picker_linux.dart @@ -19,6 +19,7 @@ class FilePickerLinux extends FilePicker { bool withReadStream = false, bool lockParentWindow = false, bool readSequential = false, + int compressionQuality = 30, }) async { final String executable = await _getPathToExecutable(); final dialogHandler = DialogHandler(executable); diff --git a/lib/src/windows/file_picker_windows.dart b/lib/src/windows/file_picker_windows.dart index 12969e81..8c71e871 100644 --- a/lib/src/windows/file_picker_windows.dart +++ b/lib/src/windows/file_picker_windows.dart @@ -27,6 +27,7 @@ class FilePickerWindows extends FilePicker { bool withReadStream = false, bool lockParentWindow = false, bool readSequential = false, + int compressionQuality = 30, }) async { final port = ReceivePort(); await Isolate.spawn( diff --git a/pubspec.yaml b/pubspec.yaml index ba8025aa..4c9d6537 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A package that allows you to use a native file explorer to pick sin homepage: https://github.com/miguelpruivo/plugins_flutter_file_picker repository: https://github.com/miguelpruivo/flutter_file_picker issue_tracker: https://github.com/miguelpruivo/flutter_file_picker/issues -version: 6.1.1 +version: 6.2.0 dependencies: flutter: