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 000000000000..0d8803f93540 --- /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 000000000000..0c91662b3f2f --- /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 000000000000..50035db91482 --- /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 000000000000..72aadf35b3e8 --- /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 getTemporaryPath() { + throw UnimplementedError('getTemporaryPath() has not been implemented.'); + } + + /// Path to a directory where the application may place application support + /// files. + Future 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 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 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 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> 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> 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 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 000000000000..cf04a164203f --- /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 000000000000..acac9d5fe7af --- /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 getTemporaryPath() { + return methodChannel.invokeMethod('getTemporaryDirectory'); + } + + Future getApplicationSupportPath() { + return methodChannel.invokeMethod('getApplicationSupportDirectory'); + } + + Future getLibraryPath() { + if (!_platform.isIOS && !_platform.isMacOS) { + throw UnsupportedError('Functionality only available on iOS/macOS'); + } + return methodChannel.invokeMethod('getLibraryDirectory'); + } + + Future getApplicationDocumentsPath() { + return methodChannel + .invokeMethod('getApplicationDocumentsDirectory'); + } + + Future getExternalStoragePath() { + if (!_platform.isAndroid) { + throw UnsupportedError('Functionality only available on Android'); + } + return methodChannel.invokeMethod('getStorageDirectory'); + } + + Future> getExternalCachePaths() { + if (!_platform.isAndroid) { + throw UnsupportedError('Functionality only available on Android'); + } + return methodChannel + .invokeListMethod('getExternalCacheDirectories'); + } + + Future> getExternalStoragePaths({ + AndroidStorageDirectory type, + }) async { + if (!_platform.isAndroid) { + throw UnsupportedError('Functionality only available on Android'); + } + return methodChannel.invokeListMethod( + 'getExternalStorageDirectories', + {'type': type?.index}, + ); + } + + Future getDownloadsPath() { + if (!_platform.isMacOS) { + throw UnsupportedError('Functionality only available on macOS'); + } + return methodChannel.invokeMethod('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 000000000000..44bc0c2c161c --- /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 000000000000..c21acdb140b6 --- /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 log = []; + + 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 [kExternalStoragePaths]; + case 'getExternalCacheDirectories': + return [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, + [isMethodCall('getTemporaryDirectory', arguments: null)], + ); + expect(path, kTemporaryPath); + }); + + test('getApplicationSupportPath', () async { + final String path = + await methodChannelPathProvider.getApplicationSupportPath(); + expect( + log, + [ + 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, + [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, + [isMethodCall('getLibraryDirectory', arguments: null)], + ); + expect(path, kLibraryPath); + }); + + test('getApplicationDocumentsPath', () async { + final String path = + await methodChannelPathProvider.getApplicationDocumentsPath(); + expect( + log, + [ + isMethodCall('getApplicationDocumentsDirectory', arguments: null) + ], + ); + expect(path, kApplicationDocumentsPath); + }); + + test('getExternalCachePaths android succeeds', () async { + final List result = + await methodChannelPathProvider.getExternalCachePaths(); + expect( + log, + [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 + [null]) { + test('getExternalStoragePaths (type: $type) android succeeds', () async { + final List result = + await methodChannelPathProvider.getExternalStoragePaths(type: type); + expect( + log, + [ + isMethodCall( + 'getExternalStorageDirectories', + arguments: {'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, + [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); + } + }); + }); +}