Show unsupported devices when no supported devices are connected (#56531)

diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart
index 466696e..f2eedde 100644
--- a/packages/flutter_tools/lib/src/base/user_messages.dart
+++ b/packages/flutter_tools/lib/src/base/user_messages.dart
@@ -237,6 +237,9 @@
       "matching '$deviceId'";
   String get flutterNoDevicesFound => 'No devices found';
   String get flutterNoSupportedDevices => 'No supported devices connected.';
+  String flutterMissPlatformProjects(List<String> unsupportedDevicesType) =>
+      'If you would like your app to run on ${unsupportedDevicesType.join(' or ')}, consider running `flutter create .` to generate projects for these platforms.';
+  String get flutterFoundButUnsupportedDevices => 'The following devices were found, but are not supported by this project:';
   String flutterFoundSpecifiedDevices(int count, String deviceId) =>
       'Found $count devices with name or id matching $deviceId:';
   String get flutterSpecifyDeviceWithAllOption =>
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 6c19930..59f5b25 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -557,6 +557,13 @@
     await descriptions(devices).forEach(globals.printStatus);
   }
 
+  static List<String> devicesPlatformTypes(List<Device> devices) {
+    return devices
+        .map(
+          (Device d) => d.platformType.toString(),
+        ).toSet().toList()..sort();
+  }
+
   /// Convert the Device object to a JSON representation suitable for serialization.
   Future<Map<String, Object>> toJson() async {
     final bool isLocalEmu = await isLocalEmulator;
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index b145380..b6de3cc 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -823,11 +823,28 @@
     if (devices.isEmpty && deviceManager.hasSpecifiedDeviceId) {
       globals.printStatus(userMessages.flutterNoMatchingDevice(deviceManager.specifiedDeviceId));
       return null;
-    } else if (devices.isEmpty && deviceManager.hasSpecifiedAllDevices) {
-      globals.printStatus(userMessages.flutterNoDevicesFound);
-      return null;
     } else if (devices.isEmpty) {
-      globals.printStatus(userMessages.flutterNoSupportedDevices);
+      if (deviceManager.hasSpecifiedAllDevices) {
+        globals.printStatus(userMessages.flutterNoDevicesFound);
+      } else {
+        globals.printStatus(userMessages.flutterNoSupportedDevices);
+      }
+      final List<Device> unsupportedDevices = await deviceManager.getDevices();
+      if (unsupportedDevices.isNotEmpty) {
+        final StringBuffer result = StringBuffer();
+        result.writeln(userMessages.flutterFoundButUnsupportedDevices);
+        result.writeAll(
+          await Device.descriptions(unsupportedDevices)
+              .map((String desc) => desc)
+              .toList(),
+          '\n',
+        );
+        result.writeln('');
+        result.writeln(userMessages.flutterMissPlatformProjects(
+          Device.devicesPlatformTypes(unsupportedDevices),
+        ));
+        globals.printStatus(result.toString());
+      }
       return null;
     } else if (devices.length > 1 && !deviceManager.hasSpecifiedAllDevices) {
       if (deviceManager.hasSpecifiedDeviceId) {
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/devices_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/devices_test.dart
index b2d1404..090e004 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/devices_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/devices_test.dart
@@ -38,6 +38,16 @@
       ProcessManager: () => MockProcessManager(),
     });
 
+    testUsingContext('get devices\' platform types', () async {
+      final List<String> platformTypes = Device.devicesPlatformTypes(
+        await deviceManager.getAllConnectedDevices(),
+      );
+      expect(platformTypes, <String>['android', 'web']);
+    }, overrides: <Type, Generator>{
+      DeviceManager: () => _FakeDeviceManager(),
+      ProcessManager: () => MockProcessManager(),
+    });
+
     testUsingContext('Outputs parsable JSON with --machine flag', () async {
       final DevicesCommand command = DevicesCommand();
       await createTestCommandRunner(command).run(<String>['devices', '--machine']);
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart
index 4e6f05b..59ce881 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart
@@ -241,6 +241,62 @@
         ProcessManager: () => mockProcessManager,
       });
 
+      testUsingContext('shows unsupported devices when no supported devices are found',  () async {
+        final RunCommand command = RunCommand();
+        applyMocksToCommand(command);
+
+        final MockDevice mockDevice = MockDevice(TargetPlatform.android_arm);
+        when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) => Future<bool>.value(true));
+        when(mockDevice.isSupported()).thenAnswer((Invocation invocation) => true);
+        when(mockDevice.supportsFastStart).thenReturn(true);
+        when(mockDevice.id).thenReturn('mock-id');
+        when(mockDevice.name).thenReturn('mock-name');
+        when(mockDevice.platformType).thenReturn(PlatformType.android);
+        when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) => Future<String>.value('api-14'));
+
+        when(mockDeviceManager.getDevices()).thenAnswer((Invocation invocation) {
+          return Future<List<Device>>.value(<Device>[
+            mockDevice,
+          ]);
+        });
+
+        when(mockDeviceManager.findTargetDevices(any)).thenAnswer(
+            (Invocation invocation) => Future<List<Device>>.value(<Device>[]),
+        );
+
+        try {
+          await createTestCommandRunner(command).run(<String>[
+            'run',
+            '--no-pub',
+            '--no-hot',
+          ]);
+          fail('Expect exception');
+        } on ToolExit catch (e) {
+          expect(e.message, null);
+        }
+
+        expect(
+          testLogger.statusText,
+          containsIgnoringWhitespace(userMessages.flutterNoSupportedDevices),
+        );
+        expect(
+          testLogger.statusText,
+          containsIgnoringWhitespace(userMessages.flutterFoundButUnsupportedDevices),
+        );
+        expect(
+          testLogger.statusText,
+          containsIgnoringWhitespace(
+            userMessages.flutterMissPlatformProjects(
+              Device.devicesPlatformTypes(<Device>[mockDevice]),
+            ),
+          ),
+        );
+      }, overrides: <Type, Generator>{
+        DeviceManager: () => mockDeviceManager,
+        FileSystem: () => fs,
+        ProcessManager: () => mockProcessManager,
+      });
+
       testUsingContext('updates cache before checking for devices', () async {
         final RunCommand command = RunCommand();
         applyMocksToCommand(command);
diff --git a/packages/flutter_tools/test/src/fake_devices.dart b/packages/flutter_tools/test/src/fake_devices.dart
index 5dd2beb..e280b7b 100644
--- a/packages/flutter_tools/test/src/fake_devices.dart
+++ b/packages/flutter_tools/test/src/fake_devices.dart
@@ -11,7 +11,7 @@
 /// (`Device.toJson()` and `--machine` flag for `devices` command)
 List<FakeDeviceJsonData> fakeDevices = <FakeDeviceJsonData>[
   FakeDeviceJsonData(
-    FakeDevice('ephemeral', 'ephemeral', true),
+    FakeDevice('ephemeral', 'ephemeral', true, true, PlatformType.android),
     <String, Object>{
       'name': 'ephemeral',
       'id': 'ephemeral',
@@ -56,9 +56,9 @@
 
 /// Fake device to test `devices` command
 class FakeDevice extends Device {
-  FakeDevice(this.name, String id, [bool ephemeral = true, this._isSupported = true]) : super(
+  FakeDevice(this.name, String id, [bool ephemeral = true, this._isSupported = true, PlatformType type = PlatformType.web]) : super(
       id,
-      platformType: PlatformType.web,
+      platformType: type,
       category: Category.mobile,
       ephemeral: ephemeral,
   );