From 8961c9bd909bf3cd60acd3df7a83832380cac17f Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 12 Apr 2022 16:27:20 -0400 Subject: [PATCH 01/10] Switch to internal copy of method channel implementation --- .../lib/image_picker_ios.dart | 280 +++++ .../image_picker_ios/pubspec.yaml | 4 +- .../test/image_picker_ios_test.dart | 973 ++++++++++++++++++ 3 files changed, 1256 insertions(+), 1 deletion(-) create mode 100644 packages/image_picker/image_picker_ios/lib/image_picker_ios.dart create mode 100644 packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart 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 new file mode 100644 index 000000000000..35e5b400004a --- /dev/null +++ b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart @@ -0,0 +1,280 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +const MethodChannel _channel = MethodChannel('plugins.flutter.io/image_picker'); + +/// An implementation of [ImagePickerPlatform] for iOS. +class ImagePickerIOS extends ImagePickerPlatform { + /// The MethodChannel that is being used by this implementation of the plugin. + @visibleForTesting + MethodChannel get channel => _channel; + + @override + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + final String? path = await _getImagePath( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? PickedFile(path) : null; + } + + @override + Future?> pickMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + final List? paths = await _getMultiImagePath( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + ); + if (paths == null) { + return null; + } + + return paths.map((dynamic path) => PickedFile(path as String)).toList(); + } + + Future?> _getMultiImagePath({ + double? maxWidth, + double? maxHeight, + int? 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 _channel.invokeMethod?>( + 'pickMultiImage', + { + 'maxWidth': maxWidth, + 'maxHeight': maxHeight, + 'imageQuality': imageQuality, + }, + ); + } + + Future _getImagePath({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) { + 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 _channel.invokeMethod( + 'pickImage', + { + 'source': source.index, + 'maxWidth': maxWidth, + 'maxHeight': maxHeight, + 'imageQuality': imageQuality, + 'cameraDevice': preferredCameraDevice.index + }, + ); + } + + @override + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final String? path = await _getVideoPath( + source: source, + maxDuration: maxDuration, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? PickedFile(path) : null; + } + + Future _getVideoPath({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) { + return _channel.invokeMethod( + 'pickVideo', + { + 'source': source.index, + 'maxDuration': maxDuration?.inSeconds, + 'cameraDevice': preferredCameraDevice.index + }, + ); + } + + @override + Future retrieveLostData() async { + final Map? result = + await _channel.invokeMapMethod('retrieve'); + + if (result == null) { + return LostData.empty(); + } + + assert(result.containsKey('path') != result.containsKey('errorCode')); + + final String? type = result['type'] as String?; + assert(type == kTypeImage || type == kTypeVideo); + + RetrieveType? retrieveType; + if (type == kTypeImage) { + retrieveType = RetrieveType.image; + } else if (type == kTypeVideo) { + retrieveType = RetrieveType.video; + } + + PlatformException? exception; + if (result.containsKey('errorCode')) { + exception = PlatformException( + code: result['errorCode']! as String, + message: result['errorMessage'] as String?); + } + + final String? path = result['path'] as String?; + + return LostData( + file: path != null ? PickedFile(path) : null, + exception: exception, + type: retrieveType, + ); + } + + @override + Future getImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + final String? path = await _getImagePath( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? XFile(path) : null; + } + + @override + Future?> getMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + final List? paths = await _getMultiImagePath( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + ); + if (paths == null) { + return null; + } + + return paths.map((dynamic path) => XFile(path as String)).toList(); + } + + @override + Future getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final String? path = await _getVideoPath( + source: source, + maxDuration: maxDuration, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? XFile(path) : null; + } + + @override + Future getLostData() async { + List? pickedFileList; + + final Map? result = + await _channel.invokeMapMethod('retrieve'); + + if (result == null) { + return LostDataResponse.empty(); + } + + assert(result.containsKey('path') != result.containsKey('errorCode')); + + final String? type = result['type'] as String?; + assert(type == kTypeImage || type == kTypeVideo); + + RetrieveType? retrieveType; + if (type == kTypeImage) { + retrieveType = RetrieveType.image; + } else if (type == kTypeVideo) { + retrieveType = RetrieveType.video; + } + + PlatformException? exception; + if (result.containsKey('errorCode')) { + exception = PlatformException( + code: result['errorCode']! as String, + message: result['errorMessage'] as String?); + } + + final String? path = result['path'] as String?; + + final List? pathList = + (result['pathList'] as List?)?.cast(); + if (pathList != null) { + pickedFileList = []; + for (final String path in pathList) { + pickedFileList.add(XFile(path)); + } + } + + return LostDataResponse( + file: path != null ? XFile(path) : null, + exception: exception, + type: retrieveType, + files: pickedFileList, + ); + } +} diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml index 2587c9a0d15b..5a953ae06979 100755 --- a/packages/image_picker/image_picker_ios/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/pubspec.yaml @@ -6,13 +6,14 @@ version: 0.8.4+11 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.8.0" flutter: plugin: implements: image_picker platforms: ios: + dartPluginClass: ImagePickerIOS pluginClass: FLTImagePickerPlugin dependencies: @@ -24,3 +25,4 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.0.0 + pigeon: ^2.0.3 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 new file mode 100644 index 000000000000..741952415fea --- /dev/null +++ b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart @@ -0,0 +1,973 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_ios/image_picker_ios.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final ImagePickerIOS picker = ImagePickerIOS(); + + final List log = []; + dynamic returnValue = ''; + + setUp(() { + returnValue = ''; + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return returnValue; + }); + + log.clear(); + }); + + group('#pickImage', () { + test('passes the image source argument correctly', () async { + await picker.pickImage(source: ImageSource.camera); + await picker.pickImage(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 1, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.pickImage(source: ImageSource.camera); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxHeight: 10.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.pickImage( + source: ImageSource.camera, + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + ], + ); + }); + + test('does not accept a invalid imageQuality argument', () { + expect( + () => picker.pickImage(imageQuality: -1, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(imageQuality: 101, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(imageQuality: -1, source: ImageSource.camera), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(imageQuality: 101, source: ImageSource.camera), + throwsArgumentError, + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.pickImage(source: ImageSource.camera, maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(source: ImageSource.camera, maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.pickImage(source: ImageSource.gallery), isNull); + expect(await picker.pickImage(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.pickImage(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.pickImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 1, + }), + ], + ); + }); + }); + + group('#pickMultiImage', () { + test('calls the method correctly', () async { + returnValue = ['0', '1']; + await picker.pickMultiImage(); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + returnValue = ['0', '1']; + await picker.pickMultiImage(); + await picker.pickMultiImage( + maxWidth: 10.0, + ); + await picker.pickMultiImage( + maxHeight: 10.0, + ); + await picker.pickMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.pickMultiImage( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.pickMultiImage( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.pickMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + returnValue = ['0', '1']; + expect( + () => picker.pickMultiImage(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.pickMultiImage(maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('does not accept a invalid imageQuality argument', () { + returnValue = ['0', '1']; + expect( + () => picker.pickMultiImage(imageQuality: -1), + throwsArgumentError, + ); + + expect( + () => picker.pickMultiImage(imageQuality: 101), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.pickMultiImage(), isNull); + expect(await picker.pickMultiImage(), isNull); + }); + }); + + group('#pickVideo', () { + test('passes the image source argument correctly', () async { + await picker.pickVideo(source: ImageSource.camera); + await picker.pickVideo(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + isMethodCall('pickVideo', arguments: { + 'source': 1, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('passes the duration argument correctly', () async { + await picker.pickVideo(source: ImageSource.camera); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10), + ); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1), + ); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(hours: 1), + ); + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 10, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 60, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 3600, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('handles a null video path response gracefully', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.pickVideo(source: ImageSource.gallery), isNull); + expect(await picker.pickVideo(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.pickVideo(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.pickVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front, + ); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 1, + }), + ], + ); + }); + }); + + group('#retrieveLostData', () { + test('retrieveLostData get success response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path', + }; + }); + final LostData response = await picker.retrieveLostData(); + expect(response.type, RetrieveType.image); + expect(response.file, isNotNull); + expect(response.file!.path, '/example/path'); + }); + + test('retrieveLostData get error response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + }; + }); + final LostData response = await picker.retrieveLostData(); + expect(response.type, RetrieveType.video); + expect(response.exception, isNotNull); + expect(response.exception!.code, 'test_error_code'); + expect(response.exception!.message, 'test_error_message'); + }); + + test('retrieveLostData get null response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return null; + }); + expect((await picker.retrieveLostData()).isEmpty, true); + }); + + test('retrieveLostData get both path and error should throw', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + 'path': '/example/path', + }; + }); + expect(picker.retrieveLostData(), throwsAssertionError); + }); + }); + + group('#getImage', () { + test('passes the image source argument correctly', () async { + await picker.getImage(source: ImageSource.camera); + await picker.getImage(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 1, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.getImage(source: ImageSource.camera); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxHeight: 10.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.getImage( + source: ImageSource.camera, + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + ], + ); + }); + + test('does not accept a invalid imageQuality argument', () { + expect( + () => picker.getImage(imageQuality: -1, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.getImage(imageQuality: 101, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.getImage(imageQuality: -1, source: ImageSource.camera), + throwsArgumentError, + ); + + expect( + () => picker.getImage(imageQuality: 101, source: ImageSource.camera), + throwsArgumentError, + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.getImage(source: ImageSource.camera, maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.getImage(source: ImageSource.camera, maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getImage(source: ImageSource.gallery), isNull); + expect(await picker.getImage(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.getImage(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 1, + }), + ], + ); + }); + }); + + group('#getMultiImage', () { + test('calls the method correctly', () async { + returnValue = ['0', '1']; + await picker.getMultiImage(); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + returnValue = ['0', '1']; + await picker.getMultiImage(); + await picker.getMultiImage( + maxWidth: 10.0, + ); + await picker.getMultiImage( + maxHeight: 10.0, + ); + await picker.getMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.getMultiImage( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.getMultiImage( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.getMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + returnValue = ['0', '1']; + expect( + () => picker.getMultiImage(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImage(maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('does not accept a invalid imageQuality argument', () { + returnValue = ['0', '1']; + expect( + () => picker.getMultiImage(imageQuality: -1), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImage(imageQuality: 101), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getMultiImage(), isNull); + expect(await picker.getMultiImage(), isNull); + }); + }); + + group('#getVideo', () { + test('passes the image source argument correctly', () async { + await picker.getVideo(source: ImageSource.camera); + await picker.getVideo(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + isMethodCall('pickVideo', arguments: { + 'source': 1, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('passes the duration argument correctly', () async { + await picker.getVideo(source: ImageSource.camera); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10), + ); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1), + ); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(hours: 1), + ); + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 10, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 60, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 3600, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('handles a null video path response gracefully', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getVideo(source: ImageSource.gallery), isNull); + expect(await picker.getVideo(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.getVideo(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front, + ); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 1, + }), + ], + ); + }); + }); + + group('#getLostData', () { + test('getLostData get success response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path', + }; + }); + final LostDataResponse response = await picker.getLostData(); + expect(response.type, RetrieveType.image); + expect(response.file, isNotNull); + expect(response.file!.path, '/example/path'); + }); + + test('getLostData should successfully retrieve multiple files', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path1', + 'pathList': ['/example/path0', '/example/path1'], + }; + }); + final LostDataResponse response = await picker.getLostData(); + expect(response.type, RetrieveType.image); + expect(response.file, isNotNull); + expect(response.file!.path, '/example/path1'); + expect(response.files!.first.path, '/example/path0'); + expect(response.files!.length, 2); + }); + + test('getLostData get error response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + }; + }); + final LostDataResponse response = await picker.getLostData(); + expect(response.type, RetrieveType.video); + expect(response.exception, isNotNull); + expect(response.exception!.code, 'test_error_code'); + expect(response.exception!.message, 'test_error_message'); + }); + + test('getLostData get null response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return null; + }); + expect((await picker.getLostData()).isEmpty, true); + }); + + test('getLostData get both path and error should throw', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + 'path': '/example/path', + }; + }); + expect(picker.getLostData(), throwsAssertionError); + }); + }); +} From 761d926a27bbe8b537836b4d426a22083d95d23f Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 13 Apr 2022 11:25:11 -0400 Subject: [PATCH 02/10] Remove Android-only functions --- .../lib/image_picker_ios.dart | 86 ------------------- 1 file changed, 86 deletions(-) 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 35e5b400004a..ced5616877ec 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 @@ -142,43 +142,6 @@ class ImagePickerIOS extends ImagePickerPlatform { ); } - @override - Future retrieveLostData() async { - final Map? result = - await _channel.invokeMapMethod('retrieve'); - - if (result == null) { - return LostData.empty(); - } - - assert(result.containsKey('path') != result.containsKey('errorCode')); - - final String? type = result['type'] as String?; - assert(type == kTypeImage || type == kTypeVideo); - - RetrieveType? retrieveType; - if (type == kTypeImage) { - retrieveType = RetrieveType.image; - } else if (type == kTypeVideo) { - retrieveType = RetrieveType.video; - } - - PlatformException? exception; - if (result.containsKey('errorCode')) { - exception = PlatformException( - code: result['errorCode']! as String, - message: result['errorMessage'] as String?); - } - - final String? path = result['path'] as String?; - - return LostData( - file: path != null ? PickedFile(path) : null, - exception: exception, - type: retrieveType, - ); - } - @override Future getImage({ required ImageSource source, @@ -228,53 +191,4 @@ class ImagePickerIOS extends ImagePickerPlatform { ); return path != null ? XFile(path) : null; } - - @override - Future getLostData() async { - List? pickedFileList; - - final Map? result = - await _channel.invokeMapMethod('retrieve'); - - if (result == null) { - return LostDataResponse.empty(); - } - - assert(result.containsKey('path') != result.containsKey('errorCode')); - - final String? type = result['type'] as String?; - assert(type == kTypeImage || type == kTypeVideo); - - RetrieveType? retrieveType; - if (type == kTypeImage) { - retrieveType = RetrieveType.image; - } else if (type == kTypeVideo) { - retrieveType = RetrieveType.video; - } - - PlatformException? exception; - if (result.containsKey('errorCode')) { - exception = PlatformException( - code: result['errorCode']! as String, - message: result['errorMessage'] as String?); - } - - final String? path = result['path'] as String?; - - final List? pathList = - (result['pathList'] as List?)?.cast(); - if (pathList != null) { - pickedFileList = []; - for (final String path in pathList) { - pickedFileList.add(XFile(path)); - } - } - - return LostDataResponse( - file: path != null ? XFile(path) : null, - exception: exception, - type: retrieveType, - files: pickedFileList, - ); - } } From 99f75e0daa790accad34d766c75625d11051970f Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 14 Apr 2022 15:29:02 -0400 Subject: [PATCH 03/10] Temporarily point to local pigeon --- packages/image_picker/image_picker_ios/pubspec.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml index 5a953ae06979..6e655cdd9c86 100755 --- a/packages/image_picker/image_picker_ios/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/pubspec.yaml @@ -25,4 +25,6 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.0.0 - pigeon: ^2.0.3 + #pigeon: ^3.0.1 + pigeon: + path: ../../../../packages/packages/pigeon From 99502554a26d6d95e91ee2d75d3c1b2c43b6105b Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 14 Apr 2022 16:35:29 -0400 Subject: [PATCH 04/10] Add Pigeon API, and update Dart to use it --- .../image_picker_ios/ios/Classes/messages.g.h | 54 ++ .../image_picker_ios/ios/Classes/messages.g.m | 209 ++++++ .../lib/image_picker_ios.dart | 87 ++- .../image_picker_ios/lib/src/messages.g.dart | 184 +++++ .../image_picker_ios/pigeons/copyright.txt | 3 + .../image_picker_ios/pigeons/messages.dart | 47 ++ .../test/image_picker_ios_test.dart | 710 +++++++++--------- .../image_picker_ios/test/test_api.dart | 127 ++++ 8 files changed, 1012 insertions(+), 409 deletions(-) create mode 100644 packages/image_picker/image_picker_ios/ios/Classes/messages.g.h create mode 100644 packages/image_picker/image_picker_ios/ios/Classes/messages.g.m create mode 100644 packages/image_picker/image_picker_ios/lib/src/messages.g.dart create mode 100644 packages/image_picker/image_picker_ios/pigeons/copyright.txt create mode 100644 packages/image_picker/image_picker_ios/pigeons/messages.dart create mode 100644 packages/image_picker/image_picker_ios/test/test_api.dart 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 new file mode 100644 index 000000000000..45b12762bc92 --- /dev/null +++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h @@ -0,0 +1,54 @@ +// 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 (v3.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import +@protocol FlutterBinaryMessenger; +@protocol FlutterMessageCodec; +@class FlutterError; +@class FlutterStandardTypedData; + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSUInteger, FLTSourceCamera) { + FLTSourceCameraRear = 0, + FLTSourceCameraFront = 1, +}; + +typedef NS_ENUM(NSUInteger, FLTSourceType) { + FLTSourceTypeCamera = 0, + FLTSourceTypeGallery = 1, +}; + +@class FLTMaxSize; +@class FLTSourceSpecification; + +@interface FLTMaxSize : NSObject ++ (instancetype)makeWithWidth:(nullable NSNumber *)width + height:(nullable NSNumber *)height; +@property(nonatomic, strong, nullable) NSNumber * width; +@property(nonatomic, strong, nullable) NSNumber * height; +@end + +@interface FLTSourceSpecification : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithType:(FLTSourceType)type + camera:(FLTSourceCamera)camera; +@property(nonatomic, assign) FLTSourceType type; +@property(nonatomic, assign) FLTSourceCamera camera; +@end + +/// The codec used by FLTImagePickerApi. +NSObject *FLTImagePickerApiGetCodec(void); + +@protocol FLTImagePickerApi +- (void)pickImageWithSource:(FLTSourceSpecification *)source maxSize:(FLTMaxSize *)maxSize quality:(nullable NSNumber *)imageQuality completion:(void(^)(NSString *_Nullable, FlutterError *_Nullable))completion; +- (void)pickMultiImageWithMaxSize:(FLTMaxSize *)maxSize quality:(nullable NSNumber *)imageQuality completion:(void(^)(NSArray *_Nullable, FlutterError *_Nullable))completion; +- (void)pickVideoWithSource:(FLTSourceSpecification *)source maxDuration:(nullable NSNumber *)maxDurationSeconds completion:(void(^)(NSString *_Nullable, FlutterError *_Nullable))completion; +@end + +extern void FLTImagePickerApiSetup(id binaryMessenger, NSObject *_Nullable api); + +NS_ASSUME_NONNULL_END 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 new file mode 100644 index 000000000000..86d836499e40 --- /dev/null +++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m @@ -0,0 +1,209 @@ +// 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 (v3.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon +#import "messages.g.h" +#import + +#if !__has_feature(objc_arc) +#error File requires ARC to be enabled. +#endif + +static NSDictionary *wrapResult(id result, FlutterError *error) { + NSDictionary *errorDict = (NSDictionary *)[NSNull null]; + if (error) { + errorDict = @{ + @"code": (error.code ? error.code : [NSNull null]), + @"message": (error.message ? error.message : [NSNull null]), + @"details": (error.details ? error.details : [NSNull null]), + }; + } + return @{ + @"result": (result ? result : [NSNull null]), + @"error": errorDict, + }; +} +static id GetNullableObject(NSDictionary* dict, id key) { + id result = dict[key]; + return (result == [NSNull null]) ? nil : result; +} +static id GetNullableObjectAtIndex(NSArray* array, NSInteger key) { + id result = array[key]; + return (result == [NSNull null]) ? nil : result; +} + + +@interface FLTMaxSize () ++ (FLTMaxSize *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end +@interface FLTSourceSpecification () ++ (FLTSourceSpecification *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end + +@implementation FLTMaxSize ++ (instancetype)makeWithWidth:(nullable NSNumber *)width + height:(nullable NSNumber *)height { + FLTMaxSize* pigeonResult = [[FLTMaxSize alloc] init]; + pigeonResult.width = width; + pigeonResult.height = height; + return pigeonResult; +} ++ (FLTMaxSize *)fromMap:(NSDictionary *)dict { + FLTMaxSize *pigeonResult = [[FLTMaxSize alloc] init]; + pigeonResult.width = GetNullableObject(dict, @"width"); + pigeonResult.height = GetNullableObject(dict, @"height"); + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary dictionaryWithObjectsAndKeys:(self.width ? self.width : [NSNull null]), @"width", (self.height ? self.height : [NSNull null]), @"height", nil]; +} +@end + +@implementation FLTSourceSpecification ++ (instancetype)makeWithType:(FLTSourceType)type + camera:(FLTSourceCamera)camera { + FLTSourceSpecification* pigeonResult = [[FLTSourceSpecification alloc] init]; + pigeonResult.type = type; + pigeonResult.camera = camera; + return pigeonResult; +} ++ (FLTSourceSpecification *)fromMap:(NSDictionary *)dict { + FLTSourceSpecification *pigeonResult = [[FLTSourceSpecification alloc] init]; + pigeonResult.type = [GetNullableObject(dict, @"type") integerValue]; + pigeonResult.camera = [GetNullableObject(dict, @"camera") integerValue]; + return pigeonResult; +} +- (NSDictionary *)toMap { + return [NSDictionary dictionaryWithObjectsAndKeys:@(self.type), @"type", @(self.camera), @"camera", nil]; +} +@end + +@interface FLTImagePickerApiCodecReader : FlutterStandardReader +@end +@implementation FLTImagePickerApiCodecReader +- (nullable id)readValueOfType:(UInt8)type +{ + switch (type) { + case 128: + return [FLTMaxSize fromMap:[self readValue]]; + + case 129: + return [FLTSourceSpecification fromMap:[self readValue]]; + + default: + return [super readValueOfType:type]; + + } +} +@end + +@interface FLTImagePickerApiCodecWriter : FlutterStandardWriter +@end +@implementation FLTImagePickerApiCodecWriter +- (void)writeValue:(id)value +{ + if ([value isKindOfClass:[FLTMaxSize class]]) { + [self writeByte:128]; + [self writeValue:[value toMap]]; + } else + if ([value isKindOfClass:[FLTSourceSpecification class]]) { + [self writeByte:129]; + [self writeValue:[value toMap]]; + } else +{ + [super writeValue:value]; + } +} +@end + +@interface FLTImagePickerApiCodecReaderWriter : FlutterStandardReaderWriter +@end +@implementation FLTImagePickerApiCodecReaderWriter +- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { + return [[FLTImagePickerApiCodecWriter alloc] initWithData:data]; +} +- (FlutterStandardReader *)readerWithData:(NSData *)data { + return [[FLTImagePickerApiCodecReader alloc] initWithData:data]; +} +@end + +NSObject *FLTImagePickerApiGetCodec() { + static dispatch_once_t sPred = 0; + static FlutterStandardMessageCodec *sSharedObject = nil; + dispatch_once(&sPred, ^{ + FLTImagePickerApiCodecReaderWriter *readerWriter = [[FLTImagePickerApiCodecReaderWriter alloc] init]; + sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; + }); + return sSharedObject; +} + + +void FLTImagePickerApiSetup(id binaryMessenger, NSObject *api) { + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickImage" + binaryMessenger:binaryMessenger + codec:FLTImagePickerApiGetCodec() ]; + if (api) { + NSCAssert([api respondsToSelector:@selector(pickImageWithSource:maxSize:quality:completion:)], @"FLTImagePickerApi api (%@) doesn't respond to @selector(pickImageWithSource:maxSize:quality:completion:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTSourceSpecification *arg_source = GetNullableObjectAtIndex(args, 0); + FLTMaxSize *arg_maxSize = GetNullableObjectAtIndex(args, 1); + NSNumber *arg_imageQuality = GetNullableObjectAtIndex(args, 2); + [api pickImageWithSource:arg_source maxSize:arg_maxSize quality:arg_imageQuality completion:^(NSString *_Nullable output, FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } + else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickMultiImage" + binaryMessenger:binaryMessenger + codec:FLTImagePickerApiGetCodec() ]; + if (api) { + NSCAssert([api respondsToSelector:@selector(pickMultiImageWithMaxSize:quality:completion:)], @"FLTImagePickerApi api (%@) doesn't respond to @selector(pickMultiImageWithMaxSize:quality:completion:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTMaxSize *arg_maxSize = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_imageQuality = GetNullableObjectAtIndex(args, 1); + [api pickMultiImageWithMaxSize:arg_maxSize quality:arg_imageQuality completion:^(NSArray *_Nullable output, FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } + else { + [channel setMessageHandler:nil]; + } + } + { + FlutterBasicMessageChannel *channel = + [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickVideo" + binaryMessenger:binaryMessenger + codec:FLTImagePickerApiGetCodec() ]; + if (api) { + NSCAssert([api respondsToSelector:@selector(pickVideoWithSource:maxDuration:completion:)], @"FLTImagePickerApi api (%@) doesn't respond to @selector(pickVideoWithSource:maxDuration:completion:)", api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTSourceSpecification *arg_source = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_maxDurationSeconds = GetNullableObjectAtIndex(args, 1); + [api pickVideoWithSource:arg_source maxDuration:arg_maxDurationSeconds completion:^(NSString *_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 ced5616877ec..9d503a6e810c 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 @@ -4,18 +4,42 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; -const MethodChannel _channel = MethodChannel('plugins.flutter.io/image_picker'); +import 'src/messages.g.dart'; + +// Converts an [ImageSource] to the corresponding Pigeon API enum value. +SourceType _convertSource(ImageSource source) { + switch (source) { + case ImageSource.camera: + return SourceType.camera; + case ImageSource.gallery: + return SourceType.gallery; + default: + throw UnimplementedError('Unknown source: $source'); + } +} + +// Converts a [CameraDevice] to the corresponding Pigeon API enum value. +SourceCamera _convertCamera(CameraDevice camera) { + switch (camera) { + case CameraDevice.front: + return SourceCamera.front; + case CameraDevice.rear: + return SourceCamera.rear; + default: + throw UnimplementedError('Unknown camera: $camera'); + } +} /// An implementation of [ImagePickerPlatform] for iOS. class ImagePickerIOS extends ImagePickerPlatform { - /// The MethodChannel that is being used by this implementation of the plugin. - @visibleForTesting - MethodChannel get channel => _channel; + final ImagePickerApi _hostApi = ImagePickerApi(); + + /// Registers this class as the default platform implementation. + static void registerWith() { + ImagePickerPlatform.instance = ImagePickerIOS(); + } @override Future pickImage({ @@ -53,11 +77,11 @@ class ImagePickerIOS extends ImagePickerPlatform { return paths.map((dynamic path) => PickedFile(path as String)).toList(); } - Future?> _getMultiImagePath({ + Future?> _getMultiImagePath({ double? maxWidth, double? maxHeight, int? imageQuality, - }) { + }) async { if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { throw ArgumentError.value( imageQuality, 'imageQuality', 'must be between 0 and 100'); @@ -71,14 +95,11 @@ class ImagePickerIOS extends ImagePickerPlatform { throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); } - return _channel.invokeMethod?>( - 'pickMultiImage', - { - 'maxWidth': maxWidth, - 'maxHeight': maxHeight, - 'imageQuality': imageQuality, - }, - ); + // TODO(stuartmorgan): Remove the cast once Pigeon supports non-nullable + // generics, https://github.com/flutter/flutter/issues/97848 + return (await _hostApi.pickMultiImage( + MaxSize(width: maxWidth, height: maxHeight), imageQuality)) + ?.cast(); } Future _getImagePath({ @@ -101,15 +122,12 @@ class ImagePickerIOS extends ImagePickerPlatform { throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); } - return _channel.invokeMethod( - 'pickImage', - { - 'source': source.index, - 'maxWidth': maxWidth, - 'maxHeight': maxHeight, - 'imageQuality': imageQuality, - 'cameraDevice': preferredCameraDevice.index - }, + return _hostApi.pickImage( + SourceSpecification( + type: _convertSource(source), + camera: _convertCamera(preferredCameraDevice)), + MaxSize(width: maxWidth, height: maxHeight), + imageQuality, ); } @@ -132,14 +150,11 @@ class ImagePickerIOS extends ImagePickerPlatform { CameraDevice preferredCameraDevice = CameraDevice.rear, Duration? maxDuration, }) { - return _channel.invokeMethod( - 'pickVideo', - { - 'source': source.index, - 'maxDuration': maxDuration?.inSeconds, - 'cameraDevice': preferredCameraDevice.index - }, - ); + return _hostApi.pickVideo( + SourceSpecification( + type: _convertSource(source), + camera: _convertCamera(preferredCameraDevice)), + maxDuration?.inSeconds); } @override @@ -166,7 +181,7 @@ class ImagePickerIOS extends ImagePickerPlatform { double? maxHeight, int? imageQuality, }) async { - final List? paths = await _getMultiImagePath( + final List? paths = await _getMultiImagePath( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, @@ -175,7 +190,7 @@ class ImagePickerIOS extends ImagePickerPlatform { return null; } - return paths.map((dynamic path) => XFile(path as String)).toList(); + return paths.map((String path) => XFile(path)).toList(); } @override 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 new file mode 100644 index 000000000000..5e2bc8048bdb --- /dev/null +++ b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart @@ -0,0 +1,184 @@ +// 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 (v3.0.1), 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 +// @dart = 2.12 +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; + +import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; + +enum SourceCamera { + rear, + front, +} + +enum SourceType { + camera, + gallery, +} + +class MaxSize { + MaxSize({ + this.width, + this.height, + }); + + double? width; + double? height; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['width'] = width; + pigeonMap['height'] = height; + return pigeonMap; + } + + static MaxSize decode(Object message) { + final Map pigeonMap = message as Map; + return MaxSize( + width: pigeonMap['width'] as double?, + height: pigeonMap['height'] as double?, + ); + } +} + +class SourceSpecification { + SourceSpecification({ + required this.type, + this.camera, + }); + + SourceType type; + SourceCamera? camera; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['type'] = type.index; + pigeonMap['camera'] = camera?.index; + return pigeonMap; + } + + static SourceSpecification decode(Object message) { + final Map pigeonMap = message as Map; + return SourceSpecification( + type: SourceType.values[pigeonMap['type']! as int] +, + camera: pigeonMap['camera'] != null + ? SourceCamera.values[pigeonMap['camera']! as int] + : null, + ); + } +} + +class _ImagePickerApiCodec extends StandardMessageCodec { + const _ImagePickerApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is MaxSize) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else + if (value is SourceSpecification) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else +{ + super.writeValue(buffer, value); + } + } + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return MaxSize.decode(readValue(buffer)!); + + case 129: + return SourceSpecification.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + + } + } +} + +class ImagePickerApi { + /// Constructor for [ImagePickerApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + ImagePickerApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _ImagePickerApiCodec(); + + Future pickImage(SourceSpecification arg_source, MaxSize arg_maxSize, int? arg_imageQuality) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickImage', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_source, arg_maxSize, arg_imageQuality]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as String?); + } + } + + Future?> pickMultiImage(MaxSize arg_maxSize, int? arg_imageQuality) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMultiImage', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_maxSize, arg_imageQuality]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as List?)?.cast(); + } + } + + Future pickVideo(SourceSpecification arg_source, int? arg_maxDurationSeconds) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickVideo', codec, binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_source, arg_maxDurationSeconds]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as String?); + } + } +} diff --git a/packages/image_picker/image_picker_ios/pigeons/copyright.txt b/packages/image_picker/image_picker_ios/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/image_picker/image_picker_ios/pigeons/copyright.txt @@ -0,0 +1,3 @@ +Copyright 2013 The Flutter Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. diff --git a/packages/image_picker/image_picker_ios/pigeons/messages.dart b/packages/image_picker/image_picker_ios/pigeons/messages.dart new file mode 100644 index 000000000000..94ac034606e9 --- /dev/null +++ b/packages/image_picker/image_picker_ios/pigeons/messages.dart @@ -0,0 +1,47 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + dartTestOut: 'test/test_api.dart', + objcHeaderOut: 'ios/Classes/messages.g.h', + objcSourceOut: 'ios/Classes/messages.g.m', + objcOptions: ObjcOptions( + prefix: 'FLT', + ), + copyrightHeader: 'pigeons/copyright.txt', +)) +class MaxSize { + MaxSize(this.width, this.height); + double? width; + double? height; +} + +// Corresponds to `CameraDevice` from the platform interface package. +enum SourceCamera { rear, front } + +// Corresponds to `ImageSource` from the platform interface package. +enum SourceType { camera, gallery } + +class SourceSpecification { + SourceSpecification(this.type, this.camera); + SourceType type; + SourceCamera? camera; +} + +@HostApi(dartHostTestHandler: 'TestHostImagePickerApi') +abstract class ImagePickerApi { + @async + @ObjCSelector('pickImageWithSource:maxSize:quality:') + String? pickImage( + SourceSpecification source, MaxSize maxSize, int? imageQuality); + @async + @ObjCSelector('pickMultiImageWithMaxSize:quality:') + List? pickMultiImage(MaxSize maxSize, int? imageQuality); + @async + @ObjCSelector('pickVideoWithSource:maxDuration:') + String? pickVideo(SourceSpecification source, int? maxDurationSeconds); +} 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 741952415fea..09517f1ef96b 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 @@ -2,27 +2,92 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_ios/image_picker_ios.dart'; +import 'package:image_picker_ios/src/messages.g.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'test_api.dart'; + +@immutable +class _LoggedMethodCall { + const _LoggedMethodCall(this.name, {required this.arguments}); + final String name; + final Map arguments; + + @override + bool operator ==(Object other) { + return other is _LoggedMethodCall && + name == other.name && + mapEquals(arguments, other.arguments); + } + + @override + int get hashCode => Object.hash(name, arguments); + + @override + String toString() { + return 'MethodCall: $name $arguments'; + } +} + +class _ApiLogger implements TestHostImagePickerApi { + // The value to return from future calls. + dynamic returnValue = ''; + final List<_LoggedMethodCall> calls = <_LoggedMethodCall>[]; + + @override + Future pickImage( + SourceSpecification source, MaxSize maxSize, int? imageQuality) async { + // Flatten arguments for easy comparison. + calls.add(_LoggedMethodCall('pickImage', arguments: { + 'source': source.type, + 'cameraDevice': source.camera, + 'maxWidth': maxSize.width, + 'maxHeight': maxSize.height, + 'imageQuality': imageQuality, + })); + return returnValue as String?; + } + + @override + Future?> pickMultiImage( + MaxSize maxSize, int? imageQuality) async { + calls.add(_LoggedMethodCall('pickMultiImage', arguments: { + 'maxWidth': maxSize.width, + 'maxHeight': maxSize.height, + 'imageQuality': imageQuality, + })); + return returnValue as List?; + } + + @override + Future pickVideo( + SourceSpecification source, int? maxDurationSeconds) async { + calls.add(_LoggedMethodCall('pickVideo', arguments: { + 'source': source.type, + 'cameraDevice': source.camera, + 'maxDuration': maxDurationSeconds, + })); + return returnValue as String?; + } +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); final ImagePickerIOS picker = ImagePickerIOS(); - - final List log = []; - dynamic returnValue = ''; + late _ApiLogger log; setUp(() { - returnValue = ''; - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return returnValue; - }); + log = _ApiLogger(); + TestHostImagePickerApi.setup(log); + }); - log.clear(); + test('registration', () async { + ImagePickerIOS.registerWith(); + expect(ImagePickerPlatform.instance, isA()); }); group('#pickImage', () { @@ -31,21 +96,21 @@ void main() { await picker.pickImage(source: ImageSource.gallery); expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), - isMethodCall('pickImage', arguments: { - 'source': 1, + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.gallery, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), ], ); @@ -84,56 +149,56 @@ void main() { ); expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), - isMethodCall('pickImage', arguments: { - 'source': 0, + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), - isMethodCall('pickImage', arguments: { - 'source': 0, + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), - isMethodCall('pickImage', arguments: { - 'source': 0, + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), - isMethodCall('pickImage', arguments: { - 'source': 0, + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), - isMethodCall('pickImage', arguments: { - 'source': 0, + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), - isMethodCall('pickImage', arguments: { - 'source': 0, + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), ], ); @@ -174,7 +239,7 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + log.returnValue = null; expect(await picker.pickImage(source: ImageSource.gallery), isNull); expect(await picker.pickImage(source: ImageSource.camera), isNull); @@ -184,14 +249,14 @@ void main() { await picker.pickImage(source: ImageSource.camera); expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0, + 'cameraDevice': SourceCamera.rear, }), ], ); @@ -203,14 +268,14 @@ void main() { preferredCameraDevice: CameraDevice.front); expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 1, + 'cameraDevice': SourceCamera.front, }), ], ); @@ -219,23 +284,24 @@ void main() { group('#pickMultiImage', () { test('calls the method correctly', () async { - returnValue = ['0', '1']; + log.returnValue = ['0', '1']; await picker.pickMultiImage(); expect( - log, - [ - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - }), + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), ], ); }); test('passes the width and height arguments correctly', () async { - returnValue = ['0', '1']; + log.returnValue = ['0', '1']; await picker.pickMultiImage(); await picker.pickMultiImage( maxWidth: 10.0, @@ -262,49 +328,55 @@ void main() { ); expect( - log, - [ - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': null, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': null, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': null, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': 70, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': 70, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': 70, - }), + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + }), ], ); }); test('does not accept a negative width or height argument', () { - returnValue = ['0', '1']; expect( () => picker.pickMultiImage(maxWidth: -1.0), throwsArgumentError, @@ -317,7 +389,6 @@ void main() { }); test('does not accept a invalid imageQuality argument', () { - returnValue = ['0', '1']; expect( () => picker.pickMultiImage(imageQuality: -1), throwsArgumentError, @@ -330,10 +401,9 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + log.returnValue = null; expect(await picker.pickMultiImage(), isNull); - expect(await picker.pickMultiImage(), isNull); }); }); @@ -343,16 +413,16 @@ void main() { await picker.pickVideo(source: ImageSource.gallery); expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'cameraDevice': SourceCamera.rear, 'maxDuration': null, }), - isMethodCall('pickVideo', arguments: { - 'source': 1, - 'cameraDevice': 0, + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.gallery, + 'cameraDevice': SourceCamera.rear, 'maxDuration': null, }), ], @@ -374,34 +444,34 @@ void main() { maxDuration: const Duration(hours: 1), ); expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, 'maxDuration': null, - 'cameraDevice': 0, + 'cameraDevice': SourceCamera.rear, }), - isMethodCall('pickVideo', arguments: { - 'source': 0, + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, 'maxDuration': 10, - 'cameraDevice': 0, + 'cameraDevice': SourceCamera.rear, }), - isMethodCall('pickVideo', arguments: { - 'source': 0, + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, 'maxDuration': 60, - 'cameraDevice': 0, + 'cameraDevice': SourceCamera.rear, }), - isMethodCall('pickVideo', arguments: { - 'source': 0, + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, 'maxDuration': 3600, - 'cameraDevice': 0, + 'cameraDevice': SourceCamera.rear, }), ], ); }); test('handles a null video path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + log.returnValue = null; expect(await picker.pickVideo(source: ImageSource.gallery), isNull); expect(await picker.pickVideo(source: ImageSource.camera), isNull); @@ -411,11 +481,11 @@ void main() { await picker.pickVideo(source: ImageSource.camera); expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'cameraDevice': SourceCamera.rear, 'maxDuration': null, }), ], @@ -429,88 +499,39 @@ void main() { ); expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, 'maxDuration': null, - 'cameraDevice': 1, + 'cameraDevice': SourceCamera.front, }), ], ); }); }); - group('#retrieveLostData', () { - test('retrieveLostData get success response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'image', - 'path': '/example/path', - }; - }); - final LostData response = await picker.retrieveLostData(); - expect(response.type, RetrieveType.image); - expect(response.file, isNotNull); - expect(response.file!.path, '/example/path'); - }); - - test('retrieveLostData get error response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - }; - }); - final LostData response = await picker.retrieveLostData(); - expect(response.type, RetrieveType.video); - expect(response.exception, isNotNull); - expect(response.exception!.code, 'test_error_code'); - expect(response.exception!.message, 'test_error_message'); - }); - - test('retrieveLostData get null response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return null; - }); - expect((await picker.retrieveLostData()).isEmpty, true); - }); - - test('retrieveLostData get both path and error should throw', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - 'path': '/example/path', - }; - }); - expect(picker.retrieveLostData(), throwsAssertionError); - }); - }); - group('#getImage', () { test('passes the image source argument correctly', () async { await picker.getImage(source: ImageSource.camera); await picker.getImage(source: ImageSource.gallery); expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), - isMethodCall('pickImage', arguments: { - 'source': 1, + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.gallery, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), ], ); @@ -549,56 +570,56 @@ void main() { ); expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), - isMethodCall('pickImage', arguments: { - 'source': 0, + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), - isMethodCall('pickImage', arguments: { - 'source': 0, + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), - isMethodCall('pickImage', arguments: { - 'source': 0, + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': null, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), - isMethodCall('pickImage', arguments: { - 'source': 0, + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': 10.0, 'maxHeight': null, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), - isMethodCall('pickImage', arguments: { - 'source': 0, + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': null, 'maxHeight': 10.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), - isMethodCall('pickImage', arguments: { - 'source': 0, + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': 10.0, 'maxHeight': 20.0, 'imageQuality': 70, - 'cameraDevice': 0 + 'cameraDevice': SourceCamera.rear }), ], ); @@ -639,7 +660,7 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + log.returnValue = null; expect(await picker.getImage(source: ImageSource.gallery), isNull); expect(await picker.getImage(source: ImageSource.camera), isNull); @@ -649,14 +670,14 @@ void main() { await picker.getImage(source: ImageSource.camera); expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 0, + 'cameraDevice': SourceCamera.rear, }), ], ); @@ -668,14 +689,14 @@ void main() { preferredCameraDevice: CameraDevice.front); expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickImage', arguments: { + 'source': SourceType.camera, 'maxWidth': null, 'maxHeight': null, 'imageQuality': null, - 'cameraDevice': 1, + 'cameraDevice': SourceCamera.front, }), ], ); @@ -684,23 +705,24 @@ void main() { group('#getMultiImage', () { test('calls the method correctly', () async { - returnValue = ['0', '1']; + log.returnValue = ['0', '1']; await picker.getMultiImage(); expect( - log, - [ - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - }), + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), ], ); }); test('passes the width and height arguments correctly', () async { - returnValue = ['0', '1']; + log.returnValue = ['0', '1']; await picker.getMultiImage(); await picker.getMultiImage( maxWidth: 10.0, @@ -727,49 +749,56 @@ void main() { ); expect( - log, - [ - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': null, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': null, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': null, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': 70, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': 70, - }), - isMethodCall('pickMultiImage', arguments: { - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': 70, - }), + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + }), + const _LoggedMethodCall('pickMultiImage', + arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + }), ], ); }); test('does not accept a negative width or height argument', () { - returnValue = ['0', '1']; + log.returnValue = ['0', '1']; expect( () => picker.getMultiImage(maxWidth: -1.0), throwsArgumentError, @@ -782,7 +811,7 @@ void main() { }); test('does not accept a invalid imageQuality argument', () { - returnValue = ['0', '1']; + log.returnValue = ['0', '1']; expect( () => picker.getMultiImage(imageQuality: -1), throwsArgumentError, @@ -795,7 +824,7 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + log.returnValue = null; expect(await picker.getMultiImage(), isNull); expect(await picker.getMultiImage(), isNull); @@ -808,16 +837,16 @@ void main() { await picker.getVideo(source: ImageSource.gallery); expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'cameraDevice': SourceCamera.rear, 'maxDuration': null, }), - isMethodCall('pickVideo', arguments: { - 'source': 1, - 'cameraDevice': 0, + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.gallery, + 'cameraDevice': SourceCamera.rear, 'maxDuration': null, }), ], @@ -839,34 +868,34 @@ void main() { maxDuration: const Duration(hours: 1), ); expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, 'maxDuration': null, - 'cameraDevice': 0, + 'cameraDevice': SourceCamera.rear, }), - isMethodCall('pickVideo', arguments: { - 'source': 0, + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, 'maxDuration': 10, - 'cameraDevice': 0, + 'cameraDevice': SourceCamera.rear, }), - isMethodCall('pickVideo', arguments: { - 'source': 0, + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, 'maxDuration': 60, - 'cameraDevice': 0, + 'cameraDevice': SourceCamera.rear, }), - isMethodCall('pickVideo', arguments: { - 'source': 0, + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, 'maxDuration': 3600, - 'cameraDevice': 0, + 'cameraDevice': SourceCamera.rear, }), ], ); }); test('handles a null video path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + log.returnValue = null; expect(await picker.getVideo(source: ImageSource.gallery), isNull); expect(await picker.getVideo(source: ImageSource.camera), isNull); @@ -876,11 +905,11 @@ void main() { await picker.getVideo(source: ImageSource.camera); expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, + 'cameraDevice': SourceCamera.rear, 'maxDuration': null, }), ], @@ -894,80 +923,15 @@ void main() { ); expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickVideo', arguments: { + 'source': SourceType.camera, 'maxDuration': null, - 'cameraDevice': 1, + 'cameraDevice': SourceCamera.front, }), ], ); }); }); - - group('#getLostData', () { - test('getLostData get success response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'image', - 'path': '/example/path', - }; - }); - final LostDataResponse response = await picker.getLostData(); - expect(response.type, RetrieveType.image); - expect(response.file, isNotNull); - expect(response.file!.path, '/example/path'); - }); - - test('getLostData should successfully retrieve multiple files', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'image', - 'path': '/example/path1', - 'pathList': ['/example/path0', '/example/path1'], - }; - }); - final LostDataResponse response = await picker.getLostData(); - expect(response.type, RetrieveType.image); - expect(response.file, isNotNull); - expect(response.file!.path, '/example/path1'); - expect(response.files!.first.path, '/example/path0'); - expect(response.files!.length, 2); - }); - - test('getLostData get error response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - }; - }); - final LostDataResponse response = await picker.getLostData(); - expect(response.type, RetrieveType.video); - expect(response.exception, isNotNull); - expect(response.exception!.code, 'test_error_code'); - expect(response.exception!.message, 'test_error_message'); - }); - - test('getLostData get null response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return null; - }); - expect((await picker.getLostData()).isEmpty, true); - }); - - test('getLostData get both path and error should throw', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - 'path': '/example/path', - }; - }); - expect(picker.getLostData(), throwsAssertionError); - }); - }); } diff --git a/packages/image_picker/image_picker_ios/test/test_api.dart b/packages/image_picker/image_picker_ios/test/test_api.dart new file mode 100644 index 000000000000..17ca538e8008 --- /dev/null +++ b/packages/image_picker/image_picker_ios/test/test_api.dart @@ -0,0 +1,127 @@ +// 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 (v3.0.1), 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 +// ignore_for_file: avoid_relative_lib_imports +// @dart = 2.12 +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; +import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +// Manually changed due to https://github.com/flutter/flutter/issues/97744 +import 'package:image_picker_ios/src/messages.g.dart'; + +class _TestHostImagePickerApiCodec extends StandardMessageCodec { + const _TestHostImagePickerApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is MaxSize) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return MaxSize.decode(readValue(buffer)!); + + case 129: + return SourceSpecification.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestHostImagePickerApi { + static const MessageCodec codec = _TestHostImagePickerApiCodec(); + + Future pickImage( + SourceSpecification source, MaxSize maxSize, int? imageQuality); + Future?> pickMultiImage(MaxSize maxSize, int? imageQuality); + Future pickVideo( + SourceSpecification source, int? maxDurationSeconds); + static void setup(TestHostImagePickerApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickImage', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImage was null.'); + final List args = (message as List?)!; + final SourceSpecification? arg_source = + (args[0] as SourceSpecification?); + assert(arg_source != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImage was null, expected non-null SourceSpecification.'); + final MaxSize? arg_maxSize = (args[1] as MaxSize?); + assert(arg_maxSize != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImage was null, expected non-null MaxSize.'); + final int? arg_imageQuality = (args[2] as int?); + final String? output = + await api.pickImage(arg_source!, arg_maxSize!, arg_imageQuality); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMultiImage', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMultiImage was null.'); + final List args = (message as List?)!; + final MaxSize? arg_maxSize = (args[0] as MaxSize?); + assert(arg_maxSize != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMultiImage was null, expected non-null MaxSize.'); + final int? arg_imageQuality = (args[1] as int?); + final List? output = + await api.pickMultiImage(arg_maxSize!, arg_imageQuality); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickVideo', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideo was null.'); + final List args = (message as List?)!; + final SourceSpecification? arg_source = + (args[0] as SourceSpecification?); + assert(arg_source != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideo was null, expected non-null SourceSpecification.'); + final int? arg_maxDurationSeconds = (args[1] as int?); + final String? output = + await api.pickVideo(arg_source!, arg_maxDurationSeconds); + return {'result': output}; + }); + } + } + } +} From ebb4d848fb52676e35672022824e3210f09a7312 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 15 Apr 2022 15:58:21 -0400 Subject: [PATCH 05/10] Update ObjC implementation to use Pigeon --- .../ios/RunnerTests/ImagePickerPluginTests.m | 140 +++--- .../ios/Classes/FLTImagePickerPlugin.m | 399 ++++++++++-------- .../ios/Classes/FLTImagePickerPlugin_Test.h | 67 ++- 3 files changed, 323 insertions(+), 283 deletions(-) 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 8df5299e54d9..13fbc5278871 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 @@ -47,14 +47,15 @@ - (void)testPluginPickImageDeviceBack { // Run test FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickImage" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; [plugin setImagePickerControllerOverrides:@[ controller ]]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + + [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraRear] + maxSize:[[FLTMaxSize alloc] init] + quality:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceRear); } @@ -78,14 +79,15 @@ - (void)testPluginPickImageDeviceFront { // Run test FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickImage" - arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; [plugin setImagePickerControllerOverrides:@[ controller ]]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + + [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraFront] + maxSize:[[FLTMaxSize alloc] init] + quality:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceFront); } @@ -109,14 +111,14 @@ - (void)testPluginPickVideoDeviceBack { // Run test FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickVideo" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; [plugin setImagePickerControllerOverrides:@[ controller ]]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + + [plugin pickVideoWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraRear] + maxDuration:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceRear); } @@ -141,14 +143,14 @@ - (void)testPluginPickVideoDeviceFront { // Run test FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickVideo" - arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; [plugin setImagePickerControllerOverrides:@[ controller ]]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + + [plugin pickVideoWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraFront] + maxDuration:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; XCTAssertEqual(controller.cameraDevice, UIImagePickerControllerCameraDeviceFront); } @@ -165,17 +167,12 @@ - (void)testPickMultiImageShouldUseUIImagePickerControllerOnPreiOS14 { FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; [plugin setImagePickerControllerOverrides:@[ mockUIImagePicker ]]; - FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"pickMultiImage" - arguments:@{ - @"maxWidth" : @(100), - @"maxHeight" : @(200), - @"imageQuality" : @(50), - }]; - - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + [plugin pickMultiImageWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] + quality:@(50) + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; OCMVerify(times(1), [mockUIImagePicker setSourceType:UIImagePickerControllerSourceTypePhotoLibrary]); } @@ -187,17 +184,15 @@ - (void)testPluginPickImageDeviceCancelClickMultipleTimes { return; } FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickImage" - arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; plugin.imagePickerControllerOverrides = @[ controller ]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; - plugin.result = ^(id result) { - }; + [plugin pickImageWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraRear] + maxSize:[[FLTMaxSize alloc] init] + quality:nil + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; // To ensure the flow does not crash by multiple cancel call [plugin imagePickerControllerDidCancel:controller]; @@ -208,14 +203,15 @@ - (void)testPluginPickImageDeviceCancelClickMultipleTimes { - (void)testPickingVideoWithDuration { FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - FlutterMethodCall *call = [FlutterMethodCall - methodCallWithMethodName:@"pickVideo" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0), @"maxDuration" : @95}]; UIImagePickerController *controller = [[UIImagePickerController alloc] init]; [plugin setImagePickerControllerOverrides:@[ controller ]]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; + + [plugin pickVideoWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeCamera + camera:FLTSourceCameraRear] + maxDuration:@(95) + completion:^(NSString *_Nullable result, FlutterError *_Nullable error){ + }]; + XCTAssertEqual(controller.videoMaximumDuration, 95); } @@ -231,37 +227,17 @@ - (void)testViewController { XCTAssertEqual([plugin viewControllerWithWindow:window], vc2); } -- (void)testPluginMultiImagePathIsNil { - FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - - dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); - __block FlutterError *pickImageResult = nil; - - plugin.result = ^(id _Nullable r) { - pickImageResult = r; - dispatch_semaphore_signal(resultSemaphore); - }; - [plugin handleSavedPathList:nil]; - - dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); - - XCTAssertEqualObjects(pickImageResult.code, @"create_error"); -} - - (void)testPluginMultiImagePathHasNullItem { FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - NSMutableArray *pathList = [NSMutableArray new]; - - [pathList addObject:[NSNull null]]; dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); __block FlutterError *pickImageResult = nil; - - plugin.result = ^(id _Nullable r) { - pickImageResult = r; - dispatch_semaphore_signal(resultSemaphore); - }; - [plugin handleSavedPathList:pathList]; + plugin.callContext = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^(NSArray *_Nullable result, FlutterError *_Nullable error) { + pickImageResult = error; + dispatch_semaphore_signal(resultSemaphore); + }]; + [plugin returnSavedPathList:@[ [NSNull null] ]]; dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); @@ -270,19 +246,17 @@ - (void)testPluginMultiImagePathHasNullItem { - (void)testPluginMultiImagePathHasItem { FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; - NSString *savedPath = @"test"; - NSMutableArray *pathList = [NSMutableArray new]; - - [pathList addObject:savedPath]; + NSArray *pathList = @[ @"test" ]; dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); __block id pickImageResult = nil; - plugin.result = ^(id _Nullable r) { - pickImageResult = r; - dispatch_semaphore_signal(resultSemaphore); - }; - [plugin handleSavedPathList:pathList]; + plugin.callContext = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^(NSArray *_Nullable result, FlutterError *_Nullable error) { + pickImageResult = result; + dispatch_semaphore_signal(resultSemaphore); + }]; + [plugin returnSavedPathList:pathList]; dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); 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 cc841d6db447..15a494921616 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m @@ -16,31 +16,24 @@ #import "FLTImagePickerMetaDataUtil.h" #import "FLTImagePickerPhotoAssetUtil.h" #import "FLTPHPickerSaveImageToPathOperation.h" +#import "messages.g.h" -/** - * Returns the value for the given key in 'dict', or nil if the value is - * NSNull. - */ -id GetNullableValueForKey(NSDictionary *dict, NSString *key) { - id value = dict[key]; - return value == [NSNull null] ? nil : value; +@implementation FLTImagePickerMethodCallContext +- (instancetype)initWithResult:(nonnull FlutterResultAdapter)result { + if (self = [super init]) { + _result = [result copy]; + } + return self; } +@end + +#pragma mark - @interface FLTImagePickerPlugin () -/** - * The maximum amount of images that are allowed to be picked. - */ -@property(assign, nonatomic) int maxImagesAllowed; - -/** - * The arguments that are passed in from the Flutter method call. - */ -@property(copy, nonatomic) NSDictionary *arguments; - /** * The PHPickerViewController instance used to pick multiple * images. @@ -58,19 +51,13 @@ @interface FLTImagePickerPlugin () *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/image_picker" - binaryMessenger:[registrar messenger]]; FLTImagePickerPlugin *instance = [FLTImagePickerPlugin new]; - [registrar addMethodCallDelegate:instance channel:channel]; + FLTImagePickerApiSetup(registrar.messenger, instance); } - (UIImagePickerController *)createImagePickerController { @@ -107,130 +94,180 @@ - (UIViewController *)viewControllerWithWindow:(UIWindow *)window { } /** - * Returns the UIImagePickerControllerCameraDevice to use given [arguments]. - * - * If the cameraDevice value that is fetched from arguments is 1 then returns - * UIImagePickerControllerCameraDeviceFront. If the cameraDevice value that is fetched - * from arguments is 0 then returns UIImagePickerControllerCameraDeviceRear. + * Returns the UIImagePickerControllerCameraDevice to use given [source]. * - * @param arguments that should be used to get cameraDevice value. + * @param source The source specification from Dart. */ -- (UIImagePickerControllerCameraDevice)getCameraDeviceFromArguments:(NSDictionary *)arguments { - NSInteger cameraDevice = [arguments[@"cameraDevice"] intValue]; - return (cameraDevice == 1) ? UIImagePickerControllerCameraDeviceFront - : UIImagePickerControllerCameraDeviceRear; +- (UIImagePickerControllerCameraDevice)cameraDeviceForSource:(FLTSourceSpecification *)source { + switch (source.camera) { + case FLTSourceCameraFront: + return UIImagePickerControllerCameraDeviceFront; + case FLTSourceCameraRear: + return UIImagePickerControllerCameraDeviceRear; + } } -- (void)pickImageWithPHPicker:(int)maxImagesAllowed API_AVAILABLE(ios(14)) { +- (void)launchPHPickerWithContext:(nonnull FLTImagePickerMethodCallContext *)context + API_AVAILABLE(ios(14)) { PHPickerConfiguration *config = [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary]; - config.selectionLimit = maxImagesAllowed; // Setting to zero allow us to pick unlimited photos + config.selectionLimit = context.maxImageCount; config.filter = [PHPickerFilter imagesFilter]; _pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config]; _pickerViewController.delegate = self; _pickerViewController.presentationController.delegate = self; - - self.maxImagesAllowed = maxImagesAllowed; + self.callContext = context; [self checkPhotoAuthorizationForAccessLevel]; } -- (void)launchUIImagePickerWithSource:(int)imageSource { +- (void)launchUIImagePickerWithSource:(nonnull FLTSourceSpecification *)source + context:(nonnull FLTImagePickerMethodCallContext *)context { UIImagePickerController *imagePickerController = [self createImagePickerController]; imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; imagePickerController.delegate = self; imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; + self.callContext = context; - self.maxImagesAllowed = 1; - - switch (imageSource) { - case SOURCE_CAMERA: - [self checkCameraAuthorizationWithImagePicker:imagePickerController]; + switch (source.type) { + case FLTSourceTypeCamera: + [self checkCameraAuthorizationWithImagePicker:imagePickerController + camera:[self cameraDeviceForSource:source]]; break; - case SOURCE_GALLERY: + case FLTSourceTypeGallery: [self checkPhotoAuthorizationWithImagePicker:imagePickerController]; break; default: - self.result([FlutterError errorWithCode:@"invalid_source" - message:@"Invalid image source." - details:nil]); + [self returnError:[FlutterError errorWithCode:@"invalid_source" + message:@"Invalid image source." + details:nil]]; break; } } -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if (self.result) { - self.result([FlutterError errorWithCode:@"multiple_request" - message:@"Cancelled by a second request" - details:nil]); - self.result = nil; - } - - self.result = result; - _arguments = call.arguments; - - if ([@"pickImage" isEqualToString:call.method]) { - int imageSource = [call.arguments[@"source"] intValue]; +#pragma mark - FLTImagePickerApi + +- (void)pickImageWithSource:(nonnull FLTSourceSpecification *)source + maxSize:(nonnull FLTMaxSize *)maxSize + quality:(nullable NSNumber *)imageQuality + completion: + (nonnull void (^)(NSString *_Nullable, FlutterError *_Nullable))completion { + [self cancelInProgressCall]; + FLTImagePickerMethodCallContext *context = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^void(NSArray *paths, FlutterError *error) { + if (paths && paths.count != 1) { + completion(nil, [FlutterError errorWithCode:@"invalid_result" + message:@"Incorrect number of return paths provided" + details:nil]); + } + completion(paths.firstObject, error); + }]; + context.maxSize = maxSize; + context.imageQuality = imageQuality; + context.maxImageCount = 1; - if (imageSource == SOURCE_GALLERY) { // Capture is not possible with PHPicker - if (@available(iOS 14, *)) { - // PHPicker is used - [self pickImageWithPHPicker:1]; - } else { - // UIImagePicker is used - [self launchUIImagePickerWithSource:imageSource]; - } - } else { - [self launchUIImagePickerWithSource:imageSource]; - } - } else if ([@"pickMultiImage" isEqualToString:call.method]) { + if (source.type == FLTSourceTypeGallery) { // Capture is not possible with PHPicker if (@available(iOS 14, *)) { - [self pickImageWithPHPicker:0]; + [self launchPHPickerWithContext:context]; } else { - [self launchUIImagePickerWithSource:SOURCE_GALLERY]; - } - } else if ([@"pickVideo" isEqualToString:call.method]) { - UIImagePickerController *imagePickerController = [self createImagePickerController]; - imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; - imagePickerController.delegate = self; - imagePickerController.mediaTypes = @[ - (NSString *)kUTTypeMovie, (NSString *)kUTTypeAVIMovie, (NSString *)kUTTypeVideo, - (NSString *)kUTTypeMPEG4 - ]; - imagePickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; - - int imageSource = [call.arguments[@"source"] intValue]; - if ([call.arguments[@"maxDuration"] isKindOfClass:[NSNumber class]]) { - NSTimeInterval max = [call.arguments[@"maxDuration"] doubleValue]; - imagePickerController.videoMaximumDuration = max; + [self launchUIImagePickerWithSource:source context:context]; } + } else { + [self launchUIImagePickerWithSource:source context:context]; + } +} - switch (imageSource) { - case SOURCE_CAMERA: - [self checkCameraAuthorizationWithImagePicker:imagePickerController]; - break; - case SOURCE_GALLERY: - [self checkPhotoAuthorizationWithImagePicker:imagePickerController]; - break; - default: - result([FlutterError errorWithCode:@"invalid_source" - message:@"Invalid video source." - details:nil]); - break; - } +- (void)pickMultiImageWithMaxSize:(nonnull FLTMaxSize *)maxSize + quality:(nullable NSNumber *)imageQuality + completion:(nonnull void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion { + FLTImagePickerMethodCallContext *context = + [[FLTImagePickerMethodCallContext alloc] initWithResult:completion]; + context.maxSize = maxSize; + context.imageQuality = imageQuality; + + if (@available(iOS 14, *)) { + [self launchPHPickerWithContext:context]; } else { - result(FlutterMethodNotImplemented); + // 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: + (nonnull void (^)(NSString *_Nullable, FlutterError *_Nullable))completion { + FLTImagePickerMethodCallContext *context = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^void(NSArray *paths, FlutterError *error) { + if (paths && paths.count != 1) { + completion(nil, [FlutterError errorWithCode:@"invalid_result" + message:@"Incorrect number of return paths provided" + details:nil]); + } + completion(paths.firstObject, error); + }]; + context.maxImageCount = 1; + + UIImagePickerController *imagePickerController = [self createImagePickerController]; + imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; + imagePickerController.delegate = self; + imagePickerController.mediaTypes = @[ + (NSString *)kUTTypeMovie, (NSString *)kUTTypeAVIMovie, (NSString *)kUTTypeVideo, + (NSString *)kUTTypeMPEG4 + ]; + imagePickerController.videoQuality = UIImagePickerControllerQualityTypeHigh; + + if (maxDurationSeconds) { + NSTimeInterval max = [maxDurationSeconds doubleValue]; + imagePickerController.videoMaximumDuration = max; + } + + self.callContext = context; + + switch (source.type) { + case FLTSourceTypeCamera: + [self checkCameraAuthorizationWithImagePicker:imagePickerController + camera:[self cameraDeviceForSource:source]]; + break; + case FLTSourceTypeGallery: + [self checkPhotoAuthorizationWithImagePicker:imagePickerController]; + break; + default: + [self returnError:[FlutterError errorWithCode:@"invalid_source" + message:@"Invalid video source." + details:nil]]; + break; + } +} + +#pragma mark - + +/** + * If a call is still in progress, cancels it by returning an error and then clearing state. + * + * TODO(stuartmorgan): Eliminate this, and instead track context per image picker (e.g., using + * associated objects). + */ +- (void)cancelInProgressCall { + if (self.callContext) { + [self returnError:[FlutterError errorWithCode:@"multiple_request" + message:@"Cancelled by a second request" + details:nil]]; + self.callContext = nil; } } -- (void)showCameraWithImagePicker:(UIImagePickerController *)imagePickerController { +- (void)showCamera:(UIImagePickerControllerCameraDevice)device + withImagePicker:(UIImagePickerController *)imagePickerController { @synchronized(self) { if (imagePickerController.beingPresented) { return; } } - UIImagePickerControllerCameraDevice device = [self getCameraDeviceFromArguments:_arguments]; // Camera is not available on simulators if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] && [UIImagePickerController isCameraDeviceAvailable:device]) { @@ -254,25 +291,24 @@ - (void)showCameraWithImagePicker:(UIImagePickerController *)imagePickerControll [[self viewControllerWithWindow:nil] presentViewController:cameraErrorAlert animated:YES completion:nil]; - self.result(nil); - self.result = nil; - _arguments = nil; + [self returnSavedPathList:nil]; } } -- (void)checkCameraAuthorizationWithImagePicker:(UIImagePickerController *)imagePickerController { +- (void)checkCameraAuthorizationWithImagePicker:(UIImagePickerController *)imagePickerController + camera:(UIImagePickerControllerCameraDevice)device { AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; switch (status) { case AVAuthorizationStatusAuthorized: - [self showCameraWithImagePicker:imagePickerController]; + [self showCamera:device withImagePicker:imagePickerController]; break; case AVAuthorizationStatusNotDetermined: { [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { dispatch_async(dispatch_get_main_queue(), ^{ if (granted) { - [self showCameraWithImagePicker:imagePickerController]; + [self showCamera:device withImagePicker:imagePickerController]; } else { [self errorNoCameraAccess:AVAuthorizationStatusDenied]; } @@ -352,15 +388,15 @@ - (void)checkPhotoAuthorizationForAccessLevel API_AVAILABLE(ios(14)) { - (void)errorNoCameraAccess:(AVAuthorizationStatus)status { switch (status) { case AVAuthorizationStatusRestricted: - self.result([FlutterError errorWithCode:@"camera_access_restricted" - message:@"The user is not allowed to use the camera." - details:nil]); + [self returnError:[FlutterError errorWithCode:@"camera_access_restricted" + message:@"The user is not allowed to use the camera." + details:nil]]; break; case AVAuthorizationStatusDenied: default: - self.result([FlutterError errorWithCode:@"camera_access_denied" - message:@"The user did not allow camera access." - details:nil]); + [self returnError:[FlutterError errorWithCode:@"camera_access_denied" + message:@"The user did not allow camera access." + details:nil]]; break; } } @@ -368,15 +404,15 @@ - (void)errorNoCameraAccess:(AVAuthorizationStatus)status { - (void)errorNoPhotoAccess:(PHAuthorizationStatus)status { switch (status) { case PHAuthorizationStatusRestricted: - self.result([FlutterError errorWithCode:@"photo_access_restricted" - message:@"The user is not allowed to use the photo." - details:nil]); + [self returnError:[FlutterError errorWithCode:@"photo_access_restricted" + message:@"The user is not allowed to use the photo." + details:nil]]; break; case PHAuthorizationStatusDenied: default: - self.result([FlutterError errorWithCode:@"photo_access_denied" - message:@"The user did not allow photo access." - details:nil]); + [self returnError:[FlutterError errorWithCode:@"photo_access_denied" + message:@"The user did not allow photo access." + details:nil]]; break; } } @@ -406,31 +442,27 @@ - (NSNumber *)getDesiredImageQuality:(NSNumber *)imageQuality { return imageQuality; } +#pragma mark - UIAdaptivePresentationControllerDelegate + - (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController { - if (self.result != nil) { - self.result(nil); - self.result = nil; - self->_arguments = nil; - } + [self returnSavedPathList:nil]; } +#pragma mark - PHPickerViewControllerDelegate + - (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray *)results API_AVAILABLE(ios(14)) { [picker dismissViewControllerAnimated:YES completion:nil]; if (results.count == 0) { - if (self.result != nil) { - self.result(nil); - self.result = nil; - self->_arguments = nil; - } + [self returnSavedPathList:nil]; return; } dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); dispatch_async(backgroundQueue, ^{ - NSNumber *maxWidth = GetNullableValueForKey(self->_arguments, @"maxWidth"); - NSNumber *maxHeight = GetNullableValueForKey(self->_arguments, @"maxHeight"); - NSNumber *imageQuality = GetNullableValueForKey(self->_arguments, @"imageQuality"); + NSNumber *maxWidth = self.callContext.maxSize.width; + NSNumber *maxHeight = self.callContext.maxSize.height; + NSNumber *imageQuality = self.callContext.imageQuality; NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; NSOperationQueue *operationQueue = [NSOperationQueue new]; NSMutableArray *pathList = [self createNSMutableArrayWithSize:results.count]; @@ -449,11 +481,13 @@ - (void)picker:(PHPickerViewController *)picker } [operationQueue waitUntilAllOperationsAreFinished]; dispatch_async(dispatch_get_main_queue(), ^{ - [self handleSavedPathList:pathList]; + [self returnSavedPathList:pathList]; }); }); } +#pragma mark - + /** * Creates an NSMutableArray of a certain size filled with NSNull objects. * @@ -470,6 +504,8 @@ - (NSMutableArray *)createNSMutableArrayWithSize:(NSUInteger)size { return mutableArray; } +#pragma mark - UIImagePickerControllerDelegate + - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { NSURL *videoURL = info[UIImagePickerControllerMediaURL]; @@ -478,7 +514,7 @@ - (void)imagePickerController:(UIImagePickerController *)picker // further didFinishPickingMediaWithInfo invocations. A nil check is necessary // to prevent below code to be unwantly executed multiple times and cause a // crash. - if (!self.result) { + if (!self.callContext) { return; } if (videoURL != nil) { @@ -493,27 +529,24 @@ - (void)imagePickerController:(UIImagePickerController *)picker [[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error]; if (error) { - self.result([FlutterError errorWithCode:@"flutter_image_picker_copy_video_error" - message:@"Could not cache the video file." - details:nil]); - self.result = nil; + [self returnError:[FlutterError errorWithCode:@"flutter_image_picker_copy_video_error" + message:@"Could not cache the video file." + details:nil]]; return; } } videoURL = destination; } } - self.result(videoURL.path); - self.result = nil; - _arguments = nil; + [self returnSavedPathList:@[ videoURL.path ]]; } else { UIImage *image = info[UIImagePickerControllerEditedImage]; if (image == nil) { image = info[UIImagePickerControllerOriginalImage]; } - NSNumber *maxWidth = GetNullableValueForKey(_arguments, @"maxWidth"); - NSNumber *maxHeight = GetNullableValueForKey(_arguments, @"maxHeight"); - NSNumber *imageQuality = GetNullableValueForKey(_arguments, @"imageQuality"); + NSNumber *maxWidth = self.callContext.maxSize.width; + NSNumber *maxHeight = self.callContext.maxSize.height; + NSNumber *imageQuality = self.callContext.imageQuality; NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; PHAsset *originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:info]; @@ -547,14 +580,11 @@ - (void)imagePickerController:(UIImagePickerController *)picker - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [picker dismissViewControllerAnimated:YES completion:nil]; - if (!self.result) { - return; - } - self.result(nil); - self.result = nil; - _arguments = nil; + [self returnSavedPathList:nil]; } +#pragma mark - + - (void)saveImageWithOriginalImageData:(NSData *)originalImageData image:(UIImage *)image maxWidth:(NSNumber *)maxWidth @@ -566,7 +596,7 @@ - (void)saveImageWithOriginalImageData:(NSData *)originalImageData maxWidth:maxWidth maxHeight:maxHeight imageQuality:imageQuality]; - [self handleSavedPathList:@[ savedPath ]]; + [self returnSavedPathList:@[ savedPath ]]; } - (void)saveImageWithPickerInfo:(NSDictionary *)info @@ -575,47 +605,42 @@ - (void)saveImageWithPickerInfo:(NSDictionary *)info NSString *savedPath = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:info image:image imageQuality:imageQuality]; - [self handleSavedPathList:@[ savedPath ]]; + [self returnSavedPathList:@[ savedPath ]]; } /** - * Applies NSMutableArray on the FLutterResult. - * - * NSString must be returned by FlutterResult if the single image - * mode is active. It is checked by maxImagesAllowed and - * returns the first object of the pathlist. + * Validates the provided paths list, then returns it as the result of the original method call, + * clearing the in-progress call state. * - * NSMutableArray must be returned by FlutterResult if the multi-image - * mode is active. After the pathlist count is checked then it returns - * the pathlist. - * - * @param pathList that should be applied to FlutterResult. + * @param pathList The paths to return. nil indicates a cancelled operation. */ -- (void)handleSavedPathList:(NSArray *)pathList { - if (!self.result) { +- (void)returnSavedPathList:(nullable NSArray *)pathList { + if (!self.callContext) { return; } - if (pathList) { - if (![pathList containsObject:[NSNull null]]) { - if ((self.maxImagesAllowed == 1)) { - self.result(pathList.firstObject); - } else { - self.result(pathList); - } - } else { - self.result([FlutterError errorWithCode:@"create_error" - message:@"pathList's items should not be null" - details:nil]); - } + if ([pathList containsObject:[NSNull null]]) { + self.callContext.result(nil, [FlutterError errorWithCode:@"create_error" + message:@"pathList's items should not be null" + details:nil]); } else { - // This should never happen. - self.result([FlutterError errorWithCode:@"create_error" - message:@"pathList should not be nil" - details:nil]); + self.callContext.result(pathList, nil); + } + self.callContext = nil; +} + +/** + * Returns the given error as the result of the original method call, clearing the in-progress + * call state. + * + * @param error The error to return. + */ +- (void)returnError:(FlutterError *)error { + if (!self.callContext) { + return; } - self.result = nil; - _arguments = nil; + self.callContext.result(nil, error); + self.callContext = nil; } @end 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 039f76de427d..dc840939362d 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 @@ -6,26 +6,65 @@ #import -/** Methods exposed for unit testing. */ -@interface FLTImagePickerPlugin () +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * The return hander 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); + +/** + * A container class for context to use when handling a method call from the Dart side. + */ +@interface FLTImagePickerMethodCallContext : NSObject + +/** + * Initializes a new context that calls |result| on completion of the operation. + */ +- (instancetype)initWithResult:(nonnull FlutterResultAdapter)result; -/** The Flutter result callback use to report results back to Flutter App. */ -@property(copy, nonatomic) FlutterResult result; +/** The callback to provide results to the Dart caller. */ +@property(nonatomic, copy, nonnull) FlutterResultAdapter result; /** - * Applies NSMutableArray on the FLutterResult. + * The maximum size to enforce on the results. * - * NSString must be returned by FlutterResult if the single image - * mode is active. It is checked by maxImagesAllowed and - * returns the first object of the pathlist. + * If nil, no resizing is done. + */ +@property(nonatomic, strong, nullable) FLTMaxSize *maxSize; + +/** + * The image quality to resample the results to. * - * NSMutableArray must be returned by FlutterResult if the multi-image - * mode is active. After the pathlist count is checked then it returns - * the pathlist. + * If nil, no resampling is done. + */ +@property(nonatomic, strong, nullable) NSNumber *imageQuality; + +/** Maximum number of images to select. 0 indicates no maximum. */ +@property(nonatomic, assign) int maxImageCount; + +@end + +#pragma mark - + +/** Methods exposed for unit testing. */ +@interface FLTImagePickerPlugin () + +/** + * The context of the Flutter method call that is currently being handled, if any. + */ +@property(strong, nonatomic, nullable) FLTImagePickerMethodCallContext *callContext; + +/** + * Validates the provided paths list, then returns it as the result of the original method call, + * clearing the in-progress call state. * - * @param pathList that should be applied to FlutterResult. + * @param pathList The paths to return. Nil is allowed to indicated a cancelled operation. */ -- (void)handleSavedPathList:(NSArray *)pathList; +- (void)returnSavedPathList:(nullable NSArray *)pathList; /** * Tells the delegate that the user cancelled the pick operation. @@ -52,3 +91,5 @@ (NSArray *)imagePickerControllers; @end + +NS_ASSUME_NONNULL_END From 8fd936ac36b918096377aacb8e8543cd7873c91a Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 15 Apr 2022 15:58:41 -0400 Subject: [PATCH 06/10] Autoformat generated Pigeon code --- .../image_picker_ios/ios/Classes/messages.g.h | 27 ++-- .../image_picker_ios/ios/Classes/messages.g.m | 137 +++++++++--------- .../image_picker_ios/lib/src/messages.g.dart | 60 ++++---- 3 files changed, 124 insertions(+), 100 deletions(-) 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 45b12762bc92..d5c03fbab040 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 @@ -25,17 +25,15 @@ typedef NS_ENUM(NSUInteger, FLTSourceType) { @class FLTSourceSpecification; @interface FLTMaxSize : NSObject -+ (instancetype)makeWithWidth:(nullable NSNumber *)width - height:(nullable NSNumber *)height; -@property(nonatomic, strong, nullable) NSNumber * width; -@property(nonatomic, strong, nullable) NSNumber * height; ++ (instancetype)makeWithWidth:(nullable NSNumber *)width height:(nullable NSNumber *)height; +@property(nonatomic, strong, nullable) NSNumber *width; +@property(nonatomic, strong, nullable) NSNumber *height; @end @interface FLTSourceSpecification : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithType:(FLTSourceType)type - camera:(FLTSourceCamera)camera; ++ (instancetype)makeWithType:(FLTSourceType)type camera:(FLTSourceCamera)camera; @property(nonatomic, assign) FLTSourceType type; @property(nonatomic, assign) FLTSourceCamera camera; @end @@ -44,11 +42,20 @@ typedef NS_ENUM(NSUInteger, FLTSourceType) { NSObject *FLTImagePickerApiGetCodec(void); @protocol FLTImagePickerApi -- (void)pickImageWithSource:(FLTSourceSpecification *)source maxSize:(FLTMaxSize *)maxSize quality:(nullable NSNumber *)imageQuality completion:(void(^)(NSString *_Nullable, FlutterError *_Nullable))completion; -- (void)pickMultiImageWithMaxSize:(FLTMaxSize *)maxSize quality:(nullable NSNumber *)imageQuality completion:(void(^)(NSArray *_Nullable, FlutterError *_Nullable))completion; -- (void)pickVideoWithSource:(FLTSourceSpecification *)source maxDuration:(nullable NSNumber *)maxDurationSeconds completion:(void(^)(NSString *_Nullable, FlutterError *_Nullable))completion; +- (void)pickImageWithSource:(FLTSourceSpecification *)source + maxSize:(FLTMaxSize *)maxSize + quality:(nullable NSNumber *)imageQuality + completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion; +- (void)pickMultiImageWithMaxSize:(FLTMaxSize *)maxSize + quality:(nullable NSNumber *)imageQuality + completion:(void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion; +- (void)pickVideoWithSource:(FLTSourceSpecification *)source + maxDuration:(nullable NSNumber *)maxDurationSeconds + completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion; @end -extern void FLTImagePickerApiSetup(id binaryMessenger, NSObject *_Nullable api); +extern void FLTImagePickerApiSetup(id binaryMessenger, + NSObject *_Nullable api); NS_ASSUME_NONNULL_END 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 86d836499e40..c95326d68eb2 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 @@ -14,26 +14,25 @@ NSDictionary *errorDict = (NSDictionary *)[NSNull null]; if (error) { errorDict = @{ - @"code": (error.code ? error.code : [NSNull null]), - @"message": (error.message ? error.message : [NSNull null]), - @"details": (error.details ? error.details : [NSNull null]), - }; + @"code" : (error.code ? error.code : [NSNull null]), + @"message" : (error.message ? error.message : [NSNull null]), + @"details" : (error.details ? error.details : [NSNull null]), + }; } return @{ - @"result": (result ? result : [NSNull null]), - @"error": errorDict, - }; + @"result" : (result ? result : [NSNull null]), + @"error" : errorDict, + }; } -static id GetNullableObject(NSDictionary* dict, id key) { +static id GetNullableObject(NSDictionary *dict, id key) { id result = dict[key]; return (result == [NSNull null]) ? nil : result; } -static id GetNullableObjectAtIndex(NSArray* array, NSInteger key) { +static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { id result = array[key]; return (result == [NSNull null]) ? nil : result; } - @interface FLTMaxSize () + (FLTMaxSize *)fromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @@ -44,9 +43,8 @@ - (NSDictionary *)toMap; @end @implementation FLTMaxSize -+ (instancetype)makeWithWidth:(nullable NSNumber *)width - height:(nullable NSNumber *)height { - FLTMaxSize* pigeonResult = [[FLTMaxSize alloc] init]; ++ (instancetype)makeWithWidth:(nullable NSNumber *)width height:(nullable NSNumber *)height { + FLTMaxSize *pigeonResult = [[FLTMaxSize alloc] init]; pigeonResult.width = width; pigeonResult.height = height; return pigeonResult; @@ -58,14 +56,15 @@ + (FLTMaxSize *)fromMap:(NSDictionary *)dict { return pigeonResult; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:(self.width ? self.width : [NSNull null]), @"width", (self.height ? self.height : [NSNull null]), @"height", nil]; + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.width ? self.width : [NSNull null]), @"width", + (self.height ? self.height : [NSNull null]), @"height", nil]; } @end @implementation FLTSourceSpecification -+ (instancetype)makeWithType:(FLTSourceType)type - camera:(FLTSourceCamera)camera { - FLTSourceSpecification* pigeonResult = [[FLTSourceSpecification alloc] init]; ++ (instancetype)makeWithType:(FLTSourceType)type camera:(FLTSourceCamera)camera { + FLTSourceSpecification *pigeonResult = [[FLTSourceSpecification alloc] init]; pigeonResult.type = type; pigeonResult.camera = camera; return pigeonResult; @@ -77,25 +76,24 @@ + (FLTSourceSpecification *)fromMap:(NSDictionary *)dict { return pigeonResult; } - (NSDictionary *)toMap { - return [NSDictionary dictionaryWithObjectsAndKeys:@(self.type), @"type", @(self.camera), @"camera", nil]; + return [NSDictionary + dictionaryWithObjectsAndKeys:@(self.type), @"type", @(self.camera), @"camera", nil]; } @end @interface FLTImagePickerApiCodecReader : FlutterStandardReader @end @implementation FLTImagePickerApiCodecReader -- (nullable id)readValueOfType:(UInt8)type -{ +- (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: + case 128: return [FLTMaxSize fromMap:[self readValue]]; - - case 129: + + case 129: return [FLTSourceSpecification fromMap:[self readValue]]; - - default: + + default: return [super readValueOfType:type]; - } } @end @@ -103,17 +101,14 @@ - (nullable id)readValueOfType:(UInt8)type @interface FLTImagePickerApiCodecWriter : FlutterStandardWriter @end @implementation FLTImagePickerApiCodecWriter -- (void)writeValue:(id)value -{ +- (void)writeValue:(id)value { if ([value isKindOfClass:[FLTMaxSize class]]) { [self writeByte:128]; [self writeValue:[value toMap]]; - } else - if ([value isKindOfClass:[FLTSourceSpecification class]]) { + } else if ([value isKindOfClass:[FLTSourceSpecification class]]) { [self writeByte:129]; [self writeValue:[value toMap]]; - } else -{ + } else { [super writeValue:value]; } } @@ -134,75 +129,87 @@ - (FlutterStandardReader *)readerWithData:(NSData *)data { static dispatch_once_t sPred = 0; static FlutterStandardMessageCodec *sSharedObject = nil; dispatch_once(&sPred, ^{ - FLTImagePickerApiCodecReaderWriter *readerWriter = [[FLTImagePickerApiCodecReaderWriter alloc] init]; + FLTImagePickerApiCodecReaderWriter *readerWriter = + [[FLTImagePickerApiCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; }); return sSharedObject; } - -void FLTImagePickerApiSetup(id binaryMessenger, NSObject *api) { +void FLTImagePickerApiSetup(id binaryMessenger, + NSObject *api) { { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickImage" + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickImage" binaryMessenger:binaryMessenger - codec:FLTImagePickerApiGetCodec() ]; + codec:FLTImagePickerApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(pickImageWithSource:maxSize:quality:completion:)], @"FLTImagePickerApi api (%@) doesn't respond to @selector(pickImageWithSource:maxSize:quality:completion:)", api); + NSCAssert([api respondsToSelector:@selector(pickImageWithSource:maxSize:quality:completion:)], + @"FLTImagePickerApi api (%@) doesn't respond to " + @"@selector(pickImageWithSource:maxSize:quality:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FLTSourceSpecification *arg_source = GetNullableObjectAtIndex(args, 0); FLTMaxSize *arg_maxSize = GetNullableObjectAtIndex(args, 1); NSNumber *arg_imageQuality = GetNullableObjectAtIndex(args, 2); - [api pickImageWithSource:arg_source maxSize:arg_maxSize quality:arg_imageQuality completion:^(NSString *_Nullable output, FlutterError *_Nullable error) { - callback(wrapResult(output, error)); - }]; + [api pickImageWithSource:arg_source + maxSize:arg_maxSize + quality:arg_imageQuality + completion:^(NSString *_Nullable output, FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickMultiImage" + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickMultiImage" binaryMessenger:binaryMessenger - codec:FLTImagePickerApiGetCodec() ]; + codec:FLTImagePickerApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(pickMultiImageWithMaxSize:quality:completion:)], @"FLTImagePickerApi api (%@) doesn't respond to @selector(pickMultiImageWithMaxSize:quality:completion:)", api); + NSCAssert([api respondsToSelector:@selector(pickMultiImageWithMaxSize:quality:completion:)], + @"FLTImagePickerApi api (%@) doesn't respond to " + @"@selector(pickMultiImageWithMaxSize:quality:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FLTMaxSize *arg_maxSize = GetNullableObjectAtIndex(args, 0); NSNumber *arg_imageQuality = GetNullableObjectAtIndex(args, 1); - [api pickMultiImageWithMaxSize:arg_maxSize quality:arg_imageQuality completion:^(NSArray *_Nullable output, FlutterError *_Nullable error) { - callback(wrapResult(output, error)); - }]; + [api pickMultiImageWithMaxSize:arg_maxSize + quality:arg_imageQuality + completion:^(NSArray *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } { - FlutterBasicMessageChannel *channel = - [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickVideo" + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickVideo" binaryMessenger:binaryMessenger - codec:FLTImagePickerApiGetCodec() ]; + codec:FLTImagePickerApiGetCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(pickVideoWithSource:maxDuration:completion:)], @"FLTImagePickerApi api (%@) doesn't respond to @selector(pickVideoWithSource:maxDuration:completion:)", api); + NSCAssert([api respondsToSelector:@selector(pickVideoWithSource:maxDuration:completion:)], + @"FLTImagePickerApi api (%@) doesn't respond to " + @"@selector(pickVideoWithSource:maxDuration:completion:)", + api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; FLTSourceSpecification *arg_source = GetNullableObjectAtIndex(args, 0); NSNumber *arg_maxDurationSeconds = GetNullableObjectAtIndex(args, 1); - [api pickVideoWithSource:arg_source maxDuration:arg_maxDurationSeconds completion:^(NSString *_Nullable output, FlutterError *_Nullable error) { - callback(wrapResult(output, error)); - }]; + [api pickVideoWithSource:arg_source + maxDuration:arg_maxDurationSeconds + completion:^(NSString *_Nullable output, FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; }]; - } - else { + } else { [channel setMessageHandler:nil]; } } 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 5e2bc8048bdb..6cd3ca31edca 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 @@ -65,8 +65,7 @@ class SourceSpecification { static SourceSpecification decode(Object message) { final Map pigeonMap = message as Map; return SourceSpecification( - type: SourceType.values[pigeonMap['type']! as int] -, + type: SourceType.values[pigeonMap['type']! as int], camera: pigeonMap['camera'] != null ? SourceCamera.values[pigeonMap['camera']! as int] : null, @@ -81,27 +80,25 @@ class _ImagePickerApiCodec extends StandardMessageCodec { if (value is MaxSize) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else - if (value is SourceSpecification) { + } else if (value is SourceSpecification) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else -{ + } else { super.writeValue(buffer, value); } } + @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: + case 128: return MaxSize.decode(readValue(buffer)!); - - case 129: + + case 129: return SourceSpecification.decode(readValue(buffer)!); - - default: + + default: return super.readValueOfType(type, buffer); - } } } @@ -110,24 +107,29 @@ class ImagePickerApi { /// Constructor for [ImagePickerApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - ImagePickerApi({BinaryMessenger? binaryMessenger}) : _binaryMessenger = binaryMessenger; + ImagePickerApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; final BinaryMessenger? _binaryMessenger; static const MessageCodec codec = _ImagePickerApiCodec(); - Future pickImage(SourceSpecification arg_source, MaxSize arg_maxSize, int? arg_imageQuality) async { + Future pickImage(SourceSpecification arg_source, MaxSize arg_maxSize, + int? arg_imageQuality) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ImagePickerApi.pickImage', codec, binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.ImagePickerApi.pickImage', codec, + binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_source, arg_maxSize, arg_imageQuality]) as Map?; + await channel.send([arg_source, arg_maxSize, arg_imageQuality]) + as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final Map error = + (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -138,18 +140,22 @@ class ImagePickerApi { } } - Future?> pickMultiImage(MaxSize arg_maxSize, int? arg_imageQuality) async { + Future?> pickMultiImage( + MaxSize arg_maxSize, int? arg_imageQuality) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ImagePickerApi.pickMultiImage', codec, binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.ImagePickerApi.pickMultiImage', codec, + binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_maxSize, arg_imageQuality]) as Map?; + await channel.send([arg_maxSize, arg_imageQuality]) + as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final Map error = + (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, @@ -160,18 +166,22 @@ class ImagePickerApi { } } - Future pickVideo(SourceSpecification arg_source, int? arg_maxDurationSeconds) async { + Future pickVideo( + SourceSpecification arg_source, int? arg_maxDurationSeconds) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ImagePickerApi.pickVideo', codec, binaryMessenger: _binaryMessenger); + 'dev.flutter.pigeon.ImagePickerApi.pickVideo', codec, + binaryMessenger: _binaryMessenger); final Map? replyMap = - await channel.send([arg_source, arg_maxDurationSeconds]) as Map?; + await channel.send([arg_source, arg_maxDurationSeconds]) + as Map?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); } else if (replyMap['error'] != null) { - final Map error = (replyMap['error'] as Map?)!; + final Map error = + (replyMap['error'] as Map?)!; throw PlatformException( code: (error['code'] as String?)!, message: error['message'] as String?, From c4e296312000e81d9a86253e43c6a22ac600d7f5 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 15 Apr 2022 16:04:57 -0400 Subject: [PATCH 07/10] Reference Pigeon 3.0.2, not yet published --- .../image_picker/image_picker_ios/ios/Classes/messages.g.h | 2 +- .../image_picker/image_picker_ios/ios/Classes/messages.g.m | 2 +- .../image_picker/image_picker_ios/lib/src/messages.g.dart | 2 +- packages/image_picker/image_picker_ios/pubspec.yaml | 4 +--- packages/image_picker/image_picker_ios/test/test_api.dart | 2 +- 5 files changed, 5 insertions(+), 7 deletions(-) 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 d5c03fbab040..310165f72f4f 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 (v3.0.1), do not edit directly. +// Autogenerated from Pigeon (v3.0.2), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @protocol FlutterBinaryMessenger; 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 c95326d68eb2..6c91c0ab264f 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 (v3.0.1), do not edit directly. +// Autogenerated from Pigeon (v3.0.2), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "messages.g.h" #import 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 6cd3ca31edca..0c5859e80ac9 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 (v3.0.1), do not edit directly. +// Autogenerated from Pigeon (v3.0.2), 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 // @dart = 2.12 diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml index 6e655cdd9c86..3f9528aa0667 100755 --- a/packages/image_picker/image_picker_ios/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/pubspec.yaml @@ -25,6 +25,4 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.0.0 - #pigeon: ^3.0.1 - pigeon: - path: ../../../../packages/packages/pigeon + pigeon: ^3.0.2 diff --git a/packages/image_picker/image_picker_ios/test/test_api.dart b/packages/image_picker/image_picker_ios/test/test_api.dart index 17ca538e8008..1f76e871521d 100644 --- a/packages/image_picker/image_picker_ios/test/test_api.dart +++ b/packages/image_picker/image_picker_ios/test/test_api.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 (v3.0.1), do not edit directly. +// Autogenerated from Pigeon (v3.0.2), 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 // ignore_for_file: avoid_relative_lib_imports From f429c602284526e2081b29f3bd355f8f9585463d Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 15 Apr 2022 16:32:09 -0400 Subject: [PATCH 08/10] Version bump --- packages/image_picker/image_picker_ios/CHANGELOG.md | 6 ++++++ packages/image_picker/image_picker_ios/pubspec.yaml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/image_picker/image_picker_ios/CHANGELOG.md b/packages/image_picker/image_picker_ios/CHANGELOG.md index 3472ade28d5b..3380d14418c2 100644 --- a/packages/image_picker/image_picker_ios/CHANGELOG.md +++ b/packages/image_picker/image_picker_ios/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.8.5 + +* Switches to an in-package method channel based on Pigeon. +* Fixes invalid casts when selecting multiple images on versions of iOS before + 14.0. + ## 0.8.4+11 * Splits from `image_picker` as a federated implementation. diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml index 3f9528aa0667..a9cd052be56a 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 video_picker plugin. repository: https://github.com/flutter/plugins/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.4+11 +version: 0.8.5 environment: sdk: ">=2.14.0 <3.0.0" From 49e175cdffa457ee61bd6ca658576e752957ac41 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 18 Apr 2022 14:46:00 -0400 Subject: [PATCH 09/10] Standardize Dart helper names --- .../image_picker_ios/lib/image_picker_ios.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 9d503a6e810c..3d1413cf0cce 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 @@ -49,7 +49,7 @@ class ImagePickerIOS extends ImagePickerPlatform { int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { - final String? path = await _getImagePath( + final String? path = await _pickImageAsPath( source: source, maxWidth: maxWidth, maxHeight: maxHeight, @@ -65,7 +65,7 @@ class ImagePickerIOS extends ImagePickerPlatform { double? maxHeight, int? imageQuality, }) async { - final List? paths = await _getMultiImagePath( + final List? paths = await _pickMultiImageAsPath( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, @@ -77,7 +77,7 @@ class ImagePickerIOS extends ImagePickerPlatform { return paths.map((dynamic path) => PickedFile(path as String)).toList(); } - Future?> _getMultiImagePath({ + Future?> _pickMultiImageAsPath({ double? maxWidth, double? maxHeight, int? imageQuality, @@ -102,7 +102,7 @@ class ImagePickerIOS extends ImagePickerPlatform { ?.cast(); } - Future _getImagePath({ + Future _pickImageAsPath({ required ImageSource source, double? maxWidth, double? maxHeight, @@ -137,7 +137,7 @@ class ImagePickerIOS extends ImagePickerPlatform { CameraDevice preferredCameraDevice = CameraDevice.rear, Duration? maxDuration, }) async { - final String? path = await _getVideoPath( + final String? path = await _pickVideoAsPath( source: source, maxDuration: maxDuration, preferredCameraDevice: preferredCameraDevice, @@ -145,7 +145,7 @@ class ImagePickerIOS extends ImagePickerPlatform { return path != null ? PickedFile(path) : null; } - Future _getVideoPath({ + Future _pickVideoAsPath({ required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, Duration? maxDuration, @@ -165,7 +165,7 @@ class ImagePickerIOS extends ImagePickerPlatform { int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { - final String? path = await _getImagePath( + final String? path = await _pickImageAsPath( source: source, maxWidth: maxWidth, maxHeight: maxHeight, @@ -181,7 +181,7 @@ class ImagePickerIOS extends ImagePickerPlatform { double? maxHeight, int? imageQuality, }) async { - final List? paths = await _getMultiImagePath( + final List? paths = await _pickMultiImageAsPath( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, @@ -199,7 +199,7 @@ class ImagePickerIOS extends ImagePickerPlatform { CameraDevice preferredCameraDevice = CameraDevice.rear, Duration? maxDuration, }) async { - final String? path = await _getVideoPath( + final String? path = await _pickVideoAsPath( source: source, maxDuration: maxDuration, preferredCameraDevice: preferredCameraDevice, From fe9980c680e9eba2cafbf3fe4d699a0c2f018f17 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 22 Apr 2022 15:08:32 -0400 Subject: [PATCH 10/10] Review comments --- .../ios/RunnerTests/ImagePickerPluginTests.m | 4 +- .../ios/Classes/FLTImagePickerPlugin.m | 83 +++++++++---------- .../ios/Classes/FLTImagePickerPlugin_Test.h | 8 +- 3 files changed, 47 insertions(+), 48 deletions(-) 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 13fbc5278871..04d491131d5b 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 @@ -237,7 +237,7 @@ - (void)testPluginMultiImagePathHasNullItem { pickImageResult = error; dispatch_semaphore_signal(resultSemaphore); }]; - [plugin returnSavedPathList:@[ [NSNull null] ]]; + [plugin sendCallResultWithSavedPathList:@[ [NSNull null] ]]; dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); @@ -256,7 +256,7 @@ - (void)testPluginMultiImagePathHasItem { pickImageResult = result; dispatch_semaphore_signal(resultSemaphore); }]; - [plugin returnSavedPathList:pathList]; + [plugin sendCallResultWithSavedPathList:pathList]; dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); 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 15a494921616..76ed9623a57c 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m @@ -139,9 +139,9 @@ - (void)launchUIImagePickerWithSource:(nonnull FLTSourceSpecification *)source [self checkPhotoAuthorizationWithImagePicker:imagePickerController]; break; default: - [self returnError:[FlutterError errorWithCode:@"invalid_source" - message:@"Invalid image source." - details:nil]]; + [self sendCallResultWithError:[FlutterError errorWithCode:@"invalid_source" + message:@"Invalid image source." + details:nil]]; break; } } @@ -237,9 +237,9 @@ - (void)pickVideoWithSource:(nonnull FLTSourceSpecification *)source [self checkPhotoAuthorizationWithImagePicker:imagePickerController]; break; default: - [self returnError:[FlutterError errorWithCode:@"invalid_source" - message:@"Invalid video source." - details:nil]]; + [self sendCallResultWithError:[FlutterError errorWithCode:@"invalid_source" + message:@"Invalid video source." + details:nil]]; break; } } @@ -254,9 +254,9 @@ - (void)pickVideoWithSource:(nonnull FLTSourceSpecification *)source */ - (void)cancelInProgressCall { if (self.callContext) { - [self returnError:[FlutterError errorWithCode:@"multiple_request" - message:@"Cancelled by a second request" - details:nil]]; + [self sendCallResultWithError:[FlutterError errorWithCode:@"multiple_request" + message:@"Cancelled by a second request" + details:nil]]; self.callContext = nil; } } @@ -291,7 +291,7 @@ - (void)showCamera:(UIImagePickerControllerCameraDevice)device [[self viewControllerWithWindow:nil] presentViewController:cameraErrorAlert animated:YES completion:nil]; - [self returnSavedPathList:nil]; + [self sendCallResultWithSavedPathList:nil]; } } @@ -388,15 +388,17 @@ - (void)checkPhotoAuthorizationForAccessLevel API_AVAILABLE(ios(14)) { - (void)errorNoCameraAccess:(AVAuthorizationStatus)status { switch (status) { case AVAuthorizationStatusRestricted: - [self returnError:[FlutterError errorWithCode:@"camera_access_restricted" - message:@"The user is not allowed to use the camera." - details:nil]]; + [self sendCallResultWithError:[FlutterError + errorWithCode:@"camera_access_restricted" + message:@"The user is not allowed to use the camera." + details:nil]]; break; case AVAuthorizationStatusDenied: default: - [self returnError:[FlutterError errorWithCode:@"camera_access_denied" - message:@"The user did not allow camera access." - details:nil]]; + [self sendCallResultWithError:[FlutterError + errorWithCode:@"camera_access_denied" + message:@"The user did not allow camera access." + details:nil]]; break; } } @@ -404,15 +406,17 @@ - (void)errorNoCameraAccess:(AVAuthorizationStatus)status { - (void)errorNoPhotoAccess:(PHAuthorizationStatus)status { switch (status) { case PHAuthorizationStatusRestricted: - [self returnError:[FlutterError errorWithCode:@"photo_access_restricted" - message:@"The user is not allowed to use the photo." - details:nil]]; + [self sendCallResultWithError:[FlutterError + errorWithCode:@"photo_access_restricted" + message:@"The user is not allowed to use the photo." + details:nil]]; break; case PHAuthorizationStatusDenied: default: - [self returnError:[FlutterError errorWithCode:@"photo_access_denied" - message:@"The user did not allow photo access." - details:nil]]; + [self sendCallResultWithError:[FlutterError + errorWithCode:@"photo_access_denied" + message:@"The user did not allow photo access." + details:nil]]; break; } } @@ -445,7 +449,7 @@ - (NSNumber *)getDesiredImageQuality:(NSNumber *)imageQuality { #pragma mark - UIAdaptivePresentationControllerDelegate - (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController { - [self returnSavedPathList:nil]; + [self sendCallResultWithSavedPathList:nil]; } #pragma mark - PHPickerViewControllerDelegate @@ -454,7 +458,7 @@ - (void)picker:(PHPickerViewController *)picker didFinishPicking:(NSArray *)results API_AVAILABLE(ios(14)) { [picker dismissViewControllerAnimated:YES completion:nil]; if (results.count == 0) { - [self returnSavedPathList:nil]; + [self sendCallResultWithSavedPathList:nil]; return; } dispatch_queue_t backgroundQueue = @@ -481,7 +485,7 @@ - (void)picker:(PHPickerViewController *)picker } [operationQueue waitUntilAllOperationsAreFinished]; dispatch_async(dispatch_get_main_queue(), ^{ - [self returnSavedPathList:pathList]; + [self sendCallResultWithSavedPathList:pathList]; }); }); } @@ -529,16 +533,17 @@ - (void)imagePickerController:(UIImagePickerController *)picker [[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error]; if (error) { - [self returnError:[FlutterError errorWithCode:@"flutter_image_picker_copy_video_error" - message:@"Could not cache the video file." - details:nil]]; + [self sendCallResultWithError:[FlutterError + errorWithCode:@"flutter_image_picker_copy_video_error" + message:@"Could not cache the video file." + details:nil]]; return; } } videoURL = destination; } } - [self returnSavedPathList:@[ videoURL.path ]]; + [self sendCallResultWithSavedPathList:@[ videoURL.path ]]; } else { UIImage *image = info[UIImagePickerControllerEditedImage]; if (image == nil) { @@ -580,7 +585,7 @@ - (void)imagePickerController:(UIImagePickerController *)picker - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [picker dismissViewControllerAnimated:YES completion:nil]; - [self returnSavedPathList:nil]; + [self sendCallResultWithSavedPathList:nil]; } #pragma mark - @@ -596,7 +601,7 @@ - (void)saveImageWithOriginalImageData:(NSData *)originalImageData maxWidth:maxWidth maxHeight:maxHeight imageQuality:imageQuality]; - [self returnSavedPathList:@[ savedPath ]]; + [self sendCallResultWithSavedPathList:@[ savedPath ]]; } - (void)saveImageWithPickerInfo:(NSDictionary *)info @@ -605,16 +610,10 @@ - (void)saveImageWithPickerInfo:(NSDictionary *)info NSString *savedPath = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:info image:image imageQuality:imageQuality]; - [self returnSavedPathList:@[ savedPath ]]; + [self sendCallResultWithSavedPathList:@[ savedPath ]]; } -/** - * Validates the provided paths list, then returns it as the result of the original method call, - * clearing the in-progress call state. - * - * @param pathList The paths to return. nil indicates a cancelled operation. - */ -- (void)returnSavedPathList:(nullable NSArray *)pathList { +- (void)sendCallResultWithSavedPathList:(nullable NSArray *)pathList { if (!self.callContext) { return; } @@ -630,12 +629,12 @@ - (void)returnSavedPathList:(nullable NSArray *)pathList { } /** - * Returns the given error as the result of the original method call, clearing the in-progress - * call state. + * Sends the given error via `callContext.result` as the result of the original platform channel + * method call, clearing the in-progress call state. * * @param error The error to return. */ -- (void)returnError:(FlutterError *)error { +- (void)sendCallResultWithError:(FlutterError *)error { if (!self.callContext) { return; } 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 dc840939362d..2c4167746c8e 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 @@ -59,12 +59,12 @@ typedef void (^FlutterResultAdapter)(NSArray *_Nullable, FlutterErro @property(strong, nonatomic, nullable) FLTImagePickerMethodCallContext *callContext; /** - * Validates the provided paths list, then returns it as the result of the original method call, - * clearing the in-progress call state. + * Validates the provided paths list, then sends it via `callContext.result` as the result of the + * original platform channel method call, clearing the in-progress call state. * - * @param pathList The paths to return. Nil is allowed to indicated a cancelled operation. + * @param pathList The paths to return. nil indicates a cancelled operation. */ -- (void)returnSavedPathList:(nullable NSArray *)pathList; +- (void)sendCallResultWithSavedPathList:(nullable NSArray *)pathList; /** * Tells the delegate that the user cancelled the pick operation.