blob: e4391b4d6b6888dccc00fc9955772e9f59175fa6 [file]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_tools_core/flutter_tools_core.dart';
import 'package:flutter_tools_extension/flutter_tools_extension.dart';
import '../base/logger.dart';
import 'extension_discovery.dart';
/// Represents a group of configuration settings provided by a specific extension.
class ExtensionSettingsGroup {
const ExtensionSettingsGroup({
required this.title,
required this.featureFlags,
required this.configOptions,
});
/// The human-readable title of the extension.
final String title;
/// Feature flags registered by the extension.
final List<FeatureFlag> featureFlags;
/// Config options registered by the extension.
final List<ConfigOption> configOptions;
/// Whether this group contains any feature flags or configuration options.
bool get isNotEmpty => featureFlags.isNotEmpty || configOptions.isNotEmpty;
/// Whether this group is empty.
bool get isEmpty => !isNotEmpty;
}
/// A host-side configuration manager that delegates feature flag and option queries to extensions.
class ExtensionConfiguration {
ExtensionConfiguration({required List<ConfigurationExtension> extensions, required Logger logger})
: extensions = List<ConfigurationExtension>.unmodifiable(extensions),
_logger = logger;
/// The active extension services executing configuration queries.
final List<ConfigurationExtension> extensions;
final Logger _logger;
/// Retrieves feature flags provided by all registered extensions.
Future<List<FeatureFlag>> fetchFeatureFlags() async {
_logger.printTrace(
'ExtensionConfiguration fetching feature flags across ${extensions.length} extension(s)...',
);
final List<List<FeatureFlag>> results = await Future.wait(
extensions.map((ConfigurationExtension ext) async {
try {
return await ext.getFeatureFlags();
} on Object catch (err, stack) {
_logger.printTrace(
'Failed to fetch feature flags from extension "${ext.title}": $err\n$stack',
);
return const <FeatureFlag>[];
}
}),
);
final flags = <FeatureFlag>[for (final list in results) ...list];
_logger.printTrace('ExtensionConfiguration retrieved ${flags.length} total feature flag(s).');
return flags;
}
/// Retrieves configuration options provided by all registered extensions.
Future<List<ConfigOption>> fetchConfigurations() async {
_logger.printTrace(
'ExtensionConfiguration fetching config options across ${extensions.length} extension(s)...',
);
final List<List<ConfigOption>> results = await Future.wait(
extensions.map((ConfigurationExtension ext) async {
try {
return await ext.getConfigurations();
} on Object catch (err, stack) {
_logger.printTrace(
'Failed to fetch config options from extension "${ext.title}": $err\n$stack',
);
return const <ConfigOption>[];
}
}),
);
final options = <ConfigOption>[for (final list in results) ...list];
_logger.printTrace(
'ExtensionConfiguration retrieved ${options.length} total config option(s).',
);
return options;
}
/// Retrieves configuration settings grouped by extension name.
Future<List<ExtensionSettingsGroup>> fetchExtensionSettings() async {
_logger.printTrace(
'ExtensionConfiguration fetching settings groups across ${extensions.length} extension(s)...',
);
final List<ExtensionSettingsGroup> groups = (await Future.wait(
extensions.map((ConfigurationExtension ext) async {
try {
final String title = ext is ConfigurationExtensionClient
? await ext.fetchTitle()
: ext.title;
final List<FeatureFlag> flags = await ext.getFeatureFlags();
final List<ConfigOption> options = await ext.getConfigurations();
return ExtensionSettingsGroup(title: title, featureFlags: flags, configOptions: options);
} on Object catch (err, stack) {
_logger.printTrace(
'Failed to fetch settings from extension "${ext.title}": $err\n$stack',
);
return null;
}
}),
)).whereType<ExtensionSettingsGroup>().toList();
_logger.printTrace('ExtensionConfiguration retrieved ${groups.length} settings group(s).');
return groups;
}
}
/// A host-side [ConfigurationExtension] client adapter that delegates RPC queries to an [ExtensionConnection].
class ConfigurationExtensionClient extends ConfigurationExtension {
ConfigurationExtensionClient(
this.connection, {
required Logger logger,
String defaultTitle = 'Tool Extension Configuration',
}) : _defaultTitle = defaultTitle,
_logger = logger;
/// The active extension isolate connection.
final ExtensionConnection connection;
final String _defaultTitle;
final Logger _logger;
String? _titleCache;
@override
String get title => _titleCache ?? _defaultTitle;
@override
Future<String> fetchTitle() async {
if (_titleCache != null) {
return _titleCache!;
}
_logger.printTrace(
'ConfigurationExtensionClient fetching title via RPC ("${ConfigurationExtension.getTitleMethod}")...',
);
try {
final Object? response = await connection.sendRequest(ConfigurationExtension.getTitleMethod);
if (response case final String title) {
_titleCache = title;
} else {
_logger.printTrace(
'ConfigurationExtensionClient received invalid title response: $response',
);
}
} on Object catch (err, stack) {
_logger.printTrace('ConfigurationExtensionClient failed to fetch title: $err\n$stack');
}
return _titleCache ?? _defaultTitle;
}
Future<List<T>> _fetchList<T>(
String method,
String entityName,
T Function(Map<String, Object?>) fromJson,
) async {
_logger.printTrace('ConfigurationExtensionClient fetching $entityName via RPC ("$method")...');
try {
final Object? rawResult = await connection.sendRequest(method);
if (rawResult case final List<Object?> list) {
final results = <T>[
for (final Object? element in list)
if (element case final Map<Object?, Object?> map) fromJson(map.cast<String, Object?>()),
];
_logger.printTrace(
'ConfigurationExtensionClient received ${results.length} $entityName via RPC.',
);
return results;
}
_logger.printTrace(
'ConfigurationExtensionClient received invalid or null $entityName response: $rawResult',
);
} on Object catch (err, stack) {
_logger.printTrace('ConfigurationExtensionClient failed to get $entityName: $err\n$stack');
}
return <T>[];
}
@override
Future<List<FeatureFlag>> getFeatureFlags() => _fetchList(
ConfigurationExtension.getFeatureFlagsMethod,
'feature flag(s)',
FeatureFlag.fromJson,
);
@override
Future<List<ConfigOption>> getConfigurations() => _fetchList(
ConfigurationExtension.getConfigurationsMethod,
'config option(s)',
ConfigOption.fromJson,
);
}