[file_selector_platform_interface] Add platform interface for new file_selector plugin (#2995)

diff --git a/CODEOWNERS b/CODEOWNERS
index 2d4bff3..160c0d5 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -11,6 +11,7 @@
 packages/connectivity/**                       @cyanglaz @matthew-carroll
 packages/device_info/**                        @matthew-carroll
 packages/espresso/**                           @collinjackson @adazh
+packages/file_selector/**                      @ditman
 packages/google_maps_flutter/**                @cyanglaz
 packages/google_sign_in/**                     @cyanglaz @mehmetf
 packages/image_picker/**                       @cyanglaz
diff --git a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md
new file mode 100644
index 0000000..0d8803f
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 1.0.0
+
+* Initial release.
diff --git a/packages/file_selector/file_selector_platform_interface/LICENSE b/packages/file_selector/file_selector_platform_interface/LICENSE
new file mode 100644
index 0000000..2c91f14
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2020 The Flutter 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.
\ No newline at end of file
diff --git a/packages/file_selector/file_selector_platform_interface/README.md b/packages/file_selector/file_selector_platform_interface/README.md
new file mode 100644
index 0000000..d750461
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/README.md
@@ -0,0 +1,26 @@
+# file_selector_platform_interface
+
+A common platform interface for the `file_selector` plugin.
+
+This interface allows platform-specific implementations of the `file_selector`
+plugin, as well as the plugin itself, to ensure they are supporting the
+same interface.
+
+# Usage
+
+To implement a new platform-specific implementation of `file_selector`, extend
+[`FileSelectorPlatform`][2] with an implementation that performs the
+platform-specific behavior, and when you register your plugin, set the default
+`FileSelectorPlatform` by calling
+`FileSelectorPlatform.instance = MyPlatformFileSelector()`.
+
+# Note on breaking changes
+
+Strongly prefer non-breaking changes (such as adding a method to the interface)
+over breaking changes for this package.
+
+See https://flutter.dev/go/platform-interface-breaking-changes for a discussion
+on why a less-clean interface is preferable to a breaking change.
+
+[1]: ../file_selector
+[2]: lib/file_selector_platform_interface.dart
diff --git a/packages/file_selector/file_selector_platform_interface/lib/file_selector_platform_interface.dart b/packages/file_selector/file_selector_platform_interface/lib/file_selector_platform_interface.dart
new file mode 100644
index 0000000..69e3064
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/lib/file_selector_platform_interface.dart
@@ -0,0 +1,2 @@
+export 'src/platform_interface/file_selector_interface.dart';
+export 'src/types/types.dart';
diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart b/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart
new file mode 100644
index 0000000..8681a1d
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart
@@ -0,0 +1,93 @@
+// Copyright 2020 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:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:meta/meta.dart';
+
+const MethodChannel _channel =
+    MethodChannel('plugins.flutter.io/file_selector');
+
+/// An implementation of [FileSelectorPlatform] that uses method channels.
+class MethodChannelFileSelector extends FileSelectorPlatform {
+  /// The MethodChannel that is being used by this implementation of the plugin.
+  @visibleForTesting
+  MethodChannel get channel => _channel;
+
+  /// Load a file from user's computer and return it as an XFile
+  @override
+  Future<XFile> openFile({
+    List<XTypeGroup> acceptedTypeGroups,
+    String initialDirectory,
+    String confirmButtonText,
+  }) async {
+    final List<String> path = await _channel.invokeListMethod<String>(
+      'openFile',
+      <String, dynamic>{
+        'acceptedTypeGroups':
+            acceptedTypeGroups?.map((group) => group.toJSON())?.toList(),
+        'initialDirectory': initialDirectory,
+        'confirmButtonText': confirmButtonText,
+        'multiple': false,
+      },
+    );
+    return path == null ? null : XFile(path?.first);
+  }
+
+  /// Load multiple files from user's computer and return it as an XFile
+  @override
+  Future<List<XFile>> openFiles({
+    List<XTypeGroup> acceptedTypeGroups,
+    String initialDirectory,
+    String confirmButtonText,
+  }) async {
+    final List<String> pathList = await _channel.invokeListMethod<String>(
+      'openFile',
+      <String, dynamic>{
+        'acceptedTypeGroups':
+            acceptedTypeGroups?.map((group) => group.toJSON())?.toList(),
+        'initialDirectory': initialDirectory,
+        'confirmButtonText': confirmButtonText,
+        'multiple': true,
+      },
+    );
+    return pathList?.map((path) => XFile(path))?.toList() ?? [];
+  }
+
+  /// Gets the path from a save dialog
+  @override
+  Future<String> getSavePath({
+    List<XTypeGroup> acceptedTypeGroups,
+    String initialDirectory,
+    String suggestedName,
+    String confirmButtonText,
+  }) async {
+    return _channel.invokeMethod<String>(
+      'getSavePath',
+      <String, dynamic>{
+        'acceptedTypeGroups':
+            acceptedTypeGroups?.map((group) => group.toJSON())?.toList(),
+        'initialDirectory': initialDirectory,
+        'suggestedName': suggestedName,
+        'confirmButtonText': confirmButtonText,
+      },
+    );
+  }
+
+  /// Gets a directory path from a dialog
+  @override
+  Future<String> getDirectoryPath({
+    String initialDirectory,
+    String confirmButtonText,
+  }) async {
+    return _channel.invokeMethod<String>(
+      'getDirectoryPath',
+      <String, dynamic>{
+        'initialDirectory': initialDirectory,
+        'confirmButtonText': confirmButtonText,
+      },
+    );
+  }
+}
diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart b/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart
new file mode 100644
index 0000000..cf23d5f
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart
@@ -0,0 +1,74 @@
+// Copyright 2020 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+
+import '../method_channel/method_channel_file_selector.dart';
+
+/// The interface that implementations of file_selector must implement.
+///
+/// Platform implementations should extend this class rather than implement it as `file_selector`
+/// does not consider newly added methods to be breaking changes. Extending this class
+/// (using `extends`) ensures that the subclass will get the default implementation, while
+/// platform implementations that `implements` this interface will be broken by newly added
+/// [FileSelectorPlatform] methods.
+abstract class FileSelectorPlatform extends PlatformInterface {
+  /// Constructs a FileSelectorPlatform.
+  FileSelectorPlatform() : super(token: _token);
+
+  static final Object _token = Object();
+
+  static FileSelectorPlatform _instance = MethodChannelFileSelector();
+
+  /// The default instance of [FileSelectorPlatform] to use.
+  ///
+  /// Defaults to [MethodChannelFileSelector].
+  static FileSelectorPlatform get instance => _instance;
+
+  /// Platform-specific plugins should set this with their own platform-specific
+  /// class that extends [FileSelectorPlatform] when they register themselves.
+  static set instance(FileSelectorPlatform instance) {
+    PlatformInterface.verifyToken(instance, _token);
+    _instance = instance;
+  }
+
+  /// Open file dialog for loading files and return a file path
+  Future<XFile> openFile({
+    List<XTypeGroup> acceptedTypeGroups,
+    String initialDirectory,
+    String confirmButtonText,
+  }) {
+    throw UnimplementedError('openFile() has not been implemented.');
+  }
+
+  /// Open file dialog for loading files and return a list of file paths
+  Future<List<XFile>> openFiles({
+    List<XTypeGroup> acceptedTypeGroups,
+    String initialDirectory,
+    String confirmButtonText,
+  }) {
+    throw UnimplementedError('openFiles() has not been implemented.');
+  }
+
+  /// Open file dialog for saving files and return a file path at which to save
+  Future<String> getSavePath({
+    List<XTypeGroup> acceptedTypeGroups,
+    String initialDirectory,
+    String suggestedName,
+    String confirmButtonText,
+  }) {
+    throw UnimplementedError('getSavePath() has not been implemented.');
+  }
+
+  /// Open file dialog for loading directories and return a directory path
+  Future<String> getDirectoryPath({
+    String initialDirectory,
+    String confirmButtonText,
+  }) {
+    throw UnimplementedError('getDirectoryPath() has not been implemented.');
+  }
+}
diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/types.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/types.dart
new file mode 100644
index 0000000..8848c67
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/lib/src/types/types.dart
@@ -0,0 +1,3 @@
+export 'x_file/x_file.dart';
+
+export 'x_type_group/x_type_group.dart';
diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/base.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/base.dart
new file mode 100644
index 0000000..7ea050f
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/base.dart
@@ -0,0 +1,82 @@
+import 'dart:convert';
+import 'dart:typed_data';
+
+/// The interface for a XFile.
+///
+/// A XFile 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.
+abstract class XFileBase {
+  /// Construct a XFile
+  XFileBase(String path);
+
+  /// Save the XFile at the indicated file path.
+  void saveTo(String path) async {
+    throw UnimplementedError('saveTo has not been implemented.');
+  }
+
+  /// 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 XFile instance instead.
+  String get path {
+    throw UnimplementedError('.path has not been implemented.');
+  }
+
+  /// The name of the file as it was selected by the user in their device.
+  ///
+  /// Use only for cosmetic reasons, do not try to use this as a path.
+  String get name {
+    throw UnimplementedError('.name has not been implemented.');
+  }
+
+  /// For web, it may be necessary for a file to know its MIME type.
+  String get mimeType {
+    throw UnimplementedError('.mimeType has not been implemented.');
+  }
+
+  /// Get the length of the file. Returns a `Future<int>` that completes with the length in bytes.
+  Future<int> length() {
+    throw UnimplementedError('.length() 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.');
+  }
+
+  /// Get the last-modified time for the XFile
+  Future<DateTime> lastModified() {
+    throw UnimplementedError('openRead() has not been implemented.');
+  }
+}
diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/html.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/html.dart
new file mode 100644
index 0000000..fe898eb
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/html.dart
@@ -0,0 +1,132 @@
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:http/http.dart' as http show readBytes;
+import 'package:meta/meta.dart';
+import 'dart:html';
+
+import '../../web_helpers/web_helpers.dart';
+import './base.dart';
+
+/// A XFile that works on web.
+///
+/// It wraps the bytes of a selected file.
+class XFile extends XFileBase {
+  String path;
+
+  final String mimeType;
+  final Uint8List _data;
+  final int _length;
+  final String name;
+  final DateTime _lastModified;
+  Element _target;
+
+  final XFileTestOverrides _overrides;
+
+  bool get _hasTestOverrides => _overrides != null;
+
+  /// Construct a XFile object from its ObjectUrl.
+  ///
+  /// Optionally, this can be initialized with `bytes` and `length`
+  /// so no http requests are performed to retrieve files later.
+  ///
+  /// `name` needs to be passed from the outside, since we only have
+  /// access to it while we create the ObjectUrl.
+  XFile(
+    this.path, {
+    this.mimeType,
+    this.name,
+    int length,
+    Uint8List bytes,
+    DateTime lastModified,
+    @visibleForTesting XFileTestOverrides overrides,
+  })  : _data = bytes,
+        _length = length,
+        _overrides = overrides,
+        _lastModified = lastModified,
+        super(path);
+
+  /// Construct an XFile from its data
+  XFile.fromData(
+    Uint8List bytes, {
+    this.mimeType,
+    this.name,
+    int length,
+    DateTime lastModified,
+    this.path,
+    @visibleForTesting XFileTestOverrides overrides,
+  })  : _data = bytes,
+        _length = length,
+        _overrides = overrides,
+        _lastModified = lastModified,
+        super(path) {
+    if (path == null) {
+      final blob = (mimeType == null) ? Blob([bytes]) : Blob([bytes], mimeType);
+      this.path = Url.createObjectUrl(blob);
+    }
+  }
+
+  @override
+  Future<DateTime> lastModified() async {
+    if (_lastModified != null) {
+      return Future.value(_lastModified);
+    }
+    return null;
+  }
+
+  Future<Uint8List> get _bytes async {
+    if (_data != null) {
+      return Future.value(UnmodifiableUint8ListView(_data));
+    }
+    return http.readBytes(path);
+  }
+
+  @override
+  Future<int> length() async {
+    return _length ?? (await _bytes).length;
+  }
+
+  @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);
+  }
+
+  /// Saves the data of this XFile at the location indicated by path.
+  /// For the web implementation, the path variable is ignored.
+  void saveTo(String path) async {
+    // Create a DOM container where we can host the anchor.
+    _target = ensureInitialized('__x_file_dom_element');
+
+    // Create an <a> tag with the appropriate download attributes and click it
+    // May be overridden with XFileTestOverrides
+    final AnchorElement element =
+        (_hasTestOverrides && _overrides.createAnchorElement != null)
+            ? _overrides.createAnchorElement(this.path, this.name)
+            : createAnchorElement(this.path, this.name);
+
+    // Clear the children in our container so we can add an element to click
+    _target.children.clear();
+    addElementToContainerAndClick(_target, element);
+  }
+}
+
+/// Overrides some functions to allow testing
+@visibleForTesting
+class XFileTestOverrides {
+  /// For overriding the creation of the file input element.
+  Element Function(String href, String suggestedName) createAnchorElement;
+
+  /// Default constructor for overrides
+  XFileTestOverrides({this.createAnchorElement});
+}
diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/interface.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/interface.dart
new file mode 100644
index 0000000..f5fe388
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/interface.dart
@@ -0,0 +1,54 @@
+import 'dart:typed_data';
+import 'package:meta/meta.dart';
+
+import './base.dart';
+
+/// A XFile is a cross-platform, simplified File abstraction.
+///
+/// It wraps the bytes of a selected file, and its (platform-dependant) path.
+class XFile extends XFileBase {
+  /// Construct a XFile object from its path.
+  ///
+  /// Optionally, this can be initialized with `bytes` and `length`
+  /// so no http requests are performed to retrieve data later.
+  ///
+  /// `name` may be passed from the outside, for those cases where the effective
+  /// `path` of the file doesn't match what the user sees when selecting it
+  /// (like in web)
+  XFile(
+    String path, {
+    String mimeType,
+    String name,
+    int length,
+    Uint8List bytes,
+    DateTime lastModified,
+    @visibleForTesting XFileTestOverrides overrides,
+  }) : super(path) {
+    throw UnimplementedError(
+        'XFile is not available in your current platform.');
+  }
+
+  /// Construct a XFile object from its data
+  XFile.fromData(
+    Uint8List bytes, {
+    String mimeType,
+    String name,
+    int length,
+    DateTime lastModified,
+    String path,
+    @visibleForTesting XFileTestOverrides overrides,
+  }) : super(path) {
+    throw UnimplementedError(
+        'XFile is not available in your current platform.');
+  }
+}
+
+/// Overrides some functions of XFile for testing purposes
+@visibleForTesting
+class XFileTestOverrides {
+  /// For overriding the creation of the file input element.
+  dynamic Function(String href, String suggestedName) createAnchorElement;
+
+  /// Default constructor for overrides
+  XFileTestOverrides({this.createAnchorElement});
+}
diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/io.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/io.dart
new file mode 100644
index 0000000..753732d
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/io.dart
@@ -0,0 +1,111 @@
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import './base.dart';
+
+/// A XFile backed by a dart:io File.
+class XFile extends XFileBase {
+  final File _file;
+  final String mimeType;
+  final DateTime _lastModified;
+  int _length;
+
+  final Uint8List _bytes;
+
+  /// Construct a XFile object backed by a dart:io File.
+  XFile(
+    String path, {
+    this.mimeType,
+    String name,
+    int length,
+    Uint8List bytes,
+    DateTime lastModified,
+  })  : _file = File(path),
+        _bytes = null,
+        _lastModified = lastModified,
+        super(path);
+
+  /// Construct an XFile from its data
+  XFile.fromData(
+    Uint8List bytes, {
+    this.mimeType,
+    String path,
+    String name,
+    int length,
+    DateTime lastModified,
+  })  : _bytes = bytes,
+        _file = File(path ?? ''),
+        _length = length,
+        _lastModified = lastModified,
+        super(path) {
+    if (length == null) {
+      _length = bytes.length;
+    }
+  }
+
+  @override
+  Future<DateTime> lastModified() {
+    if (_lastModified != null) {
+      return Future.value(_lastModified);
+    }
+    return _file.lastModified();
+  }
+
+  @override
+  void saveTo(String path) async {
+    File fileToSave = File(path);
+    await fileToSave.writeAsBytes(_bytes ?? (await readAsBytes()));
+    await fileToSave.create();
+  }
+
+  @override
+  String get path {
+    return _file.path;
+  }
+
+  @override
+  String get name {
+    return _file.path.split(Platform.pathSeparator).last;
+  }
+
+  @override
+  Future<int> length() {
+    if (_length != null) {
+      return Future.value(_length);
+    }
+    return _file.length();
+  }
+
+  @override
+  Future<String> readAsString({Encoding encoding = utf8}) {
+    if (_bytes != null) {
+      return Future.value(String.fromCharCodes(_bytes));
+    }
+    return _file.readAsString(encoding: encoding);
+  }
+
+  @override
+  Future<Uint8List> readAsBytes() {
+    if (_bytes != null) {
+      return Future.value(_bytes);
+    }
+    return _file.readAsBytes();
+  }
+
+  Stream<Uint8List> _getBytes(int start, int end) async* {
+    final bytes = _bytes;
+    yield bytes.sublist(start ?? 0, end ?? bytes.length);
+  }
+
+  @override
+  Stream<Uint8List> openRead([int start, int end]) {
+    if (_bytes != null) {
+      return _getBytes(start, end);
+    } else {
+      return _file
+          .openRead(start ?? 0, end)
+          .map((chunk) => Uint8List.fromList(chunk));
+    }
+  }
+}
diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/x_file.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/x_file.dart
new file mode 100644
index 0000000..4545c60
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/x_file.dart
@@ -0,0 +1,3 @@
+export 'interface.dart'
+    if (dart.library.html) 'html.dart'
+    if (dart.library.io) 'io.dart';
diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart
new file mode 100644
index 0000000..a7d52a7
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart
@@ -0,0 +1,46 @@
+// Copyright 2020 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.
+
+/// A set of allowed XTypes
+class XTypeGroup {
+  /// Creates a new group with the given label and file extensions.
+  XTypeGroup({
+    this.label,
+    this.extensions,
+    this.mimeTypes,
+    this.macUTIs,
+    this.webWildCards,
+  }) : assert(
+            !((extensions == null || extensions.isEmpty) &&
+                (mimeTypes == null || mimeTypes.isEmpty) &&
+                (macUTIs == null || macUTIs.isEmpty) &&
+                (webWildCards == null || webWildCards.isEmpty)),
+            "At least one type must be provided for an XTypeGroup.");
+
+  /// The 'name' or reference to this group of types
+  final String label;
+
+  /// The extensions for this group
+  final List<String> extensions;
+
+  /// The MIME types for this group
+  final List<String> mimeTypes;
+
+  /// The UTIs for this group
+  final List<String> macUTIs;
+
+  /// The web wild cards for this group (ex: image/*, video/*)
+  final List<String> webWildCards;
+
+  /// Converts this object into a JSON formatted object
+  Map<String, dynamic> toJSON() {
+    return <String, dynamic>{
+      'label': label,
+      'extensions': extensions,
+      'mimeTypes': mimeTypes,
+      'macUTIs': macUTIs,
+      'webWildCards': webWildCards,
+    };
+  }
+}
diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart b/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart
new file mode 100644
index 0000000..9e40e56
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart
@@ -0,0 +1,34 @@
+import 'dart:html';
+
+/// Create anchor element with download attribute
+AnchorElement createAnchorElement(String href, String suggestedName) {
+  final element = AnchorElement(href: href);
+
+  if (suggestedName == null) {
+    element.download = 'download';
+  } else {
+    element.download = suggestedName;
+  }
+
+  return element;
+}
+
+/// Add an element to a container and click it
+void addElementToContainerAndClick(Element container, Element element) {
+  // Add the element and click it
+  // All previous elements will be removed before adding the new one
+  container.children.add(element);
+  element.click();
+}
+
+/// Initializes a DOM container where we can host elements.
+Element ensureInitialized(String id) {
+  var target = querySelector('#${id}');
+  if (target == null) {
+    final Element targetElement = Element.tag('flt-x-file')..id = id;
+
+    querySelector('body').children.add(targetElement);
+    target = targetElement;
+  }
+  return target;
+}
diff --git a/packages/file_selector/file_selector_platform_interface/pubspec.yaml b/packages/file_selector/file_selector_platform_interface/pubspec.yaml
new file mode 100644
index 0000000..015762a
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/pubspec.yaml
@@ -0,0 +1,24 @@
+name: file_selector_platform_interface
+description: A common platform interface for the file_selector plugin.
+homepage: https://github.com/flutter/plugins/tree/master/packages/file_selector/file_selector_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.0
+
+dependencies:
+  flutter:
+    sdk: flutter
+  meta: ^1.0.5
+  http: ^0.12.0+1
+  plugin_platform_interface: ^1.0.1
+
+dev_dependencies:
+  test: ^1.15.0
+  flutter_test:
+    sdk: flutter
+  mockito: ^4.1.1
+  pedantic: ^1.8.0
+
+environment:
+  sdk: ">=2.1.0 <3.0.0"
+  flutter: ">=1.9.1+hotfix.4 <2.0.0"
diff --git a/packages/file_selector/file_selector_platform_interface/test/assets/hello.txt b/packages/file_selector/file_selector_platform_interface/test/assets/hello.txt
new file mode 100644
index 0000000..5dd01c1
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/test/assets/hello.txt
@@ -0,0 +1 @@
+Hello, world!
\ No newline at end of file
diff --git a/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart b/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart
new file mode 100644
index 0000000..6809cee
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart
@@ -0,0 +1,43 @@
+// Copyright 2020 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:mockito/mockito.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:file_selector_platform_interface/src/method_channel/method_channel_file_selector.dart';
+
+void main() {
+  group('$FileSelectorPlatform', () {
+    test('$MethodChannelFileSelector() is the default instance', () {
+      expect(FileSelectorPlatform.instance,
+          isInstanceOf<MethodChannelFileSelector>());
+    });
+
+    test('Cannot be implemented with `implements`', () {
+      expect(() {
+        FileSelectorPlatform.instance = ImplementsFileSelectorPlatform();
+      }, throwsA(isInstanceOf<AssertionError>()));
+    });
+
+    test('Can be mocked with `implements`', () {
+      final FileSelectorPlatformMock mock = FileSelectorPlatformMock();
+      FileSelectorPlatform.instance = mock;
+    });
+
+    test('Can be extended', () {
+      FileSelectorPlatform.instance = ExtendsFileSelectorPlatform();
+    });
+  });
+}
+
+class FileSelectorPlatformMock extends Mock
+    with MockPlatformInterfaceMixin
+    implements FileSelectorPlatform {}
+
+class ImplementsFileSelectorPlatform extends Mock
+    implements FileSelectorPlatform {}
+
+class ExtendsFileSelectorPlatform extends FileSelectorPlatform {}
diff --git a/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart b/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart
new file mode 100644
index 0000000..99f9fe0
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart
@@ -0,0 +1,241 @@
+// Copyright 2020 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:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:file_selector_platform_interface/src/method_channel/method_channel_file_selector.dart';
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  group('$MethodChannelFileSelector()', () {
+    MethodChannelFileSelector plugin = MethodChannelFileSelector();
+
+    final List<MethodCall> log = <MethodCall>[];
+
+    setUp(() {
+      plugin.channel.setMockMethodCallHandler((MethodCall methodCall) async {
+        log.add(methodCall);
+        return null;
+      });
+
+      log.clear();
+    });
+
+    group('#openFile', () {
+      test('passes the accepted type groups correctly', () async {
+        final group = XTypeGroup(
+          label: 'text',
+          extensions: ['.txt'],
+          mimeTypes: ['text/plain'],
+          macUTIs: ['public.text'],
+        );
+
+        final groupTwo = XTypeGroup(
+            label: 'image',
+            extensions: ['.jpg'],
+            mimeTypes: ['image/jpg'],
+            macUTIs: ['public.image'],
+            webWildCards: ['image/*']);
+
+        await plugin.openFile(acceptedTypeGroups: [group, groupTwo]);
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('openFile', arguments: <String, dynamic>{
+              'acceptedTypeGroups': [group.toJSON(), groupTwo.toJSON()],
+              'initialDirectory': null,
+              'confirmButtonText': null,
+              'multiple': false,
+            }),
+          ],
+        );
+      });
+      test('passes initialDirectory correctly', () async {
+        await plugin.openFile(initialDirectory: "/example/directory");
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('openFile', arguments: <String, dynamic>{
+              'acceptedTypeGroups': null,
+              'initialDirectory': "/example/directory",
+              'confirmButtonText': null,
+              'multiple': false,
+            }),
+          ],
+        );
+      });
+      test('passes confirmButtonText correctly', () async {
+        await plugin.openFile(confirmButtonText: "Open File");
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('openFile', arguments: <String, dynamic>{
+              'acceptedTypeGroups': null,
+              'initialDirectory': null,
+              'confirmButtonText': "Open File",
+              'multiple': false,
+            }),
+          ],
+        );
+      });
+    });
+    group('#openFiles', () {
+      test('passes the accepted type groups correctly', () async {
+        final group = XTypeGroup(
+          label: 'text',
+          extensions: ['.txt'],
+          mimeTypes: ['text/plain'],
+          macUTIs: ['public.text'],
+        );
+
+        final groupTwo = XTypeGroup(
+            label: 'image',
+            extensions: ['.jpg'],
+            mimeTypes: ['image/jpg'],
+            macUTIs: ['public.image'],
+            webWildCards: ['image/*']);
+
+        await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]);
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('openFile', arguments: <String, dynamic>{
+              'acceptedTypeGroups': [group.toJSON(), groupTwo.toJSON()],
+              'initialDirectory': null,
+              'confirmButtonText': null,
+              'multiple': true,
+            }),
+          ],
+        );
+      });
+      test('passes initialDirectory correctly', () async {
+        await plugin.openFiles(initialDirectory: "/example/directory");
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('openFile', arguments: <String, dynamic>{
+              'acceptedTypeGroups': null,
+              'initialDirectory': "/example/directory",
+              'confirmButtonText': null,
+              'multiple': true,
+            }),
+          ],
+        );
+      });
+      test('passes confirmButtonText correctly', () async {
+        await plugin.openFiles(confirmButtonText: "Open File");
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('openFile', arguments: <String, dynamic>{
+              'acceptedTypeGroups': null,
+              'initialDirectory': null,
+              'confirmButtonText': "Open File",
+              'multiple': true,
+            }),
+          ],
+        );
+      });
+    });
+
+    group('#getSavePath', () {
+      test('passes the accepted type groups correctly', () async {
+        final group = XTypeGroup(
+          label: 'text',
+          extensions: ['.txt'],
+          mimeTypes: ['text/plain'],
+          macUTIs: ['public.text'],
+        );
+
+        final groupTwo = XTypeGroup(
+            label: 'image',
+            extensions: ['.jpg'],
+            mimeTypes: ['image/jpg'],
+            macUTIs: ['public.image'],
+            webWildCards: ['image/*']);
+
+        await plugin.getSavePath(acceptedTypeGroups: [group, groupTwo]);
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('getSavePath', arguments: <String, dynamic>{
+              'acceptedTypeGroups': [group.toJSON(), groupTwo.toJSON()],
+              'initialDirectory': null,
+              'suggestedName': null,
+              'confirmButtonText': null,
+            }),
+          ],
+        );
+      });
+      test('passes initialDirectory correctly', () async {
+        await plugin.getSavePath(initialDirectory: "/example/directory");
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('getSavePath', arguments: <String, dynamic>{
+              'acceptedTypeGroups': null,
+              'initialDirectory': "/example/directory",
+              'suggestedName': null,
+              'confirmButtonText': null,
+            }),
+          ],
+        );
+      });
+      test('passes confirmButtonText correctly', () async {
+        await plugin.getSavePath(confirmButtonText: "Open File");
+
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall('getSavePath', arguments: <String, dynamic>{
+              'acceptedTypeGroups': null,
+              'initialDirectory': null,
+              'suggestedName': null,
+              'confirmButtonText': "Open File",
+            }),
+          ],
+        );
+      });
+      group('#getDirectoryPath', () {
+        test('passes initialDirectory correctly', () async {
+          await plugin.getDirectoryPath(initialDirectory: "/example/directory");
+
+          expect(
+            log,
+            <Matcher>[
+              isMethodCall('getDirectoryPath', arguments: <String, dynamic>{
+                'initialDirectory': "/example/directory",
+                'confirmButtonText': null,
+              }),
+            ],
+          );
+        });
+        test('passes confirmButtonText correctly', () async {
+          await plugin.getDirectoryPath(confirmButtonText: "Open File");
+
+          expect(
+            log,
+            <Matcher>[
+              isMethodCall('getDirectoryPath', arguments: <String, dynamic>{
+                'initialDirectory': null,
+                'confirmButtonText': "Open File",
+              }),
+            ],
+          );
+        });
+      });
+    });
+  });
+}
diff --git a/packages/file_selector/file_selector_platform_interface/test/x_file_html_test.dart b/packages/file_selector/file_selector_platform_interface/test/x_file_html_test.dart
new file mode 100644
index 0000000..f888a04
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/test/x_file_html_test.dart
@@ -0,0 +1,108 @@
+// Copyright 2020 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.
+
+@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:file_selector_platform_interface/file_selector_platform_interface.dart';
+
+import 'dart:html';
+
+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 file = XFile(textFileUrl);
+
+    test('Can be read as a string', () async {
+      expect(await file.readAsString(), equals(expectedStringContents));
+    });
+    test('Can be read as bytes', () async {
+      expect(await file.readAsBytes(), equals(bytes));
+    });
+
+    test('Can be read as a stream', () async {
+      expect(await file.openRead().first, equals(bytes));
+    });
+
+    test('Stream can be sliced', () async {
+      expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+    });
+  });
+
+  group('Create from data', () {
+    final file = XFile.fromData(bytes);
+
+    test('Can be read as a string', () async {
+      expect(await file.readAsString(), equals(expectedStringContents));
+    });
+    test('Can be read as bytes', () async {
+      expect(await file.readAsBytes(), equals(bytes));
+    });
+
+    test('Can be read as a stream', () async {
+      expect(await file.openRead().first, equals(bytes));
+    });
+
+    test('Stream can be sliced', () async {
+      expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+    });
+  });
+
+  group('saveTo(..)', () {
+    final String xFileDomElementId = '__x_file_dom_element';
+
+    group('XFile saveTo(..)', () {
+      test('creates a DOM container', () async {
+        XFile file = XFile.fromData(bytes);
+
+        await file.saveTo('');
+
+        final container = querySelector('#${xFileDomElementId}');
+
+        expect(container, isNotNull);
+      });
+
+      test('create anchor element', () async {
+        XFile file = XFile.fromData(bytes, name: textFile.name);
+
+        await file.saveTo('path');
+
+        final container = querySelector('#${xFileDomElementId}');
+        final AnchorElement element = container?.children?.firstWhere(
+            (element) => element.tagName == 'A',
+            orElse: () => null);
+
+        expect(element, isNotNull);
+        expect(element.href, file.path);
+        expect(element.download, file.name);
+      });
+
+      test('anchor element is clicked', () async {
+        final mockAnchor = AnchorElement();
+
+        XFileTestOverrides overrides = XFileTestOverrides(
+          createAnchorElement: (_, __) => mockAnchor,
+        );
+
+        XFile file =
+            XFile.fromData(bytes, name: textFile.name, overrides: overrides);
+
+        bool clicked = false;
+        mockAnchor.onClick.listen((event) => clicked = true);
+
+        await file.saveTo('path');
+
+        expect(clicked, true);
+      });
+    });
+  });
+}
diff --git a/packages/file_selector/file_selector_platform_interface/test/x_file_io_test.dart b/packages/file_selector/file_selector_platform_interface/test/x_file_io_test.dart
new file mode 100644
index 0000000..b669324
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/test/x_file_io_test.dart
@@ -0,0 +1,99 @@
+// Copyright 2020 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.
+
+@TestOn('vm') // Uses dart:io
+
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+
+// Please note that executing this test with command
+// `flutter test test/x_file_io_test.dart` will set the directory
+// to ./file_selector_platform_interface.
+//
+// This will cause our hello.txt file to be not be found. Please
+// execute this test with `flutter test` or change the path prefix
+// to ./test/assets/
+//
+// https://github.com/flutter/flutter/issues/20907
+
+final pathPrefix = './assets/';
+final path = pathPrefix + 'hello.txt';
+final String expectedStringContents = 'Hello, world!';
+final Uint8List bytes = utf8.encode(expectedStringContents);
+final File textFile = File(path);
+final String textFilePath = textFile.path;
+
+void main() {
+  group('Create with a path', () {
+    final file = XFile(textFilePath);
+
+    test('Can be read as a string', () async {
+      expect(await file.readAsString(), equals(expectedStringContents));
+    });
+    test('Can be read as bytes', () async {
+      expect(await file.readAsBytes(), equals(bytes));
+    });
+
+    test('Can be read as a stream', () async {
+      expect(await file.openRead().first, equals(bytes));
+    });
+
+    test('Stream can be sliced', () async {
+      expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+    });
+
+    test('saveTo(..) creates file', () async {
+      File removeBeforeTest = File(pathPrefix + 'newFilePath.txt');
+      if (removeBeforeTest.existsSync()) {
+        await removeBeforeTest.delete();
+      }
+
+      await file.saveTo(pathPrefix + 'newFilePath.txt');
+      File newFile = File(pathPrefix + 'newFilePath.txt');
+
+      expect(newFile.existsSync(), isTrue);
+      expect(newFile.readAsStringSync(), 'Hello, world!');
+
+      await newFile.delete();
+    });
+  });
+
+  group('Create with data', () {
+    final file = XFile.fromData(bytes);
+
+    test('Can be read as a string', () async {
+      expect(await file.readAsString(), equals(expectedStringContents));
+    });
+    test('Can be read as bytes', () async {
+      expect(await file.readAsBytes(), equals(bytes));
+    });
+
+    test('Can be read as a stream', () async {
+      expect(await file.openRead().first, equals(bytes));
+    });
+
+    test('Stream can be sliced', () async {
+      expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+    });
+
+    test('Function saveTo(..) creates file', () async {
+      File removeBeforeTest = File(pathPrefix + 'newFileData.txt');
+      if (removeBeforeTest.existsSync()) {
+        await removeBeforeTest.delete();
+      }
+
+      await file.saveTo(pathPrefix + 'newFileData.txt');
+      File newFile = File(pathPrefix + 'newFileData.txt');
+
+      expect(newFile.existsSync(), isTrue);
+      expect(newFile.readAsStringSync(), 'Hello, world!');
+
+      await newFile.delete();
+    });
+  });
+}
diff --git a/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart b/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart
new file mode 100644
index 0000000..877e530
--- /dev/null
+++ b/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart
@@ -0,0 +1,37 @@
+// Copyright 2020 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_test/flutter_test.dart';
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+
+void main() {
+  group('XTypeGroup', () {
+    test('fails assertion with no parameters set', () {
+      expect(() => XTypeGroup(), throwsAssertionError);
+    });
+
+    test('toJSON() creates correct map', () {
+      final label = 'test group';
+      final extensions = ['.txt', '.jpg'];
+      final mimeTypes = ['text/plain'];
+      final macUTIs = ['public.plain-text'];
+      final webWildCards = ['image/*'];
+
+      final group = XTypeGroup(
+        label: label,
+        extensions: extensions,
+        mimeTypes: mimeTypes,
+        macUTIs: macUTIs,
+        webWildCards: webWildCards,
+      );
+
+      final jsonMap = group.toJSON();
+      expect(jsonMap['label'], label);
+      expect(jsonMap['extensions'], extensions);
+      expect(jsonMap['mimeTypes'], mimeTypes);
+      expect(jsonMap['macUTIs'], macUTIs);
+      expect(jsonMap['webWildCards'], webWildCards);
+    });
+  });
+}