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 189 190 191 192 193 194 195 196 197
  }

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