[image_picker_android] Adjust file extension in FileUtils when it does not match its mime type (#3409)

[image_picker_android] Adjust file extension in FileUtils when it does not match its mime type
diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md
index 9591cf3..358e20c 100644
--- a/packages/image_picker/image_picker_android/CHANGELOG.md
+++ b/packages/image_picker/image_picker_android/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.8.6+5
+
+* Fixes case when file extension returned from the OS does not match its real mime type.
+
 ## 0.8.6+4
 
 * Bumps androidx.exifinterface:exifinterface from 1.3.3 to 1.3.6.
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 449480c..4ca970a 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
@@ -45,6 +45,9 @@
    * <p>Each file is placed in its own directory to avoid conflicts according to the following
    * scheme: {cacheDir}/{randomUuid}/{fileName}
    *
+   * <p>File extension is changed to match MIME type of the file, if known. Otherwise, the extension
+   * is left unchanged.
+   *
    * <p>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).
    */
@@ -57,9 +60,14 @@
       //  just clear the picked files after the app startup.
       targetDirectory.deleteOnExit();
       String fileName = getImageName(context, uri);
+      String extension = getImageExtension(context, uri);
+
       if (fileName == null) {
         Log.w("FileUtils", "Cannot get file name for " + uri);
-        fileName = "image_picker" + getImageExtension(context, uri);
+        if (extension == null) extension = ".jpg";
+        fileName = "image_picker" + extension;
+      } else if (extension != null) {
+        fileName = getBaseName(fileName) + extension;
       }
       File file = new File(targetDirectory, fileName);
       try (OutputStream outputStream = new FileOutputStream(file)) {
@@ -74,7 +82,7 @@
     }
   }
 
-  /** @return extension of image with dot, or default .jpg if it none. */
+  /** @return extension of image with dot, or null if it's empty. */
   private static String getImageExtension(Context context, Uri uriImage) {
     String extension;
 
@@ -88,12 +96,11 @@
                 Uri.fromFile(new File(uriImage.getPath())).toString());
       }
     } catch (Exception e) {
-      extension = null;
+      return null;
     }
 
     if (extension == null || extension.isEmpty()) {
-      //default extension for matches the previous behavior of the plugin
-      extension = "jpg";
+      return null;
     }
 
     return "." + extension;
@@ -121,4 +128,9 @@
     }
     out.flush();
   }
+
+  private static String getBaseName(String fileName) {
+    // Basename is everything before the last '.'.
+    return fileName.substring(0, fileName.lastIndexOf('.'));
+  }
 }
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 0ea0173..d125b78 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
@@ -15,6 +15,7 @@
 import android.database.MatrixCursor;
 import android.net.Uri;
 import android.provider.MediaStore;
+import android.webkit.MimeTypeMap;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.test.core.app.ApplicationProvider;
@@ -29,6 +30,7 @@
 import org.robolectric.Robolectric;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.shadows.ShadowContentResolver;
+import org.robolectric.shadows.ShadowMimeTypeMap;
 
 @RunWith(RobolectricTestRunner.class)
 public class FileUtilTest {
@@ -42,6 +44,10 @@
     context = ApplicationProvider.getApplicationContext();
     shadowContentResolver = shadowOf(context.getContentResolver());
     fileUtils = new FileUtils();
+    ShadowMimeTypeMap mimeTypeMap = shadowOf(MimeTypeMap.getSingleton());
+    mimeTypeMap.addExtensionMimeTypMapping("jpg", "image/jpeg");
+    mimeTypeMap.addExtensionMimeTypMapping("png", "image/png");
+    mimeTypeMap.addExtensionMimeTypMapping("webp", "image/webp");
   }
 
   @Test
@@ -74,15 +80,38 @@
 
   @Test
   public void FileUtil_getImageName() throws IOException {
-    Uri uri = Uri.parse("content://dummy/dummy.png");
+    Uri uri = MockContentProvider.PNG_URI;
     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"));
+    assertTrue(path.endsWith("a.b.png"));
+  }
+
+  @Test
+  public void FileUtil_getImageName_mismatchedType() throws IOException {
+    Uri uri = MockContentProvider.WEBP_URI;
+    Robolectric.buildContentProvider(MockContentProvider.class).create("dummy");
+    shadowContentResolver.registerInputStream(
+        uri, new ByteArrayInputStream("imageStream".getBytes(UTF_8)));
+    String path = fileUtils.getPathFromUri(context, uri);
+    assertTrue(path.endsWith("c.d.webp"));
+  }
+
+  @Test
+  public void FileUtil_getImageName_unknownType() throws IOException {
+    Uri uri = MockContentProvider.UNKNOWN_URI;
+    Robolectric.buildContentProvider(MockContentProvider.class).create("dummy");
+    shadowContentResolver.registerInputStream(
+        uri, new ByteArrayInputStream("imageStream".getBytes(UTF_8)));
+    String path = fileUtils.getPathFromUri(context, uri);
+    assertTrue(path.endsWith("e.f.g"));
   }
 
   private static class MockContentProvider extends ContentProvider {
+    public static final Uri PNG_URI = Uri.parse("content://dummy/a.b.png");
+    public static final Uri WEBP_URI = Uri.parse("content://dummy/c.d.png");
+    public static final Uri UNKNOWN_URI = Uri.parse("content://dummy/e.f.g");
 
     @Override
     public boolean onCreate() {
@@ -98,14 +127,16 @@
         @Nullable String[] selectionArgs,
         @Nullable String sortOrder) {
       MatrixCursor cursor = new MatrixCursor(new String[] {MediaStore.MediaColumns.DISPLAY_NAME});
-      cursor.addRow(new Object[] {"dummy.png"});
+      cursor.addRow(new Object[] {uri.getLastPathSegment()});
       return cursor;
     }
 
     @Nullable
     @Override
     public String getType(@NonNull Uri uri) {
-      return "image/png";
+      if (uri.equals(PNG_URI)) return "image/png";
+      if (uri.equals(WEBP_URI)) return "image/webp";
+      return null;
     }
 
     @Nullable
diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml
index 5c9be4f..41c8277 100755
--- a/packages/image_picker/image_picker_android/pubspec.yaml
+++ b/packages/image_picker/image_picker_android/pubspec.yaml
@@ -3,7 +3,7 @@
 repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_android
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
 
-version: 0.8.6+4
+version: 0.8.6+5
 
 environment:
   sdk: ">=2.17.0 <3.0.0"