diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md index 1d14021f1536..2d096e2f07c4 100644 --- a/packages/image_picker/image_picker_android/CHANGELOG.md +++ b/packages/image_picker/image_picker_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.8+1 + +* Fixes NullPointerException on pre-Android 13 devices when using Android Photo Picker to pick image or video. + ## 0.8.8 * Adds additional category II and III exif tags to be copied during photo resize. diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index 685534ec6ab8..cf45f1ba3f0d 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -7,6 +7,7 @@ import android.Manifest; import android.app.Activity; import android.content.ActivityNotFoundException; +import android.content.ClipData; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -630,7 +631,21 @@ public boolean onActivityResult( private void handleChooseImageResult(int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK && data != null) { - String path = fileUtils.getPathFromUri(activity, data.getData()); + Uri uri = data.getData(); + // On several pre-Android 13 devices using Android Photo Picker, the Uri from getData() could be null. + if (uri == null) { + ClipData clipData = data.getClipData(); + if (clipData != null && clipData.getItemCount() == 1) { + uri = clipData.getItemAt(0).getUri(); + } + } + // If there's no valid Uri, return an error + if (uri == null) { + finishWithError("no_valid_image_uri", "Cannot find the selected image."); + return; + } + + String path = fileUtils.getPathFromUri(activity, uri); handleImageResult(path, false); return; } @@ -701,7 +716,21 @@ private void handleChooseMultiImageResult(int resultCode, Intent intent) { private void handleChooseVideoResult(int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK && data != null) { - String path = fileUtils.getPathFromUri(activity, data.getData()); + Uri uri = data.getData(); + // On several pre-Android 13 devices using Android Photo Picker, the Uri from getData() could be null. + if (uri == null) { + ClipData clipData = data.getClipData(); + if (clipData != null && clipData.getItemCount() == 1) { + uri = clipData.getItemAt(0).getUri(); + } + } + // If there's no valid Uri, return an error + if (uri == null) { + finishWithError("no_valid_video_uri", "Cannot find the selected video."); + return; + } + + String path = fileUtils.getPathFromUri(activity, uri); handleVideoResult(path); return; } diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java index 73ee5a0f0d49..92c988cd6bf3 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java @@ -23,6 +23,7 @@ import android.Manifest; import android.app.Activity; import android.content.ActivityNotFoundException; +import android.content.ClipData; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -437,6 +438,114 @@ public void onActivityResult_whenPickFromGalleryCanceled_storesNothingInCache() verifyNoMoreInteractions(mockResult); } + @Test + public void + onActivityResult_whenImagePickedFromGallery_nullUriFromGetData_andNoResizeNeeded_finishesWithImagePath() { + setupMockClipData(); + + when(mockIntent.getData()).thenReturn(null); + + Mockito.doAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }) + .when(mockExecutor) + .execute(any(Runnable.class)); + ImagePickerDelegate delegate = + createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null); + + delegate.onActivityResult( + ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, Activity.RESULT_OK, mockIntent); + + @SuppressWarnings("unchecked") + ArgumentCaptor> pathListCapture = ArgumentCaptor.forClass(List.class); + verify(mockResult).success(pathListCapture.capture()); + assertEquals("originalPath", pathListCapture.getValue().get(0)); + verifyNoMoreInteractions(mockResult); + } + + @Test + public void + onActivityResult_whenVideoPickedFromGallery_nullUriFromGetData_finishesWithVideoPath() { + setupMockClipData(); + + when(mockIntent.getData()).thenReturn(null); + + Mockito.doAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }) + .when(mockExecutor) + .execute(any(Runnable.class)); + ImagePickerDelegate delegate = + createDelegateWithPendingResultAndOptions(null, DEFAULT_VIDEO_OPTIONS); + + delegate.onActivityResult( + ImagePickerDelegate.REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY, Activity.RESULT_OK, mockIntent); + + @SuppressWarnings("unchecked") + ArgumentCaptor> pathListCapture = ArgumentCaptor.forClass(List.class); + verify(mockResult).success(pathListCapture.capture()); + assertEquals("pathFromUri", pathListCapture.getValue().get(0)); + verifyNoMoreInteractions(mockResult); + } + + @Test + public void + onActivityResult_whenImagePickedFromGallery_nullUri_andNoResizeNeeded_finishesWithNoValidUriError() { + setupMockClipDataNullUri(); + + when(mockIntent.getData()).thenReturn(null); + + Mockito.doAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }) + .when(mockExecutor) + .execute(any(Runnable.class)); + ImagePickerDelegate delegate = + createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null); + + delegate.onActivityResult( + ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, Activity.RESULT_OK, mockIntent); + + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); + verify(mockResult).error(errorCaptor.capture()); + assertEquals("no_valid_image_uri", errorCaptor.getValue().code); + assertEquals("Cannot find the selected image.", errorCaptor.getValue().getMessage()); + verifyNoMoreInteractions(mockResult); + } + + @Test + public void onActivityResult_whenVideoPickedFromGallery_nullUri_finishesWithNoValidUriError() { + setupMockClipDataNullUri(); + + when(mockIntent.getData()).thenReturn(null); + + Mockito.doAnswer( + invocation -> { + ((Runnable) invocation.getArgument(0)).run(); + return null; + }) + .when(mockExecutor) + .execute(any(Runnable.class)); + ImagePickerDelegate delegate = + createDelegateWithPendingResultAndOptions(null, DEFAULT_VIDEO_OPTIONS); + + delegate.onActivityResult( + ImagePickerDelegate.REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY, Activity.RESULT_OK, mockIntent); + + @SuppressWarnings("unchecked") + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); + verify(mockResult).error(errorCaptor.capture()); + assertEquals("no_valid_video_uri", errorCaptor.getValue().code); + assertEquals("Cannot find the selected video.", errorCaptor.getValue().getMessage()); + verifyNoMoreInteractions(mockResult); + } + @Test public void onActivityResult_whenImagePickedFromGallery_andNoResizeNeeded_storesImageInCache() { Mockito.doAnswer( @@ -743,4 +852,23 @@ private void verifyFinishedWithAlreadyActiveError() { assertEquals("already_active", errorCaptor.getValue().code); assertEquals("Image picker is already active", errorCaptor.getValue().getMessage()); } + + private void setupMockClipData() { + ClipData mockClipData = mock(ClipData.class); + ClipData.Item mockItem = mock(ClipData.Item.class); + Uri mockUri = mock(Uri.class); + when(mockItem.getUri()).thenReturn(mockUri); + when(mockClipData.getItemCount()).thenReturn(1); + when(mockClipData.getItemAt(0)).thenReturn(mockItem); + when(mockIntent.getClipData()).thenReturn(mockClipData); + } + + private void setupMockClipDataNullUri() { + ClipData mockClipData = mock(ClipData.class); + ClipData.Item mockItem = mock(ClipData.Item.class); + when(mockItem.getUri()).thenReturn(null); + when(mockClipData.getItemCount()).thenReturn(1); + when(mockClipData.getItemAt(0)).thenReturn(mockItem); + when(mockIntent.getClipData()).thenReturn(mockClipData); + } } diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml index 434005967530..8232869c93ba 100755 --- a/packages/image_picker/image_picker_android/pubspec.yaml +++ b/packages/image_picker/image_picker_android/pubspec.yaml @@ -3,7 +3,7 @@ description: Android implementation of the image_picker plugin. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.8 +version: 0.8.8+1 environment: sdk: ">=2.19.0 <4.0.0"