Materialize Flutter module, Android (#20520)

diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index 2473479..a473164 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -23,7 +23,7 @@
 import 'android_sdk.dart';
 import 'android_studio.dart';
 
-const String gradleVersion = '4.1';
+const String gradleVersion = '4.4';
 final RegExp _assembleTaskPattern = new RegExp(r'assemble([^:]+): task ');
 
 GradleProject _cachedGradleProject;
@@ -44,7 +44,7 @@
   r'|If you are using NDK, verify the ndk.dir is set to a valid NDK directory.  It is currently set to .*)');
 
 FlutterPluginVersion getFlutterPluginVersion(AndroidProject project) {
-  final File plugin = project.directory.childFile(
+  final File plugin = project.hostAppGradleRoot.childFile(
       fs.path.join('buildSrc', 'src', 'main', 'groovy', 'FlutterPlugin.groovy'));
   if (plugin.existsSync()) {
     final String packageLine = plugin.readAsLinesSync().skip(4).first;
@@ -53,8 +53,8 @@
     }
     return FlutterPluginVersion.v1;
   }
-  final File appGradle = project.directory.childFile(
-      fs.path.join('app','build.gradle'));
+  final File appGradle = project.hostAppGradleRoot.childFile(
+      fs.path.join('app', 'build.gradle'));
   if (appGradle.existsSync()) {
     for (String line in appGradle.readAsLinesSync()) {
       if (line.contains(new RegExp(r'apply from: .*/flutter.gradle'))) {
@@ -93,13 +93,13 @@
 Future<GradleProject> _readGradleProject() async {
   final FlutterProject flutterProject = await FlutterProject.current();
   final String gradle = await _ensureGradle(flutterProject);
-  await updateLocalProperties(project: flutterProject);
+  updateLocalProperties(project: flutterProject);
   final Status status = logger.startProgress('Resolving dependencies...', expectSlowOperation: true);
   GradleProject project;
   try {
     final RunResult runResult = await runCheckedAsync(
       <String>[gradle, 'app:properties'],
-      workingDirectory: flutterProject.android.directory.path,
+      workingDirectory: flutterProject.android.hostAppGradleRoot.path,
       environment: _gradleEnv,
     );
     final String properties = runResult.stdout.trim();
@@ -166,7 +166,7 @@
 // Note: Gradle may be bootstrapped and possibly downloaded as a side-effect
 // of validating the Gradle executable. This may take several seconds.
 Future<String> _initializeGradle(FlutterProject project) async {
-  final Directory android = project.android.directory;
+  final Directory android = project.android.hostAppGradleRoot;
   final Status status = logger.startProgress('Initializing gradle...', expectSlowOperation: true);
   String gradle = _locateGradlewExecutable(android);
   if (gradle == null) {
@@ -205,13 +205,13 @@
 ///
 /// If [requireAndroidSdk] is true (the default) and no Android SDK is found,
 /// this will fail with a [ToolExit].
-Future<void> updateLocalProperties({
+void updateLocalProperties({
   @required FlutterProject project,
   BuildInfo buildInfo,
   bool requireAndroidSdk = true,
-}) async {
-  if (requireAndroidSdk && androidSdk == null) {
-    throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.');
+}) {
+  if (requireAndroidSdk) {
+    _exitIfNoAndroidSdk();
   }
 
   final File localProperties = project.android.localPropertiesFile;
@@ -250,6 +250,24 @@
     settings.writeContents(localProperties);
 }
 
+/// Writes standard Android local properties to the specified [properties] file.
+///
+/// Writes the path to the Android SDK, if known.
+void writeLocalProperties(File properties) {
+  final SettingsFile settings = new SettingsFile();
+  if (androidSdk != null) {
+    settings.values['sdk.dir'] = escapePath(androidSdk.directory);
+  }
+  settings.writeContents(properties);
+}
+
+/// Throws a ToolExit, if the path to the Android SDK is not known.
+void _exitIfNoAndroidSdk() {
+  if (androidSdk == null) {
+    throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.');
+  }
+}
+
 Future<Null> buildGradleProject({
   @required FlutterProject project,
   @required BuildInfo buildInfo,
@@ -263,7 +281,7 @@
   // and can be overwritten with flutter build command.
   // The default Gradle script reads the version name and number
   // from the local.properties file.
-  await updateLocalProperties(project: project, buildInfo: buildInfo);
+  updateLocalProperties(project: project, buildInfo: buildInfo);
 
   final String gradle = await _ensureGradle(project);
 
@@ -284,7 +302,7 @@
   final Status status = logger.startProgress('Running \'gradlew build\'...', expectSlowOperation: true);
   final int exitCode = await runCommandAndStreamOutput(
     <String>[fs.file(gradle).absolute.path, 'build'],
-    workingDirectory: project.android.directory.path,
+    workingDirectory: project.android.hostAppGradleRoot.path,
     allowReentrantFlutter: true,
     environment: _gradleEnv,
   );
@@ -361,7 +379,7 @@
   command.add(assembleTask);
   final int exitCode = await runCommandAndStreamOutput(
     command,
-    workingDirectory: flutterProject.android.directory.path,
+    workingDirectory: flutterProject.android.hostAppGradleRoot.path,
     allowReentrantFlutter: true,
     environment: _gradleEnv,
     filter: logger.isVerbose ? null : ndkMessageFilter,
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index 27e99bf..ac7faf5 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -107,7 +107,7 @@
       apkFile = fs.file(fs.path.join(getAndroidBuildDirectory(), 'app.apk'));
     }
 
-    final File manifest = androidProject.gradleManifestFile;
+    final File manifest = androidProject.appManifestFile;
 
     if (!manifest.existsSync())
       return null;
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index b5f6cca..fb1a2c0 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -286,8 +286,7 @@
       );
     }
     final FlutterProject project = await FlutterProject.fromDirectory(directory);
-    if (android_sdk.androidSdk != null)
-      await gradle.updateLocalProperties(project: project);
+    gradle.updateLocalProperties(project: project, requireAndroidSdk: false);
 
     final String projectName = templateContext['projectName'];
     final String organization = templateContext['organization'];
@@ -320,8 +319,7 @@
       await project.ensureReadyForPlatformSpecificTooling();
     }
 
-    if (android_sdk.androidSdk != null)
-      await gradle.updateLocalProperties(project: project);
+    gradle.updateLocalProperties(project: project, requireAndroidSdk: false);
 
     return generatedCount;
   }
@@ -373,7 +371,7 @@
     int filesCreated = 0;
     copyDirectorySync(
       cache.getArtifactDirectory('gradle_wrapper'),
-      project.android.directory,
+      project.android.hostAppGradleRoot,
       (File sourceFile, File destinationFile) {
         filesCreated++;
         final String modes = sourceFile.statSync().modeString();
diff --git a/packages/flutter_tools/lib/src/commands/materialize.dart b/packages/flutter_tools/lib/src/commands/materialize.dart
new file mode 100644
index 0000000..0c0f27d
--- /dev/null
+++ b/packages/flutter_tools/lib/src/commands/materialize.dart
@@ -0,0 +1,78 @@
+// Copyright 2018 The Chromium 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 'dart:async';
+import 'package:meta/meta.dart';
+import '../base/common.dart';
+import '../project.dart';
+import '../runner/flutter_command.dart';
+
+class MaterializeCommand extends FlutterCommand {
+  MaterializeCommand() {
+    addSubcommand(new MaterializeAndroidCommand());
+    addSubcommand(new MaterializeIosCommand());
+  }
+
+  @override
+  final String name = 'materialize';
+
+  @override
+  final String description = 'Commands for materializing host apps for a Flutter Module';
+
+  @override
+  bool get hidden => true;
+
+  @override
+  Future<Null> runCommand() async { }
+}
+
+abstract class MaterializeSubCommand extends FlutterCommand {
+  MaterializeSubCommand() {
+    requiresPubspecYaml();
+  }
+
+  FlutterProject _project;
+
+  @override
+  @mustCallSuper
+  Future<Null> runCommand() async {
+    await _project.ensureReadyForPlatformSpecificTooling();
+  }
+
+  @override
+  Future<Null> validateCommand() async {
+    await super.validateCommand();
+    _project = await FlutterProject.current();
+    if (!_project.isModule)
+      throw new ToolExit("Only projects created using 'flutter create -t module' can be materialized.");
+  }
+}
+
+class MaterializeAndroidCommand extends MaterializeSubCommand {
+  @override
+  String get name => 'android';
+
+  @override
+  String get description => 'Materialize an Android host app';
+
+  @override
+  Future<Null> runCommand() async {
+    await super.runCommand();
+    await _project.android.materialize();
+  }
+}
+
+class MaterializeIosCommand extends MaterializeSubCommand {
+  @override
+  String get name => 'ios';
+
+  @override
+  String get description => 'Materialize an iOS host app';
+
+  @override
+  Future<Null> runCommand() async {
+    await super.runCommand();
+    await _project.ios.materialize();
+  }
+}
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart
index 1ec19a7..3d31b83 100644
--- a/packages/flutter_tools/lib/src/devfs.dart
+++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -329,7 +329,7 @@
 }
 
 class DevFS {
-  /// Create a [DevFS] named [fsName] for the local files in [directory].
+  /// Create a [DevFS] named [fsName] for the local files in [rootDirectory].
   DevFS(VMService serviceProtocol,
         this.fsName,
         this.rootDirectory, {
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart
index 8943720..9a6afe8 100644
--- a/packages/flutter_tools/lib/src/plugins.dart
+++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -105,7 +105,7 @@
   return oldContents != newContents;
 }
 
-/// Returns the contents of the `.flutter-plugins` file in [directory], or
+/// Returns the contents of the `.flutter-plugins` file in [project], or
 /// null if that file does not exist.
 String _readFlutterPluginsList(FlutterProject project) {
   return project.flutterPluginsFile.existsSync()
@@ -295,8 +295,7 @@
   }
 }
 
-/// Returns whether the Flutter project at the specified [directory]
-/// has any plugin dependencies.
+/// Returns whether the specified Flutter [project] has any plugin dependencies.
 bool hasPlugins(FlutterProject project) {
   return _readFlutterPluginsList(project) != null;
 }
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index 3c8743e..d54d8e9 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -186,6 +186,10 @@
     }
   }
 
+  Future<void> materialize() async {
+    throwToolExit('flutter materialize has not yet been implemented for iOS');
+  }
+
   bool _shouldRegenerateFromTemplate() {
     return Cache.instance.fileOlderThanToolsStamp(directory.childFile('podhelper.rb'));
   }
@@ -212,62 +216,112 @@
   /// The parent of this project.
   final FlutterProject parent;
 
-  /// The directory of this project.
-  Directory get directory => parent.directory.childDirectory(isModule ? '.android' : 'android');
+  /// The Gradle root directory of the Android host app. This is the directory
+  /// containing the `app/` subdirectory and the `settings.gradle` file that
+  /// includes it in the overall Gradle project.
+  Directory get hostAppGradleRoot {
+    if (!isModule || _materializedDirectory.existsSync())
+      return _materializedDirectory;
+    return _ephemeralDirectory;
+  }
+
+  /// The Gradle root directory of the Android wrapping of Flutter and plugins.
+  /// This is the same as [hostAppGradleRoot] except when the project is
+  /// a Flutter module with a materialized host app.
+  Directory get _flutterLibGradleRoot => isModule ? _ephemeralDirectory : _materializedDirectory;
+
+  Directory get _ephemeralDirectory => parent.directory.childDirectory('.android');
+  Directory get _materializedDirectory => parent.directory.childDirectory('android');
 
   /// True, if the parent Flutter project is a module.
   bool get isModule => parent.isModule;
 
-  File get gradleManifestFile {
+  File get appManifestFile {
     return isUsingGradle()
-        ? fs.file(fs.path.join(directory.path, 'app', 'src', 'main', 'AndroidManifest.xml'))
-        : directory.childFile('AndroidManifest.xml');
+        ? fs.file(fs.path.join(hostAppGradleRoot.path, 'app', 'src', 'main', 'AndroidManifest.xml'))
+        : hostAppGradleRoot.childFile('AndroidManifest.xml');
   }
 
   File get gradleAppOutV1File => gradleAppOutV1Directory.childFile('app-debug.apk');
 
   Directory get gradleAppOutV1Directory {
-    return fs.directory(fs.path.join(directory.path, 'app', 'build', 'outputs', 'apk'));
+    return fs.directory(fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'apk'));
   }
 
   bool isUsingGradle() {
-    return directory.childFile('build.gradle').existsSync();
+    return hostAppGradleRoot.childFile('build.gradle').existsSync();
   }
 
   Future<String> applicationId() {
-    final File gradleFile = directory.childDirectory('app').childFile('build.gradle');
+    final File gradleFile = hostAppGradleRoot.childDirectory('app').childFile('build.gradle');
     return _firstMatchInFile(gradleFile, _applicationIdPattern).then((Match match) => match?.group(1));
   }
 
   Future<String> group() {
-    final File gradleFile = directory.childFile('build.gradle');
+    final File gradleFile = hostAppGradleRoot.childFile('build.gradle');
     return _firstMatchInFile(gradleFile, _groupPattern).then((Match match) => match?.group(1));
   }
 
   Future<void> ensureReadyForPlatformSpecificTooling() async {
     if (isModule && _shouldRegenerateFromTemplate()) {
-      final Template template = new Template.fromName(fs.path.join('module', 'android'));
-      template.render(
-        directory,
-        <String, dynamic>{
-          'androidIdentifier': parent.manifest.androidPackage,
-        },
-        printStatusWhenWriting: false,
-      );
-      gradle.injectGradleWrapper(directory);
+      _regenerateLibrary();
+      // Add ephemeral host app, if a materialized host app does not already exist.
+      if (!_materializedDirectory.existsSync()) {
+        _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_common'), _ephemeralDirectory);
+        _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_ephemeral'), _ephemeralDirectory);
+      }
     }
-    if (!directory.existsSync())
+    if (!hostAppGradleRoot.existsSync()) {
       return;
-    await gradle.updateLocalProperties(project: parent, requireAndroidSdk: false);
+    }
+    gradle.updateLocalProperties(project: parent, requireAndroidSdk: false);
   }
 
   bool _shouldRegenerateFromTemplate() {
-    return Cache.instance.fileOlderThanToolsStamp(directory.childFile('build.gradle'));
+    return Cache.instance.fileOlderThanToolsStamp(_ephemeralDirectory.childFile('build.gradle'));
   }
 
-  File get localPropertiesFile => directory.childFile('local.properties');
+  Future<void> materialize() async {
+    assert(isModule);
+    if (_materializedDirectory.existsSync())
+      throwToolExit('Android host app already materialized. To redo materialization, delete the android/ folder.');
+    _regenerateLibrary();
+    _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_common'), _materializedDirectory);
+    _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_materialized'), _materializedDirectory);
+    _overwriteFromTemplate(fs.path.join('module', 'android', 'gradle'), _materializedDirectory);
+    gradle.injectGradleWrapper(_materializedDirectory);
+    gradle.writeLocalProperties(_materializedDirectory.childFile('local.properties'));
+    await injectPlugins(parent);
+  }
 
-  Directory get pluginRegistrantHost => directory.childDirectory(isModule ? 'Flutter' : 'app');
+  File get localPropertiesFile => _flutterLibGradleRoot.childFile('local.properties');
+
+  Directory get pluginRegistrantHost => _flutterLibGradleRoot.childDirectory(isModule ? 'Flutter' : 'app');
+
+  void _regenerateLibrary() {
+    _deleteIfExistsSync(_ephemeralDirectory);
+    _overwriteFromTemplate(fs.path.join('module', 'android', 'library'), _ephemeralDirectory);
+    _overwriteFromTemplate(fs.path.join('module', 'android', 'gradle'), _ephemeralDirectory);
+    gradle.injectGradleWrapper(_ephemeralDirectory);
+  }
+
+  void _deleteIfExistsSync(Directory directory) {
+    if (directory.existsSync())
+      directory.deleteSync(recursive: true);
+  }
+
+  void _overwriteFromTemplate(String path, Directory target) {
+    final Template template = new Template.fromName(path);
+    template.render(
+      target,
+      <String, dynamic>{
+        'projectName': parent.manifest.appName,
+        'androidIdentifier': parent.manifest.androidPackage,
+      },
+      printStatusWhenWriting: false,
+      overwriteExisting: true,
+    );
+  }
 }
 
 /// Asynchronously returns the first line-based match for [regExp] in [file].
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 03ba79b..b5f93b3 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -905,7 +905,7 @@
     case TargetPlatform.android_x64:
     case TargetPlatform.android_x86:
       final FlutterProject project = await FlutterProject.current();
-      final String manifestPath = fs.path.relative(project.android.gradleManifestFile.path);
+      final String manifestPath = fs.path.relative(project.android.appManifestFile.path);
       return 'Is your project missing an $manifestPath?\nConsider running "flutter create ." to create one.';
     case TargetPlatform.ios:
       return 'Is your project missing an ios/Runner/Info.plist?\nConsider running "flutter create ." to create one.';