Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
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
4 changes: 4 additions & 0 deletions packages/image_picker/image_picker_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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 PendingCallState(
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 @@ interface OnPathReadyListener {

private Uri pendingCameraMediaUri;
private @Nullable PendingCallState pendingCallState;
private final Object pendingCallStateLock = new Object();

public ImagePickerDelegate(
final Activity activity,
@@ -185,7 +189,8 @@ public void onScanCompleted(String path, Uri uri) {
});
}
},
new FileUtils());
new FileUtils(),
Executors.newSingleThreadExecutor());
}

/**
@@ -203,7 +208,8 @@ public void onScanCompleted(String path, Uri uri) {
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 @@ public void onScanCompleted(String path, Uri uri) {
this.fileUriResolver = fileUriResolver;
this.fileUtils = fileUtils;
this.cache = cache;
this.executor = executor;
}

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

// 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 @@ public void takeVideoWithCamera(

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 @@ public boolean onRequestPermissionsResult(
}

@Override
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
public boolean onActivityResult(final int requestCode, final int resultCode, final Intent data) {
Runnable handlerRunnable;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this meant to be handleRunnable? or is this a runnable handler?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a Runnable running a handler function (handleChooseImageResult etc.). Would runnableHandler make more sense in your opinion?


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 handleChooseVideoResult(int resultCode, Intent data) {

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 @@ public void onPathReady(String path) {

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 @@ public void onPathReady(String path) {

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 handleMultiImageResult(
}

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 @@ private boolean setPendingOptionsAndResult(
@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,37 +751,59 @@ private void finishWithSuccess(@Nullable String imagePath) {
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) {
Messages.Result<List<String>> localResult = null;
synchronized (pendingCallStateLock) {
if (pendingCallState != null) {
localResult = pendingCallState.result;
}
pendingCallState = null;
}

if (localResult == null) {
cache.saveResult(imagePaths, null, null);
return;
} else {
localResult.success(imagePaths);
}
pendingCallState.result.success(imagePaths);
pendingCallState = null;
}

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

private void finishWithError(String errorCode, String errorMessage) {
if (pendingCallState == null) {
Messages.Result<List<String>> localResult = null;
synchronized (pendingCallStateLock) {
if (pendingCallState != null) {
localResult = pendingCallState.result;
}
pendingCallState = null;
}

if (localResult == null) {
cache.saveResult(null, errorCode, errorMessage);
return;
} else {
localResult.error(new FlutterError(errorCode, errorMessage, null));
}
pendingCallState.result.error(new FlutterError(errorCode, errorMessage, null));
pendingCallState = null;
}

private void useFrontCamera(Intent intent) {
Loading