[file_selector] Add MIME type support on macOS (#3862)

Adds a macOS 11+ codepath that uses `UTType`s, allowing for supporting MIME type (and moving off of the deprecated `allowedFileTypes`).

Fixes https://github.com/flutter/flutter/issues/117843
diff --git a/packages/file_selector/file_selector_macos/CHANGELOG.md b/packages/file_selector/file_selector_macos/CHANGELOG.md
index 2e9d4dc..acd657b 100644
--- a/packages/file_selector/file_selector_macos/CHANGELOG.md
+++ b/packages/file_selector/file_selector_macos/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.9.2
+
+* Adds support for MIME types on macOS 11+.
+
 ## 0.9.1+1
 
 * Updates references to the deprecated `macUTIs`.
diff --git a/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift b/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift
index 4e10343..a77f421 100644
--- a/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift
+++ b/packages/file_selector/file_selector_macos/example/macos/RunnerTests/RunnerTests.swift
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 import FlutterMacOS
+import UniformTypeIdentifiers
 import XCTest
 
 @testable import file_selector_macos
@@ -160,7 +161,7 @@
       baseOptions: SavePanelOptions(
         allowedFileTypes: AllowedTypes(
           extensions: ["txt", "json"],
-          mimeTypes: [],
+          mimeTypes: ["text/html"],
           utis: ["public.text", "public.image"])))
     plugin.displayOpenPanel(options: options) { result in
       switch result {
@@ -175,7 +176,62 @@
     wait(for: [called], timeout: 0.5)
     XCTAssertNotNil(panelController.openPanel)
     if let panel = panelController.openPanel {
+      if #available(macOS 11.0, *) {
+        XCTAssertTrue(panel.allowedContentTypes.contains(UTType.plainText))
+        XCTAssertTrue(panel.allowedContentTypes.contains(UTType.json))
+        XCTAssertTrue(panel.allowedContentTypes.contains(UTType.html))
+        XCTAssertTrue(panel.allowedContentTypes.contains(UTType.image))
+      } else {
+        // MIME type is not supported for the legacy codepath, but the rest should be set.
+        XCTAssertEqual(panel.allowedFileTypes, ["txt", "json", "public.text", "public.image"])
+      }
+    }
+  }
+
+  func testOpenWithFilterLegacy() throws {
+    let panelController = TestPanelController()
+    let plugin = FileSelectorPlugin(
+      viewProvider: TestViewProvider(),
+      panelController: panelController)
+    plugin.forceLegacyTypes = true
+
+    let returnPath = "/foo/bar"
+    panelController.openURLs = [URL(fileURLWithPath: returnPath)]
+
+    let called = XCTestExpectation()
+    let options = OpenPanelOptions(
+      allowsMultipleSelection: true,
+      canChooseDirectories: false,
+      canChooseFiles: true,
+      baseOptions: SavePanelOptions(
+        allowedFileTypes: AllowedTypes(
+          extensions: ["txt", "json"],
+          mimeTypes: ["text/html"],
+          utis: ["public.text", "public.image"])))
+    plugin.displayOpenPanel(options: options) { result in
+      switch result {
+      case .success(let paths):
+        XCTAssertEqual(paths[0], returnPath)
+      case .failure(let error):
+        XCTFail("\(error)")
+      }
+      called.fulfill()
+    }
+
+    wait(for: [called], timeout: 0.5)
+    XCTAssertNotNil(panelController.openPanel)
+    if let panel = panelController.openPanel {
+      // On the legacy path, the allowedFileTypes should be set directly.
       XCTAssertEqual(panel.allowedFileTypes, ["txt", "json", "public.text", "public.image"])
+
+      // They should also be translated to corresponding allowed content types.
+      if #available(macOS 11.0, *) {
+        XCTAssertTrue(panel.allowedContentTypes.contains(UTType.plainText))
+        XCTAssertTrue(panel.allowedContentTypes.contains(UTType.json))
+        XCTAssertTrue(panel.allowedContentTypes.contains(UTType.image))
+        // MIME type is not supported for the legacy codepath.
+        XCTAssertFalse(panel.allowedContentTypes.contains(UTType.html))
+      }
     }
   }
 
diff --git a/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift b/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift
index 836fcf9..8310332 100644
--- a/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift
+++ b/packages/file_selector/file_selector_macos/macos/Classes/FileSelectorPlugin.swift
@@ -2,8 +2,9 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import Cocoa
 import FlutterMacOS
-import Foundation
+import UniformTypeIdentifiers
 
 /// Protocol for showing panels, allowing for depenedency injection in tests.
 protocol PanelController {
@@ -48,6 +49,8 @@
   private let openDirectoryMethod = "getDirectoryPath"
   private let saveMethod = "getSavePath"
 
+  var forceLegacyTypes = false
+
   public static func register(with registrar: FlutterPluginRegistrar) {
     let instance = FileSelectorPlugin(
       viewProvider: DefaultViewProvider(registrar: registrar),
@@ -96,16 +99,31 @@
     }
 
     if let acceptedTypes = options.allowedFileTypes {
-      var allowedTypes: [String] = []
-      // The array values are non-null by convention even though Pigeon can't currently express
-      // that via the types; see messages.dart.
-      allowedTypes.append(contentsOf: acceptedTypes.extensions.map({ $0! }))
-      allowedTypes.append(contentsOf: acceptedTypes.utis.map({ $0! }))
-      // TODO: Add support for mimeTypes in macOS 11+. See
-      // https://github.com/flutter/flutter/issues/117843
-
-      if !allowedTypes.isEmpty {
-        panel.allowedFileTypes = allowedTypes
+      if #available(macOS 11, *), !forceLegacyTypes {
+        var allowedTypes: [UTType] = []
+        // The array values are non-null by convention even though Pigeon can't currently express
+        // that via the types; see messages.dart and https://github.com/flutter/flutter/issues/97848
+        allowedTypes.append(contentsOf: acceptedTypes.utis.compactMap({ UTType($0!) }))
+        allowedTypes.append(
+          contentsOf: acceptedTypes.extensions.flatMap({
+            UTType.types(tag: $0!, tagClass: UTTagClass.filenameExtension, conformingTo: nil)
+          }))
+        allowedTypes.append(
+          contentsOf: acceptedTypes.mimeTypes.flatMap({
+            UTType.types(tag: $0!, tagClass: UTTagClass.mimeType, conformingTo: nil)
+          }))
+        if !allowedTypes.isEmpty {
+          panel.allowedContentTypes = allowedTypes
+        }
+      } else {
+        var allowedTypes: [String] = []
+        // The array values are non-null by convention even though Pigeon can't currently express
+        // that via the types; see messages.dart and https://github.com/flutter/flutter/issues/97848
+        allowedTypes.append(contentsOf: acceptedTypes.extensions.map({ $0! }))
+        allowedTypes.append(contentsOf: acceptedTypes.utis.map({ $0! }))
+        if !allowedTypes.isEmpty {
+          panel.allowedFileTypes = allowedTypes
+        }
       }
     }
   }
diff --git a/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift b/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift
index 828c499..67007e1 100644
--- a/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift
+++ b/packages/file_selector/file_selector_macos/macos/Classes/messages.g.swift
@@ -5,12 +5,13 @@
 // See also: https://pub.dev/packages/pigeon
 
 import Foundation
+
 #if os(iOS)
-import Flutter
+  import Flutter
 #elseif os(macOS)
-import FlutterMacOS
+  import FlutterMacOS
 #else
-#error("Unsupported platform.")
+  #error("Unsupported platform.")
 #endif
 
 private func wrapResult(_ result: Any?) -> [Any?] {
@@ -22,13 +23,13 @@
     return [
       flutterError.code,
       flutterError.message,
-      flutterError.details
+      flutterError.details,
     ]
   }
   return [
     "\(error)",
     "\(type(of: error))",
-    "Stacktrace: \(Thread.callStackSymbols)"
+    "Stacktrace: \(Thread.callStackSymbols)",
   ]
 }
 
@@ -140,14 +141,14 @@
 private class FileSelectorApiCodecReader: FlutterStandardReader {
   override func readValue(ofType type: UInt8) -> Any? {
     switch type {
-      case 128:
-        return AllowedTypes.fromList(self.readValue() as! [Any])
-      case 129:
-        return OpenPanelOptions.fromList(self.readValue() as! [Any])
-      case 130:
-        return SavePanelOptions.fromList(self.readValue() as! [Any])
-      default:
-        return super.readValue(ofType: type)
+    case 128:
+      return AllowedTypes.fromList(self.readValue() as! [Any])
+    case 129:
+      return OpenPanelOptions.fromList(self.readValue() as! [Any])
+    case 130:
+      return SavePanelOptions.fromList(self.readValue() as! [Any])
+    default:
+      return super.readValue(ofType: type)
     }
   }
 }
@@ -189,11 +190,13 @@
   /// selected paths.
   ///
   /// An empty list corresponds to a cancelled selection.
-  func displayOpenPanel(options: OpenPanelOptions, completion: @escaping (Result<[String?], Error>) -> Void)
+  func displayOpenPanel(
+    options: OpenPanelOptions, completion: @escaping (Result<[String?], Error>) -> Void)
   /// Shows a save panel with the given [options], returning the selected path.
   ///
   /// A null return corresponds to a cancelled save.
-  func displaySavePanel(options: SavePanelOptions, completion: @escaping (Result<String?, Error>) -> Void)
+  func displaySavePanel(
+    options: SavePanelOptions, completion: @escaping (Result<String?, Error>) -> Void)
 }
 
 /// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -206,17 +209,19 @@
     /// selected paths.
     ///
     /// An empty list corresponds to a cancelled selection.
-    let displayOpenPanelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.FileSelectorApi.displayOpenPanel", binaryMessenger: binaryMessenger, codec: codec)
+    let displayOpenPanelChannel = FlutterBasicMessageChannel(
+      name: "dev.flutter.pigeon.FileSelectorApi.displayOpenPanel", binaryMessenger: binaryMessenger,
+      codec: codec)
     if let api = api {
       displayOpenPanelChannel.setMessageHandler { message, reply in
         let args = message as! [Any]
         let optionsArg = args[0] as! OpenPanelOptions
         api.displayOpenPanel(options: optionsArg) { result in
           switch result {
-            case .success(let res):
-              reply(wrapResult(res))
-            case .failure(let error):
-              reply(wrapError(error))
+          case .success(let res):
+            reply(wrapResult(res))
+          case .failure(let error):
+            reply(wrapError(error))
           }
         }
       }
@@ -226,17 +231,19 @@
     /// Shows a save panel with the given [options], returning the selected path.
     ///
     /// A null return corresponds to a cancelled save.
-    let displaySavePanelChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.FileSelectorApi.displaySavePanel", binaryMessenger: binaryMessenger, codec: codec)
+    let displaySavePanelChannel = FlutterBasicMessageChannel(
+      name: "dev.flutter.pigeon.FileSelectorApi.displaySavePanel", binaryMessenger: binaryMessenger,
+      codec: codec)
     if let api = api {
       displaySavePanelChannel.setMessageHandler { message, reply in
         let args = message as! [Any]
         let optionsArg = args[0] as! SavePanelOptions
         api.displaySavePanel(options: optionsArg) { result in
           switch result {
-            case .success(let res):
-              reply(wrapResult(res))
-            case .failure(let error):
-              reply(wrapError(error))
+          case .success(let res):
+            reply(wrapResult(res))
+          case .failure(let error):
+            reply(wrapError(error))
           }
         }
       }
diff --git a/packages/file_selector/file_selector_macos/pubspec.yaml b/packages/file_selector/file_selector_macos/pubspec.yaml
index 65b055d..f55079f 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.1+1
+version: 0.9.2
 
 environment:
   sdk: ">=2.18.0 <4.0.0"