custom_devices_config.dart 7.28 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// 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({
24
    required Platform platform,
25 26
    required FileSystem fileSystem,
    required Logger logger,
27 28 29 30
  }) : _platform = platform,
       _fileSystem = fileSystem,
       _logger = logger,
       _configLoader = (() => Config.managed(
31 32 33 34
         _kCustomDevicesConfigName,
         fileSystem: fileSystem,
         logger: logger,
         platform: platform,
35
       ));
36 37 38

  @visibleForTesting
  CustomDevicesConfig.test({
39
    required FileSystem fileSystem,
40
    required Logger logger,
41
    Directory? directory,
42 43 44 45 46
    Platform? platform,
  }) : _platform = platform ?? FakePlatform(),
       _fileSystem = fileSystem,
       _logger = logger,
       _configLoader = (() => Config.test(
47 48 49
         name: _kCustomDevicesConfigName,
         directory: directory,
         logger: logger,
50 51
         managed: true
       ));
52 53 54 55 56 57

  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';

58
  final Platform _platform;
59
  final FileSystem _fileSystem;
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
  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!;
  }
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104

  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);
105 106 107
      _config.setValue(_kCustomDevices, <dynamic>[
        CustomDeviceConfig.getExampleForPlatform(_platform).toJson(),
      ]);
108 109 110
    }
  }

111 112 113 114 115 116 117 118 119 120 121 122 123 124
  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;
  }

125 126 127
  /// Get the list of [CustomDeviceConfig]s that are listed in the config file
  /// including disabled ones.
  ///
128 129
  /// Throws an Exception when the config could not be loaded and logs any
  /// errors.
130
  List<CustomDeviceConfig> get devices {
131 132 133 134
    final List<dynamic>? typedListNullable = _getDevicesJsonValue();
    if (typedListNullable == null) {
      return <CustomDeviceConfig>[];
    }
135

136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
    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.
161 162
      return <CustomDeviceConfig>[];
    }
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
  }

  /// 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(),
189
        config.toJson(),
190 191 192 193 194 195 196 197
      ]
    );
  }

  /// Returns true if the config file contains a device with id [deviceId].
  bool contains(String deviceId) {
    return devices.any((CustomDeviceConfig device) => device.id == deviceId);
  }
198

199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
  /// 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;
    }
217

218 219 220
    modifiedDevices.remove(device);
    devices = modifiedDevices;
    return true;
221 222
  }

223
  String get configPath => _config.configPath;
224
}