Add module template for Android (#18697)

diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index 1cefa1f..833ae58 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -141,16 +141,13 @@
   }
 }
 
-String _locateProjectGradlew({ bool ensureExecutable = true }) {
-  final String path = fs.path.join(
-    'android',
+String _locateGradlewExecutable(Directory directory) {
+  final File gradle = directory.childFile(
     platform.isWindows ? 'gradlew.bat' : 'gradlew',
   );
 
-  if (fs.isFileSync(path)) {
-    final File gradle = fs.file(path);
-    if (ensureExecutable)
-      os.makeExecutable(gradle);
+  if (gradle.existsSync()) {
+    os.makeExecutable(gradle);
     return gradle.absolute.path;
   } else {
     return null;
@@ -165,11 +162,12 @@
 // 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() async {
+  final Directory android = fs.directory('android');
   final Status status = logger.startProgress('Initializing gradle...', expectSlowOperation: true);
-  String gradle = _locateProjectGradlew();
+  String gradle = _locateGradlewExecutable(android);
   if (gradle == null) {
-    _injectGradleWrapper();
-    gradle = _locateProjectGradlew();
+    injectGradleWrapper(android);
+    gradle = _locateGradlewExecutable(android);
   }
   if (gradle == null)
     throwToolExit('Unable to locate gradlew script');
@@ -181,11 +179,13 @@
   return gradle;
 }
 
-void _injectGradleWrapper() {
-  copyDirectorySync(cache.getArtifactDirectory('gradle_wrapper'), fs.directory('android'));
-  final String propertiesPath = fs.path.join('android', 'gradle', 'wrapper', 'gradle-wrapper.properties');
-  if (!fs.file(propertiesPath).existsSync()) {
-    fs.file(propertiesPath).writeAsStringSync('''
+/// Injects the Gradle wrapper into the specified directory.
+void injectGradleWrapper(Directory directory) {
+  copyDirectorySync(cache.getArtifactDirectory('gradle_wrapper'), directory);
+  _locateGradlewExecutable(directory);
+  final File propertiesFile = directory.childFile(fs.path.join('gradle', 'wrapper', 'gradle-wrapper.properties'));
+  if (!propertiesFile.existsSync()) {
+    propertiesFile.writeAsStringSync('''
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
@@ -196,14 +196,31 @@
   }
 }
 
-/// Create android/local.properties if needed, and update Flutter settings.
+/// Overwrite android/local.properties in the specified Flutter project, if needed.
+///
+/// Throws, if `pubspec.yaml` or Android SDK cannot be located.
 Future<void> updateLocalProperties({String projectPath, BuildInfo buildInfo}) async {
-  final File localProperties = (projectPath == null)
-      ? fs.file(fs.path.join('android', 'local.properties'))
-      : fs.file(fs.path.join(projectPath, 'android', 'local.properties'));
+  final Directory android = (projectPath == null)
+      ? fs.directory('android')
+      : fs.directory(fs.path.join(projectPath, 'android'));
   final String flutterManifest = (projectPath == null)
       ? fs.path.join(bundle.defaultManifestPath)
       : fs.path.join(projectPath, bundle.defaultManifestPath);
+  if (androidSdk == null) {
+    throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.');
+  }
+  FlutterManifest manifest;
+  try {
+    manifest = await FlutterManifest.createFromPath(flutterManifest);
+  } catch (error) {
+    throwToolExit('Failed to load pubspec.yaml: $error');
+  }
+  updateLocalPropertiesSync(android, manifest, buildInfo);
+}
+
+/// Overwrite local.properties in the specified directory, if needed.
+void updateLocalPropertiesSync(Directory android, FlutterManifest manifest, [BuildInfo buildInfo]) {
+  final File localProperties = android.childFile('local.properties');
   bool changed = false;
 
   SettingsFile settings;
@@ -211,40 +228,27 @@
     settings = new SettingsFile.parseFromFile(localProperties);
   } else {
     settings = new SettingsFile();
-    if (androidSdk == null) {
-      throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.');
+    changed = true;
+  }
+
+  void changeIfNecessary(String key, String value) {
+    if (settings.values[key] != value) {
+      settings.values[key] = value;
+      changed = true;
     }
-    settings.values['sdk.dir'] = escapePath(androidSdk.directory);
-    changed = true;
-  }
-  final String escapedRoot = escapePath(Cache.flutterRoot);
-  if (changed || settings.values['flutter.sdk'] != escapedRoot) {
-    settings.values['flutter.sdk'] = escapedRoot;
-    changed = true;
-  }
-  if (buildInfo != null && settings.values['flutter.buildMode'] != buildInfo.modeName) {
-    settings.values['flutter.buildMode'] = buildInfo.modeName;
-    changed = true;
   }
 
-  FlutterManifest manifest;
-  try {
-    manifest = await FlutterManifest.createFromPath(flutterManifest);
-  } catch (error) {
-    throwToolExit('Failed to load pubspec.yaml: $error');
-  }
-
+  if (androidSdk != null)
+    changeIfNecessary('sdk.dir', escapePath(androidSdk.directory));
+  changeIfNecessary('flutter.sdk', escapePath(Cache.flutterRoot));
+  if (buildInfo != null)
+    changeIfNecessary('flutter.buildMode', buildInfo.modeName);
   final String buildName = buildInfo?.buildName ?? manifest.buildName;
-  if (buildName != null) {
-    settings.values['flutter.versionName'] = buildName;
-    changed = true;
-  }
-
+  if (buildName != null)
+    changeIfNecessary('flutter.versionName', buildName);
   final int buildNumber = buildInfo?.buildNumber ?? manifest.buildNumber;
-  if (buildNumber != null) {
-    settings.values['flutter.versionCode'] = '$buildNumber';
-    changed = true;
-  }
+  if (buildNumber != null)
+    changeIfNecessary('flutter.versionCode', '$buildNumber');
 
   if (changed)
     settings.writeContents(localProperties);
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index 04e0000..3b3aefb 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -44,7 +44,7 @@
     argParser.addOption(
       'template',
       abbr: 't',
-      allowed: <String>['app', 'package', 'plugin'],
+      allowed: <String>['app', 'module', 'package', 'plugin'],
       help: 'Specify the type of project to create.',
       valueHelp: 'type',
       allowedHelp: <String, String>{
@@ -124,6 +124,7 @@
       throwToolExit('Unable to find package:flutter_driver in $flutterDriverPackagePath', exitCode: 2);
 
     final String template = argResults['template'];
+    final bool generateModule = template == 'module';
     final bool generatePlugin = template == 'plugin';
     final bool generatePackage = template == 'package';
 
@@ -172,6 +173,9 @@
       case 'app':
         generatedFileCount += await _generateApp(dirPath, templateContext);
         break;
+      case 'module':
+        generatedFileCount += await _generateModule(dirPath, templateContext);
+        break;
       case 'package':
         generatedFileCount += await _generatePackage(dirPath, templateContext);
         break;
@@ -185,6 +189,9 @@
     if (generatePackage) {
       final String relativePath = fs.path.relative(dirPath);
       printStatus('Your package code is in lib/${templateContext['projectName']}.dart in the $relativePath directory.');
+    } else if (generateModule) {
+      final String relativePath = fs.path.relative(dirPath);
+      printStatus('Your module code is in lib/main.dart in the $relativePath directory.');
     } else {
       // Run doctor; tell the user the next steps.
       final String relativeAppPath = fs.path.relative(appPath);
@@ -226,6 +233,25 @@
     }
   }
 
+  Future<int> _generateModule(String dirPath, Map<String, dynamic> templateContext) async {
+    int generatedCount = 0;
+    final String description = argResults.wasParsed('description')
+        ? argResults['description']
+        : 'A new flutter module project.';
+    templateContext['description'] = description;
+    generatedCount += _renderTemplate(fs.path.join('module', 'common'), dirPath, templateContext);
+    if (argResults['pub']) {
+      await pubGet(
+        context: PubContext.create,
+        directory: dirPath,
+        offline: argResults['offline'],
+      );
+      final FlutterProject project = new FlutterProject(fs.directory(dirPath));
+      await project.ensureReadyForPlatformSpecificTooling();
+    }
+    return generatedCount;
+  }
+
   Future<int> _generatePackage(String dirPath, Map<String, dynamic> templateContext) async {
     int generatedCount = 0;
     final String description = argResults.wasParsed('description')
diff --git a/packages/flutter_tools/lib/src/flutter_manifest.dart b/packages/flutter_tools/lib/src/flutter_manifest.dart
index c151839..80fef7a 100644
--- a/packages/flutter_tools/lib/src/flutter_manifest.dart
+++ b/packages/flutter_tools/lib/src/flutter_manifest.dart
@@ -87,6 +87,23 @@
     return _flutterDescriptor['uses-material-design'] ?? false;
   }
 
+  /// Properties defining how to expose this Flutter project as a module
+  /// for integration into an unspecified host app.
+  Map<String, dynamic> get moduleDescriptor {
+    return _flutterDescriptor.containsKey('module')
+        ? _flutterDescriptor['module'] ?? const <String, dynamic>{}
+        : null;
+  }
+
+  /// True if this manifest declares a Flutter module project.
+  ///
+  /// A Flutter project is considered a module when it has a `module:`
+  /// descriptor. A Flutter module project supports integration into an
+  /// existing host app.
+  ///
+  /// Such a project can be created using `flutter create -t module`.
+  bool get isModule => moduleDescriptor != null;
+
   List<Map<String, dynamic>> get fontsDescriptor {
    return _flutterDescriptor['fonts'] ?? const <Map<String, dynamic>>[];
   }
diff --git a/packages/flutter_tools/lib/src/ios/xcodeproj.dart b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
index 4dfd917..8099b89 100644
--- a/packages/flutter_tools/lib/src/ios/xcodeproj.dart
+++ b/packages/flutter_tools/lib/src/ios/xcodeproj.dart
@@ -51,6 +51,8 @@
 ///
 /// targetOverride: Optional parameter, if null or unspecified the default value
 /// from xcode_backend.sh is used 'lib/main.dart'.
+///
+/// Returns the number of files written.
 Future<void> updateGeneratedXcodeProperties({
   @required String projectPath,
   @required BuildInfo buildInfo,
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart
index 51a7f0d..f72463c 100644
--- a/packages/flutter_tools/lib/src/plugins.dart
+++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -148,7 +148,7 @@
 
   final String pluginRegistry =
       new mustache.Template(_androidPluginRegistryTemplate).renderString(context);
-  final String javaSourcePath = fs.path.join(directory, 'android', 'app', 'src', 'main', 'java');
+  final String javaSourcePath = fs.path.join(directory, 'src', 'main', 'java');
   final Directory registryDirectory =
       fs.directory(fs.path.join(javaSourcePath, 'io', 'flutter', 'plugins'));
   registryDirectory.createSync(recursive: true);
@@ -233,9 +233,11 @@
   directory ??= fs.currentDirectory.path;
   final List<Plugin> plugins = _findPlugins(directory);
   final bool changed = _writeFlutterPluginsList(directory, plugins);
-
-  if (fs.isDirectorySync(fs.path.join(directory, 'android')))
-    _writeAndroidPluginRegistrant(directory, plugins);
+  if (fs.isDirectorySync(fs.path.join(directory, '.android', 'Flutter'))) {
+    _writeAndroidPluginRegistrant(fs.path.join(directory, '.android', 'Flutter'), plugins);
+  } else if (fs.isDirectorySync(fs.path.join(directory, 'android', 'app'))) {
+    _writeAndroidPluginRegistrant(fs.path.join(directory, 'android', 'app'), plugins);
+  }
   if (fs.isDirectorySync(fs.path.join(directory, 'ios'))) {
     _writeIOSPluginRegistrant(directory, plugins);
     final CocoaPods cocoaPods = new CocoaPods();
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index f1d2b94..dae7772 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -5,10 +5,14 @@
 import 'dart:async';
 import 'dart:convert';
 
-import 'base/file_system.dart';
-import 'ios/xcodeproj.dart';
-import 'plugins.dart';
 
+import 'android/gradle.dart' as gradle;
+import 'base/file_system.dart';
+import 'cache.dart';
+import 'flutter_manifest.dart';
+import 'ios/xcodeproj.dart' as xcode;
+import 'plugins.dart';
+import 'template.dart';
 
 /// Represents the contents of a Flutter project at the specified [directory].
 class FlutterProject {
@@ -47,6 +51,9 @@
   /// The Android sub project of this project.
   AndroidProject get android => new AndroidProject(directory.childDirectory('android'));
 
+  /// The generated AndroidModule sub project of this module project.
+  AndroidModuleProject get androidModule => new AndroidModuleProject(directory.childDirectory('.android'));
+
   /// Returns true if this project has an example application
   bool get hasExampleApp => directory.childDirectory('example').childFile('pubspec.yaml').existsSync();
 
@@ -54,13 +61,19 @@
   FlutterProject get example => new FlutterProject(directory.childDirectory('example'));
 
   /// Generates project files necessary to make Gradle builds work on Android
-  /// and CocoaPods+Xcode work on iOS, for app projects only
+  /// and CocoaPods+Xcode work on iOS, for app and module projects only.
+  ///
+  /// Returns the number of files written.
   Future<void> ensureReadyForPlatformSpecificTooling() async {
     if (!directory.existsSync() || hasExampleApp) {
-      return;
+      return 0;
+    }
+    final FlutterManifest manifest = await FlutterManifest.createFromPath(directory.childFile('pubspec.yaml').path);
+    if (manifest.isModule) {
+      await androidModule.ensureReadyForPlatformSpecificTooling(manifest);
     }
     injectPlugins(directory: directory.path);
-    await generateXcodeProperties(directory.path);
+    await xcode.generateXcodeProperties(directory.path);
   }
 }
 
@@ -97,6 +110,36 @@
   }
 }
 
+/// Represents the contents of the .android-generated/ folder of a Flutter module
+/// project.
+class AndroidModuleProject {
+  AndroidModuleProject(this.directory);
+
+  final Directory directory;
+
+  Future<void> ensureReadyForPlatformSpecificTooling(FlutterManifest manifest) async {
+    if (_shouldRegenerate()) {
+      final Template template = new Template.fromName(fs.path.join('module', 'android'));
+      template.render(directory, <String, dynamic>{
+        'androidIdentifier': manifest.moduleDescriptor['androidPackage'],
+      }, printStatusWhenWriting: false);
+      gradle.injectGradleWrapper(directory);
+    }
+    gradle.updateLocalPropertiesSync(directory, manifest);
+  }
+
+  bool _shouldRegenerate() {
+    final File flutterToolsStamp = Cache.instance.getStampFileFor('flutter_tools');
+    final File buildDotGradleFile = directory.childFile('build.gradle');
+    if (!buildDotGradleFile.existsSync())
+      return true;
+    return flutterToolsStamp.existsSync() &&
+        flutterToolsStamp
+            .lastModifiedSync()
+            .isAfter(buildDotGradleFile.lastModifiedSync());
+  }
+}
+
 /// Asynchronously returns the first line-based match for [regExp] in [file].
 ///
 /// Assumes UTF8 encoding.
diff --git a/packages/flutter_tools/lib/src/template.dart b/packages/flutter_tools/lib/src/template.dart
index 71c47ba..3f22211 100644
--- a/packages/flutter_tools/lib/src/template.dart
+++ b/packages/flutter_tools/lib/src/template.dart
@@ -66,6 +66,7 @@
     Directory destination,
     Map<String, dynamic> context, {
     bool overwriteExisting = true,
+    bool printStatusWhenWriting = true,
   }) {
     destination.createSync(recursive: true);
     int fileCount = 0;
@@ -117,14 +118,17 @@
       if (finalDestinationFile.existsSync()) {
         if (overwriteExisting) {
           finalDestinationFile.deleteSync(recursive: true);
-          printStatus('  $relativePathForLogging (overwritten)');
+          if (printStatusWhenWriting)
+            printStatus('  $relativePathForLogging (overwritten)');
         } else {
           // The file exists but we cannot overwrite it, move on.
-          printTrace('  $relativePathForLogging (existing - skipped)');
+          if (printStatusWhenWriting)
+            printTrace('  $relativePathForLogging (existing - skipped)');
           return;
         }
       } else {
-        printStatus('  $relativePathForLogging (created)');
+        if (printStatusWhenWriting)
+          printStatus('  $relativePathForLogging (created)');
       }
 
       fileCount++;