[file_selector] Add file group to save return value - implementations (#4273)

Implementation package portion of https://github.com/flutter/packages/pull/4222

Part of https://github.com/flutter/flutter/issues/107093
diff --git a/packages/file_selector/file_selector_linux/CHANGELOG.md b/packages/file_selector/file_selector_linux/CHANGELOG.md
index 8888630..5c9dc1d 100644
--- a/packages/file_selector/file_selector_linux/CHANGELOG.md
+++ b/packages/file_selector/file_selector_linux/CHANGELOG.md
@@ -1,5 +1,6 @@
-## NEXT
+## 0.9.2
 
+* Adds `getSaveLocation` and deprecates `getSavePath`.
 * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18.
 
 ## 0.9.1+3
diff --git a/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart b/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart
index 2d259a6..bfd83cf 100644
--- a/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart
+++ b/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart
@@ -17,14 +17,12 @@
 
   Future<void> _saveFile() async {
     final String fileName = _nameController.text;
-    // TODO(stuartmorgan): Update this to getSaveLocation in the next federated
-    // change PR.
-    // ignore: deprecated_member_use
-    final String? path = await FileSelectorPlatform.instance.getSavePath(
-      // Operation was canceled by the user.
-      suggestedName: fileName,
+    final FileSaveLocation? result =
+        await FileSelectorPlatform.instance.getSaveLocation(
+      options: SaveDialogOptions(suggestedName: fileName),
     );
-    if (path == null) {
+    // Operation was canceled by the user.
+    if (result == null) {
       return;
     }
     final String text = _contentController.text;
@@ -32,7 +30,7 @@
     const String fileMimeType = 'text/plain';
     final XFile textFile =
         XFile.fromData(fileData, mimeType: fileMimeType, name: fileName);
-    await textFile.saveTo(path);
+    await textFile.saveTo(result.path);
   }
 
   @override
diff --git a/packages/file_selector/file_selector_linux/example/pubspec.yaml b/packages/file_selector/file_selector_linux/example/pubspec.yaml
index 1596eb3..da7410f 100644
--- a/packages/file_selector/file_selector_linux/example/pubspec.yaml
+++ b/packages/file_selector/file_selector_linux/example/pubspec.yaml
@@ -10,7 +10,7 @@
 dependencies:
   file_selector_linux:
     path: ../
-  file_selector_platform_interface: ^2.4.0
+  file_selector_platform_interface: ^2.6.0
   flutter:
     sdk: flutter
 
diff --git a/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart b/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart
index b8e3df6..b06523b 100644
--- a/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart
+++ b/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart
@@ -83,18 +83,36 @@
     String? suggestedName,
     String? confirmButtonText,
   }) async {
+    final FileSaveLocation? location = await getSaveLocation(
+        acceptedTypeGroups: acceptedTypeGroups,
+        options: SaveDialogOptions(
+          initialDirectory: initialDirectory,
+          suggestedName: suggestedName,
+          confirmButtonText: confirmButtonText,
+        ));
+    return location?.path;
+  }
+
+  @override
+  Future<FileSaveLocation?> getSaveLocation({
+    List<XTypeGroup>? acceptedTypeGroups,
+    SaveDialogOptions options = const SaveDialogOptions(),
+  }) async {
     final List<Map<String, Object>> serializedTypeGroups =
         _serializeTypeGroups(acceptedTypeGroups);
-    return _channel.invokeMethod<String>(
+    // TODO(stuartmorgan): Add the selected type group here and return it. See
+    // https://github.com/flutter/flutter/issues/107093
+    final String? path = await _channel.invokeMethod<String>(
       _getSavePathMethod,
       <String, dynamic>{
         if (serializedTypeGroups.isNotEmpty)
           _acceptedTypeGroupsKey: serializedTypeGroups,
-        _initialDirectoryKey: initialDirectory,
-        _suggestedNameKey: suggestedName,
-        _confirmButtonTextKey: confirmButtonText,
+        _initialDirectoryKey: options.initialDirectory,
+        _suggestedNameKey: options.suggestedName,
+        _confirmButtonTextKey: options.confirmButtonText,
       },
     );
+    return path == null ? null : FileSaveLocation(path);
   }
 
   @override
diff --git a/packages/file_selector/file_selector_linux/pubspec.yaml b/packages/file_selector/file_selector_linux/pubspec.yaml
index 238e0ae..0c9909f 100644
--- a/packages/file_selector/file_selector_linux/pubspec.yaml
+++ b/packages/file_selector/file_selector_linux/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Liunx implementation of the file_selector plugin.
 repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_linux
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22
-version: 0.9.1+3
+version: 0.9.2
 
 environment:
   sdk: ">=2.18.0 <4.0.0"
@@ -18,7 +18,7 @@
 
 dependencies:
   cross_file: ^0.3.1
-  file_selector_platform_interface: ^2.4.0
+  file_selector_platform_interface: ^2.6.0
   flutter:
     sdk: flutter
 
diff --git a/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart b/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart
index 5127d28..f6b2cc6 100644
--- a/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart
+++ b/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart
@@ -32,7 +32,7 @@
     expect(FileSelectorPlatform.instance, isA<FileSelectorLinux>());
   });
 
-  group('#openFile', () {
+  group('openFile', () {
     test('passes the accepted type groups correctly', () async {
       const XTypeGroup group = XTypeGroup(
         label: 'text',
@@ -135,7 +135,7 @@
     });
   });
 
-  group('#openFiles', () {
+  group('openFiles', () {
     test('passes the accepted type groups correctly', () async {
       const XTypeGroup group = XTypeGroup(
         label: 'text',
@@ -209,7 +209,7 @@
       );
 
       await expectLater(
-          plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]),
+          plugin.openFiles(acceptedTypeGroups: <XTypeGroup>[group]),
           throwsArgumentError);
     });
 
@@ -218,7 +218,7 @@
         label: 'any',
       );
 
-      await plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]);
+      await plugin.openFiles(acceptedTypeGroups: <XTypeGroup>[group]);
 
       expectMethodCall(
         log,
@@ -232,13 +232,120 @@
           ],
           'initialDirectory': null,
           'confirmButtonText': null,
-          'multiple': false,
+          'multiple': true,
         },
       );
     });
   });
 
-  group('#getSavePath', () {
+  group('getSaveLocation', () {
+    test('passes the accepted type groups correctly', () async {
+      const XTypeGroup group = XTypeGroup(
+        label: 'text',
+        extensions: <String>['txt'],
+        mimeTypes: <String>['text/plain'],
+      );
+
+      const XTypeGroup groupTwo = XTypeGroup(
+        label: 'image',
+        extensions: <String>['jpg'],
+        mimeTypes: <String>['image/jpg'],
+      );
+
+      await plugin
+          .getSaveLocation(acceptedTypeGroups: <XTypeGroup>[group, groupTwo]);
+
+      expectMethodCall(
+        log,
+        'getSavePath',
+        arguments: <String, dynamic>{
+          'acceptedTypeGroups': <Map<String, dynamic>>[
+            <String, Object>{
+              'label': 'text',
+              'extensions': <String>['*.txt'],
+              'mimeTypes': <String>['text/plain'],
+            },
+            <String, Object>{
+              'label': 'image',
+              'extensions': <String>['*.jpg'],
+              'mimeTypes': <String>['image/jpg'],
+            },
+          ],
+          'initialDirectory': null,
+          'suggestedName': null,
+          'confirmButtonText': null,
+        },
+      );
+    });
+
+    test('passes initialDirectory correctly', () async {
+      await plugin.getSaveLocation(
+          options:
+              const SaveDialogOptions(initialDirectory: '/example/directory'));
+
+      expectMethodCall(
+        log,
+        'getSavePath',
+        arguments: <String, dynamic>{
+          'initialDirectory': '/example/directory',
+          'suggestedName': null,
+          'confirmButtonText': null,
+        },
+      );
+    });
+
+    test('passes confirmButtonText correctly', () async {
+      await plugin.getSaveLocation(
+          options: const SaveDialogOptions(confirmButtonText: 'Open File'));
+
+      expectMethodCall(
+        log,
+        'getSavePath',
+        arguments: <String, dynamic>{
+          'initialDirectory': null,
+          'suggestedName': null,
+          'confirmButtonText': 'Open File',
+        },
+      );
+    });
+
+    test('throws for a type group that does not support Linux', () async {
+      const XTypeGroup group = XTypeGroup(
+        label: 'images',
+        webWildCards: <String>['images/*'],
+      );
+
+      await expectLater(
+          plugin.getSaveLocation(acceptedTypeGroups: <XTypeGroup>[group]),
+          throwsArgumentError);
+    });
+
+    test('passes a wildcard group correctly', () async {
+      const XTypeGroup group = XTypeGroup(
+        label: 'any',
+      );
+
+      await plugin.getSaveLocation(acceptedTypeGroups: <XTypeGroup>[group]);
+
+      expectMethodCall(
+        log,
+        'getSavePath',
+        arguments: <String, dynamic>{
+          'acceptedTypeGroups': <Map<String, dynamic>>[
+            <String, Object>{
+              'label': 'any',
+              'extensions': <String>['*'],
+            },
+          ],
+          'initialDirectory': null,
+          'suggestedName': null,
+          'confirmButtonText': null,
+        },
+      );
+    });
+  });
+
+  group('getSavePath (deprecated)', () {
     test('passes the accepted type groups correctly', () async {
       const XTypeGroup group = XTypeGroup(
         label: 'text',
@@ -313,7 +420,7 @@
       );
 
       await expectLater(
-          plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]),
+          plugin.getSavePath(acceptedTypeGroups: <XTypeGroup>[group]),
           throwsArgumentError);
     });
 
@@ -322,11 +429,11 @@
         label: 'any',
       );
 
-      await plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]);
+      await plugin.getSavePath(acceptedTypeGroups: <XTypeGroup>[group]);
 
       expectMethodCall(
         log,
-        'openFile',
+        'getSavePath',
         arguments: <String, dynamic>{
           'acceptedTypeGroups': <Map<String, dynamic>>[
             <String, Object>{
@@ -335,14 +442,14 @@
             },
           ],
           'initialDirectory': null,
+          'suggestedName': null,
           'confirmButtonText': null,
-          'multiple': false,
         },
       );
     });
   });
 
-  group('#getDirectoryPath', () {
+  group('getDirectoryPath', () {
     test('passes initialDirectory correctly', () async {
       await plugin.getDirectoryPath(initialDirectory: '/example/directory');
 
@@ -369,7 +476,7 @@
     });
   });
 
-  group('#getDirectoryPaths', () {
+  group('getDirectoryPaths', () {
     test('passes initialDirectory correctly', () async {
       await plugin.getDirectoryPaths(initialDirectory: '/example/directory');
 
diff --git a/packages/file_selector/file_selector_macos/CHANGELOG.md b/packages/file_selector/file_selector_macos/CHANGELOG.md
index e697974..7a19783 100644
--- a/packages/file_selector/file_selector_macos/CHANGELOG.md
+++ b/packages/file_selector/file_selector_macos/CHANGELOG.md
@@ -1,5 +1,6 @@
-## NEXT
+## 0.9.3
 
+* Adds `getSaveLocation` and deprecates `getSavePath`.
 * Updates minimum supported macOS version to 10.14.
 
 ## 0.9.2
diff --git a/packages/file_selector/file_selector_macos/example/lib/save_text_page.dart b/packages/file_selector/file_selector_macos/example/lib/save_text_page.dart
index 84180ac..6208e16 100644
--- a/packages/file_selector/file_selector_macos/example/lib/save_text_page.dart
+++ b/packages/file_selector/file_selector_macos/example/lib/save_text_page.dart
@@ -17,13 +17,11 @@
 
   Future<void> _saveFile() async {
     final String fileName = _nameController.text;
-    // TODO(stuartmorgan): Update this to getSaveLocation in the next federated
-    // change PR.
-    // ignore: deprecated_member_use
-    final String? path = await FileSelectorPlatform.instance.getSavePath(
-      suggestedName: fileName,
+    final FileSaveLocation? result =
+        await FileSelectorPlatform.instance.getSaveLocation(
+      options: SaveDialogOptions(suggestedName: fileName),
     );
-    if (path == null) {
+    if (result == null) {
       // Operation was canceled by the user.
       return;
     }
@@ -32,7 +30,7 @@
     const String fileMimeType = 'text/plain';
     final XFile textFile =
         XFile.fromData(fileData, mimeType: fileMimeType, name: fileName);
-    await textFile.saveTo(path);
+    await textFile.saveTo(result.path);
   }
 
   @override
diff --git a/packages/file_selector/file_selector_macos/example/pubspec.yaml b/packages/file_selector/file_selector_macos/example/pubspec.yaml
index 9e2d831..baa27c5 100644
--- a/packages/file_selector/file_selector_macos/example/pubspec.yaml
+++ b/packages/file_selector/file_selector_macos/example/pubspec.yaml
@@ -15,7 +15,7 @@
     # The example app is bundled with the plugin so we use a path dependency on
     # the parent directory to use the current plugin's version.
     path: ..
-  file_selector_platform_interface: ^2.4.0
+  file_selector_platform_interface: ^2.6.0
   flutter:
     sdk: flutter
 
diff --git a/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart
index 293c1e2..3489938 100644
--- a/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart
+++ b/packages/file_selector/file_selector_macos/lib/file_selector_macos.dart
@@ -60,12 +60,28 @@
     String? suggestedName,
     String? confirmButtonText,
   }) async {
-    return _hostApi.displaySavePanel(SavePanelOptions(
+    final FileSaveLocation? location = await getSaveLocation(
+        acceptedTypeGroups: acceptedTypeGroups,
+        options: SaveDialogOptions(
+          initialDirectory: initialDirectory,
+          suggestedName: suggestedName,
+          confirmButtonText: confirmButtonText,
+        ));
+    return location?.path;
+  }
+
+  @override
+  Future<FileSaveLocation?> getSaveLocation({
+    List<XTypeGroup>? acceptedTypeGroups,
+    SaveDialogOptions options = const SaveDialogOptions(),
+  }) async {
+    final String? path = await _hostApi.displaySavePanel(SavePanelOptions(
       allowedFileTypes: _allowedTypesFromTypeGroups(acceptedTypeGroups),
-      directoryPath: initialDirectory,
-      nameFieldStringValue: suggestedName,
-      prompt: confirmButtonText,
+      directoryPath: options.initialDirectory,
+      nameFieldStringValue: options.suggestedName,
+      prompt: options.confirmButtonText,
     ));
+    return path == null ? null : FileSaveLocation(path);
   }
 
   @override
diff --git a/packages/file_selector/file_selector_macos/pubspec.yaml b/packages/file_selector/file_selector_macos/pubspec.yaml
index 731a5cc..047c7ca 100644
--- a/packages/file_selector/file_selector_macos/pubspec.yaml
+++ b/packages/file_selector/file_selector_macos/pubspec.yaml
@@ -2,7 +2,7 @@
 description: macOS implementation of the file_selector plugin.
 repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_macos
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22
-version: 0.9.2
+version: 0.9.3
 
 environment:
   sdk: ">=2.18.0 <4.0.0"
@@ -18,7 +18,7 @@
 
 dependencies:
   cross_file: ^0.3.1
-  file_selector_platform_interface: ^2.4.0
+  file_selector_platform_interface: ^2.6.0
   flutter:
     sdk: flutter
 
diff --git a/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart
index 6450e6f..c7268a6 100644
--- a/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart
+++ b/packages/file_selector/file_selector_macos/test/file_selector_macos_test.dart
@@ -227,7 +227,7 @@
     });
   });
 
-  group('getSavePath', () {
+  group('getSavePath (deprecated)', () {
     test('works as expected with no arguments', () async {
       when(mockApi.displaySavePanel(any)).thenAnswer((_) async => 'foo');
 
@@ -344,6 +344,126 @@
     });
   });
 
+  group('getSaveLocation', () {
+    test('works as expected with no arguments', () async {
+      when(mockApi.displaySavePanel(any)).thenAnswer((_) async => 'foo');
+
+      final FileSaveLocation? location = await plugin.getSaveLocation();
+
+      expect(location?.path, 'foo');
+      final VerificationResult result =
+          verify(mockApi.displaySavePanel(captureAny));
+      final SavePanelOptions options = result.captured[0] as SavePanelOptions;
+      expect(options.allowedFileTypes, null);
+      expect(options.directoryPath, null);
+      expect(options.nameFieldStringValue, null);
+      expect(options.prompt, null);
+    });
+
+    test('handles cancel', () async {
+      when(mockApi.displaySavePanel(any)).thenAnswer((_) async => null);
+
+      final FileSaveLocation? location = await plugin.getSaveLocation();
+
+      expect(location, null);
+    });
+
+    test('passes the accepted type groups correctly', () async {
+      const XTypeGroup group = XTypeGroup(
+        label: 'text',
+        extensions: <String>['txt'],
+        mimeTypes: <String>['text/plain'],
+        uniformTypeIdentifiers: <String>['public.text'],
+      );
+
+      const XTypeGroup groupTwo = XTypeGroup(
+          label: 'image',
+          extensions: <String>['jpg'],
+          mimeTypes: <String>['image/jpg'],
+          uniformTypeIdentifiers: <String>['public.image'],
+          webWildCards: <String>['image/*']);
+
+      await plugin
+          .getSaveLocation(acceptedTypeGroups: <XTypeGroup>[group, groupTwo]);
+
+      final VerificationResult result =
+          verify(mockApi.displaySavePanel(captureAny));
+      final SavePanelOptions options = result.captured[0] as SavePanelOptions;
+      expect(options.allowedFileTypes!.extensions, <String>['txt', 'jpg']);
+      expect(options.allowedFileTypes!.mimeTypes,
+          <String>['text/plain', 'image/jpg']);
+      expect(options.allowedFileTypes!.utis,
+          <String>['public.text', 'public.image']);
+    });
+
+    test('passes initialDirectory correctly', () async {
+      await plugin.getSaveLocation(
+          options:
+              const SaveDialogOptions(initialDirectory: '/example/directory'));
+
+      final VerificationResult result =
+          verify(mockApi.displaySavePanel(captureAny));
+      final SavePanelOptions options = result.captured[0] as SavePanelOptions;
+      expect(options.directoryPath, '/example/directory');
+    });
+
+    test('passes confirmButtonText correctly', () async {
+      await plugin.getSaveLocation(
+          options: const SaveDialogOptions(confirmButtonText: 'Open File'));
+
+      final VerificationResult result =
+          verify(mockApi.displaySavePanel(captureAny));
+      final SavePanelOptions options = result.captured[0] as SavePanelOptions;
+      expect(options.prompt, 'Open File');
+    });
+
+    test('throws for a type group that does not support macOS', () async {
+      const XTypeGroup group = XTypeGroup(
+        label: 'images',
+        webWildCards: <String>['images/*'],
+      );
+
+      await expectLater(
+          plugin.getSaveLocation(acceptedTypeGroups: <XTypeGroup>[group]),
+          throwsArgumentError);
+    });
+
+    test('allows a wildcard group', () async {
+      const XTypeGroup group = XTypeGroup(
+        label: 'text',
+      );
+
+      await expectLater(
+          plugin.getSaveLocation(acceptedTypeGroups: <XTypeGroup>[group]),
+          completes);
+    });
+
+    test('ignores all type groups if any of them is a wildcard', () async {
+      await plugin.getSaveLocation(acceptedTypeGroups: <XTypeGroup>[
+        const XTypeGroup(
+          label: 'text',
+          extensions: <String>['txt'],
+          mimeTypes: <String>['text/plain'],
+          uniformTypeIdentifiers: <String>['public.text'],
+        ),
+        const XTypeGroup(
+          label: 'image',
+          extensions: <String>['jpg'],
+          mimeTypes: <String>['image/jpg'],
+          uniformTypeIdentifiers: <String>['public.image'],
+        ),
+        const XTypeGroup(
+          label: 'any',
+        ),
+      ]);
+
+      final VerificationResult result =
+          verify(mockApi.displaySavePanel(captureAny));
+      final SavePanelOptions options = result.captured[0] as SavePanelOptions;
+      expect(options.allowedFileTypes, null);
+    });
+  });
+
   group('getDirectoryPath', () {
     test('works as expected with no arguments', () async {
       when(mockApi.displayOpenPanel(any))
diff --git a/packages/file_selector/file_selector_web/CHANGELOG.md b/packages/file_selector/file_selector_web/CHANGELOG.md
index 43ad496..073f1c2 100644
--- a/packages/file_selector/file_selector_web/CHANGELOG.md
+++ b/packages/file_selector/file_selector_web/CHANGELOG.md
@@ -1,5 +1,6 @@
-## NEXT
+## 0.9.1
 
+* Adds `getSaveLocation` and deprecates `getSavePath`.
 * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18.
 
 ## 0.9.0+4
diff --git a/packages/file_selector/file_selector_web/example/pubspec.yaml b/packages/file_selector/file_selector_web/example/pubspec.yaml
index bc8b984..fb3e71e 100644
--- a/packages/file_selector/file_selector_web/example/pubspec.yaml
+++ b/packages/file_selector/file_selector_web/example/pubspec.yaml
@@ -6,7 +6,7 @@
   flutter: ">=3.3.0"
 
 dependencies:
-  file_selector_platform_interface: ^2.2.0
+  file_selector_platform_interface: ^2.6.0
   file_selector_web:
     path: ../
   flutter:
diff --git a/packages/file_selector/file_selector_web/lib/file_selector_web.dart b/packages/file_selector/file_selector_web/lib/file_selector_web.dart
index 748bb3a..2380e27 100644
--- a/packages/file_selector/file_selector_web/lib/file_selector_web.dart
+++ b/packages/file_selector/file_selector_web/lib/file_selector_web.dart
@@ -61,6 +61,16 @@
       '';
 
   @override
+  Future<FileSaveLocation?> getSaveLocation({
+    List<XTypeGroup>? acceptedTypeGroups,
+    SaveDialogOptions options = const SaveDialogOptions(),
+  }) async {
+    // This is intended to be passed to XFile, which ignores the path, so
+    // provide a non-null dummy value.
+    return const FileSaveLocation('');
+  }
+
+  @override
   Future<String?> getDirectoryPath({
     String? initialDirectory,
     String? confirmButtonText,
diff --git a/packages/file_selector/file_selector_web/pubspec.yaml b/packages/file_selector/file_selector_web/pubspec.yaml
index 8a601cb..81ba11a 100644
--- a/packages/file_selector/file_selector_web/pubspec.yaml
+++ b/packages/file_selector/file_selector_web/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Web platform implementation of file_selector
 repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_web
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22
-version: 0.9.0+4
+version: 0.9.1
 
 environment:
   sdk: ">=2.18.0 <4.0.0"
@@ -17,7 +17,7 @@
         fileName: file_selector_web.dart
 
 dependencies:
-  file_selector_platform_interface: ^2.3.0
+  file_selector_platform_interface: ^2.6.0
   flutter:
     sdk: flutter
   flutter_web_plugins:
diff --git a/packages/file_selector/file_selector_windows/CHANGELOG.md b/packages/file_selector/file_selector_windows/CHANGELOG.md
index 3516343..b747a45 100644
--- a/packages/file_selector/file_selector_windows/CHANGELOG.md
+++ b/packages/file_selector/file_selector_windows/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.9.3
+
+* Adds `getSaveLocation` and deprecates `getSavePath`.
+
 ## 0.9.2
 
 * Adds `getDirectoryPaths` implementation.
diff --git a/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart b/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart
index 2d259a6..c92aff0 100644
--- a/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart
+++ b/packages/file_selector/file_selector_windows/example/lib/save_text_page.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:io';
 import 'dart:typed_data';
 import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
 import 'package:flutter/material.dart';
@@ -17,22 +18,39 @@
 
   Future<void> _saveFile() async {
     final String fileName = _nameController.text;
-    // TODO(stuartmorgan): Update this to getSaveLocation in the next federated
-    // change PR.
-    // ignore: deprecated_member_use
-    final String? path = await FileSelectorPlatform.instance.getSavePath(
-      // Operation was canceled by the user.
-      suggestedName: fileName,
+    final FileSaveLocation? result =
+        await FileSelectorPlatform.instance.getSaveLocation(
+      options: SaveDialogOptions(suggestedName: fileName),
+      acceptedTypeGroups: const <XTypeGroup>[
+        XTypeGroup(
+          label: 'Plain text',
+          extensions: <String>['txt'],
+        ),
+        XTypeGroup(
+          label: 'JSON',
+          extensions: <String>['json'],
+        ),
+      ],
     );
-    if (path == null) {
+    // Operation was canceled by the user.
+    if (result == null) {
       return;
     }
+    String path = result.path;
+    // Append an extension based on the selected type group if the user didn't
+    // include one.
+    if (!path.split(Platform.pathSeparator).last.contains('.')) {
+      final XTypeGroup? activeGroup = result.activeFilter;
+      if (activeGroup != null) {
+        // The group is one of the groups passed in above, each of which has
+        // exactly one `extensions` entry.
+        path = '$path.${activeGroup.extensions!.first}';
+      }
+    }
     final String text = _contentController.text;
     final Uint8List fileData = Uint8List.fromList(text.codeUnits);
-    const String fileMimeType = 'text/plain';
-    final XFile textFile =
-        XFile.fromData(fileData, mimeType: fileMimeType, name: fileName);
-    await textFile.saveTo(path);
+    final XFile textFile = XFile.fromData(fileData, name: fileName);
+    await textFile.saveTo(result.path);
   }
 
   @override
diff --git a/packages/file_selector/file_selector_windows/example/pubspec.yaml b/packages/file_selector/file_selector_windows/example/pubspec.yaml
index a22b6dc..02a1475 100644
--- a/packages/file_selector/file_selector_windows/example/pubspec.yaml
+++ b/packages/file_selector/file_selector_windows/example/pubspec.yaml
@@ -8,7 +8,7 @@
   flutter: ">=3.3.0"
 
 dependencies:
-  file_selector_platform_interface: ^2.4.0
+  file_selector_platform_interface: ^2.6.0
   file_selector_windows:
     # When depending on this package from a real application you should use:
     #   file_selector_windows: ^x.y.z
diff --git a/packages/file_selector/file_selector_windows/example/windows/runner/Runner.rc b/packages/file_selector/file_selector_windows/example/windows/runner/Runner.rc
index 51812dc..e5666e0 100644
--- a/packages/file_selector/file_selector_windows/example/windows/runner/Runner.rc
+++ b/packages/file_selector/file_selector_windows/example/windows/runner/Runner.rc
@@ -60,14 +60,14 @@
 // Version
 //
 
-#ifdef FLUTTER_BUILD_NUMBER
-#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER
+#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
+#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
 #else
-#define VERSION_AS_NUMBER 1,0,0
+#define VERSION_AS_NUMBER 1,0,0,0
 #endif
 
-#ifdef FLUTTER_BUILD_NAME
-#define VERSION_AS_STRING #FLUTTER_BUILD_NAME
+#if defined(FLUTTER_VERSION)
+#define VERSION_AS_STRING FLUTTER_VERSION
 #else
 #define VERSION_AS_STRING "1.0.0"
 #endif
diff --git a/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart b/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart
index 0e935cc..b96886b 100644
--- a/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart
+++ b/packages/file_selector/file_selector_windows/lib/file_selector_windows.dart
@@ -21,7 +21,7 @@
     String? initialDirectory,
     String? confirmButtonText,
   }) async {
-    final List<String?> paths = await _hostApi.showOpenDialog(
+    final FileDialogResult result = await _hostApi.showOpenDialog(
         SelectionOptions(
           allowMultiple: false,
           selectFolders: false,
@@ -29,7 +29,7 @@
         ),
         initialDirectory,
         confirmButtonText);
-    return paths.isEmpty ? null : XFile(paths.first!);
+    return result.paths.isEmpty ? null : XFile(result.paths.first!);
   }
 
   @override
@@ -38,7 +38,7 @@
     String? initialDirectory,
     String? confirmButtonText,
   }) async {
-    final List<String?> paths = await _hostApi.showOpenDialog(
+    final FileDialogResult result = await _hostApi.showOpenDialog(
         SelectionOptions(
           allowMultiple: true,
           selectFolders: false,
@@ -46,7 +46,7 @@
         ),
         initialDirectory,
         confirmButtonText);
-    return paths.map((String? path) => XFile(path!)).toList();
+    return result.paths.map((String? path) => XFile(path!)).toList();
   }
 
   @override
@@ -56,16 +56,36 @@
     String? suggestedName,
     String? confirmButtonText,
   }) async {
-    final List<String?> paths = await _hostApi.showSaveDialog(
+    final FileSaveLocation? location = await getSaveLocation(
+        acceptedTypeGroups: acceptedTypeGroups,
+        options: SaveDialogOptions(
+          initialDirectory: initialDirectory,
+          suggestedName: suggestedName,
+          confirmButtonText: confirmButtonText,
+        ));
+    return location?.path;
+  }
+
+  @override
+  Future<FileSaveLocation?> getSaveLocation({
+    List<XTypeGroup>? acceptedTypeGroups,
+    SaveDialogOptions options = const SaveDialogOptions(),
+  }) async {
+    final FileDialogResult result = await _hostApi.showSaveDialog(
         SelectionOptions(
           allowMultiple: false,
           selectFolders: false,
           allowedTypes: _typeGroupsFromXTypeGroups(acceptedTypeGroups),
         ),
-        initialDirectory,
-        suggestedName,
-        confirmButtonText);
-    return paths.isEmpty ? null : paths.first!;
+        options.initialDirectory,
+        options.suggestedName,
+        options.confirmButtonText);
+    final int? groupIndex = result.typeGroupIndex;
+    return result.paths.isEmpty
+        ? null
+        : FileSaveLocation(result.paths.first!,
+            activeFilter:
+                groupIndex == null ? null : acceptedTypeGroups?[groupIndex]);
   }
 
   @override
@@ -73,7 +93,7 @@
     String? initialDirectory,
     String? confirmButtonText,
   }) async {
-    final List<String?> paths = await _hostApi.showOpenDialog(
+    final FileDialogResult result = await _hostApi.showOpenDialog(
         SelectionOptions(
           allowMultiple: false,
           selectFolders: true,
@@ -81,7 +101,7 @@
         ),
         initialDirectory,
         confirmButtonText);
-    return paths.isEmpty ? null : paths.first!;
+    return result.paths.isEmpty ? null : result.paths.first!;
   }
 
   @override
@@ -89,7 +109,7 @@
     String? initialDirectory,
     String? confirmButtonText,
   }) async {
-    final List<String?> paths = await _hostApi.showOpenDialog(
+    final FileDialogResult result = await _hostApi.showOpenDialog(
         SelectionOptions(
           allowMultiple: true,
           selectFolders: true,
@@ -97,7 +117,7 @@
         ),
         initialDirectory,
         confirmButtonText);
-    return paths.isEmpty ? <String>[] : List<String>.from(paths);
+    return result.paths.isEmpty ? <String>[] : List<String>.from(result.paths);
   }
 }
 
diff --git a/packages/file_selector/file_selector_windows/lib/src/messages.g.dart b/packages/file_selector/file_selector_windows/lib/src/messages.g.dart
index a61076b..b5b4a79 100644
--- a/packages/file_selector/file_selector_windows/lib/src/messages.g.dart
+++ b/packages/file_selector/file_selector_windows/lib/src/messages.g.dart
@@ -1,7 +1,7 @@
 // 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 (v9.1.1), do not edit directly.
+// Autogenerated from Pigeon (v10.0.1), 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
 
@@ -68,16 +68,53 @@
   }
 }
 
+/// The result from an open or save dialog.
+class FileDialogResult {
+  FileDialogResult({
+    required this.paths,
+    this.typeGroupIndex,
+  });
+
+  /// The selected paths.
+  ///
+  /// Empty if the dialog was canceled.
+  List<String?> paths;
+
+  /// The type group index (into the list provided in [SelectionOptions]) of
+  /// the group that was selected when the dialog was confirmed.
+  ///
+  /// Null if no type groups were provided, or the dialog was canceled.
+  int? typeGroupIndex;
+
+  Object encode() {
+    return <Object?>[
+      paths,
+      typeGroupIndex,
+    ];
+  }
+
+  static FileDialogResult decode(Object result) {
+    result as List<Object?>;
+    return FileDialogResult(
+      paths: (result[0] as List<Object?>?)!.cast<String?>(),
+      typeGroupIndex: result[1] as int?,
+    );
+  }
+}
+
 class _FileSelectorApiCodec extends StandardMessageCodec {
   const _FileSelectorApiCodec();
   @override
   void writeValue(WriteBuffer buffer, Object? value) {
-    if (value is SelectionOptions) {
+    if (value is FileDialogResult) {
       buffer.putUint8(128);
       writeValue(buffer, value.encode());
-    } else if (value is TypeGroup) {
+    } else if (value is SelectionOptions) {
       buffer.putUint8(129);
       writeValue(buffer, value.encode());
+    } else if (value is TypeGroup) {
+      buffer.putUint8(130);
+      writeValue(buffer, value.encode());
     } else {
       super.writeValue(buffer, value);
     }
@@ -87,8 +124,10 @@
   Object? readValueOfType(int type, ReadBuffer buffer) {
     switch (type) {
       case 128:
-        return SelectionOptions.decode(readValue(buffer)!);
+        return FileDialogResult.decode(readValue(buffer)!);
       case 129:
+        return SelectionOptions.decode(readValue(buffer)!);
+      case 130:
         return TypeGroup.decode(readValue(buffer)!);
       default:
         return super.readValueOfType(type, buffer);
@@ -106,7 +145,7 @@
 
   static const MessageCodec<Object?> codec = _FileSelectorApiCodec();
 
-  Future<List<String?>> showOpenDialog(SelectionOptions arg_options,
+  Future<FileDialogResult> showOpenDialog(SelectionOptions arg_options,
       String? arg_initialDirectory, String? arg_confirmButtonText) async {
     final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
         'dev.flutter.pigeon.FileSelectorApi.showOpenDialog', codec,
@@ -131,11 +170,11 @@
         message: 'Host platform returned null value for non-null return value.',
       );
     } else {
-      return (replyList[0] as List<Object?>?)!.cast<String?>();
+      return (replyList[0] as FileDialogResult?)!;
     }
   }
 
-  Future<List<String?>> showSaveDialog(
+  Future<FileDialogResult> showSaveDialog(
       SelectionOptions arg_options,
       String? arg_initialDirectory,
       String? arg_suggestedName,
@@ -166,7 +205,7 @@
         message: 'Host platform returned null value for non-null return value.',
       );
     } else {
-      return (replyList[0] as List<Object?>?)!.cast<String?>();
+      return (replyList[0] as FileDialogResult?)!;
     }
   }
 }
diff --git a/packages/file_selector/file_selector_windows/pigeons/messages.dart b/packages/file_selector/file_selector_windows/pigeons/messages.dart
index c3b3aff..8f82aec 100644
--- a/packages/file_selector/file_selector_windows/pigeons/messages.dart
+++ b/packages/file_selector/file_selector_windows/pigeons/messages.dart
@@ -37,14 +37,33 @@
   List<TypeGroup?> allowedTypes;
 }
 
+/// The result from an open or save dialog.
+class FileDialogResult {
+  FileDialogResult({required this.paths, this.typeGroupIndex});
+
+  /// The selected paths.
+  ///
+  /// Empty if the dialog was canceled.
+  // TODO(stuartmorgan): Make the generic type non-nullable once supported.
+  // https://github.com/flutter/flutter/issues/97848
+  // The Dart code treats the values as non-nullable.
+  List<String?> paths;
+
+  /// The type group index (into the list provided in [SelectionOptions]) of
+  /// the group that was selected when the dialog was confirmed.
+  ///
+  /// Null if no type groups were provided, or the dialog was canceled.
+  int? typeGroupIndex;
+}
+
 @HostApi(dartHostTestHandler: 'TestFileSelectorApi')
 abstract class FileSelectorApi {
-  List<String?> showOpenDialog(
+  FileDialogResult showOpenDialog(
     SelectionOptions options,
     String? initialDirectory,
     String? confirmButtonText,
   );
-  List<String?> showSaveDialog(
+  FileDialogResult showSaveDialog(
     SelectionOptions options,
     String? initialDirectory,
     String? suggestedName,
diff --git a/packages/file_selector/file_selector_windows/pubspec.yaml b/packages/file_selector/file_selector_windows/pubspec.yaml
index 39d5cf7..4cd3cc5 100644
--- a/packages/file_selector/file_selector_windows/pubspec.yaml
+++ b/packages/file_selector/file_selector_windows/pubspec.yaml
@@ -2,7 +2,7 @@
 description: Windows implementation of the file_selector plugin.
 repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_windows
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22
-version: 0.9.2
+version: 0.9.3
 
 environment:
   sdk: ">=2.18.0 <4.0.0"
@@ -18,7 +18,7 @@
 
 dependencies:
   cross_file: ^0.3.1
-  file_selector_platform_interface: ^2.4.0
+  file_selector_platform_interface: ^2.6.0
   flutter:
     sdk: flutter
 
@@ -27,4 +27,4 @@
   flutter_test:
     sdk: flutter
   mockito: 5.4.1
-  pigeon: ^9.1.0
+  pigeon: ^10.0.0
diff --git a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart
index fbe3683..4f455ee 100644
--- a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart
+++ b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart
@@ -30,9 +30,10 @@
     expect(FileSelectorPlatform.instance, isA<FileSelectorWindows>());
   });
 
-  group('#openFile', () {
+  group('openFile', () {
     setUp(() {
-      when(mockApi.showOpenDialog(any, any, any)).thenReturn(<String?>['foo']);
+      when(mockApi.showOpenDialog(any, any, any))
+          .thenReturn(FileDialogResult(paths: <String?>['foo']));
     });
 
     test('simple call works', () async {
@@ -105,10 +106,10 @@
     });
   });
 
-  group('#openFiles', () {
+  group('openFiles', () {
     setUp(() {
       when(mockApi.showOpenDialog(any, any, any))
-          .thenReturn(<String?>['foo', 'bar']);
+          .thenReturn(FileDialogResult(paths: <String?>['foo', 'bar']));
     });
 
     test('simple call works', () async {
@@ -182,9 +183,10 @@
     });
   });
 
-  group('#getDirectoryPath', () {
+  group('getDirectoryPath', () {
     setUp(() {
-      when(mockApi.showOpenDialog(any, any, any)).thenReturn(<String?>['foo']);
+      when(mockApi.showOpenDialog(any, any, any))
+          .thenReturn(FileDialogResult(paths: <String?>['foo']));
     });
 
     test('simple call works', () async {
@@ -211,10 +213,10 @@
     });
   });
 
-  group('#getDirectoryPaths', () {
+  group('getDirectoryPaths', () {
     setUp(() {
       when(mockApi.showOpenDialog(any, any, any))
-          .thenReturn(<String?>['foo', 'bar']);
+          .thenReturn(FileDialogResult(paths: <String?>['foo', 'bar']));
     });
 
     test('simple call works', () async {
@@ -242,10 +244,122 @@
     });
   });
 
-  group('#getSavePath', () {
+  group('getSaveLocation', () {
     setUp(() {
       when(mockApi.showSaveDialog(any, any, any, any))
-          .thenReturn(<String?>['foo']);
+          .thenReturn(FileDialogResult(paths: <String?>['foo']));
+    });
+
+    test('simple call works', () async {
+      final FileSaveLocation? location = await plugin.getSaveLocation();
+
+      expect(location?.path, 'foo');
+      expect(location?.activeFilter, null);
+      final VerificationResult result =
+          verify(mockApi.showSaveDialog(captureAny, null, null, null));
+      final SelectionOptions options = result.captured[0] as SelectionOptions;
+      expect(options.allowMultiple, false);
+      expect(options.selectFolders, false);
+    });
+
+    test('passes the accepted type groups correctly', () async {
+      const XTypeGroup group = XTypeGroup(
+        label: 'text',
+        extensions: <String>['txt'],
+        mimeTypes: <String>['text/plain'],
+      );
+
+      const XTypeGroup groupTwo = XTypeGroup(
+        label: 'image',
+        extensions: <String>['jpg'],
+        mimeTypes: <String>['image/jpg'],
+      );
+
+      await plugin
+          .getSaveLocation(acceptedTypeGroups: <XTypeGroup>[group, groupTwo]);
+
+      final VerificationResult result =
+          verify(mockApi.showSaveDialog(captureAny, null, null, null));
+      final SelectionOptions options = result.captured[0] as SelectionOptions;
+      expect(
+          _typeGroupListsMatch(options.allowedTypes, <TypeGroup>[
+            TypeGroup(label: 'text', extensions: <String>['txt']),
+            TypeGroup(label: 'image', extensions: <String>['jpg']),
+          ]),
+          true);
+    });
+
+    test('returns the selected type group correctly', () async {
+      when(mockApi.showSaveDialog(any, any, any, any)).thenReturn(
+          FileDialogResult(paths: <String?>['foo'], typeGroupIndex: 1));
+      const XTypeGroup group = XTypeGroup(
+        label: 'text',
+        extensions: <String>['txt'],
+        mimeTypes: <String>['text/plain'],
+      );
+
+      const XTypeGroup groupTwo = XTypeGroup(
+        label: 'image',
+        extensions: <String>['jpg'],
+        mimeTypes: <String>['image/jpg'],
+      );
+
+      final FileSaveLocation? result = await plugin
+          .getSaveLocation(acceptedTypeGroups: <XTypeGroup>[group, groupTwo]);
+
+      verify(mockApi.showSaveDialog(captureAny, null, null, null));
+
+      expect(result?.activeFilter, groupTwo);
+    });
+
+    test('passes initialDirectory correctly', () async {
+      await plugin.getSaveLocation(
+          options:
+              const SaveDialogOptions(initialDirectory: '/example/directory'));
+
+      verify(mockApi.showSaveDialog(any, '/example/directory', null, null));
+    });
+
+    test('passes suggestedName correctly', () async {
+      await plugin.getSaveLocation(
+          options: const SaveDialogOptions(suggestedName: 'baz.txt'));
+
+      verify(mockApi.showSaveDialog(any, null, 'baz.txt', null));
+    });
+
+    test('passes confirmButtonText correctly', () async {
+      await plugin.getSaveLocation(
+          options: const SaveDialogOptions(confirmButtonText: 'Save File'));
+
+      verify(mockApi.showSaveDialog(any, null, null, 'Save File'));
+    });
+
+    test('throws for a type group that does not support Windows', () async {
+      const XTypeGroup group = XTypeGroup(
+        label: 'text',
+        mimeTypes: <String>['text/plain'],
+      );
+
+      await expectLater(
+          plugin.getSaveLocation(acceptedTypeGroups: <XTypeGroup>[group]),
+          throwsArgumentError);
+    });
+
+    test('allows a wildcard group', () async {
+      const XTypeGroup group = XTypeGroup(
+        label: 'text',
+      );
+
+      await expectLater(
+          plugin.getSaveLocation(acceptedTypeGroups: <XTypeGroup>[group]),
+          completes);
+    });
+  });
+
+  group('getSavePath (deprecated)', () {
+    setUp(() {
+      when(mockApi.showSaveDialog(any, any, any, any))
+          .thenReturn(FileDialogResult(paths: <String?>['foo']));
     });
 
     test('simple call works', () async {
diff --git a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart
index ae55f2e..7168e0c 100644
--- a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart
+++ b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart
@@ -1,12 +1,14 @@
-// Mocks generated by Mockito 5.4.0 from annotations
+// Mocks generated by Mockito 5.4.1 from annotations
 // in file_selector_windows/test/file_selector_windows_test.dart.
 // Do not manually edit this file.
 
+// @dart=2.19
+
 // ignore_for_file: no_leading_underscores_for_library_prefixes
-import 'package:file_selector_windows/src/messages.g.dart' as _i3;
+import 'package:file_selector_windows/src/messages.g.dart' as _i2;
 import 'package:mockito/mockito.dart' as _i1;
 
-import 'test_api.g.dart' as _i2;
+import 'test_api.g.dart' as _i3;
 
 // ignore_for_file: type=lint
 // ignore_for_file: avoid_redundant_argument_values
@@ -19,18 +21,29 @@
 // ignore_for_file: camel_case_types
 // ignore_for_file: subtype_of_sealed_class
 
+class _FakeFileDialogResult_0 extends _i1.SmartFake
+    implements _i2.FileDialogResult {
+  _FakeFileDialogResult_0(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
 /// A class which mocks [TestFileSelectorApi].
 ///
 /// See the documentation for Mockito's code generation for more information.
 class MockTestFileSelectorApi extends _i1.Mock
-    implements _i2.TestFileSelectorApi {
+    implements _i3.TestFileSelectorApi {
   MockTestFileSelectorApi() {
     _i1.throwOnMissingStub(this);
   }
 
   @override
-  List<String?> showOpenDialog(
-    _i3.SelectionOptions? options,
+  _i2.FileDialogResult showOpenDialog(
+    _i2.SelectionOptions? options,
     String? initialDirectory,
     String? confirmButtonText,
   ) =>
@@ -43,11 +56,21 @@
             confirmButtonText,
           ],
         ),
-        returnValue: <String?>[],
-      ) as List<String?>);
+        returnValue: _FakeFileDialogResult_0(
+          this,
+          Invocation.method(
+            #showOpenDialog,
+            [
+              options,
+              initialDirectory,
+              confirmButtonText,
+            ],
+          ),
+        ),
+      ) as _i2.FileDialogResult);
   @override
-  List<String?> showSaveDialog(
-    _i3.SelectionOptions? options,
+  _i2.FileDialogResult showSaveDialog(
+    _i2.SelectionOptions? options,
     String? initialDirectory,
     String? suggestedName,
     String? confirmButtonText,
@@ -62,6 +85,17 @@
             confirmButtonText,
           ],
         ),
-        returnValue: <String?>[],
-      ) as List<String?>);
+        returnValue: _FakeFileDialogResult_0(
+          this,
+          Invocation.method(
+            #showSaveDialog,
+            [
+              options,
+              initialDirectory,
+              suggestedName,
+              confirmButtonText,
+            ],
+          ),
+        ),
+      ) as _i2.FileDialogResult);
 }
diff --git a/packages/file_selector/file_selector_windows/test/test_api.g.dart b/packages/file_selector/file_selector_windows/test/test_api.g.dart
index f9ed8e5..778ae4f 100644
--- a/packages/file_selector/file_selector_windows/test/test_api.g.dart
+++ b/packages/file_selector/file_selector_windows/test/test_api.g.dart
@@ -1,7 +1,7 @@
 // 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 (v9.1.1), do not edit directly.
+// Autogenerated from Pigeon (v10.0.1), 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
 // ignore_for_file: avoid_relative_lib_imports
@@ -17,12 +17,15 @@
   const _TestFileSelectorApiCodec();
   @override
   void writeValue(WriteBuffer buffer, Object? value) {
-    if (value is SelectionOptions) {
+    if (value is FileDialogResult) {
       buffer.putUint8(128);
       writeValue(buffer, value.encode());
-    } else if (value is TypeGroup) {
+    } else if (value is SelectionOptions) {
       buffer.putUint8(129);
       writeValue(buffer, value.encode());
+    } else if (value is TypeGroup) {
+      buffer.putUint8(130);
+      writeValue(buffer, value.encode());
     } else {
       super.writeValue(buffer, value);
     }
@@ -32,8 +35,10 @@
   Object? readValueOfType(int type, ReadBuffer buffer) {
     switch (type) {
       case 128:
-        return SelectionOptions.decode(readValue(buffer)!);
+        return FileDialogResult.decode(readValue(buffer)!);
       case 129:
+        return SelectionOptions.decode(readValue(buffer)!);
+      case 130:
         return TypeGroup.decode(readValue(buffer)!);
       default:
         return super.readValueOfType(type, buffer);
@@ -42,12 +47,14 @@
 }
 
 abstract class TestFileSelectorApi {
+  static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding =>
+      TestDefaultBinaryMessengerBinding.instance;
   static const MessageCodec<Object?> codec = _TestFileSelectorApiCodec();
 
-  List<String?> showOpenDialog(SelectionOptions options,
+  FileDialogResult showOpenDialog(SelectionOptions options,
       String? initialDirectory, String? confirmButtonText);
 
-  List<String?> showSaveDialog(
+  FileDialogResult showSaveDialog(
       SelectionOptions options,
       String? initialDirectory,
       String? suggestedName,
@@ -60,9 +67,12 @@
           'dev.flutter.pigeon.FileSelectorApi.showOpenDialog', codec,
           binaryMessenger: binaryMessenger);
       if (api == null) {
-        channel.setMockMessageHandler(null);
+        _testBinaryMessengerBinding!.defaultBinaryMessenger
+            .setMockDecodedMessageHandler<Object?>(channel, null);
       } else {
-        channel.setMockMessageHandler((Object? message) async {
+        _testBinaryMessengerBinding!.defaultBinaryMessenger
+            .setMockDecodedMessageHandler<Object?>(channel,
+                (Object? message) async {
           assert(message != null,
               'Argument for dev.flutter.pigeon.FileSelectorApi.showOpenDialog was null.');
           final List<Object?> args = (message as List<Object?>?)!;
@@ -71,7 +81,7 @@
               'Argument for dev.flutter.pigeon.FileSelectorApi.showOpenDialog was null, expected non-null SelectionOptions.');
           final String? arg_initialDirectory = (args[1] as String?);
           final String? arg_confirmButtonText = (args[2] as String?);
-          final List<String?> output = api.showOpenDialog(
+          final FileDialogResult output = api.showOpenDialog(
               arg_options!, arg_initialDirectory, arg_confirmButtonText);
           return <Object?>[output];
         });
@@ -82,9 +92,12 @@
           'dev.flutter.pigeon.FileSelectorApi.showSaveDialog', codec,
           binaryMessenger: binaryMessenger);
       if (api == null) {
-        channel.setMockMessageHandler(null);
+        _testBinaryMessengerBinding!.defaultBinaryMessenger
+            .setMockDecodedMessageHandler<Object?>(channel, null);
       } else {
-        channel.setMockMessageHandler((Object? message) async {
+        _testBinaryMessengerBinding!.defaultBinaryMessenger
+            .setMockDecodedMessageHandler<Object?>(channel,
+                (Object? message) async {
           assert(message != null,
               'Argument for dev.flutter.pigeon.FileSelectorApi.showSaveDialog was null.');
           final List<Object?> args = (message as List<Object?>?)!;
@@ -94,7 +107,7 @@
           final String? arg_initialDirectory = (args[1] as String?);
           final String? arg_suggestedName = (args[2] as String?);
           final String? arg_confirmButtonText = (args[3] as String?);
-          final List<String?> output = api.showSaveDialog(arg_options!,
+          final FileDialogResult output = api.showSaveDialog(arg_options!,
               arg_initialDirectory, arg_suggestedName, arg_confirmButtonText);
           return <Object?>[output];
         });
diff --git a/packages/file_selector/file_selector_windows/windows/file_dialog_controller.cpp b/packages/file_selector/file_selector_windows/windows/file_dialog_controller.cpp
index 5820c4a..af2a9af 100644
--- a/packages/file_selector/file_selector_windows/windows/file_dialog_controller.cpp
+++ b/packages/file_selector/file_selector_windows/windows/file_dialog_controller.cpp
@@ -51,6 +51,10 @@
   return dialog_->GetResult(out_item);
 }
 
+HRESULT FileDialogController::GetFileTypeIndex(UINT* out_index) const {
+  return dialog_->GetFileTypeIndex(out_index);
+}
+
 HRESULT FileDialogController::GetResults(IShellItemArray** out_items) const {
   IFileOpenDialogPtr open_dialog;
   HRESULT result = dialog_->QueryInterface(IID_PPV_ARGS(&open_dialog));
diff --git a/packages/file_selector/file_selector_windows/windows/file_dialog_controller.h b/packages/file_selector/file_selector_windows/windows/file_dialog_controller.h
index f5c9397..ab49292 100644
--- a/packages/file_selector/file_selector_windows/windows/file_dialog_controller.h
+++ b/packages/file_selector/file_selector_windows/windows/file_dialog_controller.h
@@ -38,6 +38,7 @@
   virtual HRESULT SetOptions(FILEOPENDIALOGOPTIONS options);
   virtual HRESULT Show(HWND parent);
   virtual HRESULT GetResult(IShellItem** out_item) const;
+  virtual HRESULT GetFileTypeIndex(UINT* out_index) const;
 
   // IFileOpenDialog wrapper. This will fail if the IFileDialog* provided to the
   // constructor was not an IFileOpenDialog instance.
diff --git a/packages/file_selector/file_selector_windows/windows/file_selector_plugin.cpp b/packages/file_selector/file_selector_windows/windows/file_selector_plugin.cpp
index b9e6d21..3569798 100644
--- a/packages/file_selector/file_selector_windows/windows/file_selector_plugin.cpp
+++ b/packages/file_selector/file_selector_windows/windows/file_selector_plugin.cpp
@@ -29,8 +29,8 @@
 
 namespace {
 
+using flutter::CustomEncodableValue;
 using flutter::EncodableList;
-using flutter::EncodableMap;
 using flutter::EncodableValue;
 
 // The kind of file dialog to show.
@@ -137,7 +137,7 @@
 
     for (const EncodableValue& filter_info_value : filters) {
       const auto& type_group = std::any_cast<TypeGroup>(
-          std::get<flutter::CustomEncodableValue>(filter_info_value));
+          std::get<CustomEncodableValue>(filter_info_value));
       filter_names.push_back(Utf16FromUtf8(type_group.label()));
       filter_extensions.push_back(L"");
       std::wstring& spec = filter_extensions.back();
@@ -158,8 +158,8 @@
         static_cast<UINT>(filter_specs.size()), filter_specs.data());
   }
 
-  // Displays the dialog, and returns the selected files, or nullopt on error.
-  std::optional<EncodableList> Show(HWND parent_window) {
+  // Displays the dialog, and returns the result, or nullopt on error.
+  std::optional<FileDialogResult> Show(HWND parent_window) {
     assert(dialog_controller_);
     last_result_ = dialog_controller_->Show(parent_window);
     if (!SUCCEEDED(last_result_)) {
@@ -190,7 +190,14 @@
       }
       files.push_back(EncodableValue(GetPathForShellItem(shell_item)));
     }
-    return files;
+    FileDialogResult result(files, nullptr);
+    UINT file_type_index;
+    if (SUCCEEDED(dialog_controller_->GetFileTypeIndex(&file_type_index)) &&
+        file_type_index > 0) {
+      // Convert from the one-based index to a Dart index.
+      result.set_type_group_index(file_type_index - 1);
+    }
+    return result;
   }
 
   // Returns the result of the last Win32 API call related to this object.
@@ -205,7 +212,7 @@
   HRESULT last_result_;
 };
 
-ErrorOr<flutter::EncodableList> ShowDialog(
+ErrorOr<FileDialogResult> ShowDialog(
     const FileDialogControllerFactory& dialog_factory, HWND parent_window,
     DialogMode mode, const SelectionOptions& options,
     const std::string* initial_directory, const std::string* suggested_name,
@@ -243,16 +250,16 @@
     dialog.SetFileTypeFilters(options.allowed_types());
   }
 
-  std::optional<EncodableList> files = dialog.Show(parent_window);
-  if (!files) {
+  std::optional<FileDialogResult> result = dialog.Show(parent_window);
+  if (!result) {
     if (dialog.last_result() != HRESULT_FROM_WIN32(ERROR_CANCELLED)) {
       return FlutterError("System error", "Could not show dialog",
                           EncodableValue(dialog.last_result()));
     } else {
-      return EncodableList();
+      return FileDialogResult(EncodableList(), nullptr);
     }
   }
-  return std::move(files.value());
+  return std::move(result.value());
 }
 
 // Returns the top-level window that owns |view|.
@@ -282,14 +289,14 @@
 
 FileSelectorPlugin::~FileSelectorPlugin() = default;
 
-ErrorOr<flutter::EncodableList> FileSelectorPlugin::ShowOpenDialog(
+ErrorOr<FileDialogResult> FileSelectorPlugin::ShowOpenDialog(
     const SelectionOptions& options, const std::string* initialDirectory,
     const std::string* confirmButtonText) {
   return ShowDialog(*controller_factory_, get_root_window_(), DialogMode::open,
                     options, initialDirectory, nullptr, confirmButtonText);
 }
 
-ErrorOr<flutter::EncodableList> FileSelectorPlugin::ShowSaveDialog(
+ErrorOr<FileDialogResult> FileSelectorPlugin::ShowSaveDialog(
     const SelectionOptions& options, const std::string* initialDirectory,
     const std::string* suggestedName, const std::string* confirmButtonText) {
   return ShowDialog(*controller_factory_, get_root_window_(), DialogMode::save,
diff --git a/packages/file_selector/file_selector_windows/windows/file_selector_plugin.h b/packages/file_selector/file_selector_windows/windows/file_selector_plugin.h
index 1388bfd..2f17f94 100644
--- a/packages/file_selector/file_selector_windows/windows/file_selector_plugin.h
+++ b/packages/file_selector/file_selector_windows/windows/file_selector_plugin.h
@@ -32,10 +32,10 @@
   virtual ~FileSelectorPlugin();
 
   // FileSelectorApi
-  ErrorOr<flutter::EncodableList> ShowOpenDialog(
+  ErrorOr<FileDialogResult> ShowOpenDialog(
       const SelectionOptions& options, const std::string* initial_directory,
       const std::string* confirm_button_text) override;
-  ErrorOr<flutter::EncodableList> ShowSaveDialog(
+  ErrorOr<FileDialogResult> ShowSaveDialog(
       const SelectionOptions& options, const std::string* initialDirectory,
       const std::string* suggestedName,
       const std::string* confirmButtonText) override;
diff --git a/packages/file_selector/file_selector_windows/windows/messages.g.cpp b/packages/file_selector/file_selector_windows/windows/messages.g.cpp
index 24b831e..a60fd92 100644
--- a/packages/file_selector/file_selector_windows/windows/messages.g.cpp
+++ b/packages/file_selector/file_selector_windows/windows/messages.g.cpp
@@ -1,7 +1,7 @@
 // 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 (v9.1.1), do not edit directly.
+// Autogenerated from Pigeon (v10.0.1), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 
 #undef _HAS_EXCEPTIONS
@@ -26,10 +26,15 @@
 
 // TypeGroup
 
+TypeGroup::TypeGroup(const std::string& label, const EncodableList& extensions)
+    : label_(label), extensions_(extensions) {}
+
 const std::string& TypeGroup::label() const { return label_; }
+
 void TypeGroup::set_label(std::string_view value_arg) { label_ = value_arg; }
 
 const EncodableList& TypeGroup::extensions() const { return extensions_; }
+
 void TypeGroup::set_extensions(const EncodableList& value_arg) {
   extensions_ = value_arg;
 }
@@ -42,29 +47,28 @@
   return list;
 }
 
-TypeGroup::TypeGroup() {}
-
-TypeGroup::TypeGroup(const EncodableList& list) {
-  auto& encodable_label = list[0];
-  if (const std::string* pointer_label =
-          std::get_if<std::string>(&encodable_label)) {
-    label_ = *pointer_label;
-  }
-  auto& encodable_extensions = list[1];
-  if (const EncodableList* pointer_extensions =
-          std::get_if<EncodableList>(&encodable_extensions)) {
-    extensions_ = *pointer_extensions;
-  }
+TypeGroup TypeGroup::FromEncodableList(const EncodableList& list) {
+  TypeGroup decoded(std::get<std::string>(list[0]),
+                    std::get<EncodableList>(list[1]));
+  return decoded;
 }
 
 // SelectionOptions
 
+SelectionOptions::SelectionOptions(bool allow_multiple, bool select_folders,
+                                   const EncodableList& allowed_types)
+    : allow_multiple_(allow_multiple),
+      select_folders_(select_folders),
+      allowed_types_(allowed_types) {}
+
 bool SelectionOptions::allow_multiple() const { return allow_multiple_; }
+
 void SelectionOptions::set_allow_multiple(bool value_arg) {
   allow_multiple_ = value_arg;
 }
 
 bool SelectionOptions::select_folders() const { return select_folders_; }
+
 void SelectionOptions::set_select_folders(bool value_arg) {
   select_folders_ = value_arg;
 }
@@ -72,6 +76,7 @@
 const EncodableList& SelectionOptions::allowed_types() const {
   return allowed_types_;
 }
+
 void SelectionOptions::set_allowed_types(const EncodableList& value_arg) {
   allowed_types_ = value_arg;
 }
@@ -85,36 +90,77 @@
   return list;
 }
 
-SelectionOptions::SelectionOptions() {}
+SelectionOptions SelectionOptions::FromEncodableList(
+    const EncodableList& list) {
+  SelectionOptions decoded(std::get<bool>(list[0]), std::get<bool>(list[1]),
+                           std::get<EncodableList>(list[2]));
+  return decoded;
+}
 
-SelectionOptions::SelectionOptions(const EncodableList& list) {
-  auto& encodable_allow_multiple = list[0];
-  if (const bool* pointer_allow_multiple =
-          std::get_if<bool>(&encodable_allow_multiple)) {
-    allow_multiple_ = *pointer_allow_multiple;
+// FileDialogResult
+
+FileDialogResult::FileDialogResult(const EncodableList& paths)
+    : paths_(paths) {}
+
+FileDialogResult::FileDialogResult(const EncodableList& paths,
+                                   const int64_t* type_group_index)
+    : paths_(paths),
+      type_group_index_(type_group_index
+                            ? std::optional<int64_t>(*type_group_index)
+                            : std::nullopt) {}
+
+const EncodableList& FileDialogResult::paths() const { return paths_; }
+
+void FileDialogResult::set_paths(const EncodableList& value_arg) {
+  paths_ = value_arg;
+}
+
+const int64_t* FileDialogResult::type_group_index() const {
+  return type_group_index_ ? &(*type_group_index_) : nullptr;
+}
+
+void FileDialogResult::set_type_group_index(const int64_t* value_arg) {
+  type_group_index_ =
+      value_arg ? std::optional<int64_t>(*value_arg) : std::nullopt;
+}
+
+void FileDialogResult::set_type_group_index(int64_t value_arg) {
+  type_group_index_ = value_arg;
+}
+
+EncodableList FileDialogResult::ToEncodableList() const {
+  EncodableList list;
+  list.reserve(2);
+  list.push_back(EncodableValue(paths_));
+  list.push_back(type_group_index_ ? EncodableValue(*type_group_index_)
+                                   : EncodableValue());
+  return list;
+}
+
+FileDialogResult FileDialogResult::FromEncodableList(
+    const EncodableList& list) {
+  FileDialogResult decoded(std::get<EncodableList>(list[0]));
+  auto& encodable_type_group_index = list[1];
+  if (!encodable_type_group_index.IsNull()) {
+    decoded.set_type_group_index(encodable_type_group_index.LongValue());
   }
-  auto& encodable_select_folders = list[1];
-  if (const bool* pointer_select_folders =
-          std::get_if<bool>(&encodable_select_folders)) {
-    select_folders_ = *pointer_select_folders;
-  }
-  auto& encodable_allowed_types = list[2];
-  if (const EncodableList* pointer_allowed_types =
-          std::get_if<EncodableList>(&encodable_allowed_types)) {
-    allowed_types_ = *pointer_allowed_types;
-  }
+  return decoded;
 }
 
 FileSelectorApiCodecSerializer::FileSelectorApiCodecSerializer() {}
+
 EncodableValue FileSelectorApiCodecSerializer::ReadValueOfType(
     uint8_t type, flutter::ByteStreamReader* stream) const {
   switch (type) {
     case 128:
-      return CustomEncodableValue(
-          SelectionOptions(std::get<EncodableList>(ReadValue(stream))));
+      return CustomEncodableValue(FileDialogResult::FromEncodableList(
+          std::get<EncodableList>(ReadValue(stream))));
     case 129:
-      return CustomEncodableValue(
-          TypeGroup(std::get<EncodableList>(ReadValue(stream))));
+      return CustomEncodableValue(SelectionOptions::FromEncodableList(
+          std::get<EncodableList>(ReadValue(stream))));
+    case 130:
+      return CustomEncodableValue(TypeGroup::FromEncodableList(
+          std::get<EncodableList>(ReadValue(stream))));
     default:
       return flutter::StandardCodecSerializer::ReadValueOfType(type, stream);
   }
@@ -124,16 +170,24 @@
     const EncodableValue& value, flutter::ByteStreamWriter* stream) const {
   if (const CustomEncodableValue* custom_value =
           std::get_if<CustomEncodableValue>(&value)) {
-    if (custom_value->type() == typeid(SelectionOptions)) {
+    if (custom_value->type() == typeid(FileDialogResult)) {
       stream->WriteByte(128);
       WriteValue(
           EncodableValue(
+              std::any_cast<FileDialogResult>(*custom_value).ToEncodableList()),
+          stream);
+      return;
+    }
+    if (custom_value->type() == typeid(SelectionOptions)) {
+      stream->WriteByte(129);
+      WriteValue(
+          EncodableValue(
               std::any_cast<SelectionOptions>(*custom_value).ToEncodableList()),
           stream);
       return;
     }
     if (custom_value->type() == typeid(TypeGroup)) {
-      stream->WriteByte(129);
+      stream->WriteByte(130);
       WriteValue(EncodableValue(
                      std::any_cast<TypeGroup>(*custom_value).ToEncodableList()),
                  stream);
@@ -176,14 +230,15 @@
               const auto& encodable_confirm_button_text_arg = args.at(2);
               const auto* confirm_button_text_arg =
                   std::get_if<std::string>(&encodable_confirm_button_text_arg);
-              ErrorOr<EncodableList> output = api->ShowOpenDialog(
+              ErrorOr<FileDialogResult> output = api->ShowOpenDialog(
                   options_arg, initial_directory_arg, confirm_button_text_arg);
               if (output.has_error()) {
                 reply(WrapError(output.error()));
                 return;
               }
               EncodableList wrapped;
-              wrapped.push_back(EncodableValue(std::move(output).TakeValue()));
+              wrapped.push_back(
+                  CustomEncodableValue(std::move(output).TakeValue()));
               reply(EncodableValue(std::move(wrapped)));
             } catch (const std::exception& exception) {
               reply(WrapError(exception.what()));
@@ -219,7 +274,7 @@
               const auto& encodable_confirm_button_text_arg = args.at(3);
               const auto* confirm_button_text_arg =
                   std::get_if<std::string>(&encodable_confirm_button_text_arg);
-              ErrorOr<EncodableList> output = api->ShowSaveDialog(
+              ErrorOr<FileDialogResult> output = api->ShowSaveDialog(
                   options_arg, initial_directory_arg, suggested_name_arg,
                   confirm_button_text_arg);
               if (output.has_error()) {
@@ -227,7 +282,8 @@
                 return;
               }
               EncodableList wrapped;
-              wrapped.push_back(EncodableValue(std::move(output).TakeValue()));
+              wrapped.push_back(
+                  CustomEncodableValue(std::move(output).TakeValue()));
               reply(EncodableValue(std::move(wrapped)));
             } catch (const std::exception& exception) {
               reply(WrapError(exception.what()));
@@ -244,6 +300,7 @@
       EncodableList{EncodableValue(std::string(error_message)),
                     EncodableValue("Error"), EncodableValue()});
 }
+
 EncodableValue FileSelectorApi::WrapError(const FlutterError& error) {
   return EncodableValue(EncodableList{EncodableValue(error.code()),
                                       EncodableValue(error.message()),
diff --git a/packages/file_selector/file_selector_windows/windows/messages.g.h b/packages/file_selector/file_selector_windows/windows/messages.g.h
index 248ca89..ab8afd7 100644
--- a/packages/file_selector/file_selector_windows/windows/messages.g.h
+++ b/packages/file_selector/file_selector_windows/windows/messages.g.h
@@ -1,7 +1,7 @@
 // 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 (v9.1.1), do not edit directly.
+// Autogenerated from Pigeon (v10.0.1), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 
 #ifndef PIGEON_MESSAGES_G_H_
@@ -41,10 +41,10 @@
 template <class T>
 class ErrorOr {
  public:
-  ErrorOr(const T& rhs) { new (&v_) T(rhs); }
-  ErrorOr(const T&& rhs) { v_ = std::move(rhs); }
-  ErrorOr(const FlutterError& rhs) { new (&v_) FlutterError(rhs); }
-  ErrorOr(const FlutterError&& rhs) { v_ = std::move(rhs); }
+  ErrorOr(const T& rhs) : v_(rhs) {}
+  ErrorOr(const T&& rhs) : v_(std::move(rhs)) {}
+  ErrorOr(const FlutterError& rhs) : v_(rhs) {}
+  ErrorOr(const FlutterError&& rhs) : v_(std::move(rhs)) {}
 
   bool has_error() const { return std::holds_alternative<FlutterError>(v_); }
   const T& value() const { return std::get<T>(v_); };
@@ -61,7 +61,10 @@
 // Generated class from Pigeon that represents data sent in messages.
 class TypeGroup {
  public:
-  TypeGroup();
+  // Constructs an object setting all fields.
+  explicit TypeGroup(const std::string& label,
+                     const flutter::EncodableList& extensions);
+
   const std::string& label() const;
   void set_label(std::string_view value_arg);
 
@@ -69,7 +72,7 @@
   void set_extensions(const flutter::EncodableList& value_arg);
 
  private:
-  TypeGroup(const flutter::EncodableList& list);
+  static TypeGroup FromEncodableList(const flutter::EncodableList& list);
   flutter::EncodableList ToEncodableList() const;
   friend class FileSelectorApi;
   friend class FileSelectorApiCodecSerializer;
@@ -80,7 +83,10 @@
 // Generated class from Pigeon that represents data sent in messages.
 class SelectionOptions {
  public:
-  SelectionOptions();
+  // Constructs an object setting all fields.
+  explicit SelectionOptions(bool allow_multiple, bool select_folders,
+                            const flutter::EncodableList& allowed_types);
+
   bool allow_multiple() const;
   void set_allow_multiple(bool value_arg);
 
@@ -91,7 +97,7 @@
   void set_allowed_types(const flutter::EncodableList& value_arg);
 
  private:
-  SelectionOptions(const flutter::EncodableList& list);
+  static SelectionOptions FromEncodableList(const flutter::EncodableList& list);
   flutter::EncodableList ToEncodableList() const;
   friend class FileSelectorApi;
   friend class FileSelectorApiCodecSerializer;
@@ -100,16 +106,49 @@
   flutter::EncodableList allowed_types_;
 };
 
+// The result from an open or save dialog.
+//
+// Generated class from Pigeon that represents data sent in messages.
+class FileDialogResult {
+ public:
+  // Constructs an object setting all non-nullable fields.
+  explicit FileDialogResult(const flutter::EncodableList& paths);
+
+  // Constructs an object setting all fields.
+  explicit FileDialogResult(const flutter::EncodableList& paths,
+                            const int64_t* type_group_index);
+
+  // The selected paths.
+  //
+  // Empty if the dialog was canceled.
+  const flutter::EncodableList& paths() const;
+  void set_paths(const flutter::EncodableList& value_arg);
+
+  // The type group index (into the list provided in [SelectionOptions]) of
+  // the group that was selected when the dialog was confirmed.
+  //
+  // Null if no type groups were provided, or the dialog was canceled.
+  const int64_t* type_group_index() const;
+  void set_type_group_index(const int64_t* value_arg);
+  void set_type_group_index(int64_t value_arg);
+
+ private:
+  static FileDialogResult FromEncodableList(const flutter::EncodableList& list);
+  flutter::EncodableList ToEncodableList() const;
+  friend class FileSelectorApi;
+  friend class FileSelectorApiCodecSerializer;
+  flutter::EncodableList paths_;
+  std::optional<int64_t> type_group_index_;
+};
+
 class FileSelectorApiCodecSerializer : public flutter::StandardCodecSerializer {
  public:
+  FileSelectorApiCodecSerializer();
   inline static FileSelectorApiCodecSerializer& GetInstance() {
     static FileSelectorApiCodecSerializer sInstance;
     return sInstance;
   }
 
-  FileSelectorApiCodecSerializer();
-
- public:
   void WriteValue(const flutter::EncodableValue& value,
                   flutter::ByteStreamWriter* stream) const override;
 
@@ -125,10 +164,10 @@
   FileSelectorApi(const FileSelectorApi&) = delete;
   FileSelectorApi& operator=(const FileSelectorApi&) = delete;
   virtual ~FileSelectorApi() {}
-  virtual ErrorOr<flutter::EncodableList> ShowOpenDialog(
+  virtual ErrorOr<FileDialogResult> ShowOpenDialog(
       const SelectionOptions& options, const std::string* initial_directory,
       const std::string* confirm_button_text) = 0;
-  virtual ErrorOr<flutter::EncodableList> ShowSaveDialog(
+  virtual ErrorOr<FileDialogResult> ShowSaveDialog(
       const SelectionOptions& options, const std::string* initial_directory,
       const std::string* suggested_name,
       const std::string* confirm_button_text) = 0;
diff --git a/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp b/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp
index 8efeb54..ee6d4dc 100644
--- a/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp
+++ b/packages/file_selector/file_selector_windows/windows/test/file_selector_plugin_test.cpp
@@ -28,37 +28,8 @@
 
 using flutter::CustomEncodableValue;
 using flutter::EncodableList;
-using flutter::EncodableMap;
 using flutter::EncodableValue;
 
-// These structs and classes are a workaround for
-// https://github.com/flutter/flutter/issues/104286 and
-// https://github.com/flutter/flutter/issues/104653.
-struct AllowMultipleArg {
-  bool value = false;
-  AllowMultipleArg(bool val) : value(val) {}
-};
-struct SelectFoldersArg {
-  bool value = false;
-  SelectFoldersArg(bool val) : value(val) {}
-};
-SelectionOptions CreateOptions(AllowMultipleArg allow_multiple,
-                               SelectFoldersArg select_folders,
-                               const EncodableList& allowed_types) {
-  SelectionOptions options;
-  options.set_allow_multiple(allow_multiple.value);
-  options.set_select_folders(select_folders.value);
-  options.set_allowed_types(allowed_types);
-  return options;
-}
-TypeGroup CreateTypeGroup(std::string_view label,
-                          const EncodableList& extensions) {
-  TypeGroup group;
-  group.set_label(label);
-  group.set_extensions(extensions);
-  return group;
-}
-
 }  // namespace
 
 TEST(FileSelectorPlugin, TestOpenSimple) {
@@ -87,17 +58,18 @@
   FileSelectorPlugin plugin(
       [fake_window] { return fake_window; },
       std::make_unique<TestFileDialogControllerFactory>(show_validator));
-  ErrorOr<EncodableList> result = plugin.ShowOpenDialog(
-      CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false),
-                    EncodableList()),
+  ErrorOr<FileDialogResult> result = plugin.ShowOpenDialog(
+      SelectionOptions(/* allow multiple = */ false,
+                       /* select folders = */ false, EncodableList()),
       nullptr, nullptr);
 
   EXPECT_TRUE(shown);
   ASSERT_FALSE(result.has_error());
-  const EncodableList& paths = result.value();
-  EXPECT_EQ(paths.size(), 1);
+  const EncodableList& paths = result.value().paths();
+  ASSERT_EQ(paths.size(), 1);
   EXPECT_EQ(std::get<std::string>(paths[0]),
             Utf8FromUtf16(fake_selected_file.path()));
+  EXPECT_EQ(result.value().type_group_index(), nullptr);
 }
 
 TEST(FileSelectorPlugin, TestOpenWithArguments) {
@@ -129,17 +101,18 @@
   // This directory must exist.
   std::string initial_directory("C:\\Program Files");
   std::string confirm_button("Open it!");
-  ErrorOr<EncodableList> result = plugin.ShowOpenDialog(
-      CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false),
-                    EncodableList()),
+  ErrorOr<FileDialogResult> result = plugin.ShowOpenDialog(
+      SelectionOptions(/* allow multiple = */ false,
+                       /* select folders = */ false, EncodableList()),
       &initial_directory, &confirm_button);
 
   EXPECT_TRUE(shown);
   ASSERT_FALSE(result.has_error());
-  const EncodableList& paths = result.value();
-  EXPECT_EQ(paths.size(), 1);
+  const EncodableList& paths = result.value().paths();
+  ASSERT_EQ(paths.size(), 1);
   EXPECT_EQ(std::get<std::string>(paths[0]),
             Utf8FromUtf16(fake_selected_file.path()));
+  EXPECT_EQ(result.value().type_group_index(), nullptr);
 }
 
 TEST(FileSelectorPlugin, TestOpenMultiple) {
@@ -173,19 +146,20 @@
   FileSelectorPlugin plugin(
       [fake_window] { return fake_window; },
       std::make_unique<TestFileDialogControllerFactory>(show_validator));
-  ErrorOr<EncodableList> result = plugin.ShowOpenDialog(
-      CreateOptions(AllowMultipleArg(true), SelectFoldersArg(false),
-                    EncodableList()),
+  ErrorOr<FileDialogResult> result = plugin.ShowOpenDialog(
+      SelectionOptions(/* allow multiple = */ true,
+                       /* select folders = */ false, EncodableList()),
       nullptr, nullptr);
 
   EXPECT_TRUE(shown);
   ASSERT_FALSE(result.has_error());
-  const EncodableList& paths = result.value();
-  EXPECT_EQ(paths.size(), 2);
+  const EncodableList& paths = result.value().paths();
+  ASSERT_EQ(paths.size(), 2);
   EXPECT_EQ(std::get<std::string>(paths[0]),
             Utf8FromUtf16(fake_selected_file_1.path()));
   EXPECT_EQ(std::get<std::string>(paths[1]),
             Utf8FromUtf16(fake_selected_file_2.path()));
+  EXPECT_EQ(result.value().type_group_index(), nullptr);
 }
 
 TEST(FileSelectorPlugin, TestOpenWithFilter) {
@@ -196,18 +170,18 @@
                                         IID_PPV_ARGS(&fake_result_array));
 
   const EncodableValue text_group =
-      CustomEncodableValue(CreateTypeGroup("Text", EncodableList({
-                                                       EncodableValue("txt"),
-                                                       EncodableValue("json"),
-                                                   })));
+      CustomEncodableValue(TypeGroup("Text", EncodableList({
+                                                 EncodableValue("txt"),
+                                                 EncodableValue("json"),
+                                             })));
   const EncodableValue image_group =
-      CustomEncodableValue(CreateTypeGroup("Images", EncodableList({
-                                                         EncodableValue("png"),
-                                                         EncodableValue("gif"),
-                                                         EncodableValue("jpeg"),
-                                                     })));
+      CustomEncodableValue(TypeGroup("Images", EncodableList({
+                                                   EncodableValue("png"),
+                                                   EncodableValue("gif"),
+                                                   EncodableValue("jpeg"),
+                                               })));
   const EncodableValue any_group =
-      CustomEncodableValue(CreateTypeGroup("Any", EncodableList()));
+      CustomEncodableValue(TypeGroup("Any", EncodableList()));
 
   bool shown = false;
   MockShow show_validator = [&shown, fake_result_array, fake_window](
@@ -234,21 +208,26 @@
   FileSelectorPlugin plugin(
       [fake_window] { return fake_window; },
       std::make_unique<TestFileDialogControllerFactory>(show_validator));
-  ErrorOr<EncodableList> result = plugin.ShowOpenDialog(
-      CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false),
-                    EncodableList({
-                        text_group,
-                        image_group,
-                        any_group,
-                    })),
-      nullptr, nullptr);
+  ErrorOr<FileDialogResult> result =
+      plugin.ShowOpenDialog(SelectionOptions(/* allow multiple = */ false,
+                                             /* select folders = */ false,
+                                             EncodableList({
+                                                 text_group,
+                                                 image_group,
+                                                 any_group,
+                                             })),
+                            nullptr, nullptr);
 
   EXPECT_TRUE(shown);
   ASSERT_FALSE(result.has_error());
-  const EncodableList& paths = result.value();
-  EXPECT_EQ(paths.size(), 1);
+  const EncodableList& paths = result.value().paths();
+  ASSERT_EQ(paths.size(), 1);
   EXPECT_EQ(std::get<std::string>(paths[0]),
             Utf8FromUtf16(fake_selected_file.path()));
+  // The test dialog controller always reports the last group as
+  // selected, so that should be what the plugin returns.
+  ASSERT_NE(result.value().type_group_index(), nullptr);
+  EXPECT_EQ(*(result.value().type_group_index()), 2);
 }
 
 TEST(FileSelectorPlugin, TestOpenCancel) {
@@ -265,15 +244,16 @@
   FileSelectorPlugin plugin(
       [fake_window] { return fake_window; },
       std::make_unique<TestFileDialogControllerFactory>(show_validator));
-  ErrorOr<EncodableList> result = plugin.ShowOpenDialog(
-      CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false),
-                    EncodableList()),
+  ErrorOr<FileDialogResult> result = plugin.ShowOpenDialog(
+      SelectionOptions(/* allow multiple = */ false,
+                       /* select folders = */ false, EncodableList()),
       nullptr, nullptr);
 
   EXPECT_TRUE(shown);
   ASSERT_FALSE(result.has_error());
-  const EncodableList& paths = result.value();
+  const EncodableList& paths = result.value().paths();
   EXPECT_EQ(paths.size(), 0);
+  EXPECT_EQ(result.value().type_group_index(), nullptr);
 }
 
 TEST(FileSelectorPlugin, TestSaveSimple) {
@@ -299,17 +279,18 @@
   FileSelectorPlugin plugin(
       [fake_window] { return fake_window; },
       std::make_unique<TestFileDialogControllerFactory>(show_validator));
-  ErrorOr<EncodableList> result = plugin.ShowSaveDialog(
-      CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false),
-                    EncodableList()),
+  ErrorOr<FileDialogResult> result = plugin.ShowSaveDialog(
+      SelectionOptions(/* allow multiple = */ false,
+                       /* select folders = */ false, EncodableList()),
       nullptr, nullptr, nullptr);
 
   EXPECT_TRUE(shown);
   ASSERT_FALSE(result.has_error());
-  const EncodableList& paths = result.value();
-  EXPECT_EQ(paths.size(), 1);
+  const EncodableList& paths = result.value().paths();
+  ASSERT_EQ(paths.size(), 1);
   EXPECT_EQ(std::get<std::string>(paths[0]),
             Utf8FromUtf16(fake_selected_file.path()));
+  EXPECT_EQ(result.value().type_group_index(), nullptr);
 }
 
 TEST(FileSelectorPlugin, TestSaveWithArguments) {
@@ -341,17 +322,78 @@
   std::string initial_directory("C:\\Program Files");
   std::string suggested_name("a name");
   std::string confirm_button("Save it!");
-  ErrorOr<EncodableList> result = plugin.ShowSaveDialog(
-      CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false),
-                    EncodableList()),
+  ErrorOr<FileDialogResult> result = plugin.ShowSaveDialog(
+      SelectionOptions(/* allow multiple = */ false,
+                       /* select folders = */ false, EncodableList()),
       &initial_directory, &suggested_name, &confirm_button);
 
   EXPECT_TRUE(shown);
   ASSERT_FALSE(result.has_error());
-  const EncodableList& paths = result.value();
-  EXPECT_EQ(paths.size(), 1);
+  const EncodableList& paths = result.value().paths();
+  ASSERT_EQ(paths.size(), 1);
   EXPECT_EQ(std::get<std::string>(paths[0]),
             Utf8FromUtf16(fake_selected_file.path()));
+  EXPECT_EQ(result.value().type_group_index(), nullptr);
+}
+
+TEST(FileSelectorPlugin, TestSaveWithFilter) {
+  const HWND fake_window = reinterpret_cast<HWND>(1337);
+  ScopedTestShellItem fake_selected_file;
+
+  const EncodableValue text_group =
+      CustomEncodableValue(TypeGroup("Text", EncodableList({
+                                                 EncodableValue("txt"),
+                                                 EncodableValue("json"),
+                                             })));
+  const EncodableValue image_group =
+      CustomEncodableValue(TypeGroup("Images", EncodableList({
+                                                   EncodableValue("png"),
+                                                   EncodableValue("gif"),
+                                                   EncodableValue("jpeg"),
+                                               })));
+
+  bool shown = false;
+  MockShow show_validator =
+      [&shown, fake_result = fake_selected_file.file(), fake_window](
+          const TestFileDialogController& dialog, HWND parent) {
+        shown = true;
+        EXPECT_EQ(parent, fake_window);
+
+        // Validate filter.
+        const std::vector<DialogFilter>& filters = dialog.GetFileTypes();
+        EXPECT_EQ(filters.size(), 2U);
+        if (filters.size() == 2U) {
+          EXPECT_EQ(filters[0].name, L"Text");
+          EXPECT_EQ(filters[0].spec, L"*.txt;*.json");
+          EXPECT_EQ(filters[1].name, L"Images");
+          EXPECT_EQ(filters[1].spec, L"*.png;*.gif;*.jpeg");
+        }
+
+        return MockShowResult(fake_result);
+      };
+
+  FileSelectorPlugin plugin(
+      [fake_window] { return fake_window; },
+      std::make_unique<TestFileDialogControllerFactory>(show_validator));
+  ErrorOr<FileDialogResult> result =
+      plugin.ShowSaveDialog(SelectionOptions(/* allow multiple = */ false,
+                                             /* select folders = */ false,
+                                             EncodableList({
+                                                 text_group,
+                                                 image_group,
+                                             })),
+                            nullptr, nullptr, nullptr);
+
+  EXPECT_TRUE(shown);
+  ASSERT_FALSE(result.has_error());
+  const EncodableList& paths = result.value().paths();
+  ASSERT_EQ(paths.size(), 1);
+  EXPECT_EQ(std::get<std::string>(paths[0]),
+            Utf8FromUtf16(fake_selected_file.path()));
+  // The test dialog controller always reports the last group as
+  // selected, so that should be what the plugin returns.
+  ASSERT_NE(result.value().type_group_index(), nullptr);
+  EXPECT_EQ(*(result.value().type_group_index()), 1);
 }
 
 TEST(FileSelectorPlugin, TestSaveCancel) {
@@ -368,15 +410,16 @@
   FileSelectorPlugin plugin(
       [fake_window] { return fake_window; },
       std::make_unique<TestFileDialogControllerFactory>(show_validator));
-  ErrorOr<EncodableList> result = plugin.ShowSaveDialog(
-      CreateOptions(AllowMultipleArg(false), SelectFoldersArg(false),
-                    EncodableList()),
+  ErrorOr<FileDialogResult> result = plugin.ShowSaveDialog(
+      SelectionOptions(/* allow multiple = */ false,
+                       /* select folders = */ false, EncodableList()),
       nullptr, nullptr, nullptr);
 
   EXPECT_TRUE(shown);
   ASSERT_FALSE(result.has_error());
-  const EncodableList& paths = result.value();
+  const EncodableList& paths = result.value().paths();
   EXPECT_EQ(paths.size(), 0);
+  EXPECT_EQ(result.value().type_group_index(), nullptr);
 }
 
 TEST(FileSelectorPlugin, TestGetDirectorySimple) {
@@ -408,16 +451,17 @@
   FileSelectorPlugin plugin(
       [fake_window] { return fake_window; },
       std::make_unique<TestFileDialogControllerFactory>(show_validator));
-  ErrorOr<EncodableList> result = plugin.ShowOpenDialog(
-      CreateOptions(AllowMultipleArg(false), SelectFoldersArg(true),
-                    EncodableList()),
+  ErrorOr<FileDialogResult> result = plugin.ShowOpenDialog(
+      SelectionOptions(/* allow multiple = */ false,
+                       /* select folders = */ true, EncodableList()),
       nullptr, nullptr);
 
   EXPECT_TRUE(shown);
   ASSERT_FALSE(result.has_error());
-  const EncodableList& paths = result.value();
-  EXPECT_EQ(paths.size(), 1);
+  const EncodableList& paths = result.value().paths();
+  ASSERT_EQ(paths.size(), 1);
   EXPECT_EQ(std::get<std::string>(paths[0]), "C:\\Program Files");
+  EXPECT_EQ(result.value().type_group_index(), nullptr);
 }
 
 TEST(FileSelectorPlugin, TestGetDirectoryMultiple) {
@@ -454,19 +498,20 @@
   FileSelectorPlugin plugin(
       [fake_window] { return fake_window; },
       std::make_unique<TestFileDialogControllerFactory>(show_validator));
-  ErrorOr<EncodableList> result = plugin.ShowOpenDialog(
-      CreateOptions(AllowMultipleArg(true), SelectFoldersArg(true),
-                    EncodableList()),
+  ErrorOr<FileDialogResult> result = plugin.ShowOpenDialog(
+      SelectionOptions(/* allow multiple = */ true, /* select folders = */ true,
+                       EncodableList()),
       nullptr, nullptr);
 
   EXPECT_TRUE(shown);
   ASSERT_FALSE(result.has_error());
-  const EncodableList& paths = result.value();
-  EXPECT_EQ(paths.size(), 2);
+  const EncodableList& paths = result.value().paths();
+  ASSERT_EQ(paths.size(), 2);
   EXPECT_EQ(std::get<std::string>(paths[0]),
             Utf8FromUtf16(fake_selected_dir_1.path()));
   EXPECT_EQ(std::get<std::string>(paths[1]),
             Utf8FromUtf16(fake_selected_dir_2.path()));
+  EXPECT_EQ(result.value().type_group_index(), nullptr);
 }
 
 TEST(FileSelectorPlugin, TestGetDirectoryCancel) {
@@ -483,15 +528,16 @@
   FileSelectorPlugin plugin(
       [fake_window] { return fake_window; },
       std::make_unique<TestFileDialogControllerFactory>(show_validator));
-  ErrorOr<EncodableList> result = plugin.ShowOpenDialog(
-      CreateOptions(AllowMultipleArg(false), SelectFoldersArg(true),
-                    EncodableList()),
+  ErrorOr<FileDialogResult> result = plugin.ShowOpenDialog(
+      SelectionOptions(/* allow multiple = */ false,
+                       /* select folders = */ true, EncodableList()),
       nullptr, nullptr);
 
   EXPECT_TRUE(shown);
   ASSERT_FALSE(result.has_error());
-  const EncodableList& paths = result.value();
+  const EncodableList& paths = result.value().paths();
   EXPECT_EQ(paths.size(), 0);
+  EXPECT_EQ(result.value().type_group_index(), nullptr);
 }
 
 }  // namespace test
diff --git a/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.cpp b/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.cpp
index 15065f9..e775aa6 100644
--- a/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.cpp
+++ b/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.cpp
@@ -60,6 +60,13 @@
   return S_OK;
 }
 
+HRESULT TestFileDialogController::GetFileTypeIndex(UINT* out_index) const {
+  // Arbitrarily always return the last group. (No -1 because the return value
+  // from GetFileTypeIndex is defined to be one-indexed.)
+  *out_index = static_cast<UINT>(filter_groups_.size());
+  return S_OK;
+}
+
 HRESULT TestFileDialogController::GetResults(
     IShellItemArray** out_items) const {
   *out_items = std::get<IShellItemArrayPtr>(mock_result_);
diff --git a/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.h b/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.h
index 1c221fc..e3aa093 100644
--- a/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.h
+++ b/packages/file_selector/file_selector_windows/windows/test/test_file_dialog_controller.h
@@ -56,6 +56,7 @@
   HRESULT SetOkButtonLabel(const wchar_t* text) override;
   HRESULT Show(HWND parent) override;
   HRESULT GetResult(IShellItem** out_item) const override;
+  HRESULT GetFileTypeIndex(UINT* out_index) const override;
   HRESULT GetResults(IShellItemArray** out_items) const override;
 
   // Accessors for validating IFileDialogController setter calls.