[path_provider] Create platform interface (#2553)

diff --git a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md
new file mode 100644
index 0000000..0d8803f
--- /dev/null
+++ b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 1.0.0
+
+* Initial release.
diff --git a/packages/path_provider/path_provider_platform_interface/LICENSE b/packages/path_provider/path_provider_platform_interface/LICENSE
new file mode 100644
index 0000000..0c91662
--- /dev/null
+++ b/packages/path_provider/path_provider_platform_interface/LICENSE
@@ -0,0 +1,27 @@
+// Copyright 2020 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/path_provider/path_provider_platform_interface/README.md b/packages/path_provider/path_provider_platform_interface/README.md
new file mode 100644
index 0000000..50035db
--- /dev/null
+++ b/packages/path_provider/path_provider_platform_interface/README.md
@@ -0,0 +1,26 @@
+# path_provider_platform_interface
+
+A common platform interface for the [`path_provider`][1] plugin.
+
+This interface allows platform-specific implementations of the `path_provider`
+plugin, as well as the plugin itself, to ensure they are supporting the
+same interface.
+
+# Usage
+
+To implement a new platform-specific implementation of `path_provider`, extend
+[`PathProviderPlatform`][2] with an implementation that performs the
+platform-specific behavior, and when you register your plugin, set the default
+`PathProviderPlatform` by calling
+`PathProviderPlatform.instance = MyPlatformPathProvider()`.
+
+# 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]: ../
+[2]: lib/path_provider_platform_interface.dart
diff --git a/packages/path_provider/path_provider_platform_interface/lib/path_provider_platform_interface.dart b/packages/path_provider/path_provider_platform_interface/lib/path_provider_platform_interface.dart
new file mode 100644
index 0000000..72aadf3
--- /dev/null
+++ b/packages/path_provider/path_provider_platform_interface/lib/path_provider_platform_interface.dart
@@ -0,0 +1,101 @@
+// Copyright 2020 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 'dart:async';
+
+import 'src/enums.dart';
+import 'src/method_channel_path_provider.dart';
+
+import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+
+export 'src/enums.dart';
+
+/// The interface that implementations of path_provider must implement.
+///
+/// Platform implementations should extend this class rather than implement it as `PathProvider`
+/// 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
+/// [PathProviderPlatform] methods.
+abstract class PathProviderPlatform extends PlatformInterface {
+  /// Constructs a PathProviderPlatform.
+  PathProviderPlatform() : super(token: _token);
+
+  static final Object _token = Object();
+
+  static PathProviderPlatform _instance = MethodChannelPathProvider();
+
+  /// The default instance of [PathProviderPlatform] to use.
+  ///
+  /// Defaults to [MethodChannelPathProvider].
+  static PathProviderPlatform get instance => _instance;
+
+  /// Platform-specific plugins should set this with their own platform-specific
+  /// class that extends [PathProviderPlatform] when they register themselves.
+  static set instance(PathProviderPlatform instance) {
+    PlatformInterface.verifyToken(instance, _token);
+    _instance = instance;
+  }
+
+  /// Path to the temporary directory on the device that is not backed up and is
+  /// suitable for storing caches of downloaded files.
+  Future<String> getTemporaryPath() {
+    throw UnimplementedError('getTemporaryPath() has not been implemented.');
+  }
+
+  /// Path to a directory where the application may place application support
+  /// files.
+  Future<String> getApplicationSupportPath() {
+    throw UnimplementedError(
+        'getApplicationSupportPath() has not been implemented.');
+  }
+
+  /// Path to the directory where application can store files that are persistent,
+  /// backed up, and not visible to the user, such as sqlite.db.
+  Future<String> getLibraryPath() {
+    throw UnimplementedError('getLibraryPath() has not been implemented.');
+  }
+
+  /// Path to a directory where the application may place data that is
+  /// user-generated, or that cannot otherwise be recreated by your application.
+  Future<String> getApplicationDocumentsPath() {
+    throw UnimplementedError(
+        'getApplicationDocumentsPath() has not been implemented.');
+  }
+
+  /// Path to a directory where the application may access top level storage.
+  /// The current operating system should be determined before issuing this
+  /// function call, as this functionality is only available on Android.
+  Future<String> getExternalStoragePath() {
+    throw UnimplementedError(
+        'getExternalStoragePath() has not been implemented.');
+  }
+
+  /// Paths to directories where application specific external cache data can be
+  /// stored. These paths typically reside on external storage like separate
+  /// partitions or SD cards. Phones may have multiple storage directories
+  /// available.
+  Future<List<String>> getExternalCachePaths() {
+    throw UnimplementedError(
+        'getExternalCachePaths() has not been implemented.');
+  }
+
+  /// Paths to directories where application specific data can be stored.
+  /// These paths typically reside on external storage like separate partitions
+  /// or SD cards. Phones may have multiple storage directories available.
+  Future<List<String>> getExternalStoragePaths({
+    /// Optional parameter. See [AndroidStorageDirectory] for more informations on
+    /// how this type translates to Android storage directories.
+    AndroidStorageDirectory type,
+  }) {
+    throw UnimplementedError(
+        'getExternalStoragePaths() has not been implemented.');
+  }
+
+  /// Path to the directory where downloaded files can be stored.
+  /// This is typically only relevant on desktop operating systems.
+  Future<String> getDownloadsPath() {
+    throw UnimplementedError('getDownloadsPath() has not been implemented.');
+  }
+}
diff --git a/packages/path_provider/path_provider_platform_interface/lib/src/enums.dart b/packages/path_provider/path_provider_platform_interface/lib/src/enums.dart
new file mode 100644
index 0000000..cf04a16
--- /dev/null
+++ b/packages/path_provider/path_provider_platform_interface/lib/src/enums.dart
@@ -0,0 +1,49 @@
+/// Corresponds to constants defined in Androids `android.os.Environment` class.
+///
+/// https://developer.android.com/reference/android/os/Environment.html#fields_1
+enum AndroidStorageDirectory {
+  /// Contains audio files that should be treated as music.
+  ///
+  /// See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_MUSIC.
+  music,
+
+  /// Contains audio files that should be treated as podcasts.
+  ///
+  /// See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_PODCASTS.
+  podcasts,
+
+  /// Contains audio files that should be treated as ringtones.
+  ///
+  /// See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_RINGTONES.
+  ringtones,
+
+  /// Contains audio files that should be treated as alarm sounds.
+  ///
+  /// See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_ALARMS.
+  alarms,
+
+  /// Contains audio files that should be treated as notification sounds.
+  ///
+  /// See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_NOTIFICATIONS.
+  notifications,
+
+  /// Contains images. See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_PICTURES.
+  pictures,
+
+  /// Contains movies. See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_MOVIES.
+  movies,
+
+  /// Contains files of any type that have been downloaded by the user.
+  ///
+  /// See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_DOWNLOADS.
+  downloads,
+
+  /// Used to hold both pictures and videos when the device filesystem is
+  /// treated like a camera's.
+  ///
+  /// See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_DCIM.
+  dcim,
+
+  /// Holds user-created documents. See https://developer.android.com/reference/android/os/Environment.html#DIRECTORY_DOCUMENTS.
+  documents,
+}
diff --git a/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart b/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart
new file mode 100644
index 0000000..acac9d5
--- /dev/null
+++ b/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart
@@ -0,0 +1,86 @@
+// Copyright 2020 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 'dart:async';
+
+import 'enums.dart';
+
+import 'package:flutter/services.dart';
+import 'package:meta/meta.dart';
+import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
+import 'package:platform/platform.dart';
+
+/// An implementation of [PathProviderPlatform] that uses method channels.
+class MethodChannelPathProvider extends PathProviderPlatform {
+  /// The method channel used to interact with the native platform.
+  @visibleForTesting
+  MethodChannel methodChannel =
+      MethodChannel('plugins.flutter.io/path_provider');
+
+  // Ideally, this property shouldn't exist, and each platform should
+  // just implement the supported methods. Once all the platforms are
+  // federated, this property should be removed.
+  Platform _platform = const LocalPlatform();
+
+  /// This API is only exposed for the unit tests. It should not be used by
+  /// any code outside of the plugin itself.
+  @visibleForTesting
+  void setMockPathProviderPlatform(Platform platform) {
+    _platform = platform;
+  }
+
+  Future<String> getTemporaryPath() {
+    return methodChannel.invokeMethod<String>('getTemporaryDirectory');
+  }
+
+  Future<String> getApplicationSupportPath() {
+    return methodChannel.invokeMethod<String>('getApplicationSupportDirectory');
+  }
+
+  Future<String> getLibraryPath() {
+    if (!_platform.isIOS && !_platform.isMacOS) {
+      throw UnsupportedError('Functionality only available on iOS/macOS');
+    }
+    return methodChannel.invokeMethod<String>('getLibraryDirectory');
+  }
+
+  Future<String> getApplicationDocumentsPath() {
+    return methodChannel
+        .invokeMethod<String>('getApplicationDocumentsDirectory');
+  }
+
+  Future<String> getExternalStoragePath() {
+    if (!_platform.isAndroid) {
+      throw UnsupportedError('Functionality only available on Android');
+    }
+    return methodChannel.invokeMethod<String>('getStorageDirectory');
+  }
+
+  Future<List<String>> getExternalCachePaths() {
+    if (!_platform.isAndroid) {
+      throw UnsupportedError('Functionality only available on Android');
+    }
+    return methodChannel
+        .invokeListMethod<String>('getExternalCacheDirectories');
+  }
+
+  Future<List<String>> getExternalStoragePaths({
+    AndroidStorageDirectory type,
+  }) async {
+    if (!_platform.isAndroid) {
+      throw UnsupportedError('Functionality only available on Android');
+    }
+    return methodChannel.invokeListMethod<String>(
+      'getExternalStorageDirectories',
+      <String, dynamic>{'type': type?.index},
+    );
+  }
+
+  Future<String> getDownloadsPath() {
+    if (!_platform.isMacOS) {
+      throw UnsupportedError('Functionality only available on macOS');
+    }
+    return methodChannel.invokeMethod<String>('getDownloadsDirectory');
+  }
+}
diff --git a/packages/path_provider/path_provider_platform_interface/pubspec.yaml b/packages/path_provider/path_provider_platform_interface/pubspec.yaml
new file mode 100644
index 0000000..44bc0c2
--- /dev/null
+++ b/packages/path_provider/path_provider_platform_interface/pubspec.yaml
@@ -0,0 +1,23 @@
+name: path_provider_platform_interface
+description: A common platform interface for the path_provider plugin.
+homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_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
+  platform: ^2.0.0
+  plugin_platform_interface: ^1.0.1
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  pedantic: ^1.8.0
+  test: any
+
+environment:
+  sdk: ">=2.0.0-dev.28.0 <3.0.0"
+  flutter: ">=1.10.0 <2.0.0"
diff --git a/packages/path_provider/path_provider_platform_interface/test/method_channel_path_provider_test.dart b/packages/path_provider/path_provider_platform_interface/test/method_channel_path_provider_test.dart
new file mode 100644
index 0000000..c21acdb
--- /dev/null
+++ b/packages/path_provider/path_provider_platform_interface/test/method_channel_path_provider_test.dart
@@ -0,0 +1,204 @@
+// Copyright 2020 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:flutter_test/flutter_test.dart';
+import 'package:path_provider_platform_interface/src/enums.dart';
+import 'package:path_provider_platform_interface/src/method_channel_path_provider.dart';
+import 'package:platform/platform.dart';
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+  const String kTemporaryPath = 'temporaryPath';
+  const String kApplicationSupportPath = 'applicationSupportPath';
+  const String kLibraryPath = 'libraryPath';
+  const String kApplicationDocumentsPath = 'applicationDocumentsPath';
+  const String kExternalCachePaths = 'externalCachePaths';
+  const String kExternalStoragePaths = 'externalStoragePaths';
+  const String kDownloadsPath = 'downloadsPath';
+
+  group('$MethodChannelPathProvider', () {
+    MethodChannelPathProvider methodChannelPathProvider;
+    final List<MethodCall> log = <MethodCall>[];
+
+    setUp(() async {
+      methodChannelPathProvider = MethodChannelPathProvider();
+
+      methodChannelPathProvider.methodChannel
+          .setMockMethodCallHandler((MethodCall methodCall) async {
+        log.add(methodCall);
+        switch (methodCall.method) {
+          case 'getTemporaryDirectory':
+            return kTemporaryPath;
+          case 'getApplicationSupportDirectory':
+            return kApplicationSupportPath;
+          case 'getLibraryDirectory':
+            return kLibraryPath;
+          case 'getApplicationDocumentsDirectory':
+            return kApplicationDocumentsPath;
+          case 'getExternalStorageDirectories':
+            return <String>[kExternalStoragePaths];
+          case 'getExternalCacheDirectories':
+            return <String>[kExternalCachePaths];
+          case 'getDownloadsDirectory':
+            return kDownloadsPath;
+          default:
+            return null;
+        }
+      });
+    });
+
+    setUp(() {
+      methodChannelPathProvider.setMockPathProviderPlatform(
+          FakePlatform(operatingSystem: 'android'));
+    });
+
+    tearDown(() {
+      log.clear();
+    });
+
+    test('getTemporaryPath', () async {
+      final String path = await methodChannelPathProvider.getTemporaryPath();
+      expect(
+        log,
+        <Matcher>[isMethodCall('getTemporaryDirectory', arguments: null)],
+      );
+      expect(path, kTemporaryPath);
+    });
+
+    test('getApplicationSupportPath', () async {
+      final String path =
+          await methodChannelPathProvider.getApplicationSupportPath();
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('getApplicationSupportDirectory', arguments: null)
+        ],
+      );
+      expect(path, kApplicationSupportPath);
+    });
+
+    test('getLibraryPath android fails', () async {
+      try {
+        await methodChannelPathProvider.getLibraryPath();
+        fail('should throw UnsupportedError');
+      } catch (e) {
+        expect(e, isUnsupportedError);
+      }
+    });
+
+    test('getLibraryPath iOS succeeds', () async {
+      methodChannelPathProvider
+          .setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios'));
+
+      final String path = await methodChannelPathProvider.getLibraryPath();
+      expect(
+        log,
+        <Matcher>[isMethodCall('getLibraryDirectory', arguments: null)],
+      );
+      expect(path, kLibraryPath);
+    });
+
+    test('getLibraryPath macOS succeeds', () async {
+      methodChannelPathProvider
+          .setMockPathProviderPlatform(FakePlatform(operatingSystem: 'macos'));
+
+      final String path = await methodChannelPathProvider.getLibraryPath();
+      expect(
+        log,
+        <Matcher>[isMethodCall('getLibraryDirectory', arguments: null)],
+      );
+      expect(path, kLibraryPath);
+    });
+
+    test('getApplicationDocumentsPath', () async {
+      final String path =
+          await methodChannelPathProvider.getApplicationDocumentsPath();
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('getApplicationDocumentsDirectory', arguments: null)
+        ],
+      );
+      expect(path, kApplicationDocumentsPath);
+    });
+
+    test('getExternalCachePaths android succeeds', () async {
+      final List<String> result =
+          await methodChannelPathProvider.getExternalCachePaths();
+      expect(
+        log,
+        <Matcher>[isMethodCall('getExternalCacheDirectories', arguments: null)],
+      );
+      expect(result.length, 1);
+      expect(result.first, kExternalCachePaths);
+    });
+
+    test('getExternalCachePaths non-android fails', () async {
+      methodChannelPathProvider
+          .setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios'));
+
+      try {
+        await methodChannelPathProvider.getExternalCachePaths();
+        fail('should throw UnsupportedError');
+      } catch (e) {
+        expect(e, isUnsupportedError);
+      }
+    });
+
+    for (AndroidStorageDirectory type
+        in AndroidStorageDirectory.values + <AndroidStorageDirectory>[null]) {
+      test('getExternalStoragePaths (type: $type) android succeeds', () async {
+        final List<String> result =
+            await methodChannelPathProvider.getExternalStoragePaths(type: type);
+        expect(
+          log,
+          <Matcher>[
+            isMethodCall(
+              'getExternalStorageDirectories',
+              arguments: <String, dynamic>{'type': type?.index},
+            )
+          ],
+        );
+
+        expect(result.length, 1);
+        expect(result.first, kExternalStoragePaths);
+      });
+
+      test('getExternalStoragePaths (type: $type) non-android fails', () async {
+        methodChannelPathProvider
+            .setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios'));
+
+        try {
+          await methodChannelPathProvider.getExternalStoragePaths();
+          fail('should throw UnsupportedError');
+        } catch (e) {
+          expect(e, isUnsupportedError);
+        }
+      });
+    } // end of for-loop
+
+    test('getDownloadsPath macos succeeds', () async {
+      methodChannelPathProvider
+          .setMockPathProviderPlatform(FakePlatform(operatingSystem: 'macos'));
+      final String result = await methodChannelPathProvider.getDownloadsPath();
+      expect(
+        log,
+        <Matcher>[isMethodCall('getDownloadsDirectory', arguments: null)],
+      );
+      expect(result, kDownloadsPath);
+    });
+
+    test('getDownloadsPath  non-macos fails', () async {
+      methodChannelPathProvider.setMockPathProviderPlatform(
+          FakePlatform(operatingSystem: 'android'));
+      try {
+        await methodChannelPathProvider.getDownloadsPath();
+        fail('should throw UnsupportedError');
+      } catch (e) {
+        expect(e, isUnsupportedError);
+      }
+    });
+  });
+}