[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)));
+    });
+  });
+}