From 7f6f8781544d6d5a09d64b9a613e499ef351b47a Mon Sep 17 00:00:00 2001 From: Matt Boetger Date: Tue, 1 Sep 2026 21:52:41 +0000 Subject: [PATCH] [file_selector_android] Handle a null stream from openInputStream --- .../file_selector_android/CHANGELOG.md | 5 ++ .../FileSelectorApiImpl.java | 7 +++ .../file_selector_android/FileUtils.java | 5 ++ .../FileSelectorAndroidPluginTest.java | 55 +++++++++++++++++++ .../file_selector_android/FileUtilsTest.java | 14 +++++ .../file_selector_android/pubspec.yaml | 2 +- 6 files changed, 87 insertions(+), 1 deletion(-) diff --git a/packages/file_selector/file_selector_android/CHANGELOG.md b/packages/file_selector/file_selector_android/CHANGELOG.md index 7ee8bc15fab0..219e55927af0 100644 --- a/packages/file_selector/file_selector_android/CHANGELOG.md +++ b/packages/file_selector/file_selector_android/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.5.2+11 + +* Fixes a crash when the content provider returns no stream for a selected + file; the pick now completes with an error instead. + ## 0.5.2+10 * Updates pigeon dev_dependency to ^27.3.2 for analyzer 14 compatibility. 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 017001c41506..87b2f42254f6 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 @@ -351,6 +351,13 @@ FileResponse toFileResponse(@NonNull Uri uri) { final byte[] bytes = new byte[size]; try (InputStream inputStream = contentResolver.openInputStream(uri)) { + if (inputStream == null) { + // `ContentResolver#openInputStream()` returns null when the provider cannot serve the + // file (for example after the provider crashed). Reading it would throw a + // NullPointerException on the activity-result callback, i.e. the main thread. + Log.w(TAG, "The content provider returned no stream for the selected file."); + return null; + } final DataInputStream dataInputStream = objectFactory.newDataInputStream(inputStream); dataInputStream.readFully(bytes); } catch (IOException 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 9b5e7e77d707..7f78c80bfb9e 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 @@ -124,6 +124,11 @@ public static String getPathFromUri(@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)) { + if (inputStream == null) { + // The provider cannot serve the file (see `FileSelectorApiImpl#toFileResponse`); surface + // it as the IO failure it is instead of dereferencing the null stream in `copy()`. + throw new IOException("The content provider returned no stream for the file."); + } String uuid = UUID.nameUUIDFromBytes(uri.toString().getBytes()).toString(); File targetDirectory = new File(context.getCacheDir(), uuid); targetDirectory.mkdir(); 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 c0710b28867d..c7a127786e03 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 @@ -12,6 +12,7 @@ import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -288,6 +289,60 @@ public void openFilesCompletesWithError_whenSecurityExceptionInGetPathFromCopyOf // Regression test for https://github.com/flutter/flutter/issues/159568: the // single-file `openFile` path must likewise surface a copy failure to Dart // instead of crashing. + @SuppressWarnings({"rawtypes", "unchecked"}) + @Test + public void openFileCompletesWithError_whenProviderReturnsNullStream() + throws FileNotFoundException { + final ContentResolver mockContentResolver = mock(ContentResolver.class); + final Uri mockUri = mock(Uri.class); + + final Cursor mockCursor = mock(Cursor.class); + when(mockCursor.moveToFirst()).thenReturn(true); + when(mockCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)).thenReturn(0); + when(mockCursor.getString(0)).thenReturn("filename"); + when(mockCursor.getColumnIndex(OpenableColumns.SIZE)).thenReturn(1); + when(mockCursor.isNull(1)).thenReturn(false); + when(mockCursor.getInt(1)).thenReturn(30); + when(mockContentResolver.query(mockUri, null, null, null, null, null)).thenReturn(mockCursor); + // A provider that cannot serve the file answers the open with null; previously this reached + // `DataInputStream#readFully` and threw a NullPointerException on the main thread. + when(mockContentResolver.openInputStream(mockUri)).thenReturn(null); + + when(mockObjectFactory.newIntent(Intent.ACTION_OPEN_DOCUMENT)).thenReturn(mockIntent); + when(mockActivity.getContentResolver()).thenReturn(mockContentResolver); + when(mockActivityBinding.getActivity()).thenReturn(mockActivity); + final FileSelectorApiImpl fileSelectorApi = + new FileSelectorApiImpl( + mockActivityBinding, mockObjectFactory, (version) -> Build.VERSION.SDK_INT >= version); + + final boolean[] callbackCalled = new boolean[1]; + final Throwable[] failure = new Throwable[1]; + fileSelectorApi.openFile( + null, + new FileTypes(Collections.emptyList(), Collections.emptyList()), + ResultCompat.asCompatCallback( + (reply) -> { + callbackCalled[0] = true; + failure[0] = reply.exceptionOrNull(); + return null; + })); + + 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); + + assertTrue(callbackCalled[0]); + assertNotNull(failure[0]); + assertTrue(failure[0].getMessage().contains("Failed to read file")); + verify(mockObjectFactory, never()).newDataInputStream(any()); + } + @SuppressWarnings({"rawtypes", "unchecked"}) @Test public void openFileCompletesWithError_whenSecurityExceptionInGetPathFromCopyOfFileFromUri() 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 59784dc9c3cb..128281f92e0c 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 @@ -10,8 +10,10 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +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; @@ -68,6 +70,18 @@ public void before() { } } + @Test + public void getPathFromCopyOfFileFromUri_throwsIOExceptionForNullStream() throws IOException { + Uri uri = Uri.parse("content://dummy/dummy.txt"); + + ContentResolver mockContentResolver = mock(ContentResolver.class); + when(mockContentResolver.openInputStream(uri)).thenReturn(null); + Context mockContext = mock(Context.class); + when(mockContext.getContentResolver()).thenReturn(mockContentResolver); + + assertThrows(IOException.class, () -> FileUtils.getPathFromCopyOfFileFromUri(mockContext, uri)); + } + @Test public void getPathFromUri_returnsExpectedPathForExternalDocumentUri() { // Uri that represents Documents/test directory on device: diff --git a/packages/file_selector/file_selector_android/pubspec.yaml b/packages/file_selector/file_selector_android/pubspec.yaml index 43c581c15123..009bffc61a8a 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.2+10 +version: 0.5.2+11 environment: sdk: ^3.12.0