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 pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [type], - ); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return (pigeonVar_replyList[0] as String?); - } - } - - Future getContainerPath(String appGroupIdentifier) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.path_provider_foundation.PathProviderApi.getContainerPath$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [appGroupIdentifier], - ); - final List? pigeonVar_replyList = - await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return (pigeonVar_replyList[0] as String?); - } - } -} diff --git a/packages/path_provider/path_provider_foundation/lib/path_provider_foundation.dart b/packages/path_provider/path_provider_foundation/lib/path_provider_foundation.dart index 64a92c5cf59..91b60fca355 100644 --- a/packages/path_provider/path_provider_foundation/lib/path_provider_foundation.dart +++ b/packages/path_provider/path_provider_foundation/lib/path_provider_foundation.dart @@ -2,114 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; - -import 'messages.g.dart'; - -/// The iOS and macOS implementation of [PathProviderPlatform]. -class PathProviderFoundation extends PathProviderPlatform { - /// Constructor that accepts a testable PathProviderPlatformProvider. - PathProviderFoundation({ - @visibleForTesting PathProviderPlatformProvider? platform, - @visibleForTesting PathProviderApi? pathProviderApi, - }) : _platformProvider = platform ?? PathProviderPlatformProvider(), - _pathProvider = pathProviderApi ?? PathProviderApi(); - - final PathProviderPlatformProvider _platformProvider; - final PathProviderApi _pathProvider; - - /// Registers this class as the default instance of [PathProviderPlatform] - static void registerWith() { - PathProviderPlatform.instance = PathProviderFoundation(); - } - - @override - Future getTemporaryPath() { - return _pathProvider.getDirectoryPath(DirectoryType.temp); - } - - @override - Future getApplicationSupportPath() async { - final String? path = await _pathProvider.getDirectoryPath( - DirectoryType.applicationSupport, - ); - if (path != null) { - // Ensure the directory exists before returning it, for consistency with - // other platforms. - await Directory(path).create(recursive: true); - } - return path; - } - - @override - Future getLibraryPath() { - return _pathProvider.getDirectoryPath(DirectoryType.library); - } - - @override - Future getApplicationDocumentsPath() { - return _pathProvider.getDirectoryPath(DirectoryType.applicationDocuments); - } - - @override - Future getApplicationCachePath() async { - final String? path = await _pathProvider.getDirectoryPath( - DirectoryType.applicationCache, - ); - if (path != null) { - // Ensure the directory exists before returning it, for consistency with - // other platforms. - await Directory(path).create(recursive: true); - } - return path; - } - - @override - Future getExternalStoragePath() async { - throw UnsupportedError( - 'getExternalStoragePath is not supported on this platform', - ); - } - - @override - Future?> getExternalCachePaths() async { - throw UnsupportedError( - 'getExternalCachePaths is not supported on this platform', - ); - } - - @override - Future?> getExternalStoragePaths({ - StorageDirectory? type, - }) async { - throw UnsupportedError( - 'getExternalStoragePaths is not supported on this platform', - ); - } - - @override - Future getDownloadsPath() { - return _pathProvider.getDirectoryPath(DirectoryType.downloads); - } - - /// Returns the path to the container of the specified App Group. - /// This is only supported for iOS. - Future getContainerPath({required String appGroupIdentifier}) async { - if (!_platformProvider.isIOS) { - throw UnsupportedError( - 'getContainerPath is not supported on this platform', - ); - } - return _pathProvider.getContainerPath(appGroupIdentifier); - } -} - -/// Helper class for returning information about the current platform. -@visibleForTesting -class PathProviderPlatformProvider { - /// Specifies whether the current platform is iOS. - bool get isIOS => Platform.isIOS; -} +// path_provider_foundation is implemented using FFI; export a stub for +// platforms that don't support FFI (e.g., web) to avoid having transitive +// dependencies break web compilation. +export 'src/path_provider_foundation_stub.dart' + if (dart.library.ffi) 'src/path_provider_foundation_real.dart'; diff --git a/packages/path_provider/path_provider_foundation/lib/src/ffi_bindings.g.dart b/packages/path_provider/path_provider_foundation/lib/src/ffi_bindings.g.dart new file mode 100644 index 00000000000..e0e01ffa9f0 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/lib/src/ffi_bindings.g.dart @@ -0,0 +1,509 @@ +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint, unused_import +import 'dart:ffi' as ffi; +import 'package:objective_c/objective_c.dart' as objc; +import 'package:ffi/ffi.dart' as pkg_ffi; + +/// Bindings for NSFileManager. +class FoundationFFI { + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + FoundationFFI(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + FoundationFFI.fromLookup( + ffi.Pointer Function(String symbolName) lookup, + ) : _lookup = lookup; + + objc.NSArray NSSearchPathForDirectoriesInDomains( + NSSearchPathDirectory directory, + int domainMask, + bool expandTilde, + ) { + return objc.NSArray.fromPointer( + _NSSearchPathForDirectoriesInDomains( + directory.value, + domainMask, + expandTilde, + ), + retain: true, + release: true, + ); + } + + late final _NSSearchPathForDirectoriesInDomainsPtr = + _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.UnsignedLong, + ffi.UnsignedLong, + ffi.Bool, + ) + > + >('NSSearchPathForDirectoriesInDomains'); + late final _NSSearchPathForDirectoriesInDomains = + _NSSearchPathForDirectoriesInDomainsPtr.asFunction< + ffi.Pointer Function(int, int, bool) + >(); +} + +enum NSSearchPathDirectory { + NSApplicationDirectory(1), + NSDemoApplicationDirectory(2), + NSDeveloperApplicationDirectory(3), + NSAdminApplicationDirectory(4), + NSLibraryDirectory(5), + NSDeveloperDirectory(6), + NSUserDirectory(7), + NSDocumentationDirectory(8), + NSDocumentDirectory(9), + NSCoreServiceDirectory(10), + NSAutosavedInformationDirectory(11), + NSDesktopDirectory(12), + NSCachesDirectory(13), + NSApplicationSupportDirectory(14), + NSDownloadsDirectory(15), + NSInputMethodsDirectory(16), + NSMoviesDirectory(17), + NSMusicDirectory(18), + NSPicturesDirectory(19), + NSPrinterDescriptionDirectory(20), + NSSharedPublicDirectory(21), + NSPreferencePanesDirectory(22), + NSApplicationScriptsDirectory(23), + NSItemReplacementDirectory(99), + NSAllApplicationsDirectory(100), + NSAllLibrariesDirectory(101), + NSTrashDirectory(102); + + final int value; + const NSSearchPathDirectory(this.value); + + static NSSearchPathDirectory fromValue(int value) => switch (value) { + 1 => NSApplicationDirectory, + 2 => NSDemoApplicationDirectory, + 3 => NSDeveloperApplicationDirectory, + 4 => NSAdminApplicationDirectory, + 5 => NSLibraryDirectory, + 6 => NSDeveloperDirectory, + 7 => NSUserDirectory, + 8 => NSDocumentationDirectory, + 9 => NSDocumentDirectory, + 10 => NSCoreServiceDirectory, + 11 => NSAutosavedInformationDirectory, + 12 => NSDesktopDirectory, + 13 => NSCachesDirectory, + 14 => NSApplicationSupportDirectory, + 15 => NSDownloadsDirectory, + 16 => NSInputMethodsDirectory, + 17 => NSMoviesDirectory, + 18 => NSMusicDirectory, + 19 => NSPicturesDirectory, + 20 => NSPrinterDescriptionDirectory, + 21 => NSSharedPublicDirectory, + 22 => NSPreferencePanesDirectory, + 23 => NSApplicationScriptsDirectory, + 99 => NSItemReplacementDirectory, + 100 => NSAllApplicationsDirectory, + 101 => NSAllLibrariesDirectory, + 102 => NSTrashDirectory, + _ => throw ArgumentError('Unknown value for NSSearchPathDirectory: $value'), + }; +} + +sealed class NSSearchPathDomainMask { + static const NSUserDomainMask = 1; + static const NSLocalDomainMask = 2; + static const NSNetworkDomainMask = 4; + static const NSSystemDomainMask = 8; + static const NSAllDomainsMask = 65535; +} + +late final _class_NSURL = objc.getClass("NSURL"); +late final _sel_fileURLWithPathComponents_ = objc.registerName( + "fileURLWithPathComponents:", +); +final _objc_msgSend_1sotr3r = objc.msgSendPointer + .cast< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ) + > + >() + .asFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ) + >(); +late final _sel_pathComponents = objc.registerName("pathComponents"); +final _objc_msgSend_151sglz = objc.msgSendPointer + .cast< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ) + > + >() + .asFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ) + >(); +late final _sel_lastPathComponent = objc.registerName("lastPathComponent"); +late final _sel_pathExtension = objc.registerName("pathExtension"); +late final _sel_URLByAppendingPathComponent_ = objc.registerName( + "URLByAppendingPathComponent:", +); +late final _sel_URLByAppendingPathComponent_isDirectory_ = objc.registerName( + "URLByAppendingPathComponent:isDirectory:", +); +final _objc_msgSend_17amj0z = objc.msgSendPointer + .cast< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Bool, + ) + > + >() + .asFunction< + ffi.Pointer Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + bool, + ) + >(); +late final _sel_URLByDeletingLastPathComponent = objc.registerName( + "URLByDeletingLastPathComponent", +); +late final _sel_URLByAppendingPathExtension_ = objc.registerName( + "URLByAppendingPathExtension:", +); +late final _sel_URLByDeletingPathExtension = objc.registerName( + "URLByDeletingPathExtension", +); +late final _sel_checkResourceIsReachableAndReturnError_ = objc.registerName( + "checkResourceIsReachableAndReturnError:", +); +final _objc_msgSend_1dom33q = objc.msgSendPointer + .cast< + ffi.NativeFunction< + ffi.Bool Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>, + ) + > + >() + .asFunction< + bool Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer>, + ) + >(); +late final _sel_URLByStandardizingPath = objc.registerName( + "URLByStandardizingPath", +); +late final _sel_URLByResolvingSymlinksInPath = objc.registerName( + "URLByResolvingSymlinksInPath", +); + +/// NSURLPathUtilities +extension NSURLPathUtilities on objc.NSURL { + /// URLByAppendingPathComponent: + objc.NSURL? URLByAppendingPathComponent(objc.NSString pathComponent) { + objc.checkOsVersionInternal( + 'NSURL.URLByAppendingPathComponent:', + iOS: (false, (4, 0, 0)), + macOS: (false, (10, 6, 0)), + ); + final $ret = _objc_msgSend_1sotr3r( + object$.ref.pointer, + _sel_URLByAppendingPathComponent_, + pathComponent.ref.pointer, + ); + return $ret.address == 0 + ? null + : objc.NSURL.fromPointer($ret, retain: true, release: true); + } + + /// URLByAppendingPathComponent:isDirectory: + objc.NSURL? URLByAppendingPathComponent$1( + objc.NSString pathComponent, { + required bool isDirectory, + }) { + objc.checkOsVersionInternal( + 'NSURL.URLByAppendingPathComponent:isDirectory:', + iOS: (false, (5, 0, 0)), + macOS: (false, (10, 7, 0)), + ); + final $ret = _objc_msgSend_17amj0z( + object$.ref.pointer, + _sel_URLByAppendingPathComponent_isDirectory_, + pathComponent.ref.pointer, + isDirectory, + ); + return $ret.address == 0 + ? null + : objc.NSURL.fromPointer($ret, retain: true, release: true); + } + + /// URLByAppendingPathExtension: + objc.NSURL? URLByAppendingPathExtension(objc.NSString pathExtension) { + objc.checkOsVersionInternal( + 'NSURL.URLByAppendingPathExtension:', + iOS: (false, (4, 0, 0)), + macOS: (false, (10, 6, 0)), + ); + final $ret = _objc_msgSend_1sotr3r( + object$.ref.pointer, + _sel_URLByAppendingPathExtension_, + pathExtension.ref.pointer, + ); + return $ret.address == 0 + ? null + : objc.NSURL.fromPointer($ret, retain: true, release: true); + } + + /// URLByDeletingLastPathComponent + objc.NSURL? get URLByDeletingLastPathComponent { + objc.checkOsVersionInternal( + 'NSURL.URLByDeletingLastPathComponent', + iOS: (false, (4, 0, 0)), + macOS: (false, (10, 6, 0)), + ); + final $ret = _objc_msgSend_151sglz( + object$.ref.pointer, + _sel_URLByDeletingLastPathComponent, + ); + return $ret.address == 0 + ? null + : objc.NSURL.fromPointer($ret, retain: true, release: true); + } + + /// URLByDeletingPathExtension + objc.NSURL? get URLByDeletingPathExtension { + objc.checkOsVersionInternal( + 'NSURL.URLByDeletingPathExtension', + iOS: (false, (4, 0, 0)), + macOS: (false, (10, 6, 0)), + ); + final $ret = _objc_msgSend_151sglz( + object$.ref.pointer, + _sel_URLByDeletingPathExtension, + ); + return $ret.address == 0 + ? null + : objc.NSURL.fromPointer($ret, retain: true, release: true); + } + + /// URLByResolvingSymlinksInPath + objc.NSURL? get URLByResolvingSymlinksInPath { + objc.checkOsVersionInternal( + 'NSURL.URLByResolvingSymlinksInPath', + iOS: (false, (4, 0, 0)), + macOS: (false, (10, 6, 0)), + ); + final $ret = _objc_msgSend_151sglz( + object$.ref.pointer, + _sel_URLByResolvingSymlinksInPath, + ); + return $ret.address == 0 + ? null + : objc.NSURL.fromPointer($ret, retain: true, release: true); + } + + /// URLByStandardizingPath + objc.NSURL? get URLByStandardizingPath { + objc.checkOsVersionInternal( + 'NSURL.URLByStandardizingPath', + iOS: (false, (4, 0, 0)), + macOS: (false, (10, 6, 0)), + ); + final $ret = _objc_msgSend_151sglz( + object$.ref.pointer, + _sel_URLByStandardizingPath, + ); + return $ret.address == 0 + ? null + : objc.NSURL.fromPointer($ret, retain: true, release: true); + } + + /// checkResourceIsReachableAndReturnError: + bool checkResourceIsReachableAndReturnError() { + objc.checkOsVersionInternal( + 'NSURL.checkResourceIsReachableAndReturnError:', + iOS: (false, (4, 0, 0)), + macOS: (false, (10, 6, 0)), + ); + final $err = pkg_ffi.calloc>(); + try { + final $ret = _objc_msgSend_1dom33q( + object$.ref.pointer, + _sel_checkResourceIsReachableAndReturnError_, + $err, + ); + objc.NSErrorException.checkErrorPointer($err.value); + return $ret; + } finally { + pkg_ffi.calloc.free($err); + } + } + + /// lastPathComponent + objc.NSString? get lastPathComponent { + objc.checkOsVersionInternal( + 'NSURL.lastPathComponent', + iOS: (false, (4, 0, 0)), + macOS: (false, (10, 6, 0)), + ); + final $ret = _objc_msgSend_151sglz( + object$.ref.pointer, + _sel_lastPathComponent, + ); + return $ret.address == 0 + ? null + : objc.NSString.fromPointer($ret, retain: true, release: true); + } + + /// pathComponents + objc.NSArray? get pathComponents { + objc.checkOsVersionInternal( + 'NSURL.pathComponents', + iOS: (false, (4, 0, 0)), + macOS: (false, (10, 6, 0)), + ); + final $ret = _objc_msgSend_151sglz( + object$.ref.pointer, + _sel_pathComponents, + ); + return $ret.address == 0 + ? null + : objc.NSArray.fromPointer($ret, retain: true, release: true); + } + + /// pathExtension + objc.NSString? get pathExtension { + objc.checkOsVersionInternal( + 'NSURL.pathExtension', + iOS: (false, (4, 0, 0)), + macOS: (false, (10, 6, 0)), + ); + final $ret = _objc_msgSend_151sglz(object$.ref.pointer, _sel_pathExtension); + return $ret.address == 0 + ? null + : objc.NSString.fromPointer($ret, retain: true, release: true); + } + + /// fileURLWithPathComponents: + static objc.NSURL? fileURLWithPathComponents(objc.NSArray components) { + objc.checkOsVersionInternal( + 'NSURL.fileURLWithPathComponents:', + iOS: (false, (4, 0, 0)), + macOS: (false, (10, 6, 0)), + ); + final $ret = _objc_msgSend_1sotr3r( + _class_NSURL, + _sel_fileURLWithPathComponents_, + components.ref.pointer, + ); + return $ret.address == 0 + ? null + : objc.NSURL.fromPointer($ret, retain: true, release: true); + } +} + +late final _class_NSFileManager = objc.getClass("NSFileManager"); +late final _sel_isKindOfClass_ = objc.registerName("isKindOfClass:"); +final _objc_msgSend_19nvye5 = objc.msgSendPointer + .cast< + ffi.NativeFunction< + ffi.Bool Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ) + > + >() + .asFunction< + bool Function( + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ) + >(); +late final _sel_defaultManager = objc.registerName("defaultManager"); +late final _sel_containerURLForSecurityApplicationGroupIdentifier_ = objc + .registerName("containerURLForSecurityApplicationGroupIdentifier:"); + +/// NSFileManager +extension type NSFileManager._(objc.ObjCObject object$) + implements objc.ObjCObject, objc.NSObject { + /// Constructs a [NSFileManager] that points to the same underlying object as [other]. + NSFileManager.as(objc.ObjCObject other) : object$ = other { + assert(isA(object$)); + } + + /// Constructs a [NSFileManager] that wraps the given raw object pointer. + NSFileManager.fromPointer( + ffi.Pointer other, { + bool retain = false, + bool release = false, + }) : object$ = objc.ObjCObject(other, retain: retain, release: release) { + assert(isA(object$)); + } + + /// Returns whether [obj] is an instance of [NSFileManager]. + static bool isA(objc.ObjCObject obj) => _objc_msgSend_19nvye5( + obj.ref.pointer, + _sel_isKindOfClass_, + _class_NSFileManager, + ); + + /// defaultManager + static NSFileManager getDefaultManager() { + final $ret = _objc_msgSend_151sglz( + _class_NSFileManager, + _sel_defaultManager, + ); + return NSFileManager.fromPointer($ret, retain: true, release: true); + } +} + +extension NSFileManager$Methods on NSFileManager { + /// containerURLForSecurityApplicationGroupIdentifier: + objc.NSURL? containerURLForSecurityApplicationGroupIdentifier( + objc.NSString groupIdentifier, + ) { + objc.checkOsVersionInternal( + 'NSFileManager.containerURLForSecurityApplicationGroupIdentifier:', + iOS: (false, (7, 0, 0)), + macOS: (false, (10, 8, 0)), + ); + final $ret = _objc_msgSend_1sotr3r( + object$.ref.pointer, + _sel_containerURLForSecurityApplicationGroupIdentifier_, + groupIdentifier.ref.pointer, + ); + return $ret.address == 0 + ? null + : objc.NSURL.fromPointer($ret, retain: true, release: true); + } +} diff --git a/packages/path_provider/path_provider_foundation/lib/src/path_provider_foundation_real.dart b/packages/path_provider/path_provider_foundation/lib/src/path_provider_foundation_real.dart new file mode 100644 index 00000000000..8be02bac2f7 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/lib/src/path_provider_foundation_real.dart @@ -0,0 +1,180 @@ +// 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 'dart:ffi' as ffi; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:objective_c/objective_c.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; + +import 'ffi_bindings.g.dart'; + +/// The iOS and macOS implementation of [PathProviderPlatform]. +class PathProviderFoundation extends PathProviderPlatform { + /// Constructor that accepts a testable PathProviderPlatformProvider. + PathProviderFoundation({ + @visibleForTesting PathProviderPlatformProvider? platform, + @visibleForTesting FoundationFFI? ffiLib, + @visibleForTesting + NSURL? Function(NSString)? + containerURLForSecurityApplicationGroupIdentifier, + }) : _platformProvider = platform ?? PathProviderPlatformProvider(), + _ffiLib = ffiLib ?? _lib, + _containerURLForSecurityApplicationGroupIdentifier = + containerURLForSecurityApplicationGroupIdentifier ?? + _sharedNSFileManagerContainerURLForSecurityApplicationGroupIdentifier; + + final PathProviderPlatformProvider _platformProvider; + final FoundationFFI _ffiLib; + final NSURL? Function(NSString) + _containerURLForSecurityApplicationGroupIdentifier; + + /// Registers this class as the default instance of [PathProviderPlatform]. + static void registerWith() { + PathProviderPlatform.instance = PathProviderFoundation(); + } + + @override + Future getTemporaryPath() async { + return _getDirectoryPath(NSSearchPathDirectory.NSCachesDirectory); + } + + @override + Future getApplicationSupportPath() async { + final String? path = _getDirectoryPath( + NSSearchPathDirectory.NSApplicationSupportDirectory, + ); + if (path != null) { + // Ensure the directory exists before returning it, for consistency with + // other platforms. + await Directory(path).create(recursive: true); + } + return path; + } + + @override + Future getLibraryPath() async { + return _getDirectoryPath(NSSearchPathDirectory.NSLibraryDirectory); + } + + @override + Future getApplicationDocumentsPath() async { + return _getDirectoryPath(NSSearchPathDirectory.NSDocumentDirectory); + } + + @override + Future getApplicationCachePath() async { + final String? path = _getDirectoryPath( + NSSearchPathDirectory.NSCachesDirectory, + ); + if (path != null) { + // Ensure the directory exists before returning it, for consistency with + // other platforms. + await Directory(path).create(recursive: true); + } + return path; + } + + @override + Future getExternalStoragePath() async { + throw UnsupportedError( + 'getExternalStoragePath is not supported on this platform', + ); + } + + @override + Future?> getExternalCachePaths() async { + throw UnsupportedError( + 'getExternalCachePaths is not supported on this platform', + ); + } + + @override + Future?> getExternalStoragePaths({ + StorageDirectory? type, + }) async { + throw UnsupportedError( + 'getExternalStoragePaths is not supported on this platform', + ); + } + + @override + Future getDownloadsPath() async { + return _getDirectoryPath(NSSearchPathDirectory.NSDownloadsDirectory); + } + + /// Returns the path to the container of the specified App Group. + /// This is only supported for iOS. + Future getContainerPath({required String appGroupIdentifier}) async { + if (!_platformProvider.isIOS) { + throw UnsupportedError( + 'getContainerPath is not supported on this platform', + ); + } + return _containerURLForSecurityApplicationGroupIdentifier( + NSString(appGroupIdentifier), + )?.path?.toDartString(); + } + + String? _getDirectoryPath(NSSearchPathDirectory directory) { + NSString? path = _getUserDirectory(directory); + if (path != null && _platformProvider.isMacOS) { + // In a non-sandboxed app, these are shared directories where applications + // are expected to use their 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 (directory == NSSearchPathDirectory.NSApplicationSupportDirectory || + directory == NSSearchPathDirectory.NSCachesDirectory) { + final NSString? bundleIdentifier = + NSBundle.getMainBundle().bundleIdentifier; + if (bundleIdentifier != null) { + final NSURL basePathURL = NSURL.fileURLWithPath(path); + path = basePathURL.URLByAppendingPathComponent( + bundleIdentifier, + )?.path; + } + } + } + return path?.toDartString(); + } + + /// Returns the user-domain directory of the given type. + NSString? _getUserDirectory(NSSearchPathDirectory directory) { + final NSArray paths = _ffiLib.NSSearchPathForDirectoriesInDomains( + directory, + NSSearchPathDomainMask.NSUserDomainMask, + true, + ); + final ObjCObject? first = paths.firstObject; + return first == null ? null : NSString.as(first); + } +} + +/// Helper class for returning information about the current platform. +@visibleForTesting +class PathProviderPlatformProvider { + /// Specifies whether the current platform is iOS. + bool get isIOS => Platform.isIOS; + + /// Specifies whether the current platform is macOS. + bool get isMacOS => Platform.isMacOS; +} + +NSURL? _sharedNSFileManagerContainerURLForSecurityApplicationGroupIdentifier( + NSString groupIdentifier, +) => NSFileManager.getDefaultManager() + .containerURLForSecurityApplicationGroupIdentifier(groupIdentifier); + +final ffi.DynamicLibrary _dylib = () { + return ffi.DynamicLibrary.open( + '/System/Library/Frameworks/Foundation.framework/Foundation', + ); +}(); + +/// The bindings to the native functions in [_dylib]. +final FoundationFFI _lib = () { + return FoundationFFI(_dylib); +}(); diff --git a/packages/path_provider/path_provider_foundation/lib/src/path_provider_foundation_stub.dart b/packages/path_provider/path_provider_foundation/lib/src/path_provider_foundation_stub.dart new file mode 100644 index 00000000000..d478136566d --- /dev/null +++ b/packages/path_provider/path_provider_foundation/lib/src/path_provider_foundation_stub.dart @@ -0,0 +1,21 @@ +// 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 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; + +// This file is a stub to satisfy the analyzer by matching the public API +// surface of the real implementation. This code should never actually be +// called. + +/// The iOS and macOS implementation of [PathProviderPlatform]. +class PathProviderFoundation extends PathProviderPlatform { + /// Registers this class as the default instance of [PathProviderPlatform]. + static void registerWith() {} + + /// Returns the path to the container of the specified App Group. + /// This is only supported for iOS. + Future getContainerPath({required String appGroupIdentifier}) async { + return null; + } +} diff --git a/packages/path_provider/path_provider_foundation/pigeons/copyright.txt b/packages/path_provider/path_provider_foundation/pigeons/copyright.txt deleted file mode 100644 index 07e5f8598a8..00000000000 --- a/packages/path_provider/path_provider_foundation/pigeons/copyright.txt +++ /dev/null @@ -1,3 +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. diff --git a/packages/path_provider/path_provider_foundation/pigeons/messages.dart b/packages/path_provider/path_provider_foundation/pigeons/messages.dart deleted file mode 100644 index 7ca732c475c..00000000000 --- a/packages/path_provider/path_provider_foundation/pigeons/messages.dart +++ /dev/null @@ -1,28 +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 'package:pigeon/pigeon.dart'; - -@ConfigurePigeon( - PigeonOptions( - input: 'pigeons/messages.dart', - swiftOut: - 'darwin/path_provider_foundation/Sources/path_provider_foundation/messages.g.swift', - dartOut: 'lib/messages.g.dart', - copyrightHeader: 'pigeons/copyright.txt', - ), -) -enum DirectoryType { - applicationDocuments, - applicationSupport, - downloads, - library, - temp, - applicationCache, -} - -@HostApi() -abstract class PathProviderApi { - String? getDirectoryPath(DirectoryType type); - String? getContainerPath(String appGroupIdentifier); -} diff --git a/packages/path_provider/path_provider_foundation/pubspec.yaml b/packages/path_provider/path_provider_foundation/pubspec.yaml index 1374199fbfe..a7daee5e898 100644 --- a/packages/path_provider/path_provider_foundation/pubspec.yaml +++ b/packages/path_provider/path_provider_foundation/pubspec.yaml @@ -2,7 +2,7 @@ name: path_provider_foundation description: iOS and macOS implementation of the path_provider plugin repository: https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider_foundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.4.4 +version: 2.5.0 environment: sdk: ^3.9.0 @@ -13,24 +13,21 @@ flutter: implements: path_provider platforms: ios: - pluginClass: PathProviderPlugin dartPluginClass: PathProviderFoundation - sharedDarwinSource: true macos: - pluginClass: PathProviderPlugin dartPluginClass: PathProviderFoundation - sharedDarwinSource: true dependencies: + ffi: ^2.1.4 flutter: sdk: flutter + objective_c: ^9.1.0 path_provider_platform_interface: ^2.1.0 dev_dependencies: + ffigen: ^20.0.0 flutter_test: sdk: flutter - path: ^1.8.0 - pigeon: ^26.1.0 topics: - files diff --git a/packages/path_provider/path_provider_foundation/test/path_provider_foundation_test.dart b/packages/path_provider/path_provider_foundation/test/path_provider_foundation_test.dart index f30fd65fa12..459444bb53a 100644 --- a/packages/path_provider/path_provider_foundation/test/path_provider_foundation_test.dart +++ b/packages/path_provider/path_provider_foundation/test/path_provider_foundation_test.dart @@ -4,15 +4,18 @@ import 'dart:io'; -import 'package:flutter/src/services/binary_messenger.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider_foundation/messages.g.dart'; -import 'package:path_provider_foundation/path_provider_foundation.dart'; +import 'package:objective_c/src/objective_c_bindings_generated.dart'; +import 'package:path_provider_foundation/src/ffi_bindings.g.dart'; +import 'package:path_provider_foundation/src/path_provider_foundation_real.dart'; + +// Most tests are in integration_test rather than here, because anything that +// needs to create Objective-C objects has to run in the real runtime. void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('PathProviderFoundation', () { - late FakePathProviderApi api; // These unit 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. @@ -20,156 +23,22 @@ void main() { setUp(() async { testRoot = Directory.systemTemp.createTempSync(); - api = FakePathProviderApi(); }); tearDown(() { testRoot.deleteSync(recursive: true); }); - test('getTemporaryPath', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation( - pathProviderApi: api, - ); - final String temporaryPath = p.join(testRoot.path, 'temporary', 'path'); - api.directoryResult = temporaryPath; - - final String? path = await pathProvider.getTemporaryPath(); - - expect(api.passedDirectoryType, DirectoryType.temp); - expect(path, temporaryPath); - }); - - test('getApplicationSupportPath', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation( - pathProviderApi: api, - ); - final String applicationSupportPath = p.join( - testRoot.path, - 'application', - 'support', - 'path', - ); - api.directoryResult = applicationSupportPath; - - final String? path = await pathProvider.getApplicationSupportPath(); - - expect(api.passedDirectoryType, DirectoryType.applicationSupport); - expect(path, applicationSupportPath); - }); - - test( - 'getApplicationSupportPath creates the directory if necessary', - () async { - final PathProviderFoundation pathProvider = PathProviderFoundation( - pathProviderApi: api, - ); - final String applicationSupportPath = p.join( - testRoot.path, - 'application', - 'support', - 'path', - ); - api.directoryResult = applicationSupportPath; - - final String? path = await pathProvider.getApplicationSupportPath(); - - expect(Directory(path!).existsSync(), isTrue); - }, - ); - - test('getLibraryPath', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation( - pathProviderApi: api, - ); - final String libraryPath = p.join(testRoot.path, 'library', 'path'); - api.directoryResult = libraryPath; - - final String? path = await pathProvider.getLibraryPath(); - - expect(api.passedDirectoryType, DirectoryType.library); - expect(path, libraryPath); - }); - - test('getApplicationDocumentsPath', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation( - pathProviderApi: api, - ); - final String applicationDocumentsPath = p.join( - testRoot.path, - 'application', - 'documents', - 'path', - ); - api.directoryResult = applicationDocumentsPath; - - final String? path = await pathProvider.getApplicationDocumentsPath(); - - expect(api.passedDirectoryType, DirectoryType.applicationDocuments); - expect(path, applicationDocumentsPath); - }); - - test('getApplicationCachePath', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation( - pathProviderApi: api, - ); - final String applicationCachePath = p.join( - testRoot.path, - 'application', - 'cache', - 'path', - ); - api.directoryResult = applicationCachePath; - - final String? path = await pathProvider.getApplicationCachePath(); - - expect(api.passedDirectoryType, DirectoryType.applicationCache); - expect(path, applicationCachePath); - }); - - test( - 'getApplicationCachePath creates the directory if necessary', - () async { - final PathProviderFoundation pathProvider = PathProviderFoundation( - pathProviderApi: api, - ); - final String applicationCachePath = p.join( - testRoot.path, - 'application', - 'cache', - 'path', - ); - api.directoryResult = applicationCachePath; - - final String? path = await pathProvider.getApplicationCachePath(); - - expect(Directory(path!).existsSync(), isTrue); - }, - ); - - test('getDownloadsPath', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation( - pathProviderApi: api, - ); - final String downloadsPath = p.join(testRoot.path, 'downloads', 'path'); - api.directoryResult = downloadsPath; - - final String? result = await pathProvider.getDownloadsPath(); - - expect(api.passedDirectoryType, DirectoryType.downloads); - expect(result, downloadsPath); - }); - test('getExternalCachePaths throws', () async { final PathProviderFoundation pathProvider = PathProviderFoundation( - pathProviderApi: api, + ffiLib: FakeFoundationFFI(), ); expect(pathProvider.getExternalCachePaths(), throwsA(isUnsupportedError)); }); test('getExternalStoragePath throws', () async { final PathProviderFoundation pathProvider = PathProviderFoundation( - pathProviderApi: api, + ffiLib: FakeFoundationFFI(), ); expect( pathProvider.getExternalStoragePath(), @@ -179,7 +48,7 @@ void main() { test('getExternalStoragePaths throws', () async { final PathProviderFoundation pathProvider = PathProviderFoundation( - pathProviderApi: api, + ffiLib: FakeFoundationFFI(), ); expect( pathProvider.getExternalStoragePaths(), @@ -187,28 +56,10 @@ void main() { ); }); - test('getContainerPath', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation( - pathProviderApi: api, - platform: FakePlatformProvider(isIOS: true), - ); - const String appGroupIdentifier = 'group.example.test'; - - final String containerPath = p.join(testRoot.path, 'container', 'path'); - api.containerResult = containerPath; - - final String? result = await pathProvider.getContainerPath( - appGroupIdentifier: appGroupIdentifier, - ); - - expect(api.passedAppGroupIdentifier, appGroupIdentifier); - expect(result, containerPath); - }); - test('getContainerPath throws on macOS', () async { final PathProviderFoundation pathProvider = PathProviderFoundation( - pathProviderApi: api, - platform: FakePlatformProvider(isIOS: false), + platform: FakePlatformProvider(isMacOS: true), + ffiLib: FakeFoundationFFI(), ); expect( pathProvider.getContainerPath(appGroupIdentifier: 'group.example.test'), @@ -218,37 +69,24 @@ void main() { }); } -/// Fake implementation of PathProviderPlatformProvider that returns iOS is true +/// Fake implementation of PathProviderPlatformProvider. class FakePlatformProvider implements PathProviderPlatformProvider { - FakePlatformProvider({required this.isIOS}); + FakePlatformProvider({this.isIOS = false, this.isMacOS = false}); @override bool isIOS; -} - -class FakePathProviderApi implements PathProviderApi { - String? directoryResult; - String? containerResult; - - DirectoryType? passedDirectoryType; - String? passedAppGroupIdentifier; - - @override - Future getDirectoryPath(DirectoryType type) async { - passedDirectoryType = type; - return directoryResult; - } - - @override - Future getContainerPath(String appGroupIdentifier) async { - passedAppGroupIdentifier = appGroupIdentifier; - return containerResult; - } @override - // ignore: non_constant_identifier_names - BinaryMessenger? get pigeonVar_binaryMessenger => null; + bool isMacOS; +} +class FakeFoundationFFI implements FoundationFFI { @override // ignore: non_constant_identifier_names - String get pigeonVar_messageChannelSuffix => ''; + NSArray NSSearchPathForDirectoriesInDomains( + NSSearchPathDirectory directory, + int domainMask, + bool expandTilde, + ) { + throw UnimplementedError(); + } } diff --git a/packages/path_provider/path_provider_foundation/tool/ffigen.dart b/packages/path_provider/path_provider_foundation/tool/ffigen.dart new file mode 100644 index 00000000000..214cba31e6f --- /dev/null +++ b/packages/path_provider/path_provider_foundation/tool/ffigen.dart @@ -0,0 +1,62 @@ +// 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 'dart:io'; + +import 'package:ffigen/ffigen.dart'; + +void main() { + final Uri packageRoot = Platform.script.resolve('../'); + FfiGenerator( + output: Output( + dartFile: packageRoot.resolve('lib/src/ffi_bindings.g.dart'), + style: const DynamicLibraryBindings( + wrapperName: 'FoundationFFI', + wrapperDocComment: 'Bindings for NSFileManager.', + ), + ), + headers: Headers( + entryPoints: [ + Uri.file( + '$macSdkPath/System/Library/Frameworks/AVFAudio.framework/Headers/AVAudioPlayer.h', + ), + ], + ), + objectiveC: ObjectiveC( + interfaces: Interfaces( + include: (Declaration declaration) { + return { + 'NSFileManager', + 'NSURL', + }.contains(declaration.originalName); + }, + includeMember: (Declaration declaration, String member) { + final String interfaceName = declaration.originalName; + final String signature = member; + return switch (interfaceName) { + 'NSFileManager' => { + 'containerURLForSecurityApplicationGroupIdentifier:', + 'defaultManager', + }.contains(signature), + 'NSURL' => { + 'fileURLWithPath:', + 'URLByAppendingPathComponent:', + }.contains(signature), + _ => false, + }; + }, + ), + categories: Categories( + include: (Declaration declaration) => { + // For URLByAppendingPathComponent: + 'NSURLPathUtilities', + }.contains(declaration.originalName), + includeTransitive: false, + ), + ), + functions: Functions.includeSet({ + 'NSSearchPathForDirectoriesInDomains', + }), + ).generate(); +} diff --git a/script/configs/allowed_unpinned_deps.yaml b/script/configs/allowed_unpinned_deps.yaml index f087d8881df..f139fc20c34 100644 --- a/script/configs/allowed_unpinned_deps.yaml +++ b/script/configs/allowed_unpinned_deps.yaml @@ -33,6 +33,7 @@ - devtools_extensions - fake_async - ffi +- ffigen - file - file_testing - gcloud @@ -51,6 +52,7 @@ - meta - mime - mockito +- objective_c - path - package_config - platform