Skip to content

Commit b26262b

Browse files
[image_picker] Add the ability to pick multiple videos - platform implementations (#9818)
Platform implementation portion of #9775 This adds support for the new `getMultiVideoWithOptions` method to all implementation packages. Part of flutter/flutter#102283 Fixes flutter/flutter#146400 ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 676643e commit b26262b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1004
-361
lines changed

packages/image_picker/image_picker_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.8.13
2+
3+
* Adds support for `getMultiVideoWithOptions`.
4+
15
## 0.8.12+25
26

37
* Updates kotlin version to 2.2.0 to enable gradle 8.11 support.

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public class ImagePickerDelegate
8181
@VisibleForTesting static final int REQUEST_CAMERA_IMAGE_PERMISSION = 2345;
8282
@VisibleForTesting static final int REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY = 2346;
8383
@VisibleForTesting static final int REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY = 2347;
84+
@VisibleForTesting static final int REQUEST_CODE_CHOOSE_MULTI_VIDEO_FROM_GALLERY = 2348;
8485

8586
@VisibleForTesting static final int REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY = 2352;
8687
@VisibleForTesting static final int REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA = 2353;
@@ -474,6 +475,38 @@ private void launchMultiPickImageFromGalleryIntent(Boolean usePhotoPicker, int l
474475
pickMultiImageIntent, REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY);
475476
}
476477

478+
public void chooseMultiVideoFromGallery(
479+
@NonNull VideoSelectionOptions options,
480+
boolean usePhotoPicker,
481+
int limit,
482+
@NonNull Messages.Result<List<String>> result) {
483+
if (!setPendingOptionsAndResult(null, options, result)) {
484+
finishWithAlreadyActiveError(result);
485+
return;
486+
}
487+
488+
launchMultiPickVideoFromGalleryIntent(usePhotoPicker, limit);
489+
}
490+
491+
private void launchMultiPickVideoFromGalleryIntent(Boolean usePhotoPicker, int limit) {
492+
Intent pickMultiVideoIntent;
493+
if (usePhotoPicker) {
494+
pickMultiVideoIntent =
495+
new ActivityResultContracts.PickMultipleVisualMedia(limit)
496+
.createIntent(
497+
activity,
498+
new PickVisualMediaRequest.Builder()
499+
.setMediaType(ActivityResultContracts.PickVisualMedia.VideoOnly.INSTANCE)
500+
.build());
501+
} else {
502+
pickMultiVideoIntent = new Intent(Intent.ACTION_GET_CONTENT);
503+
pickMultiVideoIntent.setType("video/*");
504+
pickMultiVideoIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
505+
}
506+
activity.startActivityForResult(
507+
pickMultiVideoIntent, REQUEST_CODE_CHOOSE_MULTI_VIDEO_FROM_GALLERY);
508+
}
509+
477510
public void takeImageWithCamera(
478511
@NonNull ImageSelectionOptions options, @NonNull Messages.Result<List<String>> result) {
479512
if (!setPendingOptionsAndResult(options, null, result)) {
@@ -617,6 +650,9 @@ public boolean onActivityResult(
617650
case REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY:
618651
handlerRunnable = () -> handleChooseMultiImageResult(resultCode, data);
619652
break;
653+
case REQUEST_CODE_CHOOSE_MULTI_VIDEO_FROM_GALLERY:
654+
handlerRunnable = () -> handleChooseMultiVideoResult(resultCode, data);
655+
break;
620656
case REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA:
621657
handlerRunnable = () -> handleCaptureImageResult(resultCode);
622658
break;
@@ -696,6 +732,24 @@ private void handleChooseImageResult(int resultCode, Intent data) {
696732
finishWithSuccess(null);
697733
}
698734

735+
private void handleChooseMultiVideoResult(int resultCode, Intent intent) {
736+
if (resultCode == Activity.RESULT_OK && intent != null) {
737+
ArrayList<MediaPath> paths = getPathsFromIntent(intent, false);
738+
// If there's no valid Uri, return an error
739+
if (paths == null) {
740+
finishWithError(
741+
"missing_valid_video_uri", "Cannot find at least one of the selected videos.");
742+
return;
743+
}
744+
745+
handleMediaResult(paths);
746+
return;
747+
}
748+
749+
// User cancelled choosing a video.
750+
finishWithSuccess(null);
751+
}
752+
699753
public class MediaPath {
700754
public MediaPath(@NonNull String path, @Nullable String mimeType) {
701755
this.path = path;

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,9 @@ public void pickVideos(
339339

340340
setCameraDevice(delegate, source);
341341
if (generalOptions.getAllowMultiple()) {
342-
result.error(new RuntimeException("Multi-video selection is not implemented"));
342+
int limit = ImagePickerUtils.getLimitFromOption(generalOptions);
343+
delegate.chooseMultiVideoFromGallery(
344+
options, generalOptions.getUsePhotoPicker(), limit, result);
343345
} else {
344346
switch (source.getType()) {
345347
case GALLERY:

packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,41 @@ public void pickVideos_whenSourceIsCamera_invokesTakeImageWithCamera_FrontCamera
299299
verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.FRONT));
300300
}
301301

302+
@Test
303+
public void pickVideos_invokesChooseMultiVideoFromGallery() {
304+
plugin.pickVideos(
305+
SOURCE_GALLERY,
306+
DEFAULT_VIDEO_OPTIONS,
307+
GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER,
308+
mockResult);
309+
verify(mockImagePickerDelegate)
310+
.chooseMultiVideoFromGallery(any(), eq(false), eq(Integer.MAX_VALUE), any());
311+
verifyNoInteractions(mockResult);
312+
}
313+
314+
@Test
315+
public void pickVideos_usingPhotoPicker_invokesChooseMultiVideoFromGallery() {
316+
plugin.pickVideos(
317+
SOURCE_GALLERY,
318+
DEFAULT_VIDEO_OPTIONS,
319+
GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER,
320+
mockResult);
321+
verify(mockImagePickerDelegate)
322+
.chooseMultiVideoFromGallery(any(), eq(true), eq(Integer.MAX_VALUE), any());
323+
verifyNoInteractions(mockResult);
324+
}
325+
326+
@Test
327+
public void pickVideos_withLimit5_invokesChooseMultiVideoFromGallery() {
328+
plugin.pickVideos(
329+
SOURCE_GALLERY,
330+
DEFAULT_VIDEO_OPTIONS,
331+
GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER_WITH_LIMIT,
332+
mockResult);
333+
verify(mockImagePickerDelegate).chooseMultiVideoFromGallery(any(), eq(false), eq(5), any());
334+
verifyNoInteractions(mockResult);
335+
}
336+
302337
@Test
303338
public void onConstructor_whenContextTypeIsActivity_shouldNotCrash() {
304339
new ImagePickerPlugin(mockImagePickerDelegate, mockActivity);

packages/image_picker/image_picker_android/example/lib/main.dart

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,10 @@ class _MyHomePageState extends State<MyHomePage> {
7979
Future<void> _playVideo(XFile? file) async {
8080
if (file != null && mounted) {
8181
await _disposeVideoController();
82-
late VideoPlayerController controller;
83-
84-
controller = VideoPlayerController.file(File(file.path));
82+
final VideoPlayerController controller =
83+
VideoPlayerController.file(File(file.path));
8584
_controller = controller;
86-
const double volume = 1.0;
87-
await controller.setVolume(volume);
85+
await controller.setVolume(1.0);
8886
await controller.initialize();
8987
await controller.setLooping(true);
9088
await controller.play();
@@ -95,21 +93,28 @@ class _MyHomePageState extends State<MyHomePage> {
9593
Future<void> _onImageButtonPressed(
9694
ImageSource source, {
9795
required BuildContext context,
98-
bool isMultiImage = false,
96+
bool allowMultiple = false,
9997
bool isMedia = false,
10098
}) async {
10199
if (_controller != null) {
102100
await _controller!.setVolume(0.0);
103101
}
104102
if (context.mounted) {
105103
if (_isVideo) {
106-
final XFile? file = await _picker.getVideo(
107-
source: source, maxDuration: const Duration(seconds: 10));
108-
if (file != null && context.mounted) {
109-
_showPickedSnackBar(context, <XFile>[file]);
104+
final List<XFile> files;
105+
if (allowMultiple) {
106+
files = await _picker.getMultiVideoWithOptions();
107+
} else {
108+
final XFile? file = await _picker.getVideo(
109+
source: source, maxDuration: const Duration(seconds: 10));
110+
files = <XFile>[if (file != null) file];
111+
}
112+
if (files.isNotEmpty && context.mounted) {
113+
_showPickedSnackBar(context, files);
114+
// Just play the first file, to keep the example simple.
115+
await _playVideo(files.first);
110116
}
111-
await _playVideo(file);
112-
} else if (isMultiImage) {
117+
} else if (allowMultiple) {
113118
await _displayPickImageDialog(context, true, (double? maxWidth,
114119
double? maxHeight, int? quality, int? limit) async {
115120
try {
@@ -121,7 +126,7 @@ class _MyHomePageState extends State<MyHomePage> {
121126
final List<XFile> pickedFileList = isMedia
122127
? await _picker.getMedia(
123128
options: MediaOptions(
124-
allowMultiple: isMultiImage,
129+
allowMultiple: allowMultiple,
125130
imageOptions: imageOptions,
126131
limit: limit,
127132
),
@@ -151,7 +156,7 @@ class _MyHomePageState extends State<MyHomePage> {
151156
final List<XFile> pickedFileList = <XFile>[];
152157
final XFile? media = _firstOrNull(await _picker.getMedia(
153158
options: MediaOptions(
154-
allowMultiple: isMultiImage,
159+
allowMultiple: allowMultiple,
155160
imageOptions: ImageOptions(
156161
maxWidth: maxWidth,
157162
maxHeight: maxHeight,
@@ -290,8 +295,7 @@ class _MyHomePageState extends State<MyHomePage> {
290295
Widget _buildInlineVideoPlayer(int index) {
291296
final VideoPlayerController controller =
292297
VideoPlayerController.file(File(_mediaFileList![index].path));
293-
const double volume = 1.0;
294-
controller.setVolume(volume);
298+
controller.setVolume(1.0);
295299
controller.initialize();
296300
controller.setLooping(true);
297301
controller.play();
@@ -314,7 +318,7 @@ class _MyHomePageState extends State<MyHomePage> {
314318
if (response.file != null) {
315319
if (response.type == RetrieveType.video) {
316320
_isVideo = true;
317-
await _playVideo(response.file);
321+
await _playVideo(response.files?.firstOrNull);
318322
} else {
319323
_isVideo = false;
320324
setState(() {
@@ -380,8 +384,8 @@ class _MyHomePageState extends State<MyHomePage> {
380384
_onImageButtonPressed(ImageSource.gallery, context: context);
381385
},
382386
heroTag: 'image0',
383-
tooltip: 'Pick Image from gallery',
384-
label: const Text('Pick Image from gallery'),
387+
tooltip: 'Pick image from gallery',
388+
label: const Text('Pick image from gallery'),
385389
icon: const Icon(Icons.photo),
386390
),
387391
),
@@ -393,13 +397,12 @@ class _MyHomePageState extends State<MyHomePage> {
393397
_onImageButtonPressed(
394398
ImageSource.gallery,
395399
context: context,
396-
isMultiImage: true,
397-
isMedia: true,
400+
allowMultiple: true,
398401
);
399402
},
400-
heroTag: 'multipleMedia',
401-
tooltip: 'Pick Multiple Media from gallery',
402-
label: const Text('Pick Multiple Media from gallery'),
403+
heroTag: 'image1',
404+
tooltip: 'Pick multiple images',
405+
label: const Text('Pick multiple images'),
403406
icon: const Icon(Icons.photo_library),
404407
),
405408
),
@@ -415,9 +418,9 @@ class _MyHomePageState extends State<MyHomePage> {
415418
);
416419
},
417420
heroTag: 'media',
418-
tooltip: 'Pick Single Media from gallery',
419-
label: const Text('Pick Single Media from gallery'),
420-
icon: const Icon(Icons.photo_library),
421+
tooltip: 'Pick item from gallery',
422+
label: const Text('Pick item from gallery'),
423+
icon: const Icon(Icons.photo_outlined),
421424
),
422425
),
423426
Padding(
@@ -428,13 +431,14 @@ class _MyHomePageState extends State<MyHomePage> {
428431
_onImageButtonPressed(
429432
ImageSource.gallery,
430433
context: context,
431-
isMultiImage: true,
434+
allowMultiple: true,
435+
isMedia: true,
432436
);
433437
},
434-
heroTag: 'image1',
435-
tooltip: 'Pick Multiple Image from gallery',
436-
label: const Text('Pick Multiple Image from gallery'),
437-
icon: const Icon(Icons.photo_library),
438+
heroTag: 'multipleMedia',
439+
tooltip: 'Pick multiple items',
440+
label: const Text('Pick multiple items'),
441+
icon: const Icon(Icons.photo_library_outlined),
438442
),
439443
),
440444
Padding(
@@ -445,8 +449,8 @@ class _MyHomePageState extends State<MyHomePage> {
445449
_onImageButtonPressed(ImageSource.camera, context: context);
446450
},
447451
heroTag: 'image2',
448-
tooltip: 'Take a Photo',
449-
label: const Text('Take a Photo'),
452+
tooltip: 'Take a photo',
453+
label: const Text('Take a photo'),
450454
icon: const Icon(Icons.camera_alt),
451455
),
452456
),
@@ -458,9 +462,24 @@ class _MyHomePageState extends State<MyHomePage> {
458462
_isVideo = true;
459463
_onImageButtonPressed(ImageSource.gallery, context: context);
460464
},
461-
heroTag: 'video0',
462-
tooltip: 'Pick Video from gallery',
463-
label: const Text('Pick Video from gallery'),
465+
heroTag: 'video',
466+
tooltip: 'Pick video from gallery',
467+
label: const Text('Pick video from gallery'),
468+
icon: const Icon(Icons.video_file),
469+
),
470+
),
471+
Padding(
472+
padding: const EdgeInsets.only(top: 16.0),
473+
child: FloatingActionButton.extended(
474+
backgroundColor: Colors.red,
475+
onPressed: () {
476+
_isVideo = true;
477+
_onImageButtonPressed(ImageSource.gallery,
478+
context: context, allowMultiple: true);
479+
},
480+
heroTag: 'multiVideo',
481+
tooltip: 'Pick multiple videos',
482+
label: const Text('Pick multiple videos'),
464483
icon: const Icon(Icons.video_library),
465484
),
466485
),
@@ -472,9 +491,9 @@ class _MyHomePageState extends State<MyHomePage> {
472491
_isVideo = true;
473492
_onImageButtonPressed(ImageSource.camera, context: context);
474493
},
475-
heroTag: 'video1',
476-
tooltip: 'Take a Video',
477-
label: const Text('Take a Video'),
494+
heroTag: 'takeVideo',
495+
tooltip: 'Take a video',
496+
label: const Text('Take a video'),
478497
icon: const Icon(Icons.videocam),
479498
),
480499
),

packages/image_picker/image_picker_android/example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ dependencies:
1919
# The example app is bundled with the plugin so we use a path dependency on
2020
# the parent directory to use the current plugin's version.
2121
path: ../
22-
image_picker_platform_interface: ^2.10.0
22+
image_picker_platform_interface: ^2.11.0
2323
mime: ^2.0.0
2424
video_player: ^2.1.4
2525

packages/image_picker/image_picker_android/lib/image_picker_android.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,27 @@ class ImagePickerAndroid extends ImagePickerPlatform {
261261
return path != null ? XFile(path) : null;
262262
}
263263

264+
@override
265+
Future<List<XFile>> getMultiVideoWithOptions({
266+
MultiVideoPickerOptions options = const MultiVideoPickerOptions(),
267+
}) async {
268+
final List<String> paths = await _hostApi.pickVideos(
269+
SourceSpecification(type: SourceType.gallery),
270+
VideoSelectionOptions(maxDurationSeconds: options.maxDuration?.inSeconds),
271+
GeneralOptions(
272+
allowMultiple: true,
273+
usePhotoPicker: useAndroidPhotoPicker,
274+
limit: options.limit,
275+
),
276+
);
277+
278+
if (paths.isEmpty) {
279+
return <XFile>[];
280+
}
281+
282+
return paths.map((String path) => XFile(path)).toList();
283+
}
284+
264285
MediaSelectionOptions _mediaOptionsToMediaSelectionOptions(
265286
MediaOptions mediaOptions) {
266287
final ImageSelectionOptions imageSelectionOptions =

0 commit comments

Comments
 (0)