Skip to content

Commit d155228

Browse files
authored
[image_picker] Move I/O operations to a separate thread (flutter#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 [flutter#100966](flutter#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](math1man/plugins@11f5f8a). This PR closes several issues related to ANRs. * Fixes [flutter#94120](flutter#94210) * Fixes [flutter#114080](flutter#114080) Also, it partly implements what is requested in [flutter#91393](flutter#91393).
1 parent 20b38ce commit d155228

File tree

4 files changed

+276
-50
lines changed

4 files changed

+276
-50
lines changed

packages/image_picker/image_picker_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.8.6+10
2+
3+
* Offloads picker result handling to separate thread.
4+
15
## 0.8.6+9
26

37
* Fixes compatibility with AGP versions older than 4.2.

packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java

Lines changed: 106 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
import java.util.List;
3333
import java.util.Map;
3434
import java.util.UUID;
35+
import java.util.concurrent.ExecutorService;
36+
import java.util.concurrent.Executors;
3537

3638
/**
3739
* A delegate class doing the heavy lifting for the plugin.
@@ -112,6 +114,7 @@ private PendingCallState(
112114
private final PermissionManager permissionManager;
113115
private final FileUriResolver fileUriResolver;
114116
private final FileUtils fileUtils;
117+
private final ExecutorService executor;
115118
private CameraDevice cameraDevice;
116119

117120
interface PermissionManager {
@@ -134,6 +137,7 @@ interface OnPathReadyListener {
134137

135138
private Uri pendingCameraMediaUri;
136139
private @Nullable PendingCallState pendingCallState;
140+
private final Object pendingCallStateLock = new Object();
137141

138142
public ImagePickerDelegate(
139143
final Activity activity,
@@ -185,7 +189,8 @@ public void onScanCompleted(String path, Uri uri) {
185189
});
186190
}
187191
},
188-
new FileUtils());
192+
new FileUtils(),
193+
Executors.newSingleThreadExecutor());
189194
}
190195

191196
/**
@@ -203,7 +208,8 @@ public void onScanCompleted(String path, Uri uri) {
203208
final ImagePickerCache cache,
204209
final PermissionManager permissionManager,
205210
final FileUriResolver fileUriResolver,
206-
final FileUtils fileUtils) {
211+
final FileUtils fileUtils,
212+
final ExecutorService executor) {
207213
this.activity = activity;
208214
this.externalFilesDirectory = externalFilesDirectory;
209215
this.imageResizer = imageResizer;
@@ -216,6 +222,7 @@ public void onScanCompleted(String path, Uri uri) {
216222
this.fileUriResolver = fileUriResolver;
217223
this.fileUtils = fileUtils;
218224
this.cache = cache;
225+
this.executor = executor;
219226
}
220227

221228
void setCameraDevice(CameraDevice device) {
@@ -224,19 +231,25 @@ void setCameraDevice(CameraDevice device) {
224231

225232
// Save the state of the image picker so it can be retrieved with `retrieveLostImage`.
226233
void saveStateBeforeResult() {
227-
if (pendingCallState == null) {
228-
return;
234+
ImageSelectionOptions localImageOptions;
235+
synchronized (pendingCallStateLock) {
236+
if (pendingCallState == null) {
237+
return;
238+
}
239+
localImageOptions = pendingCallState.imageOptions;
229240
}
230241

231242
cache.saveType(
232-
pendingCallState.imageOptions != null
243+
localImageOptions != null
233244
? ImagePickerCache.CacheType.IMAGE
234245
: ImagePickerCache.CacheType.VIDEO);
235-
if (pendingCallState.imageOptions != null) {
236-
cache.saveDimensionWithOutputOptions(pendingCallState.imageOptions);
246+
if (localImageOptions != null) {
247+
cache.saveDimensionWithOutputOptions(localImageOptions);
237248
}
238-
if (pendingCameraMediaUri != null) {
239-
cache.savePendingCameraMediaUriPath(pendingCameraMediaUri);
249+
250+
final Uri localPendingCameraMediaUri = pendingCameraMediaUri;
251+
if (localPendingCameraMediaUri != null) {
252+
cache.savePendingCameraMediaUriPath(localPendingCameraMediaUri);
240253
}
241254
}
242255

@@ -323,10 +336,16 @@ public void takeVideoWithCamera(
323336

324337
private void launchTakeVideoWithCameraIntent() {
325338
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
326-
if (pendingCallState != null
327-
&& pendingCallState.videoOptions != null
328-
&& pendingCallState.videoOptions.getMaxDurationSeconds() != null) {
329-
int maxSeconds = pendingCallState.videoOptions.getMaxDurationSeconds().intValue();
339+
340+
VideoSelectionOptions localVideoOptions = null;
341+
synchronized (pendingCallStateLock) {
342+
if (pendingCallState != null) {
343+
localVideoOptions = pendingCallState.videoOptions;
344+
}
345+
}
346+
347+
if (localVideoOptions != null && localVideoOptions.getMaxDurationSeconds() != null) {
348+
int maxSeconds = localVideoOptions.getMaxDurationSeconds().intValue();
330349
intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, maxSeconds);
331350
}
332351
if (cameraDevice == CameraDevice.FRONT) {
@@ -537,27 +556,31 @@ public boolean onRequestPermissionsResult(
537556
}
538557

539558
@Override
540-
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
559+
public boolean onActivityResult(final int requestCode, final int resultCode, final Intent data) {
560+
Runnable handlerRunnable;
561+
541562
switch (requestCode) {
542563
case REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY:
543-
handleChooseImageResult(resultCode, data);
564+
handlerRunnable = () -> handleChooseImageResult(resultCode, data);
544565
break;
545566
case REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY:
546-
handleChooseMultiImageResult(resultCode, data);
567+
handlerRunnable = () -> handleChooseMultiImageResult(resultCode, data);
547568
break;
548569
case REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA:
549-
handleCaptureImageResult(resultCode);
570+
handlerRunnable = () -> handleCaptureImageResult(resultCode);
550571
break;
551572
case REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY:
552-
handleChooseVideoResult(resultCode, data);
573+
handlerRunnable = () -> handleChooseVideoResult(resultCode, data);
553574
break;
554575
case REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA:
555-
handleCaptureVideoResult(resultCode);
576+
handlerRunnable = () -> handleCaptureVideoResult(resultCode);
556577
break;
557578
default:
558579
return false;
559580
}
560581

582+
executor.execute(handlerRunnable);
583+
561584
return true;
562585
}
563586

@@ -603,9 +626,11 @@ private void handleChooseVideoResult(int resultCode, Intent data) {
603626

604627
private void handleCaptureImageResult(int resultCode) {
605628
if (resultCode == Activity.RESULT_OK) {
629+
final Uri localPendingCameraMediaUri = pendingCameraMediaUri;
630+
606631
fileUriResolver.getFullImagePath(
607-
pendingCameraMediaUri != null
608-
? pendingCameraMediaUri
632+
localPendingCameraMediaUri != null
633+
? localPendingCameraMediaUri
609634
: Uri.parse(cache.retrievePendingCameraMediaUriPath()),
610635
new OnPathReadyListener() {
611636
@Override
@@ -622,9 +647,10 @@ public void onPathReady(String path) {
622647

623648
private void handleCaptureVideoResult(int resultCode) {
624649
if (resultCode == Activity.RESULT_OK) {
650+
final Uri localPendingCameraMediaUrl = pendingCameraMediaUri;
625651
fileUriResolver.getFullImagePath(
626-
pendingCameraMediaUri != null
627-
? pendingCameraMediaUri
652+
localPendingCameraMediaUrl != null
653+
? localPendingCameraMediaUrl
628654
: Uri.parse(cache.retrievePendingCameraMediaUriPath()),
629655
new OnPathReadyListener() {
630656
@Override
@@ -641,10 +667,17 @@ public void onPathReady(String path) {
641667

642668
private void handleMultiImageResult(
643669
ArrayList<String> paths, boolean shouldDeleteOriginalIfScaled) {
644-
if (pendingCallState != null && pendingCallState.imageOptions != null) {
670+
ImageSelectionOptions localImageOptions = null;
671+
synchronized (pendingCallStateLock) {
672+
if (pendingCallState != null) {
673+
localImageOptions = pendingCallState.imageOptions;
674+
}
675+
}
676+
677+
if (localImageOptions != null) {
645678
ArrayList<String> finalPath = new ArrayList<>();
646679
for (int i = 0; i < paths.size(); i++) {
647-
String finalImagePath = getResizedImagePath(paths.get(i), pendingCallState.imageOptions);
680+
String finalImagePath = getResizedImagePath(paths.get(i), localImageOptions);
648681

649682
//delete original file if scaled
650683
if (finalImagePath != null
@@ -661,8 +694,15 @@ private void handleMultiImageResult(
661694
}
662695

663696
private void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) {
664-
if (pendingCallState != null && pendingCallState.imageOptions != null) {
665-
String finalImagePath = getResizedImagePath(path, pendingCallState.imageOptions);
697+
ImageSelectionOptions localImageOptions = null;
698+
synchronized (pendingCallStateLock) {
699+
if (pendingCallState != null) {
700+
localImageOptions = pendingCallState.imageOptions;
701+
}
702+
}
703+
704+
if (localImageOptions != null) {
705+
String finalImagePath = getResizedImagePath(path, localImageOptions);
666706
//delete original file if scaled
667707
if (finalImagePath != null && !finalImagePath.equals(path) && shouldDeleteOriginalIfScaled) {
668708
new File(path).delete();
@@ -689,12 +729,13 @@ private boolean setPendingOptionsAndResult(
689729
@Nullable ImageSelectionOptions imageOptions,
690730
@Nullable VideoSelectionOptions videoOptions,
691731
@NonNull Messages.Result<List<String>> result) {
692-
if (pendingCallState != null) {
693-
return false;
732+
synchronized (pendingCallStateLock) {
733+
if (pendingCallState != null) {
734+
return false;
735+
}
736+
pendingCallState = new PendingCallState(imageOptions, videoOptions, result);
694737
}
695738

696-
pendingCallState = new PendingCallState(imageOptions, videoOptions, result);
697-
698739
// Clean up cache if a new image picker is launched.
699740
cache.clear();
700741

@@ -710,37 +751,59 @@ private void finishWithSuccess(@Nullable String imagePath) {
710751
if (imagePath != null) {
711752
pathList.add(imagePath);
712753
}
713-
if (pendingCallState == null) {
754+
755+
Messages.Result<List<String>> localResult = null;
756+
synchronized (pendingCallStateLock) {
757+
if (pendingCallState != null) {
758+
localResult = pendingCallState.result;
759+
}
760+
pendingCallState = null;
761+
}
762+
763+
if (localResult == null) {
714764
// Only save data for later retrieval if something was actually selected.
715765
if (!pathList.isEmpty()) {
716766
cache.saveResult(pathList, null, null);
717767
}
718-
return;
768+
} else {
769+
localResult.success(pathList);
719770
}
720-
pendingCallState.result.success(pathList);
721-
pendingCallState = null;
722771
}
723772

724773
private void finishWithListSuccess(ArrayList<String> imagePaths) {
725-
if (pendingCallState == null) {
774+
Messages.Result<List<String>> localResult = null;
775+
synchronized (pendingCallStateLock) {
776+
if (pendingCallState != null) {
777+
localResult = pendingCallState.result;
778+
}
779+
pendingCallState = null;
780+
}
781+
782+
if (localResult == null) {
726783
cache.saveResult(imagePaths, null, null);
727-
return;
784+
} else {
785+
localResult.success(imagePaths);
728786
}
729-
pendingCallState.result.success(imagePaths);
730-
pendingCallState = null;
731787
}
732788

733789
private void finishWithAlreadyActiveError(Messages.Result<List<String>> result) {
734790
result.error(new FlutterError("already_active", "Image picker is already active", null));
735791
}
736792

737793
private void finishWithError(String errorCode, String errorMessage) {
738-
if (pendingCallState == null) {
794+
Messages.Result<List<String>> localResult = null;
795+
synchronized (pendingCallStateLock) {
796+
if (pendingCallState != null) {
797+
localResult = pendingCallState.result;
798+
}
799+
pendingCallState = null;
800+
}
801+
802+
if (localResult == null) {
739803
cache.saveResult(null, errorCode, errorMessage);
740-
return;
804+
} else {
805+
localResult.error(new FlutterError(errorCode, errorMessage, null));
741806
}
742-
pendingCallState.result.error(new FlutterError(errorCode, errorMessage, null));
743-
pendingCallState = null;
744807
}
745808

746809
private void useFrontCamera(Intent intent) {

0 commit comments

Comments
 (0)