[image_picker_platform_interface] Introduce new PickedFile APIs. (#2791)
* Make API Async, so web can use objectUrls internally, instead of bytes.
* Introduce the PickedFile class to have a more platform agnostic return.
* Modify the platform interface to return PickedFiles.
Run tests with flutter test / flutter test --platform chrome
Co-authored-by: Rody Davis <rody.davis.jr@gmail.com>
diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md
index 7708c34..0a238bc 100644
--- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md
+++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 1.1.0
+
+* Introduce PickedFile type for the new API.
+
## 1.0.1
* Update lower bound of dart dependency to 2.1.0.
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart
index 4d96051..71704b6 100644
--- a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart
@@ -20,6 +20,24 @@
MethodChannel get channel => _channel;
@override
+ Future<PickedFile> pickImage({
+ @required ImageSource source,
+ double maxWidth,
+ double maxHeight,
+ int imageQuality,
+ CameraDevice preferredCameraDevice = CameraDevice.rear,
+ }) async {
+ String path = await pickImagePath(
+ source: source,
+ maxWidth: maxWidth,
+ maxHeight: maxHeight,
+ imageQuality: imageQuality,
+ preferredCameraDevice: preferredCameraDevice,
+ );
+ return path != null ? PickedFile(path) : null;
+ }
+
+ @override
Future<String> pickImagePath({
@required ImageSource source,
double maxWidth,
@@ -54,6 +72,20 @@
}
@override
+ Future<PickedFile> pickVideo({
+ @required ImageSource source,
+ CameraDevice preferredCameraDevice = CameraDevice.rear,
+ Duration maxDuration,
+ }) async {
+ String path = await pickVideoPath(
+ source: source,
+ maxDuration: maxDuration,
+ preferredCameraDevice: preferredCameraDevice,
+ );
+ return path != null ? PickedFile(path) : null;
+ }
+
+ @override
Future<String> pickVideoPath({
@required ImageSource source,
CameraDevice preferredCameraDevice = CameraDevice.rear,
@@ -71,10 +103,48 @@
}
@override
+ Future<LostData> retrieveLostData() async {
+ final Map<String, dynamic> result =
+ await _channel.invokeMapMethod<String, dynamic>('retrieve');
+
+ if (result == null) {
+ return LostData.empty();
+ }
+
+ assert(result.containsKey('path') ^ result.containsKey('errorCode'));
+
+ final String type = result['type'];
+ 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'], message: result['errorMessage']);
+ }
+
+ final String path = result['path'];
+
+ return LostData(
+ file: path != null ? PickedFile(path) : null,
+ exception: exception,
+ type: retrieveType,
+ );
+ }
+
+ @override
+ // ignore: deprecated_member_use_from_same_package
Future<LostDataResponse> retrieveLostDataAsDartIoFile() async {
final Map<String, dynamic> result =
await _channel.invokeMapMethod<String, dynamic>('retrieve');
if (result == null) {
+ // ignore: deprecated_member_use_from_same_package
return LostDataResponse.empty();
}
assert(result.containsKey('path') ^ result.containsKey('errorCode'));
@@ -97,6 +167,7 @@
final String path = result['path'];
+ // ignore: deprecated_member_use_from_same_package
return LostDataResponse(
file: path == null ? null : File(path),
exception: exception,
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart
index 66e74dd..94be4c2 100644
--- a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart
@@ -60,6 +60,7 @@
///
/// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost
/// in this call. You can then call [retrieveLostDataAsDartIoFile] when your app relaunches to retrieve the lost data.
+ @Deprecated('Use pickImage instead.')
Future<String> pickImagePath({
@required ImageSource source,
double maxWidth,
@@ -84,6 +85,7 @@
///
/// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost
/// in this call. You can then call [retrieveLostDataAsDartIoFile] when your app relaunches to retrieve the lost data.
+ @Deprecated('Use pickVideo instead.')
Future<String> pickVideoPath({
@required ImageSource source,
CameraDevice preferredCameraDevice = CameraDevice.rear,
@@ -92,7 +94,7 @@
throw UnimplementedError('pickVideoPath() has not been implemented.');
}
- /// Retrieve the lost image file when [pickImage] or [pickVideo] failed because the MainActivity is destroyed. (Android only)
+ /// Retrieve the lost image file when [pickImagePath] or [pickVideoPath] failed because the MainActivity is destroyed. (Android only)
///
/// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive.
/// Call this method to retrieve the lost data and process the data according to your APP's business logic.
@@ -105,8 +107,81 @@
/// See also:
/// * [LostDataResponse], for what's included in the response.
/// * [Android Activity Lifecycle](https://developer.android.com/reference/android/app/Activity.html), for more information on MainActivity destruction.
+ @Deprecated('Use retrieveLostData instead.')
Future<LostDataResponse> retrieveLostDataAsDartIoFile() {
throw UnimplementedError(
'retrieveLostDataAsDartIoFile() has not been implemented.');
}
+
+ // Next version of the API.
+
+ /// Returns a [PickedFile] with the image that was picked.
+ ///
+ /// The `source` argument controls where the image comes from. This can
+ /// be either [ImageSource.camera] or [ImageSource.gallery].
+ ///
+ /// If specified, the image will be at most `maxWidth` wide and
+ /// `maxHeight` tall. Otherwise the image will be returned at it's
+ /// original width and height.
+ ///
+ /// The `imageQuality` argument modifies the quality of the image, ranging from 0-100
+ /// where 100 is the original/max quality. If `imageQuality` is null, the image with
+ /// the original quality will be returned. Compression is only supportted for certain
+ /// image types such as JPEG. If compression is not supported for the image that is picked,
+ /// an warning message will be logged.
+ ///
+ /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera].
+ /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device.
+ /// Defaults to [CameraDevice.rear].
+ ///
+ /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost
+ /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data.
+ Future<PickedFile> pickImage({
+ @required ImageSource source,
+ double maxWidth,
+ double maxHeight,
+ int imageQuality,
+ CameraDevice preferredCameraDevice = CameraDevice.rear,
+ }) {
+ throw UnimplementedError('pickImage() has not been implemented.');
+ }
+
+ /// Returns a [PickedFile] containing the video that was picked.
+ ///
+ /// The [source] argument controls where the video comes from. This can
+ /// be either [ImageSource.camera] or [ImageSource.gallery].
+ ///
+ /// The [maxDuration] argument specifies the maximum duration of the captured video. If no [maxDuration] is specified,
+ /// the maximum duration will be infinite.
+ ///
+ /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera].
+ /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device.
+ /// Defaults to [CameraDevice.rear].
+ ///
+ /// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost
+ /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data.
+ Future<PickedFile> pickVideo({
+ @required ImageSource source,
+ CameraDevice preferredCameraDevice = CameraDevice.rear,
+ Duration maxDuration,
+ }) {
+ throw UnimplementedError('pickVideo() has not been implemented.');
+ }
+
+ /// Retrieve the lost [PickedFile] file when [pickImage] or [pickVideo] failed because the MainActivity is destroyed. (Android only)
+ ///
+ /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive.
+ /// Call this method to retrieve the lost data and process the data according to your APP's business logic.
+ ///
+ /// Returns a [LostData] object if successfully retrieved the lost data. The [LostData] object can represent either a
+ /// successful image/video selection, or a failure.
+ ///
+ /// Calling this on a non-Android platform will throw [UnimplementedError] exception.
+ ///
+ /// See also:
+ /// * [LostData], for what's included in the response.
+ /// * [Android Activity Lifecycle](https://developer.android.com/reference/android/app/Activity.html), for more information on MainActivity destruction.
+ Future<LostData> retrieveLostData() {
+ throw UnimplementedError('retrieveLostData() has not been implemented.');
+ }
}
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart
index 53e2dec..d82618b 100644
--- a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart
@@ -12,6 +12,7 @@
/// Only applies to Android.
/// See also:
/// * [ImagePicker.retrieveLostData] for more details on retrieving lost data.
+@Deprecated('Use methods that return a LostData object instead.')
class LostDataResponse {
/// Creates an instance with the given [file], [exception], and [type]. Any of
/// the params may be null, but this is never considered to be empty.
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart
new file mode 100644
index 0000000..285294e
--- /dev/null
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart
@@ -0,0 +1,58 @@
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:meta/meta.dart';
+
+/// The interface for a PickedFile.
+///
+/// A PickedFile is a container that wraps the path of a selected
+/// file by the user and (in some platforms, like web) the bytes
+/// with the contents of the file.
+///
+/// This class is a very limited subset of dart:io [File], so all
+/// the methods should seem familiar.
+@immutable
+abstract class PickedFileBase {
+ /// Construct a PickedFile
+ PickedFileBase(String path);
+
+ /// Get the path of the picked file.
+ ///
+ /// This should only be used as a backwards-compatibility clutch
+ /// for mobile apps, or cosmetic reasons only (to show the user
+ /// the path they've picked).
+ ///
+ /// Accessing the data contained in the picked file by its path
+ /// is platform-dependant (and won't work on web), so use the
+ /// byte getters in the PickedFile instance instead.
+ String get path {
+ throw UnimplementedError('.path has not been implemented.');
+ }
+
+ /// Synchronously read the entire file contents as a string using the given [Encoding].
+ ///
+ /// By default, `encoding` is [utf8].
+ ///
+ /// Throws Exception if the operation fails.
+ Future<String> readAsString({Encoding encoding = utf8}) {
+ throw UnimplementedError('readAsString() has not been implemented.');
+ }
+
+ /// Synchronously read the entire file contents as a list of bytes.
+ ///
+ /// Throws Exception if the operation fails.
+ Future<Uint8List> readAsBytes() {
+ throw UnimplementedError('readAsBytes() has not been implemented.');
+ }
+
+ /// Create a new independent [Stream] for the contents of this file.
+ ///
+ /// If `start` is present, the file will be read from byte-offset `start`. Otherwise from the beginning (index 0).
+ ///
+ /// If `end` is present, only up to byte-index `end` will be read. Otherwise, until end of file.
+ ///
+ /// In order to make sure that system resources are freed, the stream must be read to completion or the subscription on the stream must be cancelled.
+ Stream<Uint8List> openRead([int start, int end]) {
+ throw UnimplementedError('openRead() has not been implemented.');
+ }
+}
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart
new file mode 100644
index 0000000..0faf531
--- /dev/null
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart
@@ -0,0 +1,45 @@
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:http/http.dart' as http show readBytes;
+
+import './base.dart';
+
+/// A PickedFile that works on web.
+///
+/// It wraps the bytes of a selected file.
+class PickedFile extends PickedFileBase {
+ final String path;
+ final Uint8List _initBytes;
+
+ /// Construct a PickedFile object from its ObjectUrl.
+ ///
+ /// Optionally, this can be initialized with `bytes`
+ /// so no http requests are performed to retrieve files later.
+ PickedFile(this.path, {Uint8List bytes})
+ : _initBytes = bytes,
+ super(path);
+
+ Future<Uint8List> get _bytes async {
+ if (_initBytes != null) {
+ return Future.value(UnmodifiableUint8ListView(_initBytes));
+ }
+ return http.readBytes(path);
+ }
+
+ @override
+ Future<String> readAsString({Encoding encoding = utf8}) async {
+ return encoding.decode(await _bytes);
+ }
+
+ @override
+ Future<Uint8List> readAsBytes() async {
+ return Future.value(await _bytes);
+ }
+
+ @override
+ Stream<Uint8List> openRead([int start, int end]) async* {
+ final bytes = await _bytes;
+ yield bytes.sublist(start ?? 0, end ?? bytes.length);
+ }
+}
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart
new file mode 100644
index 0000000..dd64558
--- /dev/null
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart
@@ -0,0 +1,37 @@
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import './base.dart';
+
+/// A PickedFile backed by a dart:io File.
+class PickedFile extends PickedFileBase {
+ final File _file;
+
+ /// Construct a PickedFile object backed by a dart:io File.
+ PickedFile(String path)
+ : _file = File(path),
+ super(path);
+
+ @override
+ String get path {
+ return _file.path;
+ }
+
+ @override
+ Future<String> readAsString({Encoding encoding = utf8}) {
+ return _file.readAsString(encoding: encoding);
+ }
+
+ @override
+ Future<Uint8List> readAsBytes() {
+ return _file.readAsBytes();
+ }
+
+ @override
+ Stream<Uint8List> openRead([int start, int end]) {
+ return _file
+ .openRead(start ?? 0, end)
+ .map((chunk) => Uint8List.fromList(chunk));
+ }
+}
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart
new file mode 100644
index 0000000..b94e69d
--- /dev/null
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart
@@ -0,0 +1,49 @@
+// Copyright 2017 The Chromium 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:image_picker_platform_interface/src/types/types.dart';
+
+/// The response object of [ImagePicker.retrieveLostData].
+///
+/// Only applies to Android.
+/// See also:
+/// * [ImagePicker.retrieveLostData] for more details on retrieving lost data.
+class LostData {
+ /// Creates an instance with the given [file], [exception], and [type]. Any of
+ /// the params may be null, but this is never considered to be empty.
+ LostData({this.file, this.exception, this.type});
+
+ /// Initializes an instance with all member params set to null and considered
+ /// to be empty.
+ LostData.empty()
+ : file = null,
+ exception = null,
+ type = null,
+ _empty = true;
+
+ /// Whether it is an empty response.
+ ///
+ /// An empty response should have [file], [exception] and [type] to be null.
+ bool get isEmpty => _empty;
+
+ /// The file that was lost in a previous [pickImage] or [pickVideo] call due to MainActivity being destroyed.
+ ///
+ /// Can be null if [exception] exists.
+ final PickedFile file;
+
+ /// The exception of the last [pickImage] or [pickVideo].
+ ///
+ /// If the last [pickImage] or [pickVideo] threw some exception before the MainActivity destruction, this variable keeps that
+ /// exception.
+ /// You should handle this exception as if the [pickImage] or [pickVideo] got an exception when the MainActivity was not destroyed.
+ ///
+ /// Note that it is not the exception that caused the destruction of the MainActivity.
+ final PlatformException exception;
+
+ /// Can either be [RetrieveType.image] or [RetrieveType.video];
+ final RetrieveType type;
+
+ bool _empty = false;
+}
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/picked_file.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/picked_file.dart
new file mode 100644
index 0000000..b2a614c
--- /dev/null
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/picked_file.dart
@@ -0,0 +1,4 @@
+export 'lost_data.dart';
+export 'unsupported.dart'
+ if (dart.library.html) 'html.dart'
+ if (dart.library.io) 'io.dart';
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/unsupported.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/unsupported.dart
new file mode 100644
index 0000000..bc10a48
--- /dev/null
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/unsupported.dart
@@ -0,0 +1,14 @@
+import './base.dart';
+
+/// A PickedFile is a cross-platform, simplified File abstraction.
+///
+/// It wraps the bytes of a selected file, and its (platform-dependant) path.
+class PickedFile extends PickedFileBase {
+ /// Construct a PickedFile object, from its `bytes`.
+ ///
+ /// Optionally, you may pass a `path`. See caveats in [PickedFileBase.path].
+ PickedFile(String path) : super(path) {
+ throw UnimplementedError(
+ 'PickedFile is not available in your current platform.');
+ }
+}
diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart
index 9841810..9c44fae 100644
--- a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart
+++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart
@@ -2,6 +2,7 @@
export 'image_source.dart';
export 'lost_data_response.dart';
export 'retrieve_type.dart';
+export 'picked_file/picked_file.dart';
/// Denotes that an image is being picked.
const String kTypeImage = 'image';
diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml
index a4ea5d1..946cf80 100644
--- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml
+++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml
@@ -3,12 +3,13 @@
homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_platform_interface
# NOTE: We strongly prefer non-breaking changes, even at the expense of a
# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
-version: 1.0.1
+version: 1.1.0
dependencies:
flutter:
sdk: flutter
meta: ^1.1.8
+ http: ^0.12.1
plugin_platform_interface: ^1.0.2
dev_dependencies:
@@ -18,5 +19,5 @@
pedantic: ^1.8.0+1
environment:
- sdk: ">=2.1.0 <3.0.0"
+ sdk: ">=2.5.0 <3.0.0"
flutter: ">=1.10.0 <2.0.0"
diff --git a/packages/image_picker/image_picker_platform_interface/test/assets/hello.txt b/packages/image_picker/image_picker_platform_interface/test/assets/hello.txt
new file mode 100644
index 0000000..5dd01c1
--- /dev/null
+++ b/packages/image_picker/image_picker_platform_interface/test/assets/hello.txt
@@ -0,0 +1 @@
+Hello, world!
\ No newline at end of file
diff --git a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart
index 701379b..ddaad3d 100644
--- a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart
+++ b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart
@@ -299,6 +299,7 @@
'path': '/example/path',
};
});
+ // ignore: deprecated_member_use_from_same_package
final LostDataResponse response =
await picker.retrieveLostDataAsDartIoFile();
expect(response.type, RetrieveType.image);
@@ -313,6 +314,7 @@
'errorMessage': 'test_error_message',
};
});
+ // ignore: deprecated_member_use_from_same_package
final LostDataResponse response =
await picker.retrieveLostDataAsDartIoFile();
expect(response.type, RetrieveType.video);
@@ -338,6 +340,6 @@
});
expect(picker.retrieveLostDataAsDartIoFile(), throwsAssertionError);
});
- });
+ }, skip: isBrowser);
});
}
diff --git a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart
new file mode 100644
index 0000000..e7abe37
--- /dev/null
+++ b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart
@@ -0,0 +1,353 @@
+// Copyright 2019 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_platform_interface/image_picker_platform_interface.dart';
+import 'package:image_picker_platform_interface/src/method_channel/method_channel_image_picker.dart';
+
+void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ group('$MethodChannelImagePicker', () {
+ MethodChannelImagePicker picker = MethodChannelImagePicker();
+
+ final List<MethodCall> log = <MethodCall>[];
+
+ setUp(() {
+ picker.channel.setMockMethodCallHandler((MethodCall methodCall) async {
+ log.add(methodCall);
+ return '';
+ });
+
+ 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,
+ <Matcher>[
+ isMethodCall('pickImage', arguments: <String, dynamic>{
+ 'source': 0,
+ 'maxWidth': null,
+ 'maxHeight': null,
+ 'imageQuality': null,
+ 'cameraDevice': 0
+ }),
+ isMethodCall('pickImage', arguments: <String, dynamic>{
+ '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,
+ <Matcher>[
+ isMethodCall('pickImage', arguments: <String, dynamic>{
+ 'source': 0,
+ 'maxWidth': null,
+ 'maxHeight': null,
+ 'imageQuality': null,
+ 'cameraDevice': 0
+ }),
+ isMethodCall('pickImage', arguments: <String, dynamic>{
+ 'source': 0,
+ 'maxWidth': 10.0,
+ 'maxHeight': null,
+ 'imageQuality': null,
+ 'cameraDevice': 0
+ }),
+ isMethodCall('pickImage', arguments: <String, dynamic>{
+ 'source': 0,
+ 'maxWidth': null,
+ 'maxHeight': 10.0,
+ 'imageQuality': null,
+ 'cameraDevice': 0
+ }),
+ isMethodCall('pickImage', arguments: <String, dynamic>{
+ 'source': 0,
+ 'maxWidth': 10.0,
+ 'maxHeight': 20.0,
+ 'imageQuality': null,
+ 'cameraDevice': 0
+ }),
+ isMethodCall('pickImage', arguments: <String, dynamic>{
+ 'source': 0,
+ 'maxWidth': 10.0,
+ 'maxHeight': null,
+ 'imageQuality': 70,
+ 'cameraDevice': 0
+ }),
+ isMethodCall('pickImage', arguments: <String, dynamic>{
+ 'source': 0,
+ 'maxWidth': null,
+ 'maxHeight': 10.0,
+ 'imageQuality': 70,
+ 'cameraDevice': 0
+ }),
+ isMethodCall('pickImage', arguments: <String, dynamic>{
+ 'source': 0,
+ 'maxWidth': 10.0,
+ 'maxHeight': 20.0,
+ 'imageQuality': 70,
+ 'cameraDevice': 0
+ }),
+ ],
+ );
+ });
+
+ 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,
+ <Matcher>[
+ isMethodCall('pickImage', arguments: <String, dynamic>{
+ '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,
+ <Matcher>[
+ isMethodCall('pickImage', arguments: <String, dynamic>{
+ 'source': 0,
+ 'maxWidth': null,
+ 'maxHeight': null,
+ 'imageQuality': null,
+ 'cameraDevice': 1,
+ }),
+ ],
+ );
+ });
+ });
+
+ group('#pickVideoPath', () {
+ test('passes the image source argument correctly', () async {
+ await picker.pickVideo(source: ImageSource.camera);
+ await picker.pickVideo(source: ImageSource.gallery);
+
+ expect(
+ log,
+ <Matcher>[
+ isMethodCall('pickVideo', arguments: <String, dynamic>{
+ 'source': 0,
+ 'cameraDevice': 0,
+ 'maxDuration': null,
+ }),
+ isMethodCall('pickVideo', arguments: <String, dynamic>{
+ '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,
+ <Matcher>[
+ isMethodCall('pickVideo', arguments: <String, dynamic>{
+ 'source': 0,
+ 'maxDuration': null,
+ 'cameraDevice': 0,
+ }),
+ isMethodCall('pickVideo', arguments: <String, dynamic>{
+ 'source': 0,
+ 'maxDuration': 10,
+ 'cameraDevice': 0,
+ }),
+ isMethodCall('pickVideo', arguments: <String, dynamic>{
+ 'source': 0,
+ 'maxDuration': 60,
+ 'cameraDevice': 0,
+ }),
+ isMethodCall('pickVideo', arguments: <String, dynamic>{
+ '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,
+ <Matcher>[
+ isMethodCall('pickVideo', arguments: <String, dynamic>{
+ '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,
+ <Matcher>[
+ isMethodCall('pickVideo', arguments: <String, dynamic>{
+ 'source': 0,
+ 'maxDuration': null,
+ 'cameraDevice': 1,
+ }),
+ ],
+ );
+ });
+ });
+
+ group('#retrieveLostData', () {
+ test('retrieveLostData get success response', () async {
+ picker.channel.setMockMethodCallHandler((MethodCall methodCall) async {
+ return <String, String>{
+ 'type': 'image',
+ 'path': '/example/path',
+ };
+ });
+ // ignore: deprecated_member_use_from_same_package
+ final LostData response = await picker.retrieveLostData();
+ expect(response.type, RetrieveType.image);
+ expect(response.file.path, '/example/path');
+ });
+
+ test('retrieveLostData get error response', () async {
+ picker.channel.setMockMethodCallHandler((MethodCall methodCall) async {
+ return <String, String>{
+ 'type': 'video',
+ 'errorCode': 'test_error_code',
+ 'errorMessage': 'test_error_message',
+ };
+ });
+ // ignore: deprecated_member_use_from_same_package
+ final LostData response = await picker.retrieveLostData();
+ expect(response.type, RetrieveType.video);
+ 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 <String, String>{
+ 'type': 'video',
+ 'errorCode': 'test_error_code',
+ 'errorMessage': 'test_error_message',
+ 'path': '/example/path',
+ };
+ });
+ expect(picker.retrieveLostData(), throwsAssertionError);
+ });
+ });
+ });
+}
diff --git a/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart b/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart
new file mode 100644
index 0000000..49d84ff
--- /dev/null
+++ b/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart
@@ -0,0 +1,39 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@TestOn('chrome') // Uses web-only Flutter SDK
+
+import 'dart:convert';
+import 'dart:html' as html;
+import 'dart:typed_data';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
+
+final String expectedStringContents = 'Hello, world!';
+final Uint8List bytes = utf8.encode(expectedStringContents);
+final html.File textFile = html.File([bytes], 'hello.txt');
+final String textFileUrl = html.Url.createObjectUrl(textFile);
+
+void main() {
+ group('Create with an objectUrl', () {
+ final pickedFile = PickedFile(textFileUrl);
+
+ test('Can be read as a string', () async {
+ expect(await pickedFile.readAsString(), equals(expectedStringContents));
+ });
+ test('Can be read as bytes', () async {
+ expect(await pickedFile.readAsBytes(), equals(bytes));
+ });
+
+ test('Can be read as a stream', () async {
+ expect(await pickedFile.openRead().first, equals(bytes));
+ });
+
+ test('Stream can be sliced', () async {
+ expect(
+ await pickedFile.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+ });
+ });
+}
diff --git a/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart b/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart
new file mode 100644
index 0000000..94ff759
--- /dev/null
+++ b/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart
@@ -0,0 +1,39 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@TestOn('vm') // Uses dart:io
+
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
+
+final String expectedStringContents = 'Hello, world!';
+final Uint8List bytes = utf8.encode(expectedStringContents);
+final File textFile = File('./assets/hello.txt');
+final String textFilePath = textFile.path;
+
+void main() {
+ group('Create with an objectUrl', () {
+ final pickedFile = PickedFile(textFilePath);
+
+ test('Can be read as a string', () async {
+ expect(await pickedFile.readAsString(), equals(expectedStringContents));
+ });
+ test('Can be read as bytes', () async {
+ expect(await pickedFile.readAsBytes(), equals(bytes));
+ });
+
+ test('Can be read as a stream', () async {
+ expect(await pickedFile.openRead().first, equals(bytes));
+ });
+
+ test('Stream can be sliced', () async {
+ expect(
+ await pickedFile.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+ });
+ });
+}