Add device category, ephemeral, platformType for daemon (#33990)

diff --git a/packages/flutter_tools/doc/daemon.md b/packages/flutter_tools/doc/daemon.md
index a910b00..ac9d9c4 100644
--- a/packages/flutter_tools/doc/daemon.md
+++ b/packages/flutter_tools/doc/daemon.md
@@ -45,7 +45,7 @@
 Events that come from the server will have an `event` field containing the type of event, along with a `params` field.
 
 ```
-[{"event":"device.added","params":{"id":"1DD6786B-37D4-4355-AA15-B818A87A18B4","name":"iPhone XS Max","platform":"ios","emulator":true}}]
+[{"event":"device.added","params":{"id":"1DD6786B-37D4-4355-AA15-B818A87A18B4","name":"iPhone XS Max","platform":"ios","emulator":true,"ephemeral":false,"platformType":"ios","category":"mobile"}}]
 ```
 
 ## Domains and Commands
@@ -157,7 +157,15 @@
 
 #### device.getDevices
 
-Return a list of all connected devices. The `params` field will be a List; each item is a map with the fields `id`, `name`, `platform`, and `emulator` (a boolean).
+Return a list of all connected devices. The `params` field will be a List; each item is a map with the fields `id`, `name`, `platform`, `category`, `platformType`, `ephemeral`, and `emulator` (a boolean).
+
+`category` is string description of the kind of workflow the device supports. The current categories are "mobile", "web" and "desktop", or null if none.
+
+`platformType` is a string description of the platform sub-folder the device
+supports. The current catgetories are "android", "ios", "linux", "macos",
+"fuchsia", "windows", and "web". These are kept in sync with the response from `daemon.getSupportedPlatforms`.
+
+`ephemeral` is a boolean which indicates where the device needs to be manually connected to a development machine. For example, a physical Android device is ephemeral, but the "web" device (that is always present) is not.
 
 #### device.enable
 
@@ -181,11 +189,11 @@
 
 #### device.added
 
-This is sent when a device is connected (and polling has been enabled via `enable()`). The `params` field will be a map with the fields `id`, `name`, `platform`, and `emulator`.
+This is sent when a device is connected (and polling has been enabled via `enable()`). The `params` field will be a map with the fields `id`, `name`, `platform`, `category`, `platformType`, `ephemeral`, and  `emulator`. For more information on `platform`, `category`, `platformType`, and `ephemeral` see `device.getDevices`.
 
 #### device.removed
 
-This is sent when a device is disconnected (and polling has been enabled via `enable()`). The `params` field will be a map with the fields `id`, `name`, `platform`, and `emulator`.
+This is sent when a device is disconnected (and polling has been enabled via `enable()`). The `params` field will be a map with the fields `id`, `name`, `platform`, `category`, `platformType`, `ephemeral`, and  `emulator`. For more information on `platform`, `category`, `platformType`, and `ephemeral` see `device.getDevices`.
 
 ### emulator domain
 
@@ -250,6 +258,7 @@
 
 ## Changelog
 
+- 0.5.1: Added `platformType`, `ephemeral`, and `category` field to device.
 - 0.5.0: Added `daemon.getSupportedPlatforms` command
 - 0.4.2: Added `app.detach` command
 - 0.4.1: Added `flutter attach --machine`
diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index f8003c3..2bba099 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -72,7 +72,12 @@
     this.productID,
     this.modelID,
     this.deviceCodeName,
-  }) : super(id);
+  }) : super(
+      id,
+      category: Category.mobile,
+      platformType: PlatformType.android,
+      ephemeral: true,
+  );
 
   final String productID;
   final String modelID;
diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart
index 78f582c..708bddf 100644
--- a/packages/flutter_tools/lib/src/commands/daemon.dart
+++ b/packages/flutter_tools/lib/src/commands/daemon.dart
@@ -26,7 +26,7 @@
 import '../runner/flutter_command.dart';
 import '../vmservice.dart';
 
-const String protocolVersion = '0.5.0';
+const String protocolVersion = '0.5.1';
 
 /// A server process command. This command will start up a long-lived server.
 /// It reads JSON-RPC based commands from stdin, executes them, and returns
@@ -659,7 +659,12 @@
   _DeviceEventHandler _onDeviceEvent(String eventName) {
     return (Device device) {
       _serializeDeviceEvents = _serializeDeviceEvents.then<void>((_) async {
-        sendEvent(eventName, await _deviceToMap(device));
+        try {
+          final Map<String, Object> response = await _deviceToMap(device);
+          sendEvent(eventName, response);
+        } catch (err) {
+          printError(err);
+        }
       });
     };
   }
@@ -768,6 +773,9 @@
     'name': device.name,
     'platform': getNameForTargetPlatform(await device.targetPlatform),
     'emulator': await device.isLocalEmulator,
+    'category': device.category?.toString(),
+    'platformType': device.platformType?.toString(),
+    'ephemeral': device.ephemeral,
   };
 }
 
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 3947ce3..14dd869 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -5,6 +5,8 @@
 import 'dart:async';
 import 'dart:math' as math;
 
+import 'package:meta/meta.dart';
+
 import 'android/android_device.dart';
 import 'application_package.dart';
 import 'artifacts.dart';
@@ -28,6 +30,38 @@
 
 DeviceManager get deviceManager => context.get<DeviceManager>();
 
+/// A description of the kind of workflow the device supports.
+class Category {
+  const Category._(this.value);
+
+  static const Category web = Category._('web');
+  static const Category desktop = Category._('desktop');
+  static const Category mobile = Category._('mobile');
+
+  final String value;
+
+  @override
+  String toString() => value;
+}
+
+/// The platform sub-folder that a device type supports.
+class PlatformType {
+  const PlatformType._(this.value);
+
+  static const PlatformType web = PlatformType._('web');
+  static const PlatformType android = PlatformType._('android');
+  static const PlatformType ios = PlatformType._('ios');
+  static const PlatformType linux = PlatformType._('linux');
+  static const PlatformType macos = PlatformType._('macos');
+  static const PlatformType windows = PlatformType._('windows');
+  static const PlatformType fuchsia = PlatformType._('fuchsia');
+
+  final String value;
+
+  @override
+  String toString() => value;
+}
+
 /// A class to get all available devices.
 class DeviceManager {
 
@@ -208,10 +242,19 @@
 
 abstract class Device {
 
-  Device(this.id);
+  Device(this.id, {@required this.category, @required this.platformType, @required this.ephemeral});
 
   final String id;
 
+  /// The [Category] for this device type.
+  final Category category;
+
+  /// The [PlatformType] for this device.
+  final PlatformType platformType;
+
+  /// Whether this is an ephemeral device.
+  final bool ephemeral;
+
   String get name;
 
   bool get supportsStartPaused => true;
diff --git a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
index df1840e..8576789 100644
--- a/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
+++ b/packages/flutter_tools/lib/src/fuchsia/fuchsia_device.dart
@@ -175,7 +175,12 @@
 }
 
 class FuchsiaDevice extends Device {
-  FuchsiaDevice(String id, {this.name}) : super(id);
+  FuchsiaDevice(String id, {this.name}) : super(
+      id,
+      platformType: PlatformType.fuchsia,
+      category: null,
+      ephemeral: false,
+  );
 
   @override
   bool get supportsHotReload => true;
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index 15f2a2e..6c8d297 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -114,7 +114,12 @@
 class IOSDevice extends Device {
   IOSDevice(String id, { this.name, String sdkVersion })
       : _sdkVersion = sdkVersion,
-        super(id) {
+        super(
+          id,
+          category: Category.mobile,
+          platformType: PlatformType.ios,
+          ephemeral: true,
+      ) {
     _installerPath = _checkForCommand('ideviceinstaller');
     _iproxyPath = _checkForCommand('iproxy');
   }
diff --git a/packages/flutter_tools/lib/src/ios/simulators.dart b/packages/flutter_tools/lib/src/ios/simulators.dart
index b22e32d..06116b5 100644
--- a/packages/flutter_tools/lib/src/ios/simulators.dart
+++ b/packages/flutter_tools/lib/src/ios/simulators.dart
@@ -50,7 +50,7 @@
       return <IOSSimulator>[];
 
     return SimControl.instance.getConnectedDevices().map<IOSSimulator>((SimDevice device) {
-      return IOSSimulator(device.udid, name: device.name, category: device.category);
+      return IOSSimulator(device.udid, name: device.name, simulatorCategory: device.category);
     }).toList();
   }
 }
@@ -215,12 +215,17 @@
 }
 
 class IOSSimulator extends Device {
-  IOSSimulator(String id, { this.name, this.category }) : super(id);
+  IOSSimulator(String id, { this.name, this.simulatorCategory }) : super(
+      id,
+      category: Category.mobile,
+      platformType: PlatformType.ios,
+      ephemeral: true,
+  );
 
   @override
   final String name;
 
-  final String category;
+  final String simulatorCategory;
 
   @override
   Future<bool> get isLocalEmulator async => true;
@@ -435,7 +440,7 @@
   Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
 
   @override
-  Future<String> get sdkNameAndVersion async => category;
+  Future<String> get sdkNameAndVersion async => simulatorCategory;
 
   final RegExp _iosSdkRegExp = RegExp(r'iOS( |-)(\d+)');
 
diff --git a/packages/flutter_tools/lib/src/linux/linux_device.dart b/packages/flutter_tools/lib/src/linux/linux_device.dart
index 7519737..6f4c49b 100644
--- a/packages/flutter_tools/lib/src/linux/linux_device.dart
+++ b/packages/flutter_tools/lib/src/linux/linux_device.dart
@@ -19,7 +19,12 @@
 
 /// A device that represents a desktop Linux target.
 class LinuxDevice extends Device {
-  LinuxDevice() : super('Linux');
+  LinuxDevice() : super(
+      'Linux',
+      category: Category.desktop,
+      platformType: PlatformType.linux,
+      ephemeral: false,
+  );
 
   @override
   void clearLogs() { }
diff --git a/packages/flutter_tools/lib/src/macos/macos_device.dart b/packages/flutter_tools/lib/src/macos/macos_device.dart
index bf9a307..a082bf0 100644
--- a/packages/flutter_tools/lib/src/macos/macos_device.dart
+++ b/packages/flutter_tools/lib/src/macos/macos_device.dart
@@ -20,7 +20,12 @@
 
 /// A device that represents a desktop MacOS target.
 class MacOSDevice extends Device {
-  MacOSDevice() : super('macOS');
+  MacOSDevice() : super(
+      'macOS',
+      category: Category.desktop,
+      platformType: PlatformType.macos,
+      ephemeral: false,
+  );
 
   @override
   void clearLogs() { }
diff --git a/packages/flutter_tools/lib/src/tester/flutter_tester.dart b/packages/flutter_tools/lib/src/tester/flutter_tester.dart
index 3705057..a7abcf1 100644
--- a/packages/flutter_tools/lib/src/tester/flutter_tester.dart
+++ b/packages/flutter_tools/lib/src/tester/flutter_tester.dart
@@ -42,7 +42,12 @@
 
 // TODO(scheglov): This device does not currently work with full restarts.
 class FlutterTesterDevice extends Device {
-  FlutterTesterDevice(String deviceId) : super(deviceId);
+  FlutterTesterDevice(String deviceId) : super(
+      deviceId,
+      platformType: null,
+      category: null,
+      ephemeral: false,
+  );
 
   Process _process;
   final DevicePortForwarder _portForwarder = _NoopPortForwarder();
diff --git a/packages/flutter_tools/lib/src/web/web_device.dart b/packages/flutter_tools/lib/src/web/web_device.dart
index 8238690f..a98e91f 100644
--- a/packages/flutter_tools/lib/src/web/web_device.dart
+++ b/packages/flutter_tools/lib/src/web/web_device.dart
@@ -28,7 +28,12 @@
 }
 
 class WebDevice extends Device {
-  WebDevice() : super('web');
+  WebDevice() : super(
+      'web',
+      category: Category.web,
+      platformType: PlatformType.web,
+      ephemeral: false,
+  );
 
   @override
   bool get supportsHotReload => true;
diff --git a/packages/flutter_tools/lib/src/windows/windows_device.dart b/packages/flutter_tools/lib/src/windows/windows_device.dart
index 103a731..86d80ee 100644
--- a/packages/flutter_tools/lib/src/windows/windows_device.dart
+++ b/packages/flutter_tools/lib/src/windows/windows_device.dart
@@ -21,7 +21,12 @@
 
 /// A device that represents a desktop Windows target.
 class WindowsDevice extends Device {
-  WindowsDevice() : super('Windows');
+  WindowsDevice() : super(
+      'Windows',
+      category: Category.desktop,
+      platformType: PlatformType.windows,
+      ephemeral: false,
+  );
 
   @override
   void clearLogs() { }
diff --git a/packages/flutter_tools/test/android/android_device_test.dart b/packages/flutter_tools/test/android/android_device_test.dart
index 2046891..c605876 100644
--- a/packages/flutter_tools/test/android/android_device_test.dart
+++ b/packages/flutter_tools/test/android/android_device_test.dart
@@ -58,6 +58,7 @@
 ''', devices: devices);
       expect(devices, hasLength(1));
       expect(devices.first.name, 'Nexus 7');
+      expect(devices.first.category, Category.mobile);
     });
 
     testUsingContext('emulators and short listings', () {
diff --git a/packages/flutter_tools/test/device_test.dart b/packages/flutter_tools/test/device_test.dart
index 4b199c7..308ef0c 100644
--- a/packages/flutter_tools/test/device_test.dart
+++ b/packages/flutter_tools/test/device_test.dart
@@ -50,7 +50,12 @@
 }
 
 class _MockDevice extends Device {
-  _MockDevice(this.name, String id) : super(id);
+  _MockDevice(this.name, String id) : super(
+      id,
+      platformType: PlatformType.web,
+      category: Category.mobile,
+      ephemeral: true,
+  );
 
   @override
   final String name;
diff --git a/packages/flutter_tools/test/ios/devices_test.dart b/packages/flutter_tools/test/ios/devices_test.dart
index 8b1d2da..9b5f30f 100644
--- a/packages/flutter_tools/test/ios/devices_test.dart
+++ b/packages/flutter_tools/test/ios/devices_test.dart
@@ -182,6 +182,7 @@
         'This is a multi-line message,',
         '  with a non-Flutter log message following it.',
       ]);
+      expect(device.category, Category.mobile);
     }, overrides: <Type, Generator>{
       IMobileDevice: () => mockIMobileDevice,
     });
diff --git a/packages/flutter_tools/test/ios/simulators_test.dart b/packages/flutter_tools/test/ios/simulators_test.dart
index c244adf..8cff8c6 100644
--- a/packages/flutter_tools/test/ios/simulators_test.dart
+++ b/packages/flutter_tools/test/ios/simulators_test.dart
@@ -109,16 +109,22 @@
   group('sdkMajorVersion', () {
     // This new version string appears in SimulatorApp-850 CoreSimulator-518.16 beta.
     test('can be parsed from iOS-11-3', () async {
-      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', category: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3');
+      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'com.apple.CoreSimulator.SimRuntime.iOS-11-3');
 
       expect(await device.sdkMajorVersion, 11);
     });
 
     test('can be parsed from iOS 11.2', () async {
-      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', category: 'iOS 11.2');
+      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.2');
 
       expect(await device.sdkMajorVersion, 11);
     });
+
+    test('Has a simulator category', () async {
+      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.2');
+
+      expect(device.category, Category.mobile);
+    });
   });
 
   group('IOSSimulator.isSupported', () {
@@ -246,7 +252,7 @@
     });
 
     testUsingContext('uses tail on iOS versions prior to iOS 11', () async {
-      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', category: 'iOS 9.3');
+      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 9.3');
       await launchDeviceLogTool(device);
       expect(
         verify(mockProcessManager.start(captureAny, environment: null, workingDirectory: null)).captured.single,
@@ -258,7 +264,7 @@
     });
 
     testUsingContext('uses /usr/bin/log on iOS 11 and above', () async {
-      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', category: 'iOS 11.0');
+      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.0');
       await launchDeviceLogTool(device);
       expect(
         verify(mockProcessManager.start(captureAny, environment: null, workingDirectory: null)).captured.single,
@@ -280,7 +286,7 @@
     });
 
     testUsingContext('uses tail on iOS versions prior to iOS 11', () async {
-      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', category: 'iOS 9.3');
+      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 9.3');
       await launchSystemLogTool(device);
       expect(
         verify(mockProcessManager.start(captureAny, environment: null, workingDirectory: null)).captured.single,
@@ -292,7 +298,7 @@
     });
 
     testUsingContext('uses /usr/bin/log on iOS 11 and above', () async {
-      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', category: 'iOS 11.0');
+      final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.0');
       await launchSystemLogTool(device);
       verifyNever(mockProcessManager.start(any, environment: null, workingDirectory: null));
     },
@@ -330,7 +336,7 @@
           return Future<Process>.value(mockProcess);
         });
 
-      final IOSSimulator device = IOSSimulator('123456', category: 'iOS 11.0');
+      final IOSSimulator device = IOSSimulator('123456', simulatorCategory: 'iOS 11.0');
       final DeviceLogReader logReader = device.getLogReader(
         app: BuildableIOSApp(mockIosProject),
       );
@@ -430,7 +436,7 @@
     });
 
     testUsingContext("startApp uses compiled app's Info.plist to find CFBundleIdentifier", () async {
-        final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', category: 'iOS 11.2');
+        final IOSSimulator device = IOSSimulator('x', name: 'iPhone SE', simulatorCategory: 'iOS 11.2');
         when(iosWorkflow.getPlistValueFromFile(any, any)).thenReturn('correct');
 
         final Directory mockDir = fs.currentDirectory;
diff --git a/packages/flutter_tools/test/linux/linux_device_test.dart b/packages/flutter_tools/test/linux/linux_device_test.dart
index 1c49f80..97e7425 100644
--- a/packages/flutter_tools/test/linux/linux_device_test.dart
+++ b/packages/flutter_tools/test/linux/linux_device_test.dart
@@ -43,6 +43,7 @@
       expect(await device.isLatestBuildInstalled(linuxApp), true);
       expect(await device.isAppInstalled(linuxApp), true);
       expect(await device.stopApp(linuxApp), true);
+      expect(device.category, Category.desktop);
     }, overrides: <Type, Generator>{
       ProcessManager: () => mockProcessManager,
     });
diff --git a/packages/flutter_tools/test/macos/macos_device_test.dart b/packages/flutter_tools/test/macos/macos_device_test.dart
index 4dd84e3..6a1b651 100644
--- a/packages/flutter_tools/test/macos/macos_device_test.dart
+++ b/packages/flutter_tools/test/macos/macos_device_test.dart
@@ -44,6 +44,7 @@
       expect(await device.isLatestBuildInstalled(mockMacOSApp), true);
       expect(await device.isAppInstalled(mockMacOSApp), true);
       expect(await device.stopApp(mockMacOSApp), false);
+      expect(device.category, Category.desktop);
     }, overrides: <Type, Generator>{
       ProcessManager: () => mockProcessManager,
     });
diff --git a/packages/flutter_tools/test/windows/windows_device_test.dart b/packages/flutter_tools/test/windows/windows_device_test.dart
index 979a9e3..e1dc10b 100644
--- a/packages/flutter_tools/test/windows/windows_device_test.dart
+++ b/packages/flutter_tools/test/windows/windows_device_test.dart
@@ -43,6 +43,7 @@
       expect(await device.isLatestBuildInstalled(windowsApp), true);
       expect(await device.isAppInstalled(windowsApp), true);
       expect(await device.stopApp(windowsApp), false);
+      expect(device.category, Category.desktop);
     }, overrides: <Type, Generator>{
       ProcessManager: () => mockProcessManager,
     });