[flutter_tools] Ensure flutter daemon clients can detect preview device (#140112)

Part of https://github.com/flutter/flutter/issues/130277
diff --git a/packages/flutter_tools/doc/daemon.md b/packages/flutter_tools/doc/daemon.md
index 73a85c2..f570949 100644
--- a/packages/flutter_tools/doc/daemon.md
+++ b/packages/flutter_tools/doc/daemon.md
@@ -62,14 +62,22 @@
 
 #### daemon.getSupportedPlatforms
 
-The `getSupportedPlatforms()` command will enumerate all platforms supported by the project located at the provided `projectRoot`. It returns a Map with the key 'platforms' containing a List of strings which describe the set of all possibly supported platforms. Possible values include:
-   - android
-   - ios
-   - linux #experimental
-   - macos #experimental
-   - windows #experimental
-   - fuchsia #experimental
-   - web #experimental
+The `getSupportedPlatforms()` command will enumerate all platforms supported
+by the project located at the provided `projectRoot`. It returns a Map with
+the key 'platformTypes' containing a Map of platform types to a Map with the
+following entries:
+
+- isSupported (bool) - whether or not the platform type is supported
+- reasons (List<Map<String, Object>>, only included if isSupported == false) - a list of reasons why the platform is not supported
+
+The schema for each element in `reasons` is:
+
+- reasonText (String) - a description of why the platform is not supported
+- fixText (String) - human readable instructions of how to fix this reason
+- fixCode (String) - stringified version of the `_ReasonCode` enum. To be used
+by daemon clients who intend to auto-fix.
+
+The possible platform types are the `PlatformType` enumeration in the lib/src/device.dart library.
 
 #### Events
 
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index b24dfa2..10ed234 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -416,36 +416,167 @@
   /// is correct.
   Future<Map<String, Object>> getSupportedPlatforms(Map<String, Object?> args) async {
     final String? projectRoot = _getStringArg(args, 'projectRoot', required: true);
-    final List<String> result = <String>[];
+    final List<String> platformTypes = <String>[];
+    final Map<String, Object> platformTypesMap = <String, Object>{};
     try {
       final FlutterProject flutterProject = FlutterProject.fromDirectory(globals.fs.directory(projectRoot));
       final Set<SupportedPlatform> supportedPlatforms = flutterProject.getSupportedPlatforms().toSet();
-      if (featureFlags.isLinuxEnabled && supportedPlatforms.contains(SupportedPlatform.linux)) {
-        result.add('linux');
+
+      void handlePlatformType(
+        PlatformType platform,
+      ) {
+        final List<Map<String, Object>> reasons = <Map<String, Object>>[];
+        switch (platform) {
+          case PlatformType.linux:
+            if (!featureFlags.isLinuxEnabled) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the Linux feature is not enabled',
+                'fixText': 'Run "flutter config --enable-linux-desktop"',
+                'fixCode': _ReasonCode.config.name,
+              });
+            }
+            if (!supportedPlatforms.contains(SupportedPlatform.linux)) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the Linux platform is not enabled for this project',
+                'fixText': 'Run "flutter create --platforms=linux ." in your application directory',
+                'fixCode': _ReasonCode.create.name,
+              });
+            }
+          case PlatformType.macos:
+            if (!featureFlags.isMacOSEnabled) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the macOS feature is not enabled',
+                'fixText': 'Run "flutter config --enable-macos-desktop"',
+                'fixCode': _ReasonCode.config.name,
+              });
+            }
+            if (!supportedPlatforms.contains(SupportedPlatform.macos)) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the macOS platform is not enabled for this project',
+                'fixText': 'Run "flutter create --platforms=macos ." in your application directory',
+                'fixCode': _ReasonCode.create.name,
+              });
+            }
+          case PlatformType.windows:
+            if (!featureFlags.isWindowsEnabled) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the Windows feature is not enabled',
+                'fixText': 'Run "flutter config --enable-windows-desktop"',
+                'fixCode': _ReasonCode.config.name,
+              });
+            }
+            if (!supportedPlatforms.contains(SupportedPlatform.windows)) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the Windows platform is not enabled for this project',
+                'fixText': 'Run "flutter create --platforms=windows ." in your application directory',
+                'fixCode': _ReasonCode.create.name,
+              });
+            }
+          case PlatformType.ios:
+            if (!featureFlags.isIOSEnabled) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the iOS feature is not enabled',
+                'fixText': 'Run "flutter config --enable-ios"',
+                'fixCode': _ReasonCode.config.name,
+              });
+            }
+            if (!supportedPlatforms.contains(SupportedPlatform.ios)) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the iOS platform is not enabled for this project',
+                'fixText': 'Run "flutter create --platforms=ios ." in your application directory',
+                'fixCode': _ReasonCode.create.name,
+              });
+            }
+          case PlatformType.android:
+            if (!featureFlags.isAndroidEnabled) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the Android feature is not enabled',
+                'fixText': 'Run "flutter config --enable-android"',
+                'fixCode': _ReasonCode.config.name,
+              });
+            }
+            if (!supportedPlatforms.contains(SupportedPlatform.android)) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the Android platform is not enabled for this project',
+                'fixText': 'Run "flutter create --platforms=android ." in your application directory',
+                'fixCode': _ReasonCode.create.name,
+              });
+            }
+          case PlatformType.web:
+            if (!featureFlags.isWebEnabled) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the Web feature is not enabled',
+                'fixText': 'Run "flutter config --enable-web"',
+                'fixCode': _ReasonCode.config.name,
+              });
+            }
+            if (!supportedPlatforms.contains(SupportedPlatform.web)) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the Web platform is not enabled for this project',
+                'fixText': 'Run "flutter create --platforms=web ." in your application directory',
+                'fixCode': _ReasonCode.create.name,
+              });
+            }
+          case PlatformType.fuchsia:
+            if (!featureFlags.isFuchsiaEnabled) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the Fuchsia feature is not enabled',
+                'fixText': 'Run "flutter config --enable-fuchsia"',
+                'fixCode': _ReasonCode.config.name,
+              });
+            }
+            if (!supportedPlatforms.contains(SupportedPlatform.fuchsia)) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the Fuchsia platform is not enabled for this project',
+                'fixText': 'Run "flutter create --platforms=fuchsia ." in your application directory',
+                'fixCode': _ReasonCode.create.name,
+              });
+            }
+          case PlatformType.custom:
+            if (!featureFlags.areCustomDevicesEnabled) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the custom devices feature is not enabled',
+                'fixText': 'Run "flutter config --enable-custom-devices"',
+                'fixCode': _ReasonCode.config.name,
+              });
+            }
+          case PlatformType.windowsPreview:
+            // TODO(fujino): detect if there any plugins with native code
+            if (!featureFlags.isPreviewDeviceEnabled) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the Preview Device feature is not enabled',
+                'fixText': 'Run "flutter config --enable-flutter-preview',
+                'fixCode': _ReasonCode.config.name,
+              });
+            }
+            if (!supportedPlatforms.contains(SupportedPlatform.windows)) {
+              reasons.add(<String, Object>{
+                'reasonText': 'the Windows platform is not enabled for this project',
+                'fixText': 'Run "flutter create --platforms=windows ." in your application directory',
+                'fixCode': _ReasonCode.create.name,
+              });
+            }
+        }
+
+        if (reasons.isEmpty) {
+          platformTypes.add(platform.name);
+          platformTypesMap[platform.name] = const <String, Object>{
+            'isSupported': true,
+          };
+        } else {
+          platformTypesMap[platform.name] = <String, Object>{
+            'isSupported': false,
+            'reasons': reasons,
+          };
+        }
       }
-      if (featureFlags.isMacOSEnabled && supportedPlatforms.contains(SupportedPlatform.macos)) {
-        result.add('macos');
-      }
-      if (featureFlags.isWindowsEnabled && supportedPlatforms.contains(SupportedPlatform.windows)) {
-        result.add('windows');
-      }
-      if (featureFlags.isIOSEnabled && supportedPlatforms.contains(SupportedPlatform.ios)) {
-        result.add('ios');
-      }
-      if (featureFlags.isAndroidEnabled && supportedPlatforms.contains(SupportedPlatform.android)) {
-        result.add('android');
-      }
-      if (featureFlags.isWebEnabled && supportedPlatforms.contains(SupportedPlatform.web)) {
-        result.add('web');
-      }
-      if (featureFlags.isFuchsiaEnabled && supportedPlatforms.contains(SupportedPlatform.fuchsia)) {
-        result.add('fuchsia');
-      }
-      if (featureFlags.areCustomDevicesEnabled) {
-        result.add('custom');
-      }
+
+      PlatformType.values.forEach(handlePlatformType);
+
       return <String, Object>{
-        'platforms': result,
+        // TODO(fujino): delete this key https://github.com/flutter/flutter/issues/140473
+        'platforms': platformTypes,
+        'platformTypes': platformTypesMap,
       };
     } on Exception catch (err, stackTrace) {
       sendEvent('log', <String, Object?>{
@@ -454,12 +585,16 @@
         'error': true,
       });
       // On any sort of failure, fall back to Android and iOS for backwards
-      // comparability.
-      return <String, Object>{
+      // compatibility.
+      return const <String, Object>{
         'platforms': <String>[
           'android',
           'ios',
         ],
+        'platformTypes': <String, Object>{
+          'android': <String, Object>{'isSupported': true},
+          'ios': <String, Object>{'isSupported': true},
+        },
       };
     }
   }
@@ -470,6 +605,14 @@
   }
 }
 
+/// The reason a [PlatformType] is not currently supported.
+///
+/// The [name] of this value will be sent as a response to daemon client.
+enum _ReasonCode {
+  create,
+  config,
+}
+
 typedef RunOrAttach = Future<void> Function({
   Completer<DebugConnectionInfo>? connectionInfoCompleter,
   Completer<void>? appStartedCompleter,
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index d316f90..3de2799 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -45,34 +45,20 @@
 
 /// The platform sub-folder that a device type supports.
 enum PlatformType {
-  web._('web'),
-  android._('android'),
-  ios._('ios'),
-  linux._('linux'),
-  macos._('macos'),
-  windows._('windows'),
-  fuchsia._('fuchsia'),
-  custom._('custom');
-
-  const PlatformType._(this.value);
-
-  final String value;
+  web,
+  android,
+  ios,
+  linux,
+  macos,
+  windows,
+  fuchsia,
+  custom,
+  windowsPreview;
 
   @override
-  String toString() => value;
+  String toString() => name;
 
-  static PlatformType? fromString(String platformType) {
-    return const <String, PlatformType>{
-      'web': web,
-      'android': android,
-      'ios': ios,
-      'linux': linux,
-      'macos': macos,
-      'windows': windows,
-      'fuchsia': fuchsia,
-      'custom': custom,
-    }[platformType];
-  }
+  static PlatformType? fromString(String platformType) => values.asNameMap()[platformType];
 }
 
 /// A discovery mechanism for flutter-supported development devices.
diff --git a/packages/flutter_tools/lib/src/preview_device.dart b/packages/flutter_tools/lib/src/preview_device.dart
index 20b54b0..71eba1f 100644
--- a/packages/flutter_tools/lib/src/preview_device.dart
+++ b/packages/flutter_tools/lib/src/preview_device.dart
@@ -29,7 +29,7 @@
   return BundleBuilder();
 }
 
-class PreviewDeviceDiscovery extends DeviceDiscovery {
+class PreviewDeviceDiscovery extends PollingDeviceDiscovery {
   PreviewDeviceDiscovery({
     required Platform platform,
     required Artifacts artifacts,
@@ -42,7 +42,8 @@
        _processManager = processManager,
        _fileSystem = fileSystem,
        _platform = platform,
-       _features = featureFlags;
+       _features = featureFlags,
+       super('Flutter preview device');
 
   final Platform _platform;
   final Artifacts _artifacts;
@@ -61,9 +62,8 @@
   List<String> get wellKnownIds => <String>['preview'];
 
   @override
-  Future<List<Device>> devices({
+  Future<List<Device>> pollingGetDevices({
     Duration? timeout,
-    DeviceDiscoveryFilter? filter,
   }) async {
     final File previewBinary = _fileSystem.file(_artifacts.getArtifactPath(Artifact.flutterPreviewDevice));
     if (!previewBinary.existsSync()) {
@@ -76,16 +76,8 @@
       processManager: _processManager,
       previewBinary: previewBinary,
     );
-    final bool matchesRequirements;
-    if (!_features.isPreviewDeviceEnabled) {
-      matchesRequirements = false;
-    } else if (filter == null) {
-      matchesRequirements = true;
-    } else {
-      matchesRequirements = await filter.matchesRequirements(device);
-    }
     return <Device>[
-      if (matchesRequirements)
+      if (_features.isPreviewDeviceEnabled)
         device,
     ];
   }
@@ -114,7 +106,7 @@
        _fileSystem = fileSystem,
        _bundleBuilderFactory = builderFactory,
        _artifacts = artifacts,
-       super('preview', ephemeral: false, category: Category.desktop, platformType: PlatformType.custom);
+       super('preview', ephemeral: false, category: Category.desktop, platformType: PlatformType.windowsPreview);
 
   final ProcessManager _processManager;
   final Logger _logger;
@@ -161,7 +153,7 @@
   bool isSupportedForProject(FlutterProject flutterProject) => true;
 
   @override
-  String get name => 'preview';
+  String get name => 'Preview';
 
   @override
   DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder();
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/daemon_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/daemon_test.dart
index 1ddc2d4..769792d 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/daemon_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/daemon_test.dart
@@ -7,10 +7,12 @@
 import 'dart:typed_data';
 
 import 'package:fake_async/fake_async.dart';
+import 'package:file/memory.dart';
 import 'package:file/src/interface/file.dart';
 import 'package:flutter_tools/src/android/android_device.dart';
 import 'package:flutter_tools/src/android/android_workflow.dart';
 import 'package:flutter_tools/src/application_package.dart';
+import 'package:flutter_tools/src/artifacts.dart';
 import 'package:flutter_tools/src/base/dds.dart';
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/base/utils.dart';
@@ -22,8 +24,10 @@
 import 'package:flutter_tools/src/fuchsia/fuchsia_workflow.dart';
 import 'package:flutter_tools/src/globals.dart' as globals;
 import 'package:flutter_tools/src/ios/ios_workflow.dart';
+import 'package:flutter_tools/src/preview_device.dart';
 import 'package:flutter_tools/src/resident_runner.dart';
 import 'package:flutter_tools/src/vmservice.dart';
+import 'package:flutter_tools/src/windows/windows_workflow.dart';
 import 'package:test/fake.dart';
 
 import '../../src/common.dart';
@@ -121,10 +125,91 @@
 
       expect(response.data['id'], 0);
       expect(response.data['result'], isNotEmpty);
-      expect((response.data['result']! as Map<String, Object?>)['platforms'], <String>{'macos'});
+      expect(
+        response.data['result']! as Map<String, Object?>,
+        const <String, Object>{
+          'platforms': <String>['macos', 'windows', 'windowsPreview'],
+          'platformTypes': <String, Map<String, Object>>{
+            'web': <String, Object>{
+              'isSupported': false,
+              'reasons': <Map<String, String>>[
+                <String, String>{
+                  'reasonText': 'the Web feature is not enabled',
+                  'fixText': 'Run "flutter config --enable-web"',
+                  'fixCode': 'config',
+                },
+              ],
+            },
+            'android': <String, Object>{
+              'isSupported': false,
+              'reasons': <Map<String, String>>[
+                <String, String>{
+                  'reasonText': 'the Android feature is not enabled',
+                  'fixText': 'Run "flutter config --enable-android"',
+                  'fixCode': 'config',
+                },
+              ],
+            },
+            'ios': <String, Object>{
+              'isSupported': false,
+              'reasons': <Map<String, String>>[
+                <String, String>{
+                  'reasonText': 'the iOS feature is not enabled',
+                  'fixText': 'Run "flutter config --enable-ios"',
+                  'fixCode': 'config',
+                },
+              ],
+            },
+            'linux': <String, Object>{
+              'isSupported': false,
+              'reasons': <Map<String, String>>[
+                <String, String>{
+                  'reasonText': 'the Linux feature is not enabled',
+                  'fixText': 'Run "flutter config --enable-linux-desktop"',
+                  'fixCode': 'config',
+                },
+              ],
+            },
+            'macos': <String, bool>{'isSupported': true},
+            'windows': <String, bool>{'isSupported': true},
+            'fuchsia': <String, Object>{
+              'isSupported': false,
+              'reasons': <Map<String, String>>[
+                <String, String>{
+                  'reasonText': 'the Fuchsia feature is not enabled',
+                  'fixText': 'Run "flutter config --enable-fuchsia"',
+                  'fixCode': 'config',
+                },
+                <String, String>{
+                  'reasonText': 'the Fuchsia platform is not enabled for this project',
+                  'fixText': 'Run "flutter create --platforms=fuchsia ." in your application directory',
+                  'fixCode': 'create',
+                },
+              ],
+            },
+            'custom': <String, Object>{
+              'isSupported': false,
+              'reasons': <Map<String, String>>[
+                <String, String>{
+                  'reasonText': 'the custom devices feature is not enabled',
+                  'fixText': 'Run "flutter config --enable-custom-devices"',
+                  'fixCode': 'config',
+                },
+              ],
+            },
+            'windowsPreview': <String, bool>{'isSupported': true},
+          },
+        },
+      );
     }, overrides: <Type, Generator>{
       // Disable Android/iOS and enable macOS to make sure result is consistent and defaults are tested off.
-      FeatureFlags: () => TestFeatureFlags(isAndroidEnabled: false, isIOSEnabled: false, isMacOSEnabled: true),
+      FeatureFlags: () => TestFeatureFlags(
+        isAndroidEnabled: false,
+        isIOSEnabled: false,
+        isMacOSEnabled: true,
+        isPreviewDeviceEnabled: true,
+        isWindowsEnabled: true,
+      ),
     });
 
     testUsingContext('printError should send daemon.logMessage event', () async {
@@ -342,18 +427,75 @@
       final FakePollingDeviceDiscovery discoverer = FakePollingDeviceDiscovery();
       daemon.deviceDomain.addDeviceDiscoverer(discoverer);
       discoverer.addDevice(FakeAndroidDevice());
+      final MemoryFileSystem fs = MemoryFileSystem.test();
+      discoverer.addDevice(PreviewDevice(
+        processManager: FakeProcessManager.empty(),
+        logger: BufferLogger.test(),
+        fileSystem: fs,
+        previewBinary: fs.file(r'preview_device.exe'),
+        artifacts: Artifacts.test(fileSystem: fs),
+        builderFactory: () => throw UnimplementedError('TODO implement builder factory'),
+      ));
 
-      return daemonStreams.outputs.stream.skipWhile(_isConnectedEvent).first.then<void>((DaemonMessage response) async {
+      final List<Map<String, Object?>> names = <Map<String, Object?>>[];
+      await daemonStreams.outputs.stream.skipWhile(_isConnectedEvent).take(2).forEach((DaemonMessage response) async {
         expect(response.data['event'], 'device.added');
         expect(response.data['params'], isMap);
 
         final Map<String, Object?> params = castStringKeyedMap(response.data['params'])!;
-        expect(params['platform'], isNotEmpty); // the fake device has a platform of 'android-arm'
+        names.add(params);
       });
+      await daemonStreams.outputs.close();
+      expect(
+        names,
+        containsAll(const <Map<String, Object?>>[
+          <String, Object?>{
+            'id': 'device',
+            'name': 'android device',
+            'platform': 'android-arm',
+            'emulator': false,
+            'category': 'mobile',
+            'platformType': 'android',
+            'ephemeral': false,
+            'emulatorId': 'device',
+            'sdk': 'Android 12',
+            'capabilities': <String, Object?>{
+              'hotReload': true,
+              'hotRestart': true,
+              'screenshot': true,
+              'fastStart': true,
+              'flutterExit': true,
+              'hardwareRendering': true,
+              'startPaused': true,
+            },
+          },
+          <String, Object?>{
+            'id': 'preview',
+            'name': 'Preview',
+            'platform': 'windows-x64',
+            'emulator': false,
+            'category': 'desktop',
+            'platformType': 'windowsPreview',
+            'ephemeral': false,
+            'emulatorId': null,
+            'sdk': 'preview',
+            'capabilities': <String, Object?>{
+              'hotReload': true,
+              'hotRestart': true,
+              'screenshot': false,
+              'fastStart': false,
+              'flutterExit': true,
+              'hardwareRendering': true,
+              'startPaused': true,
+            },
+          },
+        ]),
+      );
     }, overrides: <Type, Generator>{
       AndroidWorkflow: () => FakeAndroidWorkflow(),
       IOSWorkflow: () => FakeIOSWorkflow(),
       FuchsiaWorkflow: () => FakeFuchsiaWorkflow(),
+      WindowsWorkflow: () => FakeWindowsWorkflow(),
     });
 
     testUsingContext('device.discoverDevices should respond with list', () async {
@@ -930,6 +1072,13 @@
 
 bool _isConnectedEvent(DaemonMessage message) => message.data['event'] == 'daemon.connected';
 
+class FakeWindowsWorkflow extends Fake implements WindowsWorkflow {
+  FakeWindowsWorkflow({ this.canListDevices = true });
+
+  @override
+  final bool canListDevices;
+}
+
 class FakeFuchsiaWorkflow extends Fake implements FuchsiaWorkflow {
   FakeFuchsiaWorkflow({ this.canListDevices = true });
 
@@ -956,7 +1105,7 @@
   final String id = 'device';
 
   @override
-  final String name = 'device';
+  final String name = 'android device';
 
   @override
   Future<String> get emulatorId async => 'device';
diff --git a/packages/flutter_tools/test/general.shard/preview_device_test.dart b/packages/flutter_tools/test/general.shard/preview_device_test.dart
index 8b2d429..f65648f 100644
--- a/packages/flutter_tools/test/general.shard/preview_device_test.dart
+++ b/packages/flutter_tools/test/general.shard/preview_device_test.dart
@@ -52,7 +52,7 @@
     );
 
     expect(await device.isLocalEmulator, false);
-    expect(device.name, 'preview');
+    expect(device.name, 'Preview');
     expect(await device.sdkNameAndVersion, 'preview');
     expect(await device.targetPlatform, TargetPlatform.windows_x64);
     expect(device.category, Category.desktop);