diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md index 793b6f0f1b62..d545d0a6d637 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+3 + +* Switches to Pigeon for internal implementation. + ## 0.8.6+2 * Fixes null pointer exception in `saveResult`. diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageOutputOptions.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageOutputOptions.java deleted file mode 100644 index 89648dcd24b3..000000000000 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImageOutputOptions.java +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.imagepicker; - -import androidx.annotation.Nullable; - -/** Stores settings for image output options. */ -public class ImageOutputOptions { - /** The maximum width of the image, if the width should be constrained. */ - @Nullable public final Double maxWidth; - /** The maximum height of the image, if the width should be constrained. */ - @Nullable public final Double maxHeight; - /** - * The output quality of the image, as a number from 0 to 100. - * - *

Defaults to 100. - */ - final int quality; - - public ImageOutputOptions( - @Nullable Double maxWidth, @Nullable Double maxHeight, @Nullable Integer quality) { - this.maxWidth = maxWidth; - this.maxHeight = maxHeight; - // Treat any invalid value as full quality. - this.quality = quality == null || quality < 0 || quality > 100 ? 100 : quality; - } -} diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java index 1593f87f8c9a..182b8a174342 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java @@ -21,14 +21,15 @@ public enum CacheType { VIDEO } - static final String MAP_KEY_PATH = "path"; static final String MAP_KEY_PATH_LIST = "pathList"; static final String MAP_KEY_MAX_WIDTH = "maxWidth"; static final String MAP_KEY_MAX_HEIGHT = "maxHeight"; static final String MAP_KEY_IMAGE_QUALITY = "imageQuality"; - private static final String MAP_KEY_TYPE = "type"; - private static final String MAP_KEY_ERROR_CODE = "errorCode"; - private static final String MAP_KEY_ERROR_MESSAGE = "errorMessage"; + static final String MAP_KEY_TYPE = "type"; + static final String MAP_KEY_ERROR = "error"; + + private static final String MAP_TYPE_VALUE_IMAGE = "image"; + private static final String MAP_TYPE_VALUE_VIDEO = "video"; private static final String FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY = "flutter_image_picker_image_path"; @@ -50,7 +51,7 @@ public enum CacheType { @VisibleForTesting static final String SHARED_PREFERENCES_NAME = "flutter_image_picker_shared_preference"; - private SharedPreferences prefs; + private final SharedPreferences prefs; ImagePickerCache(Context context) { prefs = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); @@ -59,10 +60,10 @@ public enum CacheType { void saveType(CacheType type) { switch (type) { case IMAGE: - setType("image"); + setType(MAP_TYPE_VALUE_IMAGE); break; case VIDEO: - setType("video"); + setType(MAP_TYPE_VALUE_VIDEO); break; } } @@ -71,16 +72,17 @@ private void setType(String type) { prefs.edit().putString(SHARED_PREFERENCE_TYPE_KEY, type).apply(); } - void saveDimensionWithOutputOptions(ImageOutputOptions options) { + void saveDimensionWithOutputOptions(Messages.ImageSelectionOptions options) { SharedPreferences.Editor editor = prefs.edit(); - if (options.maxWidth != null) { - editor.putLong(SHARED_PREFERENCE_MAX_WIDTH_KEY, Double.doubleToRawLongBits(options.maxWidth)); + if (options.getMaxWidth() != null) { + editor.putLong( + SHARED_PREFERENCE_MAX_WIDTH_KEY, Double.doubleToRawLongBits(options.getMaxWidth())); } - if (options.maxHeight != null) { + if (options.getMaxHeight() != null) { editor.putLong( - SHARED_PREFERENCE_MAX_HEIGHT_KEY, Double.doubleToRawLongBits(options.maxHeight)); + SHARED_PREFERENCE_MAX_HEIGHT_KEY, Double.doubleToRawLongBits(options.getMaxHeight())); } - editor.putInt(SHARED_PREFERENCE_IMAGE_QUALITY_KEY, options.quality); + editor.putInt(SHARED_PREFERENCE_IMAGE_QUALITY_KEY, options.getQuality().intValue()); editor.apply(); } @@ -115,35 +117,37 @@ void clear() { } Map getCacheMap() { - Map resultMap = new HashMap<>(); - ArrayList pathList = new ArrayList<>(); boolean hasData = false; if (prefs.contains(FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY)) { final Set imagePathList = prefs.getStringSet(FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY, null); if (imagePathList != null) { - pathList.addAll(imagePathList); + ArrayList pathList = new ArrayList<>(imagePathList); resultMap.put(MAP_KEY_PATH_LIST, pathList); hasData = true; } } if (prefs.contains(SHARED_PREFERENCE_ERROR_CODE_KEY)) { - final String errorCodeValue = prefs.getString(SHARED_PREFERENCE_ERROR_CODE_KEY, ""); - resultMap.put(MAP_KEY_ERROR_CODE, errorCodeValue); + final Messages.CacheRetrievalError.Builder error = new Messages.CacheRetrievalError.Builder(); + error.setCode(prefs.getString(SHARED_PREFERENCE_ERROR_CODE_KEY, "")); hasData = true; if (prefs.contains(SHARED_PREFERENCE_ERROR_MESSAGE_KEY)) { - final String errorMessageValue = prefs.getString(SHARED_PREFERENCE_ERROR_MESSAGE_KEY, ""); - resultMap.put(MAP_KEY_ERROR_MESSAGE, errorMessageValue); + error.setMessage(prefs.getString(SHARED_PREFERENCE_ERROR_MESSAGE_KEY, "")); } + resultMap.put(MAP_KEY_ERROR, error.build()); } if (hasData) { if (prefs.contains(SHARED_PREFERENCE_TYPE_KEY)) { final String typeValue = prefs.getString(SHARED_PREFERENCE_TYPE_KEY, ""); - resultMap.put(MAP_KEY_TYPE, typeValue); + resultMap.put( + MAP_KEY_TYPE, + typeValue.equals(MAP_TYPE_VALUE_VIDEO) + ? Messages.CacheRetrievalType.VIDEO + : Messages.CacheRetrievalType.IMAGE); } if (prefs.contains(SHARED_PREFERENCE_MAX_WIDTH_KEY)) { final long maxWidthValue = prefs.getLong(SHARED_PREFERENCE_MAX_WIDTH_KEY, 0); @@ -153,12 +157,8 @@ Map getCacheMap() { final long maxHeightValue = prefs.getLong(SHARED_PREFERENCE_MAX_HEIGHT_KEY, 0); resultMap.put(MAP_KEY_MAX_HEIGHT, Double.longBitsToDouble(maxHeightValue)); } - if (prefs.contains(SHARED_PREFERENCE_IMAGE_QUALITY_KEY)) { - final int imageQuality = prefs.getInt(SHARED_PREFERENCE_IMAGE_QUALITY_KEY, 100); - resultMap.put(MAP_KEY_IMAGE_QUALITY, imageQuality); - } else { - resultMap.put(MAP_KEY_IMAGE_QUALITY, 100); - } + final int imageQuality = prefs.getInt(SHARED_PREFERENCE_IMAGE_QUALITY_KEY, 100); + resultMap.put(MAP_KEY_IMAGE_QUALITY, imageQuality); } return resultMap; } 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 a28611e5b5d7..9b355880b8d1 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 @@ -22,8 +22,10 @@ import androidx.annotation.VisibleForTesting; import androidx.core.app.ActivityCompat; import androidx.core.content.FileProvider; -import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugins.imagepicker.Messages.FlutterError; +import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions; +import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions; import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -87,14 +89,14 @@ public enum CameraDevice { /** Holds call state during intent handling. */ private static class PendingCallState { - public final @Nullable ImageOutputOptions imageOptions; - public final @Nullable VideoOptions videoOptions; - public final @NonNull MethodChannel.Result result; + public final @Nullable ImageSelectionOptions imageOptions; + public final @Nullable VideoSelectionOptions videoOptions; + public final @NonNull Messages.Result> result; private PendingCallState( - @Nullable ImageOutputOptions imageOptions, - @Nullable VideoOptions videoOptions, - @NonNull MethodChannel.Result result) { + @Nullable ImageSelectionOptions imageOptions, + @Nullable VideoSelectionOptions videoOptions, + @NonNull Messages.Result> result) { this.imageOptions = imageOptions; this.videoOptions = videoOptions; this.result = result; @@ -195,9 +197,9 @@ public void onScanCompleted(String path, Uri uri) { final Activity activity, final File externalFilesDirectory, final ImageResizer imageResizer, - final @Nullable ImageOutputOptions pendingImageOptions, - final @Nullable VideoOptions pendingVideoOptions, - final @Nullable MethodChannel.Result result, + final @Nullable ImageSelectionOptions pendingImageOptions, + final @Nullable VideoSelectionOptions pendingVideoOptions, + final @Nullable Messages.Result> result, final ImagePickerCache cache, final PermissionManager permissionManager, final FileUriResolver fileUriResolver, @@ -220,10 +222,6 @@ void setCameraDevice(CameraDevice device) { cameraDevice = device; } - CameraDevice getCameraDevice() { - return cameraDevice; - } - // Save the state of the image picker so it can be retrieved with `retrieveLostImage`. void saveStateBeforeResult() { if (pendingCallState == null) { @@ -242,34 +240,44 @@ void saveStateBeforeResult() { } } - void retrieveLostImage(MethodChannel.Result result) { - Map resultMap = cache.getCacheMap(); + @Nullable + Messages.CacheRetrievalResult retrieveLostImage() { + Map cacheMap = cache.getCacheMap(); + if (cacheMap.isEmpty()) { + return null; + } + + Messages.CacheRetrievalResult.Builder result = new Messages.CacheRetrievalResult.Builder(); + + Messages.CacheRetrievalType type = + (Messages.CacheRetrievalType) cacheMap.get(ImagePickerCache.MAP_KEY_TYPE); + if (type != null) { + result.setType(type); + } + result.setError((Messages.CacheRetrievalError) cacheMap.get(ImagePickerCache.MAP_KEY_ERROR)); @SuppressWarnings("unchecked") ArrayList pathList = - (ArrayList) resultMap.get(ImagePickerCache.MAP_KEY_PATH_LIST); - ArrayList newPathList = new ArrayList<>(); + (ArrayList) cacheMap.get(ImagePickerCache.MAP_KEY_PATH_LIST); if (pathList != null) { + ArrayList newPathList = new ArrayList<>(); for (String path : pathList) { - Double maxWidth = (Double) resultMap.get(ImagePickerCache.MAP_KEY_MAX_WIDTH); - Double maxHeight = (Double) resultMap.get(ImagePickerCache.MAP_KEY_MAX_HEIGHT); - Integer boxedImageQuality = (Integer) resultMap.get(ImagePickerCache.MAP_KEY_IMAGE_QUALITY); + Double maxWidth = (Double) cacheMap.get(ImagePickerCache.MAP_KEY_MAX_WIDTH); + Double maxHeight = (Double) cacheMap.get(ImagePickerCache.MAP_KEY_MAX_HEIGHT); + Integer boxedImageQuality = (Integer) cacheMap.get(ImagePickerCache.MAP_KEY_IMAGE_QUALITY); int imageQuality = boxedImageQuality == null ? 100 : boxedImageQuality; newPathList.add(imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality)); } - resultMap.put(ImagePickerCache.MAP_KEY_PATH_LIST, newPathList); - resultMap.put(ImagePickerCache.MAP_KEY_PATH, newPathList.get(newPathList.size() - 1)); - } - if (resultMap.isEmpty()) { - result.success(null); - } else { - result.success(resultMap); + result.setPaths(newPathList); } + cache.clear(); + + return result.build(); } public void chooseVideoFromGallery( - VideoOptions options, boolean usePhotoPicker, MethodChannel.Result result) { + VideoSelectionOptions options, boolean usePhotoPicker, Messages.Result> result) { if (!setPendingOptionsAndResult(null, options, result)) { finishWithAlreadyActiveError(result); return; @@ -296,7 +304,8 @@ private void launchPickVideoFromGalleryIntent(Boolean useAndroidPhotoPicker) { activity.startActivityForResult(pickVideoIntent, REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY); } - public void takeVideoWithCamera(VideoOptions options, MethodChannel.Result result) { + public void takeVideoWithCamera( + VideoSelectionOptions options, Messages.Result> result) { if (!setPendingOptionsAndResult(null, options, result)) { finishWithAlreadyActiveError(result); return; @@ -316,8 +325,8 @@ private void launchTakeVideoWithCameraIntent() { Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); if (pendingCallState != null && pendingCallState.videoOptions != null - && pendingCallState.videoOptions.maxDuration != null) { - int maxSeconds = pendingCallState.videoOptions.maxDuration; + && pendingCallState.videoOptions.getMaxDurationSeconds() != null) { + int maxSeconds = pendingCallState.videoOptions.getMaxDurationSeconds().intValue(); intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, maxSeconds); } if (cameraDevice == CameraDevice.FRONT) { @@ -346,7 +355,9 @@ private void launchTakeVideoWithCameraIntent() { } public void chooseImageFromGallery( - @NonNull ImageOutputOptions options, boolean usePhotoPicker, MethodChannel.Result result) { + @NonNull ImageSelectionOptions options, + boolean usePhotoPicker, + Messages.Result> result) { if (!setPendingOptionsAndResult(options, null, result)) { finishWithAlreadyActiveError(result); return; @@ -356,7 +367,9 @@ public void chooseImageFromGallery( } public void chooseMultiImageFromGallery( - @NonNull ImageOutputOptions options, boolean usePhotoPicker, MethodChannel.Result result) { + @NonNull ImageSelectionOptions options, + boolean usePhotoPicker, + Messages.Result> result) { if (!setPendingOptionsAndResult(options, null, result)) { finishWithAlreadyActiveError(result); return; @@ -405,7 +418,7 @@ private void launchMultiPickImageFromGalleryIntent(Boolean useAndroidPhotoPicker } public void takeImageWithCamera( - @NonNull ImageOutputOptions options, MethodChannel.Result result) { + @NonNull ImageSelectionOptions options, Messages.Result> result) { if (!setPendingOptionsAndResult(options, null, result)) { finishWithAlreadyActiveError(result); return; @@ -493,7 +506,7 @@ private void grantUriPermissions(Intent intent, Uri imageUri) { @Override public boolean onRequestPermissionsResult( - int requestCode, String[] permissions, int[] grantResults) { + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { boolean permissionGranted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED; @@ -661,9 +674,12 @@ private void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled } } - private String getResizedImagePath(String path, @NonNull ImageOutputOptions outputOptions) { + private String getResizedImagePath(String path, @NonNull ImageSelectionOptions outputOptions) { return imageResizer.resizeImageIfNeeded( - path, outputOptions.maxWidth, outputOptions.maxHeight, outputOptions.quality); + path, + outputOptions.getMaxWidth(), + outputOptions.getMaxHeight(), + outputOptions.getQuality().intValue()); } private void handleVideoResult(String path) { @@ -671,9 +687,9 @@ private void handleVideoResult(String path) { } private boolean setPendingOptionsAndResult( - @Nullable ImageOutputOptions imageOptions, - @Nullable VideoOptions videoOptions, - @NonNull MethodChannel.Result result) { + @Nullable ImageSelectionOptions imageOptions, + @Nullable VideoSelectionOptions videoOptions, + @NonNull Messages.Result> result) { if (pendingCallState != null) { return false; } @@ -691,16 +707,18 @@ private boolean setPendingOptionsAndResult( // A null imagePath indicates that the image picker was cancelled without // selection. private void finishWithSuccess(@Nullable String imagePath) { + ArrayList pathList = new ArrayList<>(); + if (imagePath != null) { + pathList.add(imagePath); + } if (pendingCallState == null) { // Only save data for later retrieval if something was actually selected. - if (imagePath != null) { - ArrayList pathList = new ArrayList<>(); - pathList.add(imagePath); + if (!pathList.isEmpty()) { cache.saveResult(pathList, null, null); } return; } - pendingCallState.result.success(imagePath); + pendingCallState.result.success(pathList); pendingCallState = null; } @@ -713,8 +731,8 @@ private void finishWithListSuccess(ArrayList imagePaths) { pendingCallState = null; } - private void finishWithAlreadyActiveError(MethodChannel.Result result) { - result.error("already_active", "Image picker is already active", null); + private void finishWithAlreadyActiveError(Messages.Result> result) { + result.error(new FlutterError("already_active", "Image picker is already active", null)); } private void finishWithError(String errorCode, String errorMessage) { @@ -722,7 +740,7 @@ private void finishWithError(String errorCode, String errorMessage) { cache.saveResult(null, errorCode, errorMessage); return; } - pendingCallState.result.error(errorCode, errorMessage, null); + pendingCallState.result.error(new FlutterError(errorCode, errorMessage, null)); pendingCallState = null; } diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java index 4a4b3114c5bf..cbf52bd07782 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java @@ -7,9 +7,8 @@ import android.app.Activity; import android.app.Application; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.Lifecycle; @@ -19,14 +18,16 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugins.imagepicker.Messages.FlutterError; +import io.flutter.plugins.imagepicker.Messages.ImagePickerApi; +import io.flutter.plugins.imagepicker.Messages.Result; +import io.flutter.plugins.imagepicker.Messages.SourceSpecification; import java.io.File; +import java.util.List; @SuppressWarnings("deprecation") -public class ImagePickerPlugin - implements MethodChannel.MethodCallHandler, FlutterPlugin, ActivityAware { +public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePickerApi { private class LifeCycleObserver implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver { @@ -98,9 +99,9 @@ private class ActivityState { private Application application; private Activity activity; private ImagePickerDelegate delegate; - private MethodChannel channel; private LifeCycleObserver observer; private ActivityPluginBinding activityBinding; + private BinaryMessenger messenger; // This is null when not using v2 embedding; private Lifecycle lifecycle; @@ -110,16 +111,16 @@ private class ActivityState { final Application application, final Activity activity, final BinaryMessenger messenger, - final MethodChannel.MethodCallHandler handler, + final ImagePickerApi handler, final PluginRegistry.Registrar registrar, final ActivityPluginBinding activityBinding) { this.application = application; this.activity = activity; this.activityBinding = activityBinding; + this.messenger = messenger; delegate = constructDelegate(activity); - channel = new MethodChannel(messenger, CHANNEL); - channel.setMethodCallHandler(handler); + ImagePickerApi.setup(messenger, handler); observer = new LifeCycleObserver(activity); if (registrar != null) { // V1 embedding setup for activity listeners. @@ -153,10 +154,7 @@ void release() { lifecycle = null; } - if (channel != null) { - channel.setMethodCallHandler(null); - channel = null; - } + ImagePickerApi.setup(messenger, null); if (application != null) { application.unregisterActivityLifecycleCallbacks(observer); @@ -177,17 +175,6 @@ ImagePickerDelegate getDelegate() { } } - static final String METHOD_CALL_IMAGE = "pickImage"; - static final String METHOD_CALL_MULTI_IMAGE = "pickMultiImage"; - static final String METHOD_CALL_VIDEO = "pickVideo"; - private static final String METHOD_CALL_RETRIEVE = "retrieve"; - private static final int CAMERA_DEVICE_FRONT = 1; - private static final int CAMERA_DEVICE_REAR = 0; - private static final String CHANNEL = "plugins.flutter.io/image_picker_android"; - - private static final int SOURCE_CAMERA = 0; - private static final int SOURCE_GALLERY = 1; - private FlutterPluginBinding pluginBinding; private ActivityState activityState; @@ -226,17 +213,17 @@ final ActivityState getActivityState() { } @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { pluginBinding = binding; } @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { pluginBinding = null; } @Override - public void onAttachedToActivity(ActivityPluginBinding binding) { + public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { setup( pluginBinding.getBinaryMessenger(), (Application) pluginBinding.getApplicationContext(), @@ -256,7 +243,7 @@ public void onDetachedFromActivityForConfigChanges() { } @Override - public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) { onAttachedToActivity(binding); } @@ -287,123 +274,99 @@ final ImagePickerDelegate constructDelegate(final Activity setupActivity) { return new ImagePickerDelegate(setupActivity, externalFilesDirectory, imageResizer, cache); } - // MethodChannel.Result wrapper that responds on the platform thread. - private static class MethodResultWrapper implements MethodChannel.Result { - private MethodChannel.Result methodResult; - private Handler handler; - - MethodResultWrapper(MethodChannel.Result result) { - methodResult = result; - handler = new Handler(Looper.getMainLooper()); + private @Nullable ImagePickerDelegate getImagePickerDelegate() { + if (activityState == null || activityState.getActivity() == null) { + return null; } + return activityState.getDelegate(); + } - @Override - public void success(final Object result) { - handler.post( - new Runnable() { - @Override - public void run() { - methodResult.success(result); - } - }); + private void setCameraDevice( + @NonNull ImagePickerDelegate delegate, @NonNull SourceSpecification source) { + Messages.SourceCamera camera = source.getCamera(); + if (camera != null) { + ImagePickerDelegate.CameraDevice device; + switch (camera) { + case FRONT: + device = ImagePickerDelegate.CameraDevice.FRONT; + break; + case REAR: + default: + device = ImagePickerDelegate.CameraDevice.REAR; + break; + } + delegate.setCameraDevice(device); } + } - @Override - public void error( - final String errorCode, final String errorMessage, final Object errorDetails) { - handler.post( - new Runnable() { - @Override - public void run() { - methodResult.error(errorCode, errorMessage, errorDetails); - } - }); + @Override + public void pickImages( + @NonNull SourceSpecification source, + @NonNull Messages.ImageSelectionOptions options, + @NonNull Boolean allowMultiple, + @NonNull Boolean usePhotoPicker, + Result> result) { + ImagePickerDelegate delegate = getImagePickerDelegate(); + if (delegate == null) { + result.error( + new FlutterError( + "no_activity", "image_picker plugin requires a foreground activity.", null)); + return; } - @Override - public void notImplemented() { - handler.post( - new Runnable() { - @Override - public void run() { - methodResult.notImplemented(); - } - }); + setCameraDevice(delegate, source); + if (allowMultiple) { + delegate.chooseMultiImageFromGallery(options, usePhotoPicker, result); + } else { + switch (source.getType()) { + case GALLERY: + delegate.chooseImageFromGallery(options, usePhotoPicker, result); + break; + case CAMERA: + delegate.takeImageWithCamera(options, result); + break; + } } } @Override - public void onMethodCall(MethodCall call, MethodChannel.Result rawResult) { - if (activityState == null || activityState.getActivity() == null) { - rawResult.error("no_activity", "image_picker plugin requires a foreground activity.", null); + public void pickVideos( + @NonNull SourceSpecification source, + @NonNull Messages.VideoSelectionOptions options, + @NonNull Boolean allowMultiple, + @NonNull Boolean usePhotoPicker, + Result> result) { + ImagePickerDelegate delegate = getImagePickerDelegate(); + if (delegate == null) { + result.error( + new FlutterError( + "no_activity", "image_picker plugin requires a foreground activity.", null)); return; } - MethodChannel.Result result = new MethodResultWrapper(rawResult); - int imageSource; - ImagePickerDelegate delegate = activityState.getDelegate(); - if (call.argument("cameraDevice") != null) { - ImagePickerDelegate.CameraDevice device; - int deviceIntValue = call.argument("cameraDevice"); - if (deviceIntValue == CAMERA_DEVICE_FRONT) { - device = ImagePickerDelegate.CameraDevice.FRONT; - } else { - device = ImagePickerDelegate.CameraDevice.REAR; - } - delegate.setCameraDevice(device); - } - Boolean usePhotoPicker = call.argument("useAndroidPhotoPicker"); - if (usePhotoPicker == null) { - usePhotoPicker = false; + setCameraDevice(delegate, source); + if (allowMultiple) { + result.error(new RuntimeException("Multi-video selection is not implemented")); + } else { + switch (source.getType()) { + case GALLERY: + delegate.chooseVideoFromGallery(options, usePhotoPicker, result); + break; + case CAMERA: + delegate.takeVideoWithCamera(options, result); + break; + } } + } - switch (call.method) { - case METHOD_CALL_IMAGE: - imageSource = call.argument("source"); - ImageOutputOptions imageOptions = - new ImageOutputOptions( - call.argument("maxWidth"), - call.argument("maxHeight"), - call.argument("imageQuality")); - switch (imageSource) { - case SOURCE_GALLERY: - delegate.chooseImageFromGallery(imageOptions, usePhotoPicker, result); - break; - case SOURCE_CAMERA: - delegate.takeImageWithCamera(imageOptions, result); - break; - default: - throw new IllegalArgumentException("Invalid image source: " + imageSource); - } - break; - case METHOD_CALL_MULTI_IMAGE: - delegate.chooseMultiImageFromGallery( - new ImageOutputOptions( - call.argument("maxWidth"), - call.argument("maxHeight"), - call.argument("imageQuality")), - usePhotoPicker, - result); - break; - case METHOD_CALL_VIDEO: - imageSource = call.argument("source"); - VideoOptions videoOptions = new VideoOptions(call.argument("maxDuration")); - switch (imageSource) { - case SOURCE_GALLERY: - delegate.chooseVideoFromGallery(videoOptions, usePhotoPicker, result); - break; - case SOURCE_CAMERA: - delegate.takeVideoWithCamera(videoOptions, result); - break; - default: - throw new IllegalArgumentException("Invalid video source: " + imageSource); - } - break; - case METHOD_CALL_RETRIEVE: - delegate.retrieveLostImage(result); - break; - default: - throw new IllegalArgumentException("Unknown method " + call.method); + @Nullable + @Override + public Messages.CacheRetrievalResult retrieveLostResults() { + ImagePickerDelegate delegate = getImagePickerDelegate(); + if (delegate == null) { + throw new FlutterError( + "no_activity", "image_picker plugin requires a foreground activity.", null); } + return delegate.retrieveLostImage(); } } diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java new file mode 100644 index 000000000000..af98078c673c --- /dev/null +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java @@ -0,0 +1,685 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v9.1.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +package io.flutter.plugins.imagepicker; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) +public class Messages { + + /** Error class for passing custom error details to Flutter via a thrown PlatformException. */ + public static class FlutterError extends RuntimeException { + + /** The error code. */ + public final String code; + + /** The error details. Must be a datatype supported by the api codec. */ + public final Object details; + + public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) { + super(message); + this.code = code; + this.details = details; + } + } + + @NonNull + private static ArrayList wrapError(@NonNull Throwable exception) { + ArrayList errorList = new ArrayList(3); + if (exception instanceof FlutterError) { + FlutterError error = (FlutterError) exception; + errorList.add(error.code); + errorList.add(error.getMessage()); + errorList.add(error.details); + } else { + errorList.add(exception.toString()); + errorList.add(exception.getClass().getSimpleName()); + errorList.add( + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + } + return errorList; + } + + public enum SourceCamera { + REAR(0), + FRONT(1); + + private final int index; + + private SourceCamera(final int index) { + this.index = index; + } + } + + public enum SourceType { + CAMERA(0), + GALLERY(1); + + private final int index; + + private SourceType(final int index) { + this.index = index; + } + } + + public enum CacheRetrievalType { + IMAGE(0), + VIDEO(1); + + private final int index; + + private CacheRetrievalType(final int index) { + this.index = index; + } + } + + /** + * Options for image selection and output. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class ImageSelectionOptions { + /** If set, the max width that the image should be resized to fit in. */ + private @Nullable Double maxWidth; + + public @Nullable Double getMaxWidth() { + return maxWidth; + } + + public void setMaxWidth(@Nullable Double setterArg) { + this.maxWidth = setterArg; + } + + /** If set, the max height that the image should be resized to fit in. */ + private @Nullable Double maxHeight; + + public @Nullable Double getMaxHeight() { + return maxHeight; + } + + public void setMaxHeight(@Nullable Double setterArg) { + this.maxHeight = setterArg; + } + + /** + * The quality of the output image, from 0-100. + * + *

100 indicates original quality. + */ + private @NonNull Long quality; + + public @NonNull Long getQuality() { + return quality; + } + + public void setQuality(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"quality\" is null."); + } + this.quality = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private ImageSelectionOptions() {} + + public static final class Builder { + + private @Nullable Double maxWidth; + + public @NonNull Builder setMaxWidth(@Nullable Double setterArg) { + this.maxWidth = setterArg; + return this; + } + + private @Nullable Double maxHeight; + + public @NonNull Builder setMaxHeight(@Nullable Double setterArg) { + this.maxHeight = setterArg; + return this; + } + + private @Nullable Long quality; + + public @NonNull Builder setQuality(@NonNull Long setterArg) { + this.quality = setterArg; + return this; + } + + public @NonNull ImageSelectionOptions build() { + ImageSelectionOptions pigeonReturn = new ImageSelectionOptions(); + pigeonReturn.setMaxWidth(maxWidth); + pigeonReturn.setMaxHeight(maxHeight); + pigeonReturn.setQuality(quality); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(3); + toListResult.add(maxWidth); + toListResult.add(maxHeight); + toListResult.add(quality); + return toListResult; + } + + static @NonNull ImageSelectionOptions fromList(@NonNull ArrayList list) { + ImageSelectionOptions pigeonResult = new ImageSelectionOptions(); + Object maxWidth = list.get(0); + pigeonResult.setMaxWidth((Double) maxWidth); + Object maxHeight = list.get(1); + pigeonResult.setMaxHeight((Double) maxHeight); + Object quality = list.get(2); + pigeonResult.setQuality( + (quality == null) + ? null + : ((quality instanceof Integer) ? (Integer) quality : (Long) quality)); + return pigeonResult; + } + } + + /** + * Options for image selection and output. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class VideoSelectionOptions { + /** The maximum desired length for the video, in seconds. */ + private @Nullable Long maxDurationSeconds; + + public @Nullable Long getMaxDurationSeconds() { + return maxDurationSeconds; + } + + public void setMaxDurationSeconds(@Nullable Long setterArg) { + this.maxDurationSeconds = setterArg; + } + + public static final class Builder { + + private @Nullable Long maxDurationSeconds; + + public @NonNull Builder setMaxDurationSeconds(@Nullable Long setterArg) { + this.maxDurationSeconds = setterArg; + return this; + } + + public @NonNull VideoSelectionOptions build() { + VideoSelectionOptions pigeonReturn = new VideoSelectionOptions(); + pigeonReturn.setMaxDurationSeconds(maxDurationSeconds); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(1); + toListResult.add(maxDurationSeconds); + return toListResult; + } + + static @NonNull VideoSelectionOptions fromList(@NonNull ArrayList list) { + VideoSelectionOptions pigeonResult = new VideoSelectionOptions(); + Object maxDurationSeconds = list.get(0); + pigeonResult.setMaxDurationSeconds( + (maxDurationSeconds == null) + ? null + : ((maxDurationSeconds instanceof Integer) + ? (Integer) maxDurationSeconds + : (Long) maxDurationSeconds)); + return pigeonResult; + } + } + + /** + * Specification for the source of an image or video selection. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class SourceSpecification { + private @NonNull SourceType type; + + public @NonNull SourceType getType() { + return type; + } + + public void setType(@NonNull SourceType setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"type\" is null."); + } + this.type = setterArg; + } + + private @Nullable SourceCamera camera; + + public @Nullable SourceCamera getCamera() { + return camera; + } + + public void setCamera(@Nullable SourceCamera setterArg) { + this.camera = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private SourceSpecification() {} + + public static final class Builder { + + private @Nullable SourceType type; + + public @NonNull Builder setType(@NonNull SourceType setterArg) { + this.type = setterArg; + return this; + } + + private @Nullable SourceCamera camera; + + public @NonNull Builder setCamera(@Nullable SourceCamera setterArg) { + this.camera = setterArg; + return this; + } + + public @NonNull SourceSpecification build() { + SourceSpecification pigeonReturn = new SourceSpecification(); + pigeonReturn.setType(type); + pigeonReturn.setCamera(camera); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(type == null ? null : type.index); + toListResult.add(camera == null ? null : camera.index); + return toListResult; + } + + static @NonNull SourceSpecification fromList(@NonNull ArrayList list) { + SourceSpecification pigeonResult = new SourceSpecification(); + Object type = list.get(0); + pigeonResult.setType(type == null ? null : SourceType.values()[(int) type]); + Object camera = list.get(1); + pigeonResult.setCamera(camera == null ? null : SourceCamera.values()[(int) camera]); + return pigeonResult; + } + } + + /** + * An error that occurred during lost result retrieval. + * + *

The data here maps to the `PlatformException` that will be created from it. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class CacheRetrievalError { + private @NonNull String code; + + public @NonNull String getCode() { + return code; + } + + public void setCode(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"code\" is null."); + } + this.code = setterArg; + } + + private @Nullable String message; + + public @Nullable String getMessage() { + return message; + } + + public void setMessage(@Nullable String setterArg) { + this.message = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private CacheRetrievalError() {} + + public static final class Builder { + + private @Nullable String code; + + public @NonNull Builder setCode(@NonNull String setterArg) { + this.code = setterArg; + return this; + } + + private @Nullable String message; + + public @NonNull Builder setMessage(@Nullable String setterArg) { + this.message = setterArg; + return this; + } + + public @NonNull CacheRetrievalError build() { + CacheRetrievalError pigeonReturn = new CacheRetrievalError(); + pigeonReturn.setCode(code); + pigeonReturn.setMessage(message); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(code); + toListResult.add(message); + return toListResult; + } + + static @NonNull CacheRetrievalError fromList(@NonNull ArrayList list) { + CacheRetrievalError pigeonResult = new CacheRetrievalError(); + Object code = list.get(0); + pigeonResult.setCode((String) code); + Object message = list.get(1); + pigeonResult.setMessage((String) message); + return pigeonResult; + } + } + + /** + * The result of retrieving cached results from a previous run. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class CacheRetrievalResult { + /** The type of the retrieved data. */ + private @NonNull CacheRetrievalType type; + + public @NonNull CacheRetrievalType getType() { + return type; + } + + public void setType(@NonNull CacheRetrievalType setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"type\" is null."); + } + this.type = setterArg; + } + + /** The error from the last selection, if any. */ + private @Nullable CacheRetrievalError error; + + public @Nullable CacheRetrievalError getError() { + return error; + } + + public void setError(@Nullable CacheRetrievalError setterArg) { + this.error = setterArg; + } + + /** + * The results from the last selection, if any. + * + *

Elements must not be null, by convention. See + * https://github.com/flutter/flutter/issues/97848 + */ + private @NonNull List paths; + + public @NonNull List getPaths() { + return paths; + } + + public void setPaths(@NonNull List setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"paths\" is null."); + } + this.paths = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private CacheRetrievalResult() {} + + public static final class Builder { + + private @Nullable CacheRetrievalType type; + + public @NonNull Builder setType(@NonNull CacheRetrievalType setterArg) { + this.type = setterArg; + return this; + } + + private @Nullable CacheRetrievalError error; + + public @NonNull Builder setError(@Nullable CacheRetrievalError setterArg) { + this.error = setterArg; + return this; + } + + private @Nullable List paths; + + public @NonNull Builder setPaths(@NonNull List setterArg) { + this.paths = setterArg; + return this; + } + + public @NonNull CacheRetrievalResult build() { + CacheRetrievalResult pigeonReturn = new CacheRetrievalResult(); + pigeonReturn.setType(type); + pigeonReturn.setError(error); + pigeonReturn.setPaths(paths); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(3); + toListResult.add(type == null ? null : type.index); + toListResult.add((error == null) ? null : error.toList()); + toListResult.add(paths); + return toListResult; + } + + static @NonNull CacheRetrievalResult fromList(@NonNull ArrayList list) { + CacheRetrievalResult pigeonResult = new CacheRetrievalResult(); + Object type = list.get(0); + pigeonResult.setType(type == null ? null : CacheRetrievalType.values()[(int) type]); + Object error = list.get(1); + pigeonResult.setError( + (error == null) ? null : CacheRetrievalError.fromList((ArrayList) error)); + Object paths = list.get(2); + pigeonResult.setPaths((List) paths); + return pigeonResult; + } + } + + public interface Result { + void success(T result); + + void error(Throwable error); + } + + private static class ImagePickerApiCodec extends StandardMessageCodec { + public static final ImagePickerApiCodec INSTANCE = new ImagePickerApiCodec(); + + private ImagePickerApiCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return CacheRetrievalError.fromList((ArrayList) readValue(buffer)); + case (byte) 129: + return CacheRetrievalResult.fromList((ArrayList) readValue(buffer)); + case (byte) 130: + return ImageSelectionOptions.fromList((ArrayList) readValue(buffer)); + case (byte) 131: + return SourceSpecification.fromList((ArrayList) readValue(buffer)); + case (byte) 132: + return VideoSelectionOptions.fromList((ArrayList) readValue(buffer)); + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof CacheRetrievalError) { + stream.write(128); + writeValue(stream, ((CacheRetrievalError) value).toList()); + } else if (value instanceof CacheRetrievalResult) { + stream.write(129); + writeValue(stream, ((CacheRetrievalResult) value).toList()); + } else if (value instanceof ImageSelectionOptions) { + stream.write(130); + writeValue(stream, ((ImageSelectionOptions) value).toList()); + } else if (value instanceof SourceSpecification) { + stream.write(131); + writeValue(stream, ((SourceSpecification) value).toList()); + } else if (value instanceof VideoSelectionOptions) { + stream.write(132); + writeValue(stream, ((VideoSelectionOptions) value).toList()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface ImagePickerApi { + /** + * Selects images and returns their paths. + * + *

Elements must not be null, by convention. See + * https://github.com/flutter/flutter/issues/97848 + */ + void pickImages( + @NonNull SourceSpecification source, + @NonNull ImageSelectionOptions options, + @NonNull Boolean allowMultiple, + @NonNull Boolean usePhotoPicker, + Result> result); + /** + * Selects video and returns their paths. + * + *

Elements must not be null, by convention. See + * https://github.com/flutter/flutter/issues/97848 + */ + void pickVideos( + @NonNull SourceSpecification source, + @NonNull VideoSelectionOptions options, + @NonNull Boolean allowMultiple, + @NonNull Boolean usePhotoPicker, + Result> result); + /** Returns results from a previous app session, if any. */ + @Nullable + CacheRetrievalResult retrieveLostResults(); + + /** The codec used by ImagePickerApi. */ + static MessageCodec getCodec() { + return ImagePickerApiCodec.INSTANCE; + } + /** Sets up an instance of `ImagePickerApi` to handle messages through the `binaryMessenger`. */ + static void setup(BinaryMessenger binaryMessenger, ImagePickerApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ImagePickerApi.pickImages", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + SourceSpecification sourceArg = (SourceSpecification) args.get(0); + ImageSelectionOptions optionsArg = (ImageSelectionOptions) args.get(1); + Boolean allowMultipleArg = (Boolean) args.get(2); + Boolean usePhotoPickerArg = (Boolean) args.get(3); + Result> resultCallback = + new Result>() { + public void success(List result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.pickImages( + sourceArg, optionsArg, allowMultipleArg, usePhotoPickerArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ImagePickerApi.pickVideos", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + SourceSpecification sourceArg = (SourceSpecification) args.get(0); + VideoSelectionOptions optionsArg = (VideoSelectionOptions) args.get(1); + Boolean allowMultipleArg = (Boolean) args.get(2); + Boolean usePhotoPickerArg = (Boolean) args.get(3); + Result> resultCallback = + new Result>() { + public void success(List result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.pickVideos( + sourceArg, optionsArg, allowMultipleArg, usePhotoPickerArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.ImagePickerApi.retrieveLostResults", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + CacheRetrievalResult output = api.retrieveLostResults(); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } +} diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/VideoOptions.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/VideoOptions.java deleted file mode 100644 index e3d73204d451..000000000000 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/VideoOptions.java +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.imagepicker; - -import androidx.annotation.Nullable; - -/** Stores settings for video selection and output options. */ -public class VideoOptions { - @Nullable public final Integer maxDuration; - - public VideoOptions(@Nullable Integer maxDuration) { - this.maxDuration = maxDuration; - } -} diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java index aa6fbc75ee81..2b8cca5b314a 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java @@ -105,12 +105,14 @@ public void tearDown() throws Exception { public void imageCache_shouldBeAbleToSetAndGetQuality() { final int quality = 90; ImagePickerCache cache = new ImagePickerCache(mockActivity); - cache.saveDimensionWithOutputOptions(new ImageOutputOptions(null, null, quality)); + cache.saveDimensionWithOutputOptions( + new Messages.ImageSelectionOptions.Builder().setQuality((long) quality).build()); Map resultMap = cache.getCacheMap(); int imageQuality = (int) resultMap.get(ImagePickerCache.MAP_KEY_IMAGE_QUALITY); assertThat(imageQuality, equalTo(quality)); - cache.saveDimensionWithOutputOptions(new ImageOutputOptions(null, null, null)); + cache.saveDimensionWithOutputOptions( + new Messages.ImageSelectionOptions.Builder().setQuality((long) 100).build()); Map resultMapWithDefaultQuality = cache.getCacheMap(); int defaultImageQuality = (int) resultMapWithDefaultQuality.get(ImagePickerCache.MAP_KEY_IMAGE_QUALITY); 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 0c9fdb94fb2c..77a34b452b0e 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 @@ -10,7 +10,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -27,11 +26,12 @@ import android.content.pm.PackageManager; import android.net.Uri; import androidx.annotation.Nullable; -import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.imagepicker.Messages.FlutterError; +import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions; +import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions; import java.io.File; import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; +import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -48,15 +48,18 @@ public class ImagePickerDelegateTest { private static final Double WIDTH = 10.0; private static final Double HEIGHT = 10.0; - private static final int MAX_DURATION = 10; + private static final Long MAX_DURATION = 10L; private static final Integer IMAGE_QUALITY = 90; - private static final ImageOutputOptions DEFAULT_IMAGE_OPTIONS = - new ImageOutputOptions(null, null, null); - private static final VideoOptions DEFAULT_VIDEO_OPTIONS = new VideoOptions(null); + private static final ImageSelectionOptions DEFAULT_IMAGE_OPTIONS = + new ImageSelectionOptions.Builder().setQuality((long) 100).build(); + private static final ImageSelectionOptions RESIZE_TRIGGERING_IMAGE_OPTIONS = + new ImageSelectionOptions.Builder().setQuality((long) 100).setMaxWidth(WIDTH).build(); + private static final VideoSelectionOptions DEFAULT_VIDEO_OPTIONS = + new VideoSelectionOptions.Builder().build(); @Mock Activity mockActivity; @Mock ImageResizer mockImageResizer; - @Mock MethodChannel.Result mockResult; + @Mock Messages.Result> mockResult; @Mock ImagePickerDelegate.PermissionManager mockPermissionManager; @Mock FileUtils mockFileUtils; @Mock Intent mockIntent; @@ -128,7 +131,7 @@ public void chooseImageFromGallery_whenPendingResultExists_finishesWithAlreadyAc ImagePickerDelegate delegate = createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null); - delegate.chooseImageFromGallery(new ImageOutputOptions(null, null, null), false, mockResult); + delegate.chooseImageFromGallery(DEFAULT_IMAGE_OPTIONS, false, mockResult); verifyFinishedWithAlreadyActiveError(); verifyNoMoreInteractions(mockResult); @@ -139,8 +142,7 @@ public void chooseMultiImageFromGallery_whenPendingResultExists_finishesWithAlre ImagePickerDelegate delegate = createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null); - delegate.chooseMultiImageFromGallery( - new ImageOutputOptions(null, null, null), false, mockResult); + delegate.chooseMultiImageFromGallery(DEFAULT_IMAGE_OPTIONS, false, mockResult); verifyFinishedWithAlreadyActiveError(); verifyNoMoreInteractions(mockResult); @@ -151,7 +153,7 @@ public void chooseMultiImageFromGallery_whenPendingResultExists_finishesWithAlre public void chooseImageFromGallery_launchesChooseFromGalleryIntent() { ImagePickerDelegate delegate = createDelegate(); - delegate.chooseImageFromGallery(new ImageOutputOptions(null, null, null), false, mockResult); + delegate.chooseImageFromGallery(DEFAULT_IMAGE_OPTIONS, false, mockResult); verify(mockActivity) .startActivityForResult( @@ -163,7 +165,7 @@ public void chooseImageFromGallery_launchesChooseFromGalleryIntent() { public void chooseImageFromGallery_withPhotoPicker_launchesChooseFromGalleryIntent() { ImagePickerDelegate delegate = createDelegate(); - delegate.chooseImageFromGallery(new ImageOutputOptions(null, null, null), true, mockResult); + delegate.chooseImageFromGallery(DEFAULT_IMAGE_OPTIONS, true, mockResult); verify(mockActivity) .startActivityForResult( @@ -175,8 +177,7 @@ public void chooseImageFromGallery_withPhotoPicker_launchesChooseFromGalleryInte public void chooseMultiImageFromGallery_launchesChooseFromGalleryIntent() { ImagePickerDelegate delegate = createDelegate(); - delegate.chooseMultiImageFromGallery( - new ImageOutputOptions(null, null, null), true, mockResult); + delegate.chooseMultiImageFromGallery(DEFAULT_IMAGE_OPTIONS, true, mockResult); verify(mockActivity) .startActivityForResult( @@ -189,8 +190,7 @@ public void chooseMultiImageFromGallery_launchesChooseFromGalleryIntent() { public void chooseMultiImageFromGallery_withPhotoPicker_launchesChooseFromGalleryIntent() { ImagePickerDelegate delegate = createDelegate(); - delegate.chooseMultiImageFromGallery( - new ImageOutputOptions(null, null, null), false, mockResult); + delegate.chooseMultiImageFromGallery(DEFAULT_IMAGE_OPTIONS, false, mockResult); verify(mockActivity) .startActivityForResult( @@ -203,7 +203,7 @@ public void chooseMultiImageFromGallery_withPhotoPicker_launchesChooseFromGaller public void chooseVideoFromGallery_launchesChooseFromGalleryIntent() { ImagePickerDelegate delegate = createDelegate(); - delegate.chooseVideoFromGallery(new VideoOptions(null), true, mockResult); + delegate.chooseVideoFromGallery(DEFAULT_VIDEO_OPTIONS, true, mockResult); verify(mockActivity) .startActivityForResult( @@ -215,7 +215,7 @@ public void chooseVideoFromGallery_launchesChooseFromGalleryIntent() { public void chooseVideoFromGallery_withPhotoPicker_launchesChooseFromGalleryIntent() { ImagePickerDelegate delegate = createDelegate(); - delegate.chooseVideoFromGallery(new VideoOptions(null), true, mockResult); + delegate.chooseVideoFromGallery(DEFAULT_VIDEO_OPTIONS, true, mockResult); verify(mockActivity) .startActivityForResult( @@ -227,7 +227,7 @@ public void takeImageWithCamera_whenPendingResultExists_finishesWithAlreadyActiv ImagePickerDelegate delegate = createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null); - delegate.takeImageWithCamera(new ImageOutputOptions(null, null, null), mockResult); + delegate.takeImageWithCamera(DEFAULT_IMAGE_OPTIONS, mockResult); verifyFinishedWithAlreadyActiveError(); verifyNoMoreInteractions(mockResult); @@ -239,7 +239,7 @@ public void takeImageWithCamera_whenHasNoCameraPermission_requestsForPermission( when(mockPermissionManager.needRequestCameraPermission()).thenReturn(true); ImagePickerDelegate delegate = createDelegate(); - delegate.takeImageWithCamera(new ImageOutputOptions(null, null, null), mockResult); + delegate.takeImageWithCamera(DEFAULT_IMAGE_OPTIONS, mockResult); verify(mockPermissionManager) .askForPermission( @@ -251,7 +251,7 @@ public void takeImageWithCamera_whenCameraPermissionNotPresent_requestsForPermis when(mockPermissionManager.needRequestCameraPermission()).thenReturn(false); ImagePickerDelegate delegate = createDelegate(); - delegate.takeImageWithCamera(new ImageOutputOptions(null, null, null), mockResult); + delegate.takeImageWithCamera(DEFAULT_IMAGE_OPTIONS, mockResult); verify(mockActivity) .startActivityForResult( @@ -264,7 +264,7 @@ public void takeImageWithCamera_whenCameraPermissionNotPresent_requestsForPermis when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true); ImagePickerDelegate delegate = createDelegate(); - delegate.takeImageWithCamera(new ImageOutputOptions(null, null, null), mockResult); + delegate.takeImageWithCamera(DEFAULT_IMAGE_OPTIONS, mockResult); verify(mockActivity) .startActivityForResult( @@ -279,10 +279,12 @@ public void takeImageWithCamera_whenCameraPermissionNotPresent_requestsForPermis .when(mockActivity) .startActivityForResult(any(Intent.class), anyInt()); ImagePickerDelegate delegate = createDelegate(); - delegate.takeImageWithCamera(new ImageOutputOptions(null, null, null), mockResult); + delegate.takeImageWithCamera(DEFAULT_IMAGE_OPTIONS, mockResult); - verify(mockResult) - .error("no_available_camera", "No cameras available for taking pictures.", null); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); + verify(mockResult).error(errorCaptor.capture()); + assertEquals("no_available_camera", errorCaptor.getValue().code); + assertEquals("No cameras available for taking pictures.", errorCaptor.getValue().getMessage()); verifyNoMoreInteractions(mockResult); } @@ -291,7 +293,7 @@ public void takeImageWithCamera_writesImageToCacheDirectory() { when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true); ImagePickerDelegate delegate = createDelegate(); - delegate.takeImageWithCamera(new ImageOutputOptions(null, null, null), mockResult); + delegate.takeImageWithCamera(DEFAULT_IMAGE_OPTIONS, mockResult); mockStaticFile.verify( () -> File.createTempFile(any(), eq(".jpg"), eq(new File("/image_picker_cache"))), @@ -308,7 +310,10 @@ public void onRequestPermissionsResult_whenCameraPermissionDenied_finishesWithEr new String[] {Manifest.permission.CAMERA}, new int[] {PackageManager.PERMISSION_DENIED}); - verify(mockResult).error("camera_access_denied", "The user did not allow camera access.", null); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); + verify(mockResult).error(errorCaptor.capture()); + assertEquals("camera_access_denied", errorCaptor.getValue().code); + assertEquals("The user did not allow camera access.", errorCaptor.getValue().getMessage()); verifyNoMoreInteractions(mockResult); } @@ -343,14 +348,17 @@ public void onRequestPermissionsResult_whenCameraPermissionDenied_finishesWithEr } @Test - public void onActivityResult_whenPickFromGalleryCanceled_finishesWithNull() { + public void onActivityResult_whenPickFromGalleryCanceled_finishesWithEmptyList() { ImagePickerDelegate delegate = createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null); delegate.onActivityResult( ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, Activity.RESULT_CANCELED, null); - verify(mockResult).success(null); + @SuppressWarnings("unchecked") + ArgumentCaptor> pathListCapture = ArgumentCaptor.forClass(List.class); + verify(mockResult).success(pathListCapture.capture()); + assertEquals(0, pathListCapture.getValue().size()); verifyNoMoreInteractions(mockResult); } @@ -373,7 +381,10 @@ public void onActivityResult_whenPickFromGalleryCanceled_storesNothingInCache() delegate.onActivityResult( ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, Activity.RESULT_OK, mockIntent); - verify(mockResult).success("originalPath"); + @SuppressWarnings("unchecked") + ArgumentCaptor> pathListCapture = ArgumentCaptor.forClass(List.class); + verify(mockResult).success(pathListCapture.capture()); + assertEquals("originalPath", pathListCapture.getValue().get(0)); verifyNoMoreInteractions(mockResult); } @@ -394,11 +405,14 @@ public void onActivityResult_whenImagePickedFromGallery_andNoResizeNeeded_stores public void onActivityResult_whenImagePickedFromGallery_andResizeNeeded_finishesWithScaledImagePath() { ImagePickerDelegate delegate = - createDelegateWithPendingResultAndOptions(new ImageOutputOptions(WIDTH, null, null), null); + createDelegateWithPendingResultAndOptions(RESIZE_TRIGGERING_IMAGE_OPTIONS, null); delegate.onActivityResult( ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY, Activity.RESULT_OK, mockIntent); - verify(mockResult).success("scaledPath"); + @SuppressWarnings("unchecked") + ArgumentCaptor> pathListCapture = ArgumentCaptor.forClass(List.class); + verify(mockResult).success(pathListCapture.capture()); + assertEquals("scaledPath", pathListCapture.getValue().get(0)); verifyNoMoreInteractions(mockResult); } @@ -406,23 +420,29 @@ public void onActivityResult_whenImagePickedFromGallery_andNoResizeNeeded_stores public void onActivityResult_whenVideoPickedFromGallery_andResizeParametersSupplied_finishesWithFilePath() { ImagePickerDelegate delegate = - createDelegateWithPendingResultAndOptions(new ImageOutputOptions(WIDTH, null, null), null); + createDelegateWithPendingResultAndOptions(RESIZE_TRIGGERING_IMAGE_OPTIONS, null); delegate.onActivityResult( ImagePickerDelegate.REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY, Activity.RESULT_OK, mockIntent); - verify(mockResult).success("pathFromUri"); + @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_whenTakeImageWithCameraCanceled_finishesWithNull() { + public void onActivityResult_whenTakeImageWithCameraCanceled_finishesWithEmptyList() { ImagePickerDelegate delegate = createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null); delegate.onActivityResult( ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA, Activity.RESULT_CANCELED, null); - verify(mockResult).success(null); + @SuppressWarnings("unchecked") + ArgumentCaptor> pathListCapture = ArgumentCaptor.forClass(List.class); + verify(mockResult).success(pathListCapture.capture()); + assertEquals(0, pathListCapture.getValue().size()); verifyNoMoreInteractions(mockResult); } @@ -435,7 +455,10 @@ public void onActivityResult_whenImageTakenWithCamera_andNoResizeNeeded_finishes delegate.onActivityResult( ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA, Activity.RESULT_OK, mockIntent); - verify(mockResult).success("originalPath"); + @SuppressWarnings("unchecked") + ArgumentCaptor> pathListCapture = ArgumentCaptor.forClass(List.class); + verify(mockResult).success(pathListCapture.capture()); + assertEquals("originalPath", pathListCapture.getValue().get(0)); verifyNoMoreInteractions(mockResult); } @@ -445,11 +468,14 @@ public void onActivityResult_whenImageTakenWithCamera_andNoResizeNeeded_finishes when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString"); ImagePickerDelegate delegate = - createDelegateWithPendingResultAndOptions(new ImageOutputOptions(WIDTH, null, null), null); + createDelegateWithPendingResultAndOptions(RESIZE_TRIGGERING_IMAGE_OPTIONS, null); delegate.onActivityResult( ImagePickerDelegate.REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA, Activity.RESULT_OK, mockIntent); - verify(mockResult).success("scaledPath"); + @SuppressWarnings("unchecked") + ArgumentCaptor> pathListCapture = ArgumentCaptor.forClass(List.class); + verify(mockResult).success(pathListCapture.capture()); + assertEquals("scaledPath", pathListCapture.getValue().get(0)); verifyNoMoreInteractions(mockResult); } @@ -459,11 +485,14 @@ public void onActivityResult_whenImageTakenWithCamera_andNoResizeNeeded_finishes when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString"); ImagePickerDelegate delegate = - createDelegateWithPendingResultAndOptions(new ImageOutputOptions(WIDTH, null, null), null); + createDelegateWithPendingResultAndOptions(RESIZE_TRIGGERING_IMAGE_OPTIONS, null); delegate.onActivityResult( ImagePickerDelegate.REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA, Activity.RESULT_OK, mockIntent); - verify(mockResult).success("pathFromUri"); + @SuppressWarnings("unchecked") + ArgumentCaptor> pathListCapture = ArgumentCaptor.forClass(List.class); + verify(mockResult).success(pathListCapture.capture()); + assertEquals("pathFromUri", pathListCapture.getValue().get(0)); verifyNoMoreInteractions(mockResult); } @@ -473,41 +502,16 @@ public void onActivityResult_whenImageTakenWithCamera_andNoResizeNeeded_finishes when(cache.retrievePendingCameraMediaUriPath()).thenReturn("testString"); ImagePickerDelegate delegate = - createDelegateWithPendingResultAndOptions(null, new VideoOptions(MAX_DURATION)); + createDelegateWithPendingResultAndOptions( + null, new VideoSelectionOptions.Builder().setMaxDurationSeconds(MAX_DURATION).build()); delegate.onActivityResult( ImagePickerDelegate.REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA, Activity.RESULT_OK, mockIntent); - verify(mockResult).success("pathFromUri"); - verifyNoMoreInteractions(mockResult); - } - - @Test - public void - retrieveLostImage_shouldBeAbleToReturnLastItemFromResultMapWhenSingleFileIsRecovered() { - Map resultMap = new HashMap<>(); - ArrayList pathList = new ArrayList<>(); - pathList.add("/example/first_item"); - pathList.add("/example/last_item"); - resultMap.put("pathList", pathList); - - when(mockImageResizer.resizeImageIfNeeded(pathList.get(0), null, null, 100)) - .thenReturn(pathList.get(0)); - when(mockImageResizer.resizeImageIfNeeded(pathList.get(1), null, null, 100)) - .thenReturn(pathList.get(1)); - when(cache.getCacheMap()).thenReturn(resultMap); - - MethodChannel.Result mockResult = mock(MethodChannel.Result.class); - - ImagePickerDelegate mockDelegate = createDelegate(); - @SuppressWarnings("unchecked") - ArgumentCaptor> valueCapture = ArgumentCaptor.forClass(Map.class); - - doNothing().when(mockResult).success(valueCapture.capture()); - - mockDelegate.retrieveLostImage(mockResult); - - assertEquals("/example/last_item", valueCapture.getValue().get("path")); + ArgumentCaptor> pathListCapture = ArgumentCaptor.forClass(List.class); + verify(mockResult).success(pathListCapture.capture()); + assertEquals("pathFromUri", pathListCapture.getValue().get(0)); + verifyNoMoreInteractions(mockResult); } private ImagePickerDelegate createDelegate() { @@ -525,7 +529,7 @@ private ImagePickerDelegate createDelegate() { } private ImagePickerDelegate createDelegateWithPendingResultAndOptions( - @Nullable ImageOutputOptions imageOptions, @Nullable VideoOptions videoOptions) { + @Nullable ImageSelectionOptions imageOptions, @Nullable VideoSelectionOptions videoOptions) { return new ImagePickerDelegate( mockActivity, new File("/image_picker_cache"), @@ -540,6 +544,9 @@ private ImagePickerDelegate createDelegateWithPendingResultAndOptions( } private void verifyFinishedWithAlreadyActiveError() { - verify(mockResult).error("already_active", "Image picker is already active", null); + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); + verify(mockResult).error(errorCaptor.capture()); + assertEquals("already_active", errorCaptor.getValue().code); + assertEquals("Image picker is already active", errorCaptor.getValue().getMessage()); } } diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java index 8b9135128e75..5cc480a8c39d 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java @@ -21,29 +21,41 @@ import android.app.Activity; import android.app.Application; -import androidx.annotation.Nullable; import androidx.lifecycle.Lifecycle; import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.imagepicker.Messages.FlutterError; +import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions; +import io.flutter.plugins.imagepicker.Messages.SourceSpecification; +import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions; import java.io.File; -import java.util.HashMap; -import java.util.Map; +import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; public class ImagePickerPluginTest { - private static final int SOURCE_CAMERA = 0; - private static final int SOURCE_GALLERY = 1; - private static final String PICK_IMAGE = "pickImage"; - private static final String PICK_MULTI_IMAGE = "pickMultiImage"; - private static final String PICK_VIDEO = "pickVideo"; + private static final ImageSelectionOptions DEFAULT_IMAGE_OPTIONS = + new ImageSelectionOptions.Builder().setQuality((long) 100).build(); + private static final VideoSelectionOptions DEFAULT_VIDEO_OPTIONS = + new VideoSelectionOptions.Builder().build(); + private static final SourceSpecification SOURCE_GALLERY = + new SourceSpecification.Builder().setType(Messages.SourceType.GALLERY).build(); + private static final SourceSpecification SOURCE_CAMERA_FRONT = + new SourceSpecification.Builder() + .setType(Messages.SourceType.CAMERA) + .setCamera(Messages.SourceCamera.FRONT) + .build(); + private static final SourceSpecification SOURCE_CAMERA_REAR = + new SourceSpecification.Builder() + .setType(Messages.SourceType.CAMERA) + .setCamera(Messages.SourceCamera.REAR) + .build(); @SuppressWarnings("deprecation") @Mock @@ -55,7 +67,7 @@ public class ImagePickerPluginTest { @Mock Activity mockActivity; @Mock Application mockApplication; @Mock ImagePickerDelegate mockImagePickerDelegate; - @Mock MethodChannel.Result mockResult; + @Mock Messages.Result> mockResult; ImagePickerPlugin plugin; @@ -76,113 +88,102 @@ public void tearDown() throws Exception { } @Test - public void onMethodCall_whenActivityIsNull_finishesWithForegroundActivityRequiredError() { - MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_GALLERY, false); + public void pickImages_whenActivityIsNull_finishesWithForegroundActivityRequiredError() { ImagePickerPlugin imagePickerPluginWithNullActivity = new ImagePickerPlugin(mockImagePickerDelegate, null); - imagePickerPluginWithNullActivity.onMethodCall(call, mockResult); - verify(mockResult) - .error("no_activity", "image_picker plugin requires a foreground activity.", null); + imagePickerPluginWithNullActivity.pickImages( + SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); + + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); + verify(mockResult).error(errorCaptor.capture()); + assertEquals("no_activity", errorCaptor.getValue().code); + assertEquals( + "image_picker plugin requires a foreground activity.", errorCaptor.getValue().getMessage()); verifyNoInteractions(mockImagePickerDelegate); } @Test - public void onMethodCall_whenCalledWithUnknownMethod_throwsException() { - IllegalArgumentException e = - assertThrows( - IllegalArgumentException.class, - () -> plugin.onMethodCall(new MethodCall("test", null), mockResult)); - assertEquals(e.getMessage(), "Unknown method test"); + public void pickVideos_whenActivityIsNull_finishesWithForegroundActivityRequiredError() { + ImagePickerPlugin imagePickerPluginWithNullActivity = + new ImagePickerPlugin(mockImagePickerDelegate, null); + imagePickerPluginWithNullActivity.pickVideos( + SOURCE_CAMERA_REAR, DEFAULT_VIDEO_OPTIONS, false, false, mockResult); + + ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); + verify(mockResult).error(errorCaptor.capture()); + assertEquals("no_activity", errorCaptor.getValue().code); + assertEquals( + "image_picker plugin requires a foreground activity.", errorCaptor.getValue().getMessage()); verifyNoInteractions(mockImagePickerDelegate); - verifyNoInteractions(mockResult); } @Test - public void onMethodCall_whenCalledWithUnknownImageSource_throwsException() { - IllegalArgumentException e = - assertThrows( - IllegalArgumentException.class, - () -> plugin.onMethodCall(buildMethodCall(PICK_IMAGE, -1, false), mockResult)); - assertEquals(e.getMessage(), "Invalid image source: -1"); + public void retrieveLostResults_whenActivityIsNull_finishesWithForegroundActivityRequiredError() { + ImagePickerPlugin imagePickerPluginWithNullActivity = + new ImagePickerPlugin(mockImagePickerDelegate, null); + FlutterError error = + assertThrows(FlutterError.class, imagePickerPluginWithNullActivity::retrieveLostResults); + assertEquals("image_picker plugin requires a foreground activity.", error.getMessage()); + assertEquals("no_activity", error.code); verifyNoInteractions(mockImagePickerDelegate); - verifyNoInteractions(mockResult); } @Test - public void onMethodCall_whenSourceIsGallery_invokesChooseImageFromGallery() { - MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_GALLERY, false); - plugin.onMethodCall(call, mockResult); + public void pickImages_whenSourceIsGallery_invokesChooseImageFromGallery() { + plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); verify(mockImagePickerDelegate).chooseImageFromGallery(any(), eq(false), any()); verifyNoInteractions(mockResult); } @Test - public void onMethodCall_whenSourceIsGalleryUsingPhotoPicker_invokesChooseImageFromGallery() { - MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_GALLERY, true); - plugin.onMethodCall(call, mockResult); + public void pickImages_whenSourceIsGalleryUsingPhotoPicker_invokesChooseImageFromGallery() { + plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, true, mockResult); verify(mockImagePickerDelegate).chooseImageFromGallery(any(), eq(true), any()); verifyNoInteractions(mockResult); } @Test - public void onMethodCall_invokesChooseMultiImageFromGallery() { - MethodCall call = buildMethodCall(PICK_MULTI_IMAGE); - plugin.onMethodCall(call, mockResult); + public void pickImages_invokesChooseMultiImageFromGallery() { + plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, true, false, mockResult); verify(mockImagePickerDelegate).chooseMultiImageFromGallery(any(), eq(false), any()); verifyNoInteractions(mockResult); } @Test - public void onMethodCall_usingPhotoPicker_invokesChooseMultiImageFromGallery() { - MethodCall call = buildMethodCall(PICK_MULTI_IMAGE, SOURCE_GALLERY, true); - plugin.onMethodCall(call, mockResult); + public void pickImages_usingPhotoPicker_invokesChooseMultiImageFromGallery() { + plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, true, true, mockResult); verify(mockImagePickerDelegate).chooseMultiImageFromGallery(any(), eq(true), any()); verifyNoInteractions(mockResult); } @Test - public void onMethodCall_whenSourceIsCamera_invokesTakeImageWithCamera() { - MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_CAMERA, null); - plugin.onMethodCall(call, mockResult); + public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera() { + plugin.pickImages(SOURCE_CAMERA_REAR, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); verify(mockImagePickerDelegate).takeImageWithCamera(any(), any()); verifyNoInteractions(mockResult); } @Test - public void onMethodCall_PickingImage_whenSourceIsCamera_invokesTakeImageWithCamera_RearCamera() { - MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_CAMERA, null); - HashMap arguments = getArgumentMap(call); - arguments.put("cameraDevice", 0); - plugin.onMethodCall(call, mockResult); + public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera_RearCamera() { + plugin.pickImages(SOURCE_CAMERA_REAR, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.REAR)); } @Test - public void - onMethodCall_PickingImage_whenSourceIsCamera_invokesTakeImageWithCamera_FrontCamera() { - MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_CAMERA, null); - HashMap arguments = getArgumentMap(call); - arguments.put("cameraDevice", 1); - plugin.onMethodCall(call, mockResult); + public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera_FrontCamera() { + plugin.pickImages(SOURCE_CAMERA_FRONT, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.FRONT)); } @Test - public void onMethodCall_PickingVideo_whenSourceIsCamera_invokesTakeImageWithCamera_RearCamera() { - MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_CAMERA, null); - HashMap arguments = getArgumentMap(call); - arguments.put("cameraDevice", 0); - plugin.onMethodCall(call, mockResult); + public void pickVideos_whenSourceIsCamera_invokesTakeImageWithCamera_RearCamera() { + plugin.pickVideos(SOURCE_CAMERA_REAR, DEFAULT_VIDEO_OPTIONS, false, false, mockResult); verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.REAR)); } @Test - public void - onMethodCall_PickingVideo_whenSourceIsCamera_invokesTakeImageWithCamera_FrontCamera() { - MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_CAMERA, null); - HashMap arguments = getArgumentMap(call); - arguments.put("cameraDevice", 1); - plugin.onMethodCall(call, mockResult); + public void pickVideos_whenSourceIsCamera_invokesTakeImageWithCamera_FrontCamera() { + plugin.pickVideos(SOURCE_CAMERA_FRONT, DEFAULT_VIDEO_OPTIONS, false, false, mockResult); verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.FRONT)); } @@ -233,24 +234,4 @@ public void onDetachedFromActivity_shouldReleaseActivityState() { plugin.onDetachedFromActivity(); assertNull(plugin.getActivityState()); } - - private MethodCall buildMethodCall( - String method, final int source, @Nullable Boolean usePhotoPicker) { - final Map arguments = new HashMap<>(); - arguments.put("source", source); - if (usePhotoPicker != null) { - arguments.put("useAndroidPhotoPicker", usePhotoPicker); - } - - return new MethodCall(method, arguments); - } - - private MethodCall buildMethodCall(String method) { - return new MethodCall(method, null); - } - - @SuppressWarnings("unchecked") - private HashMap getArgumentMap(MethodCall call) { - return (HashMap) call.arguments; - } } diff --git a/packages/image_picker/image_picker_android/lib/image_picker_android.dart b/packages/image_picker/image_picker_android/lib/image_picker_android.dart index 97c981eb3178..fbc7fa7c2ad2 100644 --- a/packages/image_picker/image_picker_android/lib/image_picker_android.dart +++ b/packages/image_picker/image_picker_android/lib/image_picker_android.dart @@ -2,21 +2,20 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; -const MethodChannel _channel = - MethodChannel('plugins.flutter.io/image_picker_android'); +import 'src/messages.g.dart'; /// An Android implementation of [ImagePickerPlatform]. class ImagePickerAndroid extends ImagePickerPlatform { - /// The MethodChannel that is being used by this implementation of the plugin. - @visibleForTesting - MethodChannel get channel => _channel; + /// Creates a new plugin implemenation instance. + ImagePickerAndroid({@visibleForTesting ImagePickerApi? api}) + : _hostApi = api ?? ImagePickerApi(); + + final ImagePickerApi _hostApi; /// Registers this class as the default platform implementation. static void registerWith() { @@ -47,19 +46,19 @@ class ImagePickerAndroid extends ImagePickerPlatform { double? maxHeight, int? imageQuality, }) async { - final List? paths = await _getMultiImagePath( + final List paths = await _getMultiImagePath( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, ); - if (paths == null) { + if (paths.isEmpty) { return null; } return paths.map((dynamic path) => PickedFile(path as String)).toList(); } - Future?> _getMultiImagePath({ + Future> _getMultiImagePath({ double? maxWidth, double? maxHeight, int? imageQuality, @@ -77,15 +76,14 @@ class ImagePickerAndroid extends ImagePickerPlatform { throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); } - return _channel.invokeMethod?>( - 'pickMultiImage', - { - 'maxWidth': maxWidth, - 'maxHeight': maxHeight, - 'imageQuality': imageQuality, - 'useAndroidPhotoPicker': useAndroidPhotoPicker, - }, - ); + return _hostApi.pickImages( + SourceSpecification(type: SourceType.gallery), + ImageSelectionOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + quality: imageQuality ?? 100), + /* allowMultiple */ true, + useAndroidPhotoPicker); } Future _getImagePath({ @@ -95,7 +93,7 @@ class ImagePickerAndroid extends ImagePickerPlatform { int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, bool requestFullMetadata = true, - }) { + }) async { if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { throw ArgumentError.value( imageQuality, 'imageQuality', 'must be between 0 and 100'); @@ -109,18 +107,15 @@ class ImagePickerAndroid extends ImagePickerPlatform { throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); } - return _channel.invokeMethod( - 'pickImage', - { - 'source': source.index, - 'maxWidth': maxWidth, - 'maxHeight': maxHeight, - 'imageQuality': imageQuality, - 'cameraDevice': preferredCameraDevice.index, - 'requestFullMetadata': requestFullMetadata, - 'useAndroidPhotoPicker': useAndroidPhotoPicker, - }, - ); + final List paths = await _hostApi.pickImages( + _buildSourceSpec(source, preferredCameraDevice), + ImageSelectionOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + quality: imageQuality ?? 100), + /* allowMultiple */ false, + useAndroidPhotoPicker); + return paths.isEmpty ? null : paths.first; } @override @@ -141,16 +136,13 @@ class ImagePickerAndroid extends ImagePickerPlatform { required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, Duration? maxDuration, - }) { - return _channel.invokeMethod( - 'pickVideo', - { - 'source': source.index, - 'maxDuration': maxDuration?.inSeconds, - 'cameraDevice': preferredCameraDevice.index, - 'useAndroidPhotoPicker': useAndroidPhotoPicker, - }, - ); + }) async { + final List paths = await _hostApi.pickVideos( + _buildSourceSpec(source, preferredCameraDevice), + VideoSelectionOptions(maxDurationSeconds: maxDuration?.inSeconds), + /* allowMultiple */ false, + useAndroidPhotoPicker); + return paths.isEmpty ? null : paths.first; } @override @@ -193,12 +185,12 @@ class ImagePickerAndroid extends ImagePickerPlatform { double? maxHeight, int? imageQuality, }) async { - final List? paths = await _getMultiImagePath( + final List paths = await _getMultiImagePath( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, ); - if (paths == null) { + if (paths.isEmpty) { return null; } @@ -236,54 +228,89 @@ class ImagePickerAndroid extends ImagePickerPlatform { @override Future getLostData() async { - List? pickedFileList; - - final Map? result = - await _channel.invokeMapMethod('retrieve'); + final CacheRetrievalResult? result = await _hostApi.retrieveLostResults(); if (result == null) { return LostDataResponse.empty(); } - assert(result.containsKey('path') != result.containsKey('errorCode')); - - final String? type = result['type'] as String?; - assert(type == kTypeImage || type == kTypeVideo); + // There must either be data or an error if the response wasn't null. + assert(result.paths.isEmpty != (result.error == null)); - RetrieveType? retrieveType; - if (type == kTypeImage) { - retrieveType = RetrieveType.image; - } else if (type == kTypeVideo) { - retrieveType = RetrieveType.video; - } - - PlatformException? exception; - if (result.containsKey('errorCode')) { - exception = PlatformException( - code: result['errorCode']! as String, - message: result['errorMessage'] as String?); - } + final CacheRetrievalError? error = result.error; + final PlatformException? exception = error == null + ? null + : PlatformException(code: error.code, message: error.message); - final String? path = result['path'] as String?; - - final List? pathList = - (result['pathList'] as List?)?.cast(); - if (pathList != null) { - pickedFileList = []; - for (final String path in pathList) { - pickedFileList.add(XFile(path)); - } - } + // Entries are guaranteed not to be null, even though that's not currently + // expressable in Pigeon. + final List pickedFileList = + result.paths.map((String? path) => XFile(path!)).toList(); return LostDataResponse( - file: path != null ? XFile(path) : null, + file: pickedFileList.isEmpty ? null : pickedFileList.last, exception: exception, - type: retrieveType, + type: _retrieveTypeForCacheType(result.type), files: pickedFileList, ); } - /// Set [ImagePickerAndroid] to use Android 13 Photo Picker. + SourceSpecification _buildSourceSpec( + ImageSource source, CameraDevice device) { + return SourceSpecification( + type: _sourceSpecTypeForSource(source), + camera: _sourceSpecCameraForDevice(device)); + } + + SourceType _sourceSpecTypeForSource(ImageSource source) { + switch (source) { + case ImageSource.camera: + return SourceType.camera; + case ImageSource.gallery: + return SourceType.gallery; + } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return SourceType.gallery; + } + + SourceCamera _sourceSpecCameraForDevice(CameraDevice device) { + switch (device) { + case CameraDevice.front: + return SourceCamera.front; + case CameraDevice.rear: + return SourceCamera.rear; + } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return SourceCamera.rear; + } + + RetrieveType _retrieveTypeForCacheType(CacheRetrievalType type) { + switch (type) { + case CacheRetrievalType.image: + return RetrieveType.image; + case CacheRetrievalType.video: + return RetrieveType.video; + } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return RetrieveType.image; + } + + /// Sets [ImagePickerAndroid] to use Android 13 Photo Picker. /// /// Currently defaults to false, but the default is subject to change. bool useAndroidPhotoPicker = false; diff --git a/packages/image_picker/image_picker_android/lib/src/messages.g.dart b/packages/image_picker/image_picker_android/lib/src/messages.g.dart new file mode 100644 index 000000000000..c0506ad4daec --- /dev/null +++ b/packages/image_picker/image_picker_android/lib/src/messages.g.dart @@ -0,0 +1,337 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v9.1.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +enum SourceCamera { + rear, + front, +} + +enum SourceType { + camera, + gallery, +} + +enum CacheRetrievalType { + image, + video, +} + +/// Options for image selection and output. +class ImageSelectionOptions { + ImageSelectionOptions({ + this.maxWidth, + this.maxHeight, + required this.quality, + }); + + /// If set, the max width that the image should be resized to fit in. + double? maxWidth; + + /// If set, the max height that the image should be resized to fit in. + double? maxHeight; + + /// The quality of the output image, from 0-100. + /// + /// 100 indicates original quality. + int quality; + + Object encode() { + return [ + maxWidth, + maxHeight, + quality, + ]; + } + + static ImageSelectionOptions decode(Object result) { + result as List; + return ImageSelectionOptions( + maxWidth: result[0] as double?, + maxHeight: result[1] as double?, + quality: result[2]! as int, + ); + } +} + +/// Options for image selection and output. +class VideoSelectionOptions { + VideoSelectionOptions({ + this.maxDurationSeconds, + }); + + /// The maximum desired length for the video, in seconds. + int? maxDurationSeconds; + + Object encode() { + return [ + maxDurationSeconds, + ]; + } + + static VideoSelectionOptions decode(Object result) { + result as List; + return VideoSelectionOptions( + maxDurationSeconds: result[0] as int?, + ); + } +} + +/// Specification for the source of an image or video selection. +class SourceSpecification { + SourceSpecification({ + required this.type, + this.camera, + }); + + SourceType type; + + SourceCamera? camera; + + Object encode() { + return [ + type.index, + camera?.index, + ]; + } + + static SourceSpecification decode(Object result) { + result as List; + return SourceSpecification( + type: SourceType.values[result[0]! as int], + camera: result[1] != null ? SourceCamera.values[result[1]! as int] : null, + ); + } +} + +/// An error that occurred during lost result retrieval. +/// +/// The data here maps to the `PlatformException` that will be created from it. +class CacheRetrievalError { + CacheRetrievalError({ + required this.code, + this.message, + }); + + String code; + + String? message; + + Object encode() { + return [ + code, + message, + ]; + } + + static CacheRetrievalError decode(Object result) { + result as List; + return CacheRetrievalError( + code: result[0]! as String, + message: result[1] as String?, + ); + } +} + +/// The result of retrieving cached results from a previous run. +class CacheRetrievalResult { + CacheRetrievalResult({ + required this.type, + this.error, + required this.paths, + }); + + /// The type of the retrieved data. + CacheRetrievalType type; + + /// The error from the last selection, if any. + CacheRetrievalError? error; + + /// The results from the last selection, if any. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + List paths; + + Object encode() { + return [ + type.index, + error?.encode(), + paths, + ]; + } + + static CacheRetrievalResult decode(Object result) { + result as List; + return CacheRetrievalResult( + type: CacheRetrievalType.values[result[0]! as int], + error: result[1] != null + ? CacheRetrievalError.decode(result[1]! as List) + : null, + paths: (result[2] as List?)!.cast(), + ); + } +} + +class _ImagePickerApiCodec extends StandardMessageCodec { + const _ImagePickerApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CacheRetrievalError) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is CacheRetrievalResult) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is ImageSelectionOptions) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is VideoSelectionOptions) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CacheRetrievalError.decode(readValue(buffer)!); + case 129: + return CacheRetrievalResult.decode(readValue(buffer)!); + case 130: + return ImageSelectionOptions.decode(readValue(buffer)!); + case 131: + return SourceSpecification.decode(readValue(buffer)!); + case 132: + return VideoSelectionOptions.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class ImagePickerApi { + /// Constructor for [ImagePickerApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + ImagePickerApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _ImagePickerApiCodec(); + + /// Selects images and returns their paths. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + Future> pickImages( + SourceSpecification arg_source, + ImageSelectionOptions arg_options, + bool arg_allowMultiple, + bool arg_usePhotoPicker) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickImages', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send([ + arg_source, + arg_options, + arg_allowMultiple, + arg_usePhotoPicker + ]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as List?)!.cast(); + } + } + + /// Selects video and returns their paths. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + Future> pickVideos( + SourceSpecification arg_source, + VideoSelectionOptions arg_options, + bool arg_allowMultiple, + bool arg_usePhotoPicker) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickVideos', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send([ + arg_source, + arg_options, + arg_allowMultiple, + arg_usePhotoPicker + ]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as List?)!.cast(); + } + } + + /// Returns results from a previous app session, if any. + Future retrieveLostResults() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.retrieveLostResults', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return (replyList[0] as CacheRetrievalResult?); + } + } +} diff --git a/packages/image_picker/image_picker_android/pigeons/copyright.txt b/packages/image_picker/image_picker_android/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/image_picker/image_picker_android/pigeons/copyright.txt @@ -0,0 +1,3 @@ +Copyright 2013 The Flutter Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. diff --git a/packages/image_picker/image_picker_android/pigeons/messages.dart b/packages/image_picker/image_picker_android/pigeons/messages.dart new file mode 100644 index 000000000000..120799d0a1a1 --- /dev/null +++ b/packages/image_picker/image_picker_android/pigeons/messages.dart @@ -0,0 +1,104 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + dartTestOut: 'test/test_api.g.dart', + javaOut: 'android/src/main/java/io/flutter/plugins/imagepicker/Messages.java', + javaOptions: JavaOptions( + package: 'io.flutter.plugins.imagepicker', + ), + copyrightHeader: 'pigeons/copyright.txt', +)) + +/// Options for image selection and output. +class ImageSelectionOptions { + ImageSelectionOptions({this.maxWidth, this.maxHeight, required this.quality}); + + /// If set, the max width that the image should be resized to fit in. + double? maxWidth; + + /// If set, the max height that the image should be resized to fit in. + double? maxHeight; + + /// The quality of the output image, from 0-100. + /// + /// 100 indicates original quality. + int quality; +} + +/// Options for image selection and output. +class VideoSelectionOptions { + VideoSelectionOptions({this.maxDurationSeconds}); + + /// The maximum desired length for the video, in seconds. + int? maxDurationSeconds; +} + +// Corresponds to `CameraDevice` from the platform interface package. +enum SourceCamera { rear, front } + +// Corresponds to `ImageSource` from the platform interface package. +enum SourceType { camera, gallery } + +/// Specification for the source of an image or video selection. +class SourceSpecification { + SourceSpecification(this.type, this.camera); + SourceType type; + SourceCamera? camera; +} + +/// An error that occurred during lost result retrieval. +/// +/// The data here maps to the `PlatformException` that will be created from it. +class CacheRetrievalError { + CacheRetrievalError({required this.code, this.message}); + final String code; + final String? message; +} + +// Corresponds to `RetrieveType` from the platform interface package. +enum CacheRetrievalType { image, video } + +/// The result of retrieving cached results from a previous run. +class CacheRetrievalResult { + CacheRetrievalResult( + {required this.type, this.error, this.paths = const []}); + + /// The type of the retrieved data. + final CacheRetrievalType type; + + /// The error from the last selection, if any. + final CacheRetrievalError? error; + + /// The results from the last selection, if any. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + final List paths; +} + +@HostApi(dartHostTestHandler: 'TestHostImagePickerApi') +abstract class ImagePickerApi { + /// Selects images and returns their paths. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + @async + List pickImages(SourceSpecification source, + ImageSelectionOptions options, bool allowMultiple, bool usePhotoPicker); + + /// Selects video and returns their paths. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + @async + List pickVideos(SourceSpecification source, + VideoSelectionOptions options, bool allowMultiple, bool usePhotoPicker); + + /// Returns results from a previous app session, if any. + CacheRetrievalResult? retrieveLostResults(); +} diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml index b03d84bba6ed..3d79bbc539dc 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.6+2 +version: 0.8.6+3 environment: sdk: ">=2.17.0 <3.0.0" @@ -28,3 +28,4 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.0.0 + pigeon: ^9.1.0 diff --git a/packages/image_picker/image_picker_android/test/image_picker_android_test.dart b/packages/image_picker/image_picker_android/test/image_picker_android_test.dart index 4c9d31ae2b9a..f17d078a9031 100644 --- a/packages/image_picker/image_picker_android/test/image_picker_android_test.dart +++ b/packages/image_picker/image_picker_android/test/image_picker_android_test.dart @@ -2,31 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_android/image_picker_android.dart'; +import 'package:image_picker_android/src/messages.g.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final ImagePickerAndroid picker = ImagePickerAndroid(); - - final List log = []; - dynamic returnValue = ''; + late ImagePickerAndroid picker; + late _FakeImagePickerApi api; setUp(() { - returnValue = ''; - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler(picker.channel, - (MethodCall methodCall) async { - log.add(methodCall); - return returnValue; - }); - - log.clear(); + api = _FakeImagePickerApi(); + picker = ImagePickerAndroid(api: api); }); test('registers instance', () async { @@ -35,60 +23,38 @@ void main() { }); group('#pickImage', () { - test('passes the image source argument correctly', () async { + test('calls the method correctly', () async { + const String fakePath = '/foo.jpg'; + api.returnValue = [fakePath]; + final PickedFile? result = + await picker.pickImage(source: ImageSource.camera); + + expect(result?.path, fakePath); + expect(api.lastCall, _LastPickType.image); + expect(api.passedAllowMultiple, false); + }); + + test('passes the gallery image source argument correctly', () async { await picker.pickImage(source: ImageSource.camera); + + expect(api.passedSource?.type, SourceType.camera); + }); + + test('passes the camera image source argument correctly', () async { await picker.pickImage(source: ImageSource.gallery); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 1, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedSource?.type, SourceType.gallery); }); - test('passes the width and height arguments correctly', () async { - await picker.pickImage(source: ImageSource.camera); - await picker.pickImage( - source: ImageSource.camera, - maxWidth: 10.0, - ); - await picker.pickImage( - source: ImageSource.camera, - maxHeight: 10.0, - ); - await picker.pickImage( - source: ImageSource.camera, - maxWidth: 10.0, - maxHeight: 20.0, - ); - await picker.pickImage( - source: ImageSource.camera, - maxWidth: 10.0, - imageQuality: 70, - ); - await picker.pickImage( - source: ImageSource.camera, - maxHeight: 10.0, - imageQuality: 70, - ); + test('passes default image options', () async { + await picker.pickImage(source: ImageSource.gallery); + + expect(api.passedImageOptions?.maxWidth, null); + expect(api.passedImageOptions?.maxHeight, null); + expect(api.passedImageOptions?.quality, 100); + }); + + test('passes image option arguments correctly', () async { await picker.pickImage( source: ImageSource.camera, maxWidth: 10.0, @@ -96,74 +62,9 @@ void main() { imageQuality: 70, ); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': 70, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': 70, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': 70, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedImageOptions?.maxWidth, 10.0); + expect(api.passedImageOptions?.maxHeight, 20.0); + expect(api.passedImageOptions?.quality, 70); }); test('does not accept an invalid imageQuality argument', () { @@ -201,10 +102,7 @@ void main() { }); test('handles a null image path response gracefully', () async { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - picker.channel, (MethodCall methodCall) => null); + api.returnValue = null; expect(await picker.pickImage(source: ImageSource.gallery), isNull); expect(await picker.pickImage(source: ImageSource.camera), isNull); @@ -213,140 +111,66 @@ void main() { test('camera position defaults to back', () async { await picker.pickImage(source: ImageSource.camera); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedSource?.camera, SourceCamera.rear); }); - test('camera position can set to front', () async { + test('camera position can be set to front', () async { await picker.pickImage( source: ImageSource.camera, preferredCameraDevice: CameraDevice.front); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 1, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedSource?.camera, SourceCamera.front); + }); + + test('defaults to not using Android Photo Picker', () async { + await picker.pickImage(source: ImageSource.gallery); + + expect(api.passedPhotoPickerFlag, false); + }); + + test('allows using Android Photo Picker', () async { + picker.useAndroidPhotoPicker = true; + await picker.pickImage(source: ImageSource.gallery); + + expect(api.passedPhotoPickerFlag, true); }); }); group('#pickMultiImage', () { test('calls the method correctly', () async { - returnValue = ['0', '1']; - await picker.pickMultiImage(); + const List fakePaths = ['/foo.jgp', 'bar.jpg']; + api.returnValue = fakePaths; - expect( - log, - [ - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'useAndroidPhotoPicker': false, - }), - ], - ); + final List? files = await picker.pickMultiImage(); + + expect(api.lastCall, _LastPickType.image); + expect(api.passedAllowMultiple, true); + expect(files?.length, 2); + expect(files?[0].path, fakePaths[0]); + expect(files?[1].path, fakePaths[1]); }); - test('passes the width and height arguments correctly', () async { - returnValue = ['0', '1']; + test('passes default image options', () async { await picker.pickMultiImage(); - await picker.pickMultiImage( - maxWidth: 10.0, - ); - await picker.pickMultiImage( - maxHeight: 10.0, - ); - await picker.pickMultiImage( - maxWidth: 10.0, - maxHeight: 20.0, - ); - await picker.pickMultiImage( - maxWidth: 10.0, - imageQuality: 70, - ); - await picker.pickMultiImage( - maxHeight: 10.0, - imageQuality: 70, - ); + + expect(api.passedImageOptions?.maxWidth, null); + expect(api.passedImageOptions?.maxHeight, null); + expect(api.passedImageOptions?.quality, 100); + }); + + test('passes image option arguments correctly', () async { await picker.pickMultiImage( maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70, ); - expect( - log, - [ - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': null, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': null, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': null, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': 70, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': 70, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': 70, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedImageOptions?.maxWidth, 10.0); + expect(api.passedImageOptions?.maxHeight, 20.0); + expect(api.passedImageOptions?.quality, 70); }); test('does not accept a negative width or height argument', () { - returnValue = ['0', '1']; expect( () => picker.pickMultiImage(maxWidth: -1.0), throwsArgumentError, @@ -359,7 +183,6 @@ void main() { }); test('does not accept an invalid imageQuality argument', () { - returnValue = ['0', '1']; expect( () => picker.pickMultiImage(imageQuality: -1), throwsArgumentError, @@ -371,91 +194,68 @@ void main() { ); }); - test('handles a null image path response gracefully', () async { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - picker.channel, (MethodCall methodCall) => null); + test('handles an empty path response gracefully', () async { + api.returnValue = []; expect(await picker.pickMultiImage(), isNull); - expect(await picker.pickMultiImage(), isNull); + }); + + test('defaults to not using Android Photo Picker', () async { + await picker.pickMultiImage(); + + expect(api.passedPhotoPickerFlag, false); + }); + + test('allows using Android Photo Picker', () async { + picker.useAndroidPhotoPicker = true; + await picker.pickMultiImage(); + + expect(api.passedPhotoPickerFlag, true); }); }); group('#pickVideo', () { - test('passes the image source argument correctly', () async { + test('calls the method correctly', () async { + const String fakePath = '/foo.jpg'; + api.returnValue = [fakePath]; + final PickedFile? result = + await picker.pickVideo(source: ImageSource.camera); + + expect(result?.path, fakePath); + expect(api.lastCall, _LastPickType.video); + expect(api.passedAllowMultiple, false); + }); + + test('passes the gallery image source argument correctly', () async { await picker.pickVideo(source: ImageSource.camera); + + expect(api.passedSource?.type, SourceType.camera); + }); + + test('passes the camera image source argument correctly', () async { await picker.pickVideo(source: ImageSource.gallery); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickVideo', arguments: { - 'source': 1, - 'cameraDevice': 0, - 'maxDuration': null, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedSource?.type, SourceType.gallery); + }); + + test('passes null as the default duration', () async { + await picker.pickVideo(source: ImageSource.gallery); + + expect(api.passedVideoOptions, isNotNull); + expect(api.passedVideoOptions?.maxDurationSeconds, null); }); test('passes the duration argument correctly', () async { - await picker.pickVideo(source: ImageSource.camera); - await picker.pickVideo( - source: ImageSource.camera, - maxDuration: const Duration(seconds: 10), - ); await picker.pickVideo( source: ImageSource.camera, maxDuration: const Duration(minutes: 1), ); - await picker.pickVideo( - source: ImageSource.camera, - maxDuration: const Duration(hours: 1), - ); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 0, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 10, - 'cameraDevice': 0, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 60, - 'cameraDevice': 0, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 3600, - 'cameraDevice': 0, - 'useAndroidPhotoPicker': false, - }), - ], - ); + + expect(api.passedVideoOptions?.maxDurationSeconds, 60); }); test('handles a null video path response gracefully', () async { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - picker.channel, (MethodCall methodCall) => null); + api.returnValue = null; expect(await picker.pickVideo(source: ImageSource.gallery), isNull); expect(await picker.pickVideo(source: ImageSource.camera), isNull); @@ -464,17 +264,7 @@ void main() { test('camera position defaults to back', () async { await picker.pickVideo(source: ImageSource.camera); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedSource?.camera, SourceCamera.rear); }); test('camera position can set to front', () async { @@ -483,31 +273,28 @@ void main() { preferredCameraDevice: CameraDevice.front, ); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 1, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedSource?.camera, SourceCamera.front); + }); + + test('defaults to not using Android Photo Picker', () async { + await picker.pickVideo(source: ImageSource.gallery); + + expect(api.passedPhotoPickerFlag, false); + }); + + test('allows using Android Photo Picker', () async { + picker.useAndroidPhotoPicker = true; + await picker.pickVideo(source: ImageSource.gallery); + + expect(api.passedPhotoPickerFlag, true); }); }); group('#retrieveLostData', () { test('retrieveLostData get success response', () async { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler(picker.channel, - (MethodCall methodCall) async { - return { - 'type': 'image', - 'path': '/example/path', - }; - }); + api.returnValue = CacheRetrievalResult( + type: CacheRetrievalType.image, paths: ['/example/path']); + final LostData response = await picker.retrieveLostData(); expect(response.type, RetrieveType.image); expect(response.file, isNotNull); @@ -515,16 +302,12 @@ void main() { }); test('retrieveLostData get error response', () async { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler(picker.channel, - (MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - }; - }); + api.returnValue = CacheRetrievalResult( + type: CacheRetrievalType.video, + paths: [], + error: CacheRetrievalError( + code: 'test_error_code', message: 'test_error_message')); + final LostData response = await picker.retrieveLostData(); expect(response.type, RetrieveType.video); expect(response.exception, isNotNull); @@ -533,86 +316,54 @@ void main() { }); test('retrieveLostData get null response', () async { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler(picker.channel, - (MethodCall methodCall) async { - return null; - }); + api.returnValue = null; + expect((await picker.retrieveLostData()).isEmpty, true); }); test('retrieveLostData get both path and error should throw', () async { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler(picker.channel, - (MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - 'path': '/example/path', - }; - }); + api.returnValue = CacheRetrievalResult( + type: CacheRetrievalType.video, + paths: ['/example/path'], + error: CacheRetrievalError( + code: 'test_error_code', message: 'test_error_message')); + expect(picker.retrieveLostData(), throwsAssertionError); }); }); group('#getImage', () { - test('passes the image source argument correctly', () async { + test('calls the method correctly', () async { + const String fakePath = '/foo.jpg'; + api.returnValue = [fakePath]; + final XFile? result = await picker.getImage(source: ImageSource.camera); + + expect(result?.path, fakePath); + expect(api.lastCall, _LastPickType.image); + expect(api.passedAllowMultiple, false); + }); + + test('passes the gallery image source argument correctly', () async { await picker.getImage(source: ImageSource.camera); + + expect(api.passedSource?.type, SourceType.camera); + }); + + test('passes the camera image source argument correctly', () async { await picker.getImage(source: ImageSource.gallery); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 1, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedSource?.type, SourceType.gallery); }); - test('passes the width and height arguments correctly', () async { - await picker.getImage(source: ImageSource.camera); - await picker.getImage( - source: ImageSource.camera, - maxWidth: 10.0, - ); - await picker.getImage( - source: ImageSource.camera, - maxHeight: 10.0, - ); - await picker.getImage( - source: ImageSource.camera, - maxWidth: 10.0, - maxHeight: 20.0, - ); - await picker.getImage( - source: ImageSource.camera, - maxWidth: 10.0, - imageQuality: 70, - ); - await picker.getImage( - source: ImageSource.camera, - maxHeight: 10.0, - imageQuality: 70, - ); + test('passes default image options', () async { + await picker.getImage(source: ImageSource.gallery); + + expect(api.passedImageOptions?.maxWidth, null); + expect(api.passedImageOptions?.maxHeight, null); + expect(api.passedImageOptions?.quality, 100); + }); + + test('passes image option arguments correctly', () async { await picker.getImage( source: ImageSource.camera, maxWidth: 10.0, @@ -620,74 +371,9 @@ void main() { imageQuality: 70, ); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': 70, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': 70, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': 70, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedImageOptions?.maxWidth, 10.0); + expect(api.passedImageOptions?.maxHeight, 20.0); + expect(api.passedImageOptions?.quality, 70); }); test('does not accept an invalid imageQuality argument', () { @@ -725,10 +411,7 @@ void main() { }); test('handles a null image path response gracefully', () async { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - picker.channel, (MethodCall methodCall) => null); + api.returnValue = null; expect(await picker.getImage(source: ImageSource.gallery), isNull); expect(await picker.getImage(source: ImageSource.camera), isNull); @@ -737,20 +420,7 @@ void main() { test('camera position defaults to back', () async { await picker.getImage(source: ImageSource.camera); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedSource?.camera, SourceCamera.rear); }); test('camera position can set to front', () async { @@ -758,119 +428,58 @@ void main() { source: ImageSource.camera, preferredCameraDevice: CameraDevice.front); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 1, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedSource?.camera, SourceCamera.front); + }); + + test('defaults to not using Android Photo Picker', () async { + await picker.getImage(source: ImageSource.gallery); + + expect(api.passedPhotoPickerFlag, false); + }); + + test('allows using Android Photo Picker', () async { + picker.useAndroidPhotoPicker = true; + await picker.getImage(source: ImageSource.gallery); + + expect(api.passedPhotoPickerFlag, true); }); }); group('#getMultiImage', () { test('calls the method correctly', () async { - returnValue = ['0', '1']; - await picker.getMultiImage(); + const List fakePaths = ['/foo.jgp', 'bar.jpg']; + api.returnValue = fakePaths; - expect( - log, - [ - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'useAndroidPhotoPicker': false, - }), - ], - ); + final List? files = await picker.getMultiImage(); + + expect(api.lastCall, _LastPickType.image); + expect(api.passedAllowMultiple, true); + expect(files?.length, 2); + expect(files?[0].path, fakePaths[0]); + expect(files?[1].path, fakePaths[1]); }); - test('passes the width and height arguments correctly', () async { - returnValue = ['0', '1']; + test('passes default image options', () async { await picker.getMultiImage(); - await picker.getMultiImage( - maxWidth: 10.0, - ); - await picker.getMultiImage( - maxHeight: 10.0, - ); - await picker.getMultiImage( - maxWidth: 10.0, - maxHeight: 20.0, - ); - await picker.getMultiImage( - maxWidth: 10.0, - imageQuality: 70, - ); - await picker.getMultiImage( - maxHeight: 10.0, - imageQuality: 70, - ); + + expect(api.passedImageOptions?.maxWidth, null); + expect(api.passedImageOptions?.maxHeight, null); + expect(api.passedImageOptions?.quality, 100); + }); + + test('passes image option arguments correctly', () async { await picker.getMultiImage( maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70, ); - expect( - log, - [ - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': null, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': null, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': null, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': 70, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': 70, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': 70, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedImageOptions?.maxWidth, 10.0); + expect(api.passedImageOptions?.maxHeight, 20.0); + expect(api.passedImageOptions?.quality, 70); }); test('does not accept a negative width or height argument', () { - returnValue = ['0', '1']; expect( () => picker.getMultiImage(maxWidth: -1.0), throwsArgumentError, @@ -883,7 +492,6 @@ void main() { }); test('does not accept an invalid imageQuality argument', () { - returnValue = ['0', '1']; expect( () => picker.getMultiImage(imageQuality: -1), throwsArgumentError, @@ -895,91 +503,68 @@ void main() { ); }); - test('handles a null image path response gracefully', () async { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - picker.channel, (MethodCall methodCall) => null); + test('handles an empty image path response gracefully', () async { + api.returnValue = []; expect(await picker.getMultiImage(), isNull); expect(await picker.getMultiImage(), isNull); }); + + test('defaults to not using Android Photo Picker', () async { + await picker.getMultiImage(); + + expect(api.passedPhotoPickerFlag, false); + }); + + test('allows using Android Photo Picker', () async { + picker.useAndroidPhotoPicker = true; + await picker.getMultiImage(); + + expect(api.passedPhotoPickerFlag, true); + }); }); group('#getVideo', () { - test('passes the image source argument correctly', () async { + test('calls the method correctly', () async { + const String fakePath = '/foo.jpg'; + api.returnValue = [fakePath]; + final XFile? result = await picker.getVideo(source: ImageSource.camera); + + expect(result?.path, fakePath); + expect(api.lastCall, _LastPickType.video); + expect(api.passedAllowMultiple, false); + }); + + test('passes the gallery image source argument correctly', () async { await picker.getVideo(source: ImageSource.camera); + + expect(api.passedSource?.type, SourceType.camera); + }); + + test('passes the camera image source argument correctly', () async { await picker.getVideo(source: ImageSource.gallery); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickVideo', arguments: { - 'source': 1, - 'cameraDevice': 0, - 'maxDuration': null, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedSource?.type, SourceType.gallery); + }); + + test('passes null as the default duration', () async { + await picker.getVideo(source: ImageSource.gallery); + + expect(api.passedVideoOptions, isNotNull); + expect(api.passedVideoOptions?.maxDurationSeconds, null); }); test('passes the duration argument correctly', () async { - await picker.getVideo(source: ImageSource.camera); - await picker.getVideo( - source: ImageSource.camera, - maxDuration: const Duration(seconds: 10), - ); await picker.getVideo( source: ImageSource.camera, maxDuration: const Duration(minutes: 1), ); - await picker.getVideo( - source: ImageSource.camera, - maxDuration: const Duration(hours: 1), - ); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 0, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 10, - 'cameraDevice': 0, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 60, - 'cameraDevice': 0, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 3600, - 'cameraDevice': 0, - 'useAndroidPhotoPicker': false, - }), - ], - ); + + expect(api.passedVideoOptions?.maxDurationSeconds, 60); }); test('handles a null video path response gracefully', () async { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - picker.channel, (MethodCall methodCall) => null); + api.returnValue = null; expect(await picker.getVideo(source: ImageSource.gallery), isNull); expect(await picker.getVideo(source: ImageSource.camera), isNull); @@ -988,17 +573,7 @@ void main() { test('camera position defaults to back', () async { await picker.getVideo(source: ImageSource.camera); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedSource?.camera, SourceCamera.rear); }); test('camera position can set to front', () async { @@ -1007,31 +582,28 @@ void main() { preferredCameraDevice: CameraDevice.front, ); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 1, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedSource?.camera, SourceCamera.front); + }); + + test('defaults to not using Android Photo Picker', () async { + await picker.getVideo(source: ImageSource.gallery); + + expect(api.passedPhotoPickerFlag, false); + }); + + test('allows using Android Photo Picker', () async { + picker.useAndroidPhotoPicker = true; + await picker.getVideo(source: ImageSource.gallery); + + expect(api.passedPhotoPickerFlag, true); }); }); group('#getLostData', () { test('getLostData get success response', () async { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler(picker.channel, - (MethodCall methodCall) async { - return { - 'type': 'image', - 'path': '/example/path', - }; - }); + api.returnValue = CacheRetrievalResult( + type: CacheRetrievalType.image, paths: ['/example/path']); + final LostDataResponse response = await picker.getLostData(); expect(response.type, RetrieveType.image); expect(response.file, isNotNull); @@ -1039,16 +611,10 @@ void main() { }); test('getLostData should successfully retrieve multiple files', () async { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler(picker.channel, - (MethodCall methodCall) async { - return { - 'type': 'image', - 'path': '/example/path1', - 'pathList': ['/example/path0', '/example/path1'], - }; - }); + api.returnValue = CacheRetrievalResult( + type: CacheRetrievalType.image, + paths: ['/example/path0', '/example/path1']); + final LostDataResponse response = await picker.getLostData(); expect(response.type, RetrieveType.image); expect(response.file, isNotNull); @@ -1058,16 +624,12 @@ void main() { }); test('getLostData get error response', () async { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler(picker.channel, - (MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - }; - }); + api.returnValue = CacheRetrievalResult( + type: CacheRetrievalType.video, + paths: [], + error: CacheRetrievalError( + code: 'test_error_code', message: 'test_error_message')); + final LostDataResponse response = await picker.getLostData(); expect(response.type, RetrieveType.video); expect(response.exception, isNotNull); @@ -1076,92 +638,54 @@ void main() { }); test('getLostData get null response', () async { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler(picker.channel, - (MethodCall methodCall) async { - return null; - }); + api.returnValue = null; + expect((await picker.getLostData()).isEmpty, true); }); test('getLostData get both path and error should throw', () async { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler(picker.channel, - (MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - 'path': '/example/path', - }; - }); + api.returnValue = CacheRetrievalResult( + type: CacheRetrievalType.video, + paths: ['/example/path'], + error: CacheRetrievalError( + code: 'test_error_code', message: 'test_error_message')); + expect(picker.getLostData(), throwsAssertionError); }); }); group('#getImageFromSource', () { - test('passes the image source argument correctly', () async { + test('calls the method correctly', () async { + const String fakePath = '/foo.jpg'; + api.returnValue = [fakePath]; + final XFile? result = await picker.getImage(source: ImageSource.camera); + + expect(result?.path, fakePath); + expect(api.lastCall, _LastPickType.image); + expect(api.passedAllowMultiple, false); + }); + + test('passes the gallery image source argument correctly', () async { await picker.getImageFromSource(source: ImageSource.camera); + + expect(api.passedSource?.type, SourceType.camera); + }); + + test('passes the camera image source argument correctly', () async { await picker.getImageFromSource(source: ImageSource.gallery); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 1, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedSource?.type, SourceType.gallery); }); - test('passes the width and height arguments correctly', () async { - await picker.getImageFromSource(source: ImageSource.camera); - await picker.getImageFromSource( - source: ImageSource.camera, - options: const ImagePickerOptions(maxWidth: 10.0), - ); - await picker.getImageFromSource( - source: ImageSource.camera, - options: const ImagePickerOptions(maxHeight: 10.0), - ); - await picker.getImageFromSource( - source: ImageSource.camera, - options: const ImagePickerOptions( - maxWidth: 10.0, - maxHeight: 20.0, - ), - ); - await picker.getImageFromSource( - source: ImageSource.camera, - options: const ImagePickerOptions( - maxWidth: 10.0, - imageQuality: 70, - ), - ); - await picker.getImageFromSource( - source: ImageSource.camera, - options: const ImagePickerOptions( - maxHeight: 10.0, - imageQuality: 70, - ), - ); + test('passes default image options', () async { + await picker.getImageFromSource(source: ImageSource.gallery); + + expect(api.passedImageOptions?.maxWidth, null); + expect(api.passedImageOptions?.maxHeight, null); + expect(api.passedImageOptions?.quality, 100); + }); + + test('passes image option arguments correctly', () async { await picker.getImageFromSource( source: ImageSource.camera, options: const ImagePickerOptions( @@ -1171,74 +695,9 @@ void main() { ), ); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': 70, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': 70, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': 70, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedImageOptions?.maxWidth, 10.0); + expect(api.passedImageOptions?.maxHeight, 20.0); + expect(api.passedImageOptions?.quality, 70); }); test('does not accept an invalid imageQuality argument', () { @@ -1294,10 +753,7 @@ void main() { }); test('handles a null image path response gracefully', () async { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - picker.channel, (MethodCall methodCall) => null); + api.returnValue = null; expect( await picker.getImageFromSource(source: ImageSource.gallery), isNull); @@ -1308,72 +764,77 @@ void main() { test('camera position defaults to back', () async { await picker.getImageFromSource(source: ImageSource.camera); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedSource?.camera, SourceCamera.rear); }); - test('camera position can set to front', () async { + test('camera position can be set to front', () async { await picker.getImageFromSource( - source: ImageSource.camera, - options: const ImagePickerOptions( - preferredCameraDevice: CameraDevice.front, - ), - ); + source: ImageSource.camera, + options: const ImagePickerOptions( + preferredCameraDevice: CameraDevice.front)); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 1, - 'requestFullMetadata': true, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedSource?.camera, SourceCamera.front); }); - test('passes the full metadata argument correctly', () async { - await picker.getImageFromSource( - source: ImageSource.camera, - options: const ImagePickerOptions(requestFullMetadata: false), - ); + test('defaults to not using Android Photo Picker', () async { + await picker.getImageFromSource(source: ImageSource.gallery); - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - 'requestFullMetadata': false, - 'useAndroidPhotoPicker': false, - }), - ], - ); + expect(api.passedPhotoPickerFlag, false); + }); + + test('allows using Android Photo Picker', () async { + picker.useAndroidPhotoPicker = true; + await picker.getImageFromSource(source: ImageSource.gallery); + + expect(api.passedPhotoPickerFlag, true); }); }); } -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; +enum _LastPickType { image, video } + +class _FakeImagePickerApi implements ImagePickerApi { + // The value to return. + Object? returnValue; + + // Passed arguments. + SourceSpecification? passedSource; + ImageSelectionOptions? passedImageOptions; + VideoSelectionOptions? passedVideoOptions; + bool? passedAllowMultiple; + bool? passedPhotoPickerFlag; + _LastPickType? lastCall; + + @override + Future> pickImages( + SourceSpecification source, + ImageSelectionOptions options, + bool allowMultiple, + bool usePhotoPicker) async { + lastCall = _LastPickType.image; + passedSource = source; + passedImageOptions = options; + passedAllowMultiple = allowMultiple; + passedPhotoPickerFlag = usePhotoPicker; + return returnValue as List? ?? []; + } + + @override + Future> pickVideos( + SourceSpecification source, + VideoSelectionOptions options, + bool allowMultiple, + bool usePhotoPicker) async { + lastCall = _LastPickType.video; + passedSource = source; + passedVideoOptions = options; + passedAllowMultiple = allowMultiple; + passedPhotoPickerFlag = usePhotoPicker; + return returnValue as List? ?? []; + } + + @override + Future retrieveLostResults() async { + return returnValue as CacheRetrievalResult?; + } +} diff --git a/packages/image_picker/image_picker_android/test/test_api.g.dart b/packages/image_picker/image_picker_android/test/test_api.g.dart new file mode 100644 index 000000000000..aa5c38f52eb1 --- /dev/null +++ b/packages/image_picker/image_picker_android/test/test_api.g.dart @@ -0,0 +1,158 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v9.1.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import +// ignore_for_file: avoid_relative_lib_imports +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:image_picker_android/src/messages.g.dart'; + +class _TestHostImagePickerApiCodec extends StandardMessageCodec { + const _TestHostImagePickerApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CacheRetrievalError) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is CacheRetrievalResult) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is ImageSelectionOptions) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is VideoSelectionOptions) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CacheRetrievalError.decode(readValue(buffer)!); + case 129: + return CacheRetrievalResult.decode(readValue(buffer)!); + case 130: + return ImageSelectionOptions.decode(readValue(buffer)!); + case 131: + return SourceSpecification.decode(readValue(buffer)!); + case 132: + return VideoSelectionOptions.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestHostImagePickerApi { + static const MessageCodec codec = _TestHostImagePickerApiCodec(); + + /// Selects images and returns their paths. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + Future> pickImages(SourceSpecification source, + ImageSelectionOptions options, bool allowMultiple, bool usePhotoPicker); + + /// Selects video and returns their paths. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + Future> pickVideos(SourceSpecification source, + VideoSelectionOptions options, bool allowMultiple, bool usePhotoPicker); + + /// Returns results from a previous app session, if any. + CacheRetrievalResult? retrieveLostResults(); + + static void setup(TestHostImagePickerApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickImages', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null.'); + final List args = (message as List?)!; + final SourceSpecification? arg_source = + (args[0] as SourceSpecification?); + assert(arg_source != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null SourceSpecification.'); + final ImageSelectionOptions? arg_options = + (args[1] as ImageSelectionOptions?); + assert(arg_options != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null ImageSelectionOptions.'); + final bool? arg_allowMultiple = (args[2] as bool?); + assert(arg_allowMultiple != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null bool.'); + final bool? arg_usePhotoPicker = (args[3] as bool?); + assert(arg_usePhotoPicker != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null bool.'); + final List output = await api.pickImages(arg_source!, + arg_options!, arg_allowMultiple!, arg_usePhotoPicker!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickVideos', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null.'); + final List args = (message as List?)!; + final SourceSpecification? arg_source = + (args[0] as SourceSpecification?); + assert(arg_source != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null SourceSpecification.'); + final VideoSelectionOptions? arg_options = + (args[1] as VideoSelectionOptions?); + assert(arg_options != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null VideoSelectionOptions.'); + final bool? arg_allowMultiple = (args[2] as bool?); + assert(arg_allowMultiple != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null bool.'); + final bool? arg_usePhotoPicker = (args[3] as bool?); + assert(arg_usePhotoPicker != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null bool.'); + final List output = await api.pickVideos(arg_source!, + arg_options!, arg_allowMultiple!, arg_usePhotoPicker!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.retrieveLostResults', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + final CacheRetrievalResult? output = api.retrieveLostResults(); + return [output]; + }); + } + } + } +}