From 5961bcc505b589328c3c20f0841ba467db3f966a Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Fri, 4 Oct 2019 06:23:03 -0700 Subject: [PATCH] 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 --- .../flutter_tools/lib/src/android/gradle.dart | 1 - .../lib/src/commands/create.dart | 1 + packages/flutter_tools/lib/src/features.dart | 14 + .../lib/src/platform_plugins.dart | 72 ++++- packages/flutter_tools/lib/src/plugins.dart | 162 ++++++++-- packages/flutter_tools/lib/src/project.dart | 7 +- .../androidIdentifier/MainActivity.java.tmpl | 20 ++ .../androidIdentifier/MainActivity.kt.tmpl | 20 +- .../app/src/main/AndroidManifest.xml.tmpl | 8 +- .../src/main/AndroidManifest.xml.tmpl | 7 + .../host/MainActivity.java.tmpl | 20 ++ .../Flutter.tmpl/build.gradle.tmpl | 55 ++++ .../Flutter.tmpl/flutter.iml.copy.tmpl | 26 ++ .../src/main/AndroidManifest.xml.tmpl | 16 + .../include_flutter.groovy.copy.tmpl | 48 +++ .../settings.gradle.copy.tmpl | 5 + .../androidIdentifier/pluginClass.java.tmpl | 33 ++ .../androidIdentifier/pluginClass.kt.tmpl | 31 ++ .../test/general.shard/plugins_test.dart | 287 +++++++++++++++++- packages/flutter_tools/test/src/testbed.dart | 4 + 20 files changed, 802 insertions(+), 35 deletions(-) create mode 100644 packages/flutter_tools/templates/module/android/library_new_embedding/Flutter.tmpl/build.gradle.tmpl create mode 100644 packages/flutter_tools/templates/module/android/library_new_embedding/Flutter.tmpl/flutter.iml.copy.tmpl create mode 100644 packages/flutter_tools/templates/module/android/library_new_embedding/Flutter.tmpl/src/main/AndroidManifest.xml.tmpl create mode 100644 packages/flutter_tools/templates/module/android/library_new_embedding/include_flutter.groovy.copy.tmpl create mode 100644 packages/flutter_tools/templates/module/android/library_new_embedding/settings.gradle.copy.tmpl diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index ed64c50957d..71205ffc369 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -940,7 +940,6 @@ Future _buildGradleProjectV2( /// 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 4c35620d0b0..849205a9c80 100644 --- a/packages/flutter_tools/lib/src/commands/create.dart +++ b/packages/flutter_tools/lib/src/commands/create.dart @@ -623,6 +623,7 @@ To edit platform code in an IDE see https://flutter.dev/developing-packages/#edi '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 170126739e4..b0d979c26a0 100644 --- a/packages/flutter_tools/lib/src/features.dart +++ b/packages/flutter_tools/lib/src/features.dart @@ -36,6 +36,9 @@ class FeatureFlags { /// 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 @@ const List allFeatures = [ flutterMacOSDesktopFeature, flutterWindowsDesktopFeature, flutterBuildPluginAsAarFeature, + flutterNewAndroidEmbeddingFeature, ]; /// The [Feature] for flutter web. @@ -126,6 +130,16 @@ const Feature flutterBuildPluginAsAarFeature = Feature( ), ); +/// 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 2b9976b4e97..aba20878739 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 @@ abstract class PluginPlatform { /// 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 @@ class AndroidPlugin extends PluginPlatform { 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 toMap() { return { '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 56ebd0d12fc..997188fa108 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 @@ class Plugin { } 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 @@ class Plugin { final Map platforms = {}; 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 @@ class Plugin { 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 @@ Plugin _pluginFromPubspec(String name, Uri packageRoot) { } 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 findPlugins(FlutterProject project) { final List plugins = []; Map 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 @@ String _readFlutterPluginsList(FlutterProject project) { : 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 @@ public final class GeneratedPluginRegistrant { } '''; +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> _extractPlatformMaps(List plugins, String type) { final List> pluginConfigs = >[]; for (Plugin p in plugins) { @@ -311,26 +357,92 @@ List> _extractPlatformMaps(List plugins, String typ return pluginConfigs; } -Future _writeAndroidPluginRegistrant(FlutterProject project, List plugins) async { - final List> androidPlugins = _extractPlatformMaps(plugins, AndroidPlugin.kConfigKey); - final Map context = { - '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 _writeAndroidPluginRegistrant(FlutterProject project, List plugins) async { + final List> androidPlugins = + _extractPlatformMaps(plugins, AndroidPlugin.kConfigKey); + + final Map templateContext = { + '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 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 dd5ce157caf..3b7ad27b17f 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -608,7 +608,11 @@ class AndroidProject { 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 @@ class AndroidProject { 'projectName': parent.manifest.appName, 'androidIdentifier': parent.manifest.androidPackage, 'androidX': usesAndroidX, + 'useNewAndroidEmbedding': featureFlags.isNewAndroidEmbeddingEnabled, }, printStatusWhenWriting: false, overwriteExisting: true, diff --git a/packages/flutter_tools/templates/app/android-java.tmpl/app/src/main/java/androidIdentifier/MainActivity.java.tmpl b/packages/flutter_tools/templates/app/android-java.tmpl/app/src/main/java/androidIdentifier/MainActivity.java.tmpl index 226d9ff4708..6506de14cac 100644 --- a/packages/flutter_tools/templates/app/android-java.tmpl/app/src/main/java/androidIdentifier/MainActivity.java.tmpl +++ b/packages/flutter_tools/templates/app/android-java.tmpl/app/src/main/java/androidIdentifier/MainActivity.java.tmpl @@ -1,5 +1,24 @@ package {{androidIdentifier}}; +{{#useNewAndroidEmbedding}} +{{#androidX}} +import androidx.annotation.NonNull; +{{/androidX}} +{{^androidX}} +import android.support.annotation.NonNull; +{{/androidX}} +import dev.flutter.plugins.GeneratedPluginRegistrant; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +public class MainActivity extends FlutterActivity { + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine); + } +} +{{/useNewAndroidEmbedding}} +{{^useNewAndroidEmbedding}} import android.os.Bundle; import io.flutter.app.FlutterActivity; import io.flutter.plugins.GeneratedPluginRegistrant; @@ -11,3 +30,4 @@ public class MainActivity extends FlutterActivity { GeneratedPluginRegistrant.registerWith(this); } } +{{/useNewAndroidEmbedding}} diff --git a/packages/flutter_tools/templates/app/android-kotlin.tmpl/app/src/main/kotlin/androidIdentifier/MainActivity.kt.tmpl b/packages/flutter_tools/templates/app/android-kotlin.tmpl/app/src/main/kotlin/androidIdentifier/MainActivity.kt.tmpl index ec654f4f442..fe04735c037 100644 --- a/packages/flutter_tools/templates/app/android-kotlin.tmpl/app/src/main/kotlin/androidIdentifier/MainActivity.kt.tmpl +++ b/packages/flutter_tools/templates/app/android-kotlin.tmpl/app/src/main/kotlin/androidIdentifier/MainActivity.kt.tmpl @@ -1,7 +1,24 @@ package {{androidIdentifier}} -import android.os.Bundle +{{#useNewAndroidEmbedding}} +{{#androidX}} +import androidx.annotation.NonNull; +{{/androidX}} +{{^androidX}} +import android.support.annotation.NonNull; +{{/androidX}} +import dev.flutter.plugins.GeneratedPluginRegistrant +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +class MainActivity: FlutterActivity() { + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine); + } +} +{{/useNewAndroidEmbedding}} +{{^useNewAndroidEmbedding}} +import android.os.Bundle import io.flutter.app.FlutterActivity import io.flutter.plugins.GeneratedPluginRegistrant @@ -11,3 +28,4 @@ class MainActivity: FlutterActivity() { GeneratedPluginRegistrant.registerWith(this) } } +{{/useNewAndroidEmbedding}} diff --git a/packages/flutter_tools/templates/app/android.tmpl/app/src/main/AndroidManifest.xml.tmpl b/packages/flutter_tools/templates/app/android.tmpl/app/src/main/AndroidManifest.xml.tmpl index 4778a2080e6..bc1305e0bb3 100644 --- a/packages/flutter_tools/templates/app/android.tmpl/app/src/main/AndroidManifest.xml.tmpl +++ b/packages/flutter_tools/templates/app/android.tmpl/app/src/main/AndroidManifest.xml.tmpl @@ -1,6 +1,5 @@ - + + {{/useNewAndroidEmbedding}} diff --git a/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/AndroidManifest.xml.tmpl b/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/AndroidManifest.xml.tmpl index a78b20a52d0..3ef4d25ac86 100644 --- a/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/AndroidManifest.xml.tmpl +++ b/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/AndroidManifest.xml.tmpl @@ -36,5 +36,12 @@ + {{#useNewAndroidEmbedding}} + + + {{/useNewAndroidEmbedding}} diff --git a/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/java/androidIdentifier/host/MainActivity.java.tmpl b/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/java/androidIdentifier/host/MainActivity.java.tmpl index 3441c3ad9a5..bc7daff8615 100644 --- a/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/java/androidIdentifier/host/MainActivity.java.tmpl +++ b/packages/flutter_tools/templates/module/android/host_app_common/app.tmpl/src/main/java/androidIdentifier/host/MainActivity.java.tmpl @@ -1,6 +1,25 @@ package {{androidIdentifier}}.host; import android.os.Bundle; +{{#useNewAndroidEmbedding}} +{{#androidX}} +import androidx.annotation.NonNull; +{{/androidX}} +{{^androidX}} +import android.support.annotation.NonNull; +{{/androidX}} +import dev.flutter.plugins.GeneratedPluginRegistrant; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +public class MainActivity extends FlutterActivity { + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine); + } +} +{{/useNewAndroidEmbedding}} +{{^useNewAndroidEmbedding}} import io.flutter.app.FlutterActivity; import io.flutter.plugins.GeneratedPluginRegistrant; @@ -11,3 +30,4 @@ public class MainActivity extends FlutterActivity { GeneratedPluginRegistrant.registerWith(this); } } +{{/useNewAndroidEmbedding}} diff --git a/packages/flutter_tools/templates/module/android/library_new_embedding/Flutter.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/module/android/library_new_embedding/Flutter.tmpl/build.gradle.tmpl new file mode 100644 index 00000000000..0e9b8fda52c --- /dev/null +++ b/packages/flutter_tools/templates/module/android/library_new_embedding/Flutter.tmpl/build.gradle.tmpl @@ -0,0 +1,55 @@ +// Generated file. Do not edit. + +def localProperties = new Properties() +def localPropertiesFile = new File(buildscript.sourceFile.parentFile.parentFile, 'local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.library' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +group '{{androidIdentifier}}' +version '1.0' + +android { + compileSdkVersion 28 + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + {{#androidX}} + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + {{/androidX}} + {{^androidX}} + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + {{/androidX}} + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.12' +} diff --git a/packages/flutter_tools/templates/module/android/library_new_embedding/Flutter.tmpl/flutter.iml.copy.tmpl b/packages/flutter_tools/templates/module/android/library_new_embedding/Flutter.tmpl/flutter.iml.copy.tmpl new file mode 100644 index 00000000000..29411bd15f9 --- /dev/null +++ b/packages/flutter_tools/templates/module/android/library_new_embedding/Flutter.tmpl/flutter.iml.copy.tmpl @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/flutter_tools/templates/module/android/library_new_embedding/Flutter.tmpl/src/main/AndroidManifest.xml.tmpl b/packages/flutter_tools/templates/module/android/library_new_embedding/Flutter.tmpl/src/main/AndroidManifest.xml.tmpl new file mode 100644 index 00000000000..7e9f4f0aba0 --- /dev/null +++ b/packages/flutter_tools/templates/module/android/library_new_embedding/Flutter.tmpl/src/main/AndroidManifest.xml.tmpl @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/packages/flutter_tools/templates/module/android/library_new_embedding/include_flutter.groovy.copy.tmpl b/packages/flutter_tools/templates/module/android/library_new_embedding/include_flutter.groovy.copy.tmpl new file mode 100644 index 00000000000..7be7efbaf8a --- /dev/null +++ b/packages/flutter_tools/templates/module/android/library_new_embedding/include_flutter.groovy.copy.tmpl @@ -0,0 +1,48 @@ +// Generated file. Do not edit. + +def scriptFile = getClass().protectionDomain.codeSource.location.toURI() +def flutterProjectRoot = new File(scriptFile).parentFile.parentFile + +gradle.include ':flutter' +gradle.project(':flutter').projectDir = new File(flutterProjectRoot, '.android/Flutter') + +if (System.getProperty('build-plugins-as-aars') != 'true') { + def plugins = new Properties() + def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins') + if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } + } + + plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.toPath().resolve(path).resolve('android').toFile() + gradle.include ":$name" + gradle.project(":$name").projectDir = pluginDirectory + } +} +gradle.getGradle().projectsLoaded { g -> + g.rootProject.beforeEvaluate { p -> + _mainModuleName = binding.variables['mainModuleName'] + if (_mainModuleName != null && !_mainModuleName.empty) { + p.ext.mainModuleName = _mainModuleName + } + def subprojects = [] + def flutterProject + p.subprojects { sp -> + if (sp.name == 'flutter') { + flutterProject = sp + } else { + subprojects.add(sp) + } + } + assert flutterProject != null + flutterProject.ext.hostProjects = subprojects + flutterProject.ext.pluginBuildDir = new File(flutterProjectRoot, 'build/host') + } + g.rootProject.afterEvaluate { p -> + p.subprojects { sp -> + if (sp.name != 'flutter') { + sp.evaluationDependsOn(':flutter') + } + } + } +} diff --git a/packages/flutter_tools/templates/module/android/library_new_embedding/settings.gradle.copy.tmpl b/packages/flutter_tools/templates/module/android/library_new_embedding/settings.gradle.copy.tmpl new file mode 100644 index 00000000000..22b1cdd0595 --- /dev/null +++ b/packages/flutter_tools/templates/module/android/library_new_embedding/settings.gradle.copy.tmpl @@ -0,0 +1,5 @@ +// Generated file. Do not edit. + +rootProject.name = 'android_generated' +setBinding(new Binding([gradle: this])) +evaluate(new File(settingsDir, 'include_flutter.groovy')) diff --git a/packages/flutter_tools/templates/plugin/android-java.tmpl/src/main/java/androidIdentifier/pluginClass.java.tmpl b/packages/flutter_tools/templates/plugin/android-java.tmpl/src/main/java/androidIdentifier/pluginClass.java.tmpl index 01bb87fc25f..f6c9d526dad 100644 --- a/packages/flutter_tools/templates/plugin/android-java.tmpl/src/main/java/androidIdentifier/pluginClass.java.tmpl +++ b/packages/flutter_tools/templates/plugin/android-java.tmpl/src/main/java/androidIdentifier/pluginClass.java.tmpl @@ -1,5 +1,37 @@ package {{androidIdentifier}}; +{{#useNewAndroidEmbedding}} +{{#androidX}} +import androidx.annotation.NonNull; +{{/androidX}} +{{^androidX}} +import android.support.annotation.NonNull; +{{/androidX}} +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; + +/** {{pluginClass}} */ +public class {{pluginClass}} implements FlutterPlugin, MethodCallHandler { + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { + final MethodChannel channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "{{projectName}}"); + channel.setMethodCallHandler(new {{pluginClass}}()); + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + if (call.method.equals("getPlatformVersion")) { + result.success("Android " + android.os.Build.VERSION.RELEASE); + } else { + result.notImplemented(); + } + } +} +{{/useNewAndroidEmbedding}} +{{^useNewAndroidEmbedding}} import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -23,3 +55,4 @@ public class {{pluginClass}} implements MethodCallHandler { } } } +{{/useNewAndroidEmbedding}} diff --git a/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/src/main/kotlin/androidIdentifier/pluginClass.kt.tmpl b/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/src/main/kotlin/androidIdentifier/pluginClass.kt.tmpl index 8fb848f8d47..af45d9c9449 100644 --- a/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/src/main/kotlin/androidIdentifier/pluginClass.kt.tmpl +++ b/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/src/main/kotlin/androidIdentifier/pluginClass.kt.tmpl @@ -1,5 +1,35 @@ package {{androidIdentifier}} +{{#useNewAndroidEmbedding}} +{{#androidX}} +import androidx.annotation.NonNull; +{{/androidX}} +{{^androidX}} +import android.support.annotation.NonNull; +{{/androidX}} +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** {{pluginClass}} */ +public class {{pluginClass}}: FlutterPlugin, MethodCallHandler { + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPluginBinding) { + val channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "{{projectName}}") + channel.setMethodCallHandler({{pluginClass}}()); + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (call.method == "getPlatformVersion") { + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else { + result.notImplemented() + } + } +} +{{/useNewAndroidEmbedding}} +{{^useNewAndroidEmbedding}} import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -23,3 +53,4 @@ class {{pluginClass}}: MethodCallHandler { } } } +{{/useNewAndroidEmbedding}} diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart index f119fc80ab7..1528828d389 100644 --- a/packages/flutter_tools/test/general.shard/plugins_test.dart +++ b/packages/flutter_tools/test/general.shard/plugins_test.dart @@ -5,22 +5,22 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/dart/package_map.dart'; +import 'package:flutter_tools/src/features.dart'; +import 'package:flutter_tools/src/ios/xcodeproj.dart'; import 'package:flutter_tools/src/plugins.dart'; import 'package:flutter_tools/src/project.dart'; + import 'package:mockito/mockito.dart'; import '../src/common.dart'; import '../src/context.dart'; -class MockFlutterProject extends Mock implements FlutterProject {} -class MockIosProject extends Mock implements IosProject {} -class MockMacOSProject extends Mock implements MacOSProject {} - void main() { FileSystem fs; MockFlutterProject flutterProject; MockIosProject iosProject; MockMacOSProject macosProject; + MockAndroidProject androidProject; File packagesFile; Directory dummyPackageDirectory; @@ -33,10 +33,17 @@ void main() { when(flutterProject.flutterPluginsFile).thenReturn(flutterProject.directory.childFile('.plugins')); iosProject = MockIosProject(); when(flutterProject.ios).thenReturn(iosProject); + when(iosProject.pluginRegistrantHost).thenReturn(flutterProject.directory.childDirectory('Runner')); + when(iosProject.podfile).thenReturn(flutterProject.directory.childDirectory('ios').childFile('Podfile')); when(iosProject.podManifestLock).thenReturn(flutterProject.directory.childDirectory('ios').childFile('Podfile.lock')); macosProject = MockMacOSProject(); when(flutterProject.macos).thenReturn(macosProject); + when(macosProject.podfile).thenReturn(flutterProject.directory.childDirectory('macos').childFile('Podfile')); when(macosProject.podManifestLock).thenReturn(flutterProject.directory.childDirectory('macos').childFile('Podfile.lock')); + androidProject = MockAndroidProject(); + when(flutterProject.android).thenReturn(androidProject); + when(androidProject.pluginRegistrantHost).thenReturn(flutterProject.directory.childDirectory('android').childDirectory('app')); + when(androidProject.hostAppGradleRoot).thenReturn(flutterProject.directory.childDirectory('android')); // Set up a simple .packages file for all the tests to use, pointing to one package. dummyPackageDirectory = fs.directory('/pubcache/apackage/lib/'); @@ -103,4 +110,276 @@ flutter: FileSystem: () => fs, }); }); + + group('injectPlugins', () { + MockFeatureFlags featureFlags; + MockXcodeProjectInterpreter xcodeProjectInterpreter; + + const String kAndroidManifestUsingOldEmbedding = ''' + + + + +'''; + const String kAndroidManifestUsingNewEmbedding = ''' + + + + + +'''; + + setUp(() { + featureFlags = MockFeatureFlags(); + when(featureFlags.isLinuxEnabled).thenReturn(false); + when(featureFlags.isMacOSEnabled).thenReturn(false); + when(featureFlags.isWindowsEnabled).thenReturn(false); + when(featureFlags.isWebEnabled).thenReturn(false); + + xcodeProjectInterpreter = MockXcodeProjectInterpreter(); + when(xcodeProjectInterpreter.isInstalled).thenReturn(false); + }); + + testUsingContext('Registrant uses old embedding in app project', () async { + when(flutterProject.isModule).thenReturn(false); + when(featureFlags.isNewAndroidEmbeddingEnabled).thenReturn(false); + + await injectPlugins(flutterProject); + + final File registrant = flutterProject.directory + .childDirectory(fs.path.join('android', 'app', 'src', 'main', 'java', 'io', 'flutter', 'plugins')) + .childFile('GeneratedPluginRegistrant.java'); + + expect(registrant.existsSync(), isTrue); + expect(registrant.readAsStringSync(), contains('package io.flutter.plugins')); + expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant')); + }, overrides: { + FileSystem: () => fs, + FeatureFlags: () => featureFlags, + }); + + testUsingContext('Registrant uses new embedding if app uses new embedding', () async { + when(flutterProject.isModule).thenReturn(false); + when(featureFlags.isNewAndroidEmbeddingEnabled).thenReturn(true); + + final File androidManifest = flutterProject.directory + .childDirectory('android') + .childFile('AndroidManifest.xml') + ..createSync(recursive: true) + ..writeAsStringSync(kAndroidManifestUsingNewEmbedding); + when(androidProject.appManifestFile).thenReturn(androidManifest); + + await injectPlugins(flutterProject); + + final File registrant = flutterProject.directory + .childDirectory(fs.path.join('android', 'app', 'src', 'main', 'java', 'dev', 'flutter', 'plugins')) + .childFile('GeneratedPluginRegistrant.java'); + + expect(registrant.existsSync(), isTrue); + expect(registrant.readAsStringSync(), contains('package dev.flutter.plugins')); + expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant')); + }, overrides: { + FileSystem: () => fs, + FeatureFlags: () => featureFlags, + }); + + testUsingContext('Registrant uses shim for plugins using old embedding if app uses new embedding', () async { + when(flutterProject.isModule).thenReturn(false); + when(featureFlags.isNewAndroidEmbeddingEnabled).thenReturn(true); + + final File androidManifest = flutterProject.directory + .childDirectory('android') + .childFile('AndroidManifest.xml') + ..createSync(recursive: true) + ..writeAsStringSync(kAndroidManifestUsingNewEmbedding); + when(androidProject.appManifestFile).thenReturn(androidManifest); + + final Directory pluginUsingJavaAndNewEmbeddingDir = + fs.systemTempDirectory.createTempSync('pluginUsingJavaAndNewEmbeddingDir.'); + pluginUsingJavaAndNewEmbeddingDir + .childFile('pubspec.yaml') + .writeAsStringSync(''' +flutter: + plugin: + androidPackage: plugin1 + pluginClass: UseNewEmbedding +'''); + pluginUsingJavaAndNewEmbeddingDir + .childDirectory('android') + .childDirectory('src') + .childDirectory('main') + .childDirectory('java') + .childDirectory('plugin1') + .childFile('UseNewEmbedding.java') + ..createSync(recursive: true) + ..writeAsStringSync('import io.flutter.embedding.engine.plugins.FlutterPlugin;'); + + final Directory pluginUsingKotlinAndNewEmbeddingDir = + fs.systemTempDirectory.createTempSync('pluginUsingKotlinAndNewEmbeddingDir.'); + pluginUsingKotlinAndNewEmbeddingDir + .childFile('pubspec.yaml') + .writeAsStringSync(''' +flutter: + plugin: + androidPackage: plugin2 + pluginClass: UseNewEmbedding +'''); + pluginUsingKotlinAndNewEmbeddingDir + .childDirectory('android') + .childDirectory('src') + .childDirectory('main') + .childDirectory('kotlin') + .childDirectory('plugin2') + .childFile('UseNewEmbedding.kt') + ..createSync(recursive: true) + ..writeAsStringSync('import io.flutter.embedding.engine.plugins.FlutterPlugin'); + + final Directory pluginUsingOldEmbeddingDir = + fs.systemTempDirectory.createTempSync('pluginUsingOldEmbeddingDir.'); + pluginUsingOldEmbeddingDir + .childFile('pubspec.yaml') + .writeAsStringSync(''' +flutter: + plugin: + androidPackage: plugin3 + pluginClass: UseOldEmbedding +'''); + pluginUsingOldEmbeddingDir + .childDirectory('android') + .childDirectory('src') + .childDirectory('main') + .childDirectory('java') + .childDirectory('plugin3') + .childFile('UseOldEmbedding.java') + ..createSync(recursive: true); + + flutterProject.directory + .childFile('.packages') + .writeAsStringSync(''' +plugin1:${pluginUsingJavaAndNewEmbeddingDir.childDirectory('lib').uri.toString()} +plugin2:${pluginUsingKotlinAndNewEmbeddingDir.childDirectory('lib').uri.toString()} +plugin3:${pluginUsingOldEmbeddingDir.childDirectory('lib').uri.toString()} +'''); + + await injectPlugins(flutterProject); + + final File registrant = flutterProject.directory + .childDirectory(fs.path.join('android', 'app', 'src', 'main', 'java', 'dev', 'flutter', 'plugins')) + .childFile('GeneratedPluginRegistrant.java'); + + expect(registrant.readAsStringSync(), + contains('flutterEngine.getPlugins().add(new plugin1.UseNewEmbedding());')); + expect(registrant.readAsStringSync(), + contains('flutterEngine.getPlugins().add(new plugin2.UseNewEmbedding());')); + expect(registrant.readAsStringSync(), + contains('plugin3.UseOldEmbedding.registerWith(shimPluginRegistry.registrarFor("plugin3.UseOldEmbedding"));')); + + }, overrides: { + FileSystem: () => fs, + FeatureFlags: () => featureFlags, + XcodeProjectInterpreter: () => xcodeProjectInterpreter, + }); + + testUsingContext('Registrant doesn\'t use new embedding if app doesn\'t use new embedding', () async { + when(flutterProject.isModule).thenReturn(false); + when(featureFlags.isNewAndroidEmbeddingEnabled).thenReturn(true); + + final File androidManifest = flutterProject.directory + .childDirectory('android') + .childFile('AndroidManifest.xml') + ..createSync(recursive: true) + ..writeAsStringSync(kAndroidManifestUsingOldEmbedding); + when(androidProject.appManifestFile).thenReturn(androidManifest); + + await injectPlugins(flutterProject); + + final File registrant = flutterProject.directory + .childDirectory(fs.path.join('android', 'app', 'src', 'main', 'java', 'io', 'flutter', 'plugins')) + .childFile('GeneratedPluginRegistrant.java'); + + expect(registrant.existsSync(), isTrue); + expect(registrant.readAsStringSync(), contains('package io.flutter.plugins')); + expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant')); + }, overrides: { + FileSystem: () => fs, + FeatureFlags: () => featureFlags, + }); + + testUsingContext('Registrant uses old embedding in module project', () async { + when(flutterProject.isModule).thenReturn(true); + when(featureFlags.isNewAndroidEmbeddingEnabled).thenReturn(false); + + await injectPlugins(flutterProject); + + final File registrant = flutterProject.directory + .childDirectory(fs.path.join('android', 'app', 'src', 'main', 'java', 'io', 'flutter', 'plugins')) + .childFile('GeneratedPluginRegistrant.java'); + + expect(registrant.existsSync(), isTrue); + expect(registrant.readAsStringSync(), contains('package io.flutter.plugins')); + expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant')); + }, overrides: { + FileSystem: () => fs, + FeatureFlags: () => featureFlags, + }); + + testUsingContext('Registrant uses new embedding if module uses new embedding', () async { + when(flutterProject.isModule).thenReturn(true); + when(featureFlags.isNewAndroidEmbeddingEnabled).thenReturn(true); + + final File androidManifest = flutterProject.directory + .childDirectory('android') + .childFile('AndroidManifest.xml') + ..createSync(recursive: true) + ..writeAsStringSync(kAndroidManifestUsingNewEmbedding); + when(androidProject.appManifestFile).thenReturn(androidManifest); + + await injectPlugins(flutterProject); + + final File registrant = flutterProject.directory + .childDirectory(fs.path.join('android', 'app', 'src', 'main', 'java', 'dev', 'flutter', 'plugins')) + .childFile('GeneratedPluginRegistrant.java'); + + expect(registrant.existsSync(), isTrue); + expect(registrant.readAsStringSync(), contains('package dev.flutter.plugins')); + expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant')); + }, overrides: { + FileSystem: () => fs, + FeatureFlags: () => featureFlags, + }); + + testUsingContext('Registrant doesn\'t use new embedding if module doesn\'t use new embedding', () async { + when(flutterProject.isModule).thenReturn(true); + when(featureFlags.isNewAndroidEmbeddingEnabled).thenReturn(true); + + final File androidManifest = flutterProject.directory + .childDirectory('android') + .childFile('AndroidManifest.xml') + ..createSync(recursive: true) + ..writeAsStringSync(kAndroidManifestUsingOldEmbedding); + when(androidProject.appManifestFile).thenReturn(androidManifest); + + await injectPlugins(flutterProject); + + final File registrant = flutterProject.directory + .childDirectory(fs.path.join('android', 'app', 'src', 'main', 'java', 'io', 'flutter', 'plugins')) + .childFile('GeneratedPluginRegistrant.java'); + + expect(registrant.existsSync(), isTrue); + expect(registrant.readAsStringSync(), contains('package io.flutter.plugins')); + expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant')); + }, overrides: { + FileSystem: () => fs, + FeatureFlags: () => featureFlags, + }); + }); } + +class MockAndroidProject extends Mock implements AndroidProject {} +class MockFeatureFlags extends Mock implements FeatureFlags {} +class MockFlutterProject extends Mock implements FlutterProject {} +class MockIosProject extends Mock implements IosProject {} +class MockMacOSProject extends Mock implements MacOSProject {} +class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {} diff --git a/packages/flutter_tools/test/src/testbed.dart b/packages/flutter_tools/test/src/testbed.dart index 33a27df49c1..4c2fae13787 100644 --- a/packages/flutter_tools/test/src/testbed.dart +++ b/packages/flutter_tools/test/src/testbed.dart @@ -689,6 +689,7 @@ class TestFeatureFlags implements FeatureFlags { this.isMacOSEnabled = false, this.isWebEnabled = false, this.isWindowsEnabled = false, + this.isNewAndroidEmbeddingEnabled = false, }); @override @@ -702,4 +703,7 @@ class TestFeatureFlags implements FeatureFlags { @override final bool isWindowsEnabled; + + @override + final bool isNewAndroidEmbeddingEnabled; }