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,