diff --git a/packages/image_picker/CHANGELOG.md b/packages/image_picker/CHANGELOG.md index 11c48e8724bc..96bb4fcad8c3 100644 --- a/packages/image_picker/CHANGELOG.md +++ b/packages/image_picker/CHANGELOG.md @@ -1,6 +1,11 @@ +## 0.6.2+1 + +* Android: Fix a crash when a non-image file is picked. +* Android: Fix unwanted bitmap scaling. + ## 0.6.2 -* iOS: Fixes an issue where picking conent from Gallery would result in a crash on iOS 13. +* iOS: Fixes an issue where picking content from Gallery would result in a crash on iOS 13. ## 0.6.1+11 diff --git a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index f9318e9c5760..e34a3b5632c0 100644 --- a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -521,10 +521,7 @@ private void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled if (methodCall != null) { Double maxWidth = methodCall.argument("maxWidth"); Double maxHeight = methodCall.argument("maxHeight"); - int imageQuality = - methodCall.argument("imageQuality") == null - ? 100 - : (int) methodCall.argument("imageQuality"); + Integer imageQuality = methodCall.argument("imageQuality"); String finalImagePath = imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality); diff --git a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java index ab3120afb6d0..ca0498bfa55b 100644 --- a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java +++ b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java @@ -7,6 +7,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.util.Log; +import androidx.annotation.Nullable; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -28,31 +29,39 @@ class ImageResizer { *

If no resizing is needed, returns the path for the original image. */ String resizeImageIfNeeded( - String imagePath, Double maxWidth, Double maxHeight, int imageQuality) { + String imagePath, + @Nullable Double maxWidth, + @Nullable Double maxHeight, + @Nullable Integer imageQuality) { boolean shouldScale = - maxWidth != null || maxHeight != null || (imageQuality > -1 && imageQuality < 101); - - if (!shouldScale) { - return imagePath; + maxWidth != null || maxHeight != null || isImageQualityValid(imageQuality); + String[] pathParts = imagePath.split("/"); + String imageName = pathParts[pathParts.length - 1]; + File file; + Bitmap bmp = decodeFile(imagePath); + if (bmp == null) { + return null; } - try { - File scaledImage = resizedImage(imagePath, maxWidth, maxHeight, imageQuality); - exifDataCopier.copyExif(imagePath, scaledImage.getPath()); - - return scaledImage.getPath(); + if (!shouldScale) { + file = createImageOnExternalDirectory(imageName, bmp, 100); + } else { + file = resizedImage(bmp, maxWidth, maxHeight, imageQuality, imageName); + } + copyExif(imagePath, file.getPath()); + return file.getPath(); } catch (IOException e) { throw new RuntimeException(e); } } - private File resizedImage(String path, Double maxWidth, Double maxHeight, int imageQuality) + private File resizedImage( + Bitmap bmp, Double maxWidth, Double maxHeight, Integer imageQuality, String outputImageName) throws IOException { - Bitmap bmp = BitmapFactory.decodeFile(path); double originalWidth = bmp.getWidth() * 1.0; double originalHeight = bmp.getHeight() * 1.0; - if (imageQuality < 0 || imageQuality > 100) { + if (!isImageQualityValid(imageQuality)) { imageQuality = 100; } @@ -91,24 +100,51 @@ private File resizedImage(String path, Double maxWidth, Double maxHeight, int im } } - Bitmap scaledBmp = Bitmap.createScaledBitmap(bmp, width.intValue(), height.intValue(), false); + Bitmap scaledBmp = createScaledBitmap(bmp, width.intValue(), height.intValue(), false); + File file = + createImageOnExternalDirectory("/scaled_" + outputImageName, scaledBmp, imageQuality); + return file; + } + + private File createFile(File externalFilesDirectory, String child) { + return new File(externalFilesDirectory, child); + } + + private FileOutputStream createOutputStream(File imageFile) throws IOException { + return new FileOutputStream(imageFile); + } + + private void copyExif(String filePathOri, String filePathDest) { + exifDataCopier.copyExif(filePathOri, filePathDest); + } + + private Bitmap decodeFile(String path) { + return BitmapFactory.decodeFile(path); + } + + private Bitmap createScaledBitmap(Bitmap bmp, int width, int height, boolean filter) { + return Bitmap.createScaledBitmap(bmp, width, height, filter); + } + + private boolean isImageQualityValid(Integer imageQuality) { + return imageQuality != null && imageQuality > 0 && imageQuality < 100; + } + + private File createImageOnExternalDirectory(String name, Bitmap bitmap, int imageQuality) + throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - boolean saveAsPNG = bmp.hasAlpha(); + boolean saveAsPNG = bitmap.hasAlpha(); if (saveAsPNG) { Log.d( "ImageResizer", "image_picker: compressing is not supported for type PNG. Returning the image with original quality"); } - scaledBmp.compress( + bitmap.compress( saveAsPNG ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, imageQuality, outputStream); - - String[] pathParts = path.split("/"); - String imageName = pathParts[pathParts.length - 1]; - - File imageFile = new File(externalFilesDirectory, "/scaled_" + imageName); - FileOutputStream fileOutput = new FileOutputStream(imageFile); + File imageFile = createFile(externalFilesDirectory, name); + FileOutputStream fileOutput = createOutputStream(imageFile); fileOutput.write(outputStream.toByteArray()); fileOutput.close(); return imageFile; diff --git a/packages/image_picker/example/android/app/build.gradle b/packages/image_picker/example/android/app/build.gradle index 800e3e836a97..483268fdc18c 100755 --- a/packages/image_picker/example/android/app/build.gradle +++ b/packages/image_picker/example/android/app/build.gradle @@ -62,4 +62,5 @@ dependencies { testImplementation 'org.mockito:mockito-core:2.17.0' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + testImplementation "org.robolectric:robolectric:3.3.2" } diff --git a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java index 8e89a15abc8e..51733a503a92 100644 --- a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java +++ b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java @@ -25,10 +25,7 @@ import org.mockito.MockitoAnnotations; public class ImagePickerCacheTest { - private static final double WIDTH = 10.0; - private static final double HEIGHT = 10.0; private static final int IMAGE_QUALITY = 90; - private static final String PATH = "a_mock_path"; @Mock Activity mockActivity; @Mock SharedPreferences mockPreference; diff --git a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java index 60e1167cd87a..88fa3372766f 100644 --- a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java +++ b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java @@ -24,9 +24,9 @@ import org.mockito.MockitoAnnotations; public class ImagePickerDelegateTest { - private static final double WIDTH = 10.0; - private static final double HEIGHT = 10.0; - private static final int IMAGE_QUALITY = 100; + private static final Double WIDTH = 10.0; + private static final Double HEIGHT = 10.0; + private static final Integer IMAGE_QUALITY = 90; @Mock Activity mockActivity; @Mock ImageResizer mockImageResizer; @@ -62,13 +62,15 @@ public void setUp() { when(mockFileUtils.getPathFromUri(any(Context.class), any(Uri.class))) .thenReturn("pathFromUri"); + when(mockImageResizer.resizeImageIfNeeded("pathFromUri", null, null, null)) + .thenReturn("originalPath"); when(mockImageResizer.resizeImageIfNeeded("pathFromUri", null, null, IMAGE_QUALITY)) .thenReturn("originalPath"); - when(mockImageResizer.resizeImageIfNeeded("pathFromUri", WIDTH, HEIGHT, IMAGE_QUALITY)) + when(mockImageResizer.resizeImageIfNeeded("pathFromUri", WIDTH, HEIGHT, null)) .thenReturn("scaledPath"); - when(mockImageResizer.resizeImageIfNeeded("pathFromUri", WIDTH, null, IMAGE_QUALITY)) + when(mockImageResizer.resizeImageIfNeeded("pathFromUri", WIDTH, null, null)) .thenReturn("scaledPath"); - when(mockImageResizer.resizeImageIfNeeded("pathFromUri", null, HEIGHT, IMAGE_QUALITY)) + when(mockImageResizer.resizeImageIfNeeded("pathFromUri", null, HEIGHT, null)) .thenReturn("scaledPath"); mockFileUriResolver = new MockFileUriResolver(); diff --git a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java new file mode 100644 index 000000000000..aac7472eaac3 --- /dev/null +++ b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java @@ -0,0 +1,65 @@ +// Copyright 2019 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.imagepicker; + +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import java.io.File; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +// RobolectricTestRunner always creates a default mock bitmap when reading from file. So we cannot actually test the scaling. +// But we can still test whether the original or scaled file is created. +@RunWith(RobolectricTestRunner.class) +public class ImageResizerTest { + + ImageResizer resizer; + File imageFile; + File externalDirectory; + Bitmap originalImageBitmap; + + @Before + public void setUp() throws IOException { + MockitoAnnotations.initMocks(this); + imageFile = new File(getClass().getClassLoader().getResource("pngImage.png").getFile()); + originalImageBitmap = BitmapFactory.decodeFile(imageFile.getPath()); + TemporaryFolder temporaryFolder = new TemporaryFolder(); + temporaryFolder.create(); + externalDirectory = temporaryFolder.newFolder("image_picker_testing_path"); + resizer = new ImageResizer(externalDirectory, new ExifDataCopier()); + } + + @Test + public void onResizeImageIfNeeded_WhenQualityIsNull_ShoultNotResize_ReturnTheUnscaledFile() { + String outoutFile = resizer.resizeImageIfNeeded(imageFile.getPath(), null, null, null); + assertThat(outoutFile, equalTo(externalDirectory.getPath() + "/pngImage.png")); + } + + @Test + public void onResizeImageIfNeeded_WhenQualityIsNotNull_ShoulResize_ReturnResizedFile() { + String outoutFile = resizer.resizeImageIfNeeded(imageFile.getPath(), null, null, 50); + assertThat(outoutFile, equalTo(externalDirectory.getPath() + "/scaled_pngImage.png")); + } + + @Test + public void onResizeImageIfNeeded_WhenWidthIsNotNull_ShoulResize_ReturnResizedFile() { + String outoutFile = resizer.resizeImageIfNeeded(imageFile.getPath(), 50.0, null, null); + assertThat(outoutFile, equalTo(externalDirectory.getPath() + "/scaled_pngImage.png")); + } + + @Test + public void onResizeImageIfNeeded_WhenHeightIsNotNull_ShoulResize_ReturnResizedFile() { + String outoutFile = resizer.resizeImageIfNeeded(imageFile.getPath(), null, 50.0, null); + assertThat(outoutFile, equalTo(externalDirectory.getPath() + "/scaled_pngImage.png")); + } +} diff --git a/packages/image_picker/example/android/app/src/test/resources/pngImage.png b/packages/image_picker/example/android/app/src/test/resources/pngImage.png new file mode 100644 index 000000000000..22ac5a5a1485 Binary files /dev/null and b/packages/image_picker/example/android/app/src/test/resources/pngImage.png differ diff --git a/packages/image_picker/pubspec.yaml b/packages/image_picker/pubspec.yaml index bca5a9df2059..0e47c53e6911 100755 --- a/packages/image_picker/pubspec.yaml +++ b/packages/image_picker/pubspec.yaml @@ -5,7 +5,7 @@ authors: - Flutter Team - Rhodes Davis Jr. homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker -version: 0.6.2 +version: 0.6.2+1 flutter: plugin: