[image_picker] Move I/O operations to a separate thread (#3506)

Many I/O operations in the `image_picker` are currently carried out on the main thread (at least on Android). This blocks the UI (main) thread, freezing the UI and sometimes even causing an ANR dialog to pop up.

With these PR, many of said I/O operations now run on a separate thread. Specifically, it executes all code that is run in response to a picking result on a separate thread. So when an image is picked, for example, the callback will be ran outside of the UI thread. This change was rather easy, as said code was already making use of async paradigms.

The I/O operations carried out for caching logic (`ImagePickerDelegate.retrieveLostImage()`) are **not** changed by this PR, as my guess is that this would call for breaking changes to the API, as the result is currently returned synchronously instead.

When running the example app in [StrictMode](https://developer.android.com/reference/android/os/StrictMode) and picking an image, the problem becomes apparent, as it reports disk read/write violations. This was described in [#100966](https://github.com/flutter/flutter/issues/100966). I have verified that StrictMode no longer reports disk read/write violations after the changes. To check the output of StrictMode, I ran the example app with the changes listed [here](https://github.com/math1man/plugins/commit/11f5f8ace392d061b442d38e1237655bd5628f0f).

This PR closes several issues related to ANRs.

* Fixes [#94120](https://github.com/flutter/flutter/issues/94210)
* Fixes [#114080](https://github.com/flutter/flutter/issues/114080)

Also, it partly implements what is requested in [#91393](https://github.com/flutter/flutter/issues/91393).
diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md
index 44902aa..90ae0f4 100644
--- a/packages/image_picker/image_picker_android/CHANGELOG.md
+++ b/packages/image_picker/image_picker_android/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.8.6+10
+
+* Offloads picker result handling to separate thread.
+
 ## 0.8.6+9
 
 * Fixes compatibility with AGP versions older than 4.2.
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 eaee6e8..6aa1181 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
@@ -32,6 +32,8 @@
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 /**
  * A delegate class doing the heavy lifting for the plugin.
@@ -112,6 +114,7 @@
   private final PermissionManager permissionManager;
   private final FileUriResolver fileUriResolver;
   private final FileUtils fileUtils;
+  private final ExecutorService executor;
   private CameraDevice cameraDevice;
 
   interface PermissionManager {
@@ -134,6 +137,7 @@
 
   private Uri pendingCameraMediaUri;
   private @Nullable PendingCallState pendingCallState;
+  private final Object pendingCallStateLock = new Object();
 
   public ImagePickerDelegate(
       final Activity activity,
@@ -185,7 +189,8 @@
                 });
           }
         },
-        new FileUtils());
+        new FileUtils(),
+        Executors.newSingleThreadExecutor());
   }
 
   /**
@@ -203,7 +208,8 @@
       final ImagePickerCache cache,
       final PermissionManager permissionManager,
       final FileUriResolver fileUriResolver,
-      final FileUtils fileUtils) {
+      final FileUtils fileUtils,
+      final ExecutorService executor) {
     this.activity = activity;
     this.externalFilesDirectory = externalFilesDirectory;
     this.imageResizer = imageResizer;
@@ -216,6 +222,7 @@
     this.fileUriResolver = fileUriResolver;
     this.fileUtils = fileUtils;
     this.cache = cache;
+    this.executor = executor;
   }
 
   void setCameraDevice(CameraDevice device) {
@@ -224,19 +231,25 @@
 
   // Save the state of the image picker so it can be retrieved with `retrieveLostImage`.
   void saveStateBeforeResult() {
-    if (pendingCallState == null) {
-      return;
+    ImageSelectionOptions localImageOptions;
+    synchronized (pendingCallStateLock) {
+      if (pendingCallState == null) {
+        return;
+      }
+      localImageOptions = pendingCallState.imageOptions;
     }
 
     cache.saveType(
-        pendingCallState.imageOptions != null
+        localImageOptions != null
             ? ImagePickerCache.CacheType.IMAGE
             : ImagePickerCache.CacheType.VIDEO);
-    if (pendingCallState.imageOptions != null) {
-      cache.saveDimensionWithOutputOptions(pendingCallState.imageOptions);
+    if (localImageOptions != null) {
+      cache.saveDimensionWithOutputOptions(localImageOptions);
     }
-    if (pendingCameraMediaUri != null) {
-      cache.savePendingCameraMediaUriPath(pendingCameraMediaUri);
+
+    final Uri localPendingCameraMediaUri = pendingCameraMediaUri;
+    if (localPendingCameraMediaUri != null) {
+      cache.savePendingCameraMediaUriPath(localPendingCameraMediaUri);
     }
   }
 
@@ -323,10 +336,16 @@
 
   private void launchTakeVideoWithCameraIntent() {
     Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
-    if (pendingCallState != null
-        && pendingCallState.videoOptions != null
-        && pendingCallState.videoOptions.getMaxDurationSeconds() != null) {
-      int maxSeconds = pendingCallState.videoOptions.getMaxDurationSeconds().intValue();
+
+    VideoSelectionOptions localVideoOptions = null;
+    synchronized (pendingCallStateLock) {
+      if (pendingCallState != null) {
+        localVideoOptions = pendingCallState.videoOptions;
+      }
+    }
+
+    if (localVideoOptions != null && localVideoOptions.getMaxDurationSeconds() != null) {
+      int maxSeconds = localVideoOptions.getMaxDurationSeconds().intValue();
       intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, maxSeconds);
     }
     if (cameraDevice == CameraDevice.FRONT) {
@@ -537,27 +556,31 @@
   }
 
   @Override
-  public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
+  public boolean onActivityResult(final int requestCode, final int resultCode, final Intent data) {
+    Runnable handlerRunnable;
+
     switch (requestCode) {
       case REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY:
-        handleChooseImageResult(resultCode, data);
+        handlerRunnable = () -> handleChooseImageResult(resultCode, data);
         break;
       case REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY:
-        handleChooseMultiImageResult(resultCode, data);
+        handlerRunnable = () -> handleChooseMultiImageResult(resultCode, data);
         break;
       case REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA:
-        handleCaptureImageResult(resultCode);
+        handlerRunnable = () -> handleCaptureImageResult(resultCode);
         break;
       case REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY:
-        handleChooseVideoResult(resultCode, data);
+        handlerRunnable = () -> handleChooseVideoResult(resultCode, data);
         break;
       case REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA:
-        handleCaptureVideoResult(resultCode);
+        handlerRunnable = () -> handleCaptureVideoResult(resultCode);
         break;
       default:
         return false;
     }
 
+    executor.execute(handlerRunnable);
+
     return true;
   }
 
@@ -603,9 +626,11 @@
 
   private void handleCaptureImageResult(int resultCode) {
     if (resultCode == Activity.RESULT_OK) {
+      final Uri localPendingCameraMediaUri = pendingCameraMediaUri;
+
       fileUriResolver.getFullImagePath(
-          pendingCameraMediaUri != null
-              ? pendingCameraMediaUri
+          localPendingCameraMediaUri != null
+              ? localPendingCameraMediaUri
               : Uri.parse(cache.retrievePendingCameraMediaUriPath()),
           new OnPathReadyListener() {
             @Override
@@ -622,9 +647,10 @@
 
   private void handleCaptureVideoResult(int resultCode) {
     if (resultCode == Activity.RESULT_OK) {
+      final Uri localPendingCameraMediaUrl = pendingCameraMediaUri;
       fileUriResolver.getFullImagePath(
-          pendingCameraMediaUri != null
-              ? pendingCameraMediaUri
+          localPendingCameraMediaUrl != null
+              ? localPendingCameraMediaUrl
               : Uri.parse(cache.retrievePendingCameraMediaUriPath()),
           new OnPathReadyListener() {
             @Override
@@ -641,10 +667,17 @@
 
   private void handleMultiImageResult(
       ArrayList<String> paths, boolean shouldDeleteOriginalIfScaled) {
-    if (pendingCallState != null && pendingCallState.imageOptions != null) {
+    ImageSelectionOptions localImageOptions = null;
+    synchronized (pendingCallStateLock) {
+      if (pendingCallState != null) {
+        localImageOptions = pendingCallState.imageOptions;
+      }
+    }
+
+    if (localImageOptions != null) {
       ArrayList<String> finalPath = new ArrayList<>();
       for (int i = 0; i < paths.size(); i++) {
-        String finalImagePath = getResizedImagePath(paths.get(i), pendingCallState.imageOptions);
+        String finalImagePath = getResizedImagePath(paths.get(i), localImageOptions);
 
         //delete original file if scaled
         if (finalImagePath != null
@@ -661,8 +694,15 @@
   }
 
   private void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) {
-    if (pendingCallState != null && pendingCallState.imageOptions != null) {
-      String finalImagePath = getResizedImagePath(path, pendingCallState.imageOptions);
+    ImageSelectionOptions localImageOptions = null;
+    synchronized (pendingCallStateLock) {
+      if (pendingCallState != null) {
+        localImageOptions = pendingCallState.imageOptions;
+      }
+    }
+
+    if (localImageOptions != null) {
+      String finalImagePath = getResizedImagePath(path, localImageOptions);
       //delete original file if scaled
       if (finalImagePath != null && !finalImagePath.equals(path) && shouldDeleteOriginalIfScaled) {
         new File(path).delete();
@@ -689,12 +729,13 @@
       @Nullable ImageSelectionOptions imageOptions,
       @Nullable VideoSelectionOptions videoOptions,
       @NonNull Messages.Result<List<String>> result) {
-    if (pendingCallState != null) {
-      return false;
+    synchronized (pendingCallStateLock) {
+      if (pendingCallState != null) {
+        return false;
+      }
+      pendingCallState = new PendingCallState(imageOptions, videoOptions, result);
     }
 
-    pendingCallState = new PendingCallState(imageOptions, videoOptions, result);
-
     // Clean up cache if a new image picker is launched.
     cache.clear();
 
@@ -710,24 +751,39 @@
     if (imagePath != null) {
       pathList.add(imagePath);
     }
-    if (pendingCallState == null) {
+
+    Messages.Result<List<String>> localResult = null;
+    synchronized (pendingCallStateLock) {
+      if (pendingCallState != null) {
+        localResult = pendingCallState.result;
+      }
+      pendingCallState = null;
+    }
+
+    if (localResult == null) {
       // Only save data for later retrieval if something was actually selected.
       if (!pathList.isEmpty()) {
         cache.saveResult(pathList, null, null);
       }
-      return;
+    } else {
+      localResult.success(pathList);
     }
-    pendingCallState.result.success(pathList);
-    pendingCallState = null;
   }
 
   private void finishWithListSuccess(ArrayList<String> imagePaths) {
-    if (pendingCallState == null) {
-      cache.saveResult(imagePaths, null, null);
-      return;
+    Messages.Result<List<String>> localResult = null;
+    synchronized (pendingCallStateLock) {
+      if (pendingCallState != null) {
+        localResult = pendingCallState.result;
+      }
+      pendingCallState = null;
     }
-    pendingCallState.result.success(imagePaths);
-    pendingCallState = null;
+
+    if (localResult == null) {
+      cache.saveResult(imagePaths, null, null);
+    } else {
+      localResult.success(imagePaths);
+    }
   }
 
   private void finishWithAlreadyActiveError(Messages.Result<List<String>> result) {
@@ -735,12 +791,19 @@
   }
 
   private void finishWithError(String errorCode, String errorMessage) {
-    if (pendingCallState == null) {
-      cache.saveResult(null, errorCode, errorMessage);
-      return;
+    Messages.Result<List<String>> localResult = null;
+    synchronized (pendingCallStateLock) {
+      if (pendingCallState != null) {
+        localResult = pendingCallState.result;
+      }
+      pendingCallState = null;
     }
-    pendingCallState.result.error(new FlutterError(errorCode, errorMessage, null));
-    pendingCallState = null;
+
+    if (localResult == null) {
+      cache.saveResult(null, errorCode, errorMessage);
+    } else {
+      localResult.error(new FlutterError(errorCode, errorMessage, null));
+    }
   }
 
   private void useFrontCamera(Intent intent) {
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 77a34b4..4648e6d 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
@@ -7,6 +7,8 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.core.IsEqual.equalTo;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -32,6 +34,7 @@
 import java.io.File;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.ExecutorService;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -64,6 +67,7 @@
   @Mock FileUtils mockFileUtils;
   @Mock Intent mockIntent;
   @Mock ImagePickerCache cache;
+  @Mock ExecutorService mockExecutor;
 
   ImagePickerDelegate.FileUriResolver mockFileUriResolver;
   MockedStatic<File> mockStaticFile;
@@ -349,6 +353,13 @@
 
   @Test
   public void onActivityResult_whenPickFromGalleryCanceled_finishesWithEmptyList() {
+    Mockito.doAnswer(
+            invocation -> {
+              ((Runnable) invocation.getArgument(0)).run();
+              return null;
+            })
+        .when(mockExecutor)
+        .execute(any(Runnable.class));
     ImagePickerDelegate delegate =
         createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null);
 
@@ -364,6 +375,13 @@
 
   @Test
   public void onActivityResult_whenPickFromGalleryCanceled_storesNothingInCache() {
+    Mockito.doAnswer(
+            invocation -> {
+              ((Runnable) invocation.getArgument(0)).run();
+              return null;
+            })
+        .when(mockExecutor)
+        .execute(any(Runnable.class));
     ImagePickerDelegate delegate = createDelegate();
 
     delegate.onActivityResult(
@@ -375,6 +393,13 @@
   @Test
   public void
       onActivityResult_whenImagePickedFromGallery_andNoResizeNeeded_finishesWithImagePath() {
+    Mockito.doAnswer(
+            invocation -> {
+              ((Runnable) invocation.getArgument(0)).run();
+              return null;
+            })
+        .when(mockExecutor)
+        .execute(any(Runnable.class));
     ImagePickerDelegate delegate =
         createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null);
 
@@ -390,6 +415,13 @@
 
   @Test
   public void onActivityResult_whenImagePickedFromGallery_andNoResizeNeeded_storesImageInCache() {
+    Mockito.doAnswer(
+            invocation -> {
+              ((Runnable) invocation.getArgument(0)).run();
+              return null;
+            })
+        .when(mockExecutor)
+        .execute(any(Runnable.class));
     ImagePickerDelegate delegate = createDelegate();
 
     delegate.onActivityResult(
@@ -404,8 +436,16 @@
   @Test
   public void
       onActivityResult_whenImagePickedFromGallery_andResizeNeeded_finishesWithScaledImagePath() {
+    Mockito.doAnswer(
+            invocation -> {
+              ((Runnable) invocation.getArgument(0)).run();
+              return null;
+            })
+        .when(mockExecutor)
+        .execute(any(Runnable.class));
     ImagePickerDelegate delegate =
         createDelegateWithPendingResultAndOptions(RESIZE_TRIGGERING_IMAGE_OPTIONS, null);
+
     delegate.onActivityResult(
         ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, Activity.RESULT_OK, mockIntent);
 
@@ -419,8 +459,16 @@
   @Test
   public void
       onActivityResult_whenVideoPickedFromGallery_andResizeParametersSupplied_finishesWithFilePath() {
+    Mockito.doAnswer(
+            invocation -> {
+              ((Runnable) invocation.getArgument(0)).run();
+              return null;
+            })
+        .when(mockExecutor)
+        .execute(any(Runnable.class));
     ImagePickerDelegate delegate =
         createDelegateWithPendingResultAndOptions(RESIZE_TRIGGERING_IMAGE_OPTIONS, null);
+
     delegate.onActivityResult(
         ImagePickerDelegate.REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY, Activity.RESULT_OK, mockIntent);
 
@@ -433,6 +481,13 @@
 
   @Test
   public void onActivityResult_whenTakeImageWithCameraCanceled_finishesWithEmptyList() {
+    Mockito.doAnswer(
+            invocation -> {
+              ((Runnable) invocation.getArgument(0)).run();
+              return null;
+            })
+        .when(mockExecutor)
+        .execute(any(Runnable.class));
     ImagePickerDelegate delegate =
         createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null);
 
@@ -448,9 +503,16 @@
 
   @Test
   public void onActivityResult_whenImageTakenWithCamera_andNoResizeNeeded_finishesWithImagePath() {
+    when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString");
+    Mockito.doAnswer(
+            invocation -> {
+              ((Runnable) invocation.getArgument(0)).run();
+              return null;
+            })
+        .when(mockExecutor)
+        .execute(any(Runnable.class));
     ImagePickerDelegate delegate =
         createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null);
-    when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString");
 
     delegate.onActivityResult(
         ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA, Activity.RESULT_OK, mockIntent);
@@ -466,9 +528,16 @@
   public void
       onActivityResult_whenImageTakenWithCamera_andResizeNeeded_finishesWithScaledImagePath() {
     when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString");
-
+    Mockito.doAnswer(
+            invocation -> {
+              ((Runnable) invocation.getArgument(0)).run();
+              return null;
+            })
+        .when(mockExecutor)
+        .execute(any(Runnable.class));
     ImagePickerDelegate delegate =
         createDelegateWithPendingResultAndOptions(RESIZE_TRIGGERING_IMAGE_OPTIONS, null);
+
     delegate.onActivityResult(
         ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA, Activity.RESULT_OK, mockIntent);
 
@@ -483,9 +552,16 @@
   public void
       onActivityResult_whenVideoTakenWithCamera_andResizeParametersSupplied_finishesWithFilePath() {
     when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString");
-
+    Mockito.doAnswer(
+            invocation -> {
+              ((Runnable) invocation.getArgument(0)).run();
+              return null;
+            })
+        .when(mockExecutor)
+        .execute(any(Runnable.class));
     ImagePickerDelegate delegate =
         createDelegateWithPendingResultAndOptions(RESIZE_TRIGGERING_IMAGE_OPTIONS, null);
+
     delegate.onActivityResult(
         ImagePickerDelegate.REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA, Activity.RESULT_OK, mockIntent);
 
@@ -500,10 +576,17 @@
   public void
       onActivityResult_whenVideoTakenWithCamera_andMaxDurationParametersSupplied_finishesWithFilePath() {
     when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString");
-
+    Mockito.doAnswer(
+            invocation -> {
+              ((Runnable) invocation.getArgument(0)).run();
+              return null;
+            })
+        .when(mockExecutor)
+        .execute(any(Runnable.class));
     ImagePickerDelegate delegate =
         createDelegateWithPendingResultAndOptions(
             null, new VideoSelectionOptions.Builder().setMaxDurationSeconds(MAX_DURATION).build());
+
     delegate.onActivityResult(
         ImagePickerDelegate.REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA, Activity.RESULT_OK, mockIntent);
 
@@ -514,6 +597,80 @@
     verifyNoMoreInteractions(mockResult);
   }
 
+  @Test
+  public void onActivityResult_whenImagePickedFromGallery_returnsTrue() {
+    ImagePickerDelegate delegate = createDelegate();
+
+    boolean isHandled =
+        delegate.onActivityResult(
+            ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY,
+            Activity.RESULT_OK,
+            mockIntent);
+
+    assertTrue(isHandled);
+  }
+
+  @Test
+  public void onActivityResult_whenMultipleImagesPickedFromGallery_returnsTrue() {
+    ImagePickerDelegate delegate = createDelegate();
+
+    boolean isHandled =
+        delegate.onActivityResult(
+            ImagePickerDelegate.REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY,
+            Activity.RESULT_OK,
+            mockIntent);
+
+    assertTrue(isHandled);
+  }
+
+  @Test
+  public void onActivityResult_whenVideoPickerFromGallery_returnsTrue() {
+    ImagePickerDelegate delegate = createDelegate();
+
+    boolean isHandled =
+        delegate.onActivityResult(
+            ImagePickerDelegate.REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY,
+            Activity.RESULT_OK,
+            mockIntent);
+
+    assertTrue(isHandled);
+  }
+
+  @Test
+  public void onActivityResult_whenImageTakenWithCamera_returnsTrue() {
+    ImagePickerDelegate delegate = createDelegate();
+
+    boolean isHandled =
+        delegate.onActivityResult(
+            ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA,
+            Activity.RESULT_OK,
+            mockIntent);
+
+    assertTrue(isHandled);
+  }
+
+  @Test
+  public void onActivityResult_whenVideoTakenWithCamera_returnsTrue() {
+    ImagePickerDelegate delegate = createDelegate();
+
+    boolean isHandled =
+        delegate.onActivityResult(
+            ImagePickerDelegate.REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA,
+            Activity.RESULT_OK,
+            mockIntent);
+
+    assertTrue(isHandled);
+  }
+
+  @Test
+  public void onActivityResult_withUnknownRequest_returnsFalse() {
+    ImagePickerDelegate delegate = createDelegate();
+
+    boolean isHandled = delegate.onActivityResult(314, Activity.RESULT_OK, mockIntent);
+
+    assertFalse(isHandled);
+  }
+
   private ImagePickerDelegate createDelegate() {
     return new ImagePickerDelegate(
         mockActivity,
@@ -525,7 +682,8 @@
         cache,
         mockPermissionManager,
         mockFileUriResolver,
-        mockFileUtils);
+        mockFileUtils,
+        mockExecutor);
   }
 
   private ImagePickerDelegate createDelegateWithPendingResultAndOptions(
@@ -540,7 +698,8 @@
         cache,
         mockPermissionManager,
         mockFileUriResolver,
-        mockFileUtils);
+        mockFileUtils,
+        mockExecutor);
   }
 
   private void verifyFinishedWithAlreadyActiveError() {
diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml
index e95d14c..ec6a0b3 100755
--- a/packages/image_picker/image_picker_android/pubspec.yaml
+++ b/packages/image_picker/image_picker_android/pubspec.yaml
@@ -3,7 +3,7 @@
 repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_android
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
 
-version: 0.8.6+9
+version: 0.8.6+10
 
 environment:
   sdk: ">=2.18.0 <4.0.0"