[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);