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