Generate projects using the new Android embedding (#41666) * Generate projects using the new Android embedding * Add comment about usesNewEmbedding:true * Feedback * Rework way to detect new embedding in new apps
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index ed64c50..71205ff 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -940,7 +940,6 @@ /// Returns [true] if the current app uses AndroidX. // TODO(egarciad): https://github.com/flutter/flutter/issues/40800 // Remove `FlutterManifest.usesAndroidX` and provide a unified `AndroidProject.usesAndroidX`. -@visibleForTesting bool isAppUsingAndroidX(Directory androidDirectory) { final File properties = androidDirectory.childFile('gradle.properties'); if (!properties.existsSync()) {
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart index 4c35620..849205a 100644 --- a/packages/flutter_tools/lib/src/commands/create.dart +++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -623,6 +623,7 @@ 'description': projectDescription, 'dartSdk': '$flutterRoot/bin/cache/dart-sdk', 'androidX': androidX, + 'useNewAndroidEmbedding': featureFlags.isNewAndroidEmbeddingEnabled, 'androidMinApiLevel': android.minApiLevel, 'androidSdkVersion': android_sdk.minimumAndroidSdkVersion, 'androidFlutterJar': '$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar',
diff --git a/packages/flutter_tools/lib/src/features.dart b/packages/flutter_tools/lib/src/features.dart index 1701267..b0d979c 100644 --- a/packages/flutter_tools/lib/src/features.dart +++ b/packages/flutter_tools/lib/src/features.dart
@@ -36,6 +36,9 @@ /// Whether flutter desktop for Windows is enabled. bool get isWindowsEnabled => _isEnabled(flutterWindowsDesktopFeature); + /// Whether the new Android embedding is enabled. + bool get isNewAndroidEmbeddingEnabled => _isEnabled(flutterNewAndroidEmbeddingFeature); + // Calculate whether a particular feature is enabled for the current channel. static bool _isEnabled(Feature feature) { final String currentChannel = FlutterVersion.instance.channel; @@ -66,6 +69,7 @@ flutterMacOSDesktopFeature, flutterWindowsDesktopFeature, flutterBuildPluginAsAarFeature, + flutterNewAndroidEmbeddingFeature, ]; /// The [Feature] for flutter web. @@ -126,6 +130,16 @@ ), ); +/// The [Feature] for generating projects using the new Android embedding. +const Feature flutterNewAndroidEmbeddingFeature = Feature( + name: 'flutter create generates projects using the new Android embedding', + configSetting: 'enable-new-android-embedding', + master: FeatureChannelSetting( + available: true, + enabledByDefault: false, + ), +); + /// A [Feature] is a process for conditionally enabling tool features. /// /// All settings are optional, and if not provided will generally default to
diff --git a/packages/flutter_tools/lib/src/platform_plugins.dart b/packages/flutter_tools/lib/src/platform_plugins.dart index 2b9976b..aba2087 100644 --- a/packages/flutter_tools/lib/src/platform_plugins.dart +++ b/packages/flutter_tools/lib/src/platform_plugins.dart
@@ -5,6 +5,10 @@ import 'package:meta/meta.dart'; import 'package:yaml/yaml.dart'; +import 'base/common.dart'; +import 'base/file_system.dart'; +import 'features.dart'; + /// Marker interface for all platform specific plugin config impls. abstract class PluginPlatform { const PluginPlatform(); @@ -17,18 +21,20 @@ /// 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 { - const AndroidPlugin({ + AndroidPlugin({ @required this.name, @required this.package, @required this.pluginClass, + @required this.pluginPath, }); - factory AndroidPlugin.fromYaml(String name, YamlMap yaml) { + factory AndroidPlugin.fromYaml(String name, YamlMap yaml, String pluginPath) { assert(validate(yaml)); return AndroidPlugin( name: name, package: yaml['package'], pluginClass: yaml['pluginClass'], + pluginPath: pluginPath, ); } @@ -41,18 +47,80 @@ static const String kConfigKey = 'android'; + /// The plugin name defined in pubspec.yaml. final String name; + + /// The plugin package name defined in pubspec.yaml. final String package; + + /// The plugin main class defined in pubspec.yaml. final String pluginClass; + /// The absolute path to the plugin in the pub cache. + final String pluginPath; + @override Map<String, dynamic> toMap() { return <String, dynamic>{ 'name': name, 'package': package, 'class': pluginClass, + 'usesEmbedding2': _embeddingVersion == '2', }; } + + String _cachedEmbeddingVersion; + + /// Returns the version of the Android embedding. + String get _embeddingVersion => _cachedEmbeddingVersion ??= _getEmbeddingVersion(); + + String _getEmbeddingVersion() { + if (!featureFlags.isNewAndroidEmbeddingEnabled) { + return '1'; + } + assert(pluginPath != null); + final String baseMainPath = fs.path.join( + pluginPath, + 'android', + 'src', + 'main', + ); + File mainPluginClass = fs.file( + fs.path.join( + baseMainPath, + 'java', + package.replaceAll('.', fs.path.separator), + '$pluginClass.java', + ) + ); + // Check if the plugin is implemented in Kotlin since the plugin's pubspec.yaml + // doesn't include this information. + if (!mainPluginClass.existsSync()) { + mainPluginClass = fs.file( + fs.path.join( + baseMainPath, + 'kotlin', + package.replaceAll('.', fs.path.separator), + '$pluginClass.kt', + ) + ); + } + assert(mainPluginClass.existsSync()); + String mainClassContent; + try { + mainClassContent = mainPluginClass.readAsStringSync(); + } on FileSystemException { + throwToolExit( + 'Couldn\'t read file $mainPluginClass even though it exists. ' + 'Please verify that this file has read permission and try again.' + ); + } + if (mainClassContent + .contains('io.flutter.embedding.engine.plugins.FlutterPlugin')) { + return '2'; + } + return '1'; + } } /// Contains the parameters to template an iOS plugin.
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart index 56ebd0d..997188f 100644 --- a/packages/flutter_tools/lib/src/plugins.dart +++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -5,8 +5,10 @@ import 'dart:async'; import 'package:mustache/mustache.dart' as mustache; +import 'package:xml/xml.dart' as xml; import 'package:yaml/yaml.dart'; +import 'android/gradle.dart'; import 'base/common.dart'; import 'base/file_system.dart'; import 'dart/package_map.dart'; @@ -63,9 +65,8 @@ } if (pluginYaml != null && pluginYaml['platforms'] != null) { return Plugin._fromMultiPlatformYaml(name, path, pluginYaml); - } else { - return Plugin._fromLegacyYaml(name, path, pluginYaml); // ignore: deprecated_member_use_from_same_package } + return Plugin._fromLegacyYaml(name, path, pluginYaml); // ignore: deprecated_member_use_from_same_package } factory Plugin._fromMultiPlatformYaml(String name, String path, dynamic pluginYaml) { @@ -79,8 +80,11 @@ final Map<String, PluginPlatform> platforms = <String, PluginPlatform>{}; if (platformsYaml[AndroidPlugin.kConfigKey] != null) { - platforms[AndroidPlugin.kConfigKey] = - AndroidPlugin.fromYaml(name, platformsYaml[AndroidPlugin.kConfigKey]); + platforms[AndroidPlugin.kConfigKey] = AndroidPlugin.fromYaml( + name, + platformsYaml[AndroidPlugin.kConfigKey], + path, + ); } if (platformsYaml[IOSPlugin.kConfigKey] != null) { @@ -122,12 +126,12 @@ if (pluginYaml != null && pluginClass != null) { final String androidPackage = pluginYaml['androidPackage']; if (androidPackage != null) { - platforms[AndroidPlugin.kConfigKey] = - AndroidPlugin( - name: name, - package: pluginYaml['androidPackage'], - pluginClass: pluginClass, - ); + platforms[AndroidPlugin.kConfigKey] = AndroidPlugin( + name: name, + package: pluginYaml['androidPackage'], + pluginClass: pluginClass, + pluginPath: path, + ); } final String iosPrefix = pluginYaml['iosPrefix'] ?? ''; @@ -221,14 +225,21 @@ } final String packageRootPath = fs.path.fromUri(packageRoot); printTrace('Found plugin $name at $packageRootPath'); - return Plugin.fromYaml(name, packageRootPath, flutterConfig['plugin']); + return Plugin.fromYaml( + name, + packageRootPath, + flutterConfig['plugin'], + ); } List<Plugin> findPlugins(FlutterProject project) { final List<Plugin> plugins = <Plugin>[]; Map<String, Uri> packages; try { - final String packagesFile = fs.path.join(project.directory.path, PackageMap.globalPackagesPath); + final String packagesFile = fs.path.join( + project.directory.path, + PackageMap.globalPackagesPath, + ); packages = PackageMap(packagesFile).map; } on FormatException catch (e) { printTrace('Invalid .packages file: $e'); @@ -269,7 +280,7 @@ : null; } -const String _androidPluginRegistryTemplate = '''package io.flutter.plugins; +const String _androidPluginRegistryTemplateOldEmbedding = '''package io.flutter.plugins; import io.flutter.plugin.common.PluginRegistry; {{#plugins}} @@ -300,6 +311,41 @@ } '''; +const String _androidPluginRegistryTemplateNewEmbedding = '''package dev.flutter.plugins; + +{{#androidX}} +import androidx.annotation.NonNull; +{{/androidX}} +{{^androidX}} +import android.support.annotation.NonNull; +{{/androidX}} +import io.flutter.embedding.engine.FlutterEngine; +{{#needsShim}} +import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry; +{{/needsShim}} + +/** + * Generated file. Do not edit. + * This file is generated by the Flutter tool based on the + * plugins that support the Android platform. + */ +public final class GeneratedPluginRegistrant { + public static void registerWith(@NonNull FlutterEngine flutterEngine) { +{{#needsShim}} + ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine); +{{/needsShim}} +{{#plugins}} + {{#usesEmbedding2}} + flutterEngine.getPlugins().add(new {{package}}.{{class}}()); + {{/usesEmbedding2}} + {{^usesEmbedding2}} + {{package}}.{{class}}.registerWith(shimPluginRegistry.registrarFor("{{package}}.{{class}}")); + {{/usesEmbedding2}} +{{/plugins}} + } +} +'''; + List<Map<String, dynamic>> _extractPlatformMaps(List<Plugin> plugins, String type) { final List<Map<String, dynamic>> pluginConfigs = <Map<String, dynamic>>[]; for (Plugin p in plugins) { @@ -311,26 +357,92 @@ return pluginConfigs; } -Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin> plugins) async { - final List<Map<String, dynamic>> androidPlugins = _extractPlatformMaps(plugins, AndroidPlugin.kConfigKey); - final Map<String, dynamic> context = <String, dynamic>{ - 'plugins': androidPlugins, - }; +/// Returns the version of the Android embedding that the current +/// [project] is using. +String _getAndroidEmbeddingVersion(FlutterProject project) { + if (!featureFlags.isNewAndroidEmbeddingEnabled) { + return '1'; + } + assert(project.android != null); + final File androidManifest = project.android.appManifestFile; + assert(androidManifest.existsSync()); + xml.XmlDocument document; + try { + document = xml.parse(androidManifest.readAsStringSync()); + } on xml.XmlParserException { + throwToolExit('Error parsing ${project.android.appManifestFile} ' + 'Please ensure that the android manifest is a valid XML document and try again.'); + } on FileSystemException { + throwToolExit('Error reading ${project.android.appManifestFile} even though it exists. ' + 'Please ensure that you have read permission to this file and try again.'); + } + for (xml.XmlElement metaData in document.findAllElements('meta-data')) { + final String name = metaData.getAttribute('android:name'); + if (name == 'flutterEmbedding') { + return metaData.getAttribute('android:value'); + } + } + return '1'; +} +Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin> plugins) async { + final List<Map<String, dynamic>> androidPlugins = + _extractPlatformMaps(plugins, AndroidPlugin.kConfigKey); + + final Map<String, dynamic> templateContext = <String, dynamic>{ + 'plugins': androidPlugins, + 'androidX': isAppUsingAndroidX(project.android.hostAppGradleRoot), + }; final String javaSourcePath = fs.path.join( project.android.pluginRegistrantHost.path, 'src', 'main', 'java', ); - final String registryPath = fs.path.join( - javaSourcePath, - 'io', - 'flutter', - 'plugins', - 'GeneratedPluginRegistrant.java', + + String registryPath; + String templateContent; + + final String appEmbeddingVersion = _getAndroidEmbeddingVersion(project); + switch (appEmbeddingVersion) { + case '2': + templateContext['needsShim'] = false; + // If a plugin is using an embedding version older than 2.0 and the app is using 2.0, + // then add shim for the old plugins. + for (Map<String, dynamic> plugin in androidPlugins) { + if (!plugin['usesEmbedding2']) { + templateContext['needsShim'] = true; + break; + } + } + registryPath = fs.path.join( + javaSourcePath, + 'dev', + 'flutter', + 'plugins', + 'GeneratedPluginRegistrant.java', + ); + templateContent = _androidPluginRegistryTemplateNewEmbedding; + break; + case '1': + registryPath = fs.path.join( + javaSourcePath, + 'io', + 'flutter', + 'plugins', + 'GeneratedPluginRegistrant.java', + ); + templateContent = _androidPluginRegistryTemplateOldEmbedding; + break; + default: + throwToolExit('Unsupported Android embedding'); + } + printTrace('Generating $registryPath'); + _renderTemplateToFile( + templateContent, + templateContext, + registryPath, ); - _renderTemplateToFile(_androidPluginRegistryTemplate, context, registryPath); } const String _objcPluginRegistryHeaderTemplate = '''//
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index dd5ce15..3b7ad27 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart
@@ -608,7 +608,11 @@ void _regenerateLibrary() { _deleteIfExistsSync(ephemeralDirectory); - _overwriteFromTemplate(fs.path.join('module', 'android', 'library'), ephemeralDirectory); + _overwriteFromTemplate(fs.path.join( + 'module', + 'android', + featureFlags.isNewAndroidEmbeddingEnabled ? 'library_new_embedding' : 'library', + ), ephemeralDirectory); _overwriteFromTemplate(fs.path.join('module', 'android', 'gradle'), ephemeralDirectory); gradle.injectGradleWrapperIfNeeded(ephemeralDirectory); } @@ -621,6 +625,7 @@ 'projectName': parent.manifest.appName, 'androidIdentifier': parent.manifest.androidPackage, 'androidX': usesAndroidX, + 'useNewAndroidEmbedding': featureFlags.isNewAndroidEmbeddingEnabled, }, printStatusWhenWriting: false, overwriteExisting: true,