[image_picker_for_web] Introduce image_picker_for_web package (#2802)

diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md
new file mode 100644
index 0000000..18ff7e5
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md
@@ -0,0 +1,3 @@
+# 0.1.0
+
+* Initial open-source release.
diff --git a/packages/image_picker/image_picker_for_web/LICENSE b/packages/image_picker/image_picker_for_web/LICENSE
new file mode 100644
index 0000000..0c382ce
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/LICENSE
@@ -0,0 +1,27 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//    * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//    * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//    * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/image_picker/image_picker_for_web/README.md b/packages/image_picker/image_picker_for_web/README.md
new file mode 100644
index 0000000..81452e2
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/README.md
@@ -0,0 +1,92 @@
+# image_picker_for_web
+
+A web implementation of [`image_picker`][1].
+
+## Browser Support
+
+Since Web Browsers don't offer direct access to their users' file system,
+this plugin provides a `PickedFile` abstraction to make access access uniform
+across platforms.
+
+The web version of the plugin puts network-accessible URIs as the `path`
+in the returned `PickedFile`.
+
+### URL.createObjectURL()
+
+The `PickedFile` object in web is backed by [`URL.createObjectUrl` Web API](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL),
+which is reasonably well supported across all browsers:
+
+![Data on support for the bloburls feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/bloburls.png)
+
+However, the returned `path` attribute of the `PickedFile` points to a `network` resource, and not a
+local path in your users' drive. See **Use the plugin** below for some examples on how to use this
+return value in a cross-platform way.
+
+### input file "accept"
+
+In order to filter only video/image content, some browsers offer an [`accept` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept) in their `input type="file"` form elements:
+
+![Data on support for the input-file-accept feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/input-file-accept.png)
+
+This feature is just a convenience for users, **not validation**.
+
+Users can override this setting on their browsers. You must validate in your app (or server)
+that the user has picked the file type that you can handle.
+
+### input file "capture"
+
+In order to "take a photo", some mobile browsers offer a [`capture` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/capture):
+
+![Data on support for the html-media-capture feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/html-media-capture.png)
+
+Each browser may implement `capture` any way they please, so it may (or may not) make a
+difference in your users' experience.
+
+## Usage
+
+### Import the package
+
+This package is an unendorsed web platform implementation of `image_picker`.
+
+In order to use this, you'll need to depend in `image_picker: ^0.6.7` (which was the first version of the plugin that allowed federation), and `image_picker_for_web: ^0.1.0`.
+
+```yaml
+...
+dependencies:
+  ...
+  image_picker: ^0.6.7
+  image_picker_for_web: ^0.1.0
+  ...
+...
+```
+
+### Use the plugin
+
+You should be able to use `package:image_picker` _almost_ as normal.
+
+Once the user has picked a file, the returned `PickedFile` instance will contain a
+`network`-accessible URL (pointing to a location within the browser).
+
+The instace will also let you retrieve the bytes of the selected file across all platforms.
+
+If you want to use the path directly, your code would need look like this:
+
+```dart
+...
+if (kIsWeb) {
+  Image.network(pickedFile.path);
+} else {
+  Image.file(File(pickedFile.path));
+}
+...
+```
+
+Or, using bytes:
+
+```dart
+...
+Image.memory(await pickedFile.readAsBytes())
+...
+```
+
+[1]: https://pub.dev/packages/image_picker
diff --git a/packages/image_picker/image_picker_for_web/android/.gitignore b/packages/image_picker/image_picker_for_web/android/.gitignore
new file mode 100644
index 0000000..c6cbe56
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/android/.gitignore
@@ -0,0 +1,8 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
diff --git a/packages/image_picker/image_picker_for_web/android/build.gradle b/packages/image_picker/image_picker_for_web/android/build.gradle
new file mode 100644
index 0000000..6d8d50e
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/android/build.gradle
@@ -0,0 +1,33 @@
+group 'io.flutter.image_picker_for_web'
+version '1.0'
+
+buildscript {
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.5.0'
+    }
+}
+
+rootProject.allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+}
+
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 28
+
+    defaultConfig {
+        minSdkVersion 16
+    }
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+}
diff --git a/packages/image_picker/image_picker_for_web/android/gradle.properties b/packages/image_picker/image_picker_for_web/android/gradle.properties
new file mode 100644
index 0000000..7be3d8b
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/android/gradle.properties
@@ -0,0 +1,2 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
diff --git a/packages/image_picker/image_picker_for_web/android/gradle/wrapper/gradle-wrapper.properties b/packages/image_picker/image_picker_for_web/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..019065d
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
diff --git a/packages/image_picker/image_picker_for_web/android/settings.gradle b/packages/image_picker/image_picker_for_web/android/settings.gradle
new file mode 100644
index 0000000..07e3728
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/android/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'image_picker_for_web'
diff --git a/packages/image_picker/image_picker_for_web/android/src/main/AndroidManifest.xml b/packages/image_picker/image_picker_for_web/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b6f6992
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/android/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  package="io.flutter.image_picker_for_web">
+</manifest>
diff --git a/packages/image_picker/image_picker_for_web/android/src/main/java/io/flutter/image_picker_for_web/ImagePickerWebPlugin.java b/packages/image_picker/image_picker_for_web/android/src/main/java/io/flutter/image_picker_for_web/ImagePickerWebPlugin.java
new file mode 100644
index 0000000..18b5bf2
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/android/src/main/java/io/flutter/image_picker_for_web/ImagePickerWebPlugin.java
@@ -0,0 +1,28 @@
+// 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.
+
+package io.flutter.image_picker_for_web;
+
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.plugin.common.PluginRegistry.Registrar;
+
+/** ImagePickerWebPlugin */
+public class ImagePickerWebPlugin implements FlutterPlugin {
+  @Override
+  public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {}
+
+  // This static function is optional and equivalent to onAttachedToEngine. It supports the old
+  // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting
+  // plugin registration via this function while apps migrate to use the new Android APIs
+  // post-flutter-1.12 via https://flutter.dev/go/android-project-migration.
+  //
+  // It is encouraged to share logic between onAttachedToEngine and registerWith to keep
+  // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called
+  // depending on the user's project. onAttachedToEngine or registerWith must both be defined
+  // in the same class.
+  public static void registerWith(Registrar registrar) {}
+
+  @Override
+  public void onDetachedFromEngine(FlutterPluginBinding binding) {}
+}
diff --git a/packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec b/packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec
new file mode 100644
index 0000000..23fb795
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec
@@ -0,0 +1,20 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
+#
+Pod::Spec.new do |s|
+  s.name             = 'image_picker_for_web'
+  s.version          = '0.0.1'
+  s.summary          = 'No-op implementation of image_picker_for_web plugin to avoid build issues on iOS'
+  s.description      = <<-DESC
+temp fake image_picker_for_web plugin
+                       DESC
+  s.homepage         = 'https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web'
+  s.license          = { :file => '../LICENSE' }
+  s.author           = { 'Flutter Team' => 'flutter-dev@googlegroups.com' }
+  s.source           = { :path => '.' }
+  s.source_files = 'Classes/**/*'
+  s.public_header_files = 'Classes/**/*.h'
+  s.dependency 'Flutter'
+
+  s.ios.deployment_target = '8.0'
+end
diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart
new file mode 100644
index 0000000..ce99dd6
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart
@@ -0,0 +1,184 @@
+import 'dart:async';
+import 'dart:html' as html;
+
+import 'package:flutter_web_plugins/flutter_web_plugins.dart';
+import 'package:meta/meta.dart';
+import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
+
+final String _kImagePickerInputsDomId = '__image_picker_web-file-input';
+final String _kAcceptImageMimeType = 'image/*';
+// TODO The value below seems to not be enough for Safari (https://github.com/flutter/flutter/issues/58532)
+final String _kAcceptVideoMimeType = 'video/*';
+
+/// The web implementation of [ImagePickerPlatform].
+///
+/// This class implements the `package:image_picker` functionality for the web.
+class ImagePickerPlugin extends ImagePickerPlatform {
+  final ImagePickerPluginTestOverrides _overrides;
+  bool get _hasOverrides => _overrides != null;
+
+  html.Element _target;
+
+  /// A constructor that allows tests to override the function that creates file inputs.
+  ImagePickerPlugin({
+    @visibleForTesting ImagePickerPluginTestOverrides overrides,
+  }) : _overrides = overrides {
+    _target = _ensureInitialized(_kImagePickerInputsDomId);
+  }
+
+  /// Registers this class as the default instance of [ImagePickerPlatform].
+  static void registerWith(Registrar registrar) {
+    ImagePickerPlatform.instance = ImagePickerPlugin();
+  }
+
+  @override
+  Future<PickedFile> pickImage({
+    @required ImageSource source,
+    double maxWidth,
+    double maxHeight,
+    int imageQuality,
+    CameraDevice preferredCameraDevice = CameraDevice.rear,
+  }) {
+    String capture = computeCaptureAttribute(source, preferredCameraDevice);
+    return pickFile(accept: _kAcceptImageMimeType, capture: capture);
+  }
+
+  @override
+  Future<PickedFile> pickVideo({
+    @required ImageSource source,
+    CameraDevice preferredCameraDevice = CameraDevice.rear,
+    Duration maxDuration,
+  }) {
+    String capture = computeCaptureAttribute(source, preferredCameraDevice);
+    return pickFile(accept: _kAcceptVideoMimeType, capture: capture);
+  }
+
+  /// Injects a file input with the specified accept+capture attributes, and
+  /// returns the PickedFile that the user selected locally.
+  ///
+  /// `capture` is only supported in mobile browsers.
+  /// See https://caniuse.com/#feat=html-media-capture
+  @visibleForTesting
+  Future<PickedFile> pickFile({
+    String accept,
+    String capture,
+  }) {
+    html.FileUploadInputElement input = createInputElement(accept, capture);
+    _injectAndActivate(input);
+    return _getSelectedFile(input);
+  }
+
+  // DOM methods
+
+  /// Converts plugin configuration into a proper value for the `capture` attribute.
+  ///
+  /// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#capture
+  @visibleForTesting
+  String computeCaptureAttribute(ImageSource source, CameraDevice device) {
+    if (source == ImageSource.camera) {
+      return (device == CameraDevice.front) ? 'user' : 'environment';
+    }
+    return null;
+  }
+
+  html.File _getFileFromInput(html.FileUploadInputElement input) {
+    if (_hasOverrides) {
+      return _overrides.getFileFromInput(input);
+    }
+    return input?.files?.first;
+  }
+
+  /// Handles the OnChange event from a FileUploadInputElement object
+  /// Returns the objectURL of the selected file.
+  String _handleOnChangeEvent(html.Event event) {
+    final html.FileUploadInputElement input = event?.target;
+    final html.File file = _getFileFromInput(input);
+
+    if (file != null) {
+      return html.Url.createObjectUrl(file);
+    }
+    return null;
+  }
+
+  /// Monitors an <input type="file"> and returns the selected file.
+  Future<PickedFile> _getSelectedFile(html.FileUploadInputElement input) {
+    final Completer<PickedFile> _completer = Completer<PickedFile>();
+    // Observe the input until we can return something
+    input.onChange.first.then((event) {
+      final objectUrl = _handleOnChangeEvent(event);
+      if (!_completer.isCompleted) {
+        _completer.complete(PickedFile(objectUrl));
+      }
+    });
+    input.onError.first.then((event) {
+      if (!_completer.isCompleted) {
+        _completer.completeError(event);
+      }
+    });
+    // Note that we don't bother detaching from these streams, since the
+    // "input" gets re-created in the DOM every time the user needs to
+    // pick a file.
+    return _completer.future;
+  }
+
+  /// Initializes a DOM container where we can host input elements.
+  html.Element _ensureInitialized(String id) {
+    var target = html.querySelector('#${id}');
+    if (target == null) {
+      final html.Element targetElement =
+          html.Element.tag('flt-image-picker-inputs')..id = id;
+
+      html.querySelector('body').children.add(targetElement);
+      target = targetElement;
+    }
+    return target;
+  }
+
+  /// Creates an input element that accepts certain file types, and
+  /// allows to `capture` from the device's cameras (where supported)
+  @visibleForTesting
+  html.Element createInputElement(String accept, String capture) {
+    if (_hasOverrides) {
+      return _overrides.createInputElement(accept, capture);
+    }
+
+    html.Element element = html.FileUploadInputElement()..accept = accept;
+
+    if (capture != null) {
+      element.setAttribute('capture', capture);
+    }
+
+    return element;
+  }
+
+  /// Injects the file input element, and clicks on it
+  void _injectAndActivate(html.Element element) {
+    _target.children.clear();
+    _target.children.add(element);
+    element.click();
+  }
+}
+
+// Some tools to override behavior for unit-testing
+/// A function that creates a file input with the passed in `accept` and `capture` attributes.
+@visibleForTesting
+typedef OverrideCreateInputFunction = html.Element Function(
+  String accept,
+  String capture,
+);
+
+/// A function that extracts a [html.File] from the file `input` passed in.
+@visibleForTesting
+typedef OverrideExtractFilesFromInputFunction = html.File Function(
+  html.Element input,
+);
+
+/// Overrides for some of the functionality above.
+@visibleForTesting
+class ImagePickerPluginTestOverrides {
+  /// Override the creation of the input element.
+  OverrideCreateInputFunction createInputElement;
+
+  /// Override the extraction of the selected file from an input element.
+  OverrideExtractFilesFromInputFunction getFileFromInput;
+}
diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml
new file mode 100644
index 0000000..d25da73
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/pubspec.yaml
@@ -0,0 +1,32 @@
+name: image_picker_for_web
+description: Web platform implementation of image_picker
+homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web
+# 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump
+# the version to 2.0.0.
+# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0
+version: 0.1.0
+
+flutter:
+  plugin:
+    platforms:
+      web:
+        pluginClass: ImagePickerPlugin
+        fileName: image_picker_for_web.dart
+
+dependencies:
+  image_picker_platform_interface: ^1.1.0
+  flutter:
+    sdk: flutter
+  flutter_web_plugins:
+    sdk: flutter
+  meta: ^1.1.7
+  js: ^0.6.0
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  pedantic: ^1.8.0
+
+environment:
+  sdk: ">=2.5.0 <3.0.0"
+  flutter: ">=1.10.0 <2.0.0"
diff --git a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart
new file mode 100644
index 0000000..96d048d
--- /dev/null
+++ b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart
@@ -0,0 +1,84 @@
+// 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 dart:html
+
+import 'dart:convert';
+import 'dart:html' as html;
+import 'dart:typed_data';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:image_picker_for_web/image_picker_for_web.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");
+
+void main() {
+  // Under test...
+  ImagePickerPlugin plugin;
+
+  setUp(() {
+    plugin = ImagePickerPlugin();
+  });
+
+  test('Can select a file', () async {
+    final mockInput = html.FileUploadInputElement();
+
+    final overrides = ImagePickerPluginTestOverrides()
+      ..createInputElement = ((_, __) => mockInput)
+      ..getFileFromInput = ((_) => textFile);
+
+    final plugin = ImagePickerPlugin(overrides: overrides);
+
+    // Init the pick file dialog...
+    final file = plugin.pickFile();
+
+    // Mock the browser behavior of selecting a file...
+    mockInput.dispatchEvent(html.Event('change'));
+
+    // Now the file should be available
+    expect(file, completes);
+    // And readable
+    expect((await file).readAsBytes(), completion(isNotEmpty));
+  });
+
+  // There's no good way of detecting when the user has "aborted" the selection.
+
+  test('computeCaptureAttribute', () {
+    expect(
+      plugin.computeCaptureAttribute(ImageSource.gallery, CameraDevice.front),
+      isNull,
+    );
+    expect(
+      plugin.computeCaptureAttribute(ImageSource.gallery, CameraDevice.rear),
+      isNull,
+    );
+    expect(
+      plugin.computeCaptureAttribute(ImageSource.camera, CameraDevice.front),
+      'user',
+    );
+    expect(
+      plugin.computeCaptureAttribute(ImageSource.camera, CameraDevice.rear),
+      'environment',
+    );
+  });
+
+  group('createInputElement', () {
+    test('accept: any, capture: null', () {
+      html.Element input = plugin.createInputElement('any', null);
+
+      expect(input.attributes, containsPair('accept', 'any'));
+      expect(input.attributes, isNot(contains('capture')));
+    });
+
+    test('accept: any, capture: something', () {
+      html.Element input = plugin.createInputElement('any', 'something');
+
+      expect(input.attributes, containsPair('accept', 'any'));
+      expect(input.attributes, containsPair('capture', 'something'));
+    });
+  });
+}