From 923d12475bb8515d3deb80071f441042624df38a Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Tue, 26 Nov 2024 10:37:59 -0800 Subject: [PATCH 01/26] real rough --- .../file_selector_android/FileUtils.java | 96 ++++++++++++------- 1 file changed, 59 insertions(+), 37 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 5d0b61312b36..e5d4ba311892 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -111,43 +111,39 @@ public static String getPathFromUri(@NonNull Context context, @NonNull Uri uri) * or if a security exception is encountered when opening the input stream to start the copying. */ @Nullable - public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @NonNull Uri uri) { - try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { - String uuid = UUID.nameUUIDFromBytes(uri.toString().getBytes()).toString(); - File targetDirectory = new File(context.getCacheDir(), uuid); - targetDirectory.mkdir(); - targetDirectory.deleteOnExit(); - String fileName = getFileName(context, uri); - String extension = getFileExtension(context, uri); - - if (fileName == null) { - if (extension == null) { - throw new IllegalArgumentException("No name nor extension found for file."); - } else { - fileName = "file_selector" + extension; - } - } else if (extension != null) { - fileName = getBaseName(fileName) + extension; + public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @NonNull Uri uri) throws IOException { + if (!uri.getPath().startsWith(context.getFilesDir().getCanonicalPath())) { + throw new IllegalArgumentException(); + } + + String uuid = UUID.nameUUIDFromBytes(uri.toString().getBytes()).toString(); + File targetDirectory = new File(context.getCacheDir(), uuid); + targetDirectory.mkdir(); + targetDirectory.deleteOnExit(); + String fileName = getFileName(context, uri); + String extension = getFileExtension(context, uri); + + if (fileName == null) { + if (extension == null) { + throw new IllegalArgumentException("No name nor extension found for file."); + } else { + fileName = "file_selector" + extension; } + } else if (extension != null) { + fileName = getBaseName(fileName) + extension; + } - File file = new File(targetDirectory, fileName); + String filePath = new File(targetDirectory, fileName + extension).getPath(); + File file = saferOpenFile(targetDirectory.getCanonicalPath(), filePath); - 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; - } catch (SecurityException e) { - // Calling `ContentResolver#openInputStream()` has been reported to throw a - // `SecurityException` on some devices in certain circumstances. Instead of crashing, we - // return `null`. - // - // See https://github.com/flutter/flutter/issues/100025 for more details. - return null; + + try ( + InputStream inputStream = context.getContentResolver().openInputStream(Uri.fromFile(file)); + OutputStream outputStream = new FileOutputStream(file) + ) { + assert inputStream != null; + copy(inputStream, outputStream); + return file.getPath(); } } @@ -172,14 +168,17 @@ private static String getFileExtension(Context context, Uri uriFile) { return null; } - return "." + extension; + return "." + sanitizeFilename(extension); } /** Returns the name of the file provided by ContentResolver; this may be null. */ private static String getFileName(Context context, Uri uriFile) { try (Cursor cursor = queryFileName(context, uriFile)) { - if (cursor == null || !cursor.moveToFirst() || cursor.getColumnCount() < 1) return null; - return cursor.getString(0); + if (cursor == null || !cursor.moveToFirst() || cursor.getColumnCount() < 1) { + return null; + } + String unsanitizedFileName = cursor.getString(0); + return sanitizeFilename(unsanitizedFileName); } } @@ -206,4 +205,27 @@ private static String getBaseName(String fileName) { // Basename is everything before the last '.'. return fileName.substring(0, lastDotIndex); } + + protected static String sanitizeFilename(String displayName) { + if (displayName == null) { + return null; + } + + String[] badCharacters = new String[] { "..", "/" }; + String[] segments = displayName.split("/"); + String fileName = segments[segments.length - 1]; + for (String suspString : badCharacters) { + fileName = fileName.replace(suspString, "_"); + } + return fileName; + } + + public static File saferOpenFile(String path, String expectedDir) throws IllegalArgumentException, IOException { + File f = new File(path); + String canonicalPath = f.getCanonicalPath(); + if (!canonicalPath.startsWith(expectedDir)) { + throw new IllegalArgumentException(); + } + return f; + } } From 6ea28a1740123e5c77c3d4ddfcb8d09a92c05202 Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Tue, 26 Nov 2024 10:42:23 -0800 Subject: [PATCH 02/26] restore catches --- .../packages/file_selector_android/FileUtils.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index e5d4ba311892..26389affa48e 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -144,6 +144,18 @@ public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @Non assert inputStream != null; 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; + } catch (SecurityException e) { + // Calling `ContentResolver#openInputStream()` has been reported to throw a + // `SecurityException` on some devices in certain circumstances. Instead of crashing, we + // return `null`. + // + // See https://github.com/flutter/flutter/issues/100025 for more details. + return null; } } From 301eea63e4ab641255265917ae97fafb9235509f Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Tue, 26 Nov 2024 10:48:59 -0800 Subject: [PATCH 03/26] broken wip --- .../file_selector_android/FileUtils.java | 83 +++++++++---------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 26389affa48e..fd4cf019518c 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -111,51 +111,48 @@ public static String getPathFromUri(@NonNull Context context, @NonNull Uri uri) * or if a security exception is encountered when opening the input stream to start the copying. */ @Nullable - public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @NonNull Uri uri) throws IOException { - if (!uri.getPath().startsWith(context.getFilesDir().getCanonicalPath())) { - throw new IllegalArgumentException(); - } - - String uuid = UUID.nameUUIDFromBytes(uri.toString().getBytes()).toString(); - File targetDirectory = new File(context.getCacheDir(), uuid); - targetDirectory.mkdir(); - targetDirectory.deleteOnExit(); - String fileName = getFileName(context, uri); - String extension = getFileExtension(context, uri); - - if (fileName == null) { - if (extension == null) { - throw new IllegalArgumentException("No name nor extension found for file."); - } else { - fileName = "file_selector" + extension; + public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @NonNull Uri uri) { + try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { + String uuid = UUID.nameUUIDFromBytes(uri.toString().getBytes()).toString(); + File targetDirectory = new File(context.getCacheDir(), uuid); + targetDirectory.mkdir(); + targetDirectory.deleteOnExit(); + String fileName = getFileName(context, uri); + String extension = getFileExtension(context, uri); + + if (fileName == null) { + if (extension == null) { + throw new IllegalArgumentException("No name nor extension found for file."); + } else { + fileName = "file_selector" + extension; + } + } else if (extension != null) { + fileName = getBaseName(fileName) + extension; } - } else if (extension != null) { - fileName = getBaseName(fileName) + extension; - } - String filePath = new File(targetDirectory, fileName + extension).getPath(); - File file = saferOpenFile(targetDirectory.getCanonicalPath(), filePath); - - - try ( - InputStream inputStream = context.getContentResolver().openInputStream(Uri.fromFile(file)); - OutputStream outputStream = new FileOutputStream(file) - ) { - assert inputStream != null; - 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; - } catch (SecurityException e) { - // Calling `ContentResolver#openInputStream()` has been reported to throw a - // `SecurityException` on some devices in certain circumstances. Instead of crashing, we - // return `null`. - // - // See https://github.com/flutter/flutter/issues/100025 for more details. - return null; + String filePath = new File(targetDirectory, fileName + extension).getPath(); + File inputFile = saferOpenFile(targetDirectory.getCanonicalPath(), filePath); + + + try ( + OutputStream outputStream = new FileOutputStream(file) + ) { + assert inputStream != null; + 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; + } catch (SecurityException e) { + // Calling `ContentResolver#openInputStream()` has been reported to throw a + // `SecurityException` on some devices in certain circumstances. Instead of crashing, we + // return `null`. + // + // See https://github.com/flutter/flutter/issues/100025 for more details. + return null; + } } } From 0eae93537e16d59d7b0c4d0cdef2adcdf92a08e1 Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Tue, 26 Nov 2024 10:50:00 -0800 Subject: [PATCH 04/26] move catches --- .../file_selector_android/FileUtils.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index fd4cf019518c..5cd45180c57c 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -140,20 +140,21 @@ public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @Non assert inputStream != null; 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; - } catch (SecurityException e) { - // Calling `ContentResolver#openInputStream()` has been reported to throw a - // `SecurityException` on some devices in certain circumstances. Instead of crashing, we - // return `null`. - // - // See https://github.com/flutter/flutter/issues/100025 for more details. - return null; } } + 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; + } catch (SecurityException e) { + // Calling `ContentResolver#openInputStream()` has been reported to throw a + // `SecurityException` on some devices in certain circumstances. Instead of crashing, we + // return `null`. + // + // See https://github.com/flutter/flutter/issues/100025 for more details. + return null; + } } /** Returns the extension of file with dot, or null if it's empty. */ From 0c70fa077400a8d2435a2d5051fc6f57e57ba0e3 Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Tue, 26 Nov 2024 10:50:30 -0800 Subject: [PATCH 05/26] format --- .../dev/flutter/packages/file_selector_android/FileUtils.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 5cd45180c57c..13a8bb428445 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -141,8 +141,7 @@ public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @Non copy(inputStream, outputStream); return file.getPath(); } - } - catch (IOException e) { + } 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. From 6ca5f7fae1627cc0af1ac5bfcd34f81b2ced39fa Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Tue, 26 Nov 2024 11:19:56 -0800 Subject: [PATCH 06/26] maybe working? --- .../packages/file_selector_android/FileUtils.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 13a8bb428445..803f4ede8136 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -40,6 +40,8 @@ import java.io.OutputStream; import java.util.UUID; +import io.flutter.Log; + public class FileUtils { /** URI authority that represents access to external storage providers. */ @@ -130,16 +132,14 @@ public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @Non fileName = getBaseName(fileName) + extension; } - String filePath = new File(targetDirectory, fileName + extension).getPath(); - File inputFile = saferOpenFile(targetDirectory.getCanonicalPath(), filePath); + String filePath = new File(targetDirectory, fileName).getPath(); + File outputFile = saferOpenFile(filePath, targetDirectory.getCanonicalPath()); - try ( - OutputStream outputStream = new FileOutputStream(file) - ) { + try (OutputStream outputStream = new FileOutputStream(outputFile)) { assert inputStream != null; copy(inputStream, outputStream); - return file.getPath(); + return outputFile.getPath(); } } catch (IOException e) { // If closing the output stream fails, we cannot be sure that the From 5142fbcd8c9750403c943e736a008cafc37d0c18 Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Tue, 26 Nov 2024 11:20:30 -0800 Subject: [PATCH 07/26] remove log --- .../dev/flutter/packages/file_selector_android/FileUtils.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 803f4ede8136..bc05d296076e 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -40,8 +40,6 @@ import java.io.OutputStream; import java.util.UUID; -import io.flutter.Log; - public class FileUtils { /** URI authority that represents access to external storage providers. */ From 8be370a2ba3e886a100c489c109929f61cab6796 Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Tue, 26 Nov 2024 11:55:50 -0800 Subject: [PATCH 08/26] lint (nullability annotations) --- .../dev/flutter/packages/file_selector_android/FileUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index bc05d296076e..b810b9286471 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -213,7 +213,7 @@ private static String getBaseName(String fileName) { return fileName.substring(0, lastDotIndex); } - protected static String sanitizeFilename(String displayName) { + protected static @Nullable String sanitizeFilename(@Nullable String displayName) { if (displayName == null) { return null; } @@ -227,7 +227,7 @@ protected static String sanitizeFilename(String displayName) { return fileName; } - public static File saferOpenFile(String path, String expectedDir) throws IllegalArgumentException, IOException { + public static @NonNull File saferOpenFile(@NonNull String path, @NonNull String expectedDir) throws IllegalArgumentException, IOException { File f = new File(path); String canonicalPath = f.getCanonicalPath(); if (!canonicalPath.startsWith(expectedDir)) { From 2f3fdf252f67a24562f14a9259788b4ac5042546 Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Tue, 26 Nov 2024 13:11:34 -0800 Subject: [PATCH 09/26] tests and small other logs --- .../file_selector_android/FileUtils.java | 4 +- .../file_selector_android/FileUtilsTest.java | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index b810b9286471..18e338f07618 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -227,11 +227,11 @@ private static String getBaseName(String fileName) { return fileName; } - public static @NonNull File saferOpenFile(@NonNull String path, @NonNull String expectedDir) throws IllegalArgumentException, IOException { + protected static @NonNull File saferOpenFile(@NonNull String path, @NonNull String expectedDir) throws IllegalArgumentException, IOException { File f = new File(path); String canonicalPath = f.getCanonicalPath(); if (!canonicalPath.startsWith(expectedDir)) { - throw new IllegalArgumentException(); + throw new IllegalArgumentException("Trying to open path outside of the expected directory. File: " + f.getCanonicalPath() + " was expected to be within directory: " + expectedDir + "."); } return f; } diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java index 760874317efb..f26090253ac9 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java @@ -196,6 +196,17 @@ public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithUnknownTyp assertTrue(path.endsWith("e.f.g")); } + @Test + public void getFileExtension_throwsIllegalArgumentExceptionForFileInWrongDirectory() { + Uri uri = Uri.parse(MockMaliciousContentProvider.PNG_URI); + Robolectric.buildContentProvider(MockMaliciousContentProvider.class).create("dummy"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); + String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); + System.out.println(path); + assertTrue(path.endsWith("_bar.png")); + } + private static class MockContentProvider extends ContentProvider { public static final Uri TXT_URI = Uri.parse("content://dummy/dummydocument"); public static final Uri PNG_URI = Uri.parse("content://dummy/a.b.png"); @@ -252,4 +263,51 @@ public int update( return 0; } } + + // Mocks a malicious content provider attempting to use path indirection to modify files outside + // of the intended directory. + // See https://developer.android.com/privacy-and-security/risks/untrustworthy-contentprovider-provided-filename#don%27t-trust-user-input. + private static class MockMaliciousContentProvider extends ContentProvider { + public static String PNG_URI = "content://dummy/a.png"; + + @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[] {"foo/../..bar.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; + } + } } From 24f329db1daeb38d529f8b1348f3ef8e22406ea9 Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Tue, 26 Nov 2024 13:16:57 -0800 Subject: [PATCH 10/26] logs --- .../flutter/packages/file_selector_android/FileUtils.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 18e338f07618..e6fa15aa36d6 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -98,6 +98,11 @@ public static String getPathFromUri(@NonNull Context context, @NonNull Uri uri) * Copies the file from the given content URI to a temporary directory, retaining the original * file name if possible. * + *

If the filename contains path indirection or separators (.. or /), the end file name will be + * the segment after the final separator, with indirection replaced by underscores. + * E.g. "example/../..file.png" -> "_file.png". + * See: Improperly trusting ContentProvider-provided filename. + * *

Each file is placed in its own directory to avoid conflicts according to the following * scheme: {cacheDir}/{randomUuid}/{fileName} * @@ -213,6 +218,7 @@ private static String getBaseName(String fileName) { return fileName.substring(0, lastDotIndex); } + // From https://developer.android.com/privacy-and-security/risks/untrustworthy-contentprovider-provided-filename#sanitize-provided-filenames. protected static @Nullable String sanitizeFilename(@Nullable String displayName) { if (displayName == null) { return null; @@ -227,6 +233,7 @@ private static String getBaseName(String fileName) { return fileName; } + // From https://developer.android.com/privacy-and-security/risks/path-traversal#path-traversal-mitigations. protected static @NonNull File saferOpenFile(@NonNull String path, @NonNull String expectedDir) throws IllegalArgumentException, IOException { File f = new File(path); String canonicalPath = f.getCanonicalPath(); From 4be758b2e5853dc6a938b59a5f392b52e5187d1d Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Tue, 26 Nov 2024 13:18:17 -0800 Subject: [PATCH 11/26] test name --- .../packages/file_selector_android/FileUtilsTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java index f26090253ac9..ed3b76da7322 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java @@ -6,6 +6,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -197,13 +198,13 @@ public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithUnknownTyp } @Test - public void getFileExtension_throwsIllegalArgumentExceptionForFileInWrongDirectory() { + public void getPathFromCopyOfFileFromUri_sanitizesPathIndirection() { Uri uri = Uri.parse(MockMaliciousContentProvider.PNG_URI); Robolectric.buildContentProvider(MockMaliciousContentProvider.class).create("dummy"); shadowContentResolver.registerInputStream( uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); - System.out.println(path); + assertNotNull(path); assertTrue(path.endsWith("_bar.png")); } From 7b5897e4d5c5893854ba7ff875a74b87af432845 Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Tue, 26 Nov 2024 13:19:35 -0800 Subject: [PATCH 12/26] remove new assert; not relevant to this pr --- .../dev/flutter/packages/file_selector_android/FileUtils.java | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index e6fa15aa36d6..7bd3843f0bad 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -140,7 +140,6 @@ public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @Non try (OutputStream outputStream = new FileOutputStream(outputFile)) { - assert inputStream != null; copy(inputStream, outputStream); return outputFile.getPath(); } From 52324b7208366daef86cabf45c7a520d67fdcc18 Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Tue, 26 Nov 2024 13:44:52 -0800 Subject: [PATCH 13/26] pubspec+changelog --- packages/file_selector/file_selector_android/CHANGELOG.md | 4 ++++ packages/file_selector/file_selector_android/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/file_selector/file_selector_android/CHANGELOG.md b/packages/file_selector/file_selector_android/CHANGELOG.md index ca3067927f53..534c75f58bb3 100644 --- a/packages/file_selector/file_selector_android/CHANGELOG.md +++ b/packages/file_selector/file_selector_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.1+12 + +* Fixes a security issue related to improperly trusting filenames provided by a `ContentProvider`. + ## 0.5.1+11 * Bumps androidx.annotation:annotation from 1.9.0 to 1.9.1. diff --git a/packages/file_selector/file_selector_android/pubspec.yaml b/packages/file_selector/file_selector_android/pubspec.yaml index 76087f641971..e70379fc114d 100644 --- a/packages/file_selector/file_selector_android/pubspec.yaml +++ b/packages/file_selector/file_selector_android/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_android description: Android implementation of the file_selector package. repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.5.1+11 +version: 0.5.1+12 environment: sdk: ^3.5.0 From 88717fe60ec1128a5fd6f674d197a2d5ba88025e Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 27 Nov 2024 09:48:04 -0600 Subject: [PATCH 14/26] formatting --- .../file_selector_android/FileUtils.java | 20 +++++++++++------- .../file_selector_android/FileUtilsTest.java | 21 ++++++++++++------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 7bd3843f0bad..252c17f69c44 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -99,9 +99,10 @@ public static String getPathFromUri(@NonNull Context context, @NonNull Uri uri) * file name if possible. * *

If the filename contains path indirection or separators (.. or /), the end file name will be - * the segment after the final separator, with indirection replaced by underscores. - * E.g. "example/../..file.png" -> "_file.png". - * See: Improperly trusting ContentProvider-provided filename. + * the segment after the final separator, with indirection replaced by underscores. E.g. + * "example/../..file.png" -> "_file.png". See: Improperly + * trusting ContentProvider-provided filename. * *

Each file is placed in its own directory to avoid conflicts according to the following * scheme: {cacheDir}/{randomUuid}/{fileName} @@ -138,7 +139,6 @@ public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @Non String filePath = new File(targetDirectory, fileName).getPath(); File outputFile = saferOpenFile(filePath, targetDirectory.getCanonicalPath()); - try (OutputStream outputStream = new FileOutputStream(outputFile)) { copy(inputStream, outputStream); return outputFile.getPath(); @@ -223,7 +223,7 @@ private static String getBaseName(String fileName) { return null; } - String[] badCharacters = new String[] { "..", "/" }; + String[] badCharacters = new String[] {"..", "/"}; String[] segments = displayName.split("/"); String fileName = segments[segments.length - 1]; for (String suspString : badCharacters) { @@ -233,11 +233,17 @@ private static String getBaseName(String fileName) { } // From https://developer.android.com/privacy-and-security/risks/path-traversal#path-traversal-mitigations. - protected static @NonNull File saferOpenFile(@NonNull String path, @NonNull String expectedDir) throws IllegalArgumentException, IOException { + protected static @NonNull File saferOpenFile(@NonNull String path, @NonNull String expectedDir) + throws IllegalArgumentException, IOException { File f = new File(path); String canonicalPath = f.getCanonicalPath(); if (!canonicalPath.startsWith(expectedDir)) { - throw new IllegalArgumentException("Trying to open path outside of the expected directory. File: " + f.getCanonicalPath() + " was expected to be within directory: " + expectedDir + "."); + throw new IllegalArgumentException( + "Trying to open path outside of the expected directory. File: " + + f.getCanonicalPath() + + " was expected to be within directory: " + + expectedDir + + "."); } return f; } diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java index ed3b76da7322..13c7a2e8b924 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java @@ -202,7 +202,7 @@ public void getPathFromCopyOfFileFromUri_sanitizesPathIndirection() { Uri uri = Uri.parse(MockMaliciousContentProvider.PNG_URI); Robolectric.buildContentProvider(MockMaliciousContentProvider.class).create("dummy"); shadowContentResolver.registerInputStream( - uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); + uri, new ByteArrayInputStream("fileStream".getBytes(UTF_8))); String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); assertNotNull(path); assertTrue(path.endsWith("_bar.png")); @@ -279,11 +279,11 @@ public boolean onCreate() { @Nullable @Override public Cursor query( - @NonNull Uri uri, - @Nullable String[] projection, - @Nullable String selection, - @Nullable String[] selectionArgs, - @Nullable String sortOrder) { + @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[] {"foo/../..bar.png"}); return cursor; @@ -302,12 +302,17 @@ public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { } @Override - public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { + 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) { + public int update( + @NonNull Uri uri, + @Nullable ContentValues values, + @Nullable String selection, + @Nullable String[] selectionArgs) { return 0; } } From 314621ca36eb262897fdf5d8641b55ff1235836d Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 27 Nov 2024 10:28:16 -0600 Subject: [PATCH 15/26] Code review feedback, dart doc saferOpenFile and verify no dot dots appear in file path --- .../flutter/packages/file_selector_android/FileUtils.java | 5 ++++- .../packages/file_selector_android/FileUtilsTest.java | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 252c17f69c44..888c82374f97 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -232,7 +232,10 @@ private static String getBaseName(String fileName) { return fileName; } - // From https://developer.android.com/privacy-and-security/risks/path-traversal#path-traversal-mitigations. + /** + * Use with file name sanatization and an non-guessable directory. + * From https://developer.android.com/privacy-and-security/risks/path-traversal#path-traversal-mitigations. + */ protected static @NonNull File saferOpenFile(@NonNull String path, @NonNull String expectedDir) throws IllegalArgumentException, IOException { File f = new File(path); diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java index 13c7a2e8b924..32d1346297bc 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java @@ -206,6 +206,7 @@ public void getPathFromCopyOfFileFromUri_sanitizesPathIndirection() { String path = FileUtils.getPathFromCopyOfFileFromUri(context, uri); assertNotNull(path); assertTrue(path.endsWith("_bar.png")); + assertFalse(path.contains("..")); } private static class MockContentProvider extends ContentProvider { From c3d1d5d968fdc452a7c32b216a03048fba31a326 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 27 Nov 2024 10:44:54 -0600 Subject: [PATCH 16/26] include import --- .../flutter/packages/file_selector_android/FileUtilsTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java index 32d1346297bc..ebe69ef5cb5e 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java @@ -6,6 +6,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; From c9f7c82ec822b767ede4ada121c437693dcb6fab Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Wed, 27 Nov 2024 08:54:06 -0800 Subject: [PATCH 17/26] format --- .../dev/flutter/packages/file_selector_android/FileUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 888c82374f97..f7aa0418e749 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -233,8 +233,8 @@ private static String getBaseName(String fileName) { } /** - * Use with file name sanatization and an non-guessable directory. - * From https://developer.android.com/privacy-and-security/risks/path-traversal#path-traversal-mitigations. + * Use with file name sanatization and an non-guessable directory. From + * https://developer.android.com/privacy-and-security/risks/path-traversal#path-traversal-mitigations. */ protected static @NonNull File saferOpenFile(@NonNull String path, @NonNull String expectedDir) throws IllegalArgumentException, IOException { From 30609b1233618e987740a458180b7fd5a7e0e16c Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Wed, 27 Nov 2024 10:45:00 -0800 Subject: [PATCH 18/26] possible approach for errors --- .../FileSelectorApiImpl.java | 27 ++- .../file_selector_android/FileUtils.java | 19 +- .../GeneratedFileSelectorApi.java | 228 +++++++++++++----- .../file_selector_android/FileUtilsTest.java | 2 +- .../lib/src/file_selector_android.dart | 13 + .../lib/src/file_selector_api.g.dart | 104 +++++--- .../native_illegal_argument_exception.dart | 18 ++ .../pigeons/file_selector_api.dart | 14 ++ 8 files changed, 314 insertions(+), 111 deletions(-) create mode 100644 packages/file_selector/file_selector_android/lib/src/types/native_illegal_argument_exception.dart diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java index 555318b29959..1266d38437ea 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java @@ -357,8 +357,30 @@ GeneratedFileSelectorApi.FileResponse toFileResponse(@NonNull Uri uri) { return null; } - final String uriPath = - FileUtils.getPathFromCopyOfFileFromUri(activityPluginBinding.getActivity(), uri); + String uriPath; + GeneratedFileSelectorApi.FileSelectorNativeException nativeError = null; + + try { + uriPath = FileUtils.getPathFromCopyOfFileFromUri(activityPluginBinding.getActivity(), uri); + } 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. + uriPath = null; + } catch (SecurityException e) { + // Calling `ContentResolver#openInputStream()` has been reported to throw a + // `SecurityException` on some devices in certain circumstances. Instead of crashing, we + // return `null`. + // + // See https://github.com/flutter/flutter/issues/100025 for more details. + uriPath = null; + } catch (IllegalArgumentException e) { + uriPath = null; + nativeError = new GeneratedFileSelectorApi.FileSelectorNativeException.Builder() + .setMessage(e.getMessage() == null ? "" : e.getMessage()) + .setFileSelectorExceptionCode(GeneratedFileSelectorApi.FileSelectorExceptionCode.ILLEGAL_ARGUMENT_EXCEPTION) + .build(); + } return new GeneratedFileSelectorApi.FileResponse.Builder() .setName(name) @@ -366,6 +388,7 @@ GeneratedFileSelectorApi.FileResponse toFileResponse(@NonNull Uri uri) { .setPath(uriPath) .setMimeType(contentResolver.getType(uri)) .setSize(size.longValue()) + .setFileSelectorNativeException(nativeError) .build(); } } diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index f7aa0418e749..87e05d82138a 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -38,6 +38,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.file.InvalidPathException; import java.util.UUID; public class FileUtils { @@ -117,7 +118,7 @@ public static String getPathFromUri(@NonNull Context context, @NonNull Uri uri) * or if a security exception is encountered when opening the input stream to start the copying. */ @Nullable - public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @NonNull Uri uri) { + public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @NonNull Uri uri) throws IOException, SecurityException, IllegalArgumentException { try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { String uuid = UUID.nameUUIDFromBytes(uri.toString().getBytes()).toString(); File targetDirectory = new File(context.getCacheDir(), uuid); @@ -128,7 +129,7 @@ public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @Non if (fileName == null) { if (extension == null) { - throw new IllegalArgumentException("No name nor extension found for file."); + throw new IllegalStateException("No name nor extension found for file."); } else { fileName = "file_selector" + extension; } @@ -143,18 +144,6 @@ public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @Non copy(inputStream, outputStream); return outputFile.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; - } catch (SecurityException e) { - // Calling `ContentResolver#openInputStream()` has been reported to throw a - // `SecurityException` on some devices in certain circumstances. Instead of crashing, we - // return `null`. - // - // See https://github.com/flutter/flutter/issues/100025 for more details. - return null; } } @@ -242,7 +231,7 @@ private static String getBaseName(String fileName) { String canonicalPath = f.getCanonicalPath(); if (!canonicalPath.startsWith(expectedDir)) { throw new IllegalArgumentException( - "Trying to open path outside of the expected directory. File: " + "Trying to open path outside of the expected directory. File: " + f.getCanonicalPath() + " was expected to be within directory: " + expectedDir diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/GeneratedFileSelectorApi.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/GeneratedFileSelectorApi.java index de595ef85847..016b57184c51 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/GeneratedFileSelectorApi.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/GeneratedFileSelectorApi.java @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.6.2), do not edit directly. // See also: https://pub.dev/packages/pigeon package dev.flutter.packages.file_selector_android; @@ -22,7 +22,10 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; /** Generated class from Pigeon. */ @@ -38,7 +41,8 @@ public static class FlutterError extends RuntimeException { /** The error details. Must be a datatype supported by the api codec. */ public final Object details; - public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) { + public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) + { super(message); this.code = code; this.details = details; @@ -57,7 +61,7 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { errorList.add(exception.toString()); errorList.add(exception.getClass().getSimpleName()); errorList.add( - "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); } return errorList; } @@ -66,6 +70,107 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { @Retention(CLASS) @interface CanIgnoreReturnValue {} + public enum FileSelectorExceptionCode { + SECURITY_EXCEPTION(0), + IO_EXCEPTION(1), + ILLEGAL_ARGUMENT_EXCEPTION(2), + ILLEGAL_STATE_EXCEPTION(3); + + final int index; + + FileSelectorExceptionCode(final int index) { + this.index = index; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class FileSelectorNativeException { + private @NonNull FileSelectorExceptionCode fileSelectorExceptionCode; + + public @NonNull FileSelectorExceptionCode getFileSelectorExceptionCode() { + return fileSelectorExceptionCode; + } + + public void setFileSelectorExceptionCode(@NonNull FileSelectorExceptionCode setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"fileSelectorExceptionCode\" is null."); + } + this.fileSelectorExceptionCode = setterArg; + } + + private @NonNull String message; + + public @NonNull String getMessage() { + return message; + } + + public void setMessage(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"message\" is null."); + } + this.message = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + FileSelectorNativeException() {} + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + FileSelectorNativeException that = (FileSelectorNativeException) o; + return fileSelectorExceptionCode.equals(that.fileSelectorExceptionCode) && message.equals(that.message); + } + + @Override + public int hashCode() { + return Objects.hash(fileSelectorExceptionCode, message); + } + + public static final class Builder { + + private @Nullable FileSelectorExceptionCode fileSelectorExceptionCode; + + @CanIgnoreReturnValue + public @NonNull Builder setFileSelectorExceptionCode(@NonNull FileSelectorExceptionCode setterArg) { + this.fileSelectorExceptionCode = setterArg; + return this; + } + + private @Nullable String message; + + @CanIgnoreReturnValue + public @NonNull Builder setMessage(@NonNull String setterArg) { + this.message = setterArg; + return this; + } + + public @NonNull FileSelectorNativeException build() { + FileSelectorNativeException pigeonReturn = new FileSelectorNativeException(); + pigeonReturn.setFileSelectorExceptionCode(fileSelectorExceptionCode); + pigeonReturn.setMessage(message); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList<>(2); + toListResult.add(fileSelectorExceptionCode); + toListResult.add(message); + return toListResult; + } + + static @NonNull FileSelectorNativeException fromList(@NonNull ArrayList pigeonVar_list) { + FileSelectorNativeException pigeonResult = new FileSelectorNativeException(); + Object fileSelectorExceptionCode = pigeonVar_list.get(0); + pigeonResult.setFileSelectorExceptionCode((FileSelectorExceptionCode) fileSelectorExceptionCode); + Object message = pigeonVar_list.get(1); + pigeonResult.setMessage((String) message); + return pigeonResult; + } + } + /** Generated class from Pigeon that represents data sent in messages. */ public static final class FileResponse { private @NonNull String path; @@ -127,28 +232,30 @@ public void setBytes(@NonNull byte[] setterArg) { this.bytes = setterArg; } + private @Nullable FileSelectorNativeException fileSelectorNativeException; + + public @Nullable FileSelectorNativeException getFileSelectorNativeException() { + return fileSelectorNativeException; + } + + public void setFileSelectorNativeException(@Nullable FileSelectorNativeException setterArg) { + this.fileSelectorNativeException = setterArg; + } + /** Constructor is non-public to enforce null safety; use Builder. */ FileResponse() {} @Override public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } FileResponse that = (FileResponse) o; - return path.equals(that.path) - && Objects.equals(mimeType, that.mimeType) - && Objects.equals(name, that.name) - && size.equals(that.size) - && Arrays.equals(bytes, that.bytes); + return path.equals(that.path) && Objects.equals(mimeType, that.mimeType) && Objects.equals(name, that.name) && size.equals(that.size) && Arrays.equals(bytes, that.bytes) && Objects.equals(fileSelectorNativeException, that.fileSelectorNativeException); } @Override public int hashCode() { - int pigeonVar_result = Objects.hash(path, mimeType, name, size); + int pigeonVar_result = Objects.hash(path, mimeType, name, size, fileSelectorNativeException); pigeonVar_result = 31 * pigeonVar_result + Arrays.hashCode(bytes); return pigeonVar_result; } @@ -195,6 +302,14 @@ public static final class Builder { return this; } + private @Nullable FileSelectorNativeException fileSelectorNativeException; + + @CanIgnoreReturnValue + public @NonNull Builder setFileSelectorNativeException(@Nullable FileSelectorNativeException setterArg) { + this.fileSelectorNativeException = setterArg; + return this; + } + public @NonNull FileResponse build() { FileResponse pigeonReturn = new FileResponse(); pigeonReturn.setPath(path); @@ -202,18 +317,20 @@ public static final class Builder { pigeonReturn.setName(name); pigeonReturn.setSize(size); pigeonReturn.setBytes(bytes); + pigeonReturn.setFileSelectorNativeException(fileSelectorNativeException); return pigeonReturn; } } @NonNull ArrayList toList() { - ArrayList toListResult = new ArrayList<>(5); + ArrayList toListResult = new ArrayList<>(6); toListResult.add(path); toListResult.add(mimeType); toListResult.add(name); toListResult.add(size); toListResult.add(bytes); + toListResult.add(fileSelectorNativeException); return toListResult; } @@ -229,6 +346,8 @@ ArrayList toList() { pigeonResult.setSize((Long) size); Object bytes = pigeonVar_list.get(4); pigeonResult.setBytes((byte[]) bytes); + Object fileSelectorNativeException = pigeonVar_list.get(5); + pigeonResult.setFileSelectorNativeException((FileSelectorNativeException) fileSelectorNativeException); return pigeonResult; } } @@ -266,12 +385,8 @@ public void setExtensions(@NonNull List setterArg) { @Override public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } FileTypes that = (FileTypes) o; return mimeTypes.equals(that.mimeTypes) && extensions.equals(that.extensions); } @@ -333,9 +448,15 @@ private PigeonCodec() {} @Override protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte) 129: - return FileResponse.fromList((ArrayList) readValue(buffer)); + case (byte) 129: { + Object value = readValue(buffer); + return value == null ? null : FileSelectorExceptionCode.values()[((Long) value).intValue()]; + } case (byte) 130: + return FileSelectorNativeException.fromList((ArrayList) readValue(buffer)); + case (byte) 131: + return FileResponse.fromList((ArrayList) readValue(buffer)); + case (byte) 132: return FileTypes.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -344,11 +465,17 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { @Override protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { - if (value instanceof FileResponse) { + if (value instanceof FileSelectorExceptionCode) { stream.write(129); + writeValue(stream, value == null ? null : ((FileSelectorExceptionCode) value).index); + } else if (value instanceof FileSelectorNativeException) { + stream.write(130); + writeValue(stream, ((FileSelectorNativeException) value).toList()); + } else if (value instanceof FileResponse) { + stream.write(131); writeValue(stream, ((FileResponse) value).toList()); } else if (value instanceof FileTypes) { - stream.write(130); + stream.write(132); writeValue(stream, ((FileTypes) value).toList()); } else { super.writeValue(stream, value); @@ -356,6 +483,7 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { } } + /** Asynchronous error handling return type for non-nullable API method returns. */ public interface Result { /** Success case callback method for handling returns. */ @@ -383,57 +511,41 @@ public interface VoidResult { /** * An API to call to native code to select files or directories. * - *

Generated interface from Pigeon that represents a handler of messages from Flutter. + * Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface FileSelectorApi { /** * Opens a file dialog for loading files and returns a file path. * - *

Returns `null` if user cancels the operation. + * Returns `null` if user cancels the operation. */ - void openFile( - @Nullable String initialDirectory, - @NonNull FileTypes allowedTypes, - @NonNull NullableResult result); + void openFile(@Nullable String initialDirectory, @NonNull FileTypes allowedTypes, @NonNull NullableResult result); /** - * Opens a file dialog for loading files and returns a list of file responses chosen by the - * user. + * Opens a file dialog for loading files and returns a list of file responses + * chosen by the user. */ - void openFiles( - @Nullable String initialDirectory, - @NonNull FileTypes allowedTypes, - @NonNull Result> result); + void openFiles(@Nullable String initialDirectory, @NonNull FileTypes allowedTypes, @NonNull Result> result); /** * Opens a file dialog for loading directories and returns a directory path. * - *

Returns `null` if user cancels the operation. + * Returns `null` if user cancels the operation. */ - void getDirectoryPath( - @Nullable String initialDirectory, @NonNull NullableResult result); + void getDirectoryPath(@Nullable String initialDirectory, @NonNull NullableResult result); /** The codec used by FileSelectorApi. */ static @NonNull MessageCodec getCodec() { return PigeonCodec.INSTANCE; } - /** - * Sets up an instance of `FileSelectorApi` to handle messages through the `binaryMessenger`. - */ + /**Sets up an instance of `FileSelectorApi` to handle messages through the `binaryMessenger`. */ static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable FileSelectorApi api) { setUp(binaryMessenger, "", api); } - - static void setUp( - @NonNull BinaryMessenger binaryMessenger, - @NonNull String messageChannelSuffix, - @Nullable FileSelectorApi api) { + static void setUp(@NonNull BinaryMessenger binaryMessenger, @NonNull String messageChannelSuffix, @Nullable FileSelectorApi api) { messageChannelSuffix = messageChannelSuffix.isEmpty() ? "" : "." + messageChannelSuffix; { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.file_selector_android.FileSelectorApi.openFile" - + messageChannelSuffix, - getCodec()); + binaryMessenger, "dev.flutter.pigeon.file_selector_android.FileSelectorApi.openFile" + messageChannelSuffix, getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -463,10 +575,7 @@ public void error(Throwable error) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.file_selector_android.FileSelectorApi.openFiles" - + messageChannelSuffix, - getCodec()); + binaryMessenger, "dev.flutter.pigeon.file_selector_android.FileSelectorApi.openFiles" + messageChannelSuffix, getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -496,10 +605,7 @@ public void error(Throwable error) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.file_selector_android.FileSelectorApi.getDirectoryPath" - + messageChannelSuffix, - getCodec()); + binaryMessenger, "dev.flutter.pigeon.file_selector_android.FileSelectorApi.getDirectoryPath" + messageChannelSuffix, getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java index ebe69ef5cb5e..5c051ba172fc 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java @@ -199,7 +199,7 @@ public void getPathFromCopyOfFileFromUri_returnsExpectedPathForUriWithUnknownTyp } @Test - public void getPathFromCopyOfFileFromUri_sanitizesPathIndirection() { + public void getPathFromCopyOfFileFromUri_sanitizesPathIndirection() throws IOException { Uri uri = Uri.parse(MockMaliciousContentProvider.PNG_URI); Robolectric.buildContentProvider(MockMaliciousContentProvider.class).create("dummy"); shadowContentResolver.registerInputStream( diff --git a/packages/file_selector/file_selector_android/lib/src/file_selector_android.dart b/packages/file_selector/file_selector_android/lib/src/file_selector_android.dart index bc11695265c2..17451964270b 100644 --- a/packages/file_selector/file_selector_android/lib/src/file_selector_android.dart +++ b/packages/file_selector/file_selector_android/lib/src/file_selector_android.dart @@ -8,6 +8,7 @@ import 'package:file_selector_platform_interface/file_selector_platform_interfac import 'package:flutter/cupertino.dart'; import 'file_selector_api.g.dart'; +import 'types/native_illegal_argument_exception.dart'; /// An implementation of [FileSelectorPlatform] for Android. class FileSelectorAndroid extends FileSelectorPlatform { @@ -56,6 +57,9 @@ class FileSelectorAndroid extends FileSelectorPlatform { } XFile _xFileFromFileResponse(FileResponse file) { + if (file.fileSelectorNativeException != null) { + _resolveErrorCodeAndThrow(file.fileSelectorNativeException!); + } return XFile.fromData( file.bytes, // Note: The name parameter is not used by XFile. The XFile.name returns @@ -95,4 +99,13 @@ class FileSelectorAndroid extends FileSelectorPlatform { extensions: extensions.toList(), ); } + + void _resolveErrorCodeAndThrow(FileSelectorNativeException fileSelectorNativeException) { + switch (fileSelectorNativeException.fileSelectorExceptionCode) { + case FileSelectorExceptionCode.illegalArgumentException: + throw NativeIllegalArgumentException(fileSelectorNativeException.message); + case (FileSelectorExceptionCode.illegalStateException || FileSelectorExceptionCode.ioException || FileSelectorExceptionCode.securityException): + // unused for now + } + } } diff --git a/packages/file_selector/file_selector_android/lib/src/file_selector_api.g.dart b/packages/file_selector/file_selector_android/lib/src/file_selector_api.g.dart index 558ab5f557f4..a464fdff4fb2 100644 --- a/packages/file_selector/file_selector_android/lib/src/file_selector_api.g.dart +++ b/packages/file_selector/file_selector_android/lib/src/file_selector_api.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v22.4.2), do not edit directly. +// Autogenerated from Pigeon (v22.6.2), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -18,6 +18,39 @@ PlatformException _createConnectionError(String channelName) { ); } +enum FileSelectorExceptionCode { + securityException, + ioException, + illegalArgumentException, + illegalStateException, +} + +class FileSelectorNativeException { + FileSelectorNativeException({ + required this.fileSelectorExceptionCode, + required this.message, + }); + + FileSelectorExceptionCode fileSelectorExceptionCode; + + String message; + + Object encode() { + return [ + fileSelectorExceptionCode, + message, + ]; + } + + static FileSelectorNativeException decode(Object result) { + result as List; + return FileSelectorNativeException( + fileSelectorExceptionCode: result[0]! as FileSelectorExceptionCode, + message: result[1]! as String, + ); + } +} + class FileResponse { FileResponse({ required this.path, @@ -25,6 +58,7 @@ class FileResponse { this.name, required this.size, required this.bytes, + this.fileSelectorNativeException, }); String path; @@ -37,6 +71,8 @@ class FileResponse { Uint8List bytes; + FileSelectorNativeException? fileSelectorNativeException; + Object encode() { return [ path, @@ -44,6 +80,7 @@ class FileResponse { name, size, bytes, + fileSelectorNativeException, ]; } @@ -55,6 +92,7 @@ class FileResponse { name: result[2] as String?, size: result[3]! as int, bytes: result[4]! as Uint8List, + fileSelectorNativeException: result[5] as FileSelectorNativeException?, ); } } @@ -85,6 +123,7 @@ class FileTypes { } } + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -92,12 +131,18 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is FileResponse) { + } else if (value is FileSelectorExceptionCode) { buffer.putUint8(129); - writeValue(buffer, value.encode()); - } else if (value is FileTypes) { + writeValue(buffer, value.index); + } else if (value is FileSelectorNativeException) { buffer.putUint8(130); writeValue(buffer, value.encode()); + } else if (value is FileResponse) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is FileTypes) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -106,9 +151,14 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: + final int? value = readValue(buffer) as int?; + return value == null ? null : FileSelectorExceptionCode.values[value]; + case 130: + return FileSelectorNativeException.decode(readValue(buffer)!); + case 131: return FileResponse.decode(readValue(buffer)!); - case 130: + case 132: return FileTypes.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -121,11 +171,9 @@ class FileSelectorApi { /// Constructor for [FileSelectorApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - FileSelectorApi( - {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + FileSelectorApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = - messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -135,18 +183,15 @@ class FileSelectorApi { /// Opens a file dialog for loading files and returns a file path. /// /// Returns `null` if user cancels the operation. - Future openFile( - String? initialDirectory, FileTypes allowedTypes) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.file_selector_android.FileSelectorApi.openFile$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + Future openFile(String? initialDirectory, FileTypes allowedTypes) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.file_selector_android.FileSelectorApi.openFile$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final List? pigeonVar_replyList = await pigeonVar_channel - .send([initialDirectory, allowedTypes]) as List?; + final List? pigeonVar_replyList = + await pigeonVar_channel.send([initialDirectory, allowedTypes]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -162,18 +207,15 @@ class FileSelectorApi { /// Opens a file dialog for loading files and returns a list of file responses /// chosen by the user. - Future> openFiles( - String? initialDirectory, FileTypes allowedTypes) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.file_selector_android.FileSelectorApi.openFiles$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + Future> openFiles(String? initialDirectory, FileTypes allowedTypes) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.file_selector_android.FileSelectorApi.openFiles$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final List? pigeonVar_replyList = await pigeonVar_channel - .send([initialDirectory, allowedTypes]) as List?; + final List? pigeonVar_replyList = + await pigeonVar_channel.send([initialDirectory, allowedTypes]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -196,16 +238,14 @@ class FileSelectorApi { /// /// Returns `null` if user cancels the operation. Future getDirectoryPath(String? initialDirectory) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.file_selector_android.FileSelectorApi.getDirectoryPath$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( + final String pigeonVar_channelName = 'dev.flutter.pigeon.file_selector_android.FileSelectorApi.getDirectoryPath$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final List? pigeonVar_replyList = await pigeonVar_channel - .send([initialDirectory]) as List?; + final List? pigeonVar_replyList = + await pigeonVar_channel.send([initialDirectory]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { diff --git a/packages/file_selector/file_selector_android/lib/src/types/native_illegal_argument_exception.dart b/packages/file_selector/file_selector_android/lib/src/types/native_illegal_argument_exception.dart new file mode 100644 index 000000000000..46eaec9e7cda --- /dev/null +++ b/packages/file_selector/file_selector_android/lib/src/types/native_illegal_argument_exception.dart @@ -0,0 +1,18 @@ +// 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. + +/// A representation of a Java IllegalArgumentException in dart. +class NativeIllegalArgumentException implements Exception { + + /// Creates a [NativeIllegalArgumentException]. + NativeIllegalArgumentException(this.message); + + /// The message provided by the native error. + final String message; + + @override + String toString() { + return 'NativeIllegalArgumentException($message)'; + } +} diff --git a/packages/file_selector/file_selector_android/pigeons/file_selector_api.dart b/packages/file_selector/file_selector_android/pigeons/file_selector_api.dart index 3a6c5228a4b4..477c03109bae 100644 --- a/packages/file_selector/file_selector_android/pigeons/file_selector_api.dart +++ b/packages/file_selector/file_selector_android/pigeons/file_selector_api.dart @@ -16,12 +16,26 @@ import 'package:pigeon/pigeon.dart'; copyrightHeader: 'pigeons/copyright.txt', ), ) + +enum FileSelectorExceptionCode { + securityException, // unused + ioException, // unused + illegalArgumentException, + illegalStateException, //unused +} + +class FileSelectorNativeException implements Exception { + late final FileSelectorExceptionCode fileSelectorExceptionCode; + late final String message; +} + class FileResponse { late final String path; late final String? mimeType; late final String? name; late final int size; late final Uint8List bytes; + late final FileSelectorNativeException? fileSelectorNativeException; } class FileTypes { From 7332b4fbc9e7b927106d8a557a7369d33699b978 Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Wed, 27 Nov 2024 12:02:05 -0800 Subject: [PATCH 19/26] test 1/2 --- .../FileSelectorApiImpl.java | 4 +- .../file_selector_android/FileUtils.java | 2 +- .../FileSelectorAndroidPluginTest.java | 60 +++++++++++++++++++ .../file_selector_android/FileUtilsTest.java | 16 ----- 4 files changed, 64 insertions(+), 18 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java index 1266d38437ea..b7caaf116c3e 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java @@ -4,6 +4,8 @@ package dev.flutter.packages.file_selector_android; +import static dev.flutter.packages.file_selector_android.FileUtils.FILE_SELECTOR_EXCEPTION_PLACEHOLDER_PATH; + import android.annotation.TargetApi; import android.app.Activity; import android.content.ClipData; @@ -375,7 +377,7 @@ GeneratedFileSelectorApi.FileResponse toFileResponse(@NonNull Uri uri) { // See https://github.com/flutter/flutter/issues/100025 for more details. uriPath = null; } catch (IllegalArgumentException e) { - uriPath = null; + uriPath = FILE_SELECTOR_EXCEPTION_PLACEHOLDER_PATH; nativeError = new GeneratedFileSelectorApi.FileSelectorNativeException.Builder() .setMessage(e.getMessage() == null ? "" : e.getMessage()) .setFileSelectorExceptionCode(GeneratedFileSelectorApi.FileSelectorExceptionCode.ILLEGAL_ARGUMENT_EXCEPTION) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 87e05d82138a..7ead6b508342 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -38,13 +38,13 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.nio.file.InvalidPathException; import java.util.UUID; public class FileUtils { /** URI authority that represents access to external storage providers. */ public static final String EXTERNAL_DOCUMENT_AUTHORITY = "com.android.externalstorage.documents"; + public static final String FILE_SELECTOR_EXCEPTION_PLACEHOLDER_PATH = "FILE_SELECTOR_EXCEPTION"; /** * Retrieves path of directory represented by the specified {@code Uri}. diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java index 00533c282640..74d533323f7e 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java @@ -5,6 +5,7 @@ package dev.flutter.packages.file_selector_android; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -212,6 +213,65 @@ public void openFilesReturnsSuccessfully() throws FileNotFoundException { } } + @SuppressWarnings({"rawtypes", "unchecked"}) + @Test + public void openFileReturnsNullUriPath_whenSecurityExceptionInGetPathFromCopyOfFileFromUri() throws FileNotFoundException { + // TODO(gmackall) implement this + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Test + public void openFileReturnsNativeException_whenIllegalArgumentExceptionInGetPathFromCopyOfFileFromUri() throws FileNotFoundException { + try (MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { + final ContentResolver mockContentResolver = mock(ContentResolver.class); + + final Uri mockUri = mock(Uri.class); + mockedFileUtils + .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri))) + .thenThrow(IllegalArgumentException.class); + mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); + + when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); + when(mockObjectFactory.newDataInputStream(any())).thenReturn(mock(DataInputStream.class)); + when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); + when(mockActivityBinding.getActivity()).thenReturn(mockActivity); + final FileSelectorApiImpl fileSelectorApi = + new FileSelectorApiImpl( + mockActivityBinding, + mockObjectFactory, + (version) -> Build.VERSION.SDK_INT >= version); + + final GeneratedFileSelectorApi.NullableResult mockResult = + mock(GeneratedFileSelectorApi.NullableResult.class); + fileSelectorApi.openFile( + null, + new GeneratedFileSelectorApi.FileTypes.Builder() + .setMimeTypes(Collections.emptyList()) + .setExtensions(Collections.emptyList()) + .build(), + mockResult); + verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); + + verify(mockActivity).startActivityForResult(mockIntent, 221); + + final ArgumentCaptor listenerArgumentCaptor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); + + final Intent resultMockIntent = mock(Intent.class); + when(resultMockIntent.getData()).thenReturn(mockUri); + listenerArgumentCaptor.getValue().onActivityResult(221, Activity.RESULT_OK, resultMockIntent); + + final ArgumentCaptor fileCaptor = + ArgumentCaptor.forClass(GeneratedFileSelectorApi.FileResponse.class); + verify(mockResult).success(fileCaptor.capture()); + + final GeneratedFileSelectorApi.FileResponse file = fileCaptor.getValue(); + assertNotNull(file.getFileSelectorNativeException()); + assertEquals(file.getPath(), FileUtils.FILE_SELECTOR_EXCEPTION_PLACEHOLDER_PATH); + } + } + @SuppressWarnings({"rawtypes", "unchecked"}) @Test public void getDirectoryPathReturnsSuccessfully() { diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java index 5c051ba172fc..4ff8c4604a63 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java @@ -127,22 +127,6 @@ public void getPathFromCopyOfFileFromUri_returnsPathWithContent() throws IOExcep assertEquals("fileStream", fileStream); } - @Test - public void getPathFromCopyOfFileFromUri_returnsNullPathWhenSecurityExceptionThrown() - throws IOException { - Uri uri = Uri.parse("content://dummy/dummy.png"); - - ContentResolver mockContentResolver = mock(ContentResolver.class); - when(mockContentResolver.openInputStream(any(Uri.class))).thenThrow(SecurityException.class); - - Context mockContext = mock(Context.class); - when(mockContext.getContentResolver()).thenReturn(mockContentResolver); - - String path = FileUtils.getPathFromCopyOfFileFromUri(mockContext, uri); - - assertNull(path); - } - @Test public void getFileExtension_returnsExpectedFileExtension() throws IOException { Uri uri = MockContentProvider.TXT_URI; From 2f881f3d47c36b3b2a4d9bebbc66e06964e72dc6 Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Wed, 27 Nov 2024 12:06:14 -0800 Subject: [PATCH 20/26] format --- .../FileSelectorApiImpl.java | 6 +- .../file_selector_android/FileUtils.java | 6 +- .../GeneratedFileSelectorApi.java | 117 ++++++++++++------ .../FileSelectorAndroidPluginTest.java | 37 +++--- .../file_selector_android/FileUtilsTest.java | 4 - .../lib/src/file_selector_android.dart | 12 +- .../lib/src/file_selector_api.g.dart | 59 +++++---- .../native_illegal_argument_exception.dart | 1 - .../pigeons/file_selector_api.dart | 1 - 9 files changed, 150 insertions(+), 93 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java index b7caaf116c3e..37d38c2f5083 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileSelectorApiImpl.java @@ -378,9 +378,11 @@ GeneratedFileSelectorApi.FileResponse toFileResponse(@NonNull Uri uri) { uriPath = null; } catch (IllegalArgumentException e) { uriPath = FILE_SELECTOR_EXCEPTION_PLACEHOLDER_PATH; - nativeError = new GeneratedFileSelectorApi.FileSelectorNativeException.Builder() + nativeError = + new GeneratedFileSelectorApi.FileSelectorNativeException.Builder() .setMessage(e.getMessage() == null ? "" : e.getMessage()) - .setFileSelectorExceptionCode(GeneratedFileSelectorApi.FileSelectorExceptionCode.ILLEGAL_ARGUMENT_EXCEPTION) + .setFileSelectorExceptionCode( + GeneratedFileSelectorApi.FileSelectorExceptionCode.ILLEGAL_ARGUMENT_EXCEPTION) .build(); } diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java index 7ead6b508342..e3cd81239eb6 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/FileUtils.java @@ -44,6 +44,7 @@ public class FileUtils { /** URI authority that represents access to external storage providers. */ public static final String EXTERNAL_DOCUMENT_AUTHORITY = "com.android.externalstorage.documents"; + public static final String FILE_SELECTOR_EXCEPTION_PLACEHOLDER_PATH = "FILE_SELECTOR_EXCEPTION"; /** @@ -118,7 +119,8 @@ public static String getPathFromUri(@NonNull Context context, @NonNull Uri uri) * or if a security exception is encountered when opening the input stream to start the copying. */ @Nullable - public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @NonNull Uri uri) throws IOException, SecurityException, IllegalArgumentException { + public static String getPathFromCopyOfFileFromUri(@NonNull Context context, @NonNull Uri uri) + throws IOException, SecurityException, IllegalArgumentException { try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { String uuid = UUID.nameUUIDFromBytes(uri.toString().getBytes()).toString(); File targetDirectory = new File(context.getCacheDir(), uuid); @@ -231,7 +233,7 @@ private static String getBaseName(String fileName) { String canonicalPath = f.getCanonicalPath(); if (!canonicalPath.startsWith(expectedDir)) { throw new IllegalArgumentException( - "Trying to open path outside of the expected directory. File: " + "Trying to open path outside of the expected directory. File: " + f.getCanonicalPath() + " was expected to be within directory: " + expectedDir diff --git a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/GeneratedFileSelectorApi.java b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/GeneratedFileSelectorApi.java index 016b57184c51..ee744f5ec8d5 100644 --- a/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/GeneratedFileSelectorApi.java +++ b/packages/file_selector/file_selector_android/android/src/main/java/dev/flutter/packages/file_selector_android/GeneratedFileSelectorApi.java @@ -22,10 +22,7 @@ import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Objects; /** Generated class from Pigeon. */ @@ -41,8 +38,7 @@ public static class FlutterError extends RuntimeException { /** The error details. Must be a datatype supported by the api codec. */ public final Object details; - public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) - { + public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) { super(message); this.code = code; this.details = details; @@ -61,7 +57,7 @@ protected static ArrayList wrapError(@NonNull Throwable exception) { errorList.add(exception.toString()); errorList.add(exception.getClass().getSimpleName()); errorList.add( - "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); } return errorList; } @@ -116,10 +112,15 @@ public void setMessage(@NonNull String setterArg) { @Override public boolean equals(Object o) { - if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { return false; } + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } FileSelectorNativeException that = (FileSelectorNativeException) o; - return fileSelectorExceptionCode.equals(that.fileSelectorExceptionCode) && message.equals(that.message); + return fileSelectorExceptionCode.equals(that.fileSelectorExceptionCode) + && message.equals(that.message); } @Override @@ -132,7 +133,8 @@ public static final class Builder { private @Nullable FileSelectorExceptionCode fileSelectorExceptionCode; @CanIgnoreReturnValue - public @NonNull Builder setFileSelectorExceptionCode(@NonNull FileSelectorExceptionCode setterArg) { + public @NonNull Builder setFileSelectorExceptionCode( + @NonNull FileSelectorExceptionCode setterArg) { this.fileSelectorExceptionCode = setterArg; return this; } @@ -161,10 +163,12 @@ ArrayList toList() { return toListResult; } - static @NonNull FileSelectorNativeException fromList(@NonNull ArrayList pigeonVar_list) { + static @NonNull FileSelectorNativeException fromList( + @NonNull ArrayList pigeonVar_list) { FileSelectorNativeException pigeonResult = new FileSelectorNativeException(); Object fileSelectorExceptionCode = pigeonVar_list.get(0); - pigeonResult.setFileSelectorExceptionCode((FileSelectorExceptionCode) fileSelectorExceptionCode); + pigeonResult.setFileSelectorExceptionCode( + (FileSelectorExceptionCode) fileSelectorExceptionCode); Object message = pigeonVar_list.get(1); pigeonResult.setMessage((String) message); return pigeonResult; @@ -247,10 +251,19 @@ public void setFileSelectorNativeException(@Nullable FileSelectorNativeException @Override public boolean equals(Object o) { - if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { return false; } + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } FileResponse that = (FileResponse) o; - return path.equals(that.path) && Objects.equals(mimeType, that.mimeType) && Objects.equals(name, that.name) && size.equals(that.size) && Arrays.equals(bytes, that.bytes) && Objects.equals(fileSelectorNativeException, that.fileSelectorNativeException); + return path.equals(that.path) + && Objects.equals(mimeType, that.mimeType) + && Objects.equals(name, that.name) + && size.equals(that.size) + && Arrays.equals(bytes, that.bytes) + && Objects.equals(fileSelectorNativeException, that.fileSelectorNativeException); } @Override @@ -305,7 +318,8 @@ public static final class Builder { private @Nullable FileSelectorNativeException fileSelectorNativeException; @CanIgnoreReturnValue - public @NonNull Builder setFileSelectorNativeException(@Nullable FileSelectorNativeException setterArg) { + public @NonNull Builder setFileSelectorNativeException( + @Nullable FileSelectorNativeException setterArg) { this.fileSelectorNativeException = setterArg; return this; } @@ -347,7 +361,8 @@ ArrayList toList() { Object bytes = pigeonVar_list.get(4); pigeonResult.setBytes((byte[]) bytes); Object fileSelectorNativeException = pigeonVar_list.get(5); - pigeonResult.setFileSelectorNativeException((FileSelectorNativeException) fileSelectorNativeException); + pigeonResult.setFileSelectorNativeException( + (FileSelectorNativeException) fileSelectorNativeException); return pigeonResult; } } @@ -385,8 +400,12 @@ public void setExtensions(@NonNull List setterArg) { @Override public boolean equals(Object o) { - if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { return false; } + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } FileTypes that = (FileTypes) o; return mimeTypes.equals(that.mimeTypes) && extensions.equals(that.extensions); } @@ -448,10 +467,13 @@ private PigeonCodec() {} @Override protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { - case (byte) 129: { - Object value = readValue(buffer); - return value == null ? null : FileSelectorExceptionCode.values()[((Long) value).intValue()]; - } + case (byte) 129: + { + Object value = readValue(buffer); + return value == null + ? null + : FileSelectorExceptionCode.values()[((Long) value).intValue()]; + } case (byte) 130: return FileSelectorNativeException.fromList((ArrayList) readValue(buffer)); case (byte) 131: @@ -483,7 +505,6 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { } } - /** Asynchronous error handling return type for non-nullable API method returns. */ public interface Result { /** Success case callback method for handling returns. */ @@ -511,41 +532,57 @@ public interface VoidResult { /** * An API to call to native code to select files or directories. * - * Generated interface from Pigeon that represents a handler of messages from Flutter. + *

Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface FileSelectorApi { /** * Opens a file dialog for loading files and returns a file path. * - * Returns `null` if user cancels the operation. + *

Returns `null` if user cancels the operation. */ - void openFile(@Nullable String initialDirectory, @NonNull FileTypes allowedTypes, @NonNull NullableResult result); + void openFile( + @Nullable String initialDirectory, + @NonNull FileTypes allowedTypes, + @NonNull NullableResult result); /** - * Opens a file dialog for loading files and returns a list of file responses - * chosen by the user. + * Opens a file dialog for loading files and returns a list of file responses chosen by the + * user. */ - void openFiles(@Nullable String initialDirectory, @NonNull FileTypes allowedTypes, @NonNull Result> result); + void openFiles( + @Nullable String initialDirectory, + @NonNull FileTypes allowedTypes, + @NonNull Result> result); /** * Opens a file dialog for loading directories and returns a directory path. * - * Returns `null` if user cancels the operation. + *

Returns `null` if user cancels the operation. */ - void getDirectoryPath(@Nullable String initialDirectory, @NonNull NullableResult result); + void getDirectoryPath( + @Nullable String initialDirectory, @NonNull NullableResult result); /** The codec used by FileSelectorApi. */ static @NonNull MessageCodec getCodec() { return PigeonCodec.INSTANCE; } - /**Sets up an instance of `FileSelectorApi` to handle messages through the `binaryMessenger`. */ + /** + * Sets up an instance of `FileSelectorApi` to handle messages through the `binaryMessenger`. + */ static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable FileSelectorApi api) { setUp(binaryMessenger, "", api); } - static void setUp(@NonNull BinaryMessenger binaryMessenger, @NonNull String messageChannelSuffix, @Nullable FileSelectorApi api) { + + static void setUp( + @NonNull BinaryMessenger binaryMessenger, + @NonNull String messageChannelSuffix, + @Nullable FileSelectorApi api) { messageChannelSuffix = messageChannelSuffix.isEmpty() ? "" : "." + messageChannelSuffix; { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.file_selector_android.FileSelectorApi.openFile" + messageChannelSuffix, getCodec()); + binaryMessenger, + "dev.flutter.pigeon.file_selector_android.FileSelectorApi.openFile" + + messageChannelSuffix, + getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -575,7 +612,10 @@ public void error(Throwable error) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.file_selector_android.FileSelectorApi.openFiles" + messageChannelSuffix, getCodec()); + binaryMessenger, + "dev.flutter.pigeon.file_selector_android.FileSelectorApi.openFiles" + + messageChannelSuffix, + getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { @@ -605,7 +645,10 @@ public void error(Throwable error) { { BasicMessageChannel channel = new BasicMessageChannel<>( - binaryMessenger, "dev.flutter.pigeon.file_selector_android.FileSelectorApi.getDirectoryPath" + messageChannelSuffix, getCodec()); + binaryMessenger, + "dev.flutter.pigeon.file_selector_android.FileSelectorApi.getDirectoryPath" + + messageChannelSuffix, + getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java index 74d533323f7e..7b4926c95c10 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java @@ -215,20 +215,23 @@ public void openFilesReturnsSuccessfully() throws FileNotFoundException { @SuppressWarnings({"rawtypes", "unchecked"}) @Test - public void openFileReturnsNullUriPath_whenSecurityExceptionInGetPathFromCopyOfFileFromUri() throws FileNotFoundException { + public void openFileReturnsNullUriPath_whenSecurityExceptionInGetPathFromCopyOfFileFromUri() + throws FileNotFoundException { // TODO(gmackall) implement this } @SuppressWarnings({"rawtypes", "unchecked"}) @Test - public void openFileReturnsNativeException_whenIllegalArgumentExceptionInGetPathFromCopyOfFileFromUri() throws FileNotFoundException { + public void + openFileReturnsNativeException_whenIllegalArgumentExceptionInGetPathFromCopyOfFileFromUri() + throws FileNotFoundException { try (MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { final ContentResolver mockContentResolver = mock(ContentResolver.class); final Uri mockUri = mock(Uri.class); mockedFileUtils - .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri))) - .thenThrow(IllegalArgumentException.class); + .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri))) + .thenThrow(IllegalArgumentException.class); mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); @@ -236,26 +239,26 @@ public void openFileReturnsNativeException_whenIllegalArgumentExceptionInGetPath when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); when(mockActivityBinding.getActivity()).thenReturn(mockActivity); final FileSelectorApiImpl fileSelectorApi = - new FileSelectorApiImpl( - mockActivityBinding, - mockObjectFactory, - (version) -> Build.VERSION.SDK_INT >= version); + new FileSelectorApiImpl( + mockActivityBinding, + mockObjectFactory, + (version) -> Build.VERSION.SDK_INT >= version); final GeneratedFileSelectorApi.NullableResult mockResult = - mock(GeneratedFileSelectorApi.NullableResult.class); + mock(GeneratedFileSelectorApi.NullableResult.class); fileSelectorApi.openFile( - null, - new GeneratedFileSelectorApi.FileTypes.Builder() - .setMimeTypes(Collections.emptyList()) - .setExtensions(Collections.emptyList()) - .build(), - mockResult); + null, + new GeneratedFileSelectorApi.FileTypes.Builder() + .setMimeTypes(Collections.emptyList()) + .setExtensions(Collections.emptyList()) + .build(), + mockResult); verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); verify(mockActivity).startActivityForResult(mockIntent, 221); final ArgumentCaptor listenerArgumentCaptor = - ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); final Intent resultMockIntent = mock(Intent.class); @@ -263,7 +266,7 @@ public void openFileReturnsNativeException_whenIllegalArgumentExceptionInGetPath listenerArgumentCaptor.getValue().onActivityResult(221, Activity.RESULT_OK, resultMockIntent); final ArgumentCaptor fileCaptor = - ArgumentCaptor.forClass(GeneratedFileSelectorApi.FileResponse.class); + ArgumentCaptor.forClass(GeneratedFileSelectorApi.FileResponse.class); verify(mockResult).success(fileCaptor.capture()); final GeneratedFileSelectorApi.FileResponse file = fileCaptor.getValue(); diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java index 4ff8c4604a63..a6d0f573a536 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileUtilsTest.java @@ -8,14 +8,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import android.content.ContentProvider; diff --git a/packages/file_selector/file_selector_android/lib/src/file_selector_android.dart b/packages/file_selector/file_selector_android/lib/src/file_selector_android.dart index 17451964270b..3d8f7b1d1932 100644 --- a/packages/file_selector/file_selector_android/lib/src/file_selector_android.dart +++ b/packages/file_selector/file_selector_android/lib/src/file_selector_android.dart @@ -100,12 +100,16 @@ class FileSelectorAndroid extends FileSelectorPlatform { ); } - void _resolveErrorCodeAndThrow(FileSelectorNativeException fileSelectorNativeException) { + void _resolveErrorCodeAndThrow( + FileSelectorNativeException fileSelectorNativeException) { switch (fileSelectorNativeException.fileSelectorExceptionCode) { case FileSelectorExceptionCode.illegalArgumentException: - throw NativeIllegalArgumentException(fileSelectorNativeException.message); - case (FileSelectorExceptionCode.illegalStateException || FileSelectorExceptionCode.ioException || FileSelectorExceptionCode.securityException): - // unused for now + throw NativeIllegalArgumentException( + fileSelectorNativeException.message); + case (FileSelectorExceptionCode.illegalStateException || + FileSelectorExceptionCode.ioException || + FileSelectorExceptionCode.securityException): + // unused for now } } } diff --git a/packages/file_selector/file_selector_android/lib/src/file_selector_api.g.dart b/packages/file_selector/file_selector_android/lib/src/file_selector_api.g.dart index a464fdff4fb2..73341425bf5d 100644 --- a/packages/file_selector/file_selector_android/lib/src/file_selector_api.g.dart +++ b/packages/file_selector/file_selector_android/lib/src/file_selector_api.g.dart @@ -123,7 +123,6 @@ class FileTypes { } } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -131,16 +130,16 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is FileSelectorExceptionCode) { + } else if (value is FileSelectorExceptionCode) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is FileSelectorNativeException) { + } else if (value is FileSelectorNativeException) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is FileResponse) { + } else if (value is FileResponse) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is FileTypes) { + } else if (value is FileTypes) { buffer.putUint8(132); writeValue(buffer, value.encode()); } else { @@ -151,14 +150,14 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: final int? value = readValue(buffer) as int?; return value == null ? null : FileSelectorExceptionCode.values[value]; - case 130: + case 130: return FileSelectorNativeException.decode(readValue(buffer)!); - case 131: + case 131: return FileResponse.decode(readValue(buffer)!); - case 132: + case 132: return FileTypes.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -171,9 +170,11 @@ class FileSelectorApi { /// Constructor for [FileSelectorApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - FileSelectorApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + FileSelectorApi( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + pigeonVar_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); @@ -183,15 +184,18 @@ class FileSelectorApi { /// Opens a file dialog for loading files and returns a file path. /// /// Returns `null` if user cancels the operation. - Future openFile(String? initialDirectory, FileTypes allowedTypes) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.file_selector_android.FileSelectorApi.openFile$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + Future openFile( + String? initialDirectory, FileTypes allowedTypes) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.file_selector_android.FileSelectorApi.openFile$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final List? pigeonVar_replyList = - await pigeonVar_channel.send([initialDirectory, allowedTypes]) as List?; + final List? pigeonVar_replyList = await pigeonVar_channel + .send([initialDirectory, allowedTypes]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -207,15 +211,18 @@ class FileSelectorApi { /// Opens a file dialog for loading files and returns a list of file responses /// chosen by the user. - Future> openFiles(String? initialDirectory, FileTypes allowedTypes) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.file_selector_android.FileSelectorApi.openFiles$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + Future> openFiles( + String? initialDirectory, FileTypes allowedTypes) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.file_selector_android.FileSelectorApi.openFiles$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final List? pigeonVar_replyList = - await pigeonVar_channel.send([initialDirectory, allowedTypes]) as List?; + final List? pigeonVar_replyList = await pigeonVar_channel + .send([initialDirectory, allowedTypes]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -238,14 +245,16 @@ class FileSelectorApi { /// /// Returns `null` if user cancels the operation. Future getDirectoryPath(String? initialDirectory) async { - final String pigeonVar_channelName = 'dev.flutter.pigeon.file_selector_android.FileSelectorApi.getDirectoryPath$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + final String pigeonVar_channelName = + 'dev.flutter.pigeon.file_selector_android.FileSelectorApi.getDirectoryPath$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final List? pigeonVar_replyList = - await pigeonVar_channel.send([initialDirectory]) as List?; + final List? pigeonVar_replyList = await pigeonVar_channel + .send([initialDirectory]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { diff --git a/packages/file_selector/file_selector_android/lib/src/types/native_illegal_argument_exception.dart b/packages/file_selector/file_selector_android/lib/src/types/native_illegal_argument_exception.dart index 46eaec9e7cda..b22be8042b6e 100644 --- a/packages/file_selector/file_selector_android/lib/src/types/native_illegal_argument_exception.dart +++ b/packages/file_selector/file_selector_android/lib/src/types/native_illegal_argument_exception.dart @@ -4,7 +4,6 @@ /// A representation of a Java IllegalArgumentException in dart. class NativeIllegalArgumentException implements Exception { - /// Creates a [NativeIllegalArgumentException]. NativeIllegalArgumentException(this.message); diff --git a/packages/file_selector/file_selector_android/pigeons/file_selector_api.dart b/packages/file_selector/file_selector_android/pigeons/file_selector_api.dart index 477c03109bae..409eec5959ca 100644 --- a/packages/file_selector/file_selector_android/pigeons/file_selector_api.dart +++ b/packages/file_selector/file_selector_android/pigeons/file_selector_api.dart @@ -16,7 +16,6 @@ import 'package:pigeon/pigeon.dart'; copyrightHeader: 'pigeons/copyright.txt', ), ) - enum FileSelectorExceptionCode { securityException, // unused ioException, // unused From 131afe69a4acde9fcddd86340d7c86d8cdde1ee7 Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Wed, 27 Nov 2024 12:12:25 -0800 Subject: [PATCH 21/26] test for other case --- .../FileSelectorAndroidPluginTest.java | 67 ++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java index 7b4926c95c10..b52801fd0b31 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java @@ -215,9 +215,9 @@ public void openFilesReturnsSuccessfully() throws FileNotFoundException { @SuppressWarnings({"rawtypes", "unchecked"}) @Test - public void openFileReturnsNullUriPath_whenSecurityExceptionInGetPathFromCopyOfFileFromUri() + public void openFileThrowsIllegalStateException_whenSecurityExceptionInGetPathFromCopyOfFileFromUri() throws FileNotFoundException { - // TODO(gmackall) implement this + } @SuppressWarnings({"rawtypes", "unchecked"}) @@ -275,6 +275,69 @@ public void openFileReturnsNullUriPath_whenSecurityExceptionInGetPathFromCopyOfF } } + @SuppressWarnings({"rawtypes", "unchecked"}) + @Test + public void openFilesReturnsNativeException_whenIllegalArgumentExceptionInGetPathFromCopyOfFileFromUri() throws FileNotFoundException { + try (MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { + + final ContentResolver mockContentResolver = mock(ContentResolver.class); + + final Uri mockUri = mock(Uri.class); + final String mockUriPath = "some/path/"; + mockedFileUtils + .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri))) + .thenThrow(IllegalArgumentException.class); + mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); + + when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); + when(mockObjectFactory.newDataInputStream(any())).thenReturn(mock(DataInputStream.class)); + when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); + when(mockActivityBinding.getActivity()).thenReturn(mockActivity); + final FileSelectorApiImpl fileSelectorApi = + new FileSelectorApiImpl( + mockActivityBinding, + mockObjectFactory, + (version) -> Build.VERSION.SDK_INT >= version); + + final GeneratedFileSelectorApi.Result mockResult = + mock(GeneratedFileSelectorApi.Result.class); + fileSelectorApi.openFiles( + null, + new GeneratedFileSelectorApi.FileTypes.Builder() + .setMimeTypes(Collections.emptyList()) + .setExtensions(Collections.emptyList()) + .build(), + mockResult); + verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); + verify(mockIntent).putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + + verify(mockActivity).startActivityForResult(mockIntent, 222); + + final ArgumentCaptor listenerArgumentCaptor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); + + final Intent resultMockIntent = mock(Intent.class); + final ClipData mockClipData = mock(ClipData.class); + when(mockClipData.getItemCount()).thenReturn(1); + + final ClipData.Item mockClipDataItem = mock(ClipData.Item.class); + when(mockClipDataItem.getUri()).thenReturn(mockUri); + when(mockClipData.getItemAt(0)).thenReturn(mockClipDataItem); + + when(resultMockIntent.getClipData()).thenReturn(mockClipData); + + listenerArgumentCaptor.getValue().onActivityResult(222, Activity.RESULT_OK, resultMockIntent); + + final ArgumentCaptor fileListCaptor = ArgumentCaptor.forClass(List.class); + verify(mockResult).success(fileListCaptor.capture()); + + final List fileList = fileListCaptor.getValue(); + assertEquals(fileList.get(0).getPath(), FileUtils.FILE_SELECTOR_EXCEPTION_PLACEHOLDER_PATH); + assertNotNull(fileList.get(0).getFileSelectorNativeException()); + } + } + @SuppressWarnings({"rawtypes", "unchecked"}) @Test public void getDirectoryPathReturnsSuccessfully() { From 7f61bbc6d4c032e9011f54ee629991ab94a5c168 Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Wed, 27 Nov 2024 12:18:16 -0800 Subject: [PATCH 22/26] new test --- .../FileSelectorAndroidPluginTest.java | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java index b52801fd0b31..57facc5ee3f3 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -213,11 +214,75 @@ public void openFilesReturnsSuccessfully() throws FileNotFoundException { } } + + // TODO(gmackall) link to bug to fix this. @SuppressWarnings({"rawtypes", "unchecked"}) @Test public void openFileThrowsIllegalStateException_whenSecurityExceptionInGetPathFromCopyOfFileFromUri() throws FileNotFoundException { + try (MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { + + final ContentResolver mockContentResolver = mock(ContentResolver.class); + + final Uri mockUri = mock(Uri.class); + final String mockUriPath = "some/path/"; + mockedFileUtils + .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri))) + .thenThrow(SecurityException.class); + mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); + + final Uri mockUri2 = mock(Uri.class); + final String mockUri2Path = "some/other/path/"; + mockedFileUtils + .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri2))) + .thenAnswer((Answer) invocation -> mockUri2Path); + mockContentResolver(mockContentResolver, mockUri2, "filename2", 40, "image/jpg"); + + when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); + when(mockObjectFactory.newDataInputStream(any())).thenReturn(mock(DataInputStream.class)); + when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); + when(mockActivityBinding.getActivity()).thenReturn(mockActivity); + final FileSelectorApiImpl fileSelectorApi = + new FileSelectorApiImpl( + mockActivityBinding, + mockObjectFactory, + (version) -> Build.VERSION.SDK_INT >= version); + + final GeneratedFileSelectorApi.Result mockResult = + mock(GeneratedFileSelectorApi.Result.class); + fileSelectorApi.openFiles( + null, + new GeneratedFileSelectorApi.FileTypes.Builder() + .setMimeTypes(Collections.emptyList()) + .setExtensions(Collections.emptyList()) + .build(), + mockResult); + verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); + verify(mockIntent).putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + + verify(mockActivity).startActivityForResult(mockIntent, 222); + + final ArgumentCaptor listenerArgumentCaptor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); + + final Intent resultMockIntent = mock(Intent.class); + final ClipData mockClipData = mock(ClipData.class); + when(mockClipData.getItemCount()).thenReturn(2); + + final ClipData.Item mockClipDataItem = mock(ClipData.Item.class); + when(mockClipDataItem.getUri()).thenReturn(mockUri); + when(mockClipData.getItemAt(0)).thenReturn(mockClipDataItem); + + final ClipData.Item mockClipDataItem2 = mock(ClipData.Item.class); + when(mockClipDataItem2.getUri()).thenReturn(mockUri2); + when(mockClipData.getItemAt(1)).thenReturn(mockClipDataItem2); + + when(resultMockIntent.getClipData()).thenReturn(mockClipData); + + assertThrows(IllegalStateException.class, () -> listenerArgumentCaptor.getValue().onActivityResult(222, Activity.RESULT_OK, resultMockIntent)); + } } @SuppressWarnings({"rawtypes", "unchecked"}) From 996eba06749bc2e83550d0e81e7b0bd8ecbc029e Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Wed, 27 Nov 2024 12:18:31 -0800 Subject: [PATCH 23/26] format --- .../FileSelectorAndroidPluginTest.java | 77 ++++++++++--------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java index 57facc5ee3f3..de4b8f008c19 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java @@ -214,12 +214,12 @@ public void openFilesReturnsSuccessfully() throws FileNotFoundException { } } - // TODO(gmackall) link to bug to fix this. @SuppressWarnings({"rawtypes", "unchecked"}) @Test - public void openFileThrowsIllegalStateException_whenSecurityExceptionInGetPathFromCopyOfFileFromUri() - throws FileNotFoundException { + public void + openFileThrowsIllegalStateException_whenSecurityExceptionInGetPathFromCopyOfFileFromUri() + throws FileNotFoundException { try (MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { @@ -228,15 +228,15 @@ public void openFileThrowsIllegalStateException_whenSecurityExceptionInGetPathFr final Uri mockUri = mock(Uri.class); final String mockUriPath = "some/path/"; mockedFileUtils - .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri))) - .thenThrow(SecurityException.class); + .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri))) + .thenThrow(SecurityException.class); mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); final Uri mockUri2 = mock(Uri.class); final String mockUri2Path = "some/other/path/"; mockedFileUtils - .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri2))) - .thenAnswer((Answer) invocation -> mockUri2Path); + .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri2))) + .thenAnswer((Answer) invocation -> mockUri2Path); mockContentResolver(mockContentResolver, mockUri2, "filename2", 40, "image/jpg"); when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); @@ -244,27 +244,27 @@ public void openFileThrowsIllegalStateException_whenSecurityExceptionInGetPathFr when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); when(mockActivityBinding.getActivity()).thenReturn(mockActivity); final FileSelectorApiImpl fileSelectorApi = - new FileSelectorApiImpl( - mockActivityBinding, - mockObjectFactory, - (version) -> Build.VERSION.SDK_INT >= version); + new FileSelectorApiImpl( + mockActivityBinding, + mockObjectFactory, + (version) -> Build.VERSION.SDK_INT >= version); final GeneratedFileSelectorApi.Result mockResult = - mock(GeneratedFileSelectorApi.Result.class); + mock(GeneratedFileSelectorApi.Result.class); fileSelectorApi.openFiles( - null, - new GeneratedFileSelectorApi.FileTypes.Builder() - .setMimeTypes(Collections.emptyList()) - .setExtensions(Collections.emptyList()) - .build(), - mockResult); + null, + new GeneratedFileSelectorApi.FileTypes.Builder() + .setMimeTypes(Collections.emptyList()) + .setExtensions(Collections.emptyList()) + .build(), + mockResult); verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); verify(mockIntent).putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); verify(mockActivity).startActivityForResult(mockIntent, 222); final ArgumentCaptor listenerArgumentCaptor = - ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); final Intent resultMockIntent = mock(Intent.class); @@ -281,7 +281,12 @@ public void openFileThrowsIllegalStateException_whenSecurityExceptionInGetPathFr when(resultMockIntent.getClipData()).thenReturn(mockClipData); - assertThrows(IllegalStateException.class, () -> listenerArgumentCaptor.getValue().onActivityResult(222, Activity.RESULT_OK, resultMockIntent)); + assertThrows( + IllegalStateException.class, + () -> + listenerArgumentCaptor + .getValue() + .onActivityResult(222, Activity.RESULT_OK, resultMockIntent)); } } @@ -342,7 +347,9 @@ public void openFileThrowsIllegalStateException_whenSecurityExceptionInGetPathFr @SuppressWarnings({"rawtypes", "unchecked"}) @Test - public void openFilesReturnsNativeException_whenIllegalArgumentExceptionInGetPathFromCopyOfFileFromUri() throws FileNotFoundException { + public void + openFilesReturnsNativeException_whenIllegalArgumentExceptionInGetPathFromCopyOfFileFromUri() + throws FileNotFoundException { try (MockedStatic mockedFileUtils = mockStatic(FileUtils.class)) { final ContentResolver mockContentResolver = mock(ContentResolver.class); @@ -350,8 +357,8 @@ public void openFilesReturnsNativeException_whenIllegalArgumentExceptionInGetPat final Uri mockUri = mock(Uri.class); final String mockUriPath = "some/path/"; mockedFileUtils - .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri))) - .thenThrow(IllegalArgumentException.class); + .when(() -> FileUtils.getPathFromCopyOfFileFromUri(any(Context.class), eq(mockUri))) + .thenThrow(IllegalArgumentException.class); mockContentResolver(mockContentResolver, mockUri, "filename", 30, "text/plain"); when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); @@ -359,27 +366,27 @@ public void openFilesReturnsNativeException_whenIllegalArgumentExceptionInGetPat when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); when(mockActivityBinding.getActivity()).thenReturn(mockActivity); final FileSelectorApiImpl fileSelectorApi = - new FileSelectorApiImpl( - mockActivityBinding, - mockObjectFactory, - (version) -> Build.VERSION.SDK_INT >= version); + new FileSelectorApiImpl( + mockActivityBinding, + mockObjectFactory, + (version) -> Build.VERSION.SDK_INT >= version); final GeneratedFileSelectorApi.Result mockResult = - mock(GeneratedFileSelectorApi.Result.class); + mock(GeneratedFileSelectorApi.Result.class); fileSelectorApi.openFiles( - null, - new GeneratedFileSelectorApi.FileTypes.Builder() - .setMimeTypes(Collections.emptyList()) - .setExtensions(Collections.emptyList()) - .build(), - mockResult); + null, + new GeneratedFileSelectorApi.FileTypes.Builder() + .setMimeTypes(Collections.emptyList()) + .setExtensions(Collections.emptyList()) + .build(), + mockResult); verify(mockIntent).addCategory(Intent.CATEGORY_OPENABLE); verify(mockIntent).putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); verify(mockActivity).startActivityForResult(mockIntent, 222); final ArgumentCaptor listenerArgumentCaptor = - ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); verify(mockActivityBinding).addActivityResultListener(listenerArgumentCaptor.capture()); final Intent resultMockIntent = mock(Intent.class); From 18cb046f46946042e419ff2f94f6bac669d9b86a Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Wed, 27 Nov 2024 12:21:11 -0800 Subject: [PATCH 24/26] doc --- .../lib/src/file_selector_android.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/file_selector/file_selector_android/lib/src/file_selector_android.dart b/packages/file_selector/file_selector_android/lib/src/file_selector_android.dart index 3d8f7b1d1932..8f9e46be017b 100644 --- a/packages/file_selector/file_selector_android/lib/src/file_selector_android.dart +++ b/packages/file_selector/file_selector_android/lib/src/file_selector_android.dart @@ -58,7 +58,7 @@ class FileSelectorAndroid extends FileSelectorPlatform { XFile _xFileFromFileResponse(FileResponse file) { if (file.fileSelectorNativeException != null) { - _resolveErrorCodeAndThrow(file.fileSelectorNativeException!); + _resolveErrorCodeAndMaybeThrow(file.fileSelectorNativeException!); } return XFile.fromData( file.bytes, @@ -100,7 +100,9 @@ class FileSelectorAndroid extends FileSelectorPlatform { ); } - void _resolveErrorCodeAndThrow( + /// Translates a [FileSelectorExceptionCode] to its corresponding error and + /// handles throwing. + void _resolveErrorCodeAndMaybeThrow( FileSelectorNativeException fileSelectorNativeException) { switch (fileSelectorNativeException.fileSelectorExceptionCode) { case FileSelectorExceptionCode.illegalArgumentException: From af479e94118e34fb1bdd74d3f62c6a4474bc7f1b Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Wed, 27 Nov 2024 12:31:34 -0800 Subject: [PATCH 25/26] doc --- .../FileSelectorAndroidPluginTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java index de4b8f008c19..b213e85f05cf 100644 --- a/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java +++ b/packages/file_selector/file_selector_android/android/src/test/java/dev/flutter/packages/file_selector_android/FileSelectorAndroidPluginTest.java @@ -214,7 +214,11 @@ public void openFilesReturnsSuccessfully() throws FileNotFoundException { } } - // TODO(gmackall) link to bug to fix this. + // This test was created when error handling was moved from FileUtils.java to FileSelectorApiImpl.java + // in https://github.com/flutter/packages/pull/8184, so as to maintain the existing test. + // The behavior is actually an error case and should be fixed, + // see: https://github.com/flutter/flutter/issues/159568. + // Remove when fixed! @SuppressWarnings({"rawtypes", "unchecked"}) @Test public void From 3c4d7fbea0a06356ae1669b9181bdfe5da5ea716 Mon Sep 17 00:00:00 2001 From: Gray Mackall Date: Wed, 27 Nov 2024 12:43:34 -0800 Subject: [PATCH 26/26] export the type --- .../file_selector_android/lib/file_selector_android.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/file_selector/file_selector_android/lib/file_selector_android.dart b/packages/file_selector/file_selector_android/lib/file_selector_android.dart index c86f2da2fa13..6775131236e2 100644 --- a/packages/file_selector/file_selector_android/lib/file_selector_android.dart +++ b/packages/file_selector/file_selector_android/lib/file_selector_android.dart @@ -3,3 +3,4 @@ // found in the LICENSE file. export 'src/file_selector_android.dart'; +export 'src/types/native_illegal_argument_exception.dart';