Flutter build aar (#36732)

`flutter build aar`

This new build command works just like `flutter build apk` or `flutter build appbundle`, but for plugin and module projects.

This PR also refactors how plugins are included in app or module projects. By building the plugins as AARs, the Android Gradle plugin is able to use Jetifier to translate support libraries into AndroidX libraries for all the plugin's native code. Thus, reducing the error rate when using AndroidX in apps.

This change also allows to build modules as AARs, so developers can take these artifacts and distribute them along with the native host app without the need of the Flutter tool. This is a requirement for add to app.

`flutter build aar` generates POM artifacts (XML files) which contain metadata about the native dependencies used by the plugin. This allows Gradle to resolve dependencies at the app level. The result of this new build command is a single build/outputs/repo, the local repository that contains all the generated AARs and POM files.

In a Flutter app project, this local repo is used by the Flutter Gradle plugin to resolve the plugin dependencies. In add to app case, the developer needs to configure the local repo and the dependency manually in `build.gradle`:


repositories {
    maven {
        url "<path-to-flutter-module>build/host/outputs/repo"
    }
}

dependencies {
    implementation("<package-name>:flutter_<build-mode>:1.0@aar") {
       transitive = true
    }
}

diff --git a/dev/bots/test.dart b/dev/bots/test.dart
index 48be0bb..4ebcfc3 100644
--- a/dev/bots/test.dart
+++ b/dev/bots/test.dart
@@ -961,9 +961,14 @@
   if (subShard == 'gradle1') {
     await _runDevicelabTest('gradle_plugin_light_apk_test', env: env);
     await _runDevicelabTest('gradle_plugin_fat_apk_test', env: env);
+    await _runDevicelabTest('gradle_jetifier_test', env: env);
+    await _runDevicelabTest('gradle_plugin_dependencies_test', env: env);
+    await _runDevicelabTest('gradle_migrate_settings_test', env: env);
   }
   if (subShard == 'gradle2') {
     await _runDevicelabTest('gradle_plugin_bundle_test', env: env);
     await _runDevicelabTest('module_test', env: env);
+    await _runDevicelabTest('build_aar_plugin_test', env: env);
+    await _runDevicelabTest('build_aar_module_test', env: env);
   }
 }
diff --git a/dev/devicelab/bin/tasks/build_aar_module_test.dart b/dev/devicelab/bin/tasks/build_aar_module_test.dart
new file mode 100644
index 0000000..be55a15
--- /dev/null
+++ b/dev/devicelab/bin/tasks/build_aar_module_test.dart
@@ -0,0 +1,219 @@
+// Copyright (c) 2019 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 'dart:io';
+
+import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/framework/utils.dart';
+import 'package:path/path.dart' as path;
+
+final String gradlew = Platform.isWindows ? 'gradlew.bat' : 'gradlew';
+final String gradlewExecutable = Platform.isWindows ? gradlew : './$gradlew';
+
+/// Tests that AARs can be built on module projects.
+Future<void> main() async {
+  await task(() async {
+
+    section('Find Java');
+
+    final String javaHome = await findJavaHome();
+    if (javaHome == null)
+      return TaskResult.failure('Could not find Java');
+    print('\nUsing JAVA_HOME=$javaHome');
+
+    section('Create module project');
+
+    final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.');
+    final Directory projectDir = Directory(path.join(tempDir.path, 'hello'));
+    try {
+      await inDirectory(tempDir, () async {
+        await flutter(
+          'create',
+          options: <String>['--org', 'io.flutter.devicelab', '--template', 'module', 'hello'],
+        );
+      });
+
+      section('Add plugins');
+
+      final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
+      String content = pubspec.readAsStringSync();
+      content = content.replaceFirst(
+        '\ndependencies:\n',
+        '\ndependencies:\n  device_info:\n  package_info:\n',
+      );
+      pubspec.writeAsStringSync(content, flush: true);
+      await inDirectory(projectDir, () async {
+        await flutter(
+          'packages',
+          options: <String>['get'],
+        );
+      });
+
+      section('Build release AAR');
+
+      await inDirectory(projectDir, () async {
+        await flutter(
+          'build',
+          options: <String>['aar', '--verbose'],
+        );
+      });
+
+      final String repoPath = path.join(
+        projectDir.path,
+        'build',
+        'host',
+        'outputs',
+        'repo',
+      );
+
+      checkFileExists(path.join(
+        repoPath,
+        'io',
+        'flutter',
+        'devicelab',
+        'hello',
+        'flutter_release',
+        '1.0',
+        'flutter_release-1.0.aar',
+      ));
+
+      checkFileExists(path.join(
+        repoPath,
+        'io',
+        'flutter',
+        'devicelab',
+        'hello',
+        'flutter_release',
+        '1.0',
+        'flutter_release-1.0.pom',
+      ));
+
+      checkFileExists(path.join(
+        repoPath,
+        'io',
+        'flutter',
+        'plugins',
+        'deviceinfo',
+        'device_info_release',
+        '1.0',
+        'device_info_release-1.0.aar',
+      ));
+
+      checkFileExists(path.join(
+        repoPath,
+        'io',
+        'flutter',
+        'plugins',
+        'deviceinfo',
+        'device_info_release',
+        '1.0',
+        'device_info_release-1.0.pom',
+      ));
+
+      checkFileExists(path.join(
+        repoPath,
+        'io',
+        'flutter',
+        'plugins',
+        'packageinfo',
+        'package_info_release',
+        '1.0',
+        'package_info_release-1.0.aar',
+      ));
+
+      checkFileExists(path.join(
+        repoPath,
+        'io',
+        'flutter',
+        'plugins',
+        'packageinfo',
+        'package_info_release',
+        '1.0',
+        'package_info_release-1.0.pom',
+      ));
+
+      section('Build debug AAR');
+
+      await inDirectory(projectDir, () async {
+        await flutter(
+          'build',
+          options: <String>['aar', '--verbose', '--debug'],
+        );
+      });
+
+      checkFileExists(path.join(
+        repoPath,
+        'io',
+        'flutter',
+        'devicelab',
+        'hello',
+        'flutter_release',
+        '1.0',
+        'flutter_release-1.0.aar',
+      ));
+
+      checkFileExists(path.join(
+        repoPath,
+        'io',
+        'flutter',
+        'devicelab',
+        'hello',
+        'flutter_debug',
+        '1.0',
+        'flutter_debug-1.0.pom',
+      ));
+
+      checkFileExists(path.join(
+        repoPath,
+        'io',
+        'flutter',
+        'plugins',
+        'deviceinfo',
+        'device_info_debug',
+        '1.0',
+        'device_info_debug-1.0.aar',
+      ));
+
+      checkFileExists(path.join(
+        repoPath,
+        'io',
+        'flutter',
+        'plugins',
+        'deviceinfo',
+        'device_info_debug',
+        '1.0',
+        'device_info_debug-1.0.pom',
+      ));
+
+      checkFileExists(path.join(
+        repoPath,
+        'io',
+        'flutter',
+        'plugins',
+        'packageinfo',
+        'package_info_debug',
+        '1.0',
+        'package_info_debug-1.0.aar',
+      ));
+
+      checkFileExists(path.join(
+        repoPath,
+        'io',
+        'flutter',
+        'plugins',
+        'packageinfo',
+        'package_info_debug',
+        '1.0',
+        'package_info_debug-1.0.pom',
+      ));
+
+      return TaskResult.success(null);
+    } catch (e) {
+      return TaskResult.failure(e.toString());
+    } finally {
+      rmTree(tempDir);
+    }
+  });
+}
diff --git a/dev/devicelab/bin/tasks/build_aar_plugin_test.dart b/dev/devicelab/bin/tasks/build_aar_plugin_test.dart
new file mode 100644
index 0000000..7073810
--- /dev/null
+++ b/dev/devicelab/bin/tasks/build_aar_plugin_test.dart
@@ -0,0 +1,138 @@
+// Copyright (c) 2019 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 'dart:io';
+
+import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/framework/utils.dart';
+import 'package:path/path.dart' as path;
+
+final String gradlew = Platform.isWindows ? 'gradlew.bat' : 'gradlew';
+final String gradlewExecutable = Platform.isWindows ? gradlew : './$gradlew';
+
+/// Tests that AARs can be built on plugin projects.
+Future<void> main() async {
+  await task(() async {
+
+    section('Find Java');
+
+    final String javaHome = await findJavaHome();
+    if (javaHome == null)
+      return TaskResult.failure('Could not find Java');
+    print('\nUsing JAVA_HOME=$javaHome');
+
+    section('Create plugin project');
+
+    final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.');
+    final Directory projectDir = Directory(path.join(tempDir.path, 'hello'));
+    try {
+      await inDirectory(tempDir, () async {
+        await flutter(
+          'create',
+          options: <String>[
+            '--org', 'io.flutter.devicelab',
+            '--template', 'plugin',
+            'hello',
+          ],
+        );
+      });
+
+      section('Build release AAR');
+
+      await inDirectory(projectDir, () async {
+        await flutter(
+          'build',
+          options: <String>['aar', '--verbose'],
+        );
+      });
+
+      final String repoPath = path.join(
+        projectDir.path,
+        'build',
+        'outputs',
+        'repo',
+      );
+
+      final File releaseAar = File(path.join(
+        repoPath,
+        'io',
+        'flutter',
+        'devicelab',
+        'hello',
+        'hello_release',
+        '1.0',
+        'hello_release-1.0.aar',
+      ));
+
+      if (!exists(releaseAar)) {
+        return TaskResult.failure('Failed to build the release AAR file.');
+      }
+
+      final File releasePom = File(path.join(
+        repoPath,
+        'io',
+        'flutter',
+        'devicelab',
+        'hello',
+        'hello_release',
+        '1.0',
+        'hello_release-1.0.pom',
+      ));
+
+      if (!exists(releasePom)) {
+        return TaskResult.failure('Failed to build the release POM file.');
+      }
+
+      section('Build debug AAR');
+
+      await inDirectory(projectDir, () async {
+        await flutter(
+          'build',
+          options: <String>[
+            'aar',
+            '--verbose',
+            '--debug',
+          ],
+        );
+      });
+
+      final File debugAar = File(path.join(
+        repoPath,
+        'io',
+        'flutter',
+        'devicelab',
+        'hello',
+        'hello_debug',
+        '1.0',
+        'hello_debug-1.0.aar',
+      ));
+
+      if (!exists(debugAar)) {
+        return TaskResult.failure('Failed to build the debug AAR file.');
+      }
+
+      final File debugPom = File(path.join(
+        repoPath,
+        'io',
+        'flutter',
+        'devicelab',
+        'hello',
+        'hello_debug',
+        '1.0',
+        'hello_debug-1.0.pom',
+      ));
+
+      if (!exists(debugPom)) {
+        return TaskResult.failure('Failed to build the debug POM file.');
+      }
+
+      return TaskResult.success(null);
+    } catch (e) {
+      return TaskResult.failure(e.toString());
+    } finally {
+      rmTree(tempDir);
+    }
+  });
+}
diff --git a/dev/devicelab/bin/tasks/gradle_jetifier_test.dart b/dev/devicelab/bin/tasks/gradle_jetifier_test.dart
new file mode 100644
index 0000000..bbabfc2
--- /dev/null
+++ b/dev/devicelab/bin/tasks/gradle_jetifier_test.dart
@@ -0,0 +1,138 @@
+// Copyright (c) 2019 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 'dart:io';
+
+import 'package:flutter_devicelab/framework/apk_utils.dart';
+import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/framework/utils.dart';
+import 'package:path/path.dart' as path;
+
+final String gradlew = Platform.isWindows ? 'gradlew.bat' : 'gradlew';
+final String gradlewExecutable = Platform.isWindows ? gradlew : './$gradlew';
+
+/// Tests that Jetifier can translate plugins that use support libraries.
+Future<void> main() async {
+  await task(() async {
+
+    section('Find Java');
+
+    final String javaHome = await findJavaHome();
+    if (javaHome == null)
+      return TaskResult.failure('Could not find Java');
+    print('\nUsing JAVA_HOME=$javaHome');
+
+    section('Create Flutter AndroidX app project');
+
+    final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.');
+    final Directory projectDir = Directory(path.join(tempDir.path, 'hello'));
+    try {
+      await inDirectory(tempDir, () async {
+        await flutter(
+          'create',
+          options: <String>[
+            '--org', 'io.flutter.devicelab',
+            '--androidx',
+            'hello',
+          ],
+        );
+      });
+
+      section('Add plugin that uses support libraries');
+
+      final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
+      String content = pubspec.readAsStringSync();
+      content = content.replaceFirst(
+        '\ndependencies:\n',
+        '\ndependencies:\n  firebase_auth: 0.7.0\n',
+      );
+      pubspec.writeAsStringSync(content, flush: true);
+      await inDirectory(projectDir, () async {
+        await flutter(
+          'packages',
+          options: <String>['get'],
+        );
+      });
+
+      section('Build release APK');
+
+      await inDirectory(projectDir, () async {
+        await flutter(
+          'build',
+          options: <String>[
+            'apk',
+            '--target-platform', 'android-arm',
+            '--verbose',
+          ],
+        );
+      });
+
+      final File releaseApk = File(path.join(
+        projectDir.path,
+        'build',
+        'app',
+        'outputs',
+        'apk',
+        'release',
+        'app-release.apk',
+      ));
+
+      if (!exists(releaseApk)) {
+        return TaskResult.failure('Failed to build release APK.');
+      }
+
+      checkApkContainsClasses(releaseApk, <String>[
+        // The plugin class defined by `firebase_auth`.
+        'io.flutter.plugins.firebaseauth.FirebaseAuthPlugin',
+        // Used by `firebase_auth`.
+        'com.google.firebase.FirebaseApp',
+        // Base class for activities that enables composition of higher level components.
+        'androidx.core.app.ComponentActivity',
+      ]);
+
+      section('Build debug APK');
+
+      await inDirectory(projectDir, () async {
+        await flutter(
+          'build',
+          options: <String>[
+            'apk',
+            '--target-platform', 'android-arm',
+            '--debug', '--verbose',
+          ],
+        );
+      });
+
+      final File debugApk = File(path.join(
+        projectDir.path,
+        'build',
+        'app',
+        'outputs',
+        'apk',
+        'debug',
+        'app-debug.apk',
+      ));
+
+      if (!exists(debugApk)) {
+        return TaskResult.failure('Failed to build debug APK.');
+      }
+
+      checkApkContainsClasses(debugApk, <String>[
+        // The plugin class defined by `firebase_auth`.
+        'io.flutter.plugins.firebaseauth.FirebaseAuthPlugin',
+        // Used by `firebase_auth`.
+        'com.google.firebase.FirebaseApp',
+        // Base class for activities that enables composition of higher level components.
+        'androidx.core.app.ComponentActivity',
+      ]);
+
+      return TaskResult.success(null);
+    } catch (e) {
+      return TaskResult.failure(e.toString());
+    } finally {
+      rmTree(tempDir);
+    }
+  });
+}
diff --git a/dev/devicelab/bin/tasks/gradle_migrate_settings_test.dart b/dev/devicelab/bin/tasks/gradle_migrate_settings_test.dart
new file mode 100644
index 0000000..72a1708
--- /dev/null
+++ b/dev/devicelab/bin/tasks/gradle_migrate_settings_test.dart
@@ -0,0 +1,180 @@
+// Copyright (c) 2019 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 'dart:io';
+
+import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/framework/utils.dart';
+import 'package:path/path.dart' as path;
+
+final String gradlew = Platform.isWindows ? 'gradlew.bat' : 'gradlew';
+final String gradlewExecutable = Platform.isWindows ? gradlew : './$gradlew';
+
+/// Tests that [settings_aar.gradle] is created when possible.
+Future<void> main() async {
+  await task(() async {
+
+    section('Find Java');
+
+    final String javaHome = await findJavaHome();
+    if (javaHome == null)
+      return TaskResult.failure('Could not find Java');
+    print('\nUsing JAVA_HOME=$javaHome');
+
+    section('Create app project');
+
+    final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.');
+    final Directory projectDir = Directory(path.join(tempDir.path, 'hello'));
+    try {
+      await inDirectory(tempDir, () async {
+        await flutter(
+          'create',
+          options: <String>['hello'],
+        );
+      });
+
+      section('Override settings.gradle V1');
+
+      final String relativeNewSettingsGradle = path.join('android', 'settings_aar.gradle');
+
+      section('Build APK');
+
+      String stdout;
+      await inDirectory(projectDir, () async {
+        stdout = await evalFlutter(
+          'build',
+          options: <String>[
+            'apk',
+            '--flavor', 'does-not-exist',
+          ],
+          canFail: true, // The flavor doesn't exist.
+        );
+      });
+
+      const String newFileContent = 'include \':app\'';
+
+      final File settingsGradle = File(path.join(projectDir.path, 'android', 'settings.gradle'));
+      final File newSettingsGradle = File(path.join(projectDir.path, 'android', 'settings_aar.gradle'));
+
+      if (!newSettingsGradle.existsSync()) {
+        return TaskResult.failure('Expected file: `${newSettingsGradle.path}`.');
+      }
+
+      if (newSettingsGradle.readAsStringSync().trim() != newFileContent) {
+        return TaskResult.failure('Expected to create `${newSettingsGradle.path}` V1.');
+      }
+
+      if (!stdout.contains('Creating `$relativeNewSettingsGradle`') ||
+          !stdout.contains('`$relativeNewSettingsGradle` created successfully')) {
+        return TaskResult.failure('Expected update message in stdout.');
+      }
+
+      section('Override settings.gradle V2');
+
+      const String deprecatedFileContentV2 = '''
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withInputStream { stream -> plugins.load(stream) }
+}
+
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+    include ":\$name"
+    project(":\$name").projectDir = pluginDirectory
+}
+''';
+      settingsGradle.writeAsStringSync(deprecatedFileContentV2, flush: true);
+      newSettingsGradle.deleteSync();
+
+      section('Build APK');
+
+      await inDirectory(projectDir, () async {
+        stdout = await evalFlutter(
+          'build',
+          options: <String>[
+            'apk',
+            '--flavor', 'does-not-exist',
+          ],
+          canFail: true, // The flavor doesn't exist.
+        );
+      });
+
+      if (newSettingsGradle.readAsStringSync().trim() != newFileContent) {
+        return TaskResult.failure('Expected to create `${newSettingsGradle.path}` V2.');
+      }
+
+      if (!stdout.contains('Creating `$relativeNewSettingsGradle`') ||
+          !stdout.contains('`$relativeNewSettingsGradle` created successfully')) {
+        return TaskResult.failure('Expected update message in stdout.');
+      }
+
+      section('Override settings.gradle with custom logic');
+
+      const String customDeprecatedFileContent = '''
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withInputStream { stream -> plugins.load(stream) }
+}
+
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+    include ":\$name"
+    project(":\$name").projectDir = pluginDirectory
+}
+// some custom logic
+''';
+      settingsGradle.writeAsStringSync(customDeprecatedFileContent, flush: true);
+      newSettingsGradle.deleteSync();
+
+      section('Build APK');
+
+      final StringBuffer stderr = StringBuffer();
+      await inDirectory(projectDir, () async {
+        stdout = await evalFlutter(
+          'build',
+          options: <String>[
+            'apk',
+            '--flavor', 'does-not-exist',
+          ],
+          canFail: true, // The flavor doesn't exist.
+          stderr: stderr,
+        );
+      });
+
+      if (newSettingsGradle.existsSync()) {
+        return TaskResult.failure('Unexpected file: `${newSettingsGradle.path}`.');
+      }
+
+      if (!stdout.contains('Creating `$relativeNewSettingsGradle`')) {
+        return TaskResult.failure('Expected update message in stdout.');
+      }
+
+      if (stdout.contains('`$relativeNewSettingsGradle` created successfully')) {
+        return TaskResult.failure('Unexpected message in stdout.');
+      }
+
+      if (!stderr.toString().contains('Flutter tried to create the file '
+          '`$relativeNewSettingsGradle`, but failed.')) {
+        return TaskResult.failure('Expected failure message in stdout.');
+      }
+
+      return TaskResult.success(null);
+    } catch (e) {
+      return TaskResult.failure(e.toString());
+    } finally {
+      rmTree(tempDir);
+    }
+  });
+}
diff --git a/dev/devicelab/bin/tasks/gradle_plugin_dependencies_test.dart b/dev/devicelab/bin/tasks/gradle_plugin_dependencies_test.dart
new file mode 100644
index 0000000..2525bdf
--- /dev/null
+++ b/dev/devicelab/bin/tasks/gradle_plugin_dependencies_test.dart
@@ -0,0 +1,147 @@
+// Copyright (c) 2019 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 'dart:io';
+
+import 'package:flutter_devicelab/framework/apk_utils.dart';
+import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/framework/utils.dart';
+import 'package:path/path.dart' as path;
+
+final String gradlew = Platform.isWindows ? 'gradlew.bat' : 'gradlew';
+final String gradlewExecutable = Platform.isWindows ? gradlew : './$gradlew';
+
+/// Tests that projects can include plugins that have a transtive dependency in common.
+/// For more info see: https://github.com/flutter/flutter/issues/27254.
+Future<void> main() async {
+  await task(() async {
+
+    section('Find Java');
+
+    final String javaHome = await findJavaHome();
+    if (javaHome == null)
+      return TaskResult.failure('Could not find Java');
+    print('\nUsing JAVA_HOME=$javaHome');
+
+    section('Create Flutter AndroidX app project');
+
+    final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.');
+    final Directory projectDir = Directory(path.join(tempDir.path, 'hello'));
+    try {
+      await inDirectory(tempDir, () async {
+        await flutter(
+          'create',
+          options: <String>[
+            '--org', 'io.flutter.devicelab',
+            '--androidx',
+            'hello',
+          ],
+        );
+      });
+
+      section('Add plugin that have conflicting dependencies');
+
+      final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
+      String content = pubspec.readAsStringSync();
+
+      // `flutter_local_notifications` uses `androidx.core:core:1.0.1`
+      // `firebase_core` and `firebase_messaging` use `androidx.core:core:1.0.0`.
+      content = content.replaceFirst(
+        '\ndependencies:\n',
+        '\ndependencies:\n  flutter_local_notifications: 0.7.1+3\n  firebase_core:\n  firebase_messaging:\n',
+      );
+      pubspec.writeAsStringSync(content, flush: true);
+      await inDirectory(projectDir, () async {
+        await flutter(
+          'packages',
+          options: <String>['get'],
+        );
+      });
+
+      section('Build release APK');
+
+      await inDirectory(projectDir, () async {
+        await flutter(
+          'build',
+          options: <String>[
+            'apk',
+            '--target-platform', 'android-arm',
+            '--verbose',
+          ],
+        );
+      });
+
+      final File releaseApk = File(path.join(
+        projectDir.path,
+        'build',
+        'app',
+        'outputs',
+        'apk',
+        'release',
+        'app-release.apk',
+      ));
+
+      if (!exists(releaseApk)) {
+        return TaskResult.failure('Failed to build release APK.');
+      }
+
+      checkApkContainsClasses(releaseApk, <String>[
+        // Used by `flutter_local_notifications`.
+        'com.google.gson.Gson',
+        // Used by `firebase_core` and `firebase_messaging`.
+        'com.google.firebase.FirebaseApp',
+        // Used by `firebase_core`.
+        'com.google.firebase.FirebaseOptions',
+        // Used by `firebase_messaging`.
+        'com.google.firebase.messaging.FirebaseMessaging',
+      ]);
+
+      section('Build debug APK');
+
+      await inDirectory(projectDir, () async {
+        await flutter(
+          'build',
+          options: <String>[
+            'apk',
+            '--target-platform', 'android-arm',
+            '--debug',
+            '--verbose',
+          ],
+        );
+      });
+
+      final File debugApk = File(path.join(
+        projectDir.path,
+        'build',
+        'app',
+        'outputs',
+        'apk',
+        'debug',
+        'app-debug.apk',
+      ));
+
+      if (!exists(debugApk)) {
+        return TaskResult.failure('Failed to build debug APK.');
+      }
+
+      checkApkContainsClasses(debugApk, <String>[
+        // Used by `flutter_local_notifications`.
+        'com.google.gson.Gson',
+        // Used by `firebase_core` and `firebase_messaging`.
+        'com.google.firebase.FirebaseApp',
+        // Used by `firebase_core`.
+        'com.google.firebase.FirebaseOptions',
+        // Used by `firebase_messaging`.
+        'com.google.firebase.messaging.FirebaseMessaging',
+      ]);
+
+      return TaskResult.success(null);
+    } catch (e) {
+      return TaskResult.failure(e.toString());
+    } finally {
+      rmTree(tempDir);
+    }
+  });
+}
diff --git a/dev/devicelab/bin/tasks/module_test.dart b/dev/devicelab/bin/tasks/module_test.dart
index 1b6e891..0bdcdb0 100644
--- a/dev/devicelab/bin/tasks/module_test.dart
+++ b/dev/devicelab/bin/tasks/module_test.dart
@@ -42,7 +42,7 @@
       String content = await pubspec.readAsString();
       content = content.replaceFirst(
         '\ndependencies:\n',
-        '\ndependencies:\n  battery:\n  package_info:\n',
+        '\ndependencies:\n  device_info:\n  package_info:\n',
       );
       await pubspec.writeAsString(content, flush: true);
       await inDirectory(projectDir, () async {
diff --git a/dev/devicelab/bin/tasks/module_test_ios.dart b/dev/devicelab/bin/tasks/module_test_ios.dart
index ec71e03..f578754 100644
--- a/dev/devicelab/bin/tasks/module_test_ios.dart
+++ b/dev/devicelab/bin/tasks/module_test_ios.dart
@@ -143,7 +143,7 @@
       String content = await pubspec.readAsString();
       content = content.replaceFirst(
         '\ndependencies:\n',
-        '\ndependencies:\n  battery:\n  package_info:\n',
+        '\ndependencies:\n  device_info:\n  package_info:\n',
       );
       await pubspec.writeAsString(content, flush: true);
       await inDirectory(projectDir, () async {
diff --git a/dev/devicelab/lib/framework/apk_utils.dart b/dev/devicelab/lib/framework/apk_utils.dart
index 0dc28b9..0c42b26 100644
--- a/dev/devicelab/lib/framework/apk_utils.dart
+++ b/dev/devicelab/lib/framework/apk_utils.dart
@@ -83,6 +83,93 @@
   return text.indexOf(pattern) != text.lastIndexOf(pattern);
 }
 
+/// Utility class to analyze the content inside an APK using dexdump,
+/// which is provided by the Android SDK.
+/// https://android.googlesource.com/platform/art/+/master/dexdump/dexdump.cc
+class ApkExtractor {
+  ApkExtractor(this.apkFile);
+
+  /// The APK.
+  final File apkFile;
+
+  bool _extracted = false;
+
+  Directory _outputDir;
+
+  Future<void> _extractApk() async {
+    if (_extracted) {
+      return;
+    }
+    _outputDir = apkFile.parent.createTempSync('apk');
+    if (Platform.isWindows) {
+      await eval('7za', <String>['x', apkFile.path], workingDirectory: _outputDir.path);
+    } else {
+      await eval('unzip', <String>[apkFile.path], workingDirectory: _outputDir.path);
+    }
+    _extracted = true;
+  }
+
+  /// Returns the full path to the [dexdump] tool.
+  Future<String> _findDexDump() async {
+    final String androidHome = Platform.environment['ANDROID_HOME'] ??
+        Platform.environment['ANDROID_SDK_ROOT'];
+
+    if (androidHome == null || androidHome.isEmpty) {
+      throw Exception('Unset env flag: `ANDROID_HOME` or `ANDROID_SDK_ROOT`.');
+    }
+    String dexdumps;
+    if (Platform.isWindows) {
+      dexdumps = await eval('dir', <String>['/s/b', 'dexdump.exe'],
+          workingDirectory: androidHome);
+    } else {
+      dexdumps = await eval('find', <String>[androidHome, '-name', 'dexdump']);
+    }
+    if (dexdumps.isEmpty) {
+      throw Exception('Couldn\'t find a dexdump executable.');
+    }
+    return dexdumps.split('\n').first;
+  }
+
+  // Removes any temporary directory.
+  void dispose() {
+    if (!_extracted) {
+      return;
+    }
+    rmTree(_outputDir);
+    _extracted = true;
+  }
+
+  /// Returns true if the APK contains a given class.
+  Future<bool> containsClass(String className) async {
+    await _extractApk();
+
+    final String dexDump = await _findDexDump();
+    final String classesDex = path.join(_outputDir.path, 'classes.dex');
+
+    if (!File(classesDex).existsSync()) {
+      throw Exception('Couldn\'t find classes.dex in the APK.');
+    }
+    final String classDescriptors = await eval(dexDump,
+        <String>[classesDex], printStdout: false);
+
+    if (classDescriptors.isEmpty) {
+      throw Exception('No descriptors found in classes.dex.');
+    }
+    return classDescriptors.contains(className.replaceAll('.', '/'));
+  }
+}
+
+ /// Checks that the classes are contained in the APK, throws otherwise.
+Future<void> checkApkContainsClasses(File apk, List<String> classes) async {
+  final ApkExtractor extractor = ApkExtractor(apk);
+  for (String className in classes) {
+    if (!(await extractor.containsClass(className))) {
+      throw Exception('APK doesn\'t contain class `$className`.');
+    }
+  }
+  extractor.dispose();
+}
+
 class FlutterProject {
   FlutterProject(this.parent, this.name);
 
diff --git a/dev/devicelab/lib/framework/utils.dart b/dev/devicelab/lib/framework/utils.dart
index 68ef798..4cb5ed7 100644
--- a/dev/devicelab/lib/framework/utils.dart
+++ b/dev/devicelab/lib/framework/utils.dart
@@ -303,7 +303,7 @@
 
 /// Executes a command and returns its standard output as a String.
 ///
-/// For logging purposes, the command's output is also printed out.
+/// For logging purposes, the command's output is also printed out by default.
 Future<String> eval(
   String executable,
   List<String> arguments, {
@@ -311,6 +311,8 @@
   bool canFail = false, // as in, whether failures are ok. False means that they are fatal.
   String workingDirectory,
   StringBuffer stderr, // if not null, the stderr will be written here
+  bool printStdout = true,
+  bool printStderr = true,
 }) async {
   final Process process = await startProcess(executable, arguments, environment: environment, workingDirectory: workingDirectory);
 
@@ -321,14 +323,18 @@
       .transform<String>(utf8.decoder)
       .transform<String>(const LineSplitter())
       .listen((String line) {
-        print('stdout: $line');
+        if (printStdout) {
+          print('stdout: $line');
+        }
         output.writeln(line);
       }, onDone: () { stdoutDone.complete(); });
   process.stderr
       .transform<String>(utf8.decoder)
       .transform<String>(const LineSplitter())
       .listen((String line) {
-        print('stderr: $line');
+        if (printStderr) {
+          print('stderr: $line');
+        }
         stderr?.writeln(line);
       }, onDone: () { stderrDone.complete(); });
 
@@ -619,3 +625,10 @@
     options.add('--local-engine=${osNames[deviceOperatingSystem]}_$flavor');
   }
 }
+
+/// Checks that the file exists, otherwise throws a [FileSystemException].
+void checkFileExists(String file) {
+  if (!exists(File(file))) {
+    throw FileSystemException('Expected file to exit.', file);
+  }
+}
diff --git a/packages/flutter_tools/gradle/aar_init_script.gradle b/packages/flutter_tools/gradle/aar_init_script.gradle
new file mode 100644
index 0000000..1285c58
--- /dev/null
+++ b/packages/flutter_tools/gradle/aar_init_script.gradle
@@ -0,0 +1,128 @@
+// Copyright 2019 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.
+//
+// This script is used to initialize the build in a module or plugin project.
+// During this phase, the script applies the Maven plugin and configures the
+// destination of the local repository.
+// The local repository will contain the AAR and POM files.
+
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.artifacts.maven.MavenDeployer
+import org.gradle.api.plugins.MavenPlugin
+import org.gradle.api.tasks.Upload
+
+void configureProject(Project project, File outputDir) {
+    if (!project.hasProperty("android")) {
+        throw new GradleException("Android property not found.")
+    }
+    if (!project.android.hasProperty("libraryVariants")) {
+        throw new GradleException("Can't generate AAR on a non Android library project.");
+    }
+
+    project.apply plugin: "maven"
+
+    project.android.libraryVariants.all { variant ->
+        addAarTask(project, variant)
+    }
+    // Snapshot versions include the timestamp in the artifact name.
+    // Therefore, remove the snapshot part, so new runs of `flutter build aar` overrides existing artifacts.
+    // This version isn't relevant in Flutter since the pub version is used
+    // to resolve dependencies.
+    project.version = project.version.replace("-SNAPSHOT", "")
+
+    project.uploadArchives {
+        repositories {
+            mavenDeployer {
+                repository(url: "file://${outputDir}/outputs/repo")
+            }
+        }
+    }
+    // Check if the project uses the Flutter plugin (defined in flutter.gradle).
+    Boolean usesFlutterPlugin = project.plugins.find { it.class.name == "FlutterPlugin" } != null
+    if (!usesFlutterPlugin) {
+        // Plugins don't include their dependencies under the assumption that the parent project adds them.
+        if (project.properties['android.useAndroidX']) {
+            project.dependencies {
+                compileOnly "androidx.annotation:annotation:+"
+            }
+        } else {
+            project.dependencies {
+                compileOnly "com.android.support:support-annotations:+"
+            }
+        }
+        project.dependencies {
+            // The Flutter plugin already adds `flutter.jar`.
+            compileOnly project.files("${getFlutterRoot(project)}/bin/cache/artifacts/engine/android-arm-release/flutter.jar")
+        }
+    }
+}
+
+String getFlutterRoot(Project project) {
+    if (!project.hasProperty("flutter-root")) {
+        throw new GradleException("The `-Pflutter-root` flag must be specified.")
+    }
+    return project.property("flutter-root")
+}
+
+void addAarTask(Project project, variant) {
+    String variantName = variant.name.capitalize()
+    String taskName = "assembleAar${variantName}"
+    project.tasks.create(name: taskName) {
+        // This check is required to be able to configure the archives before `uploadArchives` runs.
+        if (!project.gradle.startParameter.taskNames.contains(taskName)) {
+            return
+        }
+        // NOTE(blasten): `android.defaultPublishConfig` must equal the variant name to build.
+        // Where variant name is `<product-flavor><Build-Type>`. However, it's too late to configure
+        // `defaultPublishConfig` at this point. Therefore, the code below ensures that the
+        // default build config uses the artifacts produced for the specific build variant.
+        Task bundle = project.tasks.findByName("bundle${variantName}Aar") // gradle:3.2.0
+        if (bundle == null) {
+            bundle = project.tasks.findByName("bundle${variantName}") // gradle:3.1.0
+        }
+        if (bundle == null) {
+            throw new GradleException("Can't generate AAR for variant ${variantName}.");
+        }
+        project.uploadArchives.repositories.mavenDeployer {
+            pom {
+                artifactId = "${project.name}_${variant.name.toLowerCase()}"
+            }
+        }
+        // Clear the current archives since the current one is assigned based on
+        // `android.defaultPublishConfig` which defaults to `release`.
+        project.configurations["archives"].artifacts.clear()
+        // Add the artifact that will be published.
+        project.artifacts.add("archives", bundle)
+        // Generate the Maven artifacts.
+        finalizedBy "uploadArchives"
+    }
+}
+
+projectsEvaluated {
+    if (rootProject.property("is-plugin").toBoolean()) {
+        if (rootProject.hasProperty("output-dir")) {
+            rootProject.buildDir = rootProject.property("output-dir")
+        } else {
+            rootProject.buildDir = "../build";
+        }
+        // In plugin projects, the Android library is the root project.
+        configureProject(rootProject, rootProject.buildDir)
+        return
+    }
+    // In module projects, the Android library project is the `:flutter` subproject.
+    Project androidLibrarySubproject = rootProject.subprojects.find { it.name == "flutter" }
+    // In module projects, the `buildDir` is defined in the `:app` subproject.
+    Project appSubproject = rootProject.subprojects.find { it.name == "app" }
+
+    assert appSubproject != null
+    assert androidLibrarySubproject != null
+
+    if (appSubproject.hasProperty("output-dir")) {
+        appSubproject.buildDir = appSubproject.property("output-dir")
+    } else {
+        appSubproject.buildDir = "../build/host"
+    }
+    configureProject(androidLibrarySubproject, appSubproject.buildDir)
+}
diff --git a/packages/flutter_tools/gradle/deprecated_settings.gradle b/packages/flutter_tools/gradle/deprecated_settings.gradle
new file mode 100644
index 0000000..98e3600
--- /dev/null
+++ b/packages/flutter_tools/gradle/deprecated_settings.gradle
@@ -0,0 +1,31 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+    include ":$name"
+    project(":$name").projectDir = pluginDirectory
+}
+;EOF
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withInputStream { stream -> plugins.load(stream) }
+}
+
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+    include ":$name"
+    project(":$name").projectDir = pluginDirectory
+}
diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle
index 52317dc..50f4028 100644
--- a/packages/flutter_tools/gradle/flutter.gradle
+++ b/packages/flutter_tools/gradle/flutter.gradle
@@ -1,8 +1,13 @@
-import java.nio.file.Path
-import java.nio.file.Paths
+// Copyright 2019 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 static groovy.io.FileType.FILES
 
 import com.android.builder.model.AndroidProject
 import com.android.build.OutputFile
+import java.nio.file.Path
+import java.nio.file.Paths
 import org.apache.tools.ant.taskdefs.condition.Os
 import org.gradle.api.DefaultTask
 import org.gradle.api.GradleException
@@ -91,7 +96,7 @@
     @Override
     void apply(Project project) {
         project.extensions.create("flutter", FlutterExtension)
-        project.afterEvaluate this.&addFlutterTask
+        project.afterEvaluate this.&addFlutterTasks
 
         // By default, assembling APKs generates fat APKs if multiple platforms are passed.
         // Configuring split per ABI allows to generate separate APKs for each abi.
@@ -203,42 +208,118 @@
                 })
             }
         }
+    }
 
+    /**
+     * Returns the directory where the plugins are built.
+     */
+    private File getPluginBuildDir(Project project) {
+        // Module projects specify this flag to include plugins in the same repo as the module project.
+        if (project.ext.has("pluginBuildDir")) {
+            return project.ext.get("pluginBuildDir")
+        }
+        return project.buildDir
+    }
+
+    private Properties getPluginList(Project project) {
         File pluginsFile = new File(project.projectDir.parentFile.parentFile, '.flutter-plugins')
-        Properties plugins = readPropertiesIfExist(pluginsFile)
+        return readPropertiesIfExist(pluginsFile)
+    }
 
-        plugins.each { name, _ ->
-            def pluginProject = project.rootProject.findProject(":$name")
-            if (pluginProject != null) {
-                project.dependencies {
-                    if (project.getConfigurations().findByName("implementation")) {
-                        implementation pluginProject
-                    } else {
-                        compile pluginProject
+    private void addPluginTasks(Project project) {
+        Properties plugins = getPluginList(project)
+        project.android.buildTypes.each { buildType ->
+            plugins.each { name, path ->
+                String buildModeValue = buildType.debuggable ? "debug" : "release"
+                List<String> taskNameParts = ["build", "plugin", buildModeValue]
+                taskNameParts.addAll(name.split("_"))
+                String taskName = toCammelCase(taskNameParts)
+                // Build types can be extended. For example, a build type can extend the `debug` mode.
+                // In such cases, prevent creating the same task.
+                if (project.tasks.findByName(taskName) == null) {
+                    project.tasks.create(name: taskName, type: FlutterPluginTask) {
+                        flutterExecutable this.flutterExecutable
+                        buildMode buildModeValue
+                        verbose isVerbose(project)
+                        pluginDir project.file(path)
+                        sourceDir project.file(project.flutter.source)
+                        intermediateDir getPluginBuildDir(project)
                     }
                 }
-                pluginProject.afterEvaluate {
-                    pluginProject.android.buildTypes {
-                        profile {
-                            initWith debug
-                        }
-                    }
-
-                    pluginProject.android.buildTypes.each {
-                        def buildMode = buildModeFor(it)
-                        addFlutterJarCompileOnlyDependency(pluginProject, it.name, project.files( flutterJar ?: baseJar[buildMode] ))
-                    }
-                    pluginProject.android.buildTypes.whenObjectAdded {
-                        def buildMode = buildModeFor(it)
-                        addFlutterJarCompileOnlyDependency(pluginProject, it.name, project.files( flutterJar ?: baseJar[buildMode] ))
-                    }
-                }
-            } else {
-                project.logger.error("Plugin project :$name not found. Please update settings.gradle.")
             }
         }
     }
 
+    private void buildPlugins(Project project, Set buildTypes) {
+        List<Project> projects = [project]
+        // Module projects set the `hostProjects` extra property in `include_flutter.groovy`.
+        // This is required to set the local repository in each host app project.
+        if (project.ext.has("hostProjects")) {
+            projects.addAll(project.ext.get("hostProjects"))
+        }
+        projects.each { hostProject ->
+            hostProject.repositories {
+                maven {
+                    url "${getPluginBuildDir(project)}/outputs/repo"
+                }
+            }
+        }
+        buildTypes.each { buildType ->
+            project.tasks.withType(FlutterPluginTask).all { pluginTask ->
+                String buildMode = buildType.debuggable ? "debug" : "release"
+                if (pluginTask.buildMode != buildMode) {
+                    return
+                }
+                pluginTask.execute()
+                pluginTask.intermediateDir.eachFileRecurse(FILES) { file ->
+                    if (file.name != "maven-metadata.xml") {
+                        return
+                    }
+                    def mavenMetadata = new XmlParser().parse(file)
+                    String groupId = mavenMetadata.groupId.text()
+                    String artifactId = mavenMetadata.artifactId.text()
+
+                    if (!artifactId.endsWith(buildMode)) {
+                        return
+                    }
+                    // Add the plugin dependency based on the Maven metadata.
+                    addApiDependencies(project, buildType.name, "$groupId:$artifactId:+@aar", {
+                        transitive = true
+                    })
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns a set with the build type names that apply to the given list of tasks
+     * required to configure the plugin dependencies.
+     */
+    private Set getBuildTypesForTasks(Project project, List<String> tasksToExecute) {
+        Set buildTypes = []
+        tasksToExecute.each { task ->
+            project.android.buildTypes.each { buildType ->
+                if (task == "androidDependencies" || task.endsWith("dependencies")) {
+                    // The tasks to query the dependencies includes all the build types.
+                    buildTypes.add(buildType)
+                } else if (task.endsWith("assemble")) {
+                    // The `assemble` task includes all the build types.
+                    buildTypes.add(buildType)
+                } else if (task.endsWith(buildType.name.capitalize())) {
+                    buildTypes.add(buildType)
+                }
+            }
+        }
+        return buildTypes
+    }
+
+    private static String toCammelCase(List<String> parts) {
+        if (parts.empty) {
+            return ""
+        }
+        return "${parts[0]}${parts[1..-1].collect { it.capitalize() }.join('')}"
+    }
+
     private String resolveProperty(Project project, String name, String defaultValue) {
         if (localProperties == null) {
             localProperties = readPropertiesIfExist(new File(project.projectDir.parentFile, "local.properties"))
@@ -287,6 +368,17 @@
         return project.hasProperty('localEngineOut')
     }
 
+    private static Boolean isVerbose(Project project) {
+        if (project.hasProperty('verbose')) {
+            return project.property('verbose').toBoolean()
+        }
+        return false
+    }
+
+    private static Boolean buildPluginAsAar() {
+        return System.getProperty('build-plugins-as-aars') == 'true'
+    }
+
     /**
      * Returns the platform that is used to extract the `libflutter.so` and the .class files.
      *
@@ -304,30 +396,24 @@
         if (project.state.failure) {
             return
         }
-
-        project.dependencies {
-            String configuration;
-            if (project.getConfigurations().findByName("compileOnly")) {
-                configuration = "${variantName}CompileOnly";
-            } else {
-                configuration = "${variantName}Provided";
-            }
-
-            add(configuration, files)
+        String configuration;
+        if (project.getConfigurations().findByName("compileOnly")) {
+            configuration = "${variantName}CompileOnly";
+        } else {
+            configuration = "${variantName}Provided";
         }
+        project.dependencies.add(configuration, files)
     }
 
-    private static void addApiDependencies(Project project, String variantName, FileCollection files) {
-        project.dependencies {
-            String configuration;
-            // `compile` dependencies are now `api` dependencies.
-            if (project.getConfigurations().findByName("api")) {
-                configuration = "${variantName}Api";
-            } else {
-                configuration = "${variantName}Compile";
-            }
-            add(configuration, files)
+    private static void addApiDependencies(Project project, String variantName, Object dependency, Closure config = null) {
+        String configuration;
+        // `compile` dependencies are now `api` dependencies.
+        if (project.getConfigurations().findByName("api")) {
+            configuration = "${variantName}Api";
+        } else {
+            configuration = "${variantName}Compile";
         }
+        project.dependencies.add(configuration, dependency, config)
     }
 
     /**
@@ -355,14 +441,13 @@
         return "${targetArch}-release"
     }
 
-    private void addFlutterTask(Project project) {
+    private void addFlutterTasks(Project project) {
         if (project.state.failure) {
             return
         }
         if (project.flutter.source == null) {
             throw new GradleException("Must provide Flutter source directory")
         }
-
         String target = project.flutter.target
         if (target == null) {
             target = 'lib/main.dart'
@@ -371,10 +456,6 @@
             target = project.property('target')
         }
 
-        Boolean verboseValue = null
-        if (project.hasProperty('verbose')) {
-            verboseValue = project.property('verbose').toBoolean()
-        }
         String[] fileSystemRootsValue = null
         if (project.hasProperty('filesystem-roots')) {
             fileSystemRootsValue = project.property('filesystem-roots').split('\\|')
@@ -440,10 +521,9 @@
                 }
             }
 
-            def flutterTasks = []
-            targetPlatforms.each { targetArch ->
+            def compileTasks = targetPlatforms.collect { targetArch ->
                 String abiValue = PLATFORM_ARCH_MAP[targetArch]
-                String taskName = "compile${FLUTTER_BUILD_PREFIX}${variant.name.capitalize()}${targetArch.replace('android-', '').capitalize()}"
+                String taskName = toCammelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name, targetArch.replace('android-', '')])
                 FlutterTask compileTask = project.tasks.create(name: taskName, type: FlutterTask) {
                     flutterRoot this.flutterRoot
                     flutterExecutable this.flutterExecutable
@@ -452,7 +532,7 @@
                     localEngineSrcPath this.localEngineSrcPath
                     abi abiValue
                     targetPath target
-                    verbose verboseValue
+                    verbose isVerbose(project)
                     fileSystemRoots fileSystemRootsValue
                     fileSystemScheme fileSystemSchemeValue
                     trackWidgetCreation trackWidgetCreationValue
@@ -466,8 +546,8 @@
                     extraFrontEndOptions extraFrontEndOptionsValue
                     extraGenSnapshotOptions extraGenSnapshotOptionsValue
                 }
-                flutterTasks.add(compileTask)
             }
+
             def libJar = project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}/libs.jar")
             def libFlutterPlatforms = targetPlatforms.collect()
             // x86/x86_64 native library used for debugging only, for now.
@@ -496,13 +576,13 @@
                         include 'lib/**'
                     }
                 }
-                dependsOn flutterTasks
+                dependsOn compileTasks
                 // Add the ELF library.
-                flutterTasks.each { flutterTask ->
-                    from(flutterTask.intermediateDir) {
+                compileTasks.each { compileTask ->
+                    from(compileTask.intermediateDir) {
                         include '*.so'
                         rename { String filename ->
-                            return "lib/${flutterTask.abi}/lib${filename}"
+                            return "lib/${compileTask.abi}/lib${filename}"
                         }
                     }
                 }
@@ -516,7 +596,7 @@
             Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")
             Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
             Task copyFlutterAssetsTask = project.tasks.create(name: "copyFlutterAssets${variant.name.capitalize()}", type: Copy) {
-                dependsOn flutterTasks
+                dependsOn compileTasks
                 if (packageAssets && cleanPackageAssets) {
                     dependsOn packageAssets
                     dependsOn cleanPackageAssets
@@ -527,7 +607,7 @@
                     variant.mergeAssets.mustRunAfter("clean${variant.mergeAssets.name.capitalize()}")
                     into variant.mergeAssets.outputDir
                 }
-                flutterTasks.each { flutterTask ->
+                compileTasks.each { flutterTask ->
                     with flutterTask.assets
                 }
             }
@@ -550,12 +630,62 @@
                 processResources.dependsOn(copyFlutterAssetsTask)
             }
         }
-
         if (project.android.hasProperty("applicationVariants")) {
             project.android.applicationVariants.all addFlutterDeps
         } else {
             project.android.libraryVariants.all addFlutterDeps
         }
+
+        if (buildPluginAsAar()) {
+            addPluginTasks(project)
+
+            List<String> tasksToExecute = project.gradle.startParameter.taskNames
+            Set buildTypes = getBuildTypesForTasks(project, tasksToExecute)
+            if (tasksToExecute.contains("clean")) {
+                // Because the plugins are built during configuration, the task "clean"
+                // cannot run in conjunction with an assembly task.
+                if (!buildTypes.empty) {
+                    throw new GradleException("Can't run the clean task along with other assemble tasks")
+                }
+            }
+            // Build plugins when a task "assembly*" will be called later.
+            if (!buildTypes.empty) {
+                // Build the plugin during configuration.
+                // This is required when Jetifier is enabled, otherwise the implementation dependency
+                // cannot be added.
+                buildPlugins(project, buildTypes)
+            }
+        } else {
+            getPluginList(project).each { name, _ ->
+                def pluginProject = project.rootProject.findProject(":$name")
+                if (pluginProject != null) {
+                    project.dependencies {
+                        if (project.getConfigurations().findByName("implementation")) {
+                            implementation pluginProject
+                        } else {
+                            compile pluginProject
+                        }
+                    }
+                    pluginProject.afterEvaluate {
+                        pluginProject.android.buildTypes {
+                            profile {
+                                initWith debug
+                            }
+                        }
+                        pluginProject.android.buildTypes.each {
+                            def buildMode = buildModeFor(it)
+                            addFlutterJarCompileOnlyDependency(pluginProject, it.name, project.files( flutterJar ?: baseJar[buildMode] ))
+                        }
+                        pluginProject.android.buildTypes.whenObjectAdded {
+                            def buildMode = buildModeFor(it)
+                            addFlutterJarCompileOnlyDependency(pluginProject, it.name, project.files( flutterJar ?: baseJar[buildMode] ))
+                        }
+                    }
+                } else {
+                    project.logger.error("Plugin project :$name not found. Please update settings.gradle.")
+                }
+            }
+        }
     }
 }
 
@@ -784,6 +914,59 @@
     }
 }
 
+class FlutterPluginTask extends DefaultTask {
+    File flutterExecutable
+    @Optional @Input
+    Boolean verbose
+    @Input
+    String buildMode
+    @Input
+    File pluginDir
+    @Input
+    File intermediateDir
+    File sourceDir
+
+    @InputFiles
+    FileCollection getSourceFiles() {
+        return project.fileTree(
+            dir: sourceDir,
+            exclude: ["android", "ios"],
+            include: ["pubspec.yaml"]
+        )
+    }
+
+    @OutputDirectory
+    File getOutputDirectory() {
+        return intermediateDir
+    }
+
+    @TaskAction
+    void build() {
+        intermediateDir.mkdirs()
+        project.exec {
+            executable flutterExecutable.absolutePath
+            workingDir pluginDir
+            args "build", "aar"
+            args "--quiet"
+            args "--suppress-analytics"
+            args "--output-dir", "${intermediateDir}"
+            switch (buildMode) {
+                case 'release':
+                    args "--release"
+                    break
+                case 'debug':
+                    args "--debug"
+                    break
+                default:
+                    assert false
+            }
+            if (verbose) {
+                args "--verbose"
+            }
+        }
+    }
+}
+
 gradle.useLogger(new FlutterEventLogger())
 
 class FlutterEventLogger extends BuildAdapter implements TaskExecutionListener {
diff --git a/packages/flutter_tools/gradle/manual_migration_settings.gradle.md b/packages/flutter_tools/gradle/manual_migration_settings.gradle.md
new file mode 100644
index 0000000..f899531
--- /dev/null
+++ b/packages/flutter_tools/gradle/manual_migration_settings.gradle.md
@@ -0,0 +1,19 @@
+To manually update `settings.gradle`, follow these steps:
+
+    1. Copy `settings.gradle` as `settings_aar.gradle`
+    2. Remove the following code from `settings_aar.gradle`:
+
+            def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+            def plugins = new Properties()
+            def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+            if (pluginsFile.exists()) {
+                pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+            }
+
+            plugins.each { name, path ->
+                def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+                include ":$name"
+                project(":$name").projectDir = pluginDirectory
+            }
+
diff --git a/packages/flutter_tools/gradle/settings_aar.gradle.tmpl b/packages/flutter_tools/gradle/settings_aar.gradle.tmpl
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/packages/flutter_tools/gradle/settings_aar.gradle.tmpl
@@ -0,0 +1 @@
+include ':app'
diff --git a/packages/flutter_tools/lib/src/android/aar.dart b/packages/flutter_tools/lib/src/android/aar.dart
new file mode 100644
index 0000000..f619f77
--- /dev/null
+++ b/packages/flutter_tools/lib/src/android/aar.dart
@@ -0,0 +1,62 @@
+// Copyright 2019 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 '../build_info.dart';
+import '../project.dart';
+
+import 'android_sdk.dart';
+import 'gradle.dart';
+
+/// Provides a method to build a module or plugin as AAR.
+abstract class AarBuilder {
+  /// Builds the AAR artifacts.
+  Future<void> build({
+    @required FlutterProject project,
+    @required AndroidBuildInfo androidBuildInfo,
+    @required String target,
+    @required String outputDir,
+  });
+}
+
+/// Default implementation of [AarBuilder].
+class AarBuilderImpl extends AarBuilder {
+  AarBuilderImpl();
+
+  /// Builds the AAR and POM files for the current Flutter module or plugin.
+  @override
+  Future<void> build({
+    @required FlutterProject project,
+    @required AndroidBuildInfo androidBuildInfo,
+    @required String target,
+    @required String outputDir,
+  }) async {
+    if (!project.android.isUsingGradle) {
+      throwToolExit(
+          'The build process for Android has changed, and the current project configuration\n'
+              'is no longer valid. Please consult\n\n'
+              '  https://github.com/flutter/flutter/wiki/Upgrading-Flutter-projects-to-build-with-gradle\n\n'
+              'for details on how to upgrade the project.'
+      );
+    }
+    if (!project.manifest.isModule && !project.manifest.isPlugin) {
+      throwToolExit('AARs can only be built for plugin or module projects.');
+    }
+    // Validate that we can find an Android SDK.
+    if (androidSdk == null) {
+      throwToolExit('No Android SDK found. Try setting the `ANDROID_SDK_ROOT` environment variable.');
+    }
+    await buildGradleAar(
+      project: project,
+      androidBuildInfo: androidBuildInfo,
+      target: target,
+      outputDir: outputDir,
+    );
+    androidSdk.reinitialize();
+  }
+}
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index bf0ad9a..edcf064 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -17,8 +17,10 @@
 import '../base/process.dart';
 import '../base/terminal.dart';
 import '../base/utils.dart';
+import '../base/version.dart';
 import '../build_info.dart';
 import '../cache.dart';
+import '../features.dart';
 import '../flutter_manifest.dart';
 import '../globals.dart';
 import '../project.dart';
@@ -27,10 +29,10 @@
 import 'android_sdk.dart';
 import 'android_studio.dart';
 
-const String gradleVersion = '4.10.2';
 final RegExp _assembleTaskPattern = RegExp(r'assemble(\S+)');
 
-GradleProject _cachedGradleProject;
+GradleProject _cachedGradleAppProject;
+GradleProject _cachedGradleLibraryProject;
 String _cachedGradleExecutable;
 
 enum FlutterPluginVersion {
@@ -102,14 +104,19 @@
     case FlutterPluginVersion.managed:
       // Fall through. The managed plugin matches plugin v2 for now.
     case FlutterPluginVersion.v2:
-      return fs.file((await _gradleProject()).apkDirectory.childFile('app.apk'));
+      return fs.file((await _gradleAppProject()).apkDirectory.childFile('app.apk'));
   }
   return null;
 }
 
-Future<GradleProject> _gradleProject() async {
-  _cachedGradleProject ??= await _readGradleProject();
-  return _cachedGradleProject;
+Future<GradleProject> _gradleAppProject() async {
+  _cachedGradleAppProject ??= await _readGradleProject(isLibrary: false);
+  return _cachedGradleAppProject;
+}
+
+Future<GradleProject> _gradleLibraryProject() async {
+  _cachedGradleLibraryProject ??= await _readGradleProject(isLibrary: true);
+  return _cachedGradleLibraryProject;
 }
 
 /// Runs `gradlew dependencies`, ensuring that dependencies are resolved and
@@ -127,32 +134,101 @@
   progress.stop();
 }
 
+/// Tries to create `settings_aar.gradle` in an app project by removing the subprojects
+/// from the existing `settings.gradle` file. This operation will fail if the existing
+/// `settings.gradle` file has local edits.
+void createSettingsAarGradle(Directory androidDirectory) {
+  final File newSettingsFile = androidDirectory.childFile('settings_aar.gradle');
+  if (newSettingsFile.existsSync()) {
+    return;
+  }
+  final File currentSettingsFile = androidDirectory.childFile('settings.gradle');
+  if (!currentSettingsFile.existsSync()) {
+    return;
+  }
+  final String currentFileContent = currentSettingsFile.readAsStringSync();
+
+  final String newSettingsRelativeFile = fs.path.relative(newSettingsFile.path);
+  final Status status = logger.startProgress('✏️  Creating `$newSettingsRelativeFile`...',
+      timeout: timeoutConfiguration.fastOperation);
+
+  final String flutterRoot = fs.path.absolute(Cache.flutterRoot);
+  final File deprecatedFile = fs.file(fs.path.join(flutterRoot, 'packages','flutter_tools',
+      'gradle', 'deprecated_settings.gradle'));
+  assert(deprecatedFile.existsSync());
+  final String settingsAarContent = fs.file(fs.path.join(flutterRoot, 'packages','flutter_tools',
+      'gradle', 'settings_aar.gradle.tmpl')).readAsStringSync();
+
+  // Get the `settings.gradle` content variants that should be patched.
+  final List<String> existingVariants = deprecatedFile.readAsStringSync().split(';EOF');
+  existingVariants.add(settingsAarContent);
+
+  bool exactMatch = false;
+  for (String fileContentVariant in existingVariants) {
+    if (currentFileContent.trim() == fileContentVariant.trim()) {
+      exactMatch = true;
+      break;
+    }
+  }
+  if (!exactMatch) {
+    status.cancel();
+    printError('*******************************************************************************************');
+    printError('Flutter tried to create the file `$newSettingsRelativeFile`, but failed.');
+    // Print how to manually update the file.
+    printError(fs.file(fs.path.join(flutterRoot, 'packages','flutter_tools',
+        'gradle', 'manual_migration_settings.gradle.md')).readAsStringSync());
+    printError('*******************************************************************************************');
+    throwToolExit('Please create the file and run this command again.');
+  }
+  // Copy the new file.
+  newSettingsFile.writeAsStringSync(settingsAarContent);
+  status.stop();
+  printStatus('✅ `$newSettingsRelativeFile` created successfully.');
+}
+
 // Note: Dependencies are resolved and possibly downloaded as a side-effect
 // of calculating the app properties using Gradle. This may take minutes.
-Future<GradleProject> _readGradleProject() async {
+Future<GradleProject> _readGradleProject({bool isLibrary = false}) async {
   final FlutterProject flutterProject = FlutterProject.current();
   final String gradle = await _ensureGradle(flutterProject);
   updateLocalProperties(project: flutterProject);
+
+  final FlutterManifest manifest = flutterProject.manifest;
+  final Directory hostAppGradleRoot = flutterProject.android.hostAppGradleRoot;
+
+  if (featureFlags.isPluginAsAarEnabled &&
+      !manifest.isPlugin && !manifest.isModule) {
+    createSettingsAarGradle(hostAppGradleRoot);
+  }
+  if (manifest.isPlugin) {
+    assert(isLibrary);
+    return GradleProject(
+      <String>['debug', 'profile', 'release'],
+      <String>[], // Plugins don't have flavors.
+      flutterProject.directory.childDirectory('build').path,
+    );
+  }
   final Status status = logger.startProgress('Resolving dependencies...', timeout: timeoutConfiguration.slowOperation);
   GradleProject project;
+  // Get the properties and tasks from Gradle, so we can determinate the `buildDir`,
+  // flavors and build types defined in the project. If gradle fails, then check if the failure is due to t
   try {
     final RunResult propertiesRunResult = await runCheckedAsync(
-      <String>[gradle, 'app:properties'],
-      workingDirectory: flutterProject.android.hostAppGradleRoot.path,
+      <String>[gradle, isLibrary ? 'properties' : 'app:properties'],
+      workingDirectory: hostAppGradleRoot.path,
       environment: _gradleEnv,
     );
     final RunResult tasksRunResult = await runCheckedAsync(
-      <String>[gradle, 'app:tasks', '--all', '--console=auto'],
-      workingDirectory: flutterProject.android.hostAppGradleRoot.path,
+      <String>[gradle, isLibrary ? 'tasks': 'app:tasks', '--all', '--console=auto'],
+      workingDirectory: hostAppGradleRoot.path,
       environment: _gradleEnv,
     );
     project = GradleProject.fromAppProperties(propertiesRunResult.stdout, tasksRunResult.stdout);
   } catch (exception) {
     if (getFlutterPluginVersion(flutterProject.android) == FlutterPluginVersion.managed) {
       status.cancel();
-      // Handle known exceptions. This will exit if handled.
-      handleKnownGradleExceptions(exception.toString());
-
+      // Handle known exceptions.
+      throwToolExitIfLicenseNotAccepted(exception);
       // Print a general Gradle error and exit.
       printError('* Error running Gradle:\n$exception\n');
       throwToolExit('Please review your Gradle project setup in the android/ folder.');
@@ -160,23 +236,23 @@
     // Fall back to the default
     project = GradleProject(
       <String>['debug', 'profile', 'release'],
-      <String>[], flutterProject.android.gradleAppOutV1Directory,
-        flutterProject.android.gradleAppBundleOutV1Directory,
+      <String>[],
+      fs.path.join(flutterProject.android.hostAppGradleRoot.path, 'app', 'build')
     );
   }
   status.stop();
   return project;
 }
 
-void handleKnownGradleExceptions(String exceptionString) {
-  // Handle Gradle error thrown when Gradle needs to download additional
-  // Android SDK components (e.g. Platform Tools), and the license
-  // for that component has not been accepted.
-  const String matcher =
+/// Handle Gradle error thrown when Gradle needs to download additional
+/// Android SDK components (e.g. Platform Tools), and the license
+/// for that component has not been accepted.
+void throwToolExitIfLicenseNotAccepted(Exception exception) {
+  const String licenseNotAcceptedMatcher =
     r'You have not accepted the license agreements of the following SDK components:'
     r'\s*\[(.+)\]';
-  final RegExp licenseFailure = RegExp(matcher, multiLine: true);
-  final Match licenseMatch = licenseFailure.firstMatch(exceptionString);
+  final RegExp licenseFailure = RegExp(licenseNotAcceptedMatcher, multiLine: true);
+  final Match licenseMatch = licenseFailure.firstMatch(exception.toString());
   if (licenseMatch != null) {
     final String missingLicenses = licenseMatch.group(1);
     final String errorMessage =
@@ -233,6 +309,7 @@
   _locateGradlewExecutable(directory);
   final File propertiesFile = directory.childFile(fs.path.join('gradle', 'wrapper', 'gradle-wrapper.properties'));
   if (!propertiesFile.existsSync()) {
+    final String gradleVersion = getGradleVersionForAndroidPlugin(directory);
     propertiesFile.writeAsStringSync('''
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
@@ -244,6 +321,78 @@
   }
 }
 
+/// Returns true if [targetVersion] is within the range [min] and [max] inclusive.
+bool _isWithinVersionRange(String targetVersion, {String min, String max}) {
+  final Version parsedTargetVersion = Version.parse(targetVersion);
+  return parsedTargetVersion >= Version.parse(min) &&
+      parsedTargetVersion <= Version.parse(max);
+}
+
+const String defaultGradleVersion = '4.10.2';
+
+/// Returns the Gradle version that is required by the given Android Gradle plugin version
+/// by picking the largest compatible version from
+/// https://developer.android.com/studio/releases/gradle-plugin#updating-gradle
+String getGradleVersionFor(String androidPluginVersion) {
+  if (_isWithinVersionRange(androidPluginVersion, min: '1.0.0', max: '1.1.3')) {
+    return '2.3';
+  }
+  if (_isWithinVersionRange(androidPluginVersion, min: '1.2.0', max: '1.3.1')) {
+    return '2.9';
+  }
+  if (_isWithinVersionRange(androidPluginVersion, min: '1.5.0', max: '1.5.0')) {
+    return '2.2.1';
+  }
+  if (_isWithinVersionRange(androidPluginVersion, min: '2.0.0', max: '2.1.2')) {
+    return '2.13';
+  }
+  if (_isWithinVersionRange(androidPluginVersion, min: '2.1.3', max: '2.2.3')) {
+    return '2.14.1';
+  }
+  if (_isWithinVersionRange(androidPluginVersion, min: '2.3.0', max: '2.9.9')) {
+    return '3.3';
+  }
+  if (_isWithinVersionRange(androidPluginVersion, min: '3.0.0', max: '3.0.9')) {
+    return '4.1';
+  }
+  if (_isWithinVersionRange(androidPluginVersion, min: '3.1.0', max: '3.1.9')) {
+    return '4.4';
+  }
+  if (_isWithinVersionRange(androidPluginVersion, min: '3.2.0', max: '3.2.1')) {
+    return '4.6';
+  }
+  if (_isWithinVersionRange(androidPluginVersion, min: '3.3.0', max: '3.3.2')) {
+    return '4.10.2';
+  }
+  if (_isWithinVersionRange(androidPluginVersion, min: '3.4.0', max: '3.5.0')) {
+    return '5.1.1';
+  }
+  throwToolExit('Unsuported Android Plugin version: $androidPluginVersion.');
+  return '';
+}
+
+final RegExp _androidPluginRegExp = RegExp('com\.android\.tools\.build\:gradle\:(\\d+\.\\d+\.\\d+\)');
+
+/// Returns the Gradle version that the current Android plugin depends on when found,
+/// otherwise it returns a default version.
+///
+/// The Android plugin version is specified in the [build.gradle] file within
+/// the project's Android directory.
+String getGradleVersionForAndroidPlugin(Directory directory) {
+  final File buildFile = directory.childFile('build.gradle');
+  if (!buildFile.existsSync()) {
+    return defaultGradleVersion;
+  }
+  final String buildFileContent = buildFile.readAsStringSync();
+  final Iterable<Match> pluginMatches = _androidPluginRegExp.allMatches(buildFileContent);
+
+  if (pluginMatches.isEmpty) {
+    return defaultGradleVersion;
+  }
+  final String androidPluginVersion = pluginMatches.first.group(1);
+  return getGradleVersionFor(androidPluginVersion);
+}
+
 /// Overwrite local.properties in the specified Flutter project's Android
 /// sub-project, if needed.
 ///
@@ -347,6 +496,95 @@
   }
 }
 
+Future<void> buildGradleAar({
+  @required FlutterProject project,
+  @required AndroidBuildInfo androidBuildInfo,
+  @required String target,
+  @required String outputDir,
+}) async {
+  final FlutterManifest manifest = project.manifest;
+
+  GradleProject gradleProject;
+  if (manifest.isModule) {
+    gradleProject = await _gradleAppProject();
+  } else if (manifest.isPlugin) {
+    gradleProject = await _gradleLibraryProject();
+  } else {
+    throwToolExit('AARs can only be built for plugin or module projects.');
+  }
+
+  if (outputDir != null && outputDir.isNotEmpty) {
+    gradleProject.buildDirectory = outputDir;
+  }
+
+  final String aarTask = gradleProject.aarTaskFor(androidBuildInfo.buildInfo);
+  if (aarTask == null) {
+    printUndefinedTask(gradleProject, androidBuildInfo.buildInfo);
+    throwToolExit('Gradle build aborted.');
+  }
+  final Status status = logger.startProgress(
+    'Running Gradle task \'$aarTask\'...',
+    timeout: timeoutConfiguration.slowOperation,
+    multilineOutput: true,
+  );
+
+  final String gradle = await _ensureGradle(project);
+  final String gradlePath = fs.file(gradle).absolute.path;
+  final String flutterRoot = fs.path.absolute(Cache.flutterRoot);
+  final String initScript = fs.path.join(flutterRoot, 'packages','flutter_tools', 'gradle', 'aar_init_script.gradle');
+  final List<String> command = <String>[
+    gradlePath,
+    '-I=$initScript',
+    '-Pflutter-root=$flutterRoot',
+    '-Poutput-dir=${gradleProject.buildDirectory}',
+    '-Pis-plugin=${manifest.isPlugin}',
+    '-Dbuild-plugins-as-aars=true',
+  ];
+
+  if (target != null && target.isNotEmpty) {
+    command.add('-Ptarget=$target');
+  }
+
+  if (androidBuildInfo.targetArchs.isNotEmpty) {
+    final String targetPlatforms = androidBuildInfo.targetArchs
+        .map(getPlatformNameForAndroidArch).join(',');
+    command.add('-Ptarget-platform=$targetPlatforms');
+  }
+  command.add(aarTask);
+
+  final Stopwatch sw = Stopwatch()..start();
+  int exitCode = 1;
+
+  try {
+    exitCode = await runCommandAndStreamOutput(
+      command,
+      workingDirectory: project.android.hostAppGradleRoot.path,
+      allowReentrantFlutter: true,
+      environment: _gradleEnv,
+      mapFunction: (String line) {
+        // Always print the full line in verbose mode.
+        if (logger.isVerbose) {
+          return line;
+        }
+        return null;
+      },
+    );
+  } finally {
+    status.stop();
+  }
+  flutterUsage.sendTiming('build', 'gradle-aar', Duration(milliseconds: sw.elapsedMilliseconds));
+
+  if (exitCode != 0) {
+    throwToolExit('Gradle task $aarTask failed with exit code $exitCode', exitCode: exitCode);
+  }
+
+  final Directory repoDirectory = gradleProject.repoDirectory;
+  if (!repoDirectory.existsSync()) {
+    throwToolExit('Gradle task $aarTask failed to produce $repoDirectory', exitCode: exitCode);
+  }
+  printStatus('Built ${fs.path.relative(repoDirectory.path)}.', color: TerminalColor.green);
+}
+
 Future<void> _buildGradleProjectV1(FlutterProject project, String gradle) async {
   // Run 'gradlew build'.
   final Status status = logger.startProgress(
@@ -389,6 +627,22 @@
   return sha;
 }
 
+void printUndefinedTask(GradleProject project, BuildInfo buildInfo) {
+  printError('');
+  printError('The Gradle project does not define a task suitable for the requested build.');
+  if (!project.buildTypes.contains(buildInfo.modeName)) {
+    printError('Review the android/app/build.gradle file and ensure it defines a ${buildInfo.modeName} build type.');
+    return;
+  }
+  if (project.productFlavors.isEmpty) {
+    printError('The android/app/build.gradle file does not define any custom product flavors.');
+    printError('You cannot use the --flavor option.');
+  } else {
+    printError('The android/app/build.gradle file defines product flavors: ${project.productFlavors.join(', ')}');
+    printError('You must specify a --flavor option to select one of them.');
+  }
+}
+
 Future<void> _buildGradleProjectV2(
   FlutterProject flutterProject,
   String gradle,
@@ -396,7 +650,7 @@
   String target,
   bool isBuildingBundle,
 ) async {
-  final GradleProject project = await _gradleProject();
+  final GradleProject project = await _gradleAppProject();
   final BuildInfo buildInfo = androidBuildInfo.buildInfo;
 
   String assembleTask;
@@ -406,22 +660,9 @@
   } else {
     assembleTask = project.assembleTaskFor(buildInfo);
   }
-
   if (assembleTask == null) {
-    printError('');
-    printError('The Gradle project does not define a task suitable for the requested build.');
-    if (!project.buildTypes.contains(buildInfo.modeName)) {
-      printError('Review the android/app/build.gradle file and ensure it defines a ${buildInfo.modeName} build type.');
-    } else {
-      if (project.productFlavors.isEmpty) {
-        printError('The android/app/build.gradle file does not define any custom product flavors.');
-        printError('You cannot use the --flavor option.');
-      } else {
-        printError('The android/app/build.gradle file defines product flavors: ${project.productFlavors.join(', ')}');
-        printError('You must specify a --flavor option to select one of them.');
-      }
-      throwToolExit('Gradle build aborted.');
-    }
+    printUndefinedTask(project, buildInfo);
+    throwToolExit('Gradle build aborted.');
   }
   final Status status = logger.startProgress(
     'Running Gradle task \'$assembleTask\'...',
@@ -460,6 +701,14 @@
         .map(getPlatformNameForAndroidArch).join(',');
     command.add('-Ptarget-platform=$targetPlatforms');
   }
+  if (featureFlags.isPluginAsAarEnabled) {
+     // Pass a system flag instead of a project flag, so this flag can be
+     // read from include_flutter.groovy.
+    command.add('-Dbuild-plugins-as-aars=true');
+    if (!flutterProject.manifest.isModule) {
+      command.add('--settings-file=settings_aar.gradle');
+    }
+  }
   command.add(assembleTask);
   bool potentialAndroidXFailure = false;
   final Stopwatch sw = Stopwatch()..start();
@@ -604,7 +853,6 @@
     // Use java bundled with Android Studio.
     env['JAVA_HOME'] = javaPath;
   }
-
   // Don't log analytics for downstream Flutter commands.
   // e.g. `flutter build bundle`.
   env['FLUTTER_SUPPRESS_ANALYTICS'] = 'true';
@@ -612,11 +860,15 @@
 }
 
 class GradleProject {
-  GradleProject(this.buildTypes, this.productFlavors, this.apkDirectory, this.bundleDirectory);
+  GradleProject(
+    this.buildTypes,
+    this.productFlavors,
+    this.buildDirectory,
+  );
 
   factory GradleProject.fromAppProperties(String properties, String tasks) {
     // Extract build directory.
-    final String buildDir = properties
+    final String buildDirectory = properties
         .split('\n')
         .firstWhere((String s) => s.startsWith('buildDir: '))
         .substring('buildDir: '.length)
@@ -648,17 +900,36 @@
     if (productFlavors.isEmpty)
       buildTypes.addAll(variants);
     return GradleProject(
-      buildTypes.toList(),
-      productFlavors.toList(),
-      fs.directory(fs.path.join(buildDir, 'outputs', 'apk')),
-      fs.directory(fs.path.join(buildDir, 'outputs', 'bundle')),
-    );
+        buildTypes.toList(),
+        productFlavors.toList(),
+        buildDirectory,
+      );
   }
 
+  /// The build types such as [release] or [debug].
   final List<String> buildTypes;
+
+  /// The product flavors defined in build.gradle.
   final List<String> productFlavors;
-  final Directory apkDirectory;
-  final Directory bundleDirectory;
+
+  /// The build directory. This is typically <project>build/.
+  String buildDirectory;
+
+  /// The directory where the APK artifact is generated.
+  Directory get apkDirectory {
+    return fs.directory(fs.path.join(buildDirectory, 'outputs', 'apk'));
+  }
+
+  /// The directory where the app bundle artifact is generated.
+  Directory get bundleDirectory {
+    return fs.directory(fs.path.join(buildDirectory, 'outputs', 'bundle'));
+  }
+
+  /// The directory where the repo is generated.
+  /// Only applicable to AARs.
+  Directory get repoDirectory {
+    return fs.directory(fs.path.join(buildDirectory, 'outputs', 'repo'));
+  }
 
   String _buildTypeFor(BuildInfo buildInfo) {
     final String modeName = camelCase(buildInfo.modeName);
@@ -708,6 +979,14 @@
     return 'bundle${toTitleCase(productFlavor)}${toTitleCase(buildType)}';
   }
 
+  String aarTaskFor(BuildInfo buildInfo) {
+    final String buildType = _buildTypeFor(buildInfo);
+    final String productFlavor = _productFlavorFor(buildInfo);
+    if (buildType == null || productFlavor == null)
+      return null;
+    return 'assembleAar${toTitleCase(productFlavor)}${toTitleCase(buildType)}';
+  }
+
   String bundleFileFor(BuildInfo buildInfo) {
     // For app bundle all bundle names are called as app.aab. Product flavors
     // & build types are differentiated as folders, where the aab will be added.
diff --git a/packages/flutter_tools/lib/src/commands/build.dart b/packages/flutter_tools/lib/src/commands/build.dart
index 1691b57..a443980 100644
--- a/packages/flutter_tools/lib/src/commands/build.dart
+++ b/packages/flutter_tools/lib/src/commands/build.dart
@@ -9,6 +9,7 @@
 import '../commands/build_windows.dart';
 
 import '../runner/flutter_command.dart';
+import 'build_aar.dart';
 import 'build_aot.dart';
 import 'build_apk.dart';
 import 'build_appbundle.dart';
@@ -19,6 +20,7 @@
 
 class BuildCommand extends FlutterCommand {
   BuildCommand({bool verboseHelp = false}) {
+    addSubcommand(BuildAarCommand(verboseHelp: verboseHelp));
     addSubcommand(BuildApkCommand(verboseHelp: verboseHelp));
     addSubcommand(BuildAppBundleCommand(verboseHelp: verboseHelp));
     addSubcommand(BuildAotCommand());
diff --git a/packages/flutter_tools/lib/src/commands/build_aar.dart b/packages/flutter_tools/lib/src/commands/build_aar.dart
new file mode 100644
index 0000000..e40f522
--- /dev/null
+++ b/packages/flutter_tools/lib/src/commands/build_aar.dart
@@ -0,0 +1,94 @@
+// Copyright 2019 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 '../android/aar.dart';
+import '../base/context.dart';
+import '../base/os.dart';
+import '../build_info.dart';
+import '../project.dart';
+import '../reporting/usage.dart';
+import '../runner/flutter_command.dart' show DevelopmentArtifact, FlutterCommandResult;
+import 'build.dart';
+
+/// The AAR builder in the current context.
+AarBuilder get aarBuilder => context.get<AarBuilder>() ?? AarBuilderImpl();
+
+class BuildAarCommand extends BuildSubCommand {
+  BuildAarCommand({bool verboseHelp = false}) {
+    addBuildModeFlags(verboseHelp: verboseHelp);
+    usesFlavorOption();
+    usesPubOption();
+    argParser
+      ..addMultiOption('target-platform',
+        splitCommas: true,
+        defaultsTo: <String>['android-arm', 'android-arm64'],
+        allowed: <String>['android-arm', 'android-arm64', 'android-x86', 'android-x64'],
+        help: 'The target platform for which the project is compiled.',
+      )
+      ..addOption('output-dir',
+        help: 'The absolute path to the directory where the repository is generated.'
+              'By default, this is \'<current-directory>android/build\'. ',
+      );
+  }
+
+  @override
+  final String name = 'aar';
+
+  @override
+  Future<Map<String, String>> get usageValues async {
+    final Map<String, String> usage = <String, String>{};
+    final FlutterProject futterProject = _getProject();
+    if (futterProject == null) {
+      return usage;
+    }
+    if (futterProject.manifest.isModule) {
+      usage[kCommandBuildAarProjectType] = 'module';
+    } else if (futterProject.manifest.isPlugin) {
+      usage[kCommandBuildAarProjectType] = 'plugin';
+    } else {
+      usage[kCommandBuildAarProjectType] = 'app';
+    }
+    usage[kCommandBuildAarTargetPlatform] =
+        (argResults['target-platform'] as List<String>).join(',');
+    return usage;
+  }
+
+  @override
+  Future<Set<DevelopmentArtifact>> get requiredArtifacts async => const <DevelopmentArtifact>{
+    DevelopmentArtifact.universal,
+    DevelopmentArtifact.android,
+  };
+
+  @override
+  final String description = 'Build a repository containing an AAR and a POM file.\n\n'
+      'The POM file is used to include the dependencies that the AAR was compiled against.\n\n'
+      'To learn more about how to use these artifacts, see '
+      'https://docs.gradle.org/current/userguide/repository_types.html#sub:maven_local';
+
+  @override
+  Future<FlutterCommandResult> runCommand() async {
+    final BuildInfo buildInfo = getBuildInfo();
+    final AndroidBuildInfo androidBuildInfo = AndroidBuildInfo(buildInfo,
+        targetArchs: argResults['target-platform'].map<AndroidArch>(getAndroidArchForName));
+
+    await aarBuilder.build(
+      project: _getProject(),
+      target: '', // Not needed because this command only builds Android's code.
+      androidBuildInfo: androidBuildInfo,
+      outputDir: argResults['output-dir'],
+    );
+    return null;
+  }
+
+  /// Returns the [FlutterProject] which is determinated from the remaining command-line
+  /// argument if any or the current working directory.
+  FlutterProject _getProject() {
+    if (argResults.rest.isEmpty) {
+      return FlutterProject.current();
+    }
+    return FlutterProject.fromPath(findProjectRoot(argResults.rest.first));
+  }
+}
diff --git a/packages/flutter_tools/lib/src/features.dart b/packages/flutter_tools/lib/src/features.dart
index 69157a8..c56c77d 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 plugins are built as AARs in app projects.
+  bool get isPluginAsAarEnabled => _isEnabled(flutterBuildPluginAsAarFeature);
+
   // Calculate whether a particular feature is enabled for the current channel.
   static bool _isEnabled(Feature feature) {
     final String currentChannel = FlutterVersion.instance.channel;
@@ -65,6 +68,7 @@
   flutterLinuxDesktopFeature,
   flutterMacOSDesktopFeature,
   flutterWindowsDesktopFeature,
+  flutterBuildPluginAsAarFeature,
 ];
 
 /// The [Feature] for flutter web.
@@ -115,6 +119,20 @@
   ),
 );
 
+/// The [Feature] for building plugins as AARs in an app project.
+const Feature flutterBuildPluginAsAarFeature = Feature(
+  name: 'Build plugins independently as AARs in app projects',
+  configSetting: 'enable-build-plugin-as-aar',
+  master: FeatureChannelSetting(
+    available: true,
+    enabledByDefault: true,
+  ),
+  dev: 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/project.dart b/packages/flutter_tools/lib/src/project.dart
index 95b526e..ece0846 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -510,10 +510,6 @@
     return fs.directory(fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'apk'));
   }
 
-  Directory get gradleAppBundleOutV1Directory {
-    return fs.directory(fs.path.join(hostAppGradleRoot.path, 'app', 'build', 'outputs', 'bundle'));
-  }
-
   /// Whether the current flutter project has an Android sub-project.
   bool existsSync() {
     return parent.isModule || _editableHostAppDirectory.existsSync();
diff --git a/packages/flutter_tools/lib/src/reporting/usage.dart b/packages/flutter_tools/lib/src/reporting/usage.dart
index ea2c739..5a596d3 100644
--- a/packages/flutter_tools/lib/src/reporting/usage.dart
+++ b/packages/flutter_tools/lib/src/reporting/usage.dart
@@ -57,13 +57,16 @@
 const String kCommandResult = 'cd26';
 const String kCommandHasTerminal = 'cd31';
 
+const String kCommandBuildAarTargetPlatform = 'cd34';
+const String kCommandBuildAarProjectType = 'cd35';
+
 const String reloadExceptionTargetPlatform = 'cd27';
 const String reloadExceptionSdkName = 'cd28';
 const String reloadExceptionEmulator = 'cd29';
 const String reloadExceptionFullRestart = 'cd30';
 
 const String enabledFlutterFeatures = 'cd32';
-// Next ID: cd34
+// Next ID: cd36
 
 Usage get flutterUsage => Usage.instance;
 
diff --git a/packages/flutter_tools/templates/module/android/library/Flutter.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/module/android/library/Flutter.tmpl/build.gradle.tmpl
index e08734e..0a8051b 100644
--- a/packages/flutter_tools/templates/module/android/library/Flutter.tmpl/build.gradle.tmpl
+++ b/packages/flutter_tools/templates/module/android/library/Flutter.tmpl/build.gradle.tmpl
@@ -26,6 +26,9 @@
 apply plugin: 'com.android.library'
 apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
 
+group '{{androidIdentifier}}'
+version '1.0'
+
 android {
     compileSdkVersion 28
 
diff --git a/packages/flutter_tools/templates/module/android/library/include_flutter.groovy.copy.tmpl b/packages/flutter_tools/templates/module/android/library/include_flutter.groovy.copy.tmpl
index c6939be..7be7efb 100644
--- a/packages/flutter_tools/templates/module/android/library/include_flutter.groovy.copy.tmpl
+++ b/packages/flutter_tools/templates/module/android/library/include_flutter.groovy.copy.tmpl
@@ -6,24 +6,37 @@
 gradle.include ':flutter'
 gradle.project(':flutter').projectDir = new File(flutterProjectRoot, '.android/Flutter')
 
-def plugins = new Properties()
-def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins')
-if (pluginsFile.exists()) {
-    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
-}
+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
+    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 ->
diff --git a/packages/flutter_tools/templates/plugin/android-java.tmpl/build.gradle.tmpl b/packages/flutter_tools/templates/plugin/android-java.tmpl/build.gradle.tmpl
index 20d1a38..c16a394 100644
--- a/packages/flutter_tools/templates/plugin/android-java.tmpl/build.gradle.tmpl
+++ b/packages/flutter_tools/templates/plugin/android-java.tmpl/build.gradle.tmpl
@@ -1,5 +1,5 @@
 group '{{androidIdentifier}}'
-version '1.0-SNAPSHOT'
+version '1.0'
 
 buildscript {
     repositories {
diff --git a/packages/flutter_tools/templates/plugin/android.tmpl/gradle/wrapper/gradle-wrapper.properties b/packages/flutter_tools/templates/plugin/android.tmpl/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..019065d
--- /dev/null
+++ b/packages/flutter_tools/templates/plugin/android.tmpl/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
diff --git a/packages/flutter_tools/test/general.shard/android/gradle_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_test.dart
index b09f73f..d6380f0 100644
--- a/packages/flutter_tools/test/general.shard/android/gradle_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/gradle_test.dart
@@ -6,7 +6,9 @@
 
 import 'package:file/memory.dart';
 import 'package:flutter_tools/src/android/gradle.dart';
+import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/base/common.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/cache.dart';
@@ -168,20 +170,20 @@
       expect(project.productFlavors, <String>['free', 'paid']);
     });
     test('should provide apk file name for default build types', () {
-      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], '/some/dir');
       expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo.debug)).first, 'app-debug.apk');
       expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo.profile)).first, 'app-profile.apk');
       expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo.release)).first, 'app-release.apk');
       expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.release, 'unknown'))).isEmpty, isTrue);
     });
     test('should provide apk file name for flavored build types', () {
-      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], '/some/dir');
       expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.debug, 'free'))).first, 'app-free-debug.apk');
       expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.release, 'paid'))).first, 'app-paid-release.apk');
       expect(project.apkFilesFor(const AndroidBuildInfo(BuildInfo(BuildMode.release, 'unknown'))).isEmpty, isTrue);
     });
     test('should provide apks for default build types and each ABI', () {
-      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], '/some/dir');
       expect(project.apkFilesFor(
         const AndroidBuildInfo(
           BuildInfo.debug,
@@ -224,7 +226,7 @@
           ).isEmpty, isTrue);
     });
     test('should provide apks for each ABI and flavored build types', () {
-      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], '/some/dir');
       expect(project.apkFilesFor(
         const AndroidBuildInfo(
           BuildInfo(BuildMode.debug, 'free'),
@@ -267,54 +269,187 @@
           ).isEmpty, isTrue);
     });
     test('should provide bundle file name for default build types', () {
-      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], '/some/dir');
       expect(project.bundleFileFor(BuildInfo.debug), 'app.aab');
       expect(project.bundleFileFor(BuildInfo.profile), 'app.aab');
       expect(project.bundleFileFor(BuildInfo.release), 'app.aab');
       expect(project.bundleFileFor(const BuildInfo(BuildMode.release, 'unknown')), 'app.aab');
     });
     test('should provide bundle file name for flavored build types', () {
-      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], '/some/dir');
       expect(project.bundleFileFor(const BuildInfo(BuildMode.debug, 'free')), 'app.aab');
       expect(project.bundleFileFor(const BuildInfo(BuildMode.release, 'paid')), 'app.aab');
       expect(project.bundleFileFor(const BuildInfo(BuildMode.release, 'unknown')), 'app.aab');
     });
     test('should provide assemble task name for default build types', () {
-      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], '/some/dir');
       expect(project.assembleTaskFor(BuildInfo.debug), 'assembleDebug');
       expect(project.assembleTaskFor(BuildInfo.profile), 'assembleProfile');
       expect(project.assembleTaskFor(BuildInfo.release), 'assembleRelease');
       expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
     });
     test('should provide assemble task name for flavored build types', () {
-      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], '/some/dir');
       expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'free')), 'assembleFreeDebug');
       expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'paid')), 'assemblePaidRelease');
       expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
     });
     test('should respect format of the flavored build types', () {
-      final GradleProject project = GradleProject(<String>['debug'], <String>['randomFlavor'], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      final GradleProject project = GradleProject(<String>['debug'], <String>['randomFlavor'], '/some/dir');
       expect(project.assembleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'assembleRandomFlavorDebug');
     });
     test('bundle should provide assemble task name for default build types', () {
-      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>[], '/some/dir');
       expect(project.bundleTaskFor(BuildInfo.debug), 'bundleDebug');
       expect(project.bundleTaskFor(BuildInfo.profile), 'bundleProfile');
       expect(project.bundleTaskFor(BuildInfo.release), 'bundleRelease');
       expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
     });
     test('bundle should provide assemble task name for flavored build types', () {
-      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], '/some/dir');
       expect(project.bundleTaskFor(const BuildInfo(BuildMode.debug, 'free')), 'bundleFreeDebug');
       expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'paid')), 'bundlePaidRelease');
       expect(project.bundleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
     });
     test('bundle should respect format of the flavored build types', () {
-      final GradleProject project = GradleProject(<String>['debug'], <String>['randomFlavor'], fs.directory('/some/dir'),fs.directory('/some/dir'));
+      final GradleProject project = GradleProject(<String>['debug'], <String>['randomFlavor'], '/some/dir');
       expect(project.bundleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'bundleRandomFlavorDebug');
     });
   });
 
+  group('Config files', () {
+    BufferLogger mockLogger;
+    Directory tempDir;
+
+    setUp(() {
+      mockLogger = BufferLogger();
+      tempDir = fs.systemTempDirectory.createTempSync('settings_aar_test.');
+
+    });
+
+    testUsingContext('create settings_aar.gradle when current settings.gradle loads plugins', () {
+      const String currentSettingsGradle = '''
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+    include ":\$name"
+    project(":\$name").projectDir = pluginDirectory
+}
+''';
+
+      const String settingsAarFile = '''
+include ':app'
+''';
+
+      tempDir.childFile('settings.gradle').writeAsStringSync(currentSettingsGradle);
+
+      final String toolGradlePath = fs.path.join(
+          fs.path.absolute(Cache.flutterRoot),
+          'packages',
+          'flutter_tools',
+          'gradle');
+      fs.directory(toolGradlePath).createSync(recursive: true);
+      fs.file(fs.path.join(toolGradlePath, 'deprecated_settings.gradle'))
+          .writeAsStringSync(currentSettingsGradle);
+
+      fs.file(fs.path.join(toolGradlePath, 'settings_aar.gradle.tmpl'))
+          .writeAsStringSync(settingsAarFile);
+
+      createSettingsAarGradle(tempDir);
+
+      expect(mockLogger.statusText, contains('created successfully'));
+      expect(tempDir.childFile('settings_aar.gradle').existsSync(), isTrue);
+
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      Logger: () => mockLogger,
+    });
+
+    testUsingContext('create settings_aar.gradle when current settings.gradle doesn\'t load plugins', () {
+      const String currentSettingsGradle = '''
+include ':app'
+''';
+
+      const String settingsAarFile = '''
+include ':app'
+''';
+
+      tempDir.childFile('settings.gradle').writeAsStringSync(currentSettingsGradle);
+
+      final String toolGradlePath = fs.path.join(
+          fs.path.absolute(Cache.flutterRoot),
+          'packages',
+          'flutter_tools',
+          'gradle');
+      fs.directory(toolGradlePath).createSync(recursive: true);
+      fs.file(fs.path.join(toolGradlePath, 'deprecated_settings.gradle'))
+          .writeAsStringSync(currentSettingsGradle);
+
+      fs.file(fs.path.join(toolGradlePath, 'settings_aar.gradle.tmpl'))
+          .writeAsStringSync(settingsAarFile);
+
+      createSettingsAarGradle(tempDir);
+
+      expect(mockLogger.statusText, contains('created successfully'));
+      expect(tempDir.childFile('settings_aar.gradle').existsSync(), isTrue);
+
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      Logger: () => mockLogger,
+    });
+  });
+
+  group('Undefined task', () {
+    BufferLogger mockLogger;
+
+    setUp(() {
+      mockLogger = BufferLogger();
+    });
+
+    testUsingContext('print undefined build type', () {
+      final GradleProject project = GradleProject(<String>['debug', 'release'],
+          const <String>['free', 'paid'], '/some/dir');
+
+      printUndefinedTask(project, const BuildInfo(BuildMode.profile, 'unknown'));
+      expect(mockLogger.errorText, contains('The Gradle project does not define a task suitable for the requested build'));
+      expect(mockLogger.errorText, contains('Review the android/app/build.gradle file and ensure it defines a profile build type'));
+    }, overrides: <Type, Generator>{
+      Logger: () => mockLogger,
+    });
+
+    testUsingContext('print no flavors', () {
+      final GradleProject project = GradleProject(<String>['debug', 'release'],
+          const <String>[], '/some/dir');
+
+      printUndefinedTask(project, const BuildInfo(BuildMode.debug, 'unknown'));
+      expect(mockLogger.errorText, contains('The Gradle project does not define a task suitable for the requested build'));
+      expect(mockLogger.errorText, contains('The android/app/build.gradle file does not define any custom product flavors'));
+      expect(mockLogger.errorText, contains('You cannot use the --flavor option'));
+    }, overrides: <Type, Generator>{
+      Logger: () => mockLogger,
+    });
+
+    testUsingContext('print flavors', () {
+      final GradleProject project = GradleProject(<String>['debug', 'release'],
+          const <String>['free', 'paid'], '/some/dir');
+
+      printUndefinedTask(project, const BuildInfo(BuildMode.debug, 'unknown'));
+      expect(mockLogger.errorText, contains('The Gradle project does not define a task suitable for the requested build'));
+      expect(mockLogger.errorText, contains('The android/app/build.gradle file defines product flavors: free, paid'));
+    }, overrides: <Type, Generator>{
+      Logger: () => mockLogger,
+    });
+  });
+
   group('Gradle local.properties', () {
     MockLocalEngineArtifacts mockArtifacts;
     MockProcessManager mockProcessManager;
@@ -540,6 +675,52 @@
       );
     });
   });
+
+  group('gradle version', () {
+    test('should be compatible with the Android plugin version', () {
+      // Granular versions.
+      expect(getGradleVersionFor('1.0.0'), '2.3');
+      expect(getGradleVersionFor('1.0.1'), '2.3');
+      expect(getGradleVersionFor('1.0.2'), '2.3');
+      expect(getGradleVersionFor('1.0.4'), '2.3');
+      expect(getGradleVersionFor('1.0.8'), '2.3');
+      expect(getGradleVersionFor('1.1.0'), '2.3');
+      expect(getGradleVersionFor('1.1.2'), '2.3');
+      expect(getGradleVersionFor('1.1.2'), '2.3');
+      expect(getGradleVersionFor('1.1.3'), '2.3');
+      // Version Ranges.
+      expect(getGradleVersionFor('1.2.0'), '2.9');
+      expect(getGradleVersionFor('1.3.1'), '2.9');
+
+      expect(getGradleVersionFor('1.5.0'), '2.2.1');
+
+      expect(getGradleVersionFor('2.0.0'), '2.13');
+      expect(getGradleVersionFor('2.1.2'), '2.13');
+
+      expect(getGradleVersionFor('2.1.3'), '2.14.1');
+      expect(getGradleVersionFor('2.2.3'), '2.14.1');
+
+      expect(getGradleVersionFor('2.3.0'), '3.3');
+
+      expect(getGradleVersionFor('3.0.0'), '4.1');
+
+      expect(getGradleVersionFor('3.1.0'), '4.4');
+
+      expect(getGradleVersionFor('3.2.0'), '4.6');
+      expect(getGradleVersionFor('3.2.1'), '4.6');
+
+      expect(getGradleVersionFor('3.3.0'), '4.10.2');
+      expect(getGradleVersionFor('3.3.2'), '4.10.2');
+
+      expect(getGradleVersionFor('3.4.0'), '5.1.1');
+      expect(getGradleVersionFor('3.5.0'), '5.1.1');
+    });
+
+    test('throws on unsupported versions', () {
+      expect(() => getGradleVersionFor('3.6.0'),
+          throwsA(predicate<Exception>((Exception e) => e is ToolExit)));
+    });
+  });
 }
 
 Platform fakePlatform(String name) {
diff --git a/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart b/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart
new file mode 100644
index 0000000..d02d764
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart
@@ -0,0 +1,87 @@
+// Copyright 2019 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 'package:args/command_runner.dart';
+import 'package:flutter_tools/src/android/aar.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/commands/build_aar.dart';
+import 'package:flutter_tools/src/reporting/usage.dart';
+import 'package:mockito/mockito.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+  Cache.disableLocking();
+
+  group('getUsage', () {
+    Directory tempDir;
+    AarBuilder mockAarBuilder;
+
+    setUp(() {
+      tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_packages_test.');
+      mockAarBuilder = MockAarBuilder();
+      when(mockAarBuilder.build(
+          project: anyNamed('project'),
+          androidBuildInfo: anyNamed('androidBuildInfo'),
+          target: anyNamed('target'),
+          outputDir: anyNamed('outputDir'))).thenAnswer((_) => Future<void>.value());
+    });
+
+    tearDown(() {
+      tryToDelete(tempDir);
+    });
+
+    Future<BuildAarCommand> runCommandIn(String target, { List<String> arguments }) async {
+      final BuildAarCommand command = BuildAarCommand();
+      final CommandRunner<void> runner = createTestCommandRunner(command);
+      await runner.run(<String>[
+        'aar',
+        ...?arguments,
+        target,
+      ]);
+      return command;
+    }
+
+    testUsingContext('indicate that project is a module', () async {
+      final String projectPath = await createProject(tempDir,
+          arguments: <String>['--no-pub', '--template=module']);
+
+      final BuildAarCommand command = await runCommandIn(projectPath);
+      expect(await command.usageValues,
+          containsPair(kCommandBuildAarProjectType, 'module'));
+
+    }, overrides: <Type, Generator>{
+      AarBuilder: () => mockAarBuilder,
+    }, timeout: allowForCreateFlutterProject);
+
+    testUsingContext('indicate that project is a plugin', () async {
+      final String projectPath = await createProject(tempDir,
+          arguments: <String>['--no-pub', '--template=plugin', '--project-name=aar_test']);
+
+      final BuildAarCommand command = await runCommandIn(projectPath);
+      expect(await command.usageValues,
+          containsPair(kCommandBuildAarProjectType, 'plugin'));
+
+    }, overrides: <Type, Generator>{
+      AarBuilder: () => mockAarBuilder,
+    }, timeout: allowForCreateFlutterProject);
+
+    testUsingContext('indicate the target platform', () async {
+      final String projectPath = await createProject(tempDir,
+          arguments: <String>['--no-pub', '--template=module']);
+
+      final BuildAarCommand command = await runCommandIn(projectPath,
+          arguments: <String>['--target-platform=android-arm']);
+      expect(await command.usageValues,
+          containsPair(kCommandBuildAarTargetPlatform, 'android-arm'));
+
+    }, overrides: <Type, Generator>{
+      AarBuilder: () => mockAarBuilder,
+    }, timeout: allowForCreateFlutterProject);
+  });
+}
+
+class MockAarBuilder extends Mock implements AarBuilder {}
diff --git a/packages/flutter_tools/test/general.shard/features_test.dart b/packages/flutter_tools/test/general.shard/features_test.dart
index 94b336b..5f56e51 100644
--- a/packages/flutter_tools/test/general.shard/features_test.dart
+++ b/packages/flutter_tools/test/general.shard/features_test.dart
@@ -418,6 +418,21 @@
 
       expect(featureFlags.isWindowsEnabled, false);
     }));
+
+    /// Plugins as AARS
+    test('plugins built as AARs with config on master', () => testbed.run(() {
+      when(mockFlutterVerion.channel).thenReturn('master');
+      when<bool>(mockFlutterConfig.getValue('enable-build-plugin-as-aar')).thenReturn(true);
+
+      expect(featureFlags.isPluginAsAarEnabled, true);
+    }));
+
+    test('plugins built as AARs with config on dev', () => testbed.run(() {
+      when(mockFlutterVerion.channel).thenReturn('dev');
+      when<bool>(mockFlutterConfig.getValue('enable-build-plugin-as-aar')).thenReturn(true);
+
+      expect(featureFlags.isPluginAsAarEnabled, true);
+    }));
   });
 }
 
diff --git a/packages/flutter_tools/test/src/testbed.dart b/packages/flutter_tools/test/src/testbed.dart
index c0a6873..634acf0 100644
--- a/packages/flutter_tools/test/src/testbed.dart
+++ b/packages/flutter_tools/test/src/testbed.dart
@@ -697,6 +697,7 @@
     this.isMacOSEnabled = false,
     this.isWebEnabled = false,
     this.isWindowsEnabled = false,
+    this.isPluginAsAarEnabled = false,
 });
 
   @override
@@ -710,4 +711,7 @@
 
   @override
   final bool isWindowsEnabled;
+
+  @override
+  final bool isPluginAsAarEnabled;
 }