Skip to content

Commit

Permalink
Merge pull request #1426 from Umar1312/master
Browse files Browse the repository at this point in the history
Add ability to compress images on android by specifying a compression quality value
  • Loading branch information
Miguel Ruivo authored Mar 4, 2024
2 parents ecdb559 + 548f5d3 commit e3ae7c0
Show file tree
Hide file tree
Showing 13 changed files with 210 additions and 29 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

Expand Down Expand Up @@ -89,13 +92,18 @@ 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;
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());
Expand All @@ -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));

Expand Down Expand Up @@ -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
Expand All @@ -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<HashMap<String, Object>> files = new ArrayList<>();

for (FileInfo file : (ArrayList<FileInfo>)data) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<String>) 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);
}

}
Expand Down
190 changes: 171 additions & 19 deletions android/src/main/java/com/mr/flutter/plugin/filepicker/FileUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,22 +30,17 @@
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 {

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<String> allowedExtensions) {

if (allowedExtensions == null || allowedExtensions.isEmpty()) {
Expand All @@ -51,18 +50,13 @@ public static String[] getMimeTypes(final ArrayList<String> allowedExtensions) {
final ArrayList<String> 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]);
Expand Down Expand Up @@ -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<String, Object> createFileInfoMap(File compressedImageFile) {
HashMap<String, Object> 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/");
Expand Down Expand Up @@ -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);
Expand All @@ -320,4 +472,4 @@ private static String getDocumentPathFromTreeUri(final Uri treeUri) {
else return File.separator;
}

}
}
2 changes: 1 addition & 1 deletion example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions example/lib/src/file_picker_demo.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class _FilePickerDemoState extends State<FilePickerDemo> {
try {
_directoryPath = null;
_paths = (await FilePicker.platform.pickFiles(
compressionQuality: 30,
type: _pickingType,
allowMultiple: _multiPick,
onFileLoading: (FilePickerStatus status) => print(status),
Expand Down
Loading

0 comments on commit e3ae7c0

Please sign in to comment.