diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md index 58c4951a2d4f..1ab21108d70f 100644 --- a/packages/image_picker/image_picker_android/CHANGELOG.md +++ b/packages/image_picker/image_picker_android/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 0.8.5+6 * Updates minimum Flutter version to 3.0. +* Fixes names of picked files to match original filenames where possible. ## 0.8.5+5 diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java index 1f51a226c7e2..449480c19d9c 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java @@ -25,55 +25,60 @@ import android.content.ContentResolver; import android.content.Context; +import android.database.Cursor; import android.net.Uri; +import android.provider.MediaStore; import android.webkit.MimeTypeMap; +import io.flutter.Log; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.UUID; class FileUtils { - + /** + * Copies the file from the given content URI to a temporary directory, retaining the original + * file name if possible. + * + *
Each file is placed in its own directory to avoid conflicts according to the following + * scheme: {cacheDir}/{randomUuid}/{fileName} + * + *
If the original file name is unknown, a predefined "image_picker" filename is used and the
+ * file extension is deduced from the mime type (with fallback to ".jpg" in case of failure).
+ */
String getPathFromUri(final Context context, final Uri uri) {
- File file = null;
- InputStream inputStream = null;
- OutputStream outputStream = null;
- boolean success = false;
- try {
- String extension = getImageExtension(context, uri);
- inputStream = context.getContentResolver().openInputStream(uri);
- file = File.createTempFile("image_picker", extension, context.getCacheDir());
- file.deleteOnExit();
- outputStream = new FileOutputStream(file);
- if (inputStream != null) {
- copy(inputStream, outputStream);
- success = true;
+ try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) {
+ String uuid = UUID.randomUUID().toString();
+ File targetDirectory = new File(context.getCacheDir(), uuid);
+ targetDirectory.mkdir();
+ // TODO(SynSzakala) according to the docs, `deleteOnExit` does not work reliably on Android; we should preferably
+ // just clear the picked files after the app startup.
+ targetDirectory.deleteOnExit();
+ String fileName = getImageName(context, uri);
+ if (fileName == null) {
+ Log.w("FileUtils", "Cannot get file name for " + uri);
+ fileName = "image_picker" + getImageExtension(context, uri);
}
- } catch (IOException ignored) {
- } finally {
- try {
- if (inputStream != null) inputStream.close();
- } catch (IOException ignored) {
- }
- try {
- if (outputStream != null) outputStream.close();
- } catch (IOException ignored) {
- // If closing the output stream fails, we cannot be sure that the
- // target file was written in full. Flushing the stream merely moves
- // the bytes into the OS, not necessarily to the file.
- success = false;
+ File file = new File(targetDirectory, fileName);
+ try (OutputStream outputStream = new FileOutputStream(file)) {
+ copy(inputStream, outputStream);
+ return file.getPath();
}
+ } catch (IOException e) {
+ // If closing the output stream fails, we cannot be sure that the
+ // target file was written in full. Flushing the stream merely moves
+ // the bytes into the OS, not necessarily to the file.
+ return null;
}
- return success ? file.getPath() : null;
}
/** @return extension of image with dot, or default .jpg if it none. */
private static String getImageExtension(Context context, Uri uriImage) {
- String extension = null;
+ String extension;
try {
- String imagePath = uriImage.getPath();
if (uriImage.getScheme().equals(ContentResolver.SCHEME_CONTENT)) {
final MimeTypeMap mime = MimeTypeMap.getSingleton();
extension = mime.getExtensionFromMimeType(context.getContentResolver().getType(uriImage));
@@ -94,6 +99,20 @@ private static String getImageExtension(Context context, Uri uriImage) {
return "." + extension;
}
+ /** @return name of the image provided by ContentResolver; this may be null. */
+ private static String getImageName(Context context, Uri uriImage) {
+ try (Cursor cursor = queryImageName(context, uriImage)) {
+ if (cursor == null || !cursor.moveToFirst() || cursor.getColumnCount() < 1) return null;
+ return cursor.getString(0);
+ }
+ }
+
+ private static Cursor queryImageName(Context context, Uri uriImage) {
+ return context
+ .getContentResolver()
+ .query(uriImage, new String[] {MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null);
+ }
+
private static void copy(InputStream in, OutputStream out) throws IOException {
final byte[] buffer = new byte[4 * 1024];
int bytesRead;
diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java
index 32e3ebc6183d..0ea0173fa954 100644
--- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java
+++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java
@@ -8,8 +8,15 @@
import static org.junit.Assert.assertTrue;
import static org.robolectric.Shadows.shadowOf;
+import android.content.ContentProvider;
+import android.content.ContentValues;
import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
import android.net.Uri;
+import android.provider.MediaStore;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
@@ -19,6 +26,7 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowContentResolver;
@@ -63,4 +71,62 @@ public void FileUtil_getImageExtension() throws IOException {
String path = fileUtils.getPathFromUri(context, uri);
assertTrue(path.endsWith(".jpg"));
}
+
+ @Test
+ public void FileUtil_getImageName() throws IOException {
+ Uri uri = Uri.parse("content://dummy/dummy.png");
+ Robolectric.buildContentProvider(MockContentProvider.class).create("dummy");
+ shadowContentResolver.registerInputStream(
+ uri, new ByteArrayInputStream("imageStream".getBytes(UTF_8)));
+ String path = fileUtils.getPathFromUri(context, uri);
+ assertTrue(path.endsWith("dummy.png"));
+ }
+
+ private static class MockContentProvider extends ContentProvider {
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Nullable
+ @Override
+ public Cursor query(
+ @NonNull Uri uri,
+ @Nullable String[] projection,
+ @Nullable String selection,
+ @Nullable String[] selectionArgs,
+ @Nullable String sortOrder) {
+ MatrixCursor cursor = new MatrixCursor(new String[] {MediaStore.MediaColumns.DISPLAY_NAME});
+ cursor.addRow(new Object[] {"dummy.png"});
+ return cursor;
+ }
+
+ @Nullable
+ @Override
+ public String getType(@NonNull Uri uri) {
+ return "image/png";
+ }
+
+ @Nullable
+ @Override
+ public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(
+ @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(
+ @NonNull Uri uri,
+ @Nullable ContentValues values,
+ @Nullable String selection,
+ @Nullable String[] selectionArgs) {
+ return 0;
+ }
+ }
}
diff --git a/packages/image_picker/image_picker_android/example/android/app/build.gradle b/packages/image_picker/image_picker_android/example/android/app/build.gradle
index 31d8c82a0a9d..f8487c7959f1 100755
--- a/packages/image_picker/image_picker_android/example/android/app/build.gradle
+++ b/packages/image_picker/image_picker_android/example/android/app/build.gradle
@@ -63,5 +63,7 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+ implementation project(':image_picker_android')
+ implementation project(':espresso')
api 'androidx.test:core:1.4.0'
}
diff --git a/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerPickTest.java b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerPickTest.java
new file mode 100644
index 000000000000..8b7ae11d5c2d
--- /dev/null
+++ b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerPickTest.java
@@ -0,0 +1,43 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package io.flutter.plugins.imagepickerexample;
+
+import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget;
+import static androidx.test.espresso.flutter.action.FlutterActions.click;
+import static androidx.test.espresso.flutter.assertion.FlutterAssertions.matches;
+import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withText;
+import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withValueKey;
+import static androidx.test.espresso.intent.Intents.intended;
+import static androidx.test.espresso.intent.Intents.intending;
+import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.net.Uri;
+import androidx.test.espresso.intent.rule.IntentsTestRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+
+public class ImagePickerPickTest {
+
+ @Rule public TestRule rule = new IntentsTestRule<>(DriverExtensionActivity.class);
+
+ @Test
+ @Ignore("Doesn't run in Firebase Test Lab: https://github.com/flutter/flutter/issues/94748")
+ public void imageIsPickedWithOriginalName() {
+ Instrumentation.ActivityResult result =
+ new Instrumentation.ActivityResult(
+ Activity.RESULT_OK, new Intent().setData(Uri.parse("content://dummy/dummy.png")));
+ intending(hasAction(Intent.ACTION_GET_CONTENT)).respondWith(result);
+ onFlutterWidget(withValueKey("image_picker_example_from_gallery")).perform(click());
+ onFlutterWidget(withText("PICK")).perform(click());
+ intended(hasAction(Intent.ACTION_GET_CONTENT));
+ onFlutterWidget(withValueKey("image_picker_example_picked_image_name"))
+ .check(matches(withText("dummy.png")));
+ }
+}
diff --git a/packages/image_picker/image_picker_android/example/android/app/src/debug/AndroidManifest.xml b/packages/image_picker/image_picker_android/example/android/app/src/debug/AndroidManifest.xml
index 6f85cefded34..317af1d1a371 100644
--- a/packages/image_picker/image_picker_android/example/android/app/src/debug/AndroidManifest.xml
+++ b/packages/image_picker/image_picker_android/example/android/app/src/debug/AndroidManifest.xml
@@ -13,5 +13,17 @@
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
+