platform_plugins.dart 11.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// 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 'package:yaml/yaml.dart';

8 9 10
import 'base/common.dart';
import 'base/file_system.dart';

11 12 13 14 15 16
/// Constant for 'pluginClass' key in plugin maps.
const String kPluginClass = 'pluginClass';

/// Constant for 'pluginClass' key in plugin maps.
const String kDartPluginClass = 'dartPluginClass';

17
/// Marker interface for all platform specific plugin config implementations.
18 19 20 21 22 23
abstract class PluginPlatform {
  const PluginPlatform();

  Map<String, dynamic> toMap();
}

24 25 26 27 28 29
abstract class NativeOrDartPlugin {
  /// Determines whether the plugin has a native implementation or if it's a
  /// Dart-only plugin.
  bool isNative();
}

30 31 32 33 34
/// Contains parameters to template an Android plugin.
///
/// The required fields include: [name] of the plugin, [package] of the plugin and
/// the [pluginClass] that will be the entry point to the plugin's native code.
class AndroidPlugin extends PluginPlatform {
35
  AndroidPlugin({
36 37 38
    @required this.name,
    @required this.package,
    @required this.pluginClass,
39
    @required this.pluginPath,
40 41
    @required FileSystem fileSystem,
  }) : _fileSystem = fileSystem;
42

43
  factory AndroidPlugin.fromYaml(String name, YamlMap yaml, String pluginPath, FileSystem fileSystem) {
44 45 46
    assert(validate(yaml));
    return AndroidPlugin(
      name: name,
47 48
      package: yaml['package'] as String,
      pluginClass: yaml['pluginClass'] as String,
49
      pluginPath: pluginPath,
50
      fileSystem: fileSystem,
51 52 53
    );
  }

54 55
  final FileSystem _fileSystem;

56 57 58 59 60 61 62 63 64
  static bool validate(YamlMap yaml) {
    if (yaml == null) {
      return false;
    }
    return yaml['package'] is String && yaml['pluginClass'] is String;
  }

  static const String kConfigKey = 'android';

65
  /// The plugin name defined in pubspec.yaml.
66
  final String name;
67 68

  /// The plugin package name defined in pubspec.yaml.
69
  final String package;
70 71

  /// The plugin main class defined in pubspec.yaml.
72 73
  final String pluginClass;

74 75 76
  /// The absolute path to the plugin in the pub cache.
  final String pluginPath;

77 78 79 80 81 82
  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
      'package': package,
      'class': pluginClass,
83 84 85
      // Mustache doesn't support complex types.
      'supportsEmbeddingV1': _supportedEmbedings.contains('1'),
      'supportsEmbeddingV2': _supportedEmbedings.contains('2'),
86 87
    };
  }
88

89
  Set<String> _cachedEmbeddingVersion;
90 91

  /// Returns the version of the Android embedding.
92
  Set<String> get _supportedEmbedings => _cachedEmbeddingVersion ??= _getSupportedEmbeddings();
93

94
  Set<String> _getSupportedEmbeddings() {
95
    assert(pluginPath != null);
96
    final Set<String> supportedEmbeddings = <String>{};
97
    final String baseMainPath = _fileSystem.path.join(
98 99 100 101 102
      pluginPath,
      'android',
      'src',
      'main',
    );
103 104

    final List<String> mainClassCandidates = <String>[
105
      _fileSystem.path.join(
106 107
        baseMainPath,
        'java',
108
        package.replaceAll('.', _fileSystem.path.separator),
109
        '$pluginClass.java',
110
      ),
111
      _fileSystem.path.join(
112 113
        baseMainPath,
        'kotlin',
114
        package.replaceAll('.', _fileSystem.path.separator),
115
        '$pluginClass.kt',
116
      )
117 118 119 120 121
    ];

    File mainPluginClass;
    bool mainClassFound = false;
    for (final String mainClassCandidate in mainClassCandidates) {
122
      mainPluginClass = _fileSystem.file(mainClassCandidate);
123 124 125 126 127 128 129 130
      if (mainPluginClass.existsSync()) {
        mainClassFound = true;
        break;
      }
    }
    if (!mainClassFound) {
      assert(mainClassCandidates.length <= 2);
      throwToolExit(
131 132
        "The plugin `$name` doesn't have a main class defined in ${mainClassCandidates.join(' or ')}. "
        "This is likely to due to an incorrect `androidPackage: $package` or `mainClass` entry in the plugin's pubspec.yaml.\n"
133 134
        'If you are the author of this plugin, fix the `androidPackage` entry or move the main class to any of locations used above. '
        'Otherwise, please contact the author of this plugin and consider using a different plugin in the meanwhile. '
135 136
      );
    }
137

138
    final String mainClassContent = mainPluginClass.readAsStringSync();
139 140
    if (mainClassContent
        .contains('io.flutter.embedding.engine.plugins.FlutterPlugin')) {
141 142 143
      supportedEmbeddings.add('2');
    } else {
      supportedEmbeddings.add('1');
144
    }
145 146 147 148 149
    if (mainClassContent.contains('PluginRegistry')
        && mainClassContent.contains('registerWith')) {
      supportedEmbeddings.add('1');
    }
    return supportedEmbeddings;
150
  }
151 152 153 154 155 156 157 158 159 160 161 162 163 164
}

/// Contains the parameters to template an iOS plugin.
///
/// The required fields include: [name] of the plugin, the [pluginClass] that
/// will be the entry point to the plugin's native code.
class IOSPlugin extends PluginPlatform {
  const IOSPlugin({
    @required this.name,
    this.classPrefix,
    @required this.pluginClass,
  });

  factory IOSPlugin.fromYaml(String name, YamlMap yaml) {
165
    assert(validate(yaml)); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/67241
166 167 168
    return IOSPlugin(
      name: name,
      classPrefix: '',
169
      pluginClass: yaml['pluginClass'] as String,
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 198 199 200
    );
  }

  static bool validate(YamlMap yaml) {
    if (yaml == null) {
      return false;
    }
    return yaml['pluginClass'] is String;
  }

  static const String kConfigKey = 'ios';

  final String name;

  /// Note, this is here only for legacy reasons. Multi-platform format
  /// always sets it to empty String.
  final String classPrefix;
  final String pluginClass;

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
      'prefix': classPrefix,
      'class': pluginClass,
    };
  }
}

/// Contains the parameters to template a macOS plugin.
///
201 202 203
/// The [name] of the plugin is required. Either [dartPluginClass] or [pluginClass] are required.
/// [pluginClass] will be the entry point to the plugin's native code.
class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin {
204 205
  const MacOSPlugin({
    @required this.name,
206 207
    this.pluginClass,
    this.dartPluginClass,
208 209 210 211
  });

  factory MacOSPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));
212 213 214 215 216
    // Treat 'none' as not present. See https://github.com/flutter/flutter/issues/57497.
    String pluginClass = yaml[kPluginClass] as String;
    if (pluginClass == 'none') {
      pluginClass = null;
    }
217 218
    return MacOSPlugin(
      name: name,
219
      pluginClass: pluginClass,
220
      dartPluginClass: yaml[kDartPluginClass] as String,
221 222 223 224 225 226 227
    );
  }

  static bool validate(YamlMap yaml) {
    if (yaml == null) {
      return false;
    }
228
    return yaml[kPluginClass] is String || yaml[kDartPluginClass] is String;
229 230 231 232 233 234
  }

  static const String kConfigKey = 'macos';

  final String name;
  final String pluginClass;
235 236 237 238
  final String dartPluginClass;

  @override
  bool isNative() => pluginClass != null;
239 240 241 242 243

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
244 245
      if (pluginClass != null) 'class': pluginClass,
      if (dartPluginClass != null) 'dartPluginClass': dartPluginClass,
246 247 248
    };
  }
}
249

250 251
/// Contains the parameters to template a Windows plugin.
///
252 253 254
/// The [name] of the plugin is required. Either [dartPluginClass] or [pluginClass] are required.
/// [pluginClass] will be the entry point to the plugin's native code.
class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin{
255 256
  const WindowsPlugin({
    @required this.name,
257 258 259
    this.pluginClass,
    this.dartPluginClass,
  }) : assert(pluginClass != null || dartPluginClass != null);
260 261 262

  factory WindowsPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));
263 264 265 266 267
    // Treat 'none' as not present. See https://github.com/flutter/flutter/issues/57497.
    String pluginClass = yaml[kPluginClass] as String;
    if (pluginClass == 'none') {
      pluginClass = null;
    }
268 269
    return WindowsPlugin(
      name: name,
270
      pluginClass: pluginClass,
271
      dartPluginClass: yaml[kDartPluginClass] as String,
272 273 274 275 276 277 278
    );
  }

  static bool validate(YamlMap yaml) {
    if (yaml == null) {
      return false;
    }
279
    return yaml[kDartPluginClass] is String || yaml[kPluginClass] is String;
280 281 282 283 284 285
  }

  static const String kConfigKey = 'windows';

  final String name;
  final String pluginClass;
286 287 288 289
  final String dartPluginClass;

  @override
  bool isNative() => pluginClass != null;
290 291 292 293 294

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
295 296 297
      if (pluginClass != null) 'class': pluginClass,
      if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass),
      if (dartPluginClass != null) 'dartPluginClass': dartPluginClass,
298 299 300 301 302 303
    };
  }
}

/// Contains the parameters to template a Linux plugin.
///
304 305 306
/// The [name] of the plugin is required. Either [dartPluginClass] or [pluginClass] are required.
/// [pluginClass] will be the entry point to the plugin's native code.
class LinuxPlugin extends PluginPlatform implements NativeOrDartPlugin {
307 308
  const LinuxPlugin({
    @required this.name,
309 310 311
    this.pluginClass,
    this.dartPluginClass,
  }) : assert(pluginClass != null || dartPluginClass != null);
312 313 314

  factory LinuxPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));
315 316 317 318 319
    // Treat 'none' as not present. See https://github.com/flutter/flutter/issues/57497.
    String pluginClass = yaml[kPluginClass] as String;
    if (pluginClass == 'none') {
      pluginClass = null;
    }
320 321
    return LinuxPlugin(
      name: name,
322
      pluginClass: pluginClass,
323
      dartPluginClass: yaml[kDartPluginClass] as String,
324 325 326 327 328 329 330
    );
  }

  static bool validate(YamlMap yaml) {
    if (yaml == null) {
      return false;
    }
331
    return yaml[kPluginClass] is String || yaml[kDartPluginClass] is String;
332 333 334 335 336 337
  }

  static const String kConfigKey = 'linux';

  final String name;
  final String pluginClass;
338 339 340 341
  final String dartPluginClass;

  @override
  bool isNative() => pluginClass != null;
342 343 344 345 346

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
347 348 349
      if (pluginClass != null) 'class': pluginClass,
      if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass),
      if (dartPluginClass != null) 'dartPluginClass': dartPluginClass,
350 351 352 353
    };
  }
}

354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
/// Contains the parameters to template a web plugin.
///
/// The required fields include: [name] of the plugin, the [pluginClass] that will
/// be the entry point to the plugin's implementation, and the [fileName]
/// containing the code.
class WebPlugin extends PluginPlatform {
  const WebPlugin({
    @required this.name,
    @required this.pluginClass,
    @required this.fileName,
  });

  factory WebPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));
    return WebPlugin(
      name: name,
370 371
      pluginClass: yaml['pluginClass'] as String,
      fileName: yaml['fileName'] as String,
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
    );
  }

  static bool validate(YamlMap yaml) {
    if (yaml == null) {
      return false;
    }
    return yaml['pluginClass'] is String && yaml['fileName'] is String;
  }

  static const String kConfigKey = 'web';

  /// The name of the plugin.
  final String name;

  /// The class containing the plugin implementation details.
  ///
  /// This class should have a static `registerWith` method defined.
  final String pluginClass;

  /// The name of the file containing the class implementation above.
  final String fileName;

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
      'class': pluginClass,
      'file': fileName,
    };
  }
}
404 405 406 407 408 409 410 411

final RegExp _internalCapitalLetterRegex = RegExp(r'(?=(?!^)[A-Z])');
String _filenameForCppClass(String className) {
  return className.splitMapJoin(
    _internalCapitalLetterRegex,
    onMatch: (_) => '_',
    onNonMatch: (String n) => n.toLowerCase());
}