[path_provider] Switch macOS to an internal method channel (#4547)

Eliminates the path_provider_macos reliance on the default, shared method channel implementation, in favor of an in-package implementation.

Now that it's trivial to do so, also moves the creation of directories when necessary to the Dart side, and unit tests it there.

Part of https://github.com/flutter/flutter/issues/94224
diff --git a/packages/path_provider/path_provider_macos/CHANGELOG.md b/packages/path_provider/path_provider_macos/CHANGELOG.md
index d88ed06..49c2058 100644
--- a/packages/path_provider/path_provider_macos/CHANGELOG.md
+++ b/packages/path_provider/path_provider_macos/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.0.4
+
+* Switches to a package-internal implementation of the platform interface.
+
 ## 2.0.3
 
 * Fixes link in README.
diff --git a/packages/path_provider/path_provider_macos/lib/path_provider_macos.dart b/packages/path_provider/path_provider_macos/lib/path_provider_macos.dart
new file mode 100644
index 0000000..6b3a6fa
--- /dev/null
+++ b/packages/path_provider/path_provider_macos/lib/path_provider_macos.dart
@@ -0,0 +1,72 @@
+// 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:io';
+
+import 'package:flutter/services.dart';
+import 'package:meta/meta.dart';
+import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
+
+/// The macOS implementation of [PathProviderPlatform].
+class PathProviderMacOS extends PathProviderPlatform {
+  /// The method channel used to interact with the native platform.
+  @visibleForTesting
+  MethodChannel methodChannel =
+      const MethodChannel('plugins.flutter.io/path_provider_macos');
+
+  /// Registers this class as the default instance of [PathProviderPlatform]
+  static void registerWith() {
+    PathProviderPlatform.instance = PathProviderMacOS();
+  }
+
+  @override
+  Future<String?> getTemporaryPath() {
+    return methodChannel.invokeMethod<String>('getTemporaryDirectory');
+  }
+
+  @override
+  Future<String?> getApplicationSupportPath() async {
+    final String? path = await methodChannel
+        .invokeMethod<String>('getApplicationSupportDirectory');
+    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<String?> getLibraryPath() {
+    return methodChannel.invokeMethod<String>('getLibraryDirectory');
+  }
+
+  @override
+  Future<String?> getApplicationDocumentsPath() {
+    return methodChannel
+        .invokeMethod<String>('getApplicationDocumentsDirectory');
+  }
+
+  @override
+  Future<String?> getExternalStoragePath() async {
+    throw UnsupportedError('getExternalStoragePath is not supported on macOS');
+  }
+
+  @override
+  Future<List<String>?> getExternalCachePaths() async {
+    throw UnsupportedError('getExternalCachePaths is not supported on macOS');
+  }
+
+  @override
+  Future<List<String>?> getExternalStoragePaths({
+    StorageDirectory? type,
+  }) async {
+    throw UnsupportedError('getExternalStoragePaths is not supported on macOS');
+  }
+
+  @override
+  Future<String?> getDownloadsPath() {
+    return methodChannel.invokeMethod<String>('getDownloadsDirectory');
+  }
+}
diff --git a/packages/path_provider/path_provider_macos/macos/Classes/PathProviderPlugin.swift b/packages/path_provider/path_provider_macos/macos/Classes/PathProviderPlugin.swift
index b308793..e138eee 100644
--- a/packages/path_provider/path_provider_macos/macos/Classes/PathProviderPlugin.swift
+++ b/packages/path_provider/path_provider_macos/macos/Classes/PathProviderPlugin.swift
@@ -8,7 +8,7 @@
 public class PathProviderPlugin: NSObject, FlutterPlugin {
   public static func register(with registrar: FlutterPluginRegistrar) {
     let channel = FlutterMethodChannel(
-      name: "plugins.flutter.io/path_provider",
+      name: "plugins.flutter.io/path_provider_macos",
       binaryMessenger: registrar.messenger)
     let instance = PathProviderPlugin()
     registrar.addMethodCallDelegate(instance, channel: channel)
@@ -25,16 +25,6 @@
       if let basePath = path {
         let basePathURL = URL.init(fileURLWithPath: basePath)
         path = basePathURL.appendingPathComponent(Bundle.main.bundleIdentifier!).path
-        do {
-          try FileManager.default.createDirectory(atPath: path!, withIntermediateDirectories: true)
-        } catch {
-          result(
-            FlutterError(
-              code: "directory_creation_failure",
-              message: error.localizedDescription,
-              details: "\(error)"))
-          return
-        }
       }
       result(path)
     case "getLibraryDirectory":
diff --git a/packages/path_provider/path_provider_macos/pubspec.yaml b/packages/path_provider/path_provider_macos/pubspec.yaml
index ac6011d..9eb1bb4 100644
--- a/packages/path_provider/path_provider_macos/pubspec.yaml
+++ b/packages/path_provider/path_provider_macos/pubspec.yaml
@@ -2,7 +2,7 @@
 description: macOS implementation of the path_provider plugin
 repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_macos
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22
-version: 2.0.3
+version: 2.0.4
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
@@ -14,10 +14,16 @@
     platforms:
       macos:
         pluginClass: PathProviderPlugin
+        dartPluginClass: PathProviderMacOS
 
 dependencies:
   flutter:
     sdk: flutter
+  meta: ^1.3.0
+  path_provider_platform_interface: ^2.0.1
 
 dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  path: ^1.8.0
   pedantic: ^1.10.0
diff --git a/packages/path_provider/path_provider_macos/test/path_provider_macos_test.dart b/packages/path_provider/path_provider_macos/test/path_provider_macos_test.dart
new file mode 100644
index 0000000..7e783aa
--- /dev/null
+++ b/packages/path_provider/path_provider_macos/test/path_provider_macos_test.dart
@@ -0,0 +1,137 @@
+// 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:io';
+
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:path/path.dart' as p;
+import 'package:path_provider_macos/path_provider_macos.dart';
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  group('PathProviderMacOS', () {
+    late PathProviderMacOS pathProvider;
+    late List<MethodCall> log;
+    // 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.
+    late Directory testRoot;
+
+    late String temporaryPath;
+    late String applicationSupportPath;
+    late String libraryPath;
+    late String applicationDocumentsPath;
+    late String downloadsPath;
+
+    setUp(() async {
+      pathProvider = PathProviderMacOS();
+
+      testRoot = Directory.systemTemp.createTempSync();
+      final String basePath = testRoot.path;
+      temporaryPath = p.join(basePath, 'temporary', 'path');
+      applicationSupportPath =
+          p.join(basePath, 'application', 'support', 'path');
+      libraryPath = p.join(basePath, 'library', 'path');
+      applicationDocumentsPath =
+          p.join(basePath, 'application', 'documents', 'path');
+      downloadsPath = p.join(basePath, 'downloads', 'path');
+
+      log = <MethodCall>[];
+      TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
+          .setMockMethodCallHandler(pathProvider.methodChannel,
+              (MethodCall methodCall) async {
+        log.add(methodCall);
+        switch (methodCall.method) {
+          case 'getTemporaryDirectory':
+            return temporaryPath;
+          case 'getApplicationSupportDirectory':
+            return applicationSupportPath;
+          case 'getLibraryDirectory':
+            return libraryPath;
+          case 'getApplicationDocumentsDirectory':
+            return applicationDocumentsPath;
+          case 'getDownloadsDirectory':
+            return downloadsPath;
+          default:
+            return null;
+        }
+      });
+    });
+
+    tearDown(() {
+      testRoot.deleteSync(recursive: true);
+    });
+
+    test('getTemporaryPath', () async {
+      final String? path = await pathProvider.getTemporaryPath();
+      expect(
+        log,
+        <Matcher>[isMethodCall('getTemporaryDirectory', arguments: null)],
+      );
+      expect(path, temporaryPath);
+    });
+
+    test('getApplicationSupportPath', () async {
+      final String? path = await pathProvider.getApplicationSupportPath();
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('getApplicationSupportDirectory', arguments: null)
+        ],
+      );
+      expect(path, applicationSupportPath);
+    });
+
+    test('getApplicationSupportPath creates the directory if necessary',
+        () async {
+      final String? path = await pathProvider.getApplicationSupportPath();
+      expect(Directory(path!).existsSync(), isTrue);
+    });
+
+    test('getLibraryPath', () async {
+      final String? path = await pathProvider.getLibraryPath();
+      expect(
+        log,
+        <Matcher>[isMethodCall('getLibraryDirectory', arguments: null)],
+      );
+      expect(path, libraryPath);
+    });
+
+    test('getApplicationDocumentsPath', () async {
+      final String? path = await pathProvider.getApplicationDocumentsPath();
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('getApplicationDocumentsDirectory', arguments: null)
+        ],
+      );
+      expect(path, applicationDocumentsPath);
+    });
+
+    test('getDownloadsPath', () async {
+      final String? result = await pathProvider.getDownloadsPath();
+      expect(
+        log,
+        <Matcher>[isMethodCall('getDownloadsDirectory', arguments: null)],
+      );
+      expect(result, downloadsPath);
+    });
+
+    test('getExternalCachePaths throws', () async {
+      expect(pathProvider.getExternalCachePaths(), throwsA(isUnsupportedError));
+    });
+
+    test('getExternalStoragePath throws', () async {
+      expect(
+          pathProvider.getExternalStoragePath(), throwsA(isUnsupportedError));
+    });
+
+    test('getExternalStoragePaths throws', () async {
+      expect(
+          pathProvider.getExternalStoragePaths(), throwsA(isUnsupportedError));
+    });
+  });
+}