[file_selector] Import Linux implementation from FDE (#6292)

diff --git a/.cirrus.yml b/.cirrus.yml
index 1ac69d6..059f5fe 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -212,9 +212,9 @@
         - flutter config --enable-linux-desktop
         - ./script/tool_runner.sh build-examples --linux
       native_test_script:
-        - ./script/tool_runner.sh native-test --linux --no-integration
+        - xvfb-run ./script/tool_runner.sh native-test --linux --no-integration
       drive_script:
-        - xvfb-run ./script/tool_runner.sh drive-examples --linux
+        - xvfb-run ./script/tool_runner.sh drive-examples --linux --exclude=script/configs/exclude_integration_linux.yaml
 
 # Heavy-workload Linux tasks.
 # These use machines with more CPUs and memory, so will reduce parallelization
diff --git a/packages/file_selector/file_selector_linux/.gitignore b/packages/file_selector/file_selector_linux/.gitignore
new file mode 100644
index 0000000..0393a47
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/.gitignore
@@ -0,0 +1,5 @@
+.dart_tool
+.packages
+.flutter-plugins
+.flutter-plugins-dependencies
+pubspec.lock
diff --git a/packages/file_selector/file_selector_linux/AUTHORS b/packages/file_selector/file_selector_linux/AUTHORS
new file mode 100644
index 0000000..557dff9
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/AUTHORS
@@ -0,0 +1,6 @@
+# Below is a list of people and organizations that have contributed
+# to the Flutter project. Names should be added to the list like so:
+#
+#   Name/Organization <email address>
+
+Google Inc.
diff --git a/packages/file_selector/file_selector_linux/CHANGELOG.md b/packages/file_selector/file_selector_linux/CHANGELOG.md
new file mode 100644
index 0000000..a63a52d
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/CHANGELOG.md
@@ -0,0 +1,19 @@
+## 0.9.0
+
+* Moves source to flutter/plugins.
+
+## 0.0.3
+
+* Adds Dart implementation for in-package method channel.
+
+## 0.0.2+1
+
+* Updates README
+
+## 0.0.2
+
+* Updates SDK constraint to signal compatibility with null safety.
+
+## 0.0.1
+
+* Initial Linux implementation of `file_selector`.
diff --git a/packages/file_selector/file_selector_linux/LICENSE b/packages/file_selector/file_selector_linux/LICENSE
new file mode 100644
index 0000000..c6823b8
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2013 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/file_selector/file_selector_linux/README.md b/packages/file_selector/file_selector_linux/README.md
new file mode 100644
index 0000000..55a0529
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/README.md
@@ -0,0 +1,11 @@
+# file\_selector\_linux
+
+The Linux implementation of [`file_selector`][1].
+
+## Usage
+
+This package is [endorsed][2], which means you can simply use `file_selector`
+normally. This package will be automatically included in your app when you do.
+
+[1]: https://pub.dev/packages/file_selector
+[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin
diff --git a/packages/file_selector/file_selector_linux/example/.gitignore b/packages/file_selector/file_selector_linux/example/.gitignore
new file mode 100644
index 0000000..7abd075
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/.gitignore
@@ -0,0 +1,48 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Currently only web supported
+android/
+ios/
+
+# Exceptions to above rules.
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/file_selector/file_selector_linux/example/.metadata b/packages/file_selector/file_selector_linux/example/.metadata
new file mode 100644
index 0000000..897381f
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 7736f3bc90270dcb0480db2ccffbf1d13c28db85
+  channel: dev
+
+project_type: app
diff --git a/packages/file_selector/file_selector_linux/example/README.md b/packages/file_selector/file_selector_linux/example/README.md
new file mode 100644
index 0000000..2f9f8c0
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/README.md
@@ -0,0 +1,4 @@
+# `file_selector_linux` example
+
+Demonstrates Linux implementation of the
+[`file_selector` plugin](https://pub.dev/packages/file_selector).
diff --git a/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart b/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart
new file mode 100644
index 0000000..0699dd1
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart
@@ -0,0 +1,83 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:flutter/material.dart';
+
+/// Screen that allows the user to select a directory using `getDirectoryPath`,
+///  then displays the selected directory in a dialog.
+class GetDirectoryPage extends StatelessWidget {
+  /// Default Constructor
+  const GetDirectoryPage({Key? key}) : super(key: key);
+
+  Future<void> _getDirectoryPath(BuildContext context) async {
+    const String confirmButtonText = 'Choose';
+    final String? directoryPath =
+        await FileSelectorPlatform.instance.getDirectoryPath(
+      confirmButtonText: confirmButtonText,
+    );
+    if (directoryPath == null) {
+      // Operation was canceled by the user.
+      return;
+    }
+    await showDialog<void>(
+      context: context,
+      builder: (BuildContext context) => TextDisplay(directoryPath),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Open a text file'),
+      ),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            ElevatedButton(
+              style: ElevatedButton.styleFrom(
+                // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+                // ignore: deprecated_member_use
+                primary: Colors.blue,
+                // ignore: deprecated_member_use
+                onPrimary: Colors.white,
+              ),
+              child: const Text('Press to ask user to choose a directory'),
+              onPressed: () => _getDirectoryPath(context),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+/// Widget that displays a text file in a dialog.
+class TextDisplay extends StatelessWidget {
+  /// Creates a `TextDisplay`.
+  const TextDisplay(this.directoryPath, {Key? key}) : super(key: key);
+
+  /// The path selected in the dialog.
+  final String directoryPath;
+
+  @override
+  Widget build(BuildContext context) {
+    return AlertDialog(
+      title: const Text('Selected Directory'),
+      content: Scrollbar(
+        child: SingleChildScrollView(
+          child: Text(directoryPath),
+        ),
+      ),
+      actions: <Widget>[
+        TextButton(
+          child: const Text('Close'),
+          onPressed: () => Navigator.pop(context),
+        ),
+      ],
+    );
+  }
+}
diff --git a/packages/file_selector/file_selector_linux/example/lib/home_page.dart b/packages/file_selector/file_selector_linux/example/lib/home_page.dart
new file mode 100644
index 0000000..a4b2ae1
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/lib/home_page.dart
@@ -0,0 +1,63 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+/// Home Page of the application.
+class HomePage extends StatelessWidget {
+  /// Default Constructor
+  const HomePage({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final ButtonStyle style = ElevatedButton.styleFrom(
+      // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+      // ignore: deprecated_member_use
+      primary: Colors.blue,
+      // ignore: deprecated_member_use
+      onPrimary: Colors.white,
+    );
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('File Selector Demo Home Page'),
+      ),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            ElevatedButton(
+              style: style,
+              child: const Text('Open a text file'),
+              onPressed: () => Navigator.pushNamed(context, '/open/text'),
+            ),
+            const SizedBox(height: 10),
+            ElevatedButton(
+              style: style,
+              child: const Text('Open an image'),
+              onPressed: () => Navigator.pushNamed(context, '/open/image'),
+            ),
+            const SizedBox(height: 10),
+            ElevatedButton(
+              style: style,
+              child: const Text('Open multiple images'),
+              onPressed: () => Navigator.pushNamed(context, '/open/images'),
+            ),
+            const SizedBox(height: 10),
+            ElevatedButton(
+              style: style,
+              child: const Text('Save a file'),
+              onPressed: () => Navigator.pushNamed(context, '/save/text'),
+            ),
+            const SizedBox(height: 10),
+            ElevatedButton(
+              style: style,
+              child: const Text('Open a get directory dialog'),
+              onPressed: () => Navigator.pushNamed(context, '/directory'),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/packages/file_selector/file_selector_linux/example/lib/main.dart b/packages/file_selector/file_selector_linux/example/lib/main.dart
new file mode 100644
index 0000000..3e44710
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/lib/main.dart
@@ -0,0 +1,42 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/material.dart';
+
+import 'get_directory_page.dart';
+import 'home_page.dart';
+import 'open_image_page.dart';
+import 'open_multiple_images_page.dart';
+import 'open_text_page.dart';
+import 'save_text_page.dart';
+
+void main() {
+  runApp(const MyApp());
+}
+
+/// MyApp is the Main Application.
+class MyApp extends StatelessWidget {
+  /// Default Constructor
+  const MyApp({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      title: 'File Selector Demo',
+      theme: ThemeData(
+        primarySwatch: Colors.blue,
+        visualDensity: VisualDensity.adaptivePlatformDensity,
+      ),
+      home: const HomePage(),
+      routes: <String, WidgetBuilder>{
+        '/open/image': (BuildContext context) => const OpenImagePage(),
+        '/open/images': (BuildContext context) =>
+            const OpenMultipleImagesPage(),
+        '/open/text': (BuildContext context) => const OpenTextPage(),
+        '/save/text': (BuildContext context) => SaveTextPage(),
+        '/directory': (BuildContext context) => const GetDirectoryPage(),
+      },
+    );
+  }
+}
diff --git a/packages/file_selector/file_selector_linux/example/lib/open_image_page.dart b/packages/file_selector/file_selector_linux/example/lib/open_image_page.dart
new file mode 100644
index 0000000..9e1d207
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/lib/open_image_page.dart
@@ -0,0 +1,94 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+/// Screen that allows the user to select an image file using
+/// `openFiles`, then displays the selected images in a gallery dialog.
+class OpenImagePage extends StatelessWidget {
+  /// Default Constructor
+  const OpenImagePage({Key? key}) : super(key: key);
+
+  Future<void> _openImageFile(BuildContext context) async {
+    final XTypeGroup typeGroup = XTypeGroup(
+      label: 'images',
+      extensions: <String>['jpg', 'png'],
+    );
+    final XFile? file = await FileSelectorPlatform.instance
+        .openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
+    if (file == null) {
+      // Operation was canceled by the user.
+      return;
+    }
+    final String fileName = file.name;
+    final String filePath = file.path;
+
+    await showDialog<void>(
+      context: context,
+      builder: (BuildContext context) => ImageDisplay(fileName, filePath),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Open an image'),
+      ),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            ElevatedButton(
+              style: ElevatedButton.styleFrom(
+                // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+                // ignore: deprecated_member_use
+                primary: Colors.blue,
+                // ignore: deprecated_member_use
+                onPrimary: Colors.white,
+              ),
+              child: const Text('Press to open an image file(png, jpg)'),
+              onPressed: () => _openImageFile(context),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+/// Widget that displays an image in a dialog.
+class ImageDisplay extends StatelessWidget {
+  /// Default Constructor.
+  const ImageDisplay(this.fileName, this.filePath, {Key? key})
+      : super(key: key);
+
+  /// The name of the selected file.
+  final String fileName;
+
+  /// The path to the selected file.
+  final String filePath;
+
+  @override
+  Widget build(BuildContext context) {
+    return AlertDialog(
+      title: Text(fileName),
+      // On web the filePath is a blob url
+      // while on other platforms it is a system path.
+      content: kIsWeb ? Image.network(filePath) : Image.file(File(filePath)),
+      actions: <Widget>[
+        TextButton(
+          child: const Text('Close'),
+          onPressed: () {
+            Navigator.pop(context);
+          },
+        ),
+      ],
+    );
+  }
+}
diff --git a/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart
new file mode 100644
index 0000000..21da8c2
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart
@@ -0,0 +1,105 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+/// Screen that allows the user to select multiple image files using
+/// `openFiles`, then displays the selected images in a gallery dialog.
+class OpenMultipleImagesPage extends StatelessWidget {
+  /// Default Constructor
+  const OpenMultipleImagesPage({Key? key}) : super(key: key);
+
+  Future<void> _openImageFile(BuildContext context) async {
+    final XTypeGroup jpgsTypeGroup = XTypeGroup(
+      label: 'JPEGs',
+      extensions: <String>['jpg', 'jpeg'],
+    );
+    final XTypeGroup pngTypeGroup = XTypeGroup(
+      label: 'PNGs',
+      extensions: <String>['png'],
+    );
+    final List<XFile> files = await FileSelectorPlatform.instance
+        .openFiles(acceptedTypeGroups: <XTypeGroup>[
+      jpgsTypeGroup,
+      pngTypeGroup,
+    ]);
+    if (files.isEmpty) {
+      // Operation was canceled by the user.
+      return;
+    }
+    await showDialog<void>(
+      context: context,
+      builder: (BuildContext context) => MultipleImagesDisplay(files),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Open multiple images'),
+      ),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            ElevatedButton(
+              style: ElevatedButton.styleFrom(
+                // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+                // ignore: deprecated_member_use
+                primary: Colors.blue,
+                // ignore: deprecated_member_use
+                onPrimary: Colors.white,
+              ),
+              child: const Text('Press to open multiple images (png, jpg)'),
+              onPressed: () => _openImageFile(context),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+/// Widget that displays a text file in a dialog.
+class MultipleImagesDisplay extends StatelessWidget {
+  /// Default Constructor.
+  const MultipleImagesDisplay(this.files, {Key? key}) : super(key: key);
+
+  /// The files containing the images.
+  final List<XFile> files;
+
+  @override
+  Widget build(BuildContext context) {
+    return AlertDialog(
+      title: const Text('Gallery'),
+      // On web the filePath is a blob url
+      // while on other platforms it is a system path.
+      content: Center(
+        child: Row(
+          children: <Widget>[
+            ...files.map(
+              (XFile file) => Flexible(
+                  child: kIsWeb
+                      ? Image.network(file.path)
+                      : Image.file(File(file.path))),
+            )
+          ],
+        ),
+      ),
+      actions: <Widget>[
+        TextButton(
+          child: const Text('Close'),
+          onPressed: () {
+            Navigator.pop(context);
+          },
+        ),
+      ],
+    );
+  }
+}
diff --git a/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart b/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart
new file mode 100644
index 0000000..05c6d16
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart
@@ -0,0 +1,91 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:flutter/material.dart';
+
+/// Screen that allows the user to select a text file using `openFile`, then
+/// displays its contents in a dialog.
+class OpenTextPage extends StatelessWidget {
+  /// Default Constructor
+  const OpenTextPage({Key? key}) : super(key: key);
+
+  Future<void> _openTextFile(BuildContext context) async {
+    final XTypeGroup typeGroup = XTypeGroup(
+      label: 'text',
+      extensions: <String>['txt', 'json'],
+    );
+    final XFile? file = await FileSelectorPlatform.instance
+        .openFile(acceptedTypeGroups: <XTypeGroup>[typeGroup]);
+    if (file == null) {
+      // Operation was canceled by the user.
+      return;
+    }
+    final String fileName = file.name;
+    final String fileContent = await file.readAsString();
+
+    await showDialog<void>(
+      context: context,
+      builder: (BuildContext context) => TextDisplay(fileName, fileContent),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Open a text file'),
+      ),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            ElevatedButton(
+              style: ElevatedButton.styleFrom(
+                // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+                // ignore: deprecated_member_use
+                primary: Colors.blue,
+                // ignore: deprecated_member_use
+                onPrimary: Colors.white,
+              ),
+              child: const Text('Press to open a text file (json, txt)'),
+              onPressed: () => _openTextFile(context),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+/// Widget that displays a text file in a dialog.
+class TextDisplay extends StatelessWidget {
+  /// Default Constructor.
+  const TextDisplay(this.fileName, this.fileContent, {Key? key})
+      : super(key: key);
+
+  /// The name of the selected file.
+  final String fileName;
+
+  /// The contents of the text file.
+  final String fileContent;
+
+  @override
+  Widget build(BuildContext context) {
+    return AlertDialog(
+      title: Text(fileName),
+      content: Scrollbar(
+        child: SingleChildScrollView(
+          child: Text(fileContent),
+        ),
+      ),
+      actions: <Widget>[
+        TextButton(
+          child: const Text('Close'),
+          onPressed: () => Navigator.pop(context),
+        ),
+      ],
+    );
+  }
+}
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
new file mode 100644
index 0000000..9803f28
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/lib/save_text_page.dart
@@ -0,0 +1,84 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:typed_data';
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:flutter/material.dart';
+
+/// Screen that allows the user to select a save location using `getSavePath`,
+/// then writes text to a file at that location.
+class SaveTextPage extends StatelessWidget {
+  /// Default Constructor
+  SaveTextPage({Key? key}) : super(key: key);
+
+  final TextEditingController _nameController = TextEditingController();
+  final TextEditingController _contentController = TextEditingController();
+
+  Future<void> _saveFile() async {
+    final String fileName = _nameController.text;
+    final String? path = await FileSelectorPlatform.instance.getSavePath(
+      // Operation was canceled by the user.
+      suggestedName: fileName,
+    );
+    if (path == null) {
+      return;
+    }
+    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);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('Save text into a file'),
+      ),
+      body: Center(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: <Widget>[
+            Container(
+              width: 300,
+              child: TextField(
+                minLines: 1,
+                maxLines: 12,
+                controller: _nameController,
+                decoration: const InputDecoration(
+                  hintText: '(Optional) Suggest File Name',
+                ),
+              ),
+            ),
+            Container(
+              width: 300,
+              child: TextField(
+                minLines: 1,
+                maxLines: 12,
+                controller: _contentController,
+                decoration: const InputDecoration(
+                  hintText: 'Enter File Contents',
+                ),
+              ),
+            ),
+            const SizedBox(height: 10),
+            ElevatedButton(
+              style: ElevatedButton.styleFrom(
+                // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724
+                // ignore: deprecated_member_use
+                primary: Colors.blue,
+                // ignore: deprecated_member_use
+                onPrimary: Colors.white,
+              ),
+              onPressed: _saveFile,
+              child: const Text('Press to save a text file'),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/packages/file_selector/file_selector_linux/example/linux/.gitignore b/packages/file_selector/file_selector_linux/example/linux/.gitignore
new file mode 100644
index 0000000..d3896c9
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/linux/.gitignore
@@ -0,0 +1 @@
+flutter/ephemeral
diff --git a/packages/file_selector/file_selector_linux/example/linux/CMakeLists.txt b/packages/file_selector/file_selector_linux/example/linux/CMakeLists.txt
new file mode 100644
index 0000000..9d7224c
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/linux/CMakeLists.txt
@@ -0,0 +1,111 @@
+cmake_minimum_required(VERSION 3.10)
+project(runner LANGUAGES CXX)
+
+set(BINARY_NAME "example")
+set(APPLICATION_ID "com.example.example")
+
+cmake_policy(SET CMP0063 NEW)
+
+set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
+
+# Configure build options.
+if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+  set(CMAKE_BUILD_TYPE "Debug" CACHE
+    STRING "Flutter build mode" FORCE)
+  set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+    "Debug" "Profile" "Release")
+endif()
+
+# Compilation settings that should be applied to most targets.
+function(APPLY_STANDARD_SETTINGS TARGET)
+  target_compile_features(${TARGET} PUBLIC cxx_std_14)
+  target_compile_options(${TARGET} PRIVATE -Wall -Werror)
+  target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
+  target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
+endfunction()
+
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+
+# Flutter library and tool build rules.
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+
+add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
+
+# Application build
+add_executable(${BINARY_NAME}
+  "main.cc"
+  "my_application.cc"
+  "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+)
+apply_standard_settings(${BINARY_NAME})
+target_link_libraries(${BINARY_NAME} PRIVATE flutter)
+target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
+add_dependencies(${BINARY_NAME} flutter_assemble)
+# Only the install-generated bundle's copy of the executable will launch
+# correctly, since the resources must in the right relative locations. To avoid
+# people trying to run the unbundled copy, put it in a subdirectory instead of
+# the default top-level location.
+set_target_properties(${BINARY_NAME}
+  PROPERTIES
+  RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
+)
+
+# Enable the test target.
+set(include_file_selector_linux_tests TRUE)
+# Provide an alias for the test target using the name expected by repo tooling.
+add_custom_target(unit_tests DEPENDS file_selector_linux_test)
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# By default, "installing" just makes a relocatable bundle in the build
+# directory.
+set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+  set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+# Start with a clean build bundle directory every time.
+install(CODE "
+  file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
+  " COMPONENT Runtime)
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+  COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+  COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+  COMPONENT Runtime)
+
+if(PLUGIN_BUNDLED_LIBRARIES)
+  install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
+    DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+    COMPONENT Runtime)
+endif()
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+  file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+  " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+  DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
+  install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+    COMPONENT Runtime)
+endif()
diff --git a/packages/file_selector/file_selector_linux/example/linux/flutter/CMakeLists.txt b/packages/file_selector/file_selector_linux/example/linux/flutter/CMakeLists.txt
new file mode 100644
index 0000000..33fd580
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/linux/flutter/CMakeLists.txt
@@ -0,0 +1,87 @@
+cmake_minimum_required(VERSION 3.10)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+
+# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
+# which isn't available in 3.10.
+function(list_prepend LIST_NAME PREFIX)
+    set(NEW_LIST "")
+    foreach(element ${${LIST_NAME}})
+        list(APPEND NEW_LIST "${PREFIX}${element}")
+    endforeach(element)
+    set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
+endfunction()
+
+# === Flutter Library ===
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
+pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
+
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+  "fl_basic_message_channel.h"
+  "fl_binary_codec.h"
+  "fl_binary_messenger.h"
+  "fl_dart_project.h"
+  "fl_engine.h"
+  "fl_json_message_codec.h"
+  "fl_json_method_codec.h"
+  "fl_message_codec.h"
+  "fl_method_call.h"
+  "fl_method_channel.h"
+  "fl_method_codec.h"
+  "fl_method_response.h"
+  "fl_plugin_registrar.h"
+  "fl_plugin_registry.h"
+  "fl_standard_message_codec.h"
+  "fl_standard_method_codec.h"
+  "fl_string_codec.h"
+  "fl_value.h"
+  "fl_view.h"
+  "flutter_linux.h"
+)
+list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+  "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
+target_link_libraries(flutter INTERFACE
+  PkgConfig::GTK
+  PkgConfig::GLIB
+  PkgConfig::GIO
+)
+add_dependencies(flutter flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+add_custom_command(
+  OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+    ${CMAKE_CURRENT_BINARY_DIR}/_phony_
+  COMMAND ${CMAKE_COMMAND} -E env
+    ${FLUTTER_TOOL_ENVIRONMENT}
+    "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
+      ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
+  VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+  "${FLUTTER_LIBRARY}"
+  ${FLUTTER_LIBRARY_HEADERS}
+)
diff --git a/packages/file_selector/file_selector_linux/example/linux/flutter/generated_plugins.cmake b/packages/file_selector/file_selector_linux/example/linux/flutter/generated_plugins.cmake
new file mode 100644
index 0000000..2db3c22
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/linux/flutter/generated_plugins.cmake
@@ -0,0 +1,24 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+  file_selector_linux
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
+  target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+  list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
+  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
+endforeach(plugin)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+  add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
+  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/packages/file_selector/file_selector_linux/example/linux/main.cc b/packages/file_selector/file_selector_linux/example/linux/main.cc
new file mode 100644
index 0000000..1507d02
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/linux/main.cc
@@ -0,0 +1,10 @@
+// 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.
+
+#include "my_application.h"
+
+int main(int argc, char** argv) {
+  g_autoptr(MyApplication) app = my_application_new();
+  return g_application_run(G_APPLICATION(app), argc, argv);
+}
diff --git a/packages/file_selector/file_selector_linux/example/linux/my_application.cc b/packages/file_selector/file_selector_linux/example/linux/my_application.cc
new file mode 100644
index 0000000..e970be0
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/linux/my_application.cc
@@ -0,0 +1,110 @@
+// 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.
+
+#include "my_application.h"
+
+#include <flutter_linux/flutter_linux.h>
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#endif
+
+#include "flutter/generated_plugin_registrant.h"
+
+struct _MyApplication {
+  GtkApplication parent_instance;
+  char** dart_entrypoint_arguments;
+};
+
+G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
+
+// Implements GApplication::activate.
+static void my_application_activate(GApplication* application) {
+  MyApplication* self = MY_APPLICATION(application);
+  GtkWindow* window =
+      GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
+
+  // Use a header bar when running in GNOME as this is the common style used
+  // by applications and is the setup most users will be using (e.g. Ubuntu
+  // desktop).
+  // If running on X and not using GNOME then just use a traditional title bar
+  // in case the window manager does more exotic layout, e.g. tiling.
+  // If running on Wayland assume the header bar will work (may need changing
+  // if future cases occur).
+  gboolean use_header_bar = TRUE;
+#ifdef GDK_WINDOWING_X11
+  GdkScreen* screen = gtk_window_get_screen(window);
+  if (GDK_IS_X11_SCREEN(screen)) {
+    const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
+    if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
+      use_header_bar = FALSE;
+    }
+  }
+#endif
+  if (use_header_bar) {
+    GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
+    gtk_widget_show(GTK_WIDGET(header_bar));
+    gtk_header_bar_set_title(header_bar, "example");
+    gtk_header_bar_set_show_close_button(header_bar, TRUE);
+    gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
+  } else {
+    gtk_window_set_title(window, "example");
+  }
+
+  gtk_window_set_default_size(window, 1280, 720);
+  gtk_widget_show(GTK_WIDGET(window));
+
+  g_autoptr(FlDartProject) project = fl_dart_project_new();
+  fl_dart_project_set_dart_entrypoint_arguments(
+      project, self->dart_entrypoint_arguments);
+
+  FlView* view = fl_view_new(project);
+  gtk_widget_show(GTK_WIDGET(view));
+  gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
+
+  fl_register_plugins(FL_PLUGIN_REGISTRY(view));
+
+  gtk_widget_grab_focus(GTK_WIDGET(view));
+}
+
+// Implements GApplication::local_command_line.
+static gboolean my_application_local_command_line(GApplication* application,
+                                                  gchar*** arguments,
+                                                  int* exit_status) {
+  MyApplication* self = MY_APPLICATION(application);
+  // Strip out the first argument as it is the binary name.
+  self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
+
+  g_autoptr(GError) error = nullptr;
+  if (!g_application_register(application, nullptr, &error)) {
+    g_warning("Failed to register: %s", error->message);
+    *exit_status = 1;
+    return TRUE;
+  }
+
+  g_application_activate(application);
+  *exit_status = 0;
+
+  return TRUE;
+}
+
+// Implements GObject::dispose.
+static void my_application_dispose(GObject* object) {
+  MyApplication* self = MY_APPLICATION(object);
+  g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
+  G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
+}
+
+static void my_application_class_init(MyApplicationClass* klass) {
+  G_APPLICATION_CLASS(klass)->activate = my_application_activate;
+  G_APPLICATION_CLASS(klass)->local_command_line =
+      my_application_local_command_line;
+  G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
+}
+
+static void my_application_init(MyApplication* self) {}
+
+MyApplication* my_application_new() {
+  return MY_APPLICATION(g_object_new(
+      my_application_get_type(), "application-id", APPLICATION_ID, nullptr));
+}
diff --git a/packages/file_selector/file_selector_linux/example/linux/my_application.h b/packages/file_selector/file_selector_linux/example/linux/my_application.h
new file mode 100644
index 0000000..6e9f0c3
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/linux/my_application.h
@@ -0,0 +1,22 @@
+// 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.
+
+#ifndef FLUTTER_MY_APPLICATION_H_
+#define FLUTTER_MY_APPLICATION_H_
+
+#include <gtk/gtk.h>
+
+G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
+                     GtkApplication)
+
+/**
+ * my_application_new:
+ *
+ * Creates a new Flutter-based application.
+ *
+ * Returns: a new #MyApplication.
+ */
+MyApplication* my_application_new();
+
+#endif  // FLUTTER_MY_APPLICATION_H_
diff --git a/packages/file_selector/file_selector_linux/example/pubspec.yaml b/packages/file_selector/file_selector_linux/example/pubspec.yaml
new file mode 100644
index 0000000..857c950
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/example/pubspec.yaml
@@ -0,0 +1,21 @@
+name: file_selector_linux_example
+description: Local testbed for Linux file_selector implementation.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+version: 1.0.0+1
+
+environment:
+  sdk: ">=2.12.0 <3.0.0"
+
+dependencies:
+  file_selector_linux:
+    path: ../
+  file_selector_platform_interface: ^2.0.0
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+flutter:
+  uses-material-design: true
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
new file mode 100644
index 0000000..430b41c
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart
@@ -0,0 +1,144 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:flutter/foundation.dart' show visibleForTesting;
+import 'package:flutter/services.dart';
+
+const MethodChannel _channel =
+    MethodChannel('plugins.flutter.dev/file_selector_linux');
+
+const String _typeGroupLabelKey = 'label';
+const String _typeGroupExtensionsKey = 'extensions';
+const String _typeGroupMimeTypesKey = 'mimeTypes';
+
+const String _openFileMethod = 'openFile';
+const String _getSavePathMethod = 'getSavePath';
+const String _getDirectoryPathMethod = 'getDirectoryPath';
+
+const String _acceptedTypeGroupsKey = 'acceptedTypeGroups';
+const String _confirmButtonTextKey = 'confirmButtonText';
+const String _initialDirectoryKey = 'initialDirectory';
+const String _multipleKey = 'multiple';
+const String _suggestedNameKey = 'suggestedName';
+
+/// An implementation of [FileSelectorPlatform] for Linux.
+class FileSelectorLinux extends FileSelectorPlatform {
+  /// The MethodChannel that is being used by this implementation of the plugin.
+  @visibleForTesting
+  MethodChannel get channel => _channel;
+
+  /// Registers the Linux implementation.
+  static void registerWith() {
+    FileSelectorPlatform.instance = FileSelectorLinux();
+  }
+
+  @override
+  Future<XFile?> openFile({
+    List<XTypeGroup>? acceptedTypeGroups,
+    String? initialDirectory,
+    String? confirmButtonText,
+  }) async {
+    final List<Map<String, Object>> serializedTypeGroups =
+        _serializeTypeGroups(acceptedTypeGroups);
+    final List<String>? path = await _channel.invokeListMethod<String>(
+      _openFileMethod,
+      <String, dynamic>{
+        if (serializedTypeGroups.isNotEmpty)
+          _acceptedTypeGroupsKey: serializedTypeGroups,
+        'initialDirectory': initialDirectory,
+        _confirmButtonTextKey: confirmButtonText,
+        _multipleKey: false,
+      },
+    );
+    return path == null ? null : XFile(path.first);
+  }
+
+  @override
+  Future<List<XFile>> openFiles({
+    List<XTypeGroup>? acceptedTypeGroups,
+    String? initialDirectory,
+    String? confirmButtonText,
+  }) async {
+    final List<Map<String, Object>> serializedTypeGroups =
+        _serializeTypeGroups(acceptedTypeGroups);
+    final List<String>? pathList = await _channel.invokeListMethod<String>(
+      _openFileMethod,
+      <String, dynamic>{
+        if (serializedTypeGroups.isNotEmpty)
+          _acceptedTypeGroupsKey: serializedTypeGroups,
+        _initialDirectoryKey: initialDirectory,
+        _confirmButtonTextKey: confirmButtonText,
+        _multipleKey: true,
+      },
+    );
+    return pathList?.map((String path) => XFile(path)).toList() ?? <XFile>[];
+  }
+
+  @override
+  Future<String?> getSavePath({
+    List<XTypeGroup>? acceptedTypeGroups,
+    String? initialDirectory,
+    String? suggestedName,
+    String? confirmButtonText,
+  }) async {
+    final List<Map<String, Object>> serializedTypeGroups =
+        _serializeTypeGroups(acceptedTypeGroups);
+    return _channel.invokeMethod<String>(
+      _getSavePathMethod,
+      <String, dynamic>{
+        if (serializedTypeGroups.isNotEmpty)
+          _acceptedTypeGroupsKey: serializedTypeGroups,
+        _initialDirectoryKey: initialDirectory,
+        _suggestedNameKey: suggestedName,
+        _confirmButtonTextKey: confirmButtonText,
+      },
+    );
+  }
+
+  @override
+  Future<String?> getDirectoryPath({
+    String? initialDirectory,
+    String? confirmButtonText,
+  }) async {
+    return _channel.invokeMethod<String>(
+      _getDirectoryPathMethod,
+      <String, dynamic>{
+        _initialDirectoryKey: initialDirectory,
+        _confirmButtonTextKey: confirmButtonText,
+      },
+    );
+  }
+}
+
+List<Map<String, Object>> _serializeTypeGroups(List<XTypeGroup>? groups) {
+  return (groups ?? <XTypeGroup>[]).map(_serializeTypeGroup).toList();
+}
+
+Map<String, Object> _serializeTypeGroup(XTypeGroup group) {
+  final Map<String, Object> serialization = <String, Object>{
+    _typeGroupLabelKey: group.label ?? '',
+  };
+  if (group.allowsAny) {
+    serialization[_typeGroupExtensionsKey] = <String>['*'];
+  } else {
+    if ((group.extensions?.isEmpty ?? true) &&
+        (group.mimeTypes?.isEmpty ?? true)) {
+      throw ArgumentError('Provided type group $group does not allow '
+          'all files, but does not set any of the Linux-supported filter '
+          'categories. "extensions" or "mimeTypes" must be non-empty for Linux '
+          'if anything is non-empty.');
+    }
+    if (group.extensions?.isNotEmpty ?? false) {
+      serialization[_typeGroupExtensionsKey] = group.extensions
+              ?.map((String extension) => '*.$extension')
+              .toList() ??
+          <String>[];
+    }
+    if (group.mimeTypes?.isNotEmpty ?? false) {
+      serialization[_typeGroupMimeTypesKey] = group.mimeTypes ?? <String>[];
+    }
+  }
+  return serialization;
+}
diff --git a/packages/file_selector/file_selector_linux/linux/CMakeLists.txt b/packages/file_selector/file_selector_linux/linux/CMakeLists.txt
new file mode 100644
index 0000000..d0316d9
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/linux/CMakeLists.txt
@@ -0,0 +1,67 @@
+cmake_minimum_required(VERSION 3.10)
+set(PROJECT_NAME "file_selector_linux")
+project(${PROJECT_NAME} LANGUAGES CXX)
+
+set(PLUGIN_NAME "${PROJECT_NAME}_plugin")
+
+list(APPEND PLUGIN_SOURCES
+  "file_selector_plugin.cc"
+)
+
+add_library(${PLUGIN_NAME} SHARED
+  "file_selector_plugin.cc"
+)
+apply_standard_settings(${PLUGIN_NAME})
+set_target_properties(${PLUGIN_NAME} PROPERTIES
+  CXX_VISIBILITY_PRESET hidden)
+target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
+target_include_directories(${PLUGIN_NAME} INTERFACE
+  "${CMAKE_CURRENT_SOURCE_DIR}/include")
+target_link_libraries(${PLUGIN_NAME} PRIVATE flutter)
+target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK)
+
+
+# === Tests ===
+
+if(${include_${PROJECT_NAME}_tests})
+if(${CMAKE_VERSION} VERSION_LESS "3.11.0")
+message("Unit tests require CMake 3.11.0 or later")
+else()
+set(TEST_RUNNER "${PROJECT_NAME}_test")
+enable_testing()
+# TODO(stuartmorgan): Consider using a single shared, pre-checked-in googletest
+# instance rather than downloading for each plugin. This approach makes sense
+# for a template, but not for a monorepo with many plugins.
+include(FetchContent)
+FetchContent_Declare(
+  googletest
+  URL https://github.com/google/googletest/archive/release-1.11.0.zip
+)
+# Prevent overriding the parent project's compiler/linker settings
+set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
+# Disable install commands for gtest so it doesn't end up in the bundle.
+set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE)
+
+FetchContent_MakeAvailable(googletest)
+
+# The plugin's exported API is not very useful for unit testing, so build the
+# sources directly into the test binary rather than using the shared library.
+add_executable(${TEST_RUNNER}
+  test/file_selector_plugin_test.cc
+  test/test_main.cc
+  ${PLUGIN_SOURCES}
+)
+apply_standard_settings(${TEST_RUNNER})
+target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
+target_link_libraries(${TEST_RUNNER} PRIVATE flutter)
+target_link_libraries(${TEST_RUNNER} PRIVATE PkgConfig::GTK)
+target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock)
+
+include(GoogleTest)
+# TODO(stuartmorgan): Switch back to gtest_discover_tests when moving to
+# flutter/plugins; it doesn't work in the FDE CI because it requires actually
+# running a GTK app, which it hasn't been set up for.
+gtest_add_tests(TARGET ${TEST_RUNNER})
+#gtest_discover_tests(${TEST_RUNNER})
+endif()  # CMake version check
+endif()  # include_${PROJECT_NAME}_tests
diff --git a/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc b/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc
new file mode 100644
index 0000000..8337719
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc
@@ -0,0 +1,246 @@
+// 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.
+
+#include "include/file_selector_linux/file_selector_plugin.h"
+
+#include <flutter_linux/flutter_linux.h>
+#include <gtk/gtk.h>
+
+#include "file_selector_plugin_private.h"
+
+// From file_selector_linux.dart
+const char kChannelName[] = "plugins.flutter.dev/file_selector_linux";
+
+const char kOpenFileMethod[] = "openFile";
+const char kGetSavePathMethod[] = "getSavePath";
+const char kGetDirectoryPathMethod[] = "getDirectoryPath";
+
+const char kAcceptedTypeGroupsKey[] = "acceptedTypeGroups";
+const char kConfirmButtonTextKey[] = "confirmButtonText";
+const char kInitialDirectoryKey[] = "initialDirectory";
+const char kMultipleKey[] = "multiple";
+const char kSuggestedNameKey[] = "suggestedName";
+
+const char kTypeGroupLabelKey[] = "label";
+const char kTypeGroupExtensionsKey[] = "extensions";
+const char kTypeGroupMimeTypesKey[] = "mimeTypes";
+
+// Errors
+const char kBadArgumentsError[] = "Bad Arguments";
+const char kNoScreenError[] = "No Screen";
+
+struct _FlFileSelectorPlugin {
+  GObject parent_instance;
+
+  FlPluginRegistrar* registrar;
+
+  // Connection to Flutter engine.
+  FlMethodChannel* channel;
+};
+
+G_DEFINE_TYPE(FlFileSelectorPlugin, fl_file_selector_plugin, G_TYPE_OBJECT)
+
+// Converts a type group received from Flutter into a GTK file filter.
+static GtkFileFilter* type_group_to_filter(FlValue* value) {
+  g_autoptr(GtkFileFilter) filter = gtk_file_filter_new();
+
+  FlValue* label = fl_value_lookup_string(value, kTypeGroupLabelKey);
+  if (label != nullptr && fl_value_get_type(label) == FL_VALUE_TYPE_STRING) {
+    gtk_file_filter_set_name(filter, fl_value_get_string(label));
+  }
+
+  FlValue* extensions = fl_value_lookup_string(value, kTypeGroupExtensionsKey);
+  if (extensions != nullptr &&
+      fl_value_get_type(extensions) == FL_VALUE_TYPE_LIST) {
+    for (size_t i = 0; i < fl_value_get_length(extensions); i++) {
+      FlValue* v = fl_value_get_list_value(extensions, i);
+      const gchar* pattern = fl_value_get_string(v);
+      gtk_file_filter_add_pattern(filter, pattern);
+    }
+  }
+  FlValue* mime_types = fl_value_lookup_string(value, kTypeGroupMimeTypesKey);
+  if (mime_types != nullptr &&
+      fl_value_get_type(mime_types) == FL_VALUE_TYPE_LIST) {
+    for (size_t i = 0; i < fl_value_get_length(mime_types); i++) {
+      FlValue* v = fl_value_get_list_value(mime_types, i);
+      const gchar* pattern = fl_value_get_string(v);
+      gtk_file_filter_add_mime_type(filter, pattern);
+    }
+  }
+
+  return GTK_FILE_FILTER(g_object_ref(filter));
+}
+
+// Creates a GtkFileChooserNative for the given method call details.
+static GtkFileChooserNative* create_dialog(
+    GtkWindow* window, GtkFileChooserAction action, const gchar* title,
+    const gchar* default_confirm_button_text, FlValue* properties) {
+  const gchar* confirm_button_text = default_confirm_button_text;
+  FlValue* value = fl_value_lookup_string(properties, kConfirmButtonTextKey);
+  if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_STRING)
+    confirm_button_text = fl_value_get_string(value);
+
+  g_autoptr(GtkFileChooserNative) dialog =
+      GTK_FILE_CHOOSER_NATIVE(gtk_file_chooser_native_new(
+          title, window, action, confirm_button_text, "_Cancel"));
+
+  value = fl_value_lookup_string(properties, kMultipleKey);
+  if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_BOOL) {
+    gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(dialog),
+                                         fl_value_get_bool(value));
+  }
+
+  value = fl_value_lookup_string(properties, kInitialDirectoryKey);
+  if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_STRING) {
+    gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),
+                                        fl_value_get_string(value));
+  }
+
+  value = fl_value_lookup_string(properties, kSuggestedNameKey);
+  if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_STRING) {
+    gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog),
+                                      fl_value_get_string(value));
+  }
+
+  value = fl_value_lookup_string(properties, kAcceptedTypeGroupsKey);
+  if (value != nullptr && fl_value_get_type(value) == FL_VALUE_TYPE_LIST) {
+    for (size_t i = 0; i < fl_value_get_length(value); i++) {
+      FlValue* type_group = fl_value_get_list_value(value, i);
+      GtkFileFilter* filter = type_group_to_filter(type_group);
+      if (filter == nullptr) {
+        return nullptr;
+      }
+      gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
+    }
+  }
+
+  return GTK_FILE_CHOOSER_NATIVE(g_object_ref(dialog));
+}
+
+// TODO(stuartmorgan): Move this logic back into method_call_cb once
+// https://github.com/flutter/flutter/issues/88724 is fixed, and test
+// through the public API instead. This only exists to move as much
+// logic as possible behind the private entry point used by unit tests.
+GtkFileChooserNative* create_dialog_for_method(GtkWindow* window,
+                                               const gchar* method,
+                                               FlValue* properties) {
+  if (strcmp(method, kOpenFileMethod) == 0) {
+    return create_dialog(window, GTK_FILE_CHOOSER_ACTION_OPEN, "Open File",
+                         "_Open", properties);
+  } else if (strcmp(method, kGetDirectoryPathMethod) == 0) {
+    return create_dialog(window, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
+                         "Choose Directory", "_Open", properties);
+  } else if (strcmp(method, kGetSavePathMethod) == 0) {
+    return create_dialog(window, GTK_FILE_CHOOSER_ACTION_SAVE, "Save File",
+                         "_Save", properties);
+  }
+  return nullptr;
+}
+
+// Shows the requested dialog type.
+static FlMethodResponse* show_dialog(FlFileSelectorPlugin* self,
+                                     const gchar* method, FlValue* properties,
+                                     bool return_list) {
+  if (fl_value_get_type(properties) != FL_VALUE_TYPE_MAP) {
+    return FL_METHOD_RESPONSE(fl_method_error_response_new(
+        kBadArgumentsError, "Argument map missing or malformed", nullptr));
+  }
+
+  FlView* view = fl_plugin_registrar_get_view(self->registrar);
+  if (view == nullptr) {
+    return FL_METHOD_RESPONSE(
+        fl_method_error_response_new(kNoScreenError, nullptr, nullptr));
+  }
+  GtkWindow* window = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(view)));
+
+  g_autoptr(GtkFileChooserNative) dialog =
+      create_dialog_for_method(window, method, properties);
+
+  if (dialog == nullptr) {
+    return FL_METHOD_RESPONSE(fl_method_error_response_new(
+        kBadArgumentsError, "Unable to create dialog from arguments", nullptr));
+  }
+
+  gint response = gtk_native_dialog_run(GTK_NATIVE_DIALOG(dialog));
+  g_autoptr(FlValue) result = nullptr;
+  if (response == GTK_RESPONSE_ACCEPT) {
+    if (return_list) {
+      result = fl_value_new_list();
+      g_autoptr(GSList) filenames =
+          gtk_file_chooser_get_filenames(GTK_FILE_CHOOSER(dialog));
+      for (GSList* link = filenames; link != nullptr; link = link->next) {
+        g_autofree gchar* filename = static_cast<gchar*>(link->data);
+        fl_value_append_take(result, fl_value_new_string(filename));
+      }
+    } else {
+      g_autofree gchar* filename =
+          gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
+      result = fl_value_new_string(filename);
+    }
+  }
+
+  return FL_METHOD_RESPONSE(fl_method_success_response_new(result));
+}
+
+// Called when a method call is received from Flutter.
+static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call,
+                           gpointer user_data) {
+  FlFileSelectorPlugin* self = FL_FILE_SELECTOR_PLUGIN(user_data);
+
+  const gchar* method = fl_method_call_get_name(method_call);
+  FlValue* args = fl_method_call_get_args(method_call);
+
+  g_autoptr(FlMethodResponse) response = nullptr;
+  if (strcmp(method, kOpenFileMethod) == 0) {
+    response = show_dialog(self, method, args, true);
+  } else if (strcmp(method, kGetDirectoryPathMethod) == 0 ||
+             strcmp(method, kGetSavePathMethod) == 0) {
+    response = show_dialog(self, method, args, false);
+  } else {
+    response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
+  }
+
+  g_autoptr(GError) error = nullptr;
+  if (!fl_method_call_respond(method_call, response, &error))
+    g_warning("Failed to send method call response: %s", error->message);
+}
+
+static void fl_file_selector_plugin_dispose(GObject* object) {
+  FlFileSelectorPlugin* self = FL_FILE_SELECTOR_PLUGIN(object);
+
+  g_clear_object(&self->registrar);
+  g_clear_object(&self->channel);
+
+  G_OBJECT_CLASS(fl_file_selector_plugin_parent_class)->dispose(object);
+}
+
+static void fl_file_selector_plugin_class_init(
+    FlFileSelectorPluginClass* klass) {
+  G_OBJECT_CLASS(klass)->dispose = fl_file_selector_plugin_dispose;
+}
+
+static void fl_file_selector_plugin_init(FlFileSelectorPlugin* self) {}
+
+FlFileSelectorPlugin* fl_file_selector_plugin_new(
+    FlPluginRegistrar* registrar) {
+  FlFileSelectorPlugin* self = FL_FILE_SELECTOR_PLUGIN(
+      g_object_new(fl_file_selector_plugin_get_type(), nullptr));
+
+  self->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar));
+
+  g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
+  self->channel =
+      fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar),
+                            kChannelName, FL_METHOD_CODEC(codec));
+  fl_method_channel_set_method_call_handler(self->channel, method_call_cb,
+                                            g_object_ref(self), g_object_unref);
+
+  return self;
+}
+
+void file_selector_plugin_register_with_registrar(
+    FlPluginRegistrar* registrar) {
+  FlFileSelectorPlugin* plugin = fl_file_selector_plugin_new(registrar);
+  g_object_unref(plugin);
+}
diff --git a/packages/file_selector/file_selector_linux/linux/file_selector_plugin_private.h b/packages/file_selector/file_selector_linux/linux/file_selector_plugin_private.h
new file mode 100644
index 0000000..e58a78c
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/linux/file_selector_plugin_private.h
@@ -0,0 +1,12 @@
+// 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.
+
+#include <flutter_linux/flutter_linux.h>
+
+#include "include/file_selector_linux/file_selector_plugin.h"
+
+// Creates a GtkFileChooserNative for the given method call.
+GtkFileChooserNative* create_dialog_for_method(GtkWindow* window,
+                                               const gchar* method,
+                                               FlValue* properties);
diff --git a/packages/file_selector/file_selector_linux/linux/include/file_selector_linux/file_selector_plugin.h b/packages/file_selector/file_selector_linux/linux/include/file_selector_linux/file_selector_plugin.h
new file mode 100644
index 0000000..98e90e5
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/linux/include/file_selector_linux/file_selector_plugin.h
@@ -0,0 +1,31 @@
+// 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.
+
+#ifndef PLUGINS_FILE_SELECTOR_LINUX_FILE_SELECTOR_PLUGIN_H_
+#define PLUGINS_FILE_SELECTOR_LINUX_FILE_SELECTOR_PLUGIN_H_
+
+// A plugin to show native save/open file choosers.
+
+#include <flutter_linux/flutter_linux.h>
+
+G_BEGIN_DECLS
+
+#ifdef FLUTTER_PLUGIN_IMPL
+#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default")))
+#else
+#define FLUTTER_PLUGIN_EXPORT
+#endif
+
+G_DECLARE_FINAL_TYPE(FlFileSelectorPlugin, fl_file_selector_plugin, FL,
+                     FILE_SELECTOR_PLUGIN, GObject)
+
+FLUTTER_PLUGIN_EXPORT FlFileSelectorPlugin* fl_file_selector_plugin_new(
+    FlPluginRegistrar* registrar);
+
+FLUTTER_PLUGIN_EXPORT void file_selector_plugin_register_with_registrar(
+    FlPluginRegistrar* registrar);
+
+G_END_DECLS
+
+#endif  // PLUGINS_FILE_SELECTOR_LINUX_FILE_SELECTOR_PLUGIN_H_
diff --git a/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc b/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc
new file mode 100644
index 0000000..84c55ac
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc
@@ -0,0 +1,171 @@
+// 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.
+
+#include "include/file_selector_linux/file_selector_plugin.h"
+
+#include <flutter_linux/flutter_linux.h>
+#include <gtest/gtest.h>
+#include <gtk/gtk.h>
+
+#include "file_selector_plugin_private.h"
+
+// TODO(stuartmorgan): Restructure the helper to take a callback for showing
+// the dialog, so that the tests can mock out that callback with something
+// that changes the selection so that the return value path can be tested
+// as well.
+// TODO(stuartmorgan): Add an injectable wrapper around
+// gtk_file_chooser_native_new to allow for testing values that are given as
+// construction paramaters and can't be queried later.
+
+TEST(FileSelectorPlugin, TestOpenSimple) {
+  g_autoptr(FlValue) args = fl_value_new_map();
+
+  g_autoptr(GtkFileChooserNative) dialog =
+      create_dialog_for_method(nullptr, "openFile", args);
+
+  ASSERT_NE(dialog, nullptr);
+  EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)),
+            GTK_FILE_CHOOSER_ACTION_OPEN);
+  EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)),
+            false);
+}
+
+TEST(FileSelectorPlugin, TestOpenMultiple) {
+  g_autoptr(FlValue) args = fl_value_new_map();
+  fl_value_set_string_take(args, "multiple", fl_value_new_bool(true));
+
+  g_autoptr(GtkFileChooserNative) dialog =
+      create_dialog_for_method(nullptr, "openFile", args);
+
+  ASSERT_NE(dialog, nullptr);
+  EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)),
+            GTK_FILE_CHOOSER_ACTION_OPEN);
+  EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)),
+            true);
+}
+
+TEST(FileSelectorPlugin, TestOpenWithFilter) {
+  g_autoptr(FlValue) type_groups = fl_value_new_list();
+
+  {
+    g_autoptr(FlValue) text_group_mime_types = fl_value_new_list();
+    fl_value_append_take(text_group_mime_types,
+                         fl_value_new_string("text/plain"));
+    g_autoptr(FlValue) text_group = fl_value_new_map();
+    fl_value_set_string_take(text_group, "label", fl_value_new_string("Text"));
+    fl_value_set_string(text_group, "mimeTypes", text_group_mime_types);
+    fl_value_append(type_groups, text_group);
+  }
+
+  {
+    g_autoptr(FlValue) image_group_extensions = fl_value_new_list();
+    fl_value_append_take(image_group_extensions, fl_value_new_string("*.png"));
+    fl_value_append_take(image_group_extensions, fl_value_new_string("*.gif"));
+    fl_value_append_take(image_group_extensions,
+                         fl_value_new_string("*.jgpeg"));
+    g_autoptr(FlValue) image_group = fl_value_new_map();
+    fl_value_set_string_take(image_group, "label",
+                             fl_value_new_string("Images"));
+    fl_value_set_string(image_group, "extensions", image_group_extensions);
+    fl_value_append(type_groups, image_group);
+  }
+
+  {
+    g_autoptr(FlValue) any_group_extensions = fl_value_new_list();
+    fl_value_append_take(any_group_extensions, fl_value_new_string("*"));
+    g_autoptr(FlValue) any_group = fl_value_new_map();
+    fl_value_set_string_take(any_group, "label", fl_value_new_string("Any"));
+    fl_value_set_string(any_group, "extensions", any_group_extensions);
+    fl_value_append(type_groups, any_group);
+  }
+
+  g_autoptr(FlValue) args = fl_value_new_map();
+  fl_value_set_string(args, "acceptedTypeGroups", type_groups);
+
+  g_autoptr(GtkFileChooserNative) dialog =
+      create_dialog_for_method(nullptr, "openFile", args);
+
+  ASSERT_NE(dialog, nullptr);
+  EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)),
+            GTK_FILE_CHOOSER_ACTION_OPEN);
+  EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)),
+            false);
+  // Validate filters.
+  g_autoptr(GSList) type_group_list =
+      gtk_file_chooser_list_filters(GTK_FILE_CHOOSER(dialog));
+  EXPECT_EQ(g_slist_length(type_group_list), 3);
+  GtkFileFilter* text_filter =
+      GTK_FILE_FILTER(g_slist_nth_data(type_group_list, 0));
+  GtkFileFilter* image_filter =
+      GTK_FILE_FILTER(g_slist_nth_data(type_group_list, 1));
+  GtkFileFilter* any_filter =
+      GTK_FILE_FILTER(g_slist_nth_data(type_group_list, 2));
+  // Filters can't be inspected, so query them to see that they match expected
+  // filter behavior.
+  GtkFileFilterInfo text_file_info = {};
+  text_file_info.contains = static_cast<GtkFileFilterFlags>(
+      GTK_FILE_FILTER_DISPLAY_NAME | GTK_FILE_FILTER_MIME_TYPE);
+  text_file_info.display_name = "foo.txt";
+  text_file_info.mime_type = "text/plain";
+  GtkFileFilterInfo image_file_info = {};
+  image_file_info.contains = static_cast<GtkFileFilterFlags>(
+      GTK_FILE_FILTER_DISPLAY_NAME | GTK_FILE_FILTER_MIME_TYPE);
+  image_file_info.display_name = "foo.png";
+  image_file_info.mime_type = "image/png";
+  EXPECT_TRUE(gtk_file_filter_filter(text_filter, &text_file_info));
+  EXPECT_FALSE(gtk_file_filter_filter(text_filter, &image_file_info));
+  EXPECT_FALSE(gtk_file_filter_filter(image_filter, &text_file_info));
+  EXPECT_TRUE(gtk_file_filter_filter(image_filter, &image_file_info));
+  EXPECT_TRUE(gtk_file_filter_filter(any_filter, &image_file_info));
+  EXPECT_TRUE(gtk_file_filter_filter(any_filter, &text_file_info));
+}
+
+TEST(FileSelectorPlugin, TestSaveSimple) {
+  g_autoptr(FlValue) args = fl_value_new_map();
+
+  g_autoptr(GtkFileChooserNative) dialog =
+      create_dialog_for_method(nullptr, "getSavePath", args);
+
+  ASSERT_NE(dialog, nullptr);
+  EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)),
+            GTK_FILE_CHOOSER_ACTION_SAVE);
+  EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)),
+            false);
+}
+
+TEST(FileSelectorPlugin, TestSaveWithArguments) {
+  g_autoptr(FlValue) args = fl_value_new_map();
+  fl_value_set_string_take(args, "initialDirectory",
+                           fl_value_new_string("/tmp"));
+  fl_value_set_string_take(args, "suggestedName",
+                           fl_value_new_string("foo.txt"));
+
+  g_autoptr(GtkFileChooserNative) dialog =
+      create_dialog_for_method(nullptr, "getSavePath", args);
+
+  ASSERT_NE(dialog, nullptr);
+  EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)),
+            GTK_FILE_CHOOSER_ACTION_SAVE);
+  EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)),
+            false);
+  g_autofree gchar* current_name =
+      gtk_file_chooser_get_current_name(GTK_FILE_CHOOSER(dialog));
+  EXPECT_STREQ(current_name, "foo.txt");
+  // TODO(stuartmorgan): gtk_file_chooser_get_current_folder doesn't seem to
+  // return a value set by gtk_file_chooser_set_current_folder, or at least
+  // doesn't in a test context, so that's not currently validated.
+}
+
+TEST(FileSelectorPlugin, TestGetDirectory) {
+  g_autoptr(FlValue) args = fl_value_new_map();
+
+  g_autoptr(GtkFileChooserNative) dialog =
+      create_dialog_for_method(nullptr, "getDirectoryPath", args);
+
+  ASSERT_NE(dialog, nullptr);
+  EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)),
+            GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER);
+  EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)),
+            false);
+}
diff --git a/packages/file_selector/file_selector_linux/linux/test/test_main.cc b/packages/file_selector/file_selector_linux/linux/test/test_main.cc
new file mode 100644
index 0000000..7e33b21
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/linux/test/test_main.cc
@@ -0,0 +1,15 @@
+// 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.
+
+#include <gtest/gtest.h>
+#include <gtk/gtk.h>
+
+int main(int argc, char** argv) {
+  gtk_init(0, nullptr);
+
+  testing::InitGoogleTest(&argc, argv);
+  int exit_code = RUN_ALL_TESTS();
+
+  return exit_code;
+}
diff --git a/packages/file_selector/file_selector_linux/pubspec.yaml b/packages/file_selector/file_selector_linux/pubspec.yaml
new file mode 100644
index 0000000..369fd44
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/pubspec.yaml
@@ -0,0 +1,27 @@
+name: file_selector_linux
+description: Liunx implementation of the file_selector plugin.
+repository: https://github.com/flutter/plugins/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.0
+
+environment:
+  sdk: ">=2.12.0 <3.0.0"
+  flutter: ">=2.8.0"
+
+flutter:
+  plugin:
+    implements: file_selector
+    platforms:
+      linux:
+        pluginClass: FileSelectorPlugin
+        dartPluginClass: FileSelectorLinux
+
+dependencies:
+  cross_file: ^0.3.1
+  file_selector_platform_interface: ^2.1.0
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  flutter_test:
+    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
new file mode 100644
index 0000000..67b9929
--- /dev/null
+++ b/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart
@@ -0,0 +1,389 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:file_selector_linux/file_selector_linux.dart';
+import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  late FileSelectorLinux plugin;
+  late List<MethodCall> log;
+
+  setUp(() {
+    plugin = FileSelectorLinux();
+    log = <MethodCall>[];
+    plugin.channel.setMockMethodCallHandler((MethodCall methodCall) async {
+      log.add(methodCall);
+      return null;
+    });
+  });
+
+  test('registers instance', () {
+    FileSelectorLinux.registerWith();
+    expect(FileSelectorPlatform.instance, isA<FileSelectorLinux>());
+  });
+
+  group('#openFile', () {
+    test('passes the accepted type groups correctly', () async {
+      final XTypeGroup group = XTypeGroup(
+        label: 'text',
+        extensions: <String>['txt'],
+        mimeTypes: <String>['text/plain'],
+        macUTIs: <String>['public.text'],
+      );
+
+      final XTypeGroup groupTwo = XTypeGroup(
+        label: 'image',
+        extensions: <String>['jpg'],
+        mimeTypes: <String>['image/jpg'],
+        macUTIs: <String>['public.image'],
+        webWildCards: <String>['image/*'],
+      );
+
+      await plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group, groupTwo]);
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('openFile', 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,
+            'confirmButtonText': null,
+            'multiple': false,
+          }),
+        ],
+      );
+    });
+
+    test('passes initialDirectory correctly', () async {
+      await plugin.openFile(initialDirectory: '/example/directory');
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('openFile', arguments: <String, dynamic>{
+            'initialDirectory': '/example/directory',
+            'confirmButtonText': null,
+            'multiple': false,
+          }),
+        ],
+      );
+    });
+
+    test('passes confirmButtonText correctly', () async {
+      await plugin.openFile(confirmButtonText: 'Open File');
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('openFile', arguments: <String, dynamic>{
+            'initialDirectory': null,
+            'confirmButtonText': 'Open File',
+            'multiple': false,
+          }),
+        ],
+      );
+    });
+
+    test('throws for a type group that does not support Linux', () async {
+      final XTypeGroup group = XTypeGroup(
+        label: 'images',
+        webWildCards: <String>['images/*'],
+      );
+
+      await expectLater(
+          plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]),
+          throwsArgumentError);
+    });
+
+    test('passes a wildcard group correctly', () async {
+      final XTypeGroup group = XTypeGroup(
+        label: 'any',
+      );
+
+      await plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]);
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('openFile', arguments: <String, dynamic>{
+            'acceptedTypeGroups': <Map<String, dynamic>>[
+              <String, Object>{
+                'label': 'any',
+                'extensions': <String>['*'],
+              },
+            ],
+            'initialDirectory': null,
+            'confirmButtonText': null,
+            'multiple': false,
+          }),
+        ],
+      );
+    });
+  });
+
+  group('#openFiles', () {
+    test('passes the accepted type groups correctly', () async {
+      final XTypeGroup group = XTypeGroup(
+        label: 'text',
+        extensions: <String>['txt'],
+        mimeTypes: <String>['text/plain'],
+        macUTIs: <String>['public.text'],
+      );
+
+      final XTypeGroup groupTwo = XTypeGroup(
+        label: 'image',
+        extensions: <String>['jpg'],
+        mimeTypes: <String>['image/jpg'],
+        macUTIs: <String>['public.image'],
+        webWildCards: <String>['image/*'],
+      );
+
+      await plugin.openFiles(acceptedTypeGroups: <XTypeGroup>[group, groupTwo]);
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('openFile', 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,
+            'confirmButtonText': null,
+            'multiple': true,
+          }),
+        ],
+      );
+    });
+
+    test('passes initialDirectory correctly', () async {
+      await plugin.openFiles(initialDirectory: '/example/directory');
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('openFile', arguments: <String, dynamic>{
+            'initialDirectory': '/example/directory',
+            'confirmButtonText': null,
+            'multiple': true,
+          }),
+        ],
+      );
+    });
+
+    test('passes confirmButtonText correctly', () async {
+      await plugin.openFiles(confirmButtonText: 'Open File');
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('openFile', arguments: <String, dynamic>{
+            'initialDirectory': null,
+            'confirmButtonText': 'Open File',
+            'multiple': true,
+          }),
+        ],
+      );
+    });
+
+    test('throws for a type group that does not support Linux', () async {
+      final XTypeGroup group = XTypeGroup(
+        label: 'images',
+        webWildCards: <String>['images/*'],
+      );
+
+      await expectLater(
+          plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]),
+          throwsArgumentError);
+    });
+
+    test('passes a wildcard group correctly', () async {
+      final XTypeGroup group = XTypeGroup(
+        label: 'any',
+      );
+
+      await plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]);
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('openFile', arguments: <String, dynamic>{
+            'acceptedTypeGroups': <Map<String, dynamic>>[
+              <String, Object>{
+                'label': 'any',
+                'extensions': <String>['*'],
+              },
+            ],
+            'initialDirectory': null,
+            'confirmButtonText': null,
+            'multiple': false,
+          }),
+        ],
+      );
+    });
+  });
+
+  group('#getSavePath', () {
+    test('passes the accepted type groups correctly', () async {
+      final XTypeGroup group = XTypeGroup(
+        label: 'text',
+        extensions: <String>['txt'],
+        mimeTypes: <String>['text/plain'],
+        macUTIs: <String>['public.text'],
+      );
+
+      final XTypeGroup groupTwo = XTypeGroup(
+        label: 'image',
+        extensions: <String>['jpg'],
+        mimeTypes: <String>['image/jpg'],
+        macUTIs: <String>['public.image'],
+        webWildCards: <String>['image/*'],
+      );
+
+      await plugin
+          .getSavePath(acceptedTypeGroups: <XTypeGroup>[group, groupTwo]);
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('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.getSavePath(initialDirectory: '/example/directory');
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('getSavePath', arguments: <String, dynamic>{
+            'initialDirectory': '/example/directory',
+            'suggestedName': null,
+            'confirmButtonText': null,
+          }),
+        ],
+      );
+    });
+
+    test('passes confirmButtonText correctly', () async {
+      await plugin.getSavePath(confirmButtonText: 'Open File');
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('getSavePath', arguments: <String, dynamic>{
+            'initialDirectory': null,
+            'suggestedName': null,
+            'confirmButtonText': 'Open File',
+          }),
+        ],
+      );
+    });
+
+    test('throws for a type group that does not support Linux', () async {
+      final XTypeGroup group = XTypeGroup(
+        label: 'images',
+        webWildCards: <String>['images/*'],
+      );
+
+      await expectLater(
+          plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]),
+          throwsArgumentError);
+    });
+
+    test('passes a wildcard group correctly', () async {
+      final XTypeGroup group = XTypeGroup(
+        label: 'any',
+      );
+
+      await plugin.openFile(acceptedTypeGroups: <XTypeGroup>[group]);
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('openFile', arguments: <String, dynamic>{
+            'acceptedTypeGroups': <Map<String, dynamic>>[
+              <String, Object>{
+                'label': 'any',
+                'extensions': <String>['*'],
+              },
+            ],
+            'initialDirectory': null,
+            'confirmButtonText': null,
+            'multiple': false,
+          }),
+        ],
+      );
+    });
+  });
+
+  group('#getDirectoryPath', () {
+    test('passes initialDirectory correctly', () async {
+      await plugin.getDirectoryPath(initialDirectory: '/example/directory');
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('getDirectoryPath', arguments: <String, dynamic>{
+            'initialDirectory': '/example/directory',
+            'confirmButtonText': null,
+          }),
+        ],
+      );
+    });
+    test('passes confirmButtonText correctly', () async {
+      await plugin.getDirectoryPath(confirmButtonText: 'Open File');
+
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('getDirectoryPath', arguments: <String, dynamic>{
+            'initialDirectory': null,
+            'confirmButtonText': 'Open File',
+          }),
+        ],
+      );
+    });
+  });
+}
diff --git a/packages/file_selector/file_selector_macos/CHANGELOG.md b/packages/file_selector/file_selector_macos/CHANGELOG.md
index 0adf1ab..499667d 100644
--- a/packages/file_selector/file_selector_macos/CHANGELOG.md
+++ b/packages/file_selector/file_selector_macos/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.9.0+1
+
+* Updates README for endorsement.
+* Updates `flutter_test` to be a `dev_dependencies` entry.
+
 ## 0.9.0
 
 * **BREAKING CHANGE**: Methods that take `XTypeGroup`s now throw an
diff --git a/packages/file_selector/file_selector_macos/README.md b/packages/file_selector/file_selector_macos/README.md
index 3241b21..10a5636 100644
--- a/packages/file_selector/file_selector_macos/README.md
+++ b/packages/file_selector/file_selector_macos/README.md
@@ -4,19 +4,12 @@
 
 ## Usage
 
-### Importing the package
-
-This implementation has not yet been endorsed, meaning that you need to
-[depend on `file_selector_macos`][2] in addition to
-[depending on `file_selector`][3].
-
-Once your pubspec includes the macOS implementation, you can use the
-`file_selector` APIs normally. You should not use the `file_selector_macos`
-APIs directly.
+This package is [endorsed][2], which means you can simply use `file_selector`
+normally. This package will be automatically included in your app when you do.
 
 ### Entitlements
 
-You will need to [add an entitlement][4] for either read-only access:
+You will need to [add an entitlement][3] for either read-only access:
 ```xml
 	<key>com.apple.security.files.user-selected.read-only</key>
 	<true/>
@@ -29,6 +22,5 @@
 depending on your use case.
 
 [1]: https://pub.dev/packages/file_selector
-[2]: https://pub.dev/packages/file_selector_macos/install
-[3]: https://pub.dev/packages/file_selector/install
-[4]: https://flutter.dev/desktop#entitlements-and-the-app-sandbox
+[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin
+[3]: https://flutter.dev/desktop#entitlements-and-the-app-sandbox
diff --git a/packages/file_selector/file_selector_macos/pubspec.yaml b/packages/file_selector/file_selector_macos/pubspec.yaml
index fc9ca4d..44b842e 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/plugins/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.0
+version: 0.9.0+1
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
@@ -21,5 +21,7 @@
   file_selector_platform_interface: ^2.1.0
   flutter:
     sdk: flutter
+
+dev_dependencies:
   flutter_test:
     sdk: flutter
diff --git a/packages/file_selector/file_selector_windows/CHANGELOG.md b/packages/file_selector/file_selector_windows/CHANGELOG.md
index b0e6f0d..6db4fb3 100644
--- a/packages/file_selector/file_selector_windows/CHANGELOG.md
+++ b/packages/file_selector/file_selector_windows/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.9.1+1
+
+* Updates README for endorsement.
+* Updates `flutter_test` to be a `dev_dependencies` entry.
+
 ## 0.9.1
 
 * Converts the method channel to Pigeon.
diff --git a/packages/file_selector/file_selector_windows/README.md b/packages/file_selector/file_selector_windows/README.md
index 69fb088..c597d70 100644
--- a/packages/file_selector/file_selector_windows/README.md
+++ b/packages/file_selector/file_selector_windows/README.md
@@ -4,16 +4,8 @@
 
 ## Usage
 
-### Importing the package
-
-This implementation has not yet been endorsed, meaning that you need to
-[depend on `file_selector_windows`][2] in addition to
-[depending on `file_selector`][3].
-
-Once your pubspec includes the Windows implementation, you can use the
-`file_selector` APIs normally. You should not use the `file_selector_windows`
-APIs directly.
+This package is [endorsed][2], which means you can simply use `file_selector`
+normally. This package will be automatically included in your app when you do.
 
 [1]: https://pub.dev/packages/file_selector
-[2]: https://pub.dev/packages/file_selector_windows/install
-[3]: https://pub.dev/packages/file_selector/install
+[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin
diff --git a/packages/file_selector/file_selector_windows/pubspec.yaml b/packages/file_selector/file_selector_windows/pubspec.yaml
index 6e39838..e4f38d8 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/plugins/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.1
+version: 0.9.1+1
 
 environment:
   sdk: ">=2.12.0 <3.0.0"
@@ -21,10 +21,10 @@
   file_selector_platform_interface: ^2.1.0
   flutter:
     sdk: flutter
-  flutter_test:
-    sdk: flutter
 
 dev_dependencies:
   build_runner: 2.1.11
+  flutter_test:
+    sdk: flutter
   mockito: ^5.1.0
   pigeon: ^3.2.5
diff --git a/script/configs/exclude_integration_linux.yaml b/script/configs/exclude_integration_linux.yaml
new file mode 100644
index 0000000..a83550e
--- /dev/null
+++ b/script/configs/exclude_integration_linux.yaml
@@ -0,0 +1,3 @@
+# Can't use Flutter integration tests due to native modal UI.
+- file_selector
+- file_selector_linux