From 786989601864517c56f48fbcd612863c56e1cba9 Mon Sep 17 00:00:00 2001 From: Tarrin Neal Date: Wed, 14 Jun 2023 17:34:05 -0700 Subject: [PATCH] [image_picker] getMedia platform implementations (#4175) Adds `getMedia` and `getMultipleMedia` methods to all image_picker platforms. ~~waiting on https://github.com/flutter/packages/pull/4174~~ precursor to https://github.com/flutter/packages/pull/3892 part of https://github.com/flutter/flutter/issues/89159 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [relevant style guides] and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/packages repo does use `dart format`.) - [x] I signed the [CLA]. - [x] The title of the PR starts with the name of the package surrounded by square brackets, e.g. `[shared_preferences]` - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated `pubspec.yaml` with an appropriate new version according to the [pub versioning philosophy], or this PR is [exempt from version changes]. - [x] I updated `CHANGELOG.md` to add a description of the change, [following repository CHANGELOG style]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [relevant style guides]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md#style [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat [pub versioning philosophy]: https://dart.dev/tools/pub/versioning [exempt from version changes]: https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#version-and-changelog-updates [following repository CHANGELOG style]: https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changelog-style [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests --- .../image_picker_android/CHANGELOG.md | 4 + .../imagepicker/ImagePickerDelegate.java | 165 +++++++++--- .../imagepicker/ImagePickerPlugin.java | 45 +++- .../flutter/plugins/imagepicker/Messages.java | 206 +++++++++++++-- .../imagepicker/ImagePickerDelegateTest.java | 29 +++ .../imagepicker/ImagePickerPluginTest.java | 106 +++++++- .../example/lib/main.dart | 164 +++++++++--- .../image_picker_android/example/pubspec.yaml | 3 +- .../lib/image_picker_android.dart | 104 ++++++-- .../lib/src/messages.g.dart | 126 ++++++++-- .../pigeons/messages.dart | 37 ++- .../image_picker_android/pubspec.yaml | 4 +- .../test/image_picker_android_test.dart | 159 +++++++++++- .../image_picker_android/test/test_api.g.dart | 87 +++++-- .../image_picker_for_web/CHANGELOG.md | 3 +- .../image_picker_for_web_test.dart | 35 ++- .../image_picker_for_web/example/pubspec.yaml | 2 +- .../lib/image_picker_for_web.dart | 27 +- .../image_picker_for_web/pubspec.yaml | 5 +- .../image_picker_ios/CHANGELOG.md | 5 + .../ios/RunnerTests/ImagePickerPluginTests.m | 78 ++++++ .../image_picker_ios/example/lib/main.dart | 168 ++++++++++--- .../image_picker_ios/example/pubspec.yaml | 3 +- .../Classes/FLTImagePickerPhotoAssetUtil.h | 5 +- .../Classes/FLTImagePickerPhotoAssetUtil.m | 14 ++ .../ios/Classes/FLTImagePickerPlugin.m | 65 +++-- .../ios/Classes/FLTImagePickerPlugin_Test.h | 5 +- .../FLTPHPickerSaveImageToPathOperation.m | 45 +++- .../image_picker_ios/ios/Classes/messages.g.h | 20 +- .../image_picker_ios/ios/Classes/messages.g.m | 75 +++++- .../lib/image_picker_ios.dart | 45 ++++ .../image_picker_ios/lib/src/messages.g.dart | 74 +++++- .../image_picker_ios/pigeons/messages.dart | 19 ++ .../image_picker_ios/pubspec.yaml | 4 +- .../test/image_picker_ios_test.dart | 234 ++++++++++++++++++ .../image_picker_ios/test/test_api.g.dart | 36 ++- .../image_picker_linux/CHANGELOG.md | 4 + .../image_picker_linux/example/lib/main.dart | 200 +++++++++++---- .../image_picker_linux/example/pubspec.yaml | 3 +- .../lib/image_picker_linux.dart | 23 ++ .../image_picker_linux/pubspec.yaml | 4 +- .../test/image_picker_linux_test.dart | 32 +++ .../image_picker_macos/CHANGELOG.md | 4 + .../image_picker_macos/example/lib/main.dart | 200 +++++++++++---- .../image_picker_macos/example/pubspec.yaml | 3 +- .../lib/image_picker_macos.dart | 24 ++ .../image_picker_macos/pubspec.yaml | 4 +- .../test/image_picker_macos_test.dart | 32 +++ .../image_picker_windows/CHANGELOG.md | 4 + .../example/lib/main.dart | 200 +++++++++++---- .../image_picker_windows/example/pubspec.yaml | 3 +- .../lib/image_picker_windows.dart | 24 ++ .../image_picker_windows/pubspec.yaml | 4 +- .../test/image_picker_windows_test.dart | 35 +++ script/configs/allowed_unpinned_deps.yaml | 1 + 55 files changed, 2561 insertions(+), 449 deletions(-) diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md index 97ccd9d57c..971cfe08f3 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.7 + +* Adds `getMedia` method. + ## 0.8.6+20 * Bumps androidx.activity:activity from 1.7.0 to 1.7.1. 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 d216309d58..685534ec6a 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 @@ -79,6 +79,7 @@ public class ImagePickerDelegate @VisibleForTesting static final int REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA = 2343; @VisibleForTesting static final int REQUEST_CAMERA_IMAGE_PERMISSION = 2345; @VisibleForTesting static final int REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY = 2346; + @VisibleForTesting static final int REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY = 2347; @VisibleForTesting static final int REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY = 2352; @VisibleForTesting static final int REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA = 2353; @@ -279,6 +280,52 @@ public class ImagePickerDelegate return result.build(); } + public void chooseMediaFromGallery( + @NonNull Messages.MediaSelectionOptions options, + @NonNull Messages.GeneralOptions generalOptions, + @NonNull Messages.Result> result) { + if (!setPendingOptionsAndResult(options.getImageSelectionOptions(), null, result)) { + finishWithAlreadyActiveError(result); + return; + } + + launchPickMediaFromGalleryIntent(generalOptions); + } + + private void launchPickMediaFromGalleryIntent(Messages.GeneralOptions generalOptions) { + Intent pickMediaIntent; + if (generalOptions.getUsePhotoPicker() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (generalOptions.getAllowMultiple()) { + pickMediaIntent = + new ActivityResultContracts.PickMultipleVisualMedia() + .createIntent( + activity, + new PickVisualMediaRequest.Builder() + .setMediaType( + ActivityResultContracts.PickVisualMedia.ImageAndVideo.INSTANCE) + .build()); + } else { + pickMediaIntent = + new ActivityResultContracts.PickVisualMedia() + .createIntent( + activity, + new PickVisualMediaRequest.Builder() + .setMediaType( + ActivityResultContracts.PickVisualMedia.ImageAndVideo.INSTANCE) + .build()); + } + } else { + pickMediaIntent = new Intent(Intent.ACTION_GET_CONTENT); + pickMediaIntent.setType("*/*"); + String[] mimeTypes = {"video/*", "image/*"}; + pickMediaIntent.putExtra("CONTENT_TYPE", mimeTypes); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + pickMediaIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, generalOptions.getAllowMultiple()); + } + } + activity.startActivityForResult(pickMediaIntent, REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY); + } + public void chooseVideoFromGallery( @NonNull VideoSelectionOptions options, boolean usePhotoPicker, @@ -291,9 +338,9 @@ public class ImagePickerDelegate launchPickVideoFromGalleryIntent(usePhotoPicker); } - private void launchPickVideoFromGalleryIntent(Boolean useAndroidPhotoPicker) { + private void launchPickVideoFromGalleryIntent(Boolean usePhotoPicker) { Intent pickVideoIntent; - if (useAndroidPhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { pickVideoIntent = new ActivityResultContracts.PickVisualMedia() .createIntent( @@ -389,9 +436,9 @@ public class ImagePickerDelegate launchMultiPickImageFromGalleryIntent(usePhotoPicker); } - private void launchPickImageFromGalleryIntent(Boolean useAndroidPhotoPicker) { + private void launchPickImageFromGalleryIntent(Boolean usePhotoPicker) { Intent pickImageIntent; - if (useAndroidPhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { pickImageIntent = new ActivityResultContracts.PickVisualMedia() .createIntent( @@ -406,9 +453,9 @@ public class ImagePickerDelegate activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY); } - private void launchMultiPickImageFromGalleryIntent(Boolean useAndroidPhotoPicker) { + private void launchMultiPickImageFromGalleryIntent(Boolean usePhotoPicker) { Intent pickMultiImageIntent; - if (useAndroidPhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { pickMultiImageIntent = new ActivityResultContracts.PickMultipleVisualMedia() .createIntent( @@ -563,6 +610,9 @@ public class ImagePickerDelegate case REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA: handlerRunnable = () -> handleCaptureImageResult(resultCode); break; + case REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY: + handlerRunnable = () -> handleChooseMediaResult(resultCode, data); + break; case REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY: handlerRunnable = () -> handleChooseVideoResult(resultCode, data); break; @@ -589,17 +639,59 @@ public class ImagePickerDelegate finishWithSuccess(null); } - private void handleChooseMultiImageResult(int resultCode, Intent intent) { + public class MediaPath { + public MediaPath(@NonNull String path, @Nullable String mimeType) { + this.path = path; + this.mimeType = mimeType; + } + + final String path; + final String mimeType; + + public @NonNull String getPath() { + return path; + } + + public @Nullable String getMimeType() { + return mimeType; + } + } + + private void handleChooseMediaResult(int resultCode, Intent intent) { if (resultCode == Activity.RESULT_OK && intent != null) { - ArrayList paths = new ArrayList<>(); + ArrayList paths = new ArrayList<>(); if (intent.getClipData() != null) { for (int i = 0; i < intent.getClipData().getItemCount(); i++) { - paths.add(fileUtils.getPathFromUri(activity, intent.getClipData().getItemAt(i).getUri())); + Uri uri = intent.getClipData().getItemAt(i).getUri(); + String path = fileUtils.getPathFromUri(activity, uri); + String mimeType = activity.getContentResolver().getType(uri); + paths.add(new MediaPath(path, mimeType)); } } else { - paths.add(fileUtils.getPathFromUri(activity, intent.getData())); + paths.add(new MediaPath(fileUtils.getPathFromUri(activity, intent.getData()), null)); } - handleMultiImageResult(paths); + handleMediaResult(paths); + return; + } + + // User cancelled choosing a picture. + finishWithSuccess(null); + } + + private void handleChooseMultiImageResult(int resultCode, Intent intent) { + if (resultCode == Activity.RESULT_OK && intent != null) { + ArrayList paths = new ArrayList<>(); + if (intent.getClipData() != null) { + for (int i = 0; i < intent.getClipData().getItemCount(); i++) { + paths.add( + new MediaPath( + fileUtils.getPathFromUri(activity, intent.getClipData().getItemAt(i).getUri()), + null)); + } + } else { + paths.add(new MediaPath(fileUtils.getPathFromUri(activity, intent.getData()), null)); + } + handleMediaResult(paths); return; } @@ -649,26 +741,6 @@ public class ImagePickerDelegate finishWithSuccess(null); } - private void handleMultiImageResult(ArrayList paths) { - ImageSelectionOptions localImageOptions = null; - synchronized (pendingCallStateLock) { - if (pendingCallState != null) { - localImageOptions = pendingCallState.imageOptions; - } - } - - if (localImageOptions != null) { - ArrayList finalPath = new ArrayList<>(); - for (int i = 0; i < paths.size(); i++) { - String finalImagePath = getResizedImagePath(paths.get(i), localImageOptions); - finalPath.add(i, finalImagePath); - } - finishWithListSuccess(finalPath); - } else { - finishWithListSuccess(paths); - } - } - void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) { ImageSelectionOptions localImageOptions = null; synchronized (pendingCallStateLock) { @@ -679,7 +751,7 @@ public class ImagePickerDelegate if (localImageOptions != null) { String finalImagePath = getResizedImagePath(path, localImageOptions); - //delete original file if scaled + // Delete original file if scaled. if (finalImagePath != null && !finalImagePath.equals(path) && shouldDeleteOriginalIfScaled) { new File(path).delete(); } @@ -697,7 +769,34 @@ public class ImagePickerDelegate outputOptions.getQuality().intValue()); } - void handleVideoResult(String path) { + private void handleMediaResult(@NonNull ArrayList paths) { + ImageSelectionOptions localImageOptions = null; + synchronized (pendingCallStateLock) { + if (pendingCallState != null) { + localImageOptions = pendingCallState.imageOptions; + } + } + + ArrayList finalPaths = new ArrayList<>(); + if (localImageOptions != null) { + for (int i = 0; i < paths.size(); i++) { + MediaPath path = paths.get(i); + String finalPath = path.path; + if (path.mimeType == null || !path.mimeType.startsWith("video/")) { + finalPath = getResizedImagePath(path.path, localImageOptions); + } + finalPaths.add(finalPath); + } + finishWithListSuccess(finalPaths); + } else { + for (int i = 0; i < paths.size(); i++) { + finalPaths.add(paths.get(i).path); + } + finishWithListSuccess(finalPaths); + } + } + + private void handleVideoResult(String path) { finishWithSuccess(path); } 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 31b2303a37..b5deb28934 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 @@ -19,10 +19,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.PluginRegistry; +import io.flutter.plugins.imagepicker.Messages.CacheRetrievalResult; import io.flutter.plugins.imagepicker.Messages.FlutterError; +import io.flutter.plugins.imagepicker.Messages.GeneralOptions; import io.flutter.plugins.imagepicker.Messages.ImagePickerApi; +import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions; +import io.flutter.plugins.imagepicker.Messages.MediaSelectionOptions; import io.flutter.plugins.imagepicker.Messages.Result; +import io.flutter.plugins.imagepicker.Messages.SourceCamera; import io.flutter.plugins.imagepicker.Messages.SourceSpecification; +import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions; import java.util.List; @SuppressWarnings("deprecation") @@ -279,7 +285,7 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic private void setCameraDevice( @NonNull ImagePickerDelegate delegate, @NonNull SourceSpecification source) { - Messages.SourceCamera camera = source.getCamera(); + SourceCamera camera = source.getCamera(); if (camera != null) { ImagePickerDelegate.CameraDevice device; switch (camera) { @@ -298,9 +304,8 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic @Override public void pickImages( @NonNull SourceSpecification source, - @NonNull Messages.ImageSelectionOptions options, - @NonNull Boolean allowMultiple, - @NonNull Boolean usePhotoPicker, + @NonNull ImageSelectionOptions options, + @NonNull GeneralOptions generalOptions, @NonNull Result> result) { ImagePickerDelegate delegate = getImagePickerDelegate(); if (delegate == null) { @@ -311,12 +316,12 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic } setCameraDevice(delegate, source); - if (allowMultiple) { - delegate.chooseMultiImageFromGallery(options, usePhotoPicker, result); + if (generalOptions.getAllowMultiple()) { + delegate.chooseMultiImageFromGallery(options, generalOptions.getUsePhotoPicker(), result); } else { switch (source.getType()) { case GALLERY: - delegate.chooseImageFromGallery(options, usePhotoPicker, result); + delegate.chooseImageFromGallery(options, generalOptions.getUsePhotoPicker(), result); break; case CAMERA: delegate.takeImageWithCamera(options, result); @@ -325,12 +330,26 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic } } + @Override + public void pickMedia( + @NonNull MediaSelectionOptions mediaSelectionOptions, + @NonNull GeneralOptions generalOptions, + @NonNull Result> result) { + ImagePickerDelegate delegate = getImagePickerDelegate(); + if (delegate == null) { + result.error( + new FlutterError( + "no_activity", "image_picker plugin requires a foreground activity.", null)); + return; + } + delegate.chooseMediaFromGallery(mediaSelectionOptions, generalOptions, result); + } + @Override public void pickVideos( @NonNull SourceSpecification source, - @NonNull Messages.VideoSelectionOptions options, - @NonNull Boolean allowMultiple, - @NonNull Boolean usePhotoPicker, + @NonNull VideoSelectionOptions options, + @NonNull GeneralOptions generalOptions, @NonNull Result> result) { ImagePickerDelegate delegate = getImagePickerDelegate(); if (delegate == null) { @@ -341,12 +360,12 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic } setCameraDevice(delegate, source); - if (allowMultiple) { + if (generalOptions.getAllowMultiple()) { result.error(new RuntimeException("Multi-video selection is not implemented")); } else { switch (source.getType()) { case GALLERY: - delegate.chooseVideoFromGallery(options, usePhotoPicker, result); + delegate.chooseVideoFromGallery(options, generalOptions.getUsePhotoPicker(), result); break; case CAMERA: delegate.takeVideoWithCamera(options, result); @@ -357,7 +376,7 @@ public class ImagePickerPlugin implements FlutterPlugin, ActivityAware, ImagePic @Nullable @Override - public Messages.CacheRetrievalResult retrieveLostResults() { + public CacheRetrievalResult retrieveLostResults() { ImagePickerDelegate delegate = getImagePickerDelegate(); if (delegate == null) { throw new FlutterError( 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 index 17390ac696..8a19cfd3c5 100644 --- 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 @@ -88,6 +88,79 @@ public class Messages { } } + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class GeneralOptions { + private @NonNull Boolean allowMultiple; + + public @NonNull Boolean getAllowMultiple() { + return allowMultiple; + } + + public void setAllowMultiple(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"allowMultiple\" is null."); + } + this.allowMultiple = setterArg; + } + + private @NonNull Boolean usePhotoPicker; + + public @NonNull Boolean getUsePhotoPicker() { + return usePhotoPicker; + } + + public void setUsePhotoPicker(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"usePhotoPicker\" is null."); + } + this.usePhotoPicker = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + GeneralOptions() {} + + public static final class Builder { + + private @Nullable Boolean allowMultiple; + + public @NonNull Builder setAllowMultiple(@NonNull Boolean setterArg) { + this.allowMultiple = setterArg; + return this; + } + + private @Nullable Boolean usePhotoPicker; + + public @NonNull Builder setUsePhotoPicker(@NonNull Boolean setterArg) { + this.usePhotoPicker = setterArg; + return this; + } + + public @NonNull GeneralOptions build() { + GeneralOptions pigeonReturn = new GeneralOptions(); + pigeonReturn.setAllowMultiple(allowMultiple); + pigeonReturn.setUsePhotoPicker(usePhotoPicker); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(allowMultiple); + toListResult.add(usePhotoPicker); + return toListResult; + } + + static @NonNull GeneralOptions fromList(@NonNull ArrayList list) { + GeneralOptions pigeonResult = new GeneralOptions(); + Object allowMultiple = list.get(0); + pigeonResult.setAllowMultiple((Boolean) allowMultiple); + Object usePhotoPicker = list.get(1); + pigeonResult.setUsePhotoPicker((Boolean) usePhotoPicker); + return pigeonResult; + } + } + /** * Options for image selection and output. * @@ -193,6 +266,58 @@ public class Messages { } } + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class MediaSelectionOptions { + private @NonNull ImageSelectionOptions imageSelectionOptions; + + public @NonNull ImageSelectionOptions getImageSelectionOptions() { + return imageSelectionOptions; + } + + public void setImageSelectionOptions(@NonNull ImageSelectionOptions setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"imageSelectionOptions\" is null."); + } + this.imageSelectionOptions = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + MediaSelectionOptions() {} + + public static final class Builder { + + private @Nullable ImageSelectionOptions imageSelectionOptions; + + public @NonNull Builder setImageSelectionOptions(@NonNull ImageSelectionOptions setterArg) { + this.imageSelectionOptions = setterArg; + return this; + } + + public @NonNull MediaSelectionOptions build() { + MediaSelectionOptions pigeonReturn = new MediaSelectionOptions(); + pigeonReturn.setImageSelectionOptions(imageSelectionOptions); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(1); + toListResult.add((imageSelectionOptions == null) ? null : imageSelectionOptions.toList()); + return toListResult; + } + + static @NonNull MediaSelectionOptions fromList(@NonNull ArrayList list) { + MediaSelectionOptions pigeonResult = new MediaSelectionOptions(); + Object imageSelectionOptions = list.get(0); + pigeonResult.setImageSelectionOptions( + (imageSelectionOptions == null) + ? null + : ImageSelectionOptions.fromList((ArrayList) imageSelectionOptions)); + return pigeonResult; + } + } + /** * Options for image selection and output. * @@ -523,10 +648,14 @@ public class Messages { case (byte) 129: return CacheRetrievalResult.fromList((ArrayList) readValue(buffer)); case (byte) 130: - return ImageSelectionOptions.fromList((ArrayList) readValue(buffer)); + return GeneralOptions.fromList((ArrayList) readValue(buffer)); case (byte) 131: - return SourceSpecification.fromList((ArrayList) readValue(buffer)); + return ImageSelectionOptions.fromList((ArrayList) readValue(buffer)); case (byte) 132: + return MediaSelectionOptions.fromList((ArrayList) readValue(buffer)); + case (byte) 133: + return SourceSpecification.fromList((ArrayList) readValue(buffer)); + case (byte) 134: return VideoSelectionOptions.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -541,14 +670,20 @@ public class Messages { } else if (value instanceof CacheRetrievalResult) { stream.write(129); writeValue(stream, ((CacheRetrievalResult) value).toList()); - } else if (value instanceof ImageSelectionOptions) { + } else if (value instanceof GeneralOptions) { stream.write(130); - writeValue(stream, ((ImageSelectionOptions) value).toList()); - } else if (value instanceof SourceSpecification) { + writeValue(stream, ((GeneralOptions) value).toList()); + } else if (value instanceof ImageSelectionOptions) { stream.write(131); + writeValue(stream, ((ImageSelectionOptions) value).toList()); + } else if (value instanceof MediaSelectionOptions) { + stream.write(132); + writeValue(stream, ((MediaSelectionOptions) value).toList()); + } else if (value instanceof SourceSpecification) { + stream.write(133); writeValue(stream, ((SourceSpecification) value).toList()); } else if (value instanceof VideoSelectionOptions) { - stream.write(132); + stream.write(134); writeValue(stream, ((VideoSelectionOptions) value).toList()); } else { super.writeValue(stream, value); @@ -567,8 +702,7 @@ public class Messages { void pickImages( @NonNull SourceSpecification source, @NonNull ImageSelectionOptions options, - @NonNull Boolean allowMultiple, - @NonNull Boolean usePhotoPicker, + @NonNull GeneralOptions generalOptions, @NonNull Result> result); /** * Selects video and returns their paths. @@ -579,8 +713,17 @@ public class Messages { void pickVideos( @NonNull SourceSpecification source, @NonNull VideoSelectionOptions options, - @NonNull Boolean allowMultiple, - @NonNull Boolean usePhotoPicker, + @NonNull GeneralOptions generalOptions, + @NonNull Result> result); + /** + * Selects images and videos and returns their paths. + * + *

Elements must not be null, by convention. See + * https://github.com/flutter/flutter/issues/97848 + */ + void pickMedia( + @NonNull MediaSelectionOptions mediaSelectionOptions, + @NonNull GeneralOptions generalOptions, @NonNull Result> result); /** Returns results from a previous app session, if any. */ @Nullable @@ -607,8 +750,7 @@ public class Messages { 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); + GeneralOptions generalOptionsArg = (GeneralOptions) args.get(2); Result> resultCallback = new Result>() { public void success(List result) { @@ -622,8 +764,7 @@ public class Messages { } }; - api.pickImages( - sourceArg, optionsArg, allowMultipleArg, usePhotoPickerArg, resultCallback); + api.pickImages(sourceArg, optionsArg, generalOptionsArg, resultCallback); }); } else { channel.setMessageHandler(null); @@ -644,8 +785,7 @@ public class Messages { 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); + GeneralOptions generalOptionsArg = (GeneralOptions) args.get(2); Result> resultCallback = new Result>() { public void success(List result) { @@ -659,8 +799,38 @@ public class Messages { } }; - api.pickVideos( - sourceArg, optionsArg, allowMultipleArg, usePhotoPickerArg, resultCallback); + api.pickVideos(sourceArg, optionsArg, generalOptionsArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ImagePickerApi.pickMedia", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + MediaSelectionOptions mediaSelectionOptionsArg = + (MediaSelectionOptions) args.get(0); + GeneralOptions generalOptionsArg = (GeneralOptions) args.get(1); + 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.pickMedia(mediaSelectionOptionsArg, generalOptionsArg, resultCallback); }); } else { channel.setMessageHandler(null); 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 efdbbae3b7..73ee5a0f0d 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 @@ -29,7 +29,9 @@ import android.content.pm.PackageManager; import android.net.Uri; import androidx.annotation.Nullable; import io.flutter.plugins.imagepicker.Messages.FlutterError; +import io.flutter.plugins.imagepicker.Messages.GeneralOptions; import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions; +import io.flutter.plugins.imagepicker.Messages.MediaSelectionOptions; import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions; import java.io.File; import java.io.IOException; @@ -61,6 +63,8 @@ public class ImagePickerDelegateTest { new ImageSelectionOptions.Builder().setQuality((long) 100).setMaxWidth(WIDTH).build(); private static final VideoSelectionOptions DEFAULT_VIDEO_OPTIONS = new VideoSelectionOptions.Builder().build(); + private static final MediaSelectionOptions DEFAULT_MEDIA_OPTIONS = + new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build(); @Mock Activity mockActivity; @Mock ImageResizer mockImageResizer; @@ -161,6 +165,18 @@ public class ImagePickerDelegateTest { verifyNoMoreInteractions(mockResult); } + @Test + public void chooseMediaFromGallery_whenPendingResultExists_finishesWithAlreadyActiveError() { + ImagePickerDelegate delegate = + createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null); + GeneralOptions generalOptions = + new GeneralOptions.Builder().setAllowMultiple(true).setUsePhotoPicker(true).build(); + delegate.chooseMediaFromGallery(DEFAULT_MEDIA_OPTIONS, generalOptions, mockResult); + + verifyFinishedWithAlreadyActiveError(); + verifyNoMoreInteractions(mockResult); + } + @Test @Config(sdk = 30) public void chooseImageFromGallery_launchesChooseFromGalleryIntent() { @@ -631,6 +647,19 @@ public class ImagePickerDelegateTest { assertTrue(isHandled); } + @Test + public void onActivityResult_whenMediaPickedFromGallery_returnsTrue() { + ImagePickerDelegate delegate = createDelegate(); + + boolean isHandled = + delegate.onActivityResult( + ImagePickerDelegate.REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY, + Activity.RESULT_OK, + mockIntent); + + assertTrue(isHandled); + } + @Test public void onActivityResult_whenVideoPickerFromGallery_returnsTrue() { ImagePickerDelegate delegate = createDelegate(); 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 cd408c5cef..b2c281ca54 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 @@ -24,7 +24,9 @@ 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.plugins.imagepicker.Messages.FlutterError; +import io.flutter.plugins.imagepicker.Messages.GeneralOptions; import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions; +import io.flutter.plugins.imagepicker.Messages.MediaSelectionOptions; import io.flutter.plugins.imagepicker.Messages.SourceSpecification; import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions; import java.util.List; @@ -40,6 +42,16 @@ public class ImagePickerPluginTest { new ImageSelectionOptions.Builder().setQuality((long) 100).build(); private static final VideoSelectionOptions DEFAULT_VIDEO_OPTIONS = new VideoSelectionOptions.Builder().build(); + private static final MediaSelectionOptions DEFAULT_MEDIA_OPTIONS = + new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build(); + private static final GeneralOptions GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER = + new GeneralOptions.Builder().setUsePhotoPicker(true).setAllowMultiple(true).build(); + private static final GeneralOptions GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_USE_PHOTO_PICKER = + new GeneralOptions.Builder().setUsePhotoPicker(true).setAllowMultiple(false).build(); + private static final GeneralOptions GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER = + new GeneralOptions.Builder().setUsePhotoPicker(false).setAllowMultiple(false).build(); + private static final GeneralOptions GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER = + new GeneralOptions.Builder().setUsePhotoPicker(false).setAllowMultiple(true).build(); private static final SourceSpecification SOURCE_GALLERY = new SourceSpecification.Builder().setType(Messages.SourceType.GALLERY).build(); private static final SourceSpecification SOURCE_CAMERA_FRONT = @@ -88,7 +100,10 @@ public class ImagePickerPluginTest { ImagePickerPlugin imagePickerPluginWithNullActivity = new ImagePickerPlugin(mockImagePickerDelegate, null); imagePickerPluginWithNullActivity.pickImages( - SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER, + mockResult); ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); verify(mockResult).error(errorCaptor.capture()); @@ -103,7 +118,10 @@ public class ImagePickerPluginTest { ImagePickerPlugin imagePickerPluginWithNullActivity = new ImagePickerPlugin(mockImagePickerDelegate, null); imagePickerPluginWithNullActivity.pickVideos( - SOURCE_CAMERA_REAR, DEFAULT_VIDEO_OPTIONS, false, false, mockResult); + SOURCE_CAMERA_REAR, + DEFAULT_VIDEO_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); verify(mockResult).error(errorCaptor.capture()); @@ -126,60 +144,126 @@ public class ImagePickerPluginTest { @Test public void pickImages_whenSourceIsGallery_invokesChooseImageFromGallery() { - plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); + plugin.pickImages( + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).chooseImageFromGallery(any(), eq(false), any()); verifyNoInteractions(mockResult); } @Test public void pickImages_whenSourceIsGalleryUsingPhotoPicker_invokesChooseImageFromGallery() { - plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, true, mockResult); + plugin.pickImages( + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).chooseImageFromGallery(any(), eq(true), any()); verifyNoInteractions(mockResult); } @Test public void pickImages_invokesChooseMultiImageFromGallery() { - plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, true, false, mockResult); + plugin.pickImages( + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).chooseMultiImageFromGallery(any(), eq(false), any()); verifyNoInteractions(mockResult); } @Test public void pickImages_usingPhotoPicker_invokesChooseMultiImageFromGallery() { - plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, true, true, mockResult); + plugin.pickImages( + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).chooseMultiImageFromGallery(any(), eq(true), any()); verifyNoInteractions(mockResult); } + @Test + public void pickMedia_invokesChooseMediaFromGallery() { + MediaSelectionOptions mediaSelectionOptions = + new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build(); + plugin.pickMedia( + mediaSelectionOptions, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); + verify(mockImagePickerDelegate) + .chooseMediaFromGallery( + eq(mediaSelectionOptions), + eq(GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER), + any()); + verifyNoInteractions(mockResult); + } + + @Test + public void pickMedia_usingPhotoPicker_invokesChooseMediaFromGallery() { + MediaSelectionOptions mediaSelectionOptions = + new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build(); + plugin.pickMedia( + mediaSelectionOptions, GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, mockResult); + verify(mockImagePickerDelegate) + .chooseMediaFromGallery( + eq(mediaSelectionOptions), + eq(GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER), + any()); + verifyNoInteractions(mockResult); + } + @Test public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera() { - plugin.pickImages(SOURCE_CAMERA_REAR, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); + plugin.pickImages( + SOURCE_CAMERA_REAR, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).takeImageWithCamera(any(), any()); verifyNoInteractions(mockResult); } @Test public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera_RearCamera() { - plugin.pickImages(SOURCE_CAMERA_REAR, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); + plugin.pickImages( + SOURCE_CAMERA_REAR, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.REAR)); } @Test public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera_FrontCamera() { - plugin.pickImages(SOURCE_CAMERA_FRONT, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); + plugin.pickImages( + SOURCE_CAMERA_FRONT, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.FRONT)); } @Test public void pickVideos_whenSourceIsCamera_invokesTakeImageWithCamera_RearCamera() { - plugin.pickVideos(SOURCE_CAMERA_REAR, DEFAULT_VIDEO_OPTIONS, false, false, mockResult); + plugin.pickVideos( + SOURCE_CAMERA_REAR, + DEFAULT_VIDEO_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.REAR)); } @Test public void pickVideos_whenSourceIsCamera_invokesTakeImageWithCamera_FrontCamera() { - plugin.pickVideos(SOURCE_CAMERA_FRONT, DEFAULT_VIDEO_OPTIONS, false, false, mockResult); + plugin.pickVideos( + SOURCE_CAMERA_FRONT, + DEFAULT_VIDEO_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.FRONT)); } diff --git a/packages/image_picker/image_picker_android/example/lib/main.dart b/packages/image_picker/image_picker_android/example/lib/main.dart index fa87587857..7d58a2a690 100755 --- a/packages/image_picker/image_picker_android/example/lib/main.dart +++ b/packages/image_picker/image_picker_android/example/lib/main.dart @@ -14,6 +14,7 @@ import 'package:flutter_driver/driver_extension.dart'; import 'package:image_picker_android/image_picker_android.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; // #enddocregion photo-picker-example +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void appMain() { @@ -55,14 +56,14 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _mediaFileList; void _setImageFileListFromFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + _mediaFileList = value == null ? null : [value]; } dynamic _pickImageError; - bool isVideo = false; + bool _isVideo = false; VideoPlayerController? _controller; VideoPlayerController? _toBeDisposed; @@ -77,18 +78,10 @@ class _MyHomePageState extends State { if (file != null && mounted) { await _disposeVideoController(); late VideoPlayerController controller; - if (kIsWeb) { - controller = VideoPlayerController.network(file.path); - } else { - controller = VideoPlayerController.file(File(file.path)); - } + + controller = VideoPlayerController.file(File(file.path)); _controller = controller; - // In web, most browsers won't honor a programmatic call to .play - // if the video has a sound track (and is not muted). - // Mute the video so it auto-plays in web! - // This is not needed if the call to .play is the result of user - // interaction (clicking on a "play" button, for example). - const double volume = kIsWeb ? 0.0 : 1.0; + const double volume = 1.0; await controller.setVolume(volume); await controller.initialize(); await controller.setLooping(true); @@ -101,12 +94,13 @@ class _MyHomePageState extends State { ImageSource source, { required BuildContext context, bool isMultiImage = false, + bool isMedia = false, }) async { if (_controller != null) { await _controller!.setVolume(0.0); } if (context.mounted) { - if (isVideo) { + if (_isVideo) { final XFile? file = await _picker.getVideo( source: source, maxDuration: const Duration(seconds: 10)); if (file != null && context.mounted) { @@ -117,15 +111,54 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { - final List? pickedFileList = await _picker.getMultiImage( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ); + final List? pickedFileList = isMedia + ? await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + ) + : await _picker.getMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); if (pickedFileList != null && context.mounted) { _showPickedSnackBar(context, pickedFileList); } - setState(() => _imageFileList = pickedFileList); + setState(() { + _mediaFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = _firstOrNull(await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + )); + + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } } catch (e) { setState(() => _pickImageError = e); } @@ -200,30 +233,37 @@ class _MyHomePageState extends State { if (retrieveError != null) { return retrieveError; } - if (_imageFileList != null) { + if (_mediaFileList != null) { return Semantics( label: 'image_picker_example_picked_images', child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { - final XFile image = _imageFileList![index]; + final XFile image = _mediaFileList![index]; + final String? mime = lookupMimeType(_mediaFileList![index].path); return Column( mainAxisSize: MainAxisSize.min, children: [ Text(image.name, key: const Key('image_picker_example_picked_image_name')), - // Why network for web? - // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform Semantics( label: 'image_picker_example_picked_image', - child: kIsWeb - ? Image.network(image.path) - : Image.file(File(image.path)), + child: mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: + Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index), ), ], ); }, - itemCount: _imageFileList!.length, + itemCount: _mediaFileList!.length, ), ); } else if (_pickImageError != null) { @@ -239,8 +279,19 @@ class _MyHomePageState extends State { } } + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + Widget _handlePreview() { - if (isVideo) { + if (_isVideo) { return _previewVideo(); } else { return _previewImages(); @@ -254,15 +305,15 @@ class _MyHomePageState extends State { } if (response.file != null) { if (response.type == RetrieveType.video) { - isVideo = true; + _isVideo = true; await _playVideo(response.file); } else { - isVideo = false; + _isVideo = false; setState(() { if (response.files == null) { _setImageFileListFromFile(response.file); } else { - _imageFileList = response.files; + _mediaFileList = response.files; } }); } @@ -316,7 +367,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( key: const Key('image_picker_example_from_gallery'), onPressed: () { - isVideo = false; + _isVideo = false; _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'image0', @@ -328,7 +379,40 @@ class _MyHomePageState extends State { padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( onPressed: () { - isVideo = false; + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; _onImageButtonPressed( ImageSource.gallery, context: context, @@ -344,7 +428,7 @@ class _MyHomePageState extends State { padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( onPressed: () { - isVideo = false; + _isVideo = false; _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'image2', @@ -357,7 +441,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( backgroundColor: Colors.red, onPressed: () { - isVideo = true; + _isVideo = true; _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'video0', @@ -370,7 +454,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( backgroundColor: Colors.red, onPressed: () { - isVideo = true; + _isVideo = true; _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'video1', @@ -510,3 +594,7 @@ class AspectRatioVideoState extends State { } } } + +T? _firstOrNull(List list) { + return list.isEmpty ? null : list.first; +} diff --git a/packages/image_picker/image_picker_android/example/pubspec.yaml b/packages/image_picker/image_picker_android/example/pubspec.yaml index 8921a37a67..1cce21a99e 100644 --- a/packages/image_picker/image_picker_android/example/pubspec.yaml +++ b/packages/image_picker/image_picker_android/example/pubspec.yaml @@ -19,7 +19,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - image_picker_platform_interface: ^2.3.0 + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 video_player: ^2.1.4 dev_dependencies: 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 fbc7fa7c2a..c9e2c875e8 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 @@ -11,12 +11,17 @@ import 'src/messages.g.dart'; /// An Android implementation of [ImagePickerPlatform]. class ImagePickerAndroid extends ImagePickerPlatform { - /// Creates a new plugin implemenation instance. + /// Creates a new plugin implementation instance. ImagePickerAndroid({@visibleForTesting ImagePickerApi? api}) : _hostApi = api ?? ImagePickerApi(); final ImagePickerApi _hostApi; + /// Sets [ImagePickerAndroid] to use Android 13 Photo Picker. + /// + /// Currently defaults to false, but the default is subject to change. + bool useAndroidPhotoPicker = false; + /// Registers this class as the default platform implementation. static void registerWith() { ImagePickerPlatform.instance = ImagePickerAndroid(); @@ -77,13 +82,14 @@ class ImagePickerAndroid extends ImagePickerPlatform { } return _hostApi.pickImages( - SourceSpecification(type: SourceType.gallery), - ImageSelectionOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - quality: imageQuality ?? 100), - /* allowMultiple */ true, - useAndroidPhotoPicker); + SourceSpecification(type: SourceType.gallery), + ImageSelectionOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + quality: imageQuality ?? 100), + GeneralOptions( + allowMultiple: true, usePhotoPicker: useAndroidPhotoPicker), + ); } Future _getImagePath({ @@ -108,13 +114,16 @@ class ImagePickerAndroid extends ImagePickerPlatform { } final List paths = await _hostApi.pickImages( - _buildSourceSpec(source, preferredCameraDevice), - ImageSelectionOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - quality: imageQuality ?? 100), - /* allowMultiple */ false, - useAndroidPhotoPicker); + _buildSourceSpec(source, preferredCameraDevice), + ImageSelectionOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + quality: imageQuality ?? 100), + GeneralOptions( + allowMultiple: false, + usePhotoPicker: useAndroidPhotoPicker, + ), + ); return paths.isEmpty ? null : paths.first; } @@ -138,10 +147,13 @@ class ImagePickerAndroid extends ImagePickerPlatform { Duration? maxDuration, }) async { final List paths = await _hostApi.pickVideos( - _buildSourceSpec(source, preferredCameraDevice), - VideoSelectionOptions(maxDurationSeconds: maxDuration?.inSeconds), - /* allowMultiple */ false, - useAndroidPhotoPicker); + _buildSourceSpec(source, preferredCameraDevice), + VideoSelectionOptions(maxDurationSeconds: maxDuration?.inSeconds), + GeneralOptions( + allowMultiple: false, + usePhotoPicker: useAndroidPhotoPicker, + ), + ); return paths.isEmpty ? null : paths.first; } @@ -197,6 +209,21 @@ class ImagePickerAndroid extends ImagePickerPlatform { return paths.map((dynamic path) => XFile(path as String)).toList(); } + @override + Future> getMedia({ + required MediaOptions options, + }) async { + return (await _hostApi.pickMedia( + _mediaOptionsToMediaSelectionOptions(options), + GeneralOptions( + allowMultiple: options.allowMultiple, + usePhotoPicker: useAndroidPhotoPicker, + ), + )) + .map((String? path) => XFile(path!)) + .toList(); + } + @override Future getVideo({ required ImageSource source, @@ -211,6 +238,38 @@ class ImagePickerAndroid extends ImagePickerPlatform { return path != null ? XFile(path) : null; } + MediaSelectionOptions _mediaOptionsToMediaSelectionOptions( + MediaOptions mediaOptions) { + final ImageSelectionOptions imageSelectionOptions = + _imageOptionsToImageSelectionOptionsWithValidator( + mediaOptions.imageOptions); + return MediaSelectionOptions( + imageSelectionOptions: imageSelectionOptions, + ); + } + + ImageSelectionOptions _imageOptionsToImageSelectionOptionsWithValidator( + ImageOptions? imageOptions) { + final double? maxHeight = imageOptions?.maxHeight; + final double? maxWidth = imageOptions?.maxWidth; + final int? imageQuality = imageOptions?.imageQuality; + + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + return ImageSelectionOptions( + quality: imageQuality ?? 100, maxHeight: maxHeight, maxWidth: maxWidth); + } + @override Future retrieveLostData() async { final LostDataResponse result = await getLostData(); @@ -243,7 +302,7 @@ class ImagePickerAndroid extends ImagePickerPlatform { : PlatformException(code: error.code, message: error.message); // Entries are guaranteed not to be null, even though that's not currently - // expressable in Pigeon. + // expressible in Pigeon. final List pickedFileList = result.paths.map((String? path) => XFile(path!)).toList(); @@ -309,9 +368,4 @@ class ImagePickerAndroid extends ImagePickerPlatform { // 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 index a4f15c8475..476e80db00 100644 --- a/packages/image_picker/image_picker_android/lib/src/messages.g.dart +++ b/packages/image_picker/image_picker_android/lib/src/messages.g.dart @@ -26,6 +26,32 @@ enum CacheRetrievalType { video, } +class GeneralOptions { + GeneralOptions({ + required this.allowMultiple, + required this.usePhotoPicker, + }); + + bool allowMultiple; + + bool usePhotoPicker; + + Object encode() { + return [ + allowMultiple, + usePhotoPicker, + ]; + } + + static GeneralOptions decode(Object result) { + result as List; + return GeneralOptions( + allowMultiple: result[0]! as bool, + usePhotoPicker: result[1]! as bool, + ); + } +} + /// Options for image selection and output. class ImageSelectionOptions { ImageSelectionOptions({ @@ -63,6 +89,28 @@ class ImageSelectionOptions { } } +class MediaSelectionOptions { + MediaSelectionOptions({ + required this.imageSelectionOptions, + }); + + ImageSelectionOptions imageSelectionOptions; + + Object encode() { + return [ + imageSelectionOptions.encode(), + ]; + } + + static MediaSelectionOptions decode(Object result) { + result as List; + return MediaSelectionOptions( + imageSelectionOptions: + ImageSelectionOptions.decode(result[0]! as List), + ); + } +} + /// Options for image selection and output. class VideoSelectionOptions { VideoSelectionOptions({ @@ -192,15 +240,21 @@ class _ImagePickerApiCodec extends StandardMessageCodec { } else if (value is CacheRetrievalResult) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is ImageSelectionOptions) { + } else if (value is GeneralOptions) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is SourceSpecification) { + } else if (value is ImageSelectionOptions) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is VideoSelectionOptions) { + } else if (value is MediaSelectionOptions) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is VideoSelectionOptions) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -214,10 +268,14 @@ class _ImagePickerApiCodec extends StandardMessageCodec { case 129: return CacheRetrievalResult.decode(readValue(buffer)!); case 130: - return ImageSelectionOptions.decode(readValue(buffer)!); + return GeneralOptions.decode(readValue(buffer)!); case 131: - return SourceSpecification.decode(readValue(buffer)!); + return ImageSelectionOptions.decode(readValue(buffer)!); case 132: + return MediaSelectionOptions.decode(readValue(buffer)!); + case 133: + return SourceSpecification.decode(readValue(buffer)!); + case 134: return VideoSelectionOptions.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -242,17 +300,13 @@ class ImagePickerApi { Future> pickImages( SourceSpecification arg_source, ImageSelectionOptions arg_options, - bool arg_allowMultiple, - bool arg_usePhotoPicker) async { + GeneralOptions arg_generalOptions) 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?; + final List? replyList = await channel + .send([arg_source, arg_options, arg_generalOptions]) + as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -281,17 +335,47 @@ class ImagePickerApi { Future> pickVideos( SourceSpecification arg_source, VideoSelectionOptions arg_options, - bool arg_allowMultiple, - bool arg_usePhotoPicker) async { + GeneralOptions arg_generalOptions) 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?; + final List? replyList = await channel + .send([arg_source, arg_options, arg_generalOptions]) + 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 images and videos and returns their paths. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + Future> pickMedia( + MediaSelectionOptions arg_mediaSelectionOptions, + GeneralOptions arg_generalOptions) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_mediaSelectionOptions, arg_generalOptions]) + as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', diff --git a/packages/image_picker/image_picker_android/pigeons/messages.dart b/packages/image_picker/image_picker_android/pigeons/messages.dart index 31ff22f1fb..9d264b5a11 100644 --- a/packages/image_picker/image_picker_android/pigeons/messages.dart +++ b/packages/image_picker/image_picker_android/pigeons/messages.dart @@ -13,6 +13,11 @@ import 'package:pigeon/pigeon.dart'; ), copyrightHeader: 'pigeons/copyright.txt', )) +class GeneralOptions { + GeneralOptions(this.allowMultiple, this.usePhotoPicker); + bool allowMultiple; + bool usePhotoPicker; +} /// Options for image selection and output. class ImageSelectionOptions { @@ -30,6 +35,14 @@ class ImageSelectionOptions { int quality; } +class MediaSelectionOptions { + MediaSelectionOptions({ + required this.imageSelectionOptions, + }); + + ImageSelectionOptions imageSelectionOptions; +} + /// Options for image selection and output. class VideoSelectionOptions { VideoSelectionOptions({this.maxDurationSeconds}); @@ -89,8 +102,11 @@ abstract class ImagePickerApi { /// https://github.com/flutter/flutter/issues/97848 @TaskQueue(type: TaskQueueType.serialBackgroundThread) @async - List pickImages(SourceSpecification source, - ImageSelectionOptions options, bool allowMultiple, bool usePhotoPicker); + List pickImages( + SourceSpecification source, + ImageSelectionOptions options, + GeneralOptions generalOptions, + ); /// Selects video and returns their paths. /// @@ -98,8 +114,21 @@ abstract class ImagePickerApi { /// https://github.com/flutter/flutter/issues/97848 @TaskQueue(type: TaskQueueType.serialBackgroundThread) @async - List pickVideos(SourceSpecification source, - VideoSelectionOptions options, bool allowMultiple, bool usePhotoPicker); + List pickVideos( + SourceSpecification source, + VideoSelectionOptions options, + GeneralOptions generalOptions, + ); + + /// Selects images and videos and returns their paths. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + @async + List pickMedia( + MediaSelectionOptions mediaSelectionOptions, + GeneralOptions generalOptions, + ); /// Returns results from a previous app session, if any. @TaskQueue(type: TaskQueueType.serialBackgroundThread) diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml index 8c61648db8..ed7c8dbd58 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+20 +version: 0.8.7 environment: sdk: ">=2.18.0 <4.0.0" @@ -22,7 +22,7 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 - image_picker_platform_interface: ^2.5.0 + image_picker_platform_interface: ^2.8.0 dev_dependencies: flutter_test: 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 f17d078a90..0b0cab4d6d 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 @@ -654,6 +654,129 @@ void main() { }); }); + group('#getMedia', () { + test('calls the method correctly', () async { + const List fakePaths = ['/foo.jgp', 'bar.jpg']; + api.returnValue = fakePaths; + + final List files = await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ); + + expect(api.lastCall, _LastPickType.image); + expect(files.length, 2); + expect(files[0].path, fakePaths[0]); + expect(files[1].path, fakePaths[1]); + }); + + test('passes default image options', () async { + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ); + + expect(api.passedImageOptions?.maxWidth, null); + expect(api.passedImageOptions?.maxHeight, null); + expect(api.passedImageOptions?.quality, 100); + }); + + test('passes image option arguments correctly', () async { + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + )); + + 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', () { + expect( + () => picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions(maxWidth: -1.0), + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions(maxHeight: -1.0), + ), + ), + throwsArgumentError, + ); + }); + + test('does not accept an invalid imageQuality argument', () { + expect( + () => picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions(imageQuality: -1), + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions(imageQuality: 101), + ), + ), + throwsArgumentError, + ); + }); + + test('handles an empty path response gracefully', () async { + api.returnValue = []; + + expect( + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ), + []); + }); + + test('defaults to not using Android Photo Picker', () async { + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ); + + expect(api.passedPhotoPickerFlag, false); + }); + + test('allows using Android Photo Picker', () async { + picker.useAndroidPhotoPicker = true; + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ); + + expect(api.passedPhotoPickerFlag, true); + }); + }); + group('#getImageFromSource', () { test('calls the method correctly', () async { const String fakePath = '/foo.jpg'; @@ -807,29 +930,41 @@ class _FakeImagePickerApi implements ImagePickerApi { @override Future> pickImages( - SourceSpecification source, - ImageSelectionOptions options, - bool allowMultiple, - bool usePhotoPicker) async { + SourceSpecification source, + ImageSelectionOptions options, + GeneralOptions generalOptions, + ) async { lastCall = _LastPickType.image; passedSource = source; passedImageOptions = options; - passedAllowMultiple = allowMultiple; - passedPhotoPickerFlag = usePhotoPicker; + passedAllowMultiple = generalOptions.allowMultiple; + passedPhotoPickerFlag = generalOptions.usePhotoPicker; + return returnValue as List? ?? []; + } + + @override + Future> pickMedia( + MediaSelectionOptions options, + GeneralOptions generalOptions, + ) async { + lastCall = _LastPickType.image; + passedImageOptions = options.imageSelectionOptions; + passedPhotoPickerFlag = generalOptions.usePhotoPicker; + passedAllowMultiple = generalOptions.allowMultiple; return returnValue as List? ?? []; } @override Future> pickVideos( - SourceSpecification source, - VideoSelectionOptions options, - bool allowMultiple, - bool usePhotoPicker) async { + SourceSpecification source, + VideoSelectionOptions options, + GeneralOptions generalOptions, + ) async { lastCall = _LastPickType.video; passedSource = source; passedVideoOptions = options; - passedAllowMultiple = allowMultiple; - passedPhotoPickerFlag = usePhotoPicker; + passedAllowMultiple = generalOptions.allowMultiple; + passedPhotoPickerFlag = generalOptions.usePhotoPicker; return returnValue as List? ?? []; } 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 index dbb6b143a9..d3b68913a6 100644 --- a/packages/image_picker/image_picker_android/test/test_api.g.dart +++ b/packages/image_picker/image_picker_android/test/test_api.g.dart @@ -23,15 +23,21 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec { } else if (value is CacheRetrievalResult) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is ImageSelectionOptions) { + } else if (value is GeneralOptions) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is SourceSpecification) { + } else if (value is ImageSelectionOptions) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is VideoSelectionOptions) { + } else if (value is MediaSelectionOptions) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is VideoSelectionOptions) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -45,10 +51,14 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec { case 129: return CacheRetrievalResult.decode(readValue(buffer)!); case 130: - return ImageSelectionOptions.decode(readValue(buffer)!); + return GeneralOptions.decode(readValue(buffer)!); case 131: - return SourceSpecification.decode(readValue(buffer)!); + return ImageSelectionOptions.decode(readValue(buffer)!); case 132: + return MediaSelectionOptions.decode(readValue(buffer)!); + case 133: + return SourceSpecification.decode(readValue(buffer)!); + case 134: return VideoSelectionOptions.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -66,14 +76,21 @@ abstract class TestHostImagePickerApi { /// 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); + ImageSelectionOptions options, GeneralOptions generalOptions); /// 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); + VideoSelectionOptions options, GeneralOptions generalOptions); + + /// Selects images and videos and returns their paths. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + Future> pickMedia(MediaSelectionOptions mediaSelectionOptions, + GeneralOptions generalOptions); /// Returns results from a previous app session, if any. CacheRetrievalResult? retrieveLostResults(); @@ -102,14 +119,12 @@ abstract class TestHostImagePickerApi { (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!); + final GeneralOptions? arg_generalOptions = + (args[2] as GeneralOptions?); + assert(arg_generalOptions != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null GeneralOptions.'); + final List output = await api.pickImages( + arg_source!, arg_options!, arg_generalOptions!); return [output]; }); } @@ -136,14 +151,40 @@ abstract class TestHostImagePickerApi { (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!); + final GeneralOptions? arg_generalOptions = + (args[2] as GeneralOptions?); + assert(arg_generalOptions != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null GeneralOptions.'); + final List output = await api.pickVideos( + arg_source!, arg_options!, arg_generalOptions!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null.'); + final List args = (message as List?)!; + final MediaSelectionOptions? arg_mediaSelectionOptions = + (args[0] as MediaSelectionOptions?); + assert(arg_mediaSelectionOptions != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null MediaSelectionOptions.'); + final GeneralOptions? arg_generalOptions = + (args[1] as GeneralOptions?); + assert(arg_generalOptions != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null GeneralOptions.'); + final List output = await api.pickMedia( + arg_mediaSelectionOptions!, arg_generalOptions!); return [output]; }); } diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index 100a9b0490..8230dd7f13 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.2.0 +* Adds `getMedia` method. * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. ## 2.1.12 diff --git a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart index 9fe40da255..256fe3463b 100644 --- a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart +++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart @@ -87,7 +87,8 @@ void main() { )); }); - testWidgets('Can select multiple files', (WidgetTester tester) async { + testWidgets('getMultiImage can select multiple files', + (WidgetTester tester) async { final html.FileUploadInputElement mockInput = html.FileUploadInputElement(); final ImagePickerPluginTestOverrides overrides = @@ -117,6 +118,38 @@ void main() { expect(secondFile.length(), completion(secondTextFile.size)); }); + testWidgets('getMedia can select multiple files', + (WidgetTester tester) async { + final html.FileUploadInputElement mockInput = html.FileUploadInputElement(); + + final ImagePickerPluginTestOverrides overrides = + ImagePickerPluginTestOverrides() + ..createInputElement = ((_, __) => mockInput) + ..getMultipleFilesFromInput = + ((_) => [textFile, secondTextFile]); + + final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides); + + // Init the pick file dialog... + final Future> files = + plugin.getMedia(options: const MediaOptions(allowMultiple: true)); + + // Mock the browser behavior of selecting a file... + mockInput.dispatchEvent(html.Event('change')); + + // Now the file should be available + expect(files, completes); + + // And readable + expect((await files).first.readAsBytes(), completion(isNotEmpty)); + + // Peek into the second file... + final XFile secondFile = (await files).elementAt(1); + expect(secondFile.readAsBytes(), completion(isNotEmpty)); + expect(secondFile.name, secondTextFile.name); + expect(secondFile.length(), completion(secondTextFile.size)); + }); + // There's no good way of detecting when the user has "aborted" the selection. testWidgets('computeCaptureAttribute', (WidgetTester tester) async { diff --git a/packages/image_picker/image_picker_for_web/example/pubspec.yaml b/packages/image_picker/image_picker_for_web/example/pubspec.yaml index 9c431bd6e9..433a160183 100644 --- a/packages/image_picker/image_picker_for_web/example/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/example/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: sdk: flutter image_picker_for_web: path: ../ - image_picker_platform_interface: ^2.2.0 + image_picker_platform_interface: ^2.8.0 dev_dependencies: flutter_driver: diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index bb261f76f3..fb88c96a59 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -8,6 +8,7 @@ import 'dart:html' as html; import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mime/mime.dart' as mime; import 'src/image_resizer.dart'; @@ -166,7 +167,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { return files.first; } - /// Injects a file input, and returns a list of XFile that the user selected locally. + /// Injects a file input, and returns a list of XFile images that the user selected locally. @override Future> getMultiImage({ double? maxWidth, @@ -189,6 +190,30 @@ class ImagePickerPlugin extends ImagePickerPlatform { return Future.wait(resized); } + /// Injects a file input, and returns a list of XFile media that the user selected locally. + @override + Future> getMedia({ + required MediaOptions options, + }) async { + final List images = await getFiles( + accept: '$_kAcceptImageMimeType,$_kAcceptVideoMimeType', + multiple: options.allowMultiple, + ); + final Iterable> resized = images.map((XFile media) { + if (mime.lookupMimeType(media.path)?.startsWith('image/') ?? false) { + return _imageResizer.resizeImageIfNeeded( + media, + options.imageOptions.maxWidth, + options.imageOptions.maxHeight, + options.imageOptions.imageQuality, + ); + } + return Future.value(media); + }); + + return Future.wait(resized); + } + /// Injects a file input with the specified accept+capture attributes, and /// returns a list of XFile that the user selected locally. /// diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index 06a7093f59..a61a5b838c 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_for_web description: Web platform implementation of image_picker repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_for_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 2.1.12 +version: 2.2.0 environment: sdk: ">=2.18.0 <4.0.0" @@ -21,7 +21,8 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - image_picker_platform_interface: ^2.2.0 + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 dev_dependencies: flutter_test: diff --git a/packages/image_picker/image_picker_ios/CHANGELOG.md b/packages/image_picker/image_picker_ios/CHANGELOG.md index 1173ddf27b..78805ad581 100644 --- a/packages/image_picker/image_picker_ios/CHANGELOG.md +++ b/packages/image_picker/image_picker_ios/CHANGELOG.md @@ -1,4 +1,9 @@ +## 0.8.8 + +* Adds `getMedia` and `getMultipleMedia` methods. + ## 0.8.7+4 + * Fixes `BuildContext` handling in example. * Updates metadata unit test to work on iOS 16.2. diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m index ede62336a9..cc2262179e 100644 --- a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m @@ -182,6 +182,32 @@ [mockUIImagePicker setSourceType:UIImagePickerControllerSourceTypePhotoLibrary]); } +- (void)testPickMediaShouldUseUIImagePickerControllerOnPreiOS14 { + if (@available(iOS 14, *)) { + return; + } + + id mockUIImagePicker = OCMClassMock([UIImagePickerController class]); + id photoLibrary = OCMClassMock([PHPhotoLibrary class]); + OCMStub(ClassMethod([photoLibrary authorizationStatus])) + .andReturn(PHAuthorizationStatusAuthorized); + + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + [plugin setImagePickerControllerOverrides:@[ mockUIImagePicker ]]; + FLTMediaSelectionOptions *mediaSelectionOptions = + [FLTMediaSelectionOptions makeWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] + imageQuality:@(50) + requestFullMetadata:@YES + allowMultiple:@YES]; + + [plugin pickMediaWithMediaSelectionOptions:mediaSelectionOptions + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; + OCMVerify(times(1), + [mockUIImagePicker setSourceType:UIImagePickerControllerSourceTypePhotoLibrary]); +} + - (void)testPickImageWithoutFullMetadata { id mockUIImagePicker = OCMClassMock([UIImagePickerController class]); id photoLibrary = OCMClassMock([PHPhotoLibrary class]); @@ -217,6 +243,28 @@ OCMVerify(times(0), [photoLibrary authorizationStatus]); } +- (void)testPickMediaWithoutFullMetadata { + id mockUIImagePicker = OCMClassMock([UIImagePickerController class]); + id photoLibrary = OCMClassMock([PHPhotoLibrary class]); + + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + [plugin setImagePickerControllerOverrides:@[ mockUIImagePicker ]]; + + FLTMediaSelectionOptions *mediaSelectionOptions = + [FLTMediaSelectionOptions makeWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] + imageQuality:@(50) + requestFullMetadata:@YES + allowMultiple:@YES]; + + [plugin pickMediaWithMediaSelectionOptions:mediaSelectionOptions + + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; + + OCMVerify(times(0), [photoLibrary authorizationStatus]); +} + #pragma mark - Test camera devices, no op on simulators - (void)testPluginPickImageDeviceCancelClickMultipleTimes { @@ -298,6 +346,36 @@ [self waitForExpectationsWithTimeout:30 handler:nil]; } +- (void)testPluginMediaPathHasNoItem { + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; + plugin.callContext = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^(NSArray *_Nullable result, FlutterError *_Nullable error) { + XCTAssertEqualObjects(result, @[]); + [resultExpectation fulfill]; + }]; + [plugin sendCallResultWithSavedPathList:@[]]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)testPluginMediaPathHasItem { + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + NSArray *pathList = @[ @"test" ]; + + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; + + plugin.callContext = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^(NSArray *_Nullable result, FlutterError *_Nullable error) { + XCTAssertEqualObjects(result, pathList); + [resultExpectation fulfill]; + }]; + [plugin sendCallResultWithSavedPathList:pathList]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + - (void)testSendsImageInvalidSourceError API_AVAILABLE(ios(14)) { id mockPickerViewController = OCMClassMock([PHPickerViewController class]); diff --git a/packages/image_picker/image_picker_ios/example/lib/main.dart b/packages/image_picker/image_picker_ios/example/lib/main.dart index 76076a5dbd..0f42b58ad2 100755 --- a/packages/image_picker/image_picker_ios/example/lib/main.dart +++ b/packages/image_picker/image_picker_ios/example/lib/main.dart @@ -10,6 +10,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void main() { @@ -38,14 +39,14 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _mediaFileList; void _setImageFileListFromFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + _mediaFileList = value == null ? null : [value]; } dynamic _pickImageError; - bool isVideo = false; + bool _isVideo = false; VideoPlayerController? _controller; VideoPlayerController? _toBeDisposed; @@ -60,18 +61,10 @@ class _MyHomePageState extends State { if (file != null && mounted) { await _disposeVideoController(); late VideoPlayerController controller; - if (kIsWeb) { - controller = VideoPlayerController.network(file.path); - } else { - controller = VideoPlayerController.file(File(file.path)); - } + + controller = VideoPlayerController.file(File(file.path)); _controller = controller; - // In web, most browsers won't honor a programmatic call to .play - // if the video has a sound track (and is not muted). - // Mute the video so it auto-plays in web! - // This is not needed if the call to .play is the result of user - // interaction (clicking on a "play" button, for example). - const double volume = kIsWeb ? 0.0 : 1.0; + const double volume = 1.0; await controller.setVolume(volume); await controller.initialize(); await controller.setLooping(true); @@ -80,13 +73,17 @@ class _MyHomePageState extends State { } } - Future _onImageButtonPressed(ImageSource source, - {required BuildContext context, bool isMultiImage = false}) async { + Future _onImageButtonPressed( + ImageSource source, { + required BuildContext context, + bool isMultiImage = false, + bool isMedia = false, + }) async { if (_controller != null) { await _controller!.setVolume(0.0); } if (context.mounted) { - if (isVideo) { + if (_isVideo) { final XFile? file = await _picker.getVideo( source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); @@ -94,18 +91,27 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { - final List pickedFileList = - await _picker.getMultiImageWithOptions( - options: MultiImagePickerOptions( - imageOptions: ImageOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ), - ), - ); + final List pickedFileList = isMedia + ? await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + ) + : await _picker.getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ), + ); setState(() { - _imageFileList = pickedFileList; + _mediaFileList = pickedFileList; }); } catch (e) { setState(() { @@ -113,6 +119,31 @@ class _MyHomePageState extends State { }); } }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = _firstOrNull(await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + )); + + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } + } catch (e) { + setState(() => _pickImageError = e); + } + }); } else { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { @@ -186,22 +217,28 @@ class _MyHomePageState extends State { if (retrieveError != null) { return retrieveError; } - if (_imageFileList != null) { + if (_mediaFileList != null) { return Semantics( label: 'image_picker_example_picked_images', child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { - // Why network for web? - // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform + final String? mime = lookupMimeType(_mediaFileList![index].path); return Semantics( label: 'image_picker_example_picked_image', - child: kIsWeb - ? Image.network(_imageFileList![index].path) - : Image.file(File(_imageFileList![index].path)), + child: mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index), ); }, - itemCount: _imageFileList!.length, + itemCount: _mediaFileList!.length, ), ); } else if (_pickImageError != null) { @@ -217,8 +254,19 @@ class _MyHomePageState extends State { } } + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = kIsWeb ? 0.0 : 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + Widget _handlePreview() { - if (isVideo) { + if (_isVideo) { return _previewVideo(); } else { return _previewImages(); @@ -240,8 +288,9 @@ class _MyHomePageState extends State { Semantics( label: 'image_picker_example_from_gallery', child: FloatingActionButton( + key: const Key('image_picker_example_from_gallery'), onPressed: () { - isVideo = false; + _isVideo = false; _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'image0', @@ -253,7 +302,40 @@ class _MyHomePageState extends State { padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( onPressed: () { - isVideo = false; + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; _onImageButtonPressed( ImageSource.gallery, context: context, @@ -269,7 +351,7 @@ class _MyHomePageState extends State { padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( onPressed: () { - isVideo = false; + _isVideo = false; _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'image2', @@ -282,7 +364,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( backgroundColor: Colors.red, onPressed: () { - isVideo = true; + _isVideo = true; _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'video0', @@ -295,7 +377,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( backgroundColor: Colors.red, onPressed: () { - isVideo = true; + _isVideo = true; _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'video1', @@ -428,3 +510,7 @@ class AspectRatioVideoState extends State { } } } + +T? _firstOrNull(List list) { + return list.isEmpty ? null : list.first; +} diff --git a/packages/image_picker/image_picker_ios/example/pubspec.yaml b/packages/image_picker/image_picker_ios/example/pubspec.yaml index d0bca043d1..9d08635370 100755 --- a/packages/image_picker/image_picker_ios/example/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/example/pubspec.yaml @@ -16,7 +16,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - image_picker_platform_interface: ^2.6.1 + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 video_player: ^2.1.4 dev_dependencies: diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h index 0016765a0f..212f09236b 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h @@ -16,7 +16,10 @@ NS_ASSUME_NONNULL_BEGIN + (nullable PHAsset *)getAssetFromPHPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14)); -// Save image with correct meta data and extention copied from the original asset. +// Saves video to temporary URL. Returns nil on failure; ++ (NSURL *)saveVideoFromURL:(NSURL *)videoURL; + +// Saves image with correct meta data and extention copied from the original asset. // maxWidth and maxHeight are used only for GIF images. + (NSString *)saveImageWithOriginalImageData:(NSData *)originalImageData image:(UIImage *)image diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m index bf712cdce3..294bbc7794 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m @@ -20,6 +20,20 @@ return fetchResult.firstObject; } ++ (NSURL *)saveVideoFromURL:(NSURL *)videoURL { + if (![[NSFileManager defaultManager] isReadableFileAtPath:[videoURL path]]) { + return nil; + } + NSString *fileName = [videoURL lastPathComponent]; + NSURL *destination = [NSURL fileURLWithPath:[self temporaryFilePath:fileName]]; + NSError *error; + [[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error]; + if (error) { + return nil; + } + return destination; +} + + (NSString *)saveImageWithOriginalImageData:(NSData *)originalImageData image:(UIImage *)image maxWidth:(NSNumber *)maxWidth diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m index 5aadecdf94..c812e35186 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m @@ -109,7 +109,13 @@ typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPic PHPickerConfiguration *config = [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary]; config.selectionLimit = context.maxImageCount; - config.filter = [PHPickerFilter imagesFilter]; + if (context.includeVideo) { + config.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[ + [PHPickerFilter imagesFilter], [PHPickerFilter videosFilter] + ]]; + } else { + config.filter = [PHPickerFilter imagesFilter]; + } _pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config]; _pickerViewController.delegate = self; @@ -128,7 +134,12 @@ typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPic UIImagePickerController *imagePickerController = [self createImagePickerController]; imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; imagePickerController.delegate = self; - imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; + if (context.includeVideo) { + imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage, (NSString *)kUTTypeMovie ]; + + } else { + imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; + } self.callContext = context; switch (source.type) { @@ -206,6 +217,29 @@ typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPic } } +- (void)pickMediaWithMediaSelectionOptions:(nonnull FLTMediaSelectionOptions *)mediaSelectionOptions + completion:(nonnull void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion { + FLTImagePickerMethodCallContext *context = + [[FLTImagePickerMethodCallContext alloc] initWithResult:completion]; + context.maxSize = [mediaSelectionOptions maxSize]; + context.imageQuality = [mediaSelectionOptions imageQuality]; + context.requestFullMetadata = [mediaSelectionOptions requestFullMetadata]; + context.includeVideo = YES; + if (![[mediaSelectionOptions allowMultiple] boolValue]) { + context.maxImageCount = 1; + } + + if (@available(iOS 14, *)) { + [self launchPHPickerWithContext:context]; + } else { + // Camera is ignored for gallery mode, so the value here is arbitrary. + [self launchUIImagePickerWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeGallery + camera:FLTSourceCameraRear] + context:context]; + } +} + - (void)pickVideoWithSource:(nonnull FLTSourceSpecification *)source maxDuration:(nullable NSNumber *)maxDurationSeconds completion: @@ -538,25 +572,16 @@ typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPic } if (videoURL != nil) { if (@available(iOS 13.0, *)) { - NSString *fileName = [videoURL lastPathComponent]; - NSURL *destination = - [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]]; - - if ([[NSFileManager defaultManager] isReadableFileAtPath:[videoURL path]]) { - NSError *error; - if (![[videoURL path] isEqualToString:[destination path]]) { - [[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error]; - - if (error) { - [self sendCallResultWithError:[FlutterError - errorWithCode:@"flutter_image_picker_copy_video_error" - message:@"Could not cache the video file." - details:nil]]; - return; - } - } - videoURL = destination; + NSURL *destination = [FLTImagePickerPhotoAssetUtil saveVideoFromURL:videoURL]; + if (destination == nil) { + [self sendCallResultWithError:[FlutterError + errorWithCode:@"flutter_image_picker_copy_video_error" + message:@"Could not cache the video file." + details:nil]]; + return; } + + videoURL = destination; } [self sendCallResultWithSavedPathList:@[ videoURL.path ]]; } else { diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h index f84921160a..99d3ef6e19 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h @@ -11,7 +11,7 @@ NS_ASSUME_NONNULL_BEGIN /** - * The return hander used for all method calls, which internally adapts the provided result list + * The return handler used for all method calls, which internally adapts the provided result list * to return either a list or a single element depending on the original call. */ typedef void (^FlutterResultAdapter)(NSArray *_Nullable, FlutterError *_Nullable); @@ -49,6 +49,9 @@ typedef void (^FlutterResultAdapter)(NSArray *_Nullable, FlutterErro /** Whether the image should be picked with full metadata (requires gallery permissions) */ @property(nonatomic, assign) BOOL requestFullMetadata; +/** Whether the picker should include videos in the list*/ +@property(nonatomic, assign) BOOL includeVideo; + @end #pragma mark - diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m index 80e03ddd65..3476721ae6 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m @@ -107,9 +107,15 @@ API_AVAILABLE(ios(14)) [self completeOperationWithPath:nil error:flutterError]; } }]; + } else if ([self.result.itemProvider + // This supports uniform types that conform to UTTypeMovie. + // This includes kUTTypeVideo, kUTTypeMPEG4, public.3gpp, kUTTypeMPEG, + // public.3gpp2, public.avi, kUTTypeQuickTimeMovie. + hasItemConformingToTypeIdentifier:UTTypeMovie.identifier]) { + [self processVideo]; } else { FlutterError *flutterError = [FlutterError errorWithCode:@"invalid_source" - message:@"Invalid image source." + message:@"Invalid media source." details:nil]; [self completeOperationWithPath:nil error:flutterError]; } @@ -184,4 +190,41 @@ API_AVAILABLE(ios(14)) } } +/** + * Processes the video. + */ +- (void)processVideo API_AVAILABLE(ios(14)) { + NSString *typeIdentifier = self.result.itemProvider.registeredTypeIdentifiers.firstObject; + [self.result.itemProvider + loadFileRepresentationForTypeIdentifier:typeIdentifier + completionHandler:^(NSURL *_Nullable videoURL, + NSError *_Nullable error) { + if (error != nil) { + FlutterError *flutterError = + [FlutterError errorWithCode:@"invalid_image" + message:error.localizedDescription + details:error.domain]; + [self completeOperationWithPath:nil error:flutterError]; + return; + } + + NSURL *destination = + [FLTImagePickerPhotoAssetUtil saveVideoFromURL:videoURL]; + if (destination == nil) { + [self + completeOperationWithPath:nil + error:[FlutterError + errorWithCode: + @"flutter_image_picker_copy_" + @"video_error" + message:@"Could not cache " + @"the video file." + details:nil]]; + return; + } + + [self completeOperationWithPath:[destination path] error:nil]; + }]; +} + @end diff --git a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h index cdde03d505..4e2c4b28c1 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h @@ -1,7 +1,7 @@ // 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.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @@ -24,6 +24,7 @@ typedef NS_ENUM(NSUInteger, FLTSourceType) { }; @class FLTMaxSize; +@class FLTMediaSelectionOptions; @class FLTSourceSpecification; @interface FLTMaxSize : NSObject @@ -32,6 +33,19 @@ typedef NS_ENUM(NSUInteger, FLTSourceType) { @property(nonatomic, strong, nullable) NSNumber *height; @end +@interface FLTMediaSelectionOptions : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithMaxSize:(FLTMaxSize *)maxSize + imageQuality:(nullable NSNumber *)imageQuality + requestFullMetadata:(NSNumber *)requestFullMetadata + allowMultiple:(NSNumber *)allowMultiple; +@property(nonatomic, strong) FLTMaxSize *maxSize; +@property(nonatomic, strong, nullable) NSNumber *imageQuality; +@property(nonatomic, strong) NSNumber *requestFullMetadata; +@property(nonatomic, strong) NSNumber *allowMultiple; +@end + @interface FLTSourceSpecification : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -57,6 +71,10 @@ NSObject *FLTImagePickerApiGetCodec(void); - (void)pickVideoWithSource:(FLTSourceSpecification *)source maxDuration:(nullable NSNumber *)maxDurationSeconds completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion; +/// Selects images and videos and returns their paths. +- (void)pickMediaWithMediaSelectionOptions:(FLTMediaSelectionOptions *)mediaSelectionOptions + completion:(void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion; @end extern void FLTImagePickerApiSetup(id binaryMessenger, diff --git a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m index a1d863639c..2a24f83670 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m @@ -1,7 +1,7 @@ // 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.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "messages.g.h" @@ -30,6 +30,12 @@ static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { - (NSArray *)toList; @end +@interface FLTMediaSelectionOptions () ++ (FLTMediaSelectionOptions *)fromList:(NSArray *)list; ++ (nullable FLTMediaSelectionOptions *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @interface FLTSourceSpecification () + (FLTSourceSpecification *)fromList:(NSArray *)list; + (nullable FLTSourceSpecification *)nullableFromList:(NSArray *)list; @@ -60,6 +66,42 @@ static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { } @end +@implementation FLTMediaSelectionOptions ++ (instancetype)makeWithMaxSize:(FLTMaxSize *)maxSize + imageQuality:(nullable NSNumber *)imageQuality + requestFullMetadata:(NSNumber *)requestFullMetadata + allowMultiple:(NSNumber *)allowMultiple { + FLTMediaSelectionOptions *pigeonResult = [[FLTMediaSelectionOptions alloc] init]; + pigeonResult.maxSize = maxSize; + pigeonResult.imageQuality = imageQuality; + pigeonResult.requestFullMetadata = requestFullMetadata; + pigeonResult.allowMultiple = allowMultiple; + return pigeonResult; +} ++ (FLTMediaSelectionOptions *)fromList:(NSArray *)list { + FLTMediaSelectionOptions *pigeonResult = [[FLTMediaSelectionOptions alloc] init]; + pigeonResult.maxSize = [FLTMaxSize nullableFromList:(GetNullableObjectAtIndex(list, 0))]; + NSAssert(pigeonResult.maxSize != nil, @""); + pigeonResult.imageQuality = GetNullableObjectAtIndex(list, 1); + pigeonResult.requestFullMetadata = GetNullableObjectAtIndex(list, 2); + NSAssert(pigeonResult.requestFullMetadata != nil, @""); + pigeonResult.allowMultiple = GetNullableObjectAtIndex(list, 3); + NSAssert(pigeonResult.allowMultiple != nil, @""); + return pigeonResult; +} ++ (nullable FLTMediaSelectionOptions *)nullableFromList:(NSArray *)list { + return (list) ? [FLTMediaSelectionOptions fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.maxSize ? [self.maxSize toList] : [NSNull null]), + (self.imageQuality ?: [NSNull null]), + (self.requestFullMetadata ?: [NSNull null]), + (self.allowMultiple ?: [NSNull null]), + ]; +} +@end + @implementation FLTSourceSpecification + (instancetype)makeWithType:(FLTSourceType)type camera:(FLTSourceCamera)camera { FLTSourceSpecification *pigeonResult = [[FLTSourceSpecification alloc] init]; @@ -92,6 +134,8 @@ static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { case 128: return [FLTMaxSize fromList:[self readValue]]; case 129: + return [FLTMediaSelectionOptions fromList:[self readValue]]; + case 130: return [FLTSourceSpecification fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -106,9 +150,12 @@ static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { if ([value isKindOfClass:[FLTMaxSize class]]) { [self writeByte:128]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FLTSourceSpecification class]]) { + } else if ([value isKindOfClass:[FLTMediaSelectionOptions class]]) { [self writeByte:129]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FLTSourceSpecification class]]) { + [self writeByte:130]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -220,4 +267,28 @@ void FLTImagePickerApiSetup(id binaryMessenger, [channel setMessageHandler:nil]; } } + /// Selects images and videos and returns their paths. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickMedia" + binaryMessenger:binaryMessenger + codec:FLTImagePickerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(pickMediaWithMediaSelectionOptions:completion:)], + @"FLTImagePickerApi api (%@) doesn't respond to " + @"@selector(pickMediaWithMediaSelectionOptions:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTMediaSelectionOptions *arg_mediaSelectionOptions = GetNullableObjectAtIndex(args, 0); + [api pickMediaWithMediaSelectionOptions:arg_mediaSelectionOptions + completion:^(NSArray *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } } diff --git a/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart index 3f76784ff0..02105f95e5 100644 --- a/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart +++ b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart @@ -175,6 +175,51 @@ class ImagePickerIOS extends ImagePickerPlatform { ); } + @override + Future> getMedia({ + required MediaOptions options, + }) async { + final MediaSelectionOptions mediaSelectionOptions = + _mediaOptionsToMediaSelectionOptions(options); + + return (await _hostApi.pickMedia(mediaSelectionOptions)) + .map((String? path) => XFile(path!)) + .toList(); + } + + MaxSize _imageOptionsToMaxSizeWithValidation(ImageOptions imageOptions) { + final double? maxHeight = imageOptions.maxHeight; + final double? maxWidth = imageOptions.maxWidth; + final int? imageQuality = imageOptions.imageQuality; + + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + + return MaxSize(width: maxWidth, height: maxHeight); + } + + MediaSelectionOptions _mediaOptionsToMediaSelectionOptions( + MediaOptions mediaOptions) { + final MaxSize maxSize = + _imageOptionsToMaxSizeWithValidation(mediaOptions.imageOptions); + return MediaSelectionOptions( + maxSize: maxSize, + imageQuality: mediaOptions.imageOptions.imageQuality, + requestFullMetadata: mediaOptions.imageOptions.requestFullMetadata, + allowMultiple: mediaOptions.allowMultiple, + ); + } + @override Future pickVideo({ required ImageSource source, diff --git a/packages/image_picker/image_picker_ios/lib/src/messages.g.dart b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart index 87596b78eb..91dde827a6 100644 --- a/packages/image_picker/image_picker_ios/lib/src/messages.g.dart +++ b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // 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.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), 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 @@ -47,6 +47,42 @@ class MaxSize { } } +class MediaSelectionOptions { + MediaSelectionOptions({ + required this.maxSize, + this.imageQuality, + required this.requestFullMetadata, + required this.allowMultiple, + }); + + MaxSize maxSize; + + int? imageQuality; + + bool requestFullMetadata; + + bool allowMultiple; + + Object encode() { + return [ + maxSize.encode(), + imageQuality, + requestFullMetadata, + allowMultiple, + ]; + } + + static MediaSelectionOptions decode(Object result) { + result as List; + return MediaSelectionOptions( + maxSize: MaxSize.decode(result[0]! as List), + imageQuality: result[1] as int?, + requestFullMetadata: result[2]! as bool, + allowMultiple: result[3]! as bool, + ); + } +} + class SourceSpecification { SourceSpecification({ required this.type, @@ -80,9 +116,12 @@ class _ImagePickerApiCodec extends StandardMessageCodec { if (value is MaxSize) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is SourceSpecification) { + } else if (value is MediaSelectionOptions) { buffer.putUint8(129); writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -94,6 +133,8 @@ class _ImagePickerApiCodec extends StandardMessageCodec { case 128: return MaxSize.decode(readValue(buffer)!); case 129: + return MediaSelectionOptions.decode(readValue(buffer)!); + case 130: return SourceSpecification.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -184,4 +225,33 @@ class ImagePickerApi { return (replyList[0] as String?); } } + + /// Selects images and videos and returns their paths. + Future> pickMedia( + MediaSelectionOptions arg_mediaSelectionOptions) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_mediaSelectionOptions]) 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(); + } + } } diff --git a/packages/image_picker/image_picker_ios/pigeons/messages.dart b/packages/image_picker/image_picker_ios/pigeons/messages.dart index d04841b0fd..fb69a6d133 100644 --- a/packages/image_picker/image_picker_ios/pigeons/messages.dart +++ b/packages/image_picker/image_picker_ios/pigeons/messages.dart @@ -20,6 +20,20 @@ class MaxSize { double? height; } +class MediaSelectionOptions { + MediaSelectionOptions({ + required this.maxSize, + this.imageQuality, + required this.requestFullMetadata, + required this.allowMultiple, + }); + + MaxSize maxSize; + int? imageQuality; + bool requestFullMetadata; + bool allowMultiple; +} + // Corresponds to `CameraDevice` from the platform interface package. enum SourceCamera { rear, front } @@ -45,4 +59,9 @@ abstract class ImagePickerApi { @async @ObjCSelector('pickVideoWithSource:maxDuration:') String? pickVideo(SourceSpecification source, int? maxDurationSeconds); + + /// Selects images and videos and returns their paths. + @async + @ObjCSelector('pickMediaWithMediaSelectionOptions:') + List pickMedia(MediaSelectionOptions mediaSelectionOptions); } diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml index 90d6dff73d..a924689145 100755 --- a/packages/image_picker/image_picker_ios/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_ios description: iOS implementation of the image_picker plugin. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.7+4 +version: 0.8.8 environment: sdk: ">=2.18.0 <4.0.0" @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - image_picker_platform_interface: ^2.6.1 + image_picker_platform_interface: ^2.8.0 dev_dependencies: flutter_test: diff --git a/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart index 2c9d52509f..da74e31f0a 100644 --- a/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart +++ b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart @@ -71,6 +71,19 @@ class _ApiLogger implements TestHostImagePickerApi { return returnValue as List?; } + @override + Future> pickMedia( + MediaSelectionOptions mediaSelectionOptions) async { + calls.add(_LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': mediaSelectionOptions.maxSize.width, + 'maxHeight': mediaSelectionOptions.maxSize.height, + 'imageQuality': mediaSelectionOptions.imageQuality, + 'requestFullMetadata': mediaSelectionOptions.requestFullMetadata, + 'allowMultiple': mediaSelectionOptions.allowMultiple, + })); + return returnValue as List; + } + @override Future pickVideo( SourceSpecification source, int? maxDurationSeconds) async { @@ -878,6 +891,227 @@ void main() { }); }); + group('#getMedia', () { + test('calls the method correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMedia(options: const MediaOptions(allowMultiple: true)); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMedia(options: const MediaOptions(allowMultiple: true)); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: 10.0, + ), + )); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxHeight: 10.0, + ), + )); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: 10.0, + maxHeight: 20.0, + ), + )); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: 10.0, + imageQuality: 70, + ), + )); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxHeight: 10.0, + imageQuality: 70, + ), + )); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + )); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + ], + ); + }); + + test('passes request metadata argument correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions(requestFullMetadata: false), + )); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': false, + 'allowMultiple': true + }), + ], + ); + }); + + test('passes allowMultiple argument correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMedia( + options: const MediaOptions( + allowMultiple: false, + )); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': false + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + log.returnValue = ['0', '1']; + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate(maxWidth: -1.0), + )), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate(maxHeight: -1.0), + )), + throwsArgumentError, + ); + }); + + test('does not accept a invalid imageQuality argument', () { + log.returnValue = ['0', '1']; + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate(imageQuality: -1), + )), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate(imageQuality: 101), + )), + throwsArgumentError, + ); + }); + + test('handles a empty path response gracefully', () async { + log.returnValue = []; + + expect( + await picker.getMedia( + options: const MediaOptions(allowMultiple: true)), + []); + }); + }); + group('#getVideo', () { test('passes the image source argument correctly', () async { await picker.getVideo(source: ImageSource.camera); diff --git a/packages/image_picker/image_picker_ios/test/test_api.g.dart b/packages/image_picker/image_picker_ios/test/test_api.g.dart index 4ac619590f..6da0400b1a 100644 --- a/packages/image_picker/image_picker_ios/test/test_api.g.dart +++ b/packages/image_picker/image_picker_ios/test/test_api.g.dart @@ -1,7 +1,7 @@ // 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.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), 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 @@ -20,9 +20,12 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec { if (value is MaxSize) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is SourceSpecification) { + } else if (value is MediaSelectionOptions) { buffer.putUint8(129); writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -34,6 +37,8 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec { case 128: return MaxSize.decode(readValue(buffer)!); case 129: + return MediaSelectionOptions.decode(readValue(buffer)!); + case 130: return SourceSpecification.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -55,6 +60,9 @@ abstract class TestHostImagePickerApi { Future pickVideo( SourceSpecification source, int? maxDurationSeconds); + /// Selects images and videos and returns their paths. + Future> pickMedia(MediaSelectionOptions mediaSelectionOptions); + static void setup(TestHostImagePickerApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -140,5 +148,29 @@ abstract class TestHostImagePickerApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null.'); + final List args = (message as List?)!; + final MediaSelectionOptions? arg_mediaSelectionOptions = + (args[0] as MediaSelectionOptions?); + assert(arg_mediaSelectionOptions != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null MediaSelectionOptions.'); + final List output = + await api.pickMedia(arg_mediaSelectionOptions!); + return [output]; + }); + } + } } } diff --git a/packages/image_picker/image_picker_linux/CHANGELOG.md b/packages/image_picker/image_picker_linux/CHANGELOG.md index d3bfbf901b..9f14cc71ce 100644 --- a/packages/image_picker/image_picker_linux/CHANGELOG.md +++ b/packages/image_picker/image_picker_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.1 + +* Adds `getMedia` method. + ## 0.2.0 * Implements initial Linux support. diff --git a/packages/image_picker/image_picker_linux/example/lib/main.dart b/packages/image_picker/image_picker_linux/example/lib/main.dart index 9e22c716a2..8f4887095c 100644 --- a/packages/image_picker/image_picker_linux/example/lib/main.dart +++ b/packages/image_picker/image_picker_linux/example/lib/main.dart @@ -9,6 +9,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void main() { @@ -37,11 +38,11 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _mediaFileList; // This must be called from within a setState() callback void _setImageFileListFromFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + _mediaFileList = value == null ? null : [value]; } dynamic _pickImageError; @@ -70,52 +71,12 @@ class _MyHomePageState extends State { } } - Future _handleMultiImagePicked(BuildContext context) async { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final List? pickedFileList = await _picker.getMultiImage( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ); - setState(() { - _imageFileList = pickedFileList; - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); - } - - Future _handleSingleImagePicked( - BuildContext context, ImageSource source) async { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final XFile? pickedFile = await _picker.getImageFromSource( - source: source, - options: ImagePickerOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ), - ); - setState(() { - _setImageFileListFromFile(pickedFile); - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); - } - - Future _onImageButtonPressed(ImageSource source, - {required BuildContext context, bool isMultiImage = false}) async { + Future _onImageButtonPressed( + ImageSource source, { + required BuildContext context, + bool isMultiImage = false, + bool isMedia = false, + }) async { if (_controller != null) { await _controller!.setVolume(0.0); } @@ -125,9 +86,83 @@ class _MyHomePageState extends State { source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); } else if (isMultiImage) { - await _handleMultiImagePicked(context); + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = isMedia + ? await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + ) + : await _picker.getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ), + ); + setState(() { + _mediaFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = _firstOrNull(await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + )); + + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } + } catch (e) { + setState(() => _pickImageError = e); + } + }); } else { - await _handleSingleImagePicked(context, source); + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ); + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); } } } @@ -180,18 +215,28 @@ class _MyHomePageState extends State { if (retrieveError != null) { return retrieveError; } - if (_imageFileList != null) { + if (_mediaFileList != null) { return Semantics( label: 'image_picker_example_picked_images', child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { + final String? mime = lookupMimeType(_mediaFileList![index].path); return Semantics( label: 'image_picker_example_picked_image', - child: Image.file(File(_imageFileList![index].path)), + child: mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index), ); }, - itemCount: _imageFileList!.length, + itemCount: _mediaFileList!.length, ), ); } else if (_pickImageError != null) { @@ -207,6 +252,17 @@ class _MyHomePageState extends State { } } + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + Widget _handlePreview() { if (_isVideo) { return _previewVideo(); @@ -230,6 +286,7 @@ class _MyHomePageState extends State { Semantics( label: 'image_picker_example_from_gallery', child: FloatingActionButton( + key: const Key('image_picker_example_from_gallery'), onPressed: () { _isVideo = false; _onImageButtonPressed(ImageSource.gallery, context: context); @@ -239,6 +296,39 @@ class _MyHomePageState extends State { child: const Icon(Icons.photo), ), ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( @@ -420,3 +510,7 @@ class AspectRatioVideoState extends State { } } } + +T? _firstOrNull(List list) { + return list.isEmpty ? null : list.first; +} diff --git a/packages/image_picker/image_picker_linux/example/pubspec.yaml b/packages/image_picker/image_picker_linux/example/pubspec.yaml index 54beb76564..76e8f25ac1 100644 --- a/packages/image_picker/image_picker_linux/example/pubspec.yaml +++ b/packages/image_picker/image_picker_linux/example/pubspec.yaml @@ -17,7 +17,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: .. - image_picker_platform_interface: ^2.7.0 + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 video_player: ^2.1.4 dev_dependencies: diff --git a/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart b/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart index f932a02117..72596ea931 100644 --- a/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart +++ b/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart @@ -154,4 +154,27 @@ class ImagePickerLinux extends CameraDelegatingImagePickerPlatform { .openFiles(acceptedTypeGroups: [typeGroup]); return files; } + + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently + // supported. If any of these arguments are supplied, they will be silently + // ignored. + @override + Future> getMedia({required MediaOptions options}) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'images and videos', extensions: ['image/*', 'video/*']); + + List files; + + if (options.allowMultiple) { + files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + } else { + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + files = [ + if (file != null) file, + ]; + } + return files; + } } diff --git a/packages/image_picker/image_picker_linux/pubspec.yaml b/packages/image_picker/image_picker_linux/pubspec.yaml index dcfd6758ad..9698991e33 100644 --- a/packages/image_picker/image_picker_linux/pubspec.yaml +++ b/packages/image_picker/image_picker_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_linux description: Linux platform implementation of image_picker repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.2.0 +version: 0.2.1 environment: sdk: ">=2.18.0 <4.0.0" @@ -20,7 +20,7 @@ dependencies: file_selector_platform_interface: ^2.2.0 flutter: sdk: flutter - image_picker_platform_interface: ^2.7.0 + image_picker_platform_interface: ^2.8.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart b/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart index 32c3d45091..004bfcc4dc 100644 --- a/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart +++ b/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart @@ -125,6 +125,38 @@ void main() { plugin.getVideo(source: ImageSource.camera), throwsStateError); }); }); + + group('media', () { + test('getMedia passes the accepted type groups correctly', () async { + await plugin.getMedia(options: const MediaOptions(allowMultiple: true)); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, + ['image/*', 'video/*']); + }); + + test('multiple media handles an empty path response gracefully', () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ), + []); + }); + + test('single media handles an empty path response gracefully', () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: false, + ), + ), + []); + }); + }); } class FakeCameraDelegate extends ImagePickerCameraDelegate { diff --git a/packages/image_picker/image_picker_macos/CHANGELOG.md b/packages/image_picker/image_picker_macos/CHANGELOG.md index 94ce98bd3a..bd79a8674c 100644 --- a/packages/image_picker/image_picker_macos/CHANGELOG.md +++ b/packages/image_picker/image_picker_macos/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.1 + +* Adds `getMedia` method. + ## 0.2.0 * Implements initial macOS support. diff --git a/packages/image_picker/image_picker_macos/example/lib/main.dart b/packages/image_picker/image_picker_macos/example/lib/main.dart index 9e22c716a2..8f4887095c 100644 --- a/packages/image_picker/image_picker_macos/example/lib/main.dart +++ b/packages/image_picker/image_picker_macos/example/lib/main.dart @@ -9,6 +9,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void main() { @@ -37,11 +38,11 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _mediaFileList; // This must be called from within a setState() callback void _setImageFileListFromFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + _mediaFileList = value == null ? null : [value]; } dynamic _pickImageError; @@ -70,52 +71,12 @@ class _MyHomePageState extends State { } } - Future _handleMultiImagePicked(BuildContext context) async { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final List? pickedFileList = await _picker.getMultiImage( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ); - setState(() { - _imageFileList = pickedFileList; - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); - } - - Future _handleSingleImagePicked( - BuildContext context, ImageSource source) async { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final XFile? pickedFile = await _picker.getImageFromSource( - source: source, - options: ImagePickerOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ), - ); - setState(() { - _setImageFileListFromFile(pickedFile); - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); - } - - Future _onImageButtonPressed(ImageSource source, - {required BuildContext context, bool isMultiImage = false}) async { + Future _onImageButtonPressed( + ImageSource source, { + required BuildContext context, + bool isMultiImage = false, + bool isMedia = false, + }) async { if (_controller != null) { await _controller!.setVolume(0.0); } @@ -125,9 +86,83 @@ class _MyHomePageState extends State { source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); } else if (isMultiImage) { - await _handleMultiImagePicked(context); + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = isMedia + ? await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + ) + : await _picker.getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ), + ); + setState(() { + _mediaFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = _firstOrNull(await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + )); + + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } + } catch (e) { + setState(() => _pickImageError = e); + } + }); } else { - await _handleSingleImagePicked(context, source); + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ); + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); } } } @@ -180,18 +215,28 @@ class _MyHomePageState extends State { if (retrieveError != null) { return retrieveError; } - if (_imageFileList != null) { + if (_mediaFileList != null) { return Semantics( label: 'image_picker_example_picked_images', child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { + final String? mime = lookupMimeType(_mediaFileList![index].path); return Semantics( label: 'image_picker_example_picked_image', - child: Image.file(File(_imageFileList![index].path)), + child: mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index), ); }, - itemCount: _imageFileList!.length, + itemCount: _mediaFileList!.length, ), ); } else if (_pickImageError != null) { @@ -207,6 +252,17 @@ class _MyHomePageState extends State { } } + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + Widget _handlePreview() { if (_isVideo) { return _previewVideo(); @@ -230,6 +286,7 @@ class _MyHomePageState extends State { Semantics( label: 'image_picker_example_from_gallery', child: FloatingActionButton( + key: const Key('image_picker_example_from_gallery'), onPressed: () { _isVideo = false; _onImageButtonPressed(ImageSource.gallery, context: context); @@ -239,6 +296,39 @@ class _MyHomePageState extends State { child: const Icon(Icons.photo), ), ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( @@ -420,3 +510,7 @@ class AspectRatioVideoState extends State { } } } + +T? _firstOrNull(List list) { + return list.isEmpty ? null : list.first; +} diff --git a/packages/image_picker/image_picker_macos/example/pubspec.yaml b/packages/image_picker/image_picker_macos/example/pubspec.yaml index e76c49286e..785a2afb22 100644 --- a/packages/image_picker/image_picker_macos/example/pubspec.yaml +++ b/packages/image_picker/image_picker_macos/example/pubspec.yaml @@ -17,7 +17,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: .. - image_picker_platform_interface: ^2.7.0 + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 video_player: ^2.1.4 dev_dependencies: diff --git a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart index 7a7e92737b..9e9447a571 100644 --- a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart +++ b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart @@ -159,4 +159,28 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { .openFiles(acceptedTypeGroups: [typeGroup]); return files; } + + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently + // supported. If any of these arguments are supplied, they will be silently + // ignored. + @override + Future> getMedia({required MediaOptions options}) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'images and videos', + extensions: ['public.image', 'public.movie']); + + List files; + + if (options.allowMultiple) { + files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + } else { + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + files = [ + if (file != null) file, + ]; + } + return files; + } } diff --git a/packages/image_picker/image_picker_macos/pubspec.yaml b/packages/image_picker/image_picker_macos/pubspec.yaml index ef97bd4bc2..9ace885e66 100644 --- a/packages/image_picker/image_picker_macos/pubspec.yaml +++ b/packages/image_picker/image_picker_macos/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_macos description: macOS platform implementation of image_picker repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.2.0 +version: 0.2.1 environment: sdk: ">=2.18.0 <4.0.0" @@ -20,7 +20,7 @@ dependencies: file_selector_platform_interface: ^2.3.0 flutter: sdk: flutter - image_picker_platform_interface: ^2.7.0 + image_picker_platform_interface: ^2.8.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart index f2b45cf33d..7e94161d4a 100644 --- a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart +++ b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart @@ -131,6 +131,38 @@ void main() { plugin.getVideo(source: ImageSource.camera), throwsStateError); }); }); + + group('media', () { + test('getMedia passes the accepted type groups correctly', () async { + await plugin.getMedia(options: const MediaOptions(allowMultiple: true)); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, + ['public.image', 'public.movie']); + }); + + test('multiple media handles an empty path response gracefully', () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ), + []); + }); + + test('single media handles an empty path response gracefully', () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: false, + ), + ), + []); + }); + }); } class FakeCameraDelegate extends ImagePickerCameraDelegate { diff --git a/packages/image_picker/image_picker_windows/CHANGELOG.md b/packages/image_picker/image_picker_windows/CHANGELOG.md index 2159d87012..bd881d1fc3 100644 --- a/packages/image_picker/image_picker_windows/CHANGELOG.md +++ b/packages/image_picker/image_picker_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.1 + +* Adds `getMedia` method. + ## 0.2.0 * Updates minimum Flutter version to 3.3. diff --git a/packages/image_picker/image_picker_windows/example/lib/main.dart b/packages/image_picker/image_picker_windows/example/lib/main.dart index 9e22c716a2..8f4887095c 100644 --- a/packages/image_picker/image_picker_windows/example/lib/main.dart +++ b/packages/image_picker/image_picker_windows/example/lib/main.dart @@ -9,6 +9,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void main() { @@ -37,11 +38,11 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _mediaFileList; // This must be called from within a setState() callback void _setImageFileListFromFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + _mediaFileList = value == null ? null : [value]; } dynamic _pickImageError; @@ -70,52 +71,12 @@ class _MyHomePageState extends State { } } - Future _handleMultiImagePicked(BuildContext context) async { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final List? pickedFileList = await _picker.getMultiImage( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ); - setState(() { - _imageFileList = pickedFileList; - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); - } - - Future _handleSingleImagePicked( - BuildContext context, ImageSource source) async { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final XFile? pickedFile = await _picker.getImageFromSource( - source: source, - options: ImagePickerOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ), - ); - setState(() { - _setImageFileListFromFile(pickedFile); - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); - } - - Future _onImageButtonPressed(ImageSource source, - {required BuildContext context, bool isMultiImage = false}) async { + Future _onImageButtonPressed( + ImageSource source, { + required BuildContext context, + bool isMultiImage = false, + bool isMedia = false, + }) async { if (_controller != null) { await _controller!.setVolume(0.0); } @@ -125,9 +86,83 @@ class _MyHomePageState extends State { source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); } else if (isMultiImage) { - await _handleMultiImagePicked(context); + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = isMedia + ? await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + ) + : await _picker.getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ), + ); + setState(() { + _mediaFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = _firstOrNull(await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + )); + + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } + } catch (e) { + setState(() => _pickImageError = e); + } + }); } else { - await _handleSingleImagePicked(context, source); + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ); + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); } } } @@ -180,18 +215,28 @@ class _MyHomePageState extends State { if (retrieveError != null) { return retrieveError; } - if (_imageFileList != null) { + if (_mediaFileList != null) { return Semantics( label: 'image_picker_example_picked_images', child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { + final String? mime = lookupMimeType(_mediaFileList![index].path); return Semantics( label: 'image_picker_example_picked_image', - child: Image.file(File(_imageFileList![index].path)), + child: mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index), ); }, - itemCount: _imageFileList!.length, + itemCount: _mediaFileList!.length, ), ); } else if (_pickImageError != null) { @@ -207,6 +252,17 @@ class _MyHomePageState extends State { } } + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + Widget _handlePreview() { if (_isVideo) { return _previewVideo(); @@ -230,6 +286,7 @@ class _MyHomePageState extends State { Semantics( label: 'image_picker_example_from_gallery', child: FloatingActionButton( + key: const Key('image_picker_example_from_gallery'), onPressed: () { _isVideo = false; _onImageButtonPressed(ImageSource.gallery, context: context); @@ -239,6 +296,39 @@ class _MyHomePageState extends State { child: const Icon(Icons.photo), ), ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( @@ -420,3 +510,7 @@ class AspectRatioVideoState extends State { } } } + +T? _firstOrNull(List list) { + return list.isEmpty ? null : list.first; +} diff --git a/packages/image_picker/image_picker_windows/example/pubspec.yaml b/packages/image_picker/image_picker_windows/example/pubspec.yaml index a645670f37..6515d50769 100644 --- a/packages/image_picker/image_picker_windows/example/pubspec.yaml +++ b/packages/image_picker/image_picker_windows/example/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: flutter: sdk: flutter - image_picker_platform_interface: ^2.7.0 + image_picker_platform_interface: ^2.8.0 image_picker_windows: # When depending on this package from a real application you should use: # image_picker_windows: ^x.y.z @@ -18,6 +18,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: .. + mime: ^1.0.4 video_player: ^2.1.4 dev_dependencies: diff --git a/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart b/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart index ba7ff4d6e7..e9e414628c 100644 --- a/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart +++ b/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart @@ -183,4 +183,28 @@ class ImagePickerWindows extends CameraDelegatingImagePickerPlatform { .openFiles(acceptedTypeGroups: [typeGroup]); return files; } + + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not + // supported on Windows. If any of these arguments is supplied, + // they will be silently ignored by the Windows version of the plugin. + @override + Future> getMedia({required MediaOptions options}) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'images and videos', + extensions: [...imageFormats, ...videoFormats]); + + List files; + + if (options.allowMultiple) { + files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + } else { + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + files = [ + if (file != null) file, + ]; + } + return files; + } } diff --git a/packages/image_picker/image_picker_windows/pubspec.yaml b/packages/image_picker/image_picker_windows/pubspec.yaml index 2ca2fc5557..e16ecbda99 100644 --- a/packages/image_picker/image_picker_windows/pubspec.yaml +++ b/packages/image_picker/image_picker_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_windows description: Windows platform implementation of image_picker repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.2.0 +version: 0.2.1 environment: sdk: ">=2.18.0 <4.0.0" @@ -20,7 +20,7 @@ dependencies: file_selector_windows: ^0.9.0 flutter: sdk: flutter - image_picker_platform_interface: ^2.7.0 + image_picker_platform_interface: ^2.8.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart index d680d782f6..6da0873af5 100644 --- a/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart +++ b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart @@ -128,6 +128,41 @@ void main() { plugin.getVideo(source: ImageSource.camera), throwsStateError); }); }); + + group('media', () { + test('getMedia passes the accepted type groups correctly', () async { + await plugin.getMedia(options: const MediaOptions(allowMultiple: true)); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, [ + ...ImagePickerWindows.imageFormats, + ...ImagePickerWindows.videoFormats + ]); + }); + + test('multiple media handles an empty path response gracefully', + () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ), + []); + }); + + test('single media handles an empty path response gracefully', () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: false, + ), + ), + []); + }); + }); }); } diff --git a/script/configs/allowed_unpinned_deps.yaml b/script/configs/allowed_unpinned_deps.yaml index 1cf35c8881..d027396f93 100644 --- a/script/configs/allowed_unpinned_deps.yaml +++ b/script/configs/allowed_unpinned_deps.yaml @@ -48,6 +48,7 @@ - logging - markdown - meta +- mime - path - shelf - shelf_static