diff --git a/packages/path_provider/path_provider_foundation/CHANGELOG.md b/packages/path_provider/path_provider_foundation/CHANGELOG.md index 69433e8b306..8fe69648209 100644 --- a/packages/path_provider/path_provider_foundation/CHANGELOG.md +++ b/packages/path_provider/path_provider_foundation/CHANGELOG.md @@ -1,5 +1,7 @@ -## NEXT +## 2.5.0 +* Replaces Flutter-plugin-based implementation with direct FFI calls to + Foundation. * Updates minimum supported SDK version to Flutter 3.29/Dart 3.7. ## 2.4.2 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..4573f44e694 --- /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 `ffigen_config.yaml`, then run: + +```bash +dart run ffigen --config ffigen_config.yaml +``` + +### 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 67ee981673a..00000000000 --- a/packages/path_provider/path_provider_foundation/darwin/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 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 8634ab149dc..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 = '12.0' - s.osx.deployment_target = '10.14' - 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 e0e82689ac8..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. All rights reserved. -// 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("12.0"), - .macOS("10.14"), - ], - 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 16f2edb84c9..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. All rights reserved. -// 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 99df5a9cde4..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. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -// Autogenerated from Pigeon (v25.5.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 711334b3cde..2b33c952a84 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,423 @@ 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(), + 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/com.example.pathProviderFoundationExample'); + }); - 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/com.example.pathProviderFoundationExample', + ); + }); + + 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/com.example.pathProviderFoundationExample', + ); + }); + + 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 MockFoundationFFI mockFfiLib = MockFoundationFFI(); + final MockNSFileManager mockFileManager = MockNSFileManager(); + final PathProviderFoundation pathProvider = PathProviderFoundation( + ffiLib: mockFfiLib, + fileManagerProvider: () => mockFileManager, + platform: FakePlatformProvider(isIOS: true), + ); + + final String containerPath = p.join(testRoot.path, 'container', 'path'); + final NSURL containerUrl = NSURL.fileURLWithPath(NSString(containerPath)); + when( + mockFileManager.containerURLForSecurityApplicationGroupIdentifier(any), + ).thenReturn(containerUrl); + + 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 +444,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..e4cecfb1147 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/example/integration_test/path_provider_test.mocks.dart @@ -0,0 +1,927 @@ +// 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 'dart:ffi' as _i5; + +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 _i6; + +// 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 + +class _FakeNSArray_0 extends _i1.SmartFake implements _i2.NSArray { + _FakeNSArray_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeNSString_1 extends _i1.SmartFake implements _i2.NSString { + _FakeNSString_1(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeObjCObjectBase_2 extends _i1.SmartFake + implements _i2.ObjCObjectBase { + _FakeObjCObjectBase_2(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeNSObject_3 extends _i1.SmartFake implements _i2.NSObject { + _FakeNSObject_3(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +class _FakeNSMethodSignature_4 extends _i1.SmartFake + implements _i2.NSMethodSignature { + _FakeNSMethodSignature_4(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, + _i3.NSSearchPathDomainMask? domainMask, + bool? expandTilde, + ) => + (super.noSuchMethod( + Invocation.method(#NSSearchPathForDirectoriesInDomains, [ + directory, + domainMask, + expandTilde, + ]), + returnValue: _FakeNSArray_0( + this, + Invocation.method(#NSSearchPathForDirectoriesInDomains, [ + directory, + domainMask, + expandTilde, + ]), + ), + returnValueForMissingStub: _FakeNSArray_0( + this, + Invocation.method(#NSSearchPathForDirectoriesInDomains, [ + directory, + domainMask, + expandTilde, + ]), + ), + ) + as _i2.NSArray); +} + +/// A class which mocks [NSBundle]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockNSBundle extends _i1.Mock implements _i3.NSBundle { + @override + _i2.NSString get debugDescription$1 => + (super.noSuchMethod( + Invocation.getter(#debugDescription$1), + returnValue: _FakeNSString_1( + this, + Invocation.getter(#debugDescription$1), + ), + returnValueForMissingStub: _FakeNSString_1( + this, + Invocation.getter(#debugDescription$1), + ), + ) + as _i2.NSString); + + @override + _i2.NSString get description$1 => + (super.noSuchMethod( + Invocation.getter(#description$1), + returnValue: _FakeNSString_1( + this, + Invocation.getter(#description$1), + ), + returnValueForMissingStub: _FakeNSString_1( + this, + Invocation.getter(#description$1), + ), + ) + as _i2.NSString); + + @override + int get hash$1 => + (super.noSuchMethod( + Invocation.getter(#hash$1), + returnValue: 0, + returnValueForMissingStub: 0, + ) + as int); + + @override + bool get isProxy => + (super.noSuchMethod( + Invocation.getter(#isProxy), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + _i2.ObjCObjectBase get superclass$1 => + (super.noSuchMethod( + Invocation.getter(#superclass$1), + returnValue: _FakeObjCObjectBase_2( + this, + Invocation.getter(#superclass$1), + ), + returnValueForMissingStub: _FakeObjCObjectBase_2( + this, + Invocation.getter(#superclass$1), + ), + ) + as _i2.ObjCObjectBase); + + @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); + + @override + _i2.NSObject autorelease() => + (super.noSuchMethod( + Invocation.method(#autorelease, []), + returnValue: _FakeNSObject_3( + this, + Invocation.method(#autorelease, []), + ), + returnValueForMissingStub: _FakeNSObject_3( + this, + Invocation.method(#autorelease, []), + ), + ) + as _i2.NSObject); + + @override + _i2.ObjCObjectBase class$1() => + (super.noSuchMethod( + Invocation.method(#class$1, []), + returnValue: _FakeObjCObjectBase_2( + this, + Invocation.method(#class$1, []), + ), + returnValueForMissingStub: _FakeObjCObjectBase_2( + this, + Invocation.method(#class$1, []), + ), + ) + as _i2.ObjCObjectBase); + + @override + bool conformsToProtocol$1(_i2.Protocol? aProtocol) => + (super.noSuchMethod( + Invocation.method(#conformsToProtocol$1, [aProtocol]), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + _i2.ObjCObjectBase copy() => + (super.noSuchMethod( + Invocation.method(#copy, []), + returnValue: _FakeObjCObjectBase_2( + this, + Invocation.method(#copy, []), + ), + returnValueForMissingStub: _FakeObjCObjectBase_2( + this, + Invocation.method(#copy, []), + ), + ) + as _i2.ObjCObjectBase); + + @override + void dealloc() => super.noSuchMethod( + Invocation.method(#dealloc, []), + returnValueForMissingStub: null, + ); + + @override + void doesNotRecognizeSelector(_i5.Pointer<_i2.ObjCSelector>? aSelector) => + super.noSuchMethod( + Invocation.method(#doesNotRecognizeSelector, [aSelector]), + returnValueForMissingStub: null, + ); + + @override + void forwardInvocation(_i2.NSInvocation? anInvocation) => super.noSuchMethod( + Invocation.method(#forwardInvocation, [anInvocation]), + returnValueForMissingStub: null, + ); + + @override + _i2.ObjCObjectBase forwardingTargetForSelector( + _i5.Pointer<_i2.ObjCSelector>? aSelector, + ) => + (super.noSuchMethod( + Invocation.method(#forwardingTargetForSelector, [aSelector]), + returnValue: _FakeObjCObjectBase_2( + this, + Invocation.method(#forwardingTargetForSelector, [aSelector]), + ), + returnValueForMissingStub: _FakeObjCObjectBase_2( + this, + Invocation.method(#forwardingTargetForSelector, [aSelector]), + ), + ) + as _i2.ObjCObjectBase); + + @override + _i2.NSObject init() => + (super.noSuchMethod( + Invocation.method(#init, []), + returnValue: _FakeNSObject_3(this, Invocation.method(#init, [])), + returnValueForMissingStub: _FakeNSObject_3( + this, + Invocation.method(#init, []), + ), + ) + as _i2.NSObject); + + @override + bool isEqual(_i2.ObjCObjectBase? object) => + (super.noSuchMethod( + Invocation.method(#isEqual, [object]), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + bool isKindOfClass(_i2.ObjCObjectBase? aClass) => + (super.noSuchMethod( + Invocation.method(#isKindOfClass, [aClass]), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + bool isMemberOfClass(_i2.ObjCObjectBase? aClass) => + (super.noSuchMethod( + Invocation.method(#isMemberOfClass, [aClass]), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + _i5.Pointer<_i5.NativeFunction<_i5.Void Function()>> methodForSelector( + _i5.Pointer<_i2.ObjCSelector>? aSelector, + ) => + (super.noSuchMethod( + Invocation.method(#methodForSelector, [aSelector]), + returnValue: _i4.dummyValue< + _i5.Pointer<_i5.NativeFunction<_i5.Void Function()>> + >(this, Invocation.method(#methodForSelector, [aSelector])), + returnValueForMissingStub: _i4.dummyValue< + _i5.Pointer<_i5.NativeFunction<_i5.Void Function()>> + >(this, Invocation.method(#methodForSelector, [aSelector])), + ) + as _i5.Pointer<_i5.NativeFunction<_i5.Void Function()>>); + + @override + _i2.NSMethodSignature methodSignatureForSelector( + _i5.Pointer<_i2.ObjCSelector>? aSelector, + ) => + (super.noSuchMethod( + Invocation.method(#methodSignatureForSelector, [aSelector]), + returnValue: _FakeNSMethodSignature_4( + this, + Invocation.method(#methodSignatureForSelector, [aSelector]), + ), + returnValueForMissingStub: _FakeNSMethodSignature_4( + this, + Invocation.method(#methodSignatureForSelector, [aSelector]), + ), + ) + as _i2.NSMethodSignature); + + @override + _i2.ObjCObjectBase mutableCopy() => + (super.noSuchMethod( + Invocation.method(#mutableCopy, []), + returnValue: _FakeObjCObjectBase_2( + this, + Invocation.method(#mutableCopy, []), + ), + returnValueForMissingStub: _FakeObjCObjectBase_2( + this, + Invocation.method(#mutableCopy, []), + ), + ) + as _i2.ObjCObjectBase); + + @override + _i2.ObjCObjectBase performSelector( + _i5.Pointer<_i2.ObjCSelector>? aSelector, + ) => + (super.noSuchMethod( + Invocation.method(#performSelector, [aSelector]), + returnValue: _FakeObjCObjectBase_2( + this, + Invocation.method(#performSelector, [aSelector]), + ), + returnValueForMissingStub: _FakeObjCObjectBase_2( + this, + Invocation.method(#performSelector, [aSelector]), + ), + ) + as _i2.ObjCObjectBase); + + @override + _i2.ObjCObjectBase performSelector$1( + _i5.Pointer<_i2.ObjCSelector>? aSelector, { + required _i2.ObjCObjectBase? withObject, + }) => + (super.noSuchMethod( + Invocation.method( + #performSelector$1, + [aSelector], + {#withObject: withObject}, + ), + returnValue: _FakeObjCObjectBase_2( + this, + Invocation.method( + #performSelector$1, + [aSelector], + {#withObject: withObject}, + ), + ), + returnValueForMissingStub: _FakeObjCObjectBase_2( + this, + Invocation.method( + #performSelector$1, + [aSelector], + {#withObject: withObject}, + ), + ), + ) + as _i2.ObjCObjectBase); + + @override + _i2.ObjCObjectBase performSelector$2( + _i5.Pointer<_i2.ObjCSelector>? aSelector, { + required _i2.ObjCObjectBase? withObject, + required _i2.ObjCObjectBase? withObject$1, + }) => + (super.noSuchMethod( + Invocation.method( + #performSelector$2, + [aSelector], + {#withObject: withObject, #withObject$1: withObject$1}, + ), + returnValue: _FakeObjCObjectBase_2( + this, + Invocation.method( + #performSelector$2, + [aSelector], + {#withObject: withObject, #withObject$1: withObject$1}, + ), + ), + returnValueForMissingStub: _FakeObjCObjectBase_2( + this, + Invocation.method( + #performSelector$2, + [aSelector], + {#withObject: withObject, #withObject$1: withObject$1}, + ), + ), + ) + as _i2.ObjCObjectBase); + + @override + void release() => super.noSuchMethod( + Invocation.method(#release, []), + returnValueForMissingStub: null, + ); + + @override + bool respondsToSelector(_i5.Pointer<_i2.ObjCSelector>? aSelector) => + (super.noSuchMethod( + Invocation.method(#respondsToSelector, [aSelector]), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + _i2.NSObject retain() => + (super.noSuchMethod( + Invocation.method(#retain, []), + returnValue: _FakeNSObject_3(this, Invocation.method(#retain, [])), + returnValueForMissingStub: _FakeNSObject_3( + this, + Invocation.method(#retain, []), + ), + ) + as _i2.NSObject); + + @override + int retainCount() => + (super.noSuchMethod( + Invocation.method(#retainCount, []), + returnValue: 0, + returnValueForMissingStub: 0, + ) + as int); + + @override + _i2.NSObject self$1() => + (super.noSuchMethod( + Invocation.method(#self$1, []), + returnValue: _FakeNSObject_3(this, Invocation.method(#self$1, [])), + returnValueForMissingStub: _FakeNSObject_3( + this, + Invocation.method(#self$1, []), + ), + ) + as _i2.NSObject); + + @override + _i5.Pointer<_i2.NSZone> zone() => + (super.noSuchMethod( + Invocation.method(#zone, []), + returnValue: _i4.dummyValue<_i5.Pointer<_i2.NSZone>>( + this, + Invocation.method(#zone, []), + ), + returnValueForMissingStub: _i4.dummyValue<_i5.Pointer<_i2.NSZone>>( + this, + Invocation.method(#zone, []), + ), + ) + as _i5.Pointer<_i2.NSZone>); +} + +/// A class which mocks [NSFileManager]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockNSFileManager extends _i1.Mock implements _i3.NSFileManager { + @override + _i2.NSString get debugDescription$1 => + (super.noSuchMethod( + Invocation.getter(#debugDescription$1), + returnValue: _FakeNSString_1( + this, + Invocation.getter(#debugDescription$1), + ), + returnValueForMissingStub: _FakeNSString_1( + this, + Invocation.getter(#debugDescription$1), + ), + ) + as _i2.NSString); + + @override + _i2.NSString get description$1 => + (super.noSuchMethod( + Invocation.getter(#description$1), + returnValue: _FakeNSString_1( + this, + Invocation.getter(#description$1), + ), + returnValueForMissingStub: _FakeNSString_1( + this, + Invocation.getter(#description$1), + ), + ) + as _i2.NSString); + + @override + int get hash$1 => + (super.noSuchMethod( + Invocation.getter(#hash$1), + returnValue: 0, + returnValueForMissingStub: 0, + ) + as int); + + @override + bool get isProxy => + (super.noSuchMethod( + Invocation.getter(#isProxy), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + _i2.ObjCObjectBase get superclass$1 => + (super.noSuchMethod( + Invocation.getter(#superclass$1), + returnValue: _FakeObjCObjectBase_2( + this, + Invocation.getter(#superclass$1), + ), + returnValueForMissingStub: _FakeObjCObjectBase_2( + this, + Invocation.getter(#superclass$1), + ), + ) + as _i2.ObjCObjectBase); + + @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); + + @override + _i2.NSURL? containerURLForSecurityApplicationGroupIdentifier( + _i2.NSString? groupIdentifier, + ) => + (super.noSuchMethod( + Invocation.method( + #containerURLForSecurityApplicationGroupIdentifier, + [groupIdentifier], + ), + returnValueForMissingStub: null, + ) + as _i2.NSURL?); + + @override + _i2.NSObject autorelease() => + (super.noSuchMethod( + Invocation.method(#autorelease, []), + returnValue: _FakeNSObject_3( + this, + Invocation.method(#autorelease, []), + ), + returnValueForMissingStub: _FakeNSObject_3( + this, + Invocation.method(#autorelease, []), + ), + ) + as _i2.NSObject); + + @override + _i2.ObjCObjectBase class$1() => + (super.noSuchMethod( + Invocation.method(#class$1, []), + returnValue: _FakeObjCObjectBase_2( + this, + Invocation.method(#class$1, []), + ), + returnValueForMissingStub: _FakeObjCObjectBase_2( + this, + Invocation.method(#class$1, []), + ), + ) + as _i2.ObjCObjectBase); + + @override + bool conformsToProtocol$1(_i2.Protocol? aProtocol) => + (super.noSuchMethod( + Invocation.method(#conformsToProtocol$1, [aProtocol]), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + _i2.ObjCObjectBase copy() => + (super.noSuchMethod( + Invocation.method(#copy, []), + returnValue: _FakeObjCObjectBase_2( + this, + Invocation.method(#copy, []), + ), + returnValueForMissingStub: _FakeObjCObjectBase_2( + this, + Invocation.method(#copy, []), + ), + ) + as _i2.ObjCObjectBase); + + @override + void dealloc() => super.noSuchMethod( + Invocation.method(#dealloc, []), + returnValueForMissingStub: null, + ); + + @override + void doesNotRecognizeSelector(_i5.Pointer<_i2.ObjCSelector>? aSelector) => + super.noSuchMethod( + Invocation.method(#doesNotRecognizeSelector, [aSelector]), + returnValueForMissingStub: null, + ); + + @override + void forwardInvocation(_i2.NSInvocation? anInvocation) => super.noSuchMethod( + Invocation.method(#forwardInvocation, [anInvocation]), + returnValueForMissingStub: null, + ); + + @override + _i2.ObjCObjectBase forwardingTargetForSelector( + _i5.Pointer<_i2.ObjCSelector>? aSelector, + ) => + (super.noSuchMethod( + Invocation.method(#forwardingTargetForSelector, [aSelector]), + returnValue: _FakeObjCObjectBase_2( + this, + Invocation.method(#forwardingTargetForSelector, [aSelector]), + ), + returnValueForMissingStub: _FakeObjCObjectBase_2( + this, + Invocation.method(#forwardingTargetForSelector, [aSelector]), + ), + ) + as _i2.ObjCObjectBase); + + @override + _i2.NSObject init() => + (super.noSuchMethod( + Invocation.method(#init, []), + returnValue: _FakeNSObject_3(this, Invocation.method(#init, [])), + returnValueForMissingStub: _FakeNSObject_3( + this, + Invocation.method(#init, []), + ), + ) + as _i2.NSObject); + + @override + bool isEqual(_i2.ObjCObjectBase? object) => + (super.noSuchMethod( + Invocation.method(#isEqual, [object]), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + bool isKindOfClass(_i2.ObjCObjectBase? aClass) => + (super.noSuchMethod( + Invocation.method(#isKindOfClass, [aClass]), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + bool isMemberOfClass(_i2.ObjCObjectBase? aClass) => + (super.noSuchMethod( + Invocation.method(#isMemberOfClass, [aClass]), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + _i5.Pointer<_i5.NativeFunction<_i5.Void Function()>> methodForSelector( + _i5.Pointer<_i2.ObjCSelector>? aSelector, + ) => + (super.noSuchMethod( + Invocation.method(#methodForSelector, [aSelector]), + returnValue: _i4.dummyValue< + _i5.Pointer<_i5.NativeFunction<_i5.Void Function()>> + >(this, Invocation.method(#methodForSelector, [aSelector])), + returnValueForMissingStub: _i4.dummyValue< + _i5.Pointer<_i5.NativeFunction<_i5.Void Function()>> + >(this, Invocation.method(#methodForSelector, [aSelector])), + ) + as _i5.Pointer<_i5.NativeFunction<_i5.Void Function()>>); + + @override + _i2.NSMethodSignature methodSignatureForSelector( + _i5.Pointer<_i2.ObjCSelector>? aSelector, + ) => + (super.noSuchMethod( + Invocation.method(#methodSignatureForSelector, [aSelector]), + returnValue: _FakeNSMethodSignature_4( + this, + Invocation.method(#methodSignatureForSelector, [aSelector]), + ), + returnValueForMissingStub: _FakeNSMethodSignature_4( + this, + Invocation.method(#methodSignatureForSelector, [aSelector]), + ), + ) + as _i2.NSMethodSignature); + + @override + _i2.ObjCObjectBase mutableCopy() => + (super.noSuchMethod( + Invocation.method(#mutableCopy, []), + returnValue: _FakeObjCObjectBase_2( + this, + Invocation.method(#mutableCopy, []), + ), + returnValueForMissingStub: _FakeObjCObjectBase_2( + this, + Invocation.method(#mutableCopy, []), + ), + ) + as _i2.ObjCObjectBase); + + @override + _i2.ObjCObjectBase performSelector( + _i5.Pointer<_i2.ObjCSelector>? aSelector, + ) => + (super.noSuchMethod( + Invocation.method(#performSelector, [aSelector]), + returnValue: _FakeObjCObjectBase_2( + this, + Invocation.method(#performSelector, [aSelector]), + ), + returnValueForMissingStub: _FakeObjCObjectBase_2( + this, + Invocation.method(#performSelector, [aSelector]), + ), + ) + as _i2.ObjCObjectBase); + + @override + _i2.ObjCObjectBase performSelector$1( + _i5.Pointer<_i2.ObjCSelector>? aSelector, { + required _i2.ObjCObjectBase? withObject, + }) => + (super.noSuchMethod( + Invocation.method( + #performSelector$1, + [aSelector], + {#withObject: withObject}, + ), + returnValue: _FakeObjCObjectBase_2( + this, + Invocation.method( + #performSelector$1, + [aSelector], + {#withObject: withObject}, + ), + ), + returnValueForMissingStub: _FakeObjCObjectBase_2( + this, + Invocation.method( + #performSelector$1, + [aSelector], + {#withObject: withObject}, + ), + ), + ) + as _i2.ObjCObjectBase); + + @override + _i2.ObjCObjectBase performSelector$2( + _i5.Pointer<_i2.ObjCSelector>? aSelector, { + required _i2.ObjCObjectBase? withObject, + required _i2.ObjCObjectBase? withObject$1, + }) => + (super.noSuchMethod( + Invocation.method( + #performSelector$2, + [aSelector], + {#withObject: withObject, #withObject$1: withObject$1}, + ), + returnValue: _FakeObjCObjectBase_2( + this, + Invocation.method( + #performSelector$2, + [aSelector], + {#withObject: withObject, #withObject$1: withObject$1}, + ), + ), + returnValueForMissingStub: _FakeObjCObjectBase_2( + this, + Invocation.method( + #performSelector$2, + [aSelector], + {#withObject: withObject, #withObject$1: withObject$1}, + ), + ), + ) + as _i2.ObjCObjectBase); + + @override + void release() => super.noSuchMethod( + Invocation.method(#release, []), + returnValueForMissingStub: null, + ); + + @override + bool respondsToSelector(_i5.Pointer<_i2.ObjCSelector>? aSelector) => + (super.noSuchMethod( + Invocation.method(#respondsToSelector, [aSelector]), + returnValue: false, + returnValueForMissingStub: false, + ) + as bool); + + @override + _i2.NSObject retain() => + (super.noSuchMethod( + Invocation.method(#retain, []), + returnValue: _FakeNSObject_3(this, Invocation.method(#retain, [])), + returnValueForMissingStub: _FakeNSObject_3( + this, + Invocation.method(#retain, []), + ), + ) + as _i2.NSObject); + + @override + int retainCount() => + (super.noSuchMethod( + Invocation.method(#retainCount, []), + returnValue: 0, + returnValueForMissingStub: 0, + ) + as int); + + @override + _i2.NSObject self$1() => + (super.noSuchMethod( + Invocation.method(#self$1, []), + returnValue: _FakeNSObject_3(this, Invocation.method(#self$1, [])), + returnValueForMissingStub: _FakeNSObject_3( + this, + Invocation.method(#self$1, []), + ), + ) + as _i2.NSObject); + + @override + _i5.Pointer<_i2.NSZone> zone() => + (super.noSuchMethod( + Invocation.method(#zone, []), + returnValue: _i4.dummyValue<_i5.Pointer<_i2.NSZone>>( + this, + Invocation.method(#zone, []), + ), + returnValueForMissingStub: _i4.dummyValue<_i5.Pointer<_i2.NSZone>>( + this, + Invocation.method(#zone, []), + ), + ) + as _i5.Pointer<_i2.NSZone>); +} + +/// A class which mocks [PathProviderPlatformProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPathProviderPlatformProvider extends _i1.Mock + implements _i6.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/pubspec.yaml b/packages/path_provider/path_provider_foundation/example/pubspec.yaml index 6c2ff307a92..981f5e8d3a4 100644 --- a/packages/path_provider/path_provider_foundation/example/pubspec.yaml +++ b/packages/path_provider/path_provider_foundation/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the path_provider plugin. publish_to: none environment: - sdk: ^3.7.0 - flutter: ">=3.29.0" + sdk: ^3.6.0 + flutter: ">=3.27.0" dependencies: flutter: @@ -19,10 +19,14 @@ dependencies: path_provider_platform_interface: ^2.1.0 dev_dependencies: + build_runner: ^2.6.0 flutter_test: sdk: flutter integration_test: sdk: flutter + mockito: ^5.4.4 + objective_c: ^8.0.0 + path: ^1.8.0 flutter: uses-material-design: true diff --git a/packages/path_provider/path_provider_foundation/ffigen_config.yaml b/packages/path_provider/path_provider_foundation/ffigen_config.yaml new file mode 100644 index 00000000000..f7fdd6463c4 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/ffigen_config.yaml @@ -0,0 +1,36 @@ + +name: FoundationFFI +description: Bindings for NSFileManager. +language: objc +output: + bindings: 'lib/src/ffi_bindings.g.dart' +headers: + entry-points: + - '$MACOS_SDK/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h' +exclude-all-by-default: true +include-transitive-objc-categories: false +objc-interfaces: + include: + - 'NSBundle' + - 'NSFileManager' + - 'NSURL' + member-filter: + NSBundle: + include: + - "bundleIdentifier" + - "mainBundle" + NSFileManager: + include: + - "containerURLForSecurityApplicationGroupIdentifier:" + - "defaultManager" + NSURL: + include: + - "fileURLWithPath:" + - "URLByAppendingPathComponent:" +objc-categories: + include: + # For URLByAppendingPathComponent: + - 'NSURLPathUtilities' +functions: + include: + - 'NSSearchPathForDirectoriesInDomains' diff --git a/packages/path_provider/path_provider_foundation/lib/messages.g.dart b/packages/path_provider/path_provider_foundation/lib/messages.g.dart deleted file mode 100644 index 892b73bd1fe..00000000000 --- a/packages/path_provider/path_provider_foundation/lib/messages.g.dart +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -// Autogenerated from Pigeon (v25.5.0), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; - -import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; -import 'package:flutter/services.dart'; - -PlatformException _createConnectionError(String channelName) { - return PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); -} - -List wrapResponse({ - Object? result, - PlatformException? error, - bool empty = false, -}) { - if (empty) { - return []; - } - if (error == null) { - return [result]; - } - return [error.code, error.message, error.details]; -} - -enum DirectoryType { - applicationDocuments, - applicationSupport, - downloads, - library, - temp, - applicationCache, -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else if (value is DirectoryType) { - buffer.putUint8(129); - writeValue(buffer, value.index); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 129: - final int? value = readValue(buffer) as int?; - return value == null ? null : DirectoryType.values[value]; - default: - return super.readValueOfType(type, buffer); - } - } -} - -class PathProviderApi { - /// Constructor for [PathProviderApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - PathProviderApi({ - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = - messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec 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 d769c09f3de..16dd68816e9 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,112 +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, - }) : _platformProvider = platform ?? PathProviderPlatformProvider(); - - final PathProviderPlatformProvider _platformProvider; - final PathProviderApi _pathProvider = PathProviderApi(); - - /// 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..cc1b6080ef0 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/lib/src/ffi_bindings.g.dart @@ -0,0 +1,566 @@ +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint +import 'dart:ffi' as ffi; +import 'package:objective_c/objective_c.dart' as objc; + +/// 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, + NSSearchPathDomainMask domainMask, + bool expandTilde, + ) { + return objc.NSArray.castFromPointer( + _NSSearchPathForDirectoriesInDomains( + directory.value, + domainMask.value, + 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) + >(); +} + +late final _class_NSBundle = objc.getClass("NSBundle"); +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_mainBundle = objc.registerName("mainBundle"); +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_bundleIdentifier = objc.registerName("bundleIdentifier"); + +/// NSBundle +class NSBundle extends objc.NSObject { + NSBundle._( + ffi.Pointer pointer, { + bool retain = false, + bool release = false, + }) : super.castFromPointer(pointer, retain: retain, release: release); + + /// Constructs a [NSBundle] that points to the same underlying object as [other]. + NSBundle.castFrom(objc.ObjCObjectBase other) + : this._(other.ref.pointer, retain: true, release: true); + + /// Constructs a [NSBundle] that wraps the given raw object pointer. + NSBundle.castFromPointer( + ffi.Pointer other, { + bool retain = false, + bool release = false, + }) : this._(other, retain: retain, release: release); + + /// Returns whether [obj] is an instance of [NSBundle]. + static bool isInstance(objc.ObjCObjectBase obj) { + return _objc_msgSend_19nvye5( + obj.ref.pointer, + _sel_isKindOfClass_, + _class_NSBundle, + ); + } + + /// mainBundle + static NSBundle getMainBundle() { + final _ret = _objc_msgSend_151sglz(_class_NSBundle, _sel_mainBundle); + return NSBundle.castFromPointer(_ret, retain: true, release: true); + } + + /// bundleIdentifier + objc.NSString? get bundleIdentifier { + final _ret = _objc_msgSend_151sglz(this.ref.pointer, _sel_bundleIdentifier); + return _ret.address == 0 + ? null + : objc.NSString.castFromPointer(_ret, retain: true, release: true); + } +} + +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'), + }; +} + +enum NSSearchPathDomainMask { + NSUserDomainMask(1), + NSLocalDomainMask(2), + NSNetworkDomainMask(4), + NSSystemDomainMask(8), + NSAllDomainsMask(65535); + + final int value; + const NSSearchPathDomainMask(this.value); + + static NSSearchPathDomainMask fromValue(int value) => switch (value) { + 1 => NSUserDomainMask, + 2 => NSLocalDomainMask, + 4 => NSNetworkDomainMask, + 8 => NSSystemDomainMask, + 65535 => NSAllDomainsMask, + _ => + throw ArgumentError('Unknown value for NSSearchPathDomainMask: $value'), + }; +} + +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"); +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 { + /// 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.castFromPointer(_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(this.ref.pointer, _sel_pathComponents); + return _ret.address == 0 + ? null + : objc.NSArray.castFromPointer(_ret, retain: true, release: true); + } + + /// lastPathComponent + objc.NSString? get lastPathComponent { + objc.checkOsVersionInternal( + 'NSURL.lastPathComponent', + iOS: (false, (4, 0, 0)), + macOS: (false, (10, 6, 0)), + ); + final _ret = _objc_msgSend_151sglz( + this.ref.pointer, + _sel_lastPathComponent, + ); + return _ret.address == 0 + ? null + : objc.NSString.castFromPointer(_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(this.ref.pointer, _sel_pathExtension); + return _ret.address == 0 + ? null + : objc.NSString.castFromPointer(_ret, retain: true, release: true); + } + + /// 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( + this.ref.pointer, + _sel_URLByAppendingPathComponent_, + pathComponent.ref.pointer, + ); + return _ret.address == 0 + ? null + : objc.NSURL.castFromPointer(_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( + this.ref.pointer, + _sel_URLByAppendingPathComponent_isDirectory_, + pathComponent.ref.pointer, + isDirectory, + ); + return _ret.address == 0 + ? null + : objc.NSURL.castFromPointer(_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( + this.ref.pointer, + _sel_URLByDeletingLastPathComponent, + ); + return _ret.address == 0 + ? null + : objc.NSURL.castFromPointer(_ret, retain: true, release: true); + } + + /// URLByAppendingPathExtension: + objc.NSURL? URLByAppendingPathExtension(objc.NSString pathExtension$1) { + objc.checkOsVersionInternal( + 'NSURL.URLByAppendingPathExtension:', + iOS: (false, (4, 0, 0)), + macOS: (false, (10, 6, 0)), + ); + final _ret = _objc_msgSend_1sotr3r( + this.ref.pointer, + _sel_URLByAppendingPathExtension_, + pathExtension$1.ref.pointer, + ); + return _ret.address == 0 + ? null + : objc.NSURL.castFromPointer(_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( + this.ref.pointer, + _sel_URLByDeletingPathExtension, + ); + return _ret.address == 0 + ? null + : objc.NSURL.castFromPointer(_ret, retain: true, release: true); + } + + /// checkResourceIsReachableAndReturnError: + bool checkResourceIsReachableAndReturnError( + ffi.Pointer> error, + ) { + objc.checkOsVersionInternal( + 'NSURL.checkResourceIsReachableAndReturnError:', + iOS: (false, (4, 0, 0)), + macOS: (false, (10, 6, 0)), + ); + return _objc_msgSend_1dom33q( + this.ref.pointer, + _sel_checkResourceIsReachableAndReturnError_, + error, + ); + } + + /// URLByStandardizingPath + objc.NSURL? get URLByStandardizingPath { + objc.checkOsVersionInternal( + 'NSURL.URLByStandardizingPath', + iOS: (false, (4, 0, 0)), + macOS: (false, (10, 6, 0)), + ); + final _ret = _objc_msgSend_151sglz( + this.ref.pointer, + _sel_URLByStandardizingPath, + ); + return _ret.address == 0 + ? null + : objc.NSURL.castFromPointer(_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( + this.ref.pointer, + _sel_URLByResolvingSymlinksInPath, + ); + return _ret.address == 0 + ? null + : objc.NSURL.castFromPointer(_ret, retain: true, release: true); + } +} + +late final _class_NSFileManager = objc.getClass("NSFileManager"); +late final _sel_defaultManager = objc.registerName("defaultManager"); +late final _sel_containerURLForSecurityApplicationGroupIdentifier_ = objc + .registerName("containerURLForSecurityApplicationGroupIdentifier:"); + +/// NSFileManager +class NSFileManager extends objc.NSObject { + NSFileManager._( + ffi.Pointer pointer, { + bool retain = false, + bool release = false, + }) : super.castFromPointer(pointer, retain: retain, release: release); + + /// Constructs a [NSFileManager] that points to the same underlying object as [other]. + NSFileManager.castFrom(objc.ObjCObjectBase other) + : this._(other.ref.pointer, retain: true, release: true); + + /// Constructs a [NSFileManager] that wraps the given raw object pointer. + NSFileManager.castFromPointer( + ffi.Pointer other, { + bool retain = false, + bool release = false, + }) : this._(other, retain: retain, release: release); + + /// Returns whether [obj] is an instance of [NSFileManager]. + static bool isInstance(objc.ObjCObjectBase obj) { + return _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.castFromPointer(_ret, retain: true, release: true); + } + + /// 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( + this.ref.pointer, + _sel_containerURLForSecurityApplicationGroupIdentifier_, + groupIdentifier.ref.pointer, + ); + return _ret.address == 0 + ? null + : objc.NSURL.castFromPointer(_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..170fea24020 --- /dev/null +++ b/packages/path_provider/path_provider_foundation/lib/src/path_provider_foundation_real.dart @@ -0,0 +1,176 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart: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 NSFileManager Function()? fileManagerProvider, + }) : _platformProvider = platform ?? PathProviderPlatformProvider(), + _ffiLib = ffiLib ?? _lib, + _fileManagerProvider = + fileManagerProvider ?? _defaultFileManagerProvider; + + final PathProviderPlatformProvider _platformProvider; + final FoundationFFI _ffiLib; + final NSFileManager Function() _fileManagerProvider; + + /// 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 _fileManagerProvider() + .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 ObjCObjectBase? first = paths.firstObject; + return first == null ? null : NSString.castFrom(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; +} + +NSFileManager _defaultFileManagerProvider() => + NSFileManager.getDefaultManager(); + +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..af32e00366e --- /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. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package: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 1236b63caf3..00000000000 --- a/packages/path_provider/path_provider_foundation/pigeons/copyright.txt +++ /dev/null @@ -1,3 +0,0 @@ -Copyright 2013 The Flutter Authors. All rights reserved. -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 c1028d06df0..00000000000 --- a/packages/path_provider/path_provider_foundation/pigeons/messages.dart +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -import 'package: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', - dartTestOut: 'test/messages_test.g.dart', - copyrightHeader: 'pigeons/copyright.txt', - ), -) -enum DirectoryType { - applicationDocuments, - applicationSupport, - downloads, - library, - temp, - applicationCache, -} - -@HostApi(dartHostTestHandler: 'TestPathProviderApi') -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 d7ff1920389..7abed9cd652 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.2 +version: 2.5.0 environment: sdk: ^3.7.0 @@ -13,26 +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.3 flutter: sdk: flutter + objective_c: ^8.0.0 path_provider_platform_interface: ^2.1.0 dev_dependencies: - build_runner: ^2.3.2 + ffigen: ^19.0.0 flutter_test: sdk: flutter - mockito: ^5.4.4 - path: ^1.8.0 - pigeon: ^25.5.0 topics: - files diff --git a/packages/path_provider/path_provider_foundation/test/messages_test.g.dart b/packages/path_provider/path_provider_foundation/test/messages_test.g.dart deleted file mode 100644 index c5ae64af05d..00000000000 --- a/packages/path_provider/path_provider_foundation/test/messages_test.g.dart +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -// Autogenerated from Pigeon (v25.5.0), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import, no_leading_underscores_for_local_identifiers -// ignore_for_file: avoid_relative_lib_imports -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; -import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:path_provider_foundation/messages.g.dart'; - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else if (value is DirectoryType) { - buffer.putUint8(129); - writeValue(buffer, value.index); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 129: - final int? value = readValue(buffer) as int?; - return value == null ? null : DirectoryType.values[value]; - default: - return super.readValueOfType(type, buffer); - } - } -} - -abstract class TestPathProviderApi { - static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => - TestDefaultBinaryMessengerBinding.instance; - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - String? getDirectoryPath(DirectoryType type); - - String? getContainerPath(String appGroupIdentifier); - - static void setUp( - TestPathProviderApi? api, { - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) { - messageChannelSuffix = - messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - { - final BasicMessageChannel - pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.path_provider_foundation.PathProviderApi.getDirectoryPath$messageChannelSuffix', - pigeonChannelCodec, - binaryMessenger: binaryMessenger, - ); - if (api == null) { - _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(pigeonVar_channel, null); - } else { - _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(pigeonVar_channel, ( - Object? message, - ) async { - assert( - message != null, - 'Argument for dev.flutter.pigeon.path_provider_foundation.PathProviderApi.getDirectoryPath was null.', - ); - final List args = (message as List?)!; - final DirectoryType? arg_type = (args[0] as DirectoryType?); - assert( - arg_type != null, - 'Argument for dev.flutter.pigeon.path_provider_foundation.PathProviderApi.getDirectoryPath was null, expected non-null DirectoryType.', - ); - try { - final String? output = api.getDirectoryPath(arg_type!); - return [output]; - } on PlatformException catch (e) { - return wrapResponse(error: e); - } catch (e) { - return wrapResponse( - error: PlatformException( - code: 'error', - message: e.toString(), - ), - ); - } - }); - } - } - { - final BasicMessageChannel - pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.path_provider_foundation.PathProviderApi.getContainerPath$messageChannelSuffix', - pigeonChannelCodec, - binaryMessenger: binaryMessenger, - ); - if (api == null) { - _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(pigeonVar_channel, null); - } else { - _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(pigeonVar_channel, ( - Object? message, - ) async { - assert( - message != null, - 'Argument for dev.flutter.pigeon.path_provider_foundation.PathProviderApi.getContainerPath was null.', - ); - final List args = (message as List?)!; - final String? arg_appGroupIdentifier = (args[0] as String?); - assert( - arg_appGroupIdentifier != null, - 'Argument for dev.flutter.pigeon.path_provider_foundation.PathProviderApi.getContainerPath was null, expected non-null String.', - ); - try { - final String? output = api.getContainerPath( - arg_appGroupIdentifier!, - ); - return [output]; - } on PlatformException catch (e) { - return wrapResponse(error: e); - } catch (e) { - return wrapResponse( - error: PlatformException( - code: 'error', - message: e.toString(), - ), - ); - } - }); - } - } - } -} 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 bc181296c9c..a85b663ea07 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 @@ -5,21 +5,17 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.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'; -import 'messages_test.g.dart'; -import 'path_provider_foundation_test.mocks.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. -@GenerateMocks([TestPathProviderApi]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('PathProviderFoundation', () { - late MockTestPathProviderApi mockApi; // 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. @@ -27,154 +23,23 @@ void main() { setUp(() async { testRoot = Directory.systemTemp.createTempSync(); - mockApi = MockTestPathProviderApi(); - TestPathProviderApi.setUp(mockApi); }); tearDown(() { testRoot.deleteSync(recursive: true); }); - test('getTemporaryPath', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation(); - final String temporaryPath = p.join(testRoot.path, 'temporary', 'path'); - when( - mockApi.getDirectoryPath(DirectoryType.temp), - ).thenReturn(temporaryPath); - - final String? path = await pathProvider.getTemporaryPath(); - - verify(mockApi.getDirectoryPath(DirectoryType.temp)); - expect(path, temporaryPath); - }); - - test('getApplicationSupportPath', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation(); - final String applicationSupportPath = p.join( - testRoot.path, - 'application', - 'support', - 'path', - ); - when( - mockApi.getDirectoryPath(DirectoryType.applicationSupport), - ).thenReturn(applicationSupportPath); - - final String? path = await pathProvider.getApplicationSupportPath(); - - verify(mockApi.getDirectoryPath(DirectoryType.applicationSupport)); - expect(path, applicationSupportPath); - }); - - test( - 'getApplicationSupportPath creates the directory if necessary', - () async { - final PathProviderFoundation pathProvider = PathProviderFoundation(); - final String applicationSupportPath = p.join( - testRoot.path, - 'application', - 'support', - 'path', - ); - when( - mockApi.getDirectoryPath(DirectoryType.applicationSupport), - ).thenReturn(applicationSupportPath); - - final String? path = await pathProvider.getApplicationSupportPath(); - - expect(Directory(path!).existsSync(), isTrue); - }, - ); - - test('getLibraryPath', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation(); - final String libraryPath = p.join(testRoot.path, 'library', 'path'); - when( - mockApi.getDirectoryPath(DirectoryType.library), - ).thenReturn(libraryPath); - - final String? path = await pathProvider.getLibraryPath(); - - verify(mockApi.getDirectoryPath(DirectoryType.library)); - expect(path, libraryPath); - }); - - test('getApplicationDocumentsPath', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation(); - final String applicationDocumentsPath = p.join( - testRoot.path, - 'application', - 'documents', - 'path', - ); - when( - mockApi.getDirectoryPath(DirectoryType.applicationDocuments), - ).thenReturn(applicationDocumentsPath); - - final String? path = await pathProvider.getApplicationDocumentsPath(); - - verify(mockApi.getDirectoryPath(DirectoryType.applicationDocuments)); - expect(path, applicationDocumentsPath); - }); - - test('getApplicationCachePath', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation(); - final String applicationCachePath = p.join( - testRoot.path, - 'application', - 'cache', - 'path', - ); - when( - mockApi.getDirectoryPath(DirectoryType.applicationCache), - ).thenReturn(applicationCachePath); - - final String? path = await pathProvider.getApplicationCachePath(); - - verify(mockApi.getDirectoryPath(DirectoryType.applicationCache)); - expect(path, applicationCachePath); - }); - - test( - 'getApplicationCachePath creates the directory if necessary', - () async { - final PathProviderFoundation pathProvider = PathProviderFoundation(); - final String applicationCachePath = p.join( - testRoot.path, - 'application', - 'cache', - 'path', - ); - when( - mockApi.getDirectoryPath(DirectoryType.applicationCache), - ).thenReturn(applicationCachePath); - - final String? path = await pathProvider.getApplicationCachePath(); - - expect(Directory(path!).existsSync(), isTrue); - }, - ); - - test('getDownloadsPath', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation(); - final String downloadsPath = p.join(testRoot.path, 'downloads', 'path'); - when( - mockApi.getDirectoryPath(DirectoryType.downloads), - ).thenReturn(downloadsPath); - - final String? result = await pathProvider.getDownloadsPath(); - - verify(mockApi.getDirectoryPath(DirectoryType.downloads)); - expect(result, downloadsPath); - }); - test('getExternalCachePaths throws', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation(); + final PathProviderFoundation pathProvider = PathProviderFoundation( + ffiLib: FakeFoundationFFI(), + ); expect(pathProvider.getExternalCachePaths(), throwsA(isUnsupportedError)); }); test('getExternalStoragePath throws', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation(); + final PathProviderFoundation pathProvider = PathProviderFoundation( + ffiLib: FakeFoundationFFI(), + ); expect( pathProvider.getExternalStoragePath(), throwsA(isUnsupportedError), @@ -182,35 +47,19 @@ void main() { }); test('getExternalStoragePaths throws', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation(); + final PathProviderFoundation pathProvider = PathProviderFoundation( + ffiLib: FakeFoundationFFI(), + ); expect( pathProvider.getExternalStoragePaths(), throwsA(isUnsupportedError), ); }); - test('getContainerPath', () async { - final PathProviderFoundation pathProvider = PathProviderFoundation( - platform: FakePlatformProvider(isIOS: true), - ); - const String appGroupIdentifier = 'group.example.test'; - - final String containerPath = p.join(testRoot.path, 'container', 'path'); - when( - mockApi.getContainerPath(appGroupIdentifier), - ).thenReturn(containerPath); - - final String? result = await pathProvider.getContainerPath( - appGroupIdentifier: appGroupIdentifier, - ); - - verify(mockApi.getContainerPath(appGroupIdentifier)); - expect(result, containerPath); - }); - test('getContainerPath throws on macOS', () async { final PathProviderFoundation pathProvider = PathProviderFoundation( - platform: FakePlatformProvider(isIOS: false), + platform: FakePlatformProvider(isMacOS: true), + ffiLib: FakeFoundationFFI(), ); expect( pathProvider.getContainerPath(appGroupIdentifier: 'group.example.test'), @@ -220,9 +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; + + @override + bool isMacOS; +} + +class FakeFoundationFFI implements FoundationFFI { + @override + // ignore: non_constant_identifier_names + NSArray NSSearchPathForDirectoriesInDomains( + NSSearchPathDirectory directory, + NSSearchPathDomainMask domainMask, + bool expandTilde, + ) { + throw UnimplementedError(); + } } diff --git a/packages/path_provider/path_provider_foundation/test/path_provider_foundation_test.mocks.dart b/packages/path_provider/path_provider_foundation/test/path_provider_foundation_test.mocks.dart deleted file mode 100644 index 93cb1ad2a48..00000000000 --- a/packages/path_provider/path_provider_foundation/test/path_provider_foundation_test.mocks.dart +++ /dev/null @@ -1,44 +0,0 @@ -// Mocks generated by Mockito 5.4.4 from annotations -// in path_provider_foundation/test/path_provider_foundation_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:path_provider_foundation/messages.g.dart' as _i3; - -import 'messages_test.g.dart' as _i2; - -// 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: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class - -/// A class which mocks [TestPathProviderApi]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockTestPathProviderApi extends _i1.Mock - implements _i2.TestPathProviderApi { - MockTestPathProviderApi() { - _i1.throwOnMissingStub(this); - } - - @override - String? getDirectoryPath(_i3.DirectoryType? type) => - (super.noSuchMethod(Invocation.method(#getDirectoryPath, [type])) - as String?); - - @override - String? getContainerPath(String? appGroupIdentifier) => - (super.noSuchMethod( - Invocation.method(#getContainerPath, [appGroupIdentifier]), - ) - as String?); -}