diff --git a/packages/path_provider/path_provider_foundation/CHANGELOG.md b/packages/path_provider/path_provider_foundation/CHANGELOG.md
index ffce0d465e7..1c0ffee40bf 100644
--- a/packages/path_provider/path_provider_foundation/CHANGELOG.md
+++ b/packages/path_provider/path_provider_foundation/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 2.5.0
+
+* Replaces Flutter-plugin-based implementation with direct FFI calls to
+ Foundation.
+
## 2.4.4
* Updates to Pigeon 26.
diff --git a/packages/path_provider/path_provider_foundation/CONTRIBUTING.md b/packages/path_provider/path_provider_foundation/CONTRIBUTING.md
new file mode 100644
index 00000000000..e5c45fb48bf
--- /dev/null
+++ b/packages/path_provider/path_provider_foundation/CONTRIBUTING.md
@@ -0,0 +1,18 @@
+# Contributing
+
+## `ffigen`
+
+This package uses [ffigen](https://pub.dev/packages/ffigen) to call Foundation
+methods, rather than using the standard Flutter plugin structure. To add new
+functionality to the FFI interface, update `tool/ffigen.dart`, then run:
+
+```bash
+dart run tool/ffigen.dart
+```
+
+### Configuration philosophy
+
+This package intentionally uses very strict filtering rules to include only the
+necessary methods and functions. This is partially to keep the package small,
+but mostly to avoid unnecessarily generating anything that requires native code
+helpers, which would require setting up a native compilation step.
diff --git a/packages/path_provider/path_provider_foundation/darwin/RunnerTests/RunnerTests.swift b/packages/path_provider/path_provider_foundation/darwin/RunnerTests/RunnerTests.swift
deleted file mode 100644
index 608b7f05971..00000000000
--- a/packages/path_provider/path_provider_foundation/darwin/RunnerTests/RunnerTests.swift
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright 2013 The Flutter Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import XCTest
-
-@testable import path_provider_foundation
-
-#if os(iOS)
- import Flutter
-#elseif os(macOS)
- import FlutterMacOS
-#endif
-
-class RunnerTests: XCTestCase {
- func testGetTemporaryDirectory() throws {
- let plugin = PathProviderPlugin()
- let path = plugin.getDirectoryPath(type: .temp)
- XCTAssertEqual(
- path,
- NSSearchPathForDirectoriesInDomains(
- FileManager.SearchPathDirectory.cachesDirectory,
- FileManager.SearchPathDomainMask.userDomainMask,
- true
- ).first)
- }
-
- func testGetApplicationDocumentsDirectory() throws {
- let plugin = PathProviderPlugin()
- let path = plugin.getDirectoryPath(type: .applicationDocuments)
- XCTAssertEqual(
- path,
- NSSearchPathForDirectoriesInDomains(
- FileManager.SearchPathDirectory.documentDirectory,
- FileManager.SearchPathDomainMask.userDomainMask,
- true
- ).first)
- }
-
- func testGetApplicationSupportDirectory() throws {
- let plugin = PathProviderPlugin()
- let path = plugin.getDirectoryPath(type: .applicationSupport)
- #if os(iOS)
- // On iOS, the application support directory path should be just the system application
- // support path.
- XCTAssertEqual(
- path,
- NSSearchPathForDirectoriesInDomains(
- FileManager.SearchPathDirectory.applicationSupportDirectory,
- FileManager.SearchPathDomainMask.userDomainMask,
- true
- ).first)
- #else
- // On macOS, the application support directory path should be the system application
- // support path with an added subdirectory based on the app name.
- XCTAssert(
- path!.hasPrefix(
- NSSearchPathForDirectoriesInDomains(
- FileManager.SearchPathDirectory.applicationSupportDirectory,
- FileManager.SearchPathDomainMask.userDomainMask,
- true
- ).first!))
- XCTAssert(path!.hasSuffix("Example"))
- #endif
- }
-
- func testGetLibraryDirectory() throws {
- let plugin = PathProviderPlugin()
- let path = plugin.getDirectoryPath(type: .library)
- XCTAssertEqual(
- path,
- NSSearchPathForDirectoriesInDomains(
- FileManager.SearchPathDirectory.libraryDirectory,
- FileManager.SearchPathDomainMask.userDomainMask,
- true
- ).first)
- }
-
- func testGetDownloadsDirectory() throws {
- let plugin = PathProviderPlugin()
- let path = plugin.getDirectoryPath(type: .downloads)
- XCTAssertEqual(
- path,
- NSSearchPathForDirectoriesInDomains(
- FileManager.SearchPathDirectory.downloadsDirectory,
- FileManager.SearchPathDomainMask.userDomainMask,
- true
- ).first)
- }
-}
diff --git a/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation.podspec b/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation.podspec
deleted file mode 100644
index 5a20d27dfc6..00000000000
--- a/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation.podspec
+++ /dev/null
@@ -1,26 +0,0 @@
-#
-# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
-#
-Pod::Spec.new do |s|
- s.name = 'path_provider_foundation'
- s.version = '0.0.1'
- s.summary = 'An iOS and macOS implementation of the path_provider plugin.'
- s.description = <<-DESC
- An iOS and macOS implementation of the Flutter plugin for getting commonly used locations on the filesystem.
- DESC
- s.homepage = 'https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_foundation'
- s.license = { :type => 'BSD', :file => '../LICENSE' }
- s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
- s.source = { :http => 'https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_foundation' }
- s.source_files = 'path_provider_foundation/Sources/path_provider_foundation/**/*.swift'
- s.ios.dependency 'Flutter'
- s.osx.dependency 'FlutterMacOS'
- s.ios.deployment_target = '13.0'
- s.osx.deployment_target = '10.15'
- s.ios.xcconfig = {
- 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift',
- 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift',
- }
- s.swift_version = '5.0'
- s.resource_bundles = {'path_provider_foundation_privacy' => ['path_provider_foundation/Sources/path_provider_foundation/Resources/PrivacyInfo.xcprivacy']}
-end
diff --git a/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation/Package.swift b/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation/Package.swift
deleted file mode 100644
index 88e470e06a2..00000000000
--- a/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation/Package.swift
+++ /dev/null
@@ -1,28 +0,0 @@
-// swift-tools-version: 5.9
-
-// Copyright 2013 The Flutter Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import PackageDescription
-
-let package = Package(
- name: "path_provider_foundation",
- platforms: [
- .iOS("13.0"),
- .macOS("10.15"),
- ],
- products: [
- .library(name: "path-provider-foundation", targets: ["path_provider_foundation"])
- ],
- dependencies: [],
- targets: [
- .target(
- name: "path_provider_foundation",
- dependencies: [],
- resources: [
- .process("Resources")
- ]
- )
- ]
-)
diff --git a/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation/Sources/path_provider_foundation/PathProviderPlugin.swift b/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation/Sources/path_provider_foundation/PathProviderPlugin.swift
deleted file mode 100644
index 0dc61bc8d9a..00000000000
--- a/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation/Sources/path_provider_foundation/PathProviderPlugin.swift
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright 2013 The Flutter Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-import Foundation
-
-#if os(iOS)
- import Flutter
-#elseif os(macOS)
- import FlutterMacOS
-#endif
-
-public class PathProviderPlugin: NSObject, FlutterPlugin, PathProviderApi {
- public static func register(with registrar: FlutterPluginRegistrar) {
- let instance = PathProviderPlugin()
- // Workaround for https://github.com/flutter/flutter/issues/118103.
- #if os(iOS)
- let messenger = registrar.messenger()
- #else
- let messenger = registrar.messenger
- #endif
- PathProviderApiSetup.setUp(binaryMessenger: messenger, api: instance)
- }
-
- func getDirectoryPath(type: DirectoryType) -> String? {
- var path = getDirectory(ofType: fileManagerDirectoryForType(type))
- #if os(macOS)
- // In a non-sandboxed app, these are shared directories where applications are
- // expected to use its bundle ID as a subdirectory. (For non-sandboxed apps,
- // adding the extra path is harmless).
- // This is not done for iOS, for compatibility with older versions of the
- // plugin.
- if type == .applicationSupport || type == .applicationCache {
- if let basePath = path {
- let basePathURL = URL.init(fileURLWithPath: basePath)
- path = basePathURL.appendingPathComponent(Bundle.main.bundleIdentifier!).path
- }
- }
- #endif
- return path
- }
-
- // Returns the path for the container of the specified app group.
- func getContainerPath(appGroupIdentifier: String) -> String? {
- return FileManager.default.containerURL(
- forSecurityApplicationGroupIdentifier: appGroupIdentifier)?.path
- }
-}
-
-/// Returns the FileManager constant corresponding to the given type.
-private func fileManagerDirectoryForType(_ type: DirectoryType) -> FileManager.SearchPathDirectory {
- switch type {
- case .applicationCache:
- return FileManager.SearchPathDirectory.cachesDirectory
- case .applicationDocuments:
- return FileManager.SearchPathDirectory.documentDirectory
- case .applicationSupport:
- return FileManager.SearchPathDirectory.applicationSupportDirectory
- case .downloads:
- return FileManager.SearchPathDirectory.downloadsDirectory
- case .library:
- return FileManager.SearchPathDirectory.libraryDirectory
- case .temp:
- return FileManager.SearchPathDirectory.cachesDirectory
- }
-}
-
-/// Returns the user-domain directory of the given type.
-private func getDirectory(ofType directory: FileManager.SearchPathDirectory) -> String? {
- let paths = NSSearchPathForDirectoriesInDomains(
- directory,
- FileManager.SearchPathDomainMask.userDomainMask,
- true)
- return paths.first
-}
diff --git a/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation/Sources/path_provider_foundation/Resources/PrivacyInfo.xcprivacy b/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation/Sources/path_provider_foundation/Resources/PrivacyInfo.xcprivacy
deleted file mode 100644
index a34b7e2e60c..00000000000
--- a/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation/Sources/path_provider_foundation/Resources/PrivacyInfo.xcprivacy
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
- NSPrivacyTrackingDomains
-
- NSPrivacyAccessedAPITypes
-
- NSPrivacyCollectedDataTypes
-
- NSPrivacyTracking
-
-
-
diff --git a/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation/Sources/path_provider_foundation/messages.g.swift b/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation/Sources/path_provider_foundation/messages.g.swift
deleted file mode 100644
index 3c045880dc6..00000000000
--- a/packages/path_provider/path_provider_foundation/darwin/path_provider_foundation/Sources/path_provider_foundation/messages.g.swift
+++ /dev/null
@@ -1,171 +0,0 @@
-// Copyright 2013 The Flutter Authors
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-// Autogenerated from Pigeon (v26.1.0), do not edit directly.
-// See also: https://pub.dev/packages/pigeon
-
-import Foundation
-
-#if os(iOS)
- import Flutter
-#elseif os(macOS)
- import FlutterMacOS
-#else
- #error("Unsupported platform.")
-#endif
-
-/// Error class for passing custom error details to Dart side.
-final class PigeonError: Error {
- let code: String
- let message: String?
- let details: Sendable?
-
- init(code: String, message: String?, details: Sendable?) {
- self.code = code
- self.message = message
- self.details = details
- }
-
- var localizedDescription: String {
- return
- "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")"
- }
-}
-
-private func wrapResult(_ result: Any?) -> [Any?] {
- return [result]
-}
-
-private func wrapError(_ error: Any) -> [Any?] {
- if let pigeonError = error as? PigeonError {
- return [
- pigeonError.code,
- pigeonError.message,
- pigeonError.details,
- ]
- }
- if let flutterError = error as? FlutterError {
- return [
- flutterError.code,
- flutterError.message,
- flutterError.details,
- ]
- }
- return [
- "\(error)",
- "\(type(of: error))",
- "Stacktrace: \(Thread.callStackSymbols)",
- ]
-}
-
-private func isNullish(_ value: Any?) -> Bool {
- return value is NSNull || value == nil
-}
-
-private func nilOrValue(_ value: Any?) -> T? {
- if value is NSNull { return nil }
- return value as! T?
-}
-
-enum DirectoryType: Int {
- case applicationDocuments = 0
- case applicationSupport = 1
- case downloads = 2
- case library = 3
- case temp = 4
- case applicationCache = 5
-}
-
-private class MessagesPigeonCodecReader: FlutterStandardReader {
- override func readValue(ofType type: UInt8) -> Any? {
- switch type {
- case 129:
- let enumResultAsInt: Int? = nilOrValue(self.readValue() as! Int?)
- if let enumResultAsInt = enumResultAsInt {
- return DirectoryType(rawValue: enumResultAsInt)
- }
- return nil
- default:
- return super.readValue(ofType: type)
- }
- }
-}
-
-private class MessagesPigeonCodecWriter: FlutterStandardWriter {
- override func writeValue(_ value: Any) {
- if let value = value as? DirectoryType {
- super.writeByte(129)
- super.writeValue(value.rawValue)
- } else {
- super.writeValue(value)
- }
- }
-}
-
-private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter {
- override func reader(with data: Data) -> FlutterStandardReader {
- return MessagesPigeonCodecReader(data: data)
- }
-
- override func writer(with data: NSMutableData) -> FlutterStandardWriter {
- return MessagesPigeonCodecWriter(data: data)
- }
-}
-
-class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
- static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter())
-}
-
-/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
-protocol PathProviderApi {
- func getDirectoryPath(type: DirectoryType) throws -> String?
- func getContainerPath(appGroupIdentifier: String) throws -> String?
-}
-
-/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
-class PathProviderApiSetup {
- static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared }
- /// Sets up an instance of `PathProviderApi` to handle messages through the `binaryMessenger`.
- static func setUp(
- binaryMessenger: FlutterBinaryMessenger, api: PathProviderApi?,
- messageChannelSuffix: String = ""
- ) {
- let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
- let getDirectoryPathChannel = FlutterBasicMessageChannel(
- name:
- "dev.flutter.pigeon.path_provider_foundation.PathProviderApi.getDirectoryPath\(channelSuffix)",
- binaryMessenger: binaryMessenger, codec: codec)
- if let api = api {
- getDirectoryPathChannel.setMessageHandler { message, reply in
- let args = message as! [Any?]
- let typeArg = args[0] as! DirectoryType
- do {
- let result = try api.getDirectoryPath(type: typeArg)
- reply(wrapResult(result))
- } catch {
- reply(wrapError(error))
- }
- }
- } else {
- getDirectoryPathChannel.setMessageHandler(nil)
- }
- let getContainerPathChannel = FlutterBasicMessageChannel(
- name:
- "dev.flutter.pigeon.path_provider_foundation.PathProviderApi.getContainerPath\(channelSuffix)",
- binaryMessenger: binaryMessenger, codec: codec)
- if let api = api {
- getContainerPathChannel.setMessageHandler { message, reply in
- let args = message as! [Any?]
- let appGroupIdentifierArg = args[0] as! String
- do {
- let result = try api.getContainerPath(appGroupIdentifier: appGroupIdentifierArg)
- reply(wrapResult(result))
- } catch {
- reply(wrapError(error))
- }
- }
- } else {
- getContainerPathChannel.setMessageHandler(nil)
- }
- }
-}
diff --git a/packages/path_provider/path_provider_foundation/example/build.yaml b/packages/path_provider/path_provider_foundation/example/build.yaml
new file mode 100644
index 00000000000..ef6f032afd8
--- /dev/null
+++ b/packages/path_provider/path_provider_foundation/example/build.yaml
@@ -0,0 +1,13 @@
+targets:
+ $default:
+ sources:
+ - $package$
+ - lib/$lib$
+ - lib/**.dart
+ - test/**.dart
+ - integration_test/**.dart
+ builders:
+ mockito|mockBuilder:
+ generate_for:
+ - test/**.dart
+ - integration_test/**.dart
diff --git a/packages/path_provider/path_provider_foundation/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider_foundation/example/integration_test/path_provider_test.dart
index 456d274d45b..2bd88a2c98f 100644
--- a/packages/path_provider/path_provider_foundation/example/integration_test/path_provider_test.dart
+++ b/packages/path_provider/path_provider_foundation/example/integration_test/path_provider_test.dart
@@ -5,69 +5,418 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
-import 'package:path_provider_foundation/path_provider_foundation.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+import 'package:objective_c/objective_c.dart';
+import 'package:path/path.dart' as p;
+import 'package:path_provider_foundation/src/ffi_bindings.g.dart';
+import 'package:path_provider_foundation/src/path_provider_foundation_real.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
+import 'path_provider_test.mocks.dart';
+
+@GenerateNiceMocks(>[
+ MockSpec(),
+ MockSpec(),
+ MockSpec(),
+])
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
- testWidgets('getTemporaryDirectory', (WidgetTester tester) async {
- final PathProviderPlatform provider = PathProviderPlatform.instance;
- final String? result = await provider.getTemporaryPath();
- _verifySampleFile(result, 'temporaryDirectory');
- });
+ // This group contains standard integration tests that do end-to-end testing
+ // of the calls into the platform.
+ group('end-to-end', () {
+ testWidgets('getTemporaryDirectory', (WidgetTester tester) async {
+ final PathProviderPlatform provider = PathProviderPlatform.instance;
+ final String? result = await provider.getTemporaryPath();
+ _verifySampleFile(result, 'temporaryDirectory');
+ });
+
+ testWidgets('getApplicationDocumentsDirectory', (
+ WidgetTester tester,
+ ) async {
+ final PathProviderPlatform provider = PathProviderPlatform.instance;
+ final String? result = await provider.getApplicationDocumentsPath();
+ if (Platform.isMacOS) {
+ // _verifySampleFile causes hangs in driver when sandboxing is disabled
+ // because the path changes from an app specific directory to
+ // ~/Documents, which requires additional permissions to access on macOS.
+ // Instead, validate that a non-empty path was returned.
+ expect(result, isNotEmpty);
+ } else {
+ _verifySampleFile(result, 'applicationDocuments');
+ }
+ });
+
+ testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async {
+ final PathProviderPlatform provider = PathProviderPlatform.instance;
+ final String? result = await provider.getApplicationSupportPath();
+ _verifySampleFile(result, 'applicationSupport');
+ });
+
+ testWidgets('getApplicationCacheDirectory', (WidgetTester tester) async {
+ final PathProviderPlatform provider = PathProviderPlatform.instance;
+ final String? result = await provider.getApplicationCachePath();
+ _verifySampleFile(result, 'applicationCache');
+ });
+
+ testWidgets('getLibraryDirectory', (WidgetTester tester) async {
+ final PathProviderPlatform provider = PathProviderPlatform.instance;
+ final String? result = await provider.getLibraryPath();
+ _verifySampleFile(result, 'library');
+ });
- testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async {
- final PathProviderPlatform provider = PathProviderPlatform.instance;
- final String? result = await provider.getApplicationDocumentsPath();
- if (Platform.isMacOS) {
- // _verifySampleFile causes hangs in driver when sandboxing is disabled
- // because the path changes from an app specific directory to
- // ~/Documents, which requires additional permissions to access on macOS.
- // Instead, validate that a non-empty path was returned.
+ testWidgets('getDownloadsDirectory', (WidgetTester tester) async {
+ final PathProviderPlatform provider = PathProviderPlatform.instance;
+ final String? result = await provider.getDownloadsPath();
+ // _verifySampleFile causes hangs in driver for some reason, so just
+ // validate that a non-empty path was returned.
expect(result, isNotEmpty);
- } else {
- _verifySampleFile(result, 'applicationDocuments');
- }
- });
+ });
- testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async {
- final PathProviderPlatform provider = PathProviderPlatform.instance;
- final String? result = await provider.getApplicationSupportPath();
- _verifySampleFile(result, 'applicationSupport');
+ testWidgets('getContainerDirectory', (WidgetTester tester) async {
+ if (Platform.isIOS) {
+ final PathProviderFoundation provider = PathProviderFoundation();
+ final String? result = await provider.getContainerPath(
+ appGroupIdentifier: 'group.flutter.appGroupTest',
+ );
+ _verifySampleFile(result, 'appGroup');
+ }
+ });
});
- testWidgets('getApplicationCacheDirectory', (WidgetTester tester) async {
- final PathProviderPlatform provider = PathProviderPlatform.instance;
- final String? result = await provider.getApplicationCachePath();
- _verifySampleFile(result, 'applicationCache');
- });
+ // This group contains tests that would normally be Dart unit tests in the
+ // test/ directory, but can't be because they use Objective-C types (NSURL,
+ // NSString, etc.) that aren't available in an actual unit test. For these
+ // tests, the platform is stubbed out.
+ group('unit', () {
+ final ValueVariant platformVariants =
+ ValueVariant({
+ FakePlatformProvider(isIOS: true),
+ FakePlatformProvider(isMacOS: true),
+ });
- testWidgets('getLibraryDirectory', (WidgetTester tester) async {
- final PathProviderPlatform provider = PathProviderPlatform.instance;
- final String? result = await provider.getLibraryPath();
- _verifySampleFile(result, 'library');
- });
+ // These tests use the actual filesystem, since an injectable filesystem
+ // would add a runtime dependency to the package, so everything is contained
+ // to a temporary directory.
+ late Directory testRoot;
- testWidgets('getDownloadsDirectory', (WidgetTester tester) async {
- final PathProviderPlatform provider = PathProviderPlatform.instance;
- final String? result = await provider.getDownloadsPath();
- // _verifySampleFile causes hangs in driver for some reason, so just
- // validate that a non-empty path was returned.
- expect(result, isNotEmpty);
- });
+ setUp(() async {
+ testRoot = Directory.systemTemp.createTempSync();
+ });
+
+ tearDown(() {
+ testRoot.deleteSync(recursive: true);
+ });
+
+ testWidgets('getTemporaryPath iOS', (_) async {
+ final MockFoundationFFI mockFfiLib = MockFoundationFFI();
+ final PathProviderFoundation pathProvider = PathProviderFoundation(
+ ffiLib: mockFfiLib,
+ platform: FakePlatformProvider(isIOS: true),
+ );
+
+ final String temporaryPath = p.join(testRoot.path, 'temporary', 'path');
+ when(
+ mockFfiLib.NSSearchPathForDirectoriesInDomains(
+ NSSearchPathDirectory.NSCachesDirectory,
+ NSSearchPathDomainMask.NSUserDomainMask,
+ true,
+ ),
+ ).thenReturn(_arrayWithString(temporaryPath));
+
+ final String? path = await pathProvider.getTemporaryPath();
+
+ expect(path, temporaryPath);
+ });
+
+ testWidgets('getTemporaryPath macOS', (_) async {
+ final MockFoundationFFI mockFfiLib = MockFoundationFFI();
+ final PathProviderFoundation pathProvider = PathProviderFoundation(
+ ffiLib: mockFfiLib,
+ platform: FakePlatformProvider(isMacOS: true),
+ );
+
+ final String temporaryPath = p.join(testRoot.path, 'temporary', 'path');
+ when(
+ mockFfiLib.NSSearchPathForDirectoriesInDomains(
+ NSSearchPathDirectory.NSCachesDirectory,
+ NSSearchPathDomainMask.NSUserDomainMask,
+ true,
+ ),
+ ).thenReturn(_arrayWithString(temporaryPath));
+
+ final String? path = await pathProvider.getTemporaryPath();
+
+ // On macOS, the bundle ID should be appended to the path.
+ expect(path, '$temporaryPath/dev.flutter.plugins.pathProviderExample');
+ });
- testWidgets('getContainerDirectory', (WidgetTester tester) async {
- if (Platform.isIOS) {
- final PathProviderFoundation provider = PathProviderFoundation();
- final String? result = await provider.getContainerPath(
- appGroupIdentifier: 'group.flutter.appGroupTest',
+ testWidgets('getApplicationSupportPath iOS', (_) async {
+ final MockFoundationFFI mockFfiLib = MockFoundationFFI();
+ final PathProviderFoundation pathProvider = PathProviderFoundation(
+ ffiLib: mockFfiLib,
+ platform: FakePlatformProvider(isIOS: true),
);
- _verifySampleFile(result, 'appGroup');
- }
+
+ final String applicationSupportPath = p.join(
+ testRoot.path,
+ 'application',
+ 'support',
+ 'path',
+ );
+ when(
+ mockFfiLib.NSSearchPathForDirectoriesInDomains(
+ NSSearchPathDirectory.NSApplicationSupportDirectory,
+ NSSearchPathDomainMask.NSUserDomainMask,
+ true,
+ ),
+ ).thenReturn(_arrayWithString(applicationSupportPath));
+
+ final String? path = await pathProvider.getApplicationSupportPath();
+
+ expect(path, applicationSupportPath);
+ });
+
+ testWidgets('getApplicationSupportPath macOS', (_) async {
+ final MockFoundationFFI mockFfiLib = MockFoundationFFI();
+ final PathProviderFoundation pathProvider = PathProviderFoundation(
+ ffiLib: mockFfiLib,
+ platform: FakePlatformProvider(isMacOS: true),
+ );
+
+ final String applicationSupportPath = p.join(
+ testRoot.path,
+ 'application',
+ 'support',
+ 'path',
+ );
+ when(
+ mockFfiLib.NSSearchPathForDirectoriesInDomains(
+ NSSearchPathDirectory.NSApplicationSupportDirectory,
+ NSSearchPathDomainMask.NSUserDomainMask,
+ true,
+ ),
+ ).thenReturn(_arrayWithString(applicationSupportPath));
+
+ final String? path = await pathProvider.getApplicationSupportPath();
+
+ // On macOS, the bundle ID should be appended to the path.
+ expect(
+ path,
+ '$applicationSupportPath/dev.flutter.plugins.pathProviderExample',
+ );
+ });
+
+ testWidgets(
+ 'getApplicationSupportPath creates the directory if necessary',
+ (_) async {
+ final MockFoundationFFI mockFfiLib = MockFoundationFFI();
+ final PathProviderFoundation pathProvider = PathProviderFoundation(
+ ffiLib: mockFfiLib,
+ platform: platformVariants.currentValue,
+ );
+
+ final String applicationSupportPath = p.join(
+ testRoot.path,
+ 'application',
+ 'support',
+ 'path',
+ );
+ when(
+ mockFfiLib.NSSearchPathForDirectoriesInDomains(
+ NSSearchPathDirectory.NSApplicationSupportDirectory,
+ NSSearchPathDomainMask.NSUserDomainMask,
+ true,
+ ),
+ ).thenReturn(_arrayWithString(applicationSupportPath));
+
+ final String? path = await pathProvider.getApplicationSupportPath();
+
+ expect(Directory(path!).existsSync(), isTrue);
+ },
+ variant: platformVariants,
+ );
+
+ testWidgets('getLibraryPath', (_) async {
+ final MockFoundationFFI mockFfiLib = MockFoundationFFI();
+ final PathProviderFoundation pathProvider = PathProviderFoundation(
+ ffiLib: mockFfiLib,
+ platform: platformVariants.currentValue,
+ );
+
+ final String libraryPath = p.join(testRoot.path, 'library', 'path');
+ when(
+ mockFfiLib.NSSearchPathForDirectoriesInDomains(
+ NSSearchPathDirectory.NSLibraryDirectory,
+ NSSearchPathDomainMask.NSUserDomainMask,
+ true,
+ ),
+ ).thenReturn(_arrayWithString(libraryPath));
+
+ final String? path = await pathProvider.getLibraryPath();
+
+ expect(path, libraryPath);
+ }, variant: platformVariants);
+
+ testWidgets('getApplicationDocumentsPath', (_) async {
+ final MockFoundationFFI mockFfiLib = MockFoundationFFI();
+ final PathProviderFoundation pathProvider = PathProviderFoundation(
+ ffiLib: mockFfiLib,
+ platform: platformVariants.currentValue,
+ );
+
+ final String applicationDocumentsPath = p.join(
+ testRoot.path,
+ 'application',
+ 'documents',
+ 'path',
+ );
+ when(
+ mockFfiLib.NSSearchPathForDirectoriesInDomains(
+ NSSearchPathDirectory.NSDocumentDirectory,
+ NSSearchPathDomainMask.NSUserDomainMask,
+ true,
+ ),
+ ).thenReturn(_arrayWithString(applicationDocumentsPath));
+
+ final String? path = await pathProvider.getApplicationDocumentsPath();
+
+ expect(path, applicationDocumentsPath);
+ }, variant: platformVariants);
+
+ testWidgets('getApplicationCachePath iOS', (_) async {
+ final MockFoundationFFI mockFfiLib = MockFoundationFFI();
+ final PathProviderFoundation pathProvider = PathProviderFoundation(
+ ffiLib: mockFfiLib,
+ platform: FakePlatformProvider(isIOS: true),
+ );
+
+ final String applicationCachePath = p.join(
+ testRoot.path,
+ 'application',
+ 'cache',
+ 'path',
+ );
+ when(
+ mockFfiLib.NSSearchPathForDirectoriesInDomains(
+ NSSearchPathDirectory.NSCachesDirectory,
+ NSSearchPathDomainMask.NSUserDomainMask,
+ true,
+ ),
+ ).thenReturn(_arrayWithString(applicationCachePath));
+
+ final String? path = await pathProvider.getApplicationCachePath();
+
+ expect(path, applicationCachePath);
+ });
+
+ testWidgets('getApplicationCachePath macOS', (_) async {
+ final MockFoundationFFI mockFfiLib = MockFoundationFFI();
+ final PathProviderFoundation pathProvider = PathProviderFoundation(
+ ffiLib: mockFfiLib,
+ platform: FakePlatformProvider(isMacOS: true),
+ );
+
+ final String applicationCachePath = p.join(
+ testRoot.path,
+ 'application',
+ 'cache',
+ 'path',
+ );
+ when(
+ mockFfiLib.NSSearchPathForDirectoriesInDomains(
+ NSSearchPathDirectory.NSCachesDirectory,
+ NSSearchPathDomainMask.NSUserDomainMask,
+ true,
+ ),
+ ).thenReturn(_arrayWithString(applicationCachePath));
+
+ final String? path = await pathProvider.getApplicationCachePath();
+
+ // On macOS, the bundle ID should be appended to the path.
+ expect(
+ path,
+ '$applicationCachePath/dev.flutter.plugins.pathProviderExample',
+ );
+ });
+
+ testWidgets(
+ 'getApplicationCachePath creates the directory if necessary',
+ (_) async {
+ final MockFoundationFFI mockFfiLib = MockFoundationFFI();
+ final PathProviderFoundation pathProvider = PathProviderFoundation(
+ ffiLib: mockFfiLib,
+ platform: platformVariants.currentValue,
+ );
+
+ final String applicationCachePath = p.join(
+ testRoot.path,
+ 'application',
+ 'cache',
+ 'path',
+ );
+ when(
+ mockFfiLib.NSSearchPathForDirectoriesInDomains(
+ NSSearchPathDirectory.NSCachesDirectory,
+ NSSearchPathDomainMask.NSUserDomainMask,
+ true,
+ ),
+ ).thenReturn(_arrayWithString(applicationCachePath));
+
+ final String? path = await pathProvider.getApplicationCachePath();
+
+ expect(Directory(path!).existsSync(), isTrue);
+ },
+ variant: platformVariants,
+ );
+
+ testWidgets('getDownloadsPath', (_) async {
+ final MockFoundationFFI mockFfiLib = MockFoundationFFI();
+ final PathProviderFoundation pathProvider = PathProviderFoundation(
+ ffiLib: mockFfiLib,
+ platform: platformVariants.currentValue,
+ );
+
+ final String downloadsPath = p.join(testRoot.path, 'downloads', 'path');
+ when(
+ mockFfiLib.NSSearchPathForDirectoriesInDomains(
+ NSSearchPathDirectory.NSDownloadsDirectory,
+ NSSearchPathDomainMask.NSUserDomainMask,
+ true,
+ ),
+ ).thenReturn(_arrayWithString(downloadsPath));
+
+ final String? result = await pathProvider.getDownloadsPath();
+
+ expect(result, downloadsPath);
+ }, variant: platformVariants);
+
+ testWidgets('getContainerPath', (_) async {
+ final String containerPath = p.join(testRoot.path, 'container', 'path');
+ final NSURL containerUrl = NSURL.fileURLWithPath(NSString(containerPath));
+
+ final MockFoundationFFI mockFfiLib = MockFoundationFFI();
+ final PathProviderFoundation pathProvider = PathProviderFoundation(
+ ffiLib: mockFfiLib,
+ containerURLForSecurityApplicationGroupIdentifier: (_) => containerUrl,
+ platform: FakePlatformProvider(isIOS: true),
+ );
+
+ const String appGroupIdentifier = 'group.example.test';
+ final String? result = await pathProvider.getContainerPath(
+ appGroupIdentifier: appGroupIdentifier,
+ );
+
+ expect(result, containerPath);
+ });
});
}
+NSArray _arrayWithString(String s) {
+ return NSArray.arrayWithObject(NSString(s));
+}
+
/// Verify a file called [name] in [directoryPath] by recreating it with test
/// contents when necessary.
///
@@ -90,3 +439,17 @@ void _verifySampleFile(String? directoryPath, String name) {
expect(directory.listSync(), isNotEmpty);
file.deleteSync();
}
+
+/// Fake implementation of PathProviderPlatformProvider.
+class FakePlatformProvider implements PathProviderPlatformProvider {
+ FakePlatformProvider({this.isIOS = false, this.isMacOS = false})
+ : assert(isIOS != isMacOS);
+ @override
+ bool isIOS;
+
+ @override
+ bool isMacOS;
+
+ @override
+ String toString() => isIOS ? 'iOS' : 'macOS';
+}
diff --git a/packages/path_provider/path_provider_foundation/example/integration_test/path_provider_test.mocks.dart b/packages/path_provider/path_provider_foundation/example/integration_test/path_provider_test.mocks.dart
new file mode 100644
index 00000000000..24980d636be
--- /dev/null
+++ b/packages/path_provider/path_provider_foundation/example/integration_test/path_provider_test.mocks.dart
@@ -0,0 +1,111 @@
+// Mocks generated by Mockito 5.4.6 from annotations
+// in path_provider_example/integration_test/path_provider_test.dart.
+// Do not manually edit this file.
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'package:mockito/mockito.dart' as _i1;
+import 'package:mockito/src/dummies.dart' as _i4;
+import 'package:objective_c/objective_c.dart' as _i2;
+import 'package:path_provider_foundation/src/ffi_bindings.g.dart' as _i3;
+import 'package:path_provider_foundation/src/path_provider_foundation_real.dart'
+ as _i5;
+
+// ignore_for_file: type=lint
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: deprecated_member_use
+// ignore_for_file: deprecated_member_use_from_same_package
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: must_be_immutable
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: camel_case_types
+// ignore_for_file: subtype_of_sealed_class
+// ignore_for_file: invalid_use_of_internal_member
+
+class _FakeObjCObject_0 extends _i1.SmartFake implements _i2.ObjCObject {
+ _FakeObjCObject_0(Object parent, Invocation parentInvocation)
+ : super(parent, parentInvocation);
+}
+
+/// A class which mocks [FoundationFFI].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockFoundationFFI extends _i1.Mock implements _i3.FoundationFFI {
+ @override
+ _i2.NSArray NSSearchPathForDirectoriesInDomains(
+ _i3.NSSearchPathDirectory? directory,
+ int? domainMask,
+ bool? expandTilde,
+ ) =>
+ (super.noSuchMethod(
+ Invocation.method(#NSSearchPathForDirectoriesInDomains, [
+ directory,
+ domainMask,
+ expandTilde,
+ ]),
+ returnValue: _FakeObjCObject_0(
+ this,
+ Invocation.method(#NSSearchPathForDirectoriesInDomains, [
+ directory,
+ domainMask,
+ expandTilde,
+ ]),
+ ),
+ returnValueForMissingStub: _FakeObjCObject_0(
+ this,
+ Invocation.method(#NSSearchPathForDirectoriesInDomains, [
+ directory,
+ domainMask,
+ expandTilde,
+ ]),
+ ),
+ )
+ as _i2.NSArray);
+}
+
+/// A class which mocks [ObjCObject].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockObjCObject extends _i1.Mock implements _i2.ObjCObject {
+ @override
+ _i2.ObjCObjectRef get ref =>
+ (super.noSuchMethod(
+ Invocation.getter(#ref),
+ returnValue: _i4.dummyValue<_i2.ObjCObjectRef>(
+ this,
+ Invocation.getter(#ref),
+ ),
+ returnValueForMissingStub: _i4.dummyValue<_i2.ObjCObjectRef>(
+ this,
+ Invocation.getter(#ref),
+ ),
+ )
+ as _i2.ObjCObjectRef);
+}
+
+/// A class which mocks [PathProviderPlatformProvider].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockPathProviderPlatformProvider extends _i1.Mock
+ implements _i5.PathProviderPlatformProvider {
+ @override
+ bool get isIOS =>
+ (super.noSuchMethod(
+ Invocation.getter(#isIOS),
+ returnValue: false,
+ returnValueForMissingStub: false,
+ )
+ as bool);
+
+ @override
+ bool get isMacOS =>
+ (super.noSuchMethod(
+ Invocation.getter(#isMacOS),
+ returnValue: false,
+ returnValueForMissingStub: false,
+ )
+ as bool);
+}
diff --git a/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.pbxproj
index 16c8997bc44..ea6743595e0 100644
--- a/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.pbxproj
@@ -53,6 +53,7 @@
5DB8EF5A2759054360D79B8D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
86F7986E9DC17432CC8AE464 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
91DA83C3D33EB641BAEA3087 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
@@ -101,6 +102,7 @@
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
+ 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@@ -201,6 +203,7 @@
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ D4AF0CAAE697EF439AFEC08C /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -353,6 +356,23 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
+ D4AF0CAAE697EF439AFEC08C /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -467,7 +487,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.example.pathProviderFoundationExample;
+ PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.pathProviderExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -641,7 +661,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.example.pathProviderFoundationExample;
+ PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.pathProviderExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -664,7 +684,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.example.pathProviderFoundationExample;
+ PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.pathProviderExample;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
diff --git a/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 3ea14b59077..a02dac9162d 100644
--- a/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -44,6 +44,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
pigeonChannelCodec = _PigeonCodec();
-
- final String pigeonVar_messageChannelSuffix;
-
- Future getDirectoryPath(DirectoryType type) async {
- final String pigeonVar_channelName =
- 'dev.flutter.pigeon.path_provider_foundation.PathProviderApi.getDirectoryPath$pigeonVar_messageChannelSuffix';
- final BasicMessageChannel