Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/file_selector/file_selector_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<PluginRegistry.ActivityResultListener> 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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion packages/file_selector/file_selector_android/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading