// 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:meta/meta.dart'; import '../base/config.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; import '../base/platform.dart'; import '../cache.dart'; import 'custom_device_config.dart'; /// Represents the custom devices config file on disk which in turn /// contains a list of individual custom device configs. class CustomDevicesConfig { /// Load a [CustomDevicesConfig] from a (possibly non-existent) location on disk. /// /// The config is loaded on construction. Any error while loading will be logged /// but will not result in an exception being thrown. The file will not be deleted /// when it's not valid JSON (which other configurations do) and will not /// be implicitly created when it doesn't exist. CustomDevicesConfig({ required Platform platform, required FileSystem fileSystem, required Logger logger, }) : _platform = platform, _fileSystem = fileSystem, _logger = logger, _configLoader = (() => Config.managed( _kCustomDevicesConfigName, fileSystem: fileSystem, logger: logger, platform: platform, )); @visibleForTesting CustomDevicesConfig.test({ required FileSystem fileSystem, required Logger logger, Directory? directory, Platform? platform, }) : _platform = platform ?? FakePlatform(), _fileSystem = fileSystem, _logger = logger, _configLoader = (() => Config.test( name: _kCustomDevicesConfigName, directory: directory, logger: logger, managed: true )); static const String _kCustomDevicesConfigName = 'custom_devices.json'; static const String _kCustomDevicesConfigKey = 'custom-devices'; static const String _kSchema = r'$schema'; static const String _kCustomDevices = 'custom-devices'; final Platform _platform; final FileSystem _fileSystem; final Logger _logger; final Config Function() _configLoader; // When the custom devices feature is disabled, CustomDevicesConfig is // constructed anyway. So loading the config in the constructor isn't a good // idea. (The Config ctor logs any errors) // // I also didn't want to introduce a FeatureFlags argument to the constructor // and conditionally load the config when the feature is enabled, because // sometimes we need that Config object even when the feature is disabled. // For example inside ensureFileExists, which is used when enabling // the feature. // // Instead, users of this config should handle the feature flags. So for // example don't get [devices] when the feature is disabled. Config? __config; Config get _config { __config ??= _configLoader(); return __config!; } String get _defaultSchema { final Uri uri = _fileSystem .directory(Cache.flutterRoot) .childDirectory('packages') .childDirectory('flutter_tools') .childDirectory('static') .childFile('custom-devices.schema.json') .uri; // otherwise it won't contain the Uri schema, so the file:// at the start // will be missing assert(uri.isAbsolute); return uri.toString(); } /// Ensure the config file exists on disk by creating one with default values /// if it doesn't exist yet. /// /// The config file should always be present so we can give the user a path /// to a file they can edit. void ensureFileExists() { if (!_fileSystem.file(_config.configPath).existsSync()) { _config.setValue(_kSchema, _defaultSchema); _config.setValue(_kCustomDevices, <dynamic>[ CustomDeviceConfig.getExampleForPlatform(_platform).toJson(), ]); } } List<dynamic>? _getDevicesJsonValue() { final dynamic json = _config.getValue(_kCustomDevicesConfigKey); if (json == null) { return null; } else if (json is! List) { const String msg = "Could not load custom devices config. config['$_kCustomDevicesConfigKey'] is not a JSON array."; _logger.printError(msg); throw const CustomDeviceRevivalException(msg); } return json; } /// Get the list of [CustomDeviceConfig]s that are listed in the config file /// including disabled ones. /// /// Throws an Exception when the config could not be loaded and logs any /// errors. List<CustomDeviceConfig> get devices { final List<dynamic>? typedListNullable = _getDevicesJsonValue(); if (typedListNullable == null) { return <CustomDeviceConfig>[]; } final List<dynamic> typedList = typedListNullable; final List<CustomDeviceConfig> revived = <CustomDeviceConfig>[]; for (final MapEntry<int, dynamic> entry in typedList.asMap().entries) { try { revived.add(CustomDeviceConfig.fromJson(entry.value)); } on CustomDeviceRevivalException catch (e) { final String msg = 'Could not load custom device from config index ${entry.key}: $e'; _logger.printError(msg); throw CustomDeviceRevivalException(msg); } } return revived; } /// Get the list of [CustomDeviceConfig]s that are listed in the config file /// including disabled ones. /// /// Returns an empty list when the config could not be loaded and logs any /// errors. List<CustomDeviceConfig> tryGetDevices() { try { return devices; } on Exception { // any Exceptions are logged by [devices] already. return <CustomDeviceConfig>[]; } } /// Set the list of [CustomDeviceConfig]s in the config file. /// /// It should generally be avoided to call this often, since this could mean /// data loss. If you want to add or remove a device from the config, /// consider using [add] or [remove]. set devices(List<CustomDeviceConfig> configs) { _config.setValue( _kCustomDevicesConfigKey, configs.map<dynamic>((CustomDeviceConfig c) => c.toJson()).toList() ); } /// Add a custom device to the config file. /// /// Works even when some of the custom devices in the config file are not /// valid. /// /// May throw a [CustomDeviceRevivalException] if `config['custom-devices']` /// is not a list. void add(CustomDeviceConfig config) { _config.setValue( _kCustomDevicesConfigKey, <dynamic>[ ...?_getDevicesJsonValue(), config.toJson(), ] ); } /// Returns true if the config file contains a device with id [deviceId]. bool contains(String deviceId) { return devices.any((CustomDeviceConfig device) => device.id == deviceId); } /// Removes the first device with this device id from the config file. /// /// Returns true if the device was successfully removed, false if a device /// with this id could not be found. bool remove(String deviceId) { final List<CustomDeviceConfig> modifiedDevices = devices; // we use this instead of filtering so we can detect if we actually removed // anything. final CustomDeviceConfig? device = modifiedDevices .cast<CustomDeviceConfig?>() .firstWhere((CustomDeviceConfig? d) => d!.id == deviceId, orElse: () => null ); if (device == null) { return false; } modifiedDevices.remove(device); devices = modifiedDevices; return true; } String get configPath => _config.configPath; }