| // 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, |
| ); |
| } |