blob: cb68c6cfe34715ed84572b665b28c6fa361fdee5 [file] [log] [blame]
// 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.
required Platform platform,
required FileSystem fileSystem,
required Logger logger,
}) : _platform = platform,
_fileSystem = fileSystem,
_logger = logger,
_configLoader = (() => Config.managed(
fileSystem: fileSystem,
logger: logger,
platform: platform,
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
// otherwise it won't contain the Uri schema, so the file:// at the start
// will be missing
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>[
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.";
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 {
} on CustomDeviceRevivalException catch (e) {
final String msg = 'Could not load custom device from config index ${entry.key}: $e';
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) {
_kCustomDevicesConfigKey,<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) {
/// Returns true if the config file contains a device with id [deviceId].
bool contains(String deviceId) {
return devices.any((CustomDeviceConfig device) => == 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
.firstWhere((CustomDeviceConfig? d) => d!.id == deviceId,
orElse: () => null
if (device == null) {
return false;
devices = modifiedDevices;
return true;
String get configPath => _config.configPath;