[file_selector] Add `getDirectoryPaths` (#3871)

Exposes the new `getDirectoryPaths` API, and updates the desktop implementation package constraints to ensure that the implementations are present.

This is a slightly updated recreation of
https://github.com/flutter/plugins/pull/6576

Fixes https://github.com/flutter/flutter/issues/74323
diff --git a/packages/file_selector/file_selector/CHANGELOG.md b/packages/file_selector/file_selector/CHANGELOG.md
index ed6ce16..0111f0d 100644
--- a/packages/file_selector/file_selector/CHANGELOG.md
+++ b/packages/file_selector/file_selector/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.9.3
+
+* Adds `getDirectoryPaths` for selecting multiple directories.
+
 ## 0.9.2+5
 
 * Updates references to the deprecated `macUTIs`.
diff --git a/packages/file_selector/file_selector/example/lib/get_multiple_directories_page.dart b/packages/file_selector/file_selector/example/lib/get_multiple_directories_page.dart
new file mode 100644
index 0000000..bdae92f
--- /dev/null
+++ b/packages/file_selector/file_selector/example/lib/get_multiple_directories_page.dart
@@ -0,0 +1,89 @@
+// 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:file_selector/file_selector.dart';
+import 'package:flutter/material.dart';
+
+/// Screen that allows the user to select one or more directories using `getDirectoryPaths`,
+/// then displays the selected directories in a dialog.
+class GetMultipleDirectoriesPage extends StatelessWidget {
+  /// Returns a new instance of the page.
+  const GetMultipleDirectoriesPage({super.key});
+
+  Future<void> _getDirectoryPaths(BuildContext context) async {
+    const String confirmButtonText = 'Choose';
+    final List<String?> directoryPaths = await getDirectoryPaths(
+      confirmButtonText: confirmButtonText,
+    );
+    if (directoryPaths.isEmpty) {
+      // Operation was canceled by the user.
+      return;
+    }
+    String paths = '';
+    for (final String? path in directoryPaths) {
+      paths += '${path!} \n';
+    }
+    if (context.mounted) {
+      await showDialog<void>(
+        context: context,
+        builder: (BuildContext context) => TextDisplay(paths),
+      );
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Select multiple directories'),
+      ),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            ElevatedButton(
+              style: ElevatedButton.styleFrom(
+                // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+                // ignore: deprecated_member_use
+                primary: Colors.blue,
+                // ignore: deprecated_member_use
+                onPrimary: Colors.white,
+              ),
+              child: const Text(
+                  'Press to ask user to choose multiple directories'),
+              onPressed: () => _getDirectoryPaths(context),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+/// Widget that displays a text file in a dialog.
+class TextDisplay extends StatelessWidget {
+  /// Creates a `TextDisplay`.
+  const TextDisplay(this.directoriesPaths, {super.key});
+
+  /// The path selected in the dialog.
+  final String directoriesPaths;
+
+  @override
+  Widget build(BuildContext context) {
+    return AlertDialog(
+      title: const Text('Selected Directories'),
+      content: Scrollbar(
+        child: SingleChildScrollView(
+          child: Text(directoriesPaths),
+        ),
+      ),
+      actions: <Widget>[
+        TextButton(
+          child: const Text('Close'),
+          onPressed: () => Navigator.pop(context),
+        ),
+      ],
+    );
+  }
+}
diff --git a/packages/file_selector/file_selector/example/lib/home_page.dart b/packages/file_selector/file_selector/example/lib/home_page.dart
index a532dc8..052dba0 100644
--- a/packages/file_selector/file_selector/example/lib/home_page.dart
+++ b/packages/file_selector/file_selector/example/lib/home_page.dart
@@ -55,6 +55,13 @@
               child: const Text('Open a get directory dialog'),
               onPressed: () => Navigator.pushNamed(context, '/directory'),
             ),
+            const SizedBox(height: 10),
+            ElevatedButton(
+              style: style,
+              child: const Text('Open a get multi directories dialog'),
+              onPressed: () =>
+                  Navigator.pushNamed(context, '/multi-directories'),
+            ),
           ],
         ),
       ),
diff --git a/packages/file_selector/file_selector/example/lib/main.dart b/packages/file_selector/file_selector/example/lib/main.dart
index 27b34e8..19ef8c0 100644
--- a/packages/file_selector/file_selector/example/lib/main.dart
+++ b/packages/file_selector/file_selector/example/lib/main.dart
@@ -5,6 +5,7 @@
 import 'package:flutter/material.dart';
 
 import 'get_directory_page.dart';
+import 'get_multiple_directories_page.dart';
 import 'home_page.dart';
 import 'open_image_page.dart';
 import 'open_multiple_images_page.dart';
@@ -36,6 +37,8 @@
         '/open/text': (BuildContext context) => const OpenTextPage(),
         '/save/text': (BuildContext context) => SaveTextPage(),
         '/directory': (BuildContext context) => GetDirectoryPage(),
+        '/multi-directories': (BuildContext context) =>
+            const GetMultipleDirectoriesPage()
       },
     );
   }
diff --git a/packages/file_selector/file_selector/lib/file_selector.dart b/packages/file_selector/file_selector/lib/file_selector.dart
index f357af0..c224956 100644
--- a/packages/file_selector/file_selector/lib/file_selector.dart
+++ b/packages/file_selector/file_selector/lib/file_selector.dart
@@ -106,6 +106,7 @@
 }
 
 /// Opens a directory selection dialog and returns the path chosen by the user.
+///
 /// This always returns `null` on the web.
 ///
 /// [initialDirectory] is the full path to the directory that will be displayed
@@ -123,3 +124,24 @@
   return FileSelectorPlatform.instance.getDirectoryPath(
       initialDirectory: initialDirectory, confirmButtonText: confirmButtonText);
 }
+
+/// Opens a directory selection dialog and returns a list of the paths chosen
+/// by the user.
+///
+/// This always returns an empty array on the web.
+///
+/// [initialDirectory] is the full path to the directory that will be displayed
+/// when the dialog is opened. When not provided, the platform will pick an
+/// initial location.
+///
+/// [confirmButtonText] is the text in the confirmation button of the dialog.
+/// When not provided, the default OS label is used (for example, "Open").
+///
+/// Returns an empty array if the user cancels the operation.
+Future<List<String?>> getDirectoryPaths({
+  String? initialDirectory,
+  String? confirmButtonText,
+}) async {
+  return FileSelectorPlatform.instance.getDirectoryPaths(
+      initialDirectory: initialDirectory, confirmButtonText: confirmButtonText);
+}
diff --git a/packages/file_selector/file_selector/pubspec.yaml b/packages/file_selector/file_selector/pubspec.yaml
index becf6d3..00f11e1 100644
--- a/packages/file_selector/file_selector/pubspec.yaml
+++ b/packages/file_selector/file_selector/pubspec.yaml
@@ -3,11 +3,11 @@
   directories, using native file selection UI.
 repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22
-version: 0.9.2+5
+version: 0.9.3
 
 environment:
-  sdk: ">=2.17.0 <4.0.0"
-  flutter: ">=3.0.0"
+  sdk: ">=2.18.0 <4.0.0"
+  flutter: ">=3.3.0"
 
 flutter:
   plugin:
@@ -25,11 +25,11 @@
 
 dependencies:
   file_selector_ios: ^0.5.0
-  file_selector_linux: ^0.9.0
-  file_selector_macos: ^0.9.0
+  file_selector_linux: ^0.9.1
+  file_selector_macos: ^0.9.1
   file_selector_platform_interface: ^2.3.0
   file_selector_web: ^0.9.0
-  file_selector_windows: ^0.9.0
+  file_selector_windows: ^0.9.2
   flutter:
     sdk: flutter
 
diff --git a/packages/file_selector/file_selector/test/file_selector_test.dart b/packages/file_selector/file_selector/test/file_selector_test.dart
index 13c986b..cdcebe0 100644
--- a/packages/file_selector/file_selector/test/file_selector_test.dart
+++ b/packages/file_selector/file_selector/test/file_selector_test.dart
@@ -154,7 +154,7 @@
             confirmButtonText: confirmButtonText,
             acceptedTypeGroups: acceptedTypeGroups,
             suggestedName: suggestedName)
-        ..setPathResponse(expectedSavePath);
+        ..setPathsResponse(<String>[expectedSavePath]);
 
       final String? savePath = await getSavePath(
         initialDirectory: initialDirectory,
@@ -167,7 +167,7 @@
     });
 
     test('works with no arguments', () async {
-      fakePlatformImplementation.setPathResponse(expectedSavePath);
+      fakePlatformImplementation.setPathsResponse(<String>[expectedSavePath]);
 
       final String? savePath = await getSavePath();
       expect(savePath, expectedSavePath);
@@ -176,7 +176,7 @@
     test('sets the initial directory', () async {
       fakePlatformImplementation
         ..setExpectations(initialDirectory: initialDirectory)
-        ..setPathResponse(expectedSavePath);
+        ..setPathsResponse(<String>[expectedSavePath]);
 
       final String? savePath =
           await getSavePath(initialDirectory: initialDirectory);
@@ -186,7 +186,7 @@
     test('sets the button confirmation label', () async {
       fakePlatformImplementation
         ..setExpectations(confirmButtonText: confirmButtonText)
-        ..setPathResponse(expectedSavePath);
+        ..setPathsResponse(<String>[expectedSavePath]);
 
       final String? savePath =
           await getSavePath(confirmButtonText: confirmButtonText);
@@ -196,7 +196,7 @@
     test('sets the accepted type groups', () async {
       fakePlatformImplementation
         ..setExpectations(acceptedTypeGroups: acceptedTypeGroups)
-        ..setPathResponse(expectedSavePath);
+        ..setPathsResponse(<String>[expectedSavePath]);
 
       final String? savePath =
           await getSavePath(acceptedTypeGroups: acceptedTypeGroups);
@@ -206,7 +206,7 @@
     test('sets the suggested name', () async {
       fakePlatformImplementation
         ..setExpectations(suggestedName: suggestedName)
-        ..setPathResponse(expectedSavePath);
+        ..setPathsResponse(<String>[expectedSavePath]);
 
       final String? savePath = await getSavePath(suggestedName: suggestedName);
       expect(savePath, expectedSavePath);
@@ -221,7 +221,7 @@
         ..setExpectations(
             initialDirectory: initialDirectory,
             confirmButtonText: confirmButtonText)
-        ..setPathResponse(expectedDirectoryPath);
+        ..setPathsResponse(<String>[expectedDirectoryPath]);
 
       final String? directoryPath = await getDirectoryPath(
         initialDirectory: initialDirectory,
@@ -232,7 +232,8 @@
     });
 
     test('works with no arguments', () async {
-      fakePlatformImplementation.setPathResponse(expectedDirectoryPath);
+      fakePlatformImplementation
+          .setPathsResponse(<String>[expectedDirectoryPath]);
 
       final String? directoryPath = await getDirectoryPath();
       expect(directoryPath, expectedDirectoryPath);
@@ -241,7 +242,7 @@
     test('sets the initial directory', () async {
       fakePlatformImplementation
         ..setExpectations(initialDirectory: initialDirectory)
-        ..setPathResponse(expectedDirectoryPath);
+        ..setPathsResponse(<String>[expectedDirectoryPath]);
 
       final String? directoryPath =
           await getDirectoryPath(initialDirectory: initialDirectory);
@@ -251,13 +252,62 @@
     test('sets the button confirmation label', () async {
       fakePlatformImplementation
         ..setExpectations(confirmButtonText: confirmButtonText)
-        ..setPathResponse(expectedDirectoryPath);
+        ..setPathsResponse(<String>[expectedDirectoryPath]);
 
       final String? directoryPath =
           await getDirectoryPath(confirmButtonText: confirmButtonText);
       expect(directoryPath, expectedDirectoryPath);
     });
   });
+
+  group('getDirectoryPaths', () {
+    const List<String> expectedDirectoryPaths = <String>[
+      '/example/path',
+      '/example/2/path'
+    ];
+
+    test('works', () async {
+      fakePlatformImplementation
+        ..setExpectations(
+            initialDirectory: initialDirectory,
+            confirmButtonText: confirmButtonText)
+        ..setPathsResponse(expectedDirectoryPaths);
+
+      final List<String?> directoryPaths = await getDirectoryPaths(
+        initialDirectory: initialDirectory,
+        confirmButtonText: confirmButtonText,
+      );
+
+      expect(directoryPaths, expectedDirectoryPaths);
+    });
+
+    test('works with no arguments', () async {
+      fakePlatformImplementation.setPathsResponse(expectedDirectoryPaths);
+
+      final List<String?> directoryPaths = await getDirectoryPaths();
+      expect(directoryPaths, expectedDirectoryPaths);
+    });
+
+    test('sets the initial directory', () async {
+      fakePlatformImplementation
+        ..setExpectations(initialDirectory: initialDirectory)
+        ..setPathsResponse(expectedDirectoryPaths);
+
+      final List<String?> directoryPaths =
+          await getDirectoryPaths(initialDirectory: initialDirectory);
+      expect(directoryPaths, expectedDirectoryPaths);
+    });
+
+    test('sets the button confirmation label', () async {
+      fakePlatformImplementation
+        ..setExpectations(confirmButtonText: confirmButtonText)
+        ..setPathsResponse(expectedDirectoryPaths);
+
+      final List<String?> directoryPaths =
+          await getDirectoryPaths(confirmButtonText: confirmButtonText);
+      expect(directoryPaths, expectedDirectoryPaths);
+    });
+  });
 }
 
 class FakeFileSelector extends Fake
@@ -270,7 +320,7 @@
   String? suggestedName;
   // Return values.
   List<XFile>? files;
-  String? path;
+  List<String>? paths;
 
   void setExpectations({
     List<XTypeGroup> acceptedTypeGroups = const <XTypeGroup>[],
@@ -290,8 +340,8 @@
   }
 
   // ignore: use_setters_to_change_properties
-  void setPathResponse(String path) {
-    this.path = path;
+  void setPathsResponse(List<String> paths) {
+    this.paths = paths;
   }
 
   @override
@@ -329,7 +379,7 @@
     expect(initialDirectory, this.initialDirectory);
     expect(suggestedName, this.suggestedName);
     expect(confirmButtonText, this.confirmButtonText);
-    return path;
+    return paths?[0];
   }
 
   @override
@@ -339,6 +389,16 @@
   }) async {
     expect(initialDirectory, this.initialDirectory);
     expect(confirmButtonText, this.confirmButtonText);
-    return path;
+    return paths?[0];
+  }
+
+  @override
+  Future<List<String>> getDirectoryPaths({
+    String? initialDirectory,
+    String? confirmButtonText,
+  }) async {
+    expect(initialDirectory, this.initialDirectory);
+    expect(confirmButtonText, this.confirmButtonText);
+    return paths!;
   }
 }