Refactor gradle.dart (#43479)

diff --git a/packages/flutter_tools/lib/src/android/android_builder.dart b/packages/flutter_tools/lib/src/android/android_builder.dart
index c06925f..77bf76a 100644
--- a/packages/flutter_tools/lib/src/android/android_builder.dart
+++ b/packages/flutter_tools/lib/src/android/android_builder.dart
@@ -6,19 +6,23 @@
 
 import 'package:meta/meta.dart';
 
-import '../base/common.dart';
+import '../android/gradle_errors.dart';
 import '../base/context.dart';
+import '../base/file_system.dart';
 import '../build_info.dart';
 import '../project.dart';
-
 import 'android_sdk.dart';
 import 'gradle.dart';
 
 /// The builder in the current context.
-AndroidBuilder get androidBuilder => context.get<AndroidBuilder>() ?? _AndroidBuilderImpl();
+AndroidBuilder get androidBuilder {
+  return context.get<AndroidBuilder>() ?? const _AndroidBuilderImpl();
+}
 
 /// Provides the methods to build Android artifacts.
+// TODO(egarciad): https://github.com/flutter/flutter/issues/43863
 abstract class AndroidBuilder {
+  const AndroidBuilder();
   /// Builds an AAR artifact.
   Future<void> buildAar({
     @required FlutterProject project,
@@ -44,7 +48,7 @@
 
 /// Default implementation of [AarBuilder].
 class _AndroidBuilderImpl extends AndroidBuilder {
-  _AndroidBuilderImpl();
+  const _AndroidBuilderImpl();
 
   /// Builds the AAR and POM files for the current Flutter module or plugin.
   @override
@@ -54,27 +58,18 @@
     @required String target,
     @required String outputDir,
   }) async {
-    if (!project.android.isUsingGradle) {
-      throwToolExit(
-          'The build process for Android has changed, and the current project configuration '
-          '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.');
-    }
     try {
+      Directory outputDirectory =
+        fs.directory(outputDir ?? project.android.buildDirectory);
+      if (project.isModule) {
+        // Module projects artifacts are located in `build/host`.
+        outputDirectory = outputDirectory.childDirectory('host');
+      }
       await buildGradleAar(
         project: project,
         androidBuildInfo: androidBuildInfo,
         target: target,
-        outputDir: outputDir,
+        outputDir: outputDirectory,
       );
     } finally {
       androidSdk.reinitialize();
@@ -88,24 +83,13 @@
     @required AndroidBuildInfo androidBuildInfo,
     @required String target,
   }) async {
-    if (!project.android.isUsingGradle) {
-      throwToolExit(
-          'The build process for Android has changed, and the current project configuration '
-          '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.'
-      );
-    }
-    // Validate that we can find an android sdk.
-    if (androidSdk == null) {
-      throwToolExit('No Android SDK found. Try setting the ANDROID_SDK_ROOT environment variable.');
-    }
     try {
-      await buildGradleProject(
+      await buildGradleApp(
         project: project,
         androidBuildInfo: androidBuildInfo,
         target: target,
         isBuildingBundle: false,
+        localGradleErrors: gradleErrors,
       );
     } finally {
       androidSdk.reinitialize();
@@ -119,54 +103,16 @@
     @required AndroidBuildInfo androidBuildInfo,
     @required String target,
   }) async {
-    if (!project.android.isUsingGradle) {
-      throwToolExit(
-          'The build process for Android has changed, and the current project configuration '
-          '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.'
-      );
-    }
-    // Validate that we can find an android sdk.
-    if (androidSdk == null) {
-      throwToolExit('No Android SDK found. Try setting the ANDROID_HOME environment variable.');
-    }
-
     try {
-      await buildGradleProject(
+      await buildGradleApp(
         project: project,
         androidBuildInfo: androidBuildInfo,
         target: target,
         isBuildingBundle: true,
+        localGradleErrors: gradleErrors,
       );
     } finally {
       androidSdk.reinitialize();
     }
   }
 }
-
-/// A fake implementation of [AndroidBuilder].
-@visibleForTesting
-class FakeAndroidBuilder implements AndroidBuilder {
-  @override
-  Future<void> buildAar({
-    @required FlutterProject project,
-    @required AndroidBuildInfo androidBuildInfo,
-    @required String target,
-    @required String outputDir,
-  }) async {}
-
-  @override
-  Future<void> buildApk({
-    @required FlutterProject project,
-    @required AndroidBuildInfo androidBuildInfo,
-    @required String target,
-  }) async {}
-
-  @override
-  Future<void> buildAab({
-    @required FlutterProject project,
-    @required AndroidBuildInfo androidBuildInfo,
-    @required String target,
-  }) async {}
-}
diff --git a/packages/flutter_tools/lib/src/android/android_studio.dart b/packages/flutter_tools/lib/src/android/android_studio.dart
index 04e3474..9ded536 100644
--- a/packages/flutter_tools/lib/src/android/android_studio.dart
+++ b/packages/flutter_tools/lib/src/android/android_studio.dart
@@ -236,7 +236,7 @@
 
     // Read all $HOME/.AndroidStudio*/system/.home files. There may be several
     // pointing to the same installation, so we grab only the latest one.
-    if (fs.directory(homeDirPath).existsSync()) {
+    if (homeDirPath != null && fs.directory(homeDirPath).existsSync()) {
       for (FileSystemEntity entity in fs.directory(homeDirPath).listSync(followLinks: false)) {
         if (entity is Directory && entity.basename.startsWith('.AndroidStudio')) {
           final AndroidStudio studio = AndroidStudio.fromHomeDot(entity);
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index 0dd05ae..dff67bd 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -10,16 +10,12 @@
 import '../android/android_sdk.dart';
 import '../artifacts.dart';
 import '../base/common.dart';
-import '../base/context.dart';
 import '../base/file_system.dart';
 import '../base/io.dart';
 import '../base/logger.dart';
-import '../base/os.dart';
-import '../base/platform.dart';
 import '../base/process.dart';
 import '../base/terminal.dart';
 import '../base/utils.dart';
-import '../base/version.dart';
 import '../build_info.dart';
 import '../cache.dart';
 import '../flutter_manifest.dart';
@@ -27,136 +23,131 @@
 import '../project.dart';
 import '../reporting/reporting.dart';
 import 'android_sdk.dart';
-import 'android_studio.dart';
+import 'gradle_errors.dart';
+import 'gradle_utils.dart';
 
-/// Gradle utils in the current [AppContext].
-GradleUtils get gradleUtils => context.get<GradleUtils>();
-
-/// Provides utilities to run a Gradle task,
-/// such as finding the Gradle executable or constructing a Gradle project.
-class GradleUtils {
-  /// Empty constructor.
-  GradleUtils();
-
-  String _cachedExecutable;
-  /// Gets the Gradle executable path.
-  /// This is the `gradlew` or `gradlew.bat` script in the `android/` directory.
-  Future<String> getExecutable(FlutterProject project) async {
-    _cachedExecutable ??= await _initializeGradle(project);
-    return _cachedExecutable;
-  }
-
-  /// Cached app projects. The key is the [FluterProject]'s path, the value is [GradleProject].
-  final Map<String, GradleProject> _cachedAppProject = <String, GradleProject>{};
-
-  /// Gets the [GradleProject] for the [project] if built as an app.
-  Future<GradleProject> getAppProject(FlutterProject project) async {
-    final String projectPath = project.directory.path;
-    _cachedAppProject[projectPath] ??= await _readGradleProject(project, isLibrary: false);
-    return _cachedAppProject[projectPath];
-  }
-
-  /// Cached library projects such as plugins or modules.
-  /// The key is the [FluterProject]'s path, the value is [GradleProject].
-  final Map<String, GradleProject> _cachedLibraryProject = <String, GradleProject>{};
-
-  /// Gets the [GradleProject] for the [project] if built as a library.
-  Future<GradleProject> getLibraryProject(FlutterProject project) async {
-    final String projectPath = project.directory.path;
-    _cachedLibraryProject[projectPath] ??= await _readGradleProject(project, isLibrary: true);
-    return _cachedLibraryProject[projectPath];
-  }
+/// The directory where the APK artifact is generated.
+@visibleForTesting
+Directory getApkDirectory(FlutterProject project) {
+  return project.isModule
+    ? project.android.buildDirectory
+        .childDirectory('host')
+        .childDirectory('outputs')
+        .childDirectory('apk')
+    : project.android.buildDirectory
+        .childDirectory('app')
+        .childDirectory('outputs')
+        .childDirectory('apk');
 }
 
-final RegExp _assembleTaskPattern = RegExp(r'assemble(\S+)');
-
-enum FlutterPluginVersion {
-  none,
-  v1,
-  v2,
-  managed,
+/// The directory where the app bundle artifact is generated.
+@visibleForTesting
+Directory getBundleDirectory(FlutterProject project) {
+  return project.isModule
+    ? project.android.buildDirectory
+        .childDirectory('host')
+        .childDirectory('outputs')
+        .childDirectory('bundle')
+    : project.android.buildDirectory
+        .childDirectory('app')
+        .childDirectory('outputs')
+        .childDirectory('bundle');
 }
 
-// Investigation documented in #13975 suggests the filter should be a subset
-// of the impact of -q, but users insist they see the error message sometimes
-// anyway.  If we can prove it really is impossible, delete the filter.
-// This technically matches everything *except* the NDK message, since it's
-// passed to a function that filters out all lines that don't match a filter.
-final RegExp ndkMessageFilter = RegExp(r'^(?!NDK is missing a ".*" directory'
-  r'|If you are not using NDK, unset the NDK variable from ANDROID_NDK_HOME or local.properties to remove this warning'
-  r'|If you are using NDK, verify the ndk.dir is set to a valid NDK directory.  It is currently set to .*)');
+/// The directory where the repo is generated.
+/// Only applicable to AARs.
+@visibleForTesting
+Directory getRepoDirectory(Directory buildDirectory) {
+  return buildDirectory
+    .childDirectory('outputs')
+    .childDirectory('repo');
+}
 
-// This regex is intentionally broad. AndroidX errors can manifest in multiple
-// different ways and each one depends on the specific code config and
-// filesystem paths of the project. Throwing the broadest net possible here to
-// catch all known and likely cases.
-//
-// Example stack traces:
-//
-// https://github.com/flutter/flutter/issues/27226 "AAPT: error: resource android:attr/fontVariationSettings not found."
-// https://github.com/flutter/flutter/issues/27106 "Android resource linking failed|Daemon: AAPT2|error: failed linking references"
-// https://github.com/flutter/flutter/issues/27493 "error: cannot find symbol import androidx.annotation.NonNull;"
-// https://github.com/flutter/flutter/issues/23995 "error: package android.support.annotation does not exist import android.support.annotation.NonNull;"
-final RegExp androidXFailureRegex = RegExp(r'(AAPT|androidx|android\.support)');
+/// Returns the name of Gradle task that starts with [prefix].
+String _taskFor(String prefix, BuildInfo buildInfo) {
+  final String buildType = camelCase(buildInfo.modeName);
+  final String productFlavor = buildInfo.flavor ?? '';
+  return '$prefix${toTitleCase(productFlavor)}${toTitleCase(buildType)}';
+}
 
-final RegExp androidXPluginWarningRegex = RegExp(r'\*{57}'
-  r"|WARNING: This version of (\w+) will break your Android build if it or its dependencies aren't compatible with AndroidX."
-  r'|See https://goo.gl/CP92wY for more information on the problem and how to fix it.'
-  r'|This warning prints for all Android build failures. The real root cause of the error may be unrelated.');
+/// Returns the task to build an APK.
+@visibleForTesting
+String getAssembleTaskFor(BuildInfo buildInfo) {
+  return _taskFor('assemble', buildInfo);
+}
 
-FlutterPluginVersion getFlutterPluginVersion(AndroidProject project) {
+/// Returns the task to build an AAB.
+@visibleForTesting
+String getBundleTaskFor(BuildInfo buildInfo) {
+  return _taskFor('bundle', buildInfo);
+}
+
+/// Returns the task to build an AAR.
+@visibleForTesting
+String getAarTaskFor(BuildInfo buildInfo) {
+  return _taskFor('assembleAar', buildInfo);
+}
+
+/// Returns the output APK file names for a given [AndroidBuildInfo].
+///
+/// For example, when [splitPerAbi] is true, multiple APKs are created.
+Iterable<String> _apkFilesFor(AndroidBuildInfo androidBuildInfo) {
+  final String buildType = camelCase(androidBuildInfo.buildInfo.modeName);
+  final String productFlavor = androidBuildInfo.buildInfo.flavor ?? '';
+  final String flavorString = productFlavor.isEmpty ? '' : '-$productFlavor';
+  if (androidBuildInfo.splitPerAbi) {
+    return androidBuildInfo.targetArchs.map<String>((AndroidArch arch) {
+      final String abi = getNameForAndroidArch(arch);
+      return 'app$flavorString-$abi-$buildType.apk';
+    });
+  }
+  return <String>['app$flavorString-$buildType.apk'];
+}
+
+/// Returns true if the current version of the Gradle plugin is supported.
+bool _isSupportedVersion(AndroidProject project) {
   final File plugin = project.hostAppGradleRoot.childFile(
       fs.path.join('buildSrc', 'src', 'main', 'groovy', 'FlutterPlugin.groovy'));
   if (plugin.existsSync()) {
-    final String packageLine = plugin.readAsLinesSync().skip(4).first;
-    if (packageLine == 'package io.flutter.gradle') {
-      return FlutterPluginVersion.v2;
-    }
-    return FlutterPluginVersion.v1;
+    return false;
   }
   final File appGradle = project.hostAppGradleRoot.childFile(
       fs.path.join('app', 'build.gradle'));
-  if (appGradle.existsSync()) {
-    for (String line in appGradle.readAsLinesSync()) {
-      if (line.contains(RegExp(r'apply from: .*/flutter.gradle'))) {
-        return FlutterPluginVersion.managed;
-      }
-      if (line.contains("def flutterPluginVersion = 'managed'")) {
-        return FlutterPluginVersion.managed;
-      }
+  if (!appGradle.existsSync()) {
+    return false;
+  }
+  for (String line in appGradle.readAsLinesSync()) {
+    if (line.contains(RegExp(r'apply from: .*/flutter.gradle')) ||
+        line.contains("def flutterPluginVersion = 'managed'")) {
+      return true;
     }
   }
-  return FlutterPluginVersion.none;
+  return false;
 }
 
 /// Returns the apk file created by [buildGradleProject]
 Future<File> getGradleAppOut(AndroidProject androidProject) async {
-  switch (getFlutterPluginVersion(androidProject)) {
-    case FlutterPluginVersion.none:
-      // Fall through. Pretend we're v1, and just go with it.
-    case FlutterPluginVersion.v1:
-      return androidProject.gradleAppOutV1File;
-    case FlutterPluginVersion.managed:
-      // Fall through. The managed plugin matches plugin v2 for now.
-    case FlutterPluginVersion.v2:
-      final GradleProject gradleProject =
-          await gradleUtils.getAppProject(FlutterProject.current());
-      return fs.file(gradleProject.apkDirectory.childFile('app.apk'));
+  if (!_isSupportedVersion(androidProject)) {
+    _exitWithUnsupportedProjectMessage();
   }
-  return null;
+  return getApkDirectory(androidProject.parent).childFile('app.apk');
 }
 
 /// Runs `gradlew dependencies`, ensuring that dependencies are resolved and
 /// potentially downloaded.
 Future<void> checkGradleDependencies() async {
-  final Status progress = logger.startProgress('Ensuring gradle dependencies are up to date...', timeout: timeoutConfiguration.slowOperation);
+  final Status progress = logger.startProgress(
+    'Ensuring gradle dependencies are up to date...',
+    timeout: timeoutConfiguration.slowOperation,
+  );
   final FlutterProject flutterProject = FlutterProject.current();
-  final String gradlew = await gradleUtils.getExecutable(flutterProject);
-  await processUtils.run(
-    <String>[gradlew, 'dependencies'],
+  await processUtils.run(<String>[
+      gradleUtils.getExecutable(flutterProject),
+      'dependencies',
+    ],
     throwOnError: true,
     workingDirectory: flutterProject.android.hostAppGradleRoot.path,
-    environment: gradleEnv,
+    environment: gradleEnvironment,
   );
   androidSdk.reinitialize();
   progress.stop();
@@ -200,456 +191,312 @@
   }
   if (!exactMatch) {
     status.cancel();
-    printError('*******************************************************************************************');
-    printError('Flutter tried to create the file `$newSettingsRelativeFile`, but failed.');
+    printStatus('$warningMark 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',
+    printStatus(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.');
+  printStatus('$successMark `$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(
-  FlutterProject flutterProject, {
-  bool isLibrary = false,
-}) async {
-  final String gradlew = await gradleUtils.getExecutable(flutterProject);
-
-  updateLocalProperties(project: flutterProject);
-
-  final FlutterManifest manifest = flutterProject.manifest;
-  final Directory hostAppGradleRoot = flutterProject.android.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 processUtils.run(
-      <String>[gradlew, if (isLibrary) 'properties' else 'app:properties'],
-      throwOnError: true,
-      workingDirectory: hostAppGradleRoot.path,
-      environment: gradleEnv,
-    );
-    final RunResult tasksRunResult = await processUtils.run(
-      <String>[gradlew, if (isLibrary) 'tasks' else 'app:tasks', '--all', '--console=auto'],
-      throwOnError: true,
-      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.
-      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.');
-    }
-    // Fall back to the default
-    project = GradleProject(
-      <String>['debug', 'profile', 'release'],
-      <String>[],
-      fs.path.join(flutterProject.android.hostAppGradleRoot.path, 'app', 'build'),
-    );
-  }
-  status.stop();
-  return project;
-}
-
-/// 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(licenseNotAcceptedMatcher, multiLine: true);
-  final Match licenseMatch = licenseFailure.firstMatch(exception.toString());
-  if (licenseMatch != null) {
-    final String missingLicenses = licenseMatch.group(1);
-    final String errorMessage =
-      '\n\n* Error running Gradle:\n'
-      'Unable to download needed Android SDK components, as the following licenses have not been accepted:\n'
-      '$missingLicenses\n\n'
-      'To resolve this, please run the following command in a Terminal:\n'
-      'flutter doctor --android-licenses';
-    throwToolExit(errorMessage);
-  }
-}
-
-String _locateGradlewExecutable(Directory directory) {
-  final File gradle = directory.childFile(
-    platform.isWindows ? 'gradlew.bat' : 'gradlew',
-  );
-  if (gradle.existsSync()) {
-    return gradle.absolute.path;
-  }
-  return null;
-}
-
-// Gradle crashes for several known reasons when downloading that are not
-// actionable by flutter.
-const List<String> _kKnownErrorPrefixes = <String>[
-  'java.io.FileNotFoundException: https://downloads.gradle.org',
-  'java.io.IOException: Unable to tunnel through proxy',
-  'java.lang.RuntimeException: Timeout of',
-  'java.util.zip.ZipException: error in opening zip file',
-  'javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake',
-  'java.net.SocketException: Connection reset',
-  'java.io.FileNotFoundException',
-];
-
-// Note: Gradle may be bootstrapped and possibly downloaded as a side-effect
-// of validating the Gradle executable. This may take several seconds.
-Future<String> _initializeGradle(FlutterProject project) async {
-  final Directory android = project.android.hostAppGradleRoot;
-  final Status status = logger.startProgress('Initializing gradle...',
-      timeout: timeoutConfiguration.slowOperation);
-
-
-  // Update the project if needed.
-  // TODO(egarciad): https://github.com/flutter/flutter/issues/40460.
-  migrateToR8(android);
-  injectGradleWrapperIfNeeded(android);
-
-  final String gradle = _locateGradlewExecutable(android);
-  if (gradle == null) {
-    status.stop();
-    throwToolExit('Unable to locate gradlew script');
-  }
-  printTrace('Using gradle from $gradle.');
-  // Validates the Gradle executable by asking for its version.
-  // Makes Gradle Wrapper download and install Gradle distribution, if needed.
-  try {
-    await processUtils.run(
-      <String>[gradle, '-v'],
-      throwOnError: true,
-      environment: gradleEnv,
-    );
-  } on ProcessException catch (e) {
-    final String error = e.toString();
-    // TODO(jonahwilliams): automatically retry on network errors.
-    if (_kKnownErrorPrefixes.any((String candidate) => error.contains(candidate))) {
-      throwToolExit(
-        '$gradle threw an error while trying to update itself.'
-        ' Try rerunning to retry the update.\n$e');
-    }
-    // gradlew is missing execute.
-    if (error.contains('Permission denied')) {
-      throwToolExit(
-        '$gradle does not have permission to execute by your user.\n'
-        'You should change the ownership of the project directory to your user'
-        ', or move the project to a directory with execute permissions.\n$error'
-      );
-    }
-    // No idea what went wrong but we can't do anything about it.
-    if (error.contains('ProcessException: Process exited abnormally')) {
-      throwToolExit(
-        '$gradle exited abnormally. Try rerunning with \'-v\' for more '
-        'infomration, or check the gradlew script above for errors.\n$error');
-    }
-    rethrow;
-  } finally {
-    status.stop();
-  }
-  return gradle;
-}
-
-/// Migrates the Android's [directory] to R8.
-/// https://developer.android.com/studio/build/shrink-code
-@visibleForTesting
-void migrateToR8(Directory directory) {
-  final File gradleProperties = directory.childFile('gradle.properties');
-  if (!gradleProperties.existsSync()) {
-    throwToolExit('Expected file ${gradleProperties.path}.');
-  }
-  final String propertiesContent = gradleProperties.readAsStringSync();
-  if (propertiesContent.contains('android.enableR8')) {
-    printTrace('gradle.properties already sets `android.enableR8`');
-    return;
-  }
-  printTrace('set `android.enableR8=true` in gradle.properties');
-  try {
-    // Add `android.enableR8=true` to the next line in gradle.properties.
-    if (propertiesContent.isNotEmpty && !propertiesContent.endsWith('\n')) {
-      gradleProperties
-        .writeAsStringSync('\nandroid.enableR8=true\n', mode: FileMode.append);
-    } else {
-      gradleProperties
-        .writeAsStringSync('android.enableR8=true\n', mode: FileMode.append);
-    }
-  } on FileSystemException {
-    throwToolExit(
-      'The tool failed to add `android.enableR8=true` to ${gradleProperties.path}. '
-      'Please update the file manually and try this command again.'
-    );
-  }
-}
-
-/// Injects the Gradle wrapper files if any of these files don't exist in [directory].
-void injectGradleWrapperIfNeeded(Directory directory) {
-  copyDirectorySync(
-    cache.getArtifactDirectory('gradle_wrapper'),
-    directory,
-    shouldCopyFile: (File sourceFile, File destinationFile) {
-      // Don't override the existing files in the project.
-      return !destinationFile.existsSync();
-    },
-    onFileCopied: (File sourceFile, File destinationFile) {
-      final String modes = sourceFile.statSync().modeString();
-      if (modes != null && modes.contains('x')) {
-        os.makeExecutable(destinationFile);
-      }
-    },
-  );
-  // Add the `gradle-wrapper.properties` file if it doesn't exist.
-  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
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersion-all.zip
-''', flush: true,
-    );
-  }
-}
-
-/// 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 = '5.6.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.6.2';
-  }
-  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.
+/// Builds an app.
 ///
-/// 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.
-///
-/// If [requireAndroidSdk] is true (the default) and no Android SDK is found,
-/// this will fail with a [ToolExit].
-void updateLocalProperties({
-  @required FlutterProject project,
-  BuildInfo buildInfo,
-  bool requireAndroidSdk = true,
-}) {
-  if (requireAndroidSdk) {
-    _exitIfNoAndroidSdk();
-  }
-
-  final File localProperties = project.android.localPropertiesFile;
-  bool changed = false;
-
-  SettingsFile settings;
-  if (localProperties.existsSync()) {
-    settings = SettingsFile.parseFromFile(localProperties);
-  } else {
-    settings = SettingsFile();
-    changed = true;
-  }
-
-  void changeIfNecessary(String key, String value) {
-    if (settings.values[key] != value) {
-      if (value == null) {
-        settings.values.remove(key);
-      } else {
-        settings.values[key] = value;
-      }
-      changed = true;
-    }
-  }
-
-  final FlutterManifest manifest = project.manifest;
-
-  if (androidSdk != null) {
-    changeIfNecessary('sdk.dir', escapePath(androidSdk.directory));
-  }
-
-  changeIfNecessary('flutter.sdk', escapePath(Cache.flutterRoot));
-
-  if (buildInfo != null) {
-    changeIfNecessary('flutter.buildMode', buildInfo.modeName);
-    final String buildName = validatedBuildNameForPlatform(TargetPlatform.android_arm, buildInfo.buildName ?? manifest.buildName);
-    changeIfNecessary('flutter.versionName', buildName);
-    final String buildNumber = validatedBuildNumberForPlatform(TargetPlatform.android_arm, buildInfo.buildNumber ?? manifest.buildNumber);
-    changeIfNecessary('flutter.versionCode', buildNumber?.toString());
-  }
-
-  if (changed) {
-    settings.writeContents(localProperties);
-  }
-}
-
-/// Writes standard Android local properties to the specified [properties] file.
-///
-/// Writes the path to the Android SDK, if known.
-void writeLocalProperties(File properties) {
-  final SettingsFile settings = SettingsFile();
-  if (androidSdk != null) {
-    settings.values['sdk.dir'] = escapePath(androidSdk.directory);
-  }
-  settings.writeContents(properties);
-}
-
-/// Throws a ToolExit, if the path to the Android SDK is not known.
-void _exitIfNoAndroidSdk() {
-  if (androidSdk == null) {
-    throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.');
-  }
-}
-
-Future<void> buildGradleProject({
+/// * [project] is typically [FlutterProject.current()].
+/// * [androidBuildInfo] is the build configuration.
+/// * [target] is the target dart entrypoint. Typically, `lib/main.dart`.
+/// * If [isBuildingBundle] is `true`, then the output artifact is an `*.aab`,
+///   otherwise the output artifact is an `*.apk`.
+/// * The plugins are built as AARs if [shouldBuildPluginAsAar] is `true`. This isn't set by default
+///   because it makes the build slower proportional to the number of plugins.
+/// * [retries] is the max number of build retries in case one of the [GradleHandledError] handler
+///   returns [GradleBuildStatus.retry] or [GradleBuildStatus.retryWithAarPlugins].
+Future<void> buildGradleApp({
   @required FlutterProject project,
   @required AndroidBuildInfo androidBuildInfo,
   @required String target,
   @required bool isBuildingBundle,
+  @required List<GradleHandledError> localGradleErrors,
+  bool shouldBuildPluginAsAar = false,
+  int retries = 1,
 }) async {
-  // Update the local.properties file with the build mode, version name and code.
-  // FlutterPlugin v1 reads local.properties to determine build mode. Plugin v2
-  // uses the standard Android way to determine what to build, but we still
-  // update local.properties, in case we want to use it in the future.
-  // Version name and number are provided by the pubspec.yaml file
-  // and can be overwritten with flutter build command.
+  if (androidSdk == null) {
+    exitWithNoSdkMessage();
+  }
+  if (!project.android.isUsingGradle) {
+    _exitWithProjectNotUsingGradleMessage();
+  }
+  if (!_isSupportedVersion(project.android)) {
+    _exitWithUnsupportedProjectMessage();
+  }
+
+  final bool usesAndroidX = isAppUsingAndroidX(project.android.hostAppGradleRoot);
+  if (usesAndroidX) {
+    BuildEvent('app-using-android-x').send();
+  } else if (!usesAndroidX) {
+    BuildEvent('app-not-using-android-x').send();
+    printStatus('$warningMark Your app isn\'t using AndroidX.', emphasis: true);
+    printStatus(
+      'To avoid potential build failures, you can quickly migrate your app '
+      'by following the steps on https://goo.gl/CP92wY.',
+      indent: 4,
+    );
+  }
   // The default Gradle script reads the version name and number
   // from the local.properties file.
   updateLocalProperties(project: project, buildInfo: androidBuildInfo.buildInfo);
 
-  switch (getFlutterPluginVersion(project.android)) {
-    case FlutterPluginVersion.none:
-      // Fall through. Pretend it's v1, and just go for it.
-    case FlutterPluginVersion.v1:
-      return _buildGradleProjectV1(project);
-    case FlutterPluginVersion.managed:
-      // Fall through. Managed plugin builds the same way as plugin v2.
-    case FlutterPluginVersion.v2:
-      return _buildGradleProjectV2(project, androidBuildInfo, target, isBuildingBundle);
+  if (shouldBuildPluginAsAar) {
+    // Create a settings.gradle that doesn't import the plugins as subprojects.
+    createSettingsAarGradle(project.android.hostAppGradleRoot);
+    await buildPluginsAsAar(
+      project,
+      androidBuildInfo,
+      buildDirectory: project.android.buildDirectory.childDirectory('app'),
+    );
+  }
+
+  final BuildInfo buildInfo = androidBuildInfo.buildInfo;
+  final String assembleTask = isBuildingBundle
+    ? getBundleTaskFor(buildInfo)
+    : getAssembleTaskFor(buildInfo);
+
+  final Status status = logger.startProgress(
+    'Running Gradle task \'$assembleTask\'...',
+    timeout: timeoutConfiguration.slowOperation,
+    multilineOutput: true,
+  );
+
+  final List<String> command = <String>[
+    gradleUtils.getExecutable(project),
+  ];
+  if (logger.isVerbose) {
+    command.add('-Pverbose=true');
+  } else {
+    command.add('-q');
+  }
+  if (artifacts is LocalEngineArtifacts) {
+    final LocalEngineArtifacts localEngineArtifacts = artifacts;
+    printTrace('Using local engine: ${localEngineArtifacts.engineOutPath}');
+    command.add('-PlocalEngineOut=${localEngineArtifacts.engineOutPath}');
+  }
+  if (target != null) {
+    command.add('-Ptarget=$target');
+  }
+  assert(buildInfo.trackWidgetCreation != null);
+  command.add('-Ptrack-widget-creation=${buildInfo.trackWidgetCreation}');
+
+  if (buildInfo.extraFrontEndOptions != null) {
+    command.add('-Pextra-front-end-options=${buildInfo.extraFrontEndOptions}');
+  }
+  if (buildInfo.extraGenSnapshotOptions != null) {
+    command.add('-Pextra-gen-snapshot-options=${buildInfo.extraGenSnapshotOptions}');
+  }
+  if (buildInfo.fileSystemRoots != null && buildInfo.fileSystemRoots.isNotEmpty) {
+    command.add('-Pfilesystem-roots=${buildInfo.fileSystemRoots.join('|')}');
+  }
+  if (buildInfo.fileSystemScheme != null) {
+    command.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}');
+  }
+  if (androidBuildInfo.splitPerAbi) {
+    command.add('-Psplit-per-abi=true');
+  }
+  if (androidBuildInfo.shrink) {
+    command.add('-Pshrink=true');
+  }
+  if (androidBuildInfo.targetArchs.isNotEmpty) {
+    final String targetPlatforms = androidBuildInfo
+      .targetArchs
+      .map(getPlatformNameForAndroidArch).join(',');
+    command.add('-Ptarget-platform=$targetPlatforms');
+  }
+  if (shouldBuildPluginAsAar) {
+    // 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');
+    // Don't use settings.gradle from the current project since it includes the plugins as subprojects.
+    command.add('--settings-file=settings_aar.gradle');
+  }
+  command.add(assembleTask);
+
+  final Stopwatch sw = Stopwatch()..start();
+  int exitCode = 1;
+  GradleHandledError detectedGradleError;
+  String detectedGradleErrorLine;
+  try {
+    exitCode = await processUtils.stream(
+      command,
+      workingDirectory: project.android.hostAppGradleRoot.path,
+      allowReentrantFlutter: true,
+      environment: gradleEnvironment,
+      mapFunction: (String line) {
+        // This message was removed from first-party plugins,
+        // but older plugin versions still display this message.
+        if (androidXPluginWarningRegex.hasMatch(line)) {
+          // Don't pipe.
+          return null;
+        }
+        if (detectedGradleError != null) {
+          // Pipe stdout/sterr from Gradle.
+          return line;
+        }
+        for (final GradleHandledError gradleError in localGradleErrors) {
+          if (gradleError.test(line)) {
+            detectedGradleErrorLine = line;
+            detectedGradleError = gradleError;
+            // The first error match wins.
+            break;
+          }
+        }
+        // Pipe stdout/sterr from Gradle.
+        return line;
+      },
+    );
+  } finally {
+    status.stop();
+  }
+
+  flutterUsage.sendTiming('build', 'gradle', sw.elapsed);
+
+  if (exitCode != 0) {
+    if (detectedGradleError == null) {
+      BuildEvent('gradle--unkown-failure').send();
+      throwToolExit(
+        'Gradle task $assembleTask failed with exit code $exitCode',
+        exitCode: exitCode,
+      );
+    } else {
+      final GradleBuildStatus status = await detectedGradleError.handler(
+        line: detectedGradleErrorLine,
+        project: project,
+        usesAndroidX: usesAndroidX,
+        shouldBuildPluginAsAar: shouldBuildPluginAsAar,
+      );
+
+      if (retries >= 1) {
+        final String successEventLabel = 'gradle--${detectedGradleError.eventLabel}-success';
+        switch (status) {
+          case GradleBuildStatus.retry:
+            await buildGradleApp(
+              project: project,
+              androidBuildInfo: androidBuildInfo,
+              target: target,
+              isBuildingBundle: isBuildingBundle,
+              localGradleErrors: localGradleErrors,
+              shouldBuildPluginAsAar: shouldBuildPluginAsAar,
+              retries: retries - 1,
+            );
+            BuildEvent(successEventLabel).send();
+            return;
+          case GradleBuildStatus.retryWithAarPlugins:
+            await buildGradleApp(
+              project: project,
+              androidBuildInfo: androidBuildInfo,
+              target: target,
+              isBuildingBundle: isBuildingBundle,
+              localGradleErrors: localGradleErrors,
+              shouldBuildPluginAsAar: true,
+              retries: retries - 1,
+            );
+            BuildEvent(successEventLabel).send();
+            return;
+          case GradleBuildStatus.exit:
+            // noop.
+        }
+      }
+      BuildEvent('gradle--${detectedGradleError.eventLabel}-failure').send();
+      throwToolExit(
+        'Gradle task $assembleTask failed with exit code $exitCode',
+        exitCode: exitCode,
+      );
+    }
+  }
+
+  if (isBuildingBundle) {
+    final File bundleFile = findBundleFile(project, buildInfo);
+    if (bundleFile == null) {
+      throwToolExit('Gradle build failed to produce an Android bundle package.');
+    }
+
+    final String appSize = (buildInfo.mode == BuildMode.debug)
+      ? '' // Don't display the size when building a debug variant.
+      : ' (${getSizeAsMB(bundleFile.lengthSync())})';
+
+    printStatus(
+      '$successMark Built ${fs.path.relative(bundleFile.path)}$appSize.',
+      color: TerminalColor.green,
+    );
+    return;
+  }
+  // Gradle produced an APK.
+  final Iterable<File> apkFiles = findApkFiles(project, androidBuildInfo);
+  if (apkFiles.isEmpty) {
+    throwToolExit('Gradle build failed to produce an Android package.');
+  }
+
+  final Directory apkDirectory = getApkDirectory(project);
+  // Copy the first APK to app.apk, so `flutter run` can find it.
+  // TODO(egarciad): Handle multiple APKs.
+  apkFiles.first.copySync(apkDirectory.childFile('app.apk').path);
+  printTrace('calculateSha: $apkDirectory/app.apk');
+
+  final File apkShaFile = apkDirectory.childFile('app.apk.sha1');
+  apkShaFile.writeAsStringSync(_calculateSha(apkFiles.first));
+
+  for (File apkFile in apkFiles) {
+    final String appSize = (buildInfo.mode == BuildMode.debug)
+      ? '' // Don't display the size when building a debug variant.
+      : ' (${getSizeAsMB(apkFile.lengthSync())})';
+    printStatus(
+      '$successMark Built ${fs.path.relative(apkFile.path)}$appSize.',
+      color: TerminalColor.green,
+    );
   }
 }
 
+/// Builds AAR and POM files.
+///
+/// * [project] is typically [FlutterProject.current()].
+/// * [androidBuildInfo] is the build configuration.
+/// * [target] is the target dart entrypoint. Typically, `lib/main.dart`.
+/// * [outputDir] is the destination of the artifacts,
 Future<void> buildGradleAar({
   @required FlutterProject project,
   @required AndroidBuildInfo androidBuildInfo,
   @required String target,
-  @required String outputDir,
+  @required Directory outputDir,
 }) async {
+  if (androidSdk == null) {
+    exitWithNoSdkMessage();
+  }
   final FlutterManifest manifest = project.manifest;
-
-  GradleProject gradleProject;
-  if (manifest.isModule) {
-    gradleProject = await gradleUtils.getAppProject(project);
-  } else if (manifest.isPlugin) {
-    gradleProject = await gradleUtils.getLibraryProject(project);
-  } else {
+  if (!manifest.isModule && !manifest.isPlugin) {
     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 String aarTask = getAarTaskFor(androidBuildInfo.buildInfo);
   final Status status = logger.startProgress(
     'Running Gradle task \'$aarTask\'...',
     timeout: timeoutConfiguration.slowOperation,
     multilineOutput: true,
   );
 
-  final String gradlew = await gradleUtils.getExecutable(project);
   final String flutterRoot = fs.path.absolute(Cache.flutterRoot);
-  final String initScript = fs.path.join(flutterRoot, 'packages','flutter_tools', 'gradle', 'aar_init_script.gradle');
+  final String initScript = fs.path.join(
+    flutterRoot,
+    'packages',
+    'flutter_tools',
+    'gradle',
+    'aar_init_script.gradle',
+  );
   final List<String> command = <String>[
-    gradlew,
+    gradleUtils.getExecutable(project),
     '-I=$initScript',
     '-Pflutter-root=$flutterRoot',
-    '-Poutput-dir=${gradleProject.buildDirectory}',
+    '-Poutput-dir=${outputDir.path}',
     '-Pis-plugin=${manifest.isPlugin}',
   ];
 
@@ -676,7 +523,7 @@
       command,
       workingDirectory: project.android.hostAppGradleRoot.path,
       allowReentrantFlutter: true,
-      environment: gradleEnv,
+      environment: gradleEnvironment,
     );
   } finally {
     status.stop();
@@ -686,41 +533,24 @@
   if (result.exitCode != 0) {
     printStatus(result.stdout, wrap: false);
     printError(result.stderr, wrap: false);
-    throwToolExit('Gradle task $aarTask failed with exit code $exitCode.', exitCode: exitCode);
+    throwToolExit(
+      'Gradle task $aarTask failed with exit code $exitCode.',
+      exitCode: exitCode,
+    );
   }
-
-  final Directory repoDirectory = gradleProject.repoDirectory;
+  final Directory repoDirectory = getRepoDirectory(outputDir);
   if (!repoDirectory.existsSync()) {
     printStatus(result.stdout, wrap: false);
     printError(result.stderr, wrap: false);
-    throwToolExit('Gradle task $aarTask failed to produce $repoDirectory.', exitCode: exitCode);
+    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) async {
-  final String gradlew = await gradleUtils.getExecutable(project);
-  // Run 'gradlew build'.
-  final Status status = logger.startProgress(
-    'Running \'gradlew build\'...',
-    timeout: timeoutConfiguration.slowOperation,
-    multilineOutput: true,
+  printStatus(
+    '$successMark Built ${fs.path.relative(repoDirectory.path)}.',
+    color: TerminalColor.green,
   );
-  final Stopwatch sw = Stopwatch()..start();
-  final int exitCode = await processUtils.stream(
-    <String>[fs.file(gradlew).absolute.path, 'build'],
-    workingDirectory: project.android.hostAppGradleRoot.path,
-    allowReentrantFlutter: true,
-    environment: gradleEnv,
-  );
-  status.stop();
-  flutterUsage.sendTiming('build', 'gradle-v1', Duration(milliseconds: sw.elapsedMilliseconds));
-
-  if (exitCode != 0) {
-    throwToolExit('Gradle build failed: $exitCode', exitCode: exitCode);
-  }
-
-  printStatus('Built ${fs.path.relative(project.android.gradleAppOutV1File.path)}.');
 }
 
 String _hex(List<int> bytes) {
@@ -735,251 +565,31 @@
   final Stopwatch sw = Stopwatch()..start();
   final List<int> bytes = file.readAsBytesSync();
   printTrace('calculateSha: reading file took ${sw.elapsedMilliseconds}us');
-  flutterUsage.sendTiming('build', 'apk-sha-read', Duration(milliseconds: sw.elapsedMilliseconds));
+  flutterUsage.sendTiming('build', 'apk-sha-read', sw.elapsed);
   sw.reset();
   final String sha = _hex(sha1.convert(bytes).bytes);
   printTrace('calculateSha: computing sha took ${sw.elapsedMilliseconds}us');
-  flutterUsage.sendTiming('build', 'apk-sha-calc', Duration(milliseconds: sw.elapsedMilliseconds));
+  flutterUsage.sendTiming('build', 'apk-sha-calc', sw.elapsed);
   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.');
-  }
+void _exitWithUnsupportedProjectMessage() {
+  BuildEvent('unsupported-project', eventError: 'gradle-plugin').send();
+  throwToolExit(
+    '$warningMark Your app is using an unsupported Gradle project. '
+    'To fix this problem, create a new project by running `flutter create -t app <app-directory>` '
+    'and then move the dart code, assets and pubspec.yaml to the new project.',
+  );
 }
 
-Future<void> _buildGradleProjectV2(
-  FlutterProject flutterProject,
-  AndroidBuildInfo androidBuildInfo,
-  String target,
-  bool isBuildingBundle, {
-  bool shouldBuildPluginAsAar = false,
-}) async {
-  final String gradlew = await gradleUtils.getExecutable(flutterProject);
-  final GradleProject gradleProject = await gradleUtils.getAppProject(flutterProject);
-
-  if (shouldBuildPluginAsAar) {
-    // Create a settings.gradle that doesn't import the plugins as subprojects.
-    createSettingsAarGradle(flutterProject.android.hostAppGradleRoot);
-    await buildPluginsAsAar(
-      flutterProject,
-      androidBuildInfo,
-      buildDirectory: gradleProject.buildDirectory,
-    );
-  }
-
-  final String exclamationMark = terminal.color('[!]', TerminalColor.red);
-  final bool usesAndroidX = isAppUsingAndroidX(flutterProject.android.hostAppGradleRoot);
-
-  if (usesAndroidX) {
-    BuildEvent('app-using-android-x').send();
-  } else if (!usesAndroidX) {
-    BuildEvent('app-not-using-android-x').send();
-    printStatus('$exclamationMark Your app isn\'t using AndroidX.', emphasis: true);
-    printStatus(
-      'To avoid potential build failures, you can quickly migrate your app '
-      'by following the steps on https://goo.gl/CP92wY.',
-      indent: 4,
-    );
-  }
-  final BuildInfo buildInfo = androidBuildInfo.buildInfo;
-
-  String assembleTask;
-
-  if (isBuildingBundle) {
-    assembleTask = gradleProject.bundleTaskFor(buildInfo);
-  } else {
-    assembleTask = gradleProject.assembleTaskFor(buildInfo);
-  }
-  if (assembleTask == null) {
-    printUndefinedTask(gradleProject, buildInfo);
-    throwToolExit('Gradle build aborted.');
-  }
-  final Status status = logger.startProgress(
-    'Running Gradle task \'$assembleTask\'...',
-    timeout: timeoutConfiguration.slowOperation,
-    multilineOutput: true,
+void _exitWithProjectNotUsingGradleMessage() {
+  BuildEvent('unsupported-project', eventError: 'app-not-using-gradle').send();
+  throwToolExit(
+    '$warningMark The build process for Android has changed, and the '
+    'current project configuration 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.'
   );
-  final List<String> command = <String>[gradlew];
-  if (logger.isVerbose) {
-    command.add('-Pverbose=true');
-  } else {
-    command.add('-q');
-  }
-  if (artifacts is LocalEngineArtifacts) {
-    final LocalEngineArtifacts localEngineArtifacts = artifacts;
-    printTrace('Using local engine: ${localEngineArtifacts.engineOutPath}');
-    command.add('-PlocalEngineOut=${localEngineArtifacts.engineOutPath}');
-  }
-  if (target != null) {
-    command.add('-Ptarget=$target');
-  }
-  assert(buildInfo.trackWidgetCreation != null);
-  command.add('-Ptrack-widget-creation=${buildInfo.trackWidgetCreation}');
-  if (buildInfo.extraFrontEndOptions != null) {
-    command.add('-Pextra-front-end-options=${buildInfo.extraFrontEndOptions}');
-  }
-  if (buildInfo.extraGenSnapshotOptions != null) {
-    command.add('-Pextra-gen-snapshot-options=${buildInfo.extraGenSnapshotOptions}');
-  }
-  if (buildInfo.fileSystemRoots != null && buildInfo.fileSystemRoots.isNotEmpty) {
-    command.add('-Pfilesystem-roots=${buildInfo.fileSystemRoots.join('|')}');
-  }
-  if (buildInfo.fileSystemScheme != null) {
-    command.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}');
-  }
-  if (androidBuildInfo.splitPerAbi) {
-    command.add('-Psplit-per-abi=true');
-  }
-  if (androidBuildInfo.shrink) {
-    command.add('-Pshrink=true');
-  }
-  if (androidBuildInfo.targetArchs.isNotEmpty) {
-    final String targetPlatforms = androidBuildInfo.targetArchs
-        .map(getPlatformNameForAndroidArch).join(',');
-    command.add('-Ptarget-platform=$targetPlatforms');
-  }
-  if (shouldBuildPluginAsAar) {
-     // 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');
-    // Don't use settings.gradle from the current project since it includes the plugins as subprojects.
-    command.add('--settings-file=settings_aar.gradle');
-  }
-  command.add(assembleTask);
-  bool potentialAndroidXFailure = false;
-  bool potentialR8Failure = false;
-  final Stopwatch sw = Stopwatch()..start();
-  int exitCode = 1;
-  try {
-    exitCode = await processUtils.stream(
-      command,
-      workingDirectory: flutterProject.android.hostAppGradleRoot.path,
-      allowReentrantFlutter: true,
-      environment: gradleEnv,
-      // TODO(mklim): if AndroidX warnings are no longer required, we can remove
-      // them from this map function.
-      mapFunction: (String line) {
-        final bool isAndroidXPluginWarning = androidXPluginWarningRegex.hasMatch(line);
-        if (!isAndroidXPluginWarning && androidXFailureRegex.hasMatch(line)) {
-          potentialAndroidXFailure = true;
-        }
-        // R8 errors include references to this package.
-        if (!potentialR8Failure && androidBuildInfo.shrink &&
-            line.contains('com.android.tools.r8')) {
-          potentialR8Failure = true;
-        }
-        // Always print the full line in verbose mode.
-        if (logger.isVerbose) {
-          return line;
-        } else if (isAndroidXPluginWarning || !ndkMessageFilter.hasMatch(line)) {
-          return null;
-        }
-        return line;
-      },
-    );
-  } finally {
-    status.stop();
-  }
-
-  if (exitCode != 0) {
-    if (potentialR8Failure) {
-      printStatus('$exclamationMark The shrinker may have failed to optimize the Java bytecode.', emphasis: true);
-      printStatus('To disable the shrinker, pass the `--no-shrink` flag to this command.', indent: 4);
-      printStatus('To learn more, see: https://developer.android.com/studio/build/shrink-code', indent: 4);
-      BuildEvent('r8-failure').send();
-    } else if (potentialAndroidXFailure) {
-      final bool hasPlugins = flutterProject.flutterPluginsFile.existsSync();
-      if (!hasPlugins) {
-        // If the app doesn't use any plugin, then it's unclear where the incompatibility is coming from.
-        BuildEvent('android-x-failure', eventError: 'app-not-using-plugins').send();
-      }
-      if (hasPlugins && !usesAndroidX) {
-        // If the app isn't using AndroidX, then the app is likely using a plugin already migrated to AndroidX.
-        printStatus('AndroidX incompatibilities may have caused this build to fail. ');
-        printStatus('Please migrate your app to AndroidX. See https://goo.gl/CP92wY.');
-        BuildEvent('android-x-failure', eventError: 'app-not-using-androidx').send();
-      }
-      if (hasPlugins && usesAndroidX && shouldBuildPluginAsAar) {
-        // This is a dependency conflict instead of an AndroidX failure since by this point
-        // the app is using AndroidX, the plugins are built as AARs, Jetifier translated
-        // Support libraries for AndroidX equivalents.
-        BuildEvent('android-x-failure', eventError: 'using-jetifier').send();
-      }
-      if (hasPlugins && usesAndroidX && !shouldBuildPluginAsAar) {
-        printStatus(
-          'The built failed likely due to AndroidX incompatibilities in a plugin. '
-          'The tool is about to try using Jetfier to solve the incompatibility.'
-        );
-        BuildEvent('android-x-failure', eventError: 'not-using-jetifier').send();
-        // The app is using Androidx, but Jetifier hasn't run yet.
-        // Call the current method again, build the plugins as AAR, so Jetifier can translate
-        // the dependencies.
-        // NOTE: Don't build the plugins as AARs by default since this drastically increases
-        // the build time.
-        await _buildGradleProjectV2(
-          flutterProject,
-          androidBuildInfo,
-          target,
-          isBuildingBundle,
-          shouldBuildPluginAsAar: true,
-        );
-        return;
-      }
-    }
-    throwToolExit('Gradle task $assembleTask failed with exit code $exitCode', exitCode: exitCode);
-  }
-  flutterUsage.sendTiming('build', 'gradle-v2', Duration(milliseconds: sw.elapsedMilliseconds));
-
-  if (!isBuildingBundle) {
-    final Iterable<File> apkFiles = findApkFiles(gradleProject, androidBuildInfo);
-    if (apkFiles.isEmpty) {
-      throwToolExit('Gradle build failed to produce an Android package.');
-    }
-    // Copy the first APK to app.apk, so `flutter run`, `flutter install`, etc. can find it.
-    // TODO(blasten): Handle multiple APKs.
-    apkFiles.first.copySync(gradleProject.apkDirectory.childFile('app.apk').path);
-
-    printTrace('calculateSha: ${gradleProject.apkDirectory}/app.apk');
-    final File apkShaFile = gradleProject.apkDirectory.childFile('app.apk.sha1');
-    apkShaFile.writeAsStringSync(_calculateSha(apkFiles.first));
-
-    for (File apkFile in apkFiles) {
-      String appSize;
-      if (buildInfo.mode == BuildMode.debug) {
-        appSize = '';
-      } else {
-        appSize = ' (${getSizeAsMB(apkFile.lengthSync())})';
-      }
-      printStatus('Built ${fs.path.relative(apkFile.path)}$appSize.',
-          color: TerminalColor.green);
-    }
-  } else {
-    final File bundleFile = findBundleFile(gradleProject, buildInfo);
-    if (bundleFile == null) {
-      throwToolExit('Gradle build failed to produce an Android bundle package.');
-    }
-
-    String appSize;
-    if (buildInfo.mode == BuildMode.debug) {
-      appSize = '';
-    } else {
-      appSize = ' (${getSizeAsMB(bundleFile.lengthSync())})';
-    }
-    printStatus('Built ${fs.path.relative(bundleFile.path)}$appSize.',
-        color: TerminalColor.green);
-  }
 }
 
 /// Returns [true] if the current app uses AndroidX.
@@ -998,7 +608,7 @@
 Future<void> buildPluginsAsAar(
   FlutterProject flutterProject,
   AndroidBuildInfo androidBuildInfo, {
-  String buildDirectory,
+  Directory buildDirectory,
 }) async {
   final File flutterPluginFile = flutterProject.flutterPluginsFile;
   if (!flutterPluginFile.existsSync()) {
@@ -1031,37 +641,41 @@
       // Log the entire plugin entry in `.flutter-plugins` since it
       // includes the plugin name and the version.
       BuildEvent('plugin-aar-failure', eventError: plugin).send();
-      throwToolExit('The plugin $pluginName could not be built due to the issue above. ');
+      throwToolExit('The plugin $pluginName could not be built due to the issue above.');
     }
   }
 }
 
+/// Returns the APK files for a given [FlutterProject] and [AndroidBuildInfo].
 @visibleForTesting
-Iterable<File> findApkFiles(GradleProject project, AndroidBuildInfo androidBuildInfo) {
-  final Iterable<String> apkFileNames = project.apkFilesFor(androidBuildInfo);
+Iterable<File> findApkFiles(
+  FlutterProject project,
+  AndroidBuildInfo androidBuildInfo)
+{
+  final Iterable<String> apkFileNames = _apkFilesFor(androidBuildInfo);
   if (apkFileNames.isEmpty) {
     return const <File>[];
   }
-
+  final Directory apkDirectory = getApkDirectory(project);
   return apkFileNames.expand<File>((String apkFileName) {
-    File apkFile = project.apkDirectory.childFile(apkFileName);
+    File apkFile = apkDirectory.childFile(apkFileName);
     if (apkFile.existsSync()) {
       return <File>[apkFile];
     }
     final BuildInfo buildInfo = androidBuildInfo.buildInfo;
     final String modeName = camelCase(buildInfo.modeName);
-    apkFile = project.apkDirectory
-        .childDirectory(modeName)
-        .childFile(apkFileName);
+    apkFile = apkDirectory
+      .childDirectory(modeName)
+      .childFile(apkFileName);
     if (apkFile.existsSync()) {
       return <File>[apkFile];
     }
     if (buildInfo.flavor != null) {
       // Android Studio Gradle plugin v3 adds flavor to path.
-      apkFile = project.apkDirectory
-          .childDirectory(buildInfo.flavor)
-          .childDirectory(modeName)
-          .childFile(apkFileName);
+      apkFile = apkDirectory
+        .childDirectory(buildInfo.flavor)
+        .childDirectory(modeName)
+        .childFile(apkFileName);
       if (apkFile.existsSync()) {
         return <File>[apkFile];
       }
@@ -1071,22 +685,21 @@
 }
 
 @visibleForTesting
-File findBundleFile(GradleProject project, BuildInfo buildInfo) {
+File findBundleFile(FlutterProject project, BuildInfo buildInfo) {
   final List<File> fileCandidates = <File>[
-    project.bundleDirectory
+    getBundleDirectory(project)
       .childDirectory(camelCase(buildInfo.modeName))
       .childFile('app.aab'),
-    project.bundleDirectory
+    getBundleDirectory(project)
       .childDirectory(camelCase(buildInfo.modeName))
       .childFile('app-${buildInfo.modeName}.aab'),
   ];
-
   if (buildInfo.flavor != null) {
     // The Android Gradle plugin 3.0.0 adds the flavor name to the path.
     // For example: In release mode, if the flavor name is `foo_bar`, then
     // the directory name is `foo_barRelease`.
     fileCandidates.add(
-      project.bundleDirectory
+      getBundleDirectory(project)
         .childDirectory('${buildInfo.flavor}${camelCase('_' + buildInfo.modeName)}')
         .childFile('app.aab'));
 
@@ -1094,7 +707,7 @@
     // For example: In release mode, if the flavor name is `foo_bar`, then
     // the file name name is `app-foo_bar-release.aab`.
     fileCandidates.add(
-      project.bundleDirectory
+      getBundleDirectory(project)
         .childDirectory('${buildInfo.flavor}${camelCase('_' + buildInfo.modeName)}')
         .childFile('app-${buildInfo.flavor}-${buildInfo.modeName}.aab'));
   }
@@ -1105,152 +718,3 @@
   }
   return null;
 }
-
-/// The environment variables needed to run Gradle.
-Map<String, String> get gradleEnv {
-  final Map<String, String> env = Map<String, String>.from(platform.environment);
-  if (javaPath != null) {
-    // 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';
-  return env;
-}
-
-class GradleProject {
-  GradleProject(
-    this.buildTypes,
-    this.productFlavors,
-    this.buildDirectory,
-  );
-
-  factory GradleProject.fromAppProperties(String properties, String tasks) {
-    // Extract build directory.
-    final String buildDirectory = properties
-        .split('\n')
-        .firstWhere((String s) => s.startsWith('buildDir: '))
-        .substring('buildDir: '.length)
-        .trim();
-
-    // Extract build types and product flavors.
-    final Set<String> variants = <String>{};
-    for (String s in tasks.split('\n')) {
-      final Match match = _assembleTaskPattern.matchAsPrefix(s);
-      if (match != null) {
-        final String variant = match.group(1).toLowerCase();
-        if (!variant.endsWith('test')) {
-          variants.add(variant);
-        }
-      }
-    }
-    final Set<String> buildTypes = <String>{};
-    final Set<String> productFlavors = <String>{};
-    for (final String variant1 in variants) {
-      for (final String variant2 in variants) {
-        if (variant2.startsWith(variant1) && variant2 != variant1) {
-          final String buildType = variant2.substring(variant1.length);
-          if (variants.contains(buildType)) {
-            buildTypes.add(buildType);
-            productFlavors.add(variant1);
-          }
-        }
-      }
-    }
-    if (productFlavors.isEmpty) {
-      buildTypes.addAll(variants);
-    }
-    return GradleProject(
-        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;
-
-  /// 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);
-    if (buildTypes.contains(modeName.toLowerCase())) {
-      return modeName;
-    }
-    return null;
-  }
-
-  String _productFlavorFor(BuildInfo buildInfo) {
-    if (buildInfo.flavor == null) {
-      return productFlavors.isEmpty ? '' : null;
-    } else if (productFlavors.contains(buildInfo.flavor)) {
-      return buildInfo.flavor;
-    }
-    return null;
-  }
-
-  String assembleTaskFor(BuildInfo buildInfo) {
-    final String buildType = _buildTypeFor(buildInfo);
-    final String productFlavor = _productFlavorFor(buildInfo);
-    if (buildType == null || productFlavor == null) {
-      return null;
-    }
-    return 'assemble${toTitleCase(productFlavor)}${toTitleCase(buildType)}';
-  }
-
-  Iterable<String> apkFilesFor(AndroidBuildInfo androidBuildInfo) {
-    final String buildType = _buildTypeFor(androidBuildInfo.buildInfo);
-    final String productFlavor = _productFlavorFor(androidBuildInfo.buildInfo);
-    if (buildType == null || productFlavor == null) {
-      return const <String>[];
-    }
-
-    final String flavorString = productFlavor.isEmpty ? '' : '-' + productFlavor;
-    if (androidBuildInfo.splitPerAbi) {
-      return androidBuildInfo.targetArchs.map<String>((AndroidArch arch) {
-        final String abi = getNameForAndroidArch(arch);
-        return 'app$flavorString-$abi-$buildType.apk';
-      });
-    }
-    return <String>['app$flavorString-$buildType.apk'];
-  }
-
-  String bundleTaskFor(BuildInfo buildInfo) {
-    final String buildType = _buildTypeFor(buildInfo);
-    final String productFlavor = _productFlavorFor(buildInfo);
-    if (buildType == null || productFlavor == null) {
-      return null;
-    }
-    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)}';
-  }
-}
diff --git a/packages/flutter_tools/lib/src/android/gradle_errors.dart b/packages/flutter_tools/lib/src/android/gradle_errors.dart
new file mode 100644
index 0000000..bcec1a7
--- /dev/null
+++ b/packages/flutter_tools/lib/src/android/gradle_errors.dart
@@ -0,0 +1,326 @@
+// 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:meta/meta.dart';
+
+import '../base/process.dart';
+import '../base/terminal.dart';
+import '../globals.dart';
+import '../project.dart';
+import '../reporting/reporting.dart';
+import 'gradle_utils.dart';
+
+typedef GradleErrorTest = bool Function(String);
+
+/// A Gradle error handled by the tool.
+class GradleHandledError{
+  const GradleHandledError({
+    this.test,
+    this.handler,
+    this.eventLabel,
+  });
+
+  /// The test function.
+  /// Returns [true] if the current error message should be handled.
+  final GradleErrorTest test;
+
+  /// The handler function.
+  final Future<GradleBuildStatus> Function({
+    String line,
+    FlutterProject project,
+    bool usesAndroidX,
+    bool shouldBuildPluginAsAar,
+  }) handler;
+
+  /// The [BuildEvent] label is named gradle--[eventLabel].
+  /// If not empty, the build event is logged along with
+  /// additional metadata such as the attempt number.
+  final String eventLabel;
+}
+
+/// The status of the Gradle build.
+enum GradleBuildStatus{
+  /// The tool cannot recover from the failure and should exit.
+  exit,
+  /// The tool can retry the exact same build.
+  retry,
+  /// The tool can build the plugins as AAR and retry the build.
+  retryWithAarPlugins,
+}
+
+/// Returns a simple test function that evaluates to [true] if
+/// [errorMessage] is contained in the error message.
+GradleErrorTest _lineMatcher(List<String> errorMessages) {
+  return (String line) {
+    return errorMessages.any((String errorMessage) => line.contains(errorMessage));
+  };
+}
+
+/// The list of Gradle errors that the tool can handle.
+///
+/// The handlers are executed in the order in which they appear in the list.
+///
+/// Only the first error handler for which the [test] function returns [true]
+/// is handled. As a result, sort error handlers based on how strict the [test]
+/// function is to eliminate false positives.
+final List<GradleHandledError> gradleErrors = <GradleHandledError>[
+  licenseNotAcceptedHandler,
+  networkErrorHandler,
+  permissionDeniedErrorHandler,
+  flavorUndefinedHandler,
+  r8FailureHandler,
+  androidXFailureHandler,
+];
+
+// Permission defined error message.
+@visibleForTesting
+final GradleHandledError permissionDeniedErrorHandler = GradleHandledError(
+  test: _lineMatcher(const <String>[
+    'Permission denied',
+  ]),
+  handler: ({
+    String line,
+    FlutterProject project,
+    bool usesAndroidX,
+    bool shouldBuildPluginAsAar,
+  }) async {
+    printStatus('$warningMark Gradle does not have permission to execute by your user.', emphasis: true);
+    printStatus(
+      'You should change the ownership of the project directory to your user, '
+      'or move the project to a directory with execute permissions.',
+      indent: 4
+    );
+    return GradleBuildStatus.exit;
+  },
+  eventLabel: 'permission-denied',
+);
+
+// Gradle crashes for several known reasons when downloading that are not
+// actionable by flutter.
+@visibleForTesting
+final GradleHandledError networkErrorHandler = GradleHandledError(
+  test: _lineMatcher(const <String>[
+    'java.io.FileNotFoundException: https://downloads.gradle.org',
+    'java.io.IOException: Unable to tunnel through proxy',
+    'java.lang.RuntimeException: Timeout of',
+    'java.util.zip.ZipException: error in opening zip file',
+    'javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake',
+    'java.net.SocketException: Connection reset',
+    'java.io.FileNotFoundException',
+  ]),
+  handler: ({
+    String line,
+    FlutterProject project,
+    bool usesAndroidX,
+    bool shouldBuildPluginAsAar,
+  }) async {
+    printError(
+      '$warningMark Gradle threw an error while trying to update itself. '
+      'Retrying the update...'
+    );
+    return GradleBuildStatus.retry;
+  },
+  eventLabel: 'network',
+);
+
+// R8 failure.
+@visibleForTesting
+final GradleHandledError r8FailureHandler = GradleHandledError(
+  test: _lineMatcher(const <String>[
+    'com.android.tools.r8',
+  ]),
+  handler: ({
+    String line,
+    FlutterProject project,
+    bool usesAndroidX,
+    bool shouldBuildPluginAsAar,
+  }) async {
+    printStatus('$warningMark The shrinker may have failed to optimize the Java bytecode.', emphasis: true);
+    printStatus('To disable the shrinker, pass the `--no-shrink` flag to this command.', indent: 4);
+    printStatus('To learn more, see: https://developer.android.com/studio/build/shrink-code', indent: 4);
+    return GradleBuildStatus.exit;
+  },
+  eventLabel: 'r8',
+);
+
+// AndroidX failure.
+//
+// This regex is intentionally broad. AndroidX errors can manifest in multiple
+// different ways and each one depends on the specific code config and
+// filesystem paths of the project. Throwing the broadest net possible here to
+// catch all known and likely cases.
+//
+// Example stack traces:
+// https://github.com/flutter/flutter/issues/27226 "AAPT: error: resource android:attr/fontVariationSettings not found."
+// https://github.com/flutter/flutter/issues/27106 "Android resource linking failed|Daemon: AAPT2|error: failed linking references"
+// https://github.com/flutter/flutter/issues/27493 "error: cannot find symbol import androidx.annotation.NonNull;"
+// https://github.com/flutter/flutter/issues/23995 "error: package android.support.annotation does not exist import android.support.annotation.NonNull;"
+final RegExp _androidXFailureRegex = RegExp(r'(AAPT|androidx|android\.support)');
+
+final RegExp androidXPluginWarningRegex = RegExp(r'\*{57}'
+  r"|WARNING: This version of (\w+) will break your Android build if it or its dependencies aren't compatible with AndroidX."
+  r'|See https://goo.gl/CP92wY for more information on the problem and how to fix it.'
+  r'|This warning prints for all Android build failures. The real root cause of the error may be unrelated.');
+
+@visibleForTesting
+final GradleHandledError androidXFailureHandler = GradleHandledError(
+  test: (String line) {
+    return !androidXPluginWarningRegex.hasMatch(line) &&
+           _androidXFailureRegex.hasMatch(line);
+  },
+  handler: ({
+    String line,
+    FlutterProject project,
+    bool usesAndroidX,
+    bool shouldBuildPluginAsAar,
+  }) async {
+    final bool hasPlugins = project.flutterPluginsFile.existsSync();
+    if (!hasPlugins) {
+      // If the app doesn't use any plugin, then it's unclear where
+      // the incompatibility is coming from.
+      BuildEvent(
+        'gradle--android-x-failure',
+        eventError: 'app-not-using-plugins',
+      ).send();
+    }
+    if (hasPlugins && !usesAndroidX) {
+      // If the app isn't using AndroidX, then the app is likely using
+      // a plugin already migrated to AndroidX.
+      printStatus(
+        'AndroidX incompatibilities may have caused this build to fail. '
+        'Please migrate your app to AndroidX. See https://goo.gl/CP92wY.'
+      );
+      BuildEvent(
+        'gradle--android-x-failure',
+        eventError: 'app-not-using-androidx',
+      ).send();
+    }
+    if (hasPlugins && usesAndroidX && shouldBuildPluginAsAar) {
+      // This is a dependency conflict instead of an AndroidX failure since
+      // by this point the app is using AndroidX, the plugins are built as
+      // AARs, Jetifier translated Support libraries for AndroidX equivalents.
+      BuildEvent(
+        'gradle--android-x-failure',
+        eventError: 'using-jetifier',
+      ).send();
+    }
+    if (hasPlugins && usesAndroidX && !shouldBuildPluginAsAar) {
+      printStatus(
+        'The built failed likely due to AndroidX incompatibilities in a plugin. '
+        'The tool is about to try using Jetfier to solve the incompatibility.'
+      );
+      BuildEvent(
+        'gradle--android-x-failure',
+        eventError: 'not-using-jetifier',
+      ).send();
+      return GradleBuildStatus.retryWithAarPlugins;
+    }
+    return GradleBuildStatus.exit;
+  },
+  eventLabel: 'android-x',
+);
+
+/// 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.
+@visibleForTesting
+final GradleHandledError licenseNotAcceptedHandler = GradleHandledError(
+  test: _lineMatcher(const <String>[
+    'You have not accepted the license agreements of the following SDK components',
+  ]),
+  handler: ({
+    String line,
+    FlutterProject project,
+    bool usesAndroidX,
+    bool shouldBuildPluginAsAar,
+  }) async {
+    const String licenseNotAcceptedMatcher =
+      r'You have not accepted the license agreements of the following SDK components:'
+      r'\s*\[(.+)\]';
+
+    final RegExp licenseFailure = RegExp(licenseNotAcceptedMatcher, multiLine: true);
+    assert(licenseFailure != null);
+    final Match licenseMatch = licenseFailure.firstMatch(line);
+    printStatus(
+      '$warningMark Unable to download needed Android SDK components, as the '
+      'following licenses have not been accepted:\n'
+      '${licenseMatch.group(1)}\n\n'
+      'To resolve this, please run the following command in a Terminal:\n'
+      'flutter doctor --android-licenses'
+    );
+    return GradleBuildStatus.exit;
+  },
+  eventLabel: 'license-not-accepted',
+);
+
+final RegExp _undefinedTaskPattern = RegExp(r'Task .+ not found in root project.');
+
+final RegExp _assembleTaskPattern = RegExp(r'assemble(\S+)');
+
+/// Handler when a flavor is undefined.
+@visibleForTesting
+final GradleHandledError flavorUndefinedHandler = GradleHandledError(
+  test: (String line) {
+    return _undefinedTaskPattern.hasMatch(line);
+  },
+  handler: ({
+    String line,
+    FlutterProject project,
+    bool usesAndroidX,
+    bool shouldBuildPluginAsAar,
+  }) async {
+    final RunResult tasksRunResult = await processUtils.run(
+      <String>[
+        gradleUtils.getExecutable(project),
+        'app:tasks' ,
+        '--all',
+        '--console=auto',
+      ],
+      throwOnError: true,
+      workingDirectory: project.android.hostAppGradleRoot.path,
+      environment: gradleEnvironment,
+    );
+    // Extract build types and product flavors.
+    final Set<String> variants = <String>{};
+    for (String task in tasksRunResult.stdout.split('\n')) {
+      final Match match = _assembleTaskPattern.matchAsPrefix(task);
+      if (match != null) {
+        final String variant = match.group(1).toLowerCase();
+        if (!variant.endsWith('test')) {
+          variants.add(variant);
+        }
+      }
+    }
+    final Set<String> productFlavors = <String>{};
+    for (final String variant1 in variants) {
+      for (final String variant2 in variants) {
+        if (variant2.startsWith(variant1) && variant2 != variant1) {
+          final String buildType = variant2.substring(variant1.length);
+          if (variants.contains(buildType)) {
+            productFlavors.add(variant1);
+          }
+        }
+      }
+    }
+    printStatus(
+      '\n$warningMark  Gradle project does not define a task suitable '
+      'for the requested build.'
+    );
+    if (productFlavors.isEmpty) {
+      printStatus(
+        'The android/app/build.gradle file does not define '
+        'any custom product flavors. '
+        'You cannot use the --flavor option.'
+      );
+    } else {
+      printStatus(
+        'The android/app/build.gradle file defines product '
+        'flavors: ${productFlavors.join(', ')} '
+        'You must specify a --flavor option to select one of them.'
+      );
+    }
+    return GradleBuildStatus.exit;
+  },
+  eventLabel: 'flavor-undefined',
+);
diff --git a/packages/flutter_tools/lib/src/android/gradle_utils.dart b/packages/flutter_tools/lib/src/android/gradle_utils.dart
new file mode 100644
index 0000000..0b9eaa0
--- /dev/null
+++ b/packages/flutter_tools/lib/src/android/gradle_utils.dart
@@ -0,0 +1,284 @@
+// 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:meta/meta.dart';
+
+import '../android/android_sdk.dart';
+import '../base/common.dart';
+import '../base/context.dart';
+import '../base/file_system.dart';
+import '../base/os.dart';
+import '../base/platform.dart';
+import '../base/terminal.dart';
+import '../base/utils.dart';
+import '../base/version.dart';
+import '../build_info.dart';
+import '../cache.dart';
+import '../globals.dart';
+import '../project.dart';
+import '../reporting/reporting.dart';
+import 'android_sdk.dart';
+import 'android_studio.dart';
+
+/// The environment variables needed to run Gradle.
+Map<String, String> get gradleEnvironment {
+  final Map<String, String> environment = Map<String, String>.from(platform.environment);
+  if (javaPath != null) {
+    // Use java bundled with Android Studio.
+    environment['JAVA_HOME'] = javaPath;
+  }
+  // Don't log analytics for downstream Flutter commands.
+  // e.g. `flutter build bundle`.
+  environment['FLUTTER_SUPPRESS_ANALYTICS'] = 'true';
+  return environment;
+}
+
+/// Gradle utils in the current [AppContext].
+GradleUtils get gradleUtils => context.get<GradleUtils>();
+
+/// Provides utilities to run a Gradle task,
+/// such as finding the Gradle executable or constructing a Gradle project.
+class GradleUtils {
+  /// Gets the Gradle executable path and prepares the Gradle project.
+  /// This is the `gradlew` or `gradlew.bat` script in the `android/` directory.
+  String getExecutable(FlutterProject project) {
+    final Directory androidDir = project.android.hostAppGradleRoot;
+    // Update the project if needed.
+    // TODO(egarciad): https://github.com/flutter/flutter/issues/40460
+    migrateToR8(androidDir);
+    injectGradleWrapperIfNeeded(androidDir);
+
+    final File gradle = androidDir.childFile(
+      platform.isWindows ? 'gradlew.bat' : 'gradlew',
+    );
+    if (gradle.existsSync()) {
+      printTrace('Using gradle from ${gradle.absolute.path}.');
+      return gradle.absolute.path;
+    }
+    throwToolExit(
+      'Unable to locate gradlew script. Please check that ${gradle.path} '
+      'exists or that ${gradle.dirname} can be read.'
+    );
+    return null;
+  }
+}
+
+/// Migrates the Android's [directory] to R8.
+/// https://developer.android.com/studio/build/shrink-code
+@visibleForTesting
+void migrateToR8(Directory directory) {
+  final File gradleProperties = directory.childFile('gradle.properties');
+  if (!gradleProperties.existsSync()) {
+    throwToolExit(
+      'Expected file ${gradleProperties.path}. '
+      'Please ensure that this file exists or that ${gradleProperties.dirname} can be read.'
+    );
+  }
+  final String propertiesContent = gradleProperties.readAsStringSync();
+  if (propertiesContent.contains('android.enableR8')) {
+    printTrace('gradle.properties already sets `android.enableR8`');
+    return;
+  }
+  printTrace('set `android.enableR8=true` in gradle.properties');
+  try {
+    if (propertiesContent.isNotEmpty && !propertiesContent.endsWith('\n')) {
+      // Add a new line if the file doesn't end with a new line.
+      gradleProperties.writeAsStringSync('\n', mode: FileMode.append);
+    }
+    gradleProperties.writeAsStringSync('android.enableR8=true\n', mode: FileMode.append);
+  } on FileSystemException {
+    throwToolExit(
+      'The tool failed to add `android.enableR8=true` to ${gradleProperties.path}. '
+      'Please update the file manually and try this command again.'
+    );
+  }
+}
+
+/// Injects the Gradle wrapper files if any of these files don't exist in [directory].
+void injectGradleWrapperIfNeeded(Directory directory) {
+  copyDirectorySync(
+    cache.getArtifactDirectory('gradle_wrapper'),
+    directory,
+    shouldCopyFile: (File sourceFile, File destinationFile) {
+      // Don't override the existing files in the project.
+      return !destinationFile.existsSync();
+    },
+    onFileCopied: (File sourceFile, File destinationFile) {
+      final String modes = sourceFile.statSync().modeString();
+      if (modes != null && modes.contains('x')) {
+        os.makeExecutable(destinationFile);
+      }
+    },
+  );
+  // Add the `gradle-wrapper.properties` file if it doesn't exist.
+  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
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersion-all.zip
+''', flush: true,
+    );
+  }
+}
+
+const String _defaultGradleVersion = '5.6.2';
+
+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);
+}
+
+/// Returns true if [targetVersion] is within the range [min] and [max] inclusive.
+bool _isWithinVersionRange(
+  String targetVersion, {
+  @required String min,
+  @required String max,
+}) {
+  assert(min != null);
+  assert(max != null);
+  final Version parsedTargetVersion = Version.parse(targetVersion);
+  return parsedTargetVersion >= Version.parse(min) &&
+         parsedTargetVersion <= Version.parse(max);
+}
+
+/// 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.6.2';
+  }
+  throwToolExit('Unsuported Android Plugin version: $androidPluginVersion.');
+  return '';
+}
+
+/// Overwrite local.properties in the specified Flutter project's Android
+/// sub-project, if needed.
+///
+/// If [requireAndroidSdk] is true (the default) and no Android SDK is found,
+/// this will fail with a [ToolExit].
+void updateLocalProperties({
+  @required FlutterProject project,
+  BuildInfo buildInfo,
+  bool requireAndroidSdk = true,
+}) {
+  if (requireAndroidSdk && androidSdk == null) {
+    exitWithNoSdkMessage();
+  }
+  final File localProperties = project.android.localPropertiesFile;
+  bool changed = false;
+
+  SettingsFile settings;
+  if (localProperties.existsSync()) {
+    settings = SettingsFile.parseFromFile(localProperties);
+  } else {
+    settings = SettingsFile();
+    changed = true;
+  }
+
+  void changeIfNecessary(String key, String value) {
+    if (settings.values[key] == value) {
+      return;
+    }
+    if (value == null) {
+      settings.values.remove(key);
+    } else {
+      settings.values[key] = value;
+    }
+    changed = true;
+  }
+
+  if (androidSdk != null) {
+    changeIfNecessary('sdk.dir', escapePath(androidSdk.directory));
+  }
+
+  changeIfNecessary('flutter.sdk', escapePath(Cache.flutterRoot));
+  if (buildInfo != null) {
+    changeIfNecessary('flutter.buildMode', buildInfo.modeName);
+    final String buildName = validatedBuildNameForPlatform(
+      TargetPlatform.android_arm,
+      buildInfo.buildName ?? project.manifest.buildName,
+    );
+    changeIfNecessary('flutter.versionName', buildName);
+    final String buildNumber = validatedBuildNumberForPlatform(
+      TargetPlatform.android_arm,
+      buildInfo.buildNumber ?? project.manifest.buildNumber,
+    );
+    changeIfNecessary('flutter.versionCode', buildNumber?.toString());
+  }
+
+  if (changed) {
+    settings.writeContents(localProperties);
+  }
+}
+
+/// Writes standard Android local properties to the specified [properties] file.
+///
+/// Writes the path to the Android SDK, if known.
+void writeLocalProperties(File properties) {
+  final SettingsFile settings = SettingsFile();
+  if (androidSdk != null) {
+    settings.values['sdk.dir'] = escapePath(androidSdk.directory);
+  }
+  settings.writeContents(properties);
+}
+
+void exitWithNoSdkMessage() {
+  BuildEvent('unsupported-project', eventError: 'android-sdk-not-found').send();
+  throwToolExit(
+    '$warningMark No Android SDK found. '
+    'Try setting the ANDROID_HOME environment variable.'
+  );
+}
diff --git a/packages/flutter_tools/lib/src/base/terminal.dart b/packages/flutter_tools/lib/src/base/terminal.dart
index 22ba9aa..09bfd59 100644
--- a/packages/flutter_tools/lib/src/base/terminal.dart
+++ b/packages/flutter_tools/lib/src/base/terminal.dart
@@ -23,6 +23,17 @@
 AnsiTerminal get terminal {
   return context?.get<AnsiTerminal>() ?? _defaultAnsiTerminal;
 }
+
+/// Warning mark to use in stdout or stderr.
+String get warningMark {
+  return terminal.bolden(terminal.color('[!]', TerminalColor.red));
+}
+
+/// Success mark to use in stdout.
+String get successMark {
+  return terminal.bolden(terminal.color('✓', TerminalColor.green));
+}
+
 final AnsiTerminal _defaultAnsiTerminal = AnsiTerminal();
 
 OutputPreferences get outputPreferences {
diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart
index c52fb18..f18d33c 100644
--- a/packages/flutter_tools/lib/src/cache.dart
+++ b/packages/flutter_tools/lib/src/cache.dart
@@ -6,7 +6,7 @@
 
 import 'package:meta/meta.dart';
 
-import 'android/gradle.dart';
+import 'android/gradle_utils.dart';
 import 'base/common.dart';
 import 'base/context.dart';
 import 'base/file_system.dart';
@@ -914,7 +914,7 @@
           '--project-cache-dir', tempDir.path,
           'resolveDependencies',
         ],
-        environment: gradleEnv);
+        environment: gradleEnvironment);
       if (processResult.exitCode != 0) {
         printError('Failed to download the Android dependencies');
       }
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index 4d26cb9..808456c 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -10,7 +10,7 @@
 
 import '../android/android.dart' as android;
 import '../android/android_sdk.dart' as android_sdk;
-import '../android/gradle.dart' as gradle;
+import '../android/gradle_utils.dart' as gradle;
 import '../base/common.dart';
 import '../base/file_system.dart';
 import '../base/net.dart';
diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart
index c909973..e210afd 100644
--- a/packages/flutter_tools/lib/src/context_runner.dart
+++ b/packages/flutter_tools/lib/src/context_runner.dart
@@ -7,7 +7,7 @@
 import 'android/android_sdk.dart';
 import 'android/android_studio.dart';
 import 'android/android_workflow.dart';
-import 'android/gradle.dart';
+import 'android/gradle_utils.dart';
 import 'application_package.dart';
 import 'artifacts.dart';
 import 'asset.dart';
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index 9568e58..5bc5c77 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -7,7 +7,7 @@
 import 'package:meta/meta.dart';
 import 'package:yaml/yaml.dart';
 
-import 'android/gradle.dart' as gradle;
+import 'android/gradle_utils.dart' as gradle;
 import 'base/common.dart';
 import 'base/context.dart';
 import 'base/file_system.dart';
@@ -574,6 +574,11 @@
     return _firstMatchInFile(gradleFile, _groupPattern)?.group(1);
   }
 
+  /// The build directory where the Android artifacts are placed.
+  Directory get buildDirectory {
+    return parent.directory.childDirectory('build');
+  }
+
   Future<void> ensureReadyForPlatformSpecificTooling() async {
     if (isModule && _shouldRegenerateFromTemplate()) {
       _regenerateLibrary();
diff --git a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart
new file mode 100644
index 0000000..aca7919
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart
@@ -0,0 +1,602 @@
+// 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:file/memory.dart';
+
+import 'package:flutter_tools/src/android/gradle_utils.dart';
+import 'package:flutter_tools/src/android/gradle_errors.dart';
+import 'package:flutter_tools/src/base/context.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/project.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
+import 'package:mockito/mockito.dart';
+import 'package:platform/platform.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+import '../../src/mocks.dart';
+
+void main() {
+  group('gradleErrors', () {
+    test('list of errors', () {
+      // If you added a new Gradle error, please update this test.
+      expect(gradleErrors,
+        equals(<GradleHandledError>[
+          licenseNotAcceptedHandler,
+          networkErrorHandler,
+          permissionDeniedErrorHandler,
+          flavorUndefinedHandler,
+          r8FailureHandler,
+          androidXFailureHandler,
+        ])
+      );
+    });
+  });
+
+  group('network errors', () {
+    testUsingContext('throws toolExit if gradle fails while downloading', () async {
+      const String errorMessage = '''
+Exception in thread "main" java.io.FileNotFoundException: https://downloads.gradle.org/distributions/gradle-4.1.1-all.zip
+at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1872)
+at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1474)
+at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254)
+at org.gradle.wrapper.Download.downloadInternal(Download.java:58)
+at org.gradle.wrapper.Download.download(Download.java:44)
+at org.gradle.wrapper.Install\$1.call(Install.java:61)
+at org.gradle.wrapper.Install\$1.call(Install.java:48)
+at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:65)
+at org.gradle.wrapper.Install.createDist(Install.java:48)
+at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128)
+at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
+
+      expect(testErrorMessage(errorMessage, networkErrorHandler), isTrue);
+      expect(await networkErrorHandler.handler(), equals(GradleBuildStatus.retry));
+
+      final BufferLogger logger = context.get<Logger>();
+      expect(logger.errorText,
+        contains(
+          'Gradle threw an error while trying to update itself. '
+          'Retrying the update...'
+        )
+      );
+    });
+
+    testUsingContext('throw toolExit if gradle fails downloading with proxy error', () async {
+      const String errorMessage = '''
+Exception in thread "main" java.io.IOException: Unable to tunnel through proxy. Proxy returns "HTTP/1.1 400 Bad Request"
+at sun.net.www.protocol.http.HttpURLConnection.doTunneling(HttpURLConnection.java:2124)
+at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:183)
+at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1546)
+at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1474)
+at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254)
+at org.gradle.wrapper.Download.downloadInternal(Download.java:58)
+at org.gradle.wrapper.Download.download(Download.java:44)
+at org.gradle.wrapper.Install\$1.call(Install.java:61)
+at org.gradle.wrapper.Install\$1.call(Install.java:48)
+at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:65)
+at org.gradle.wrapper.Install.createDist(Install.java:48)
+at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128)
+at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
+
+      expect(testErrorMessage(errorMessage, networkErrorHandler), isTrue);
+      expect(await networkErrorHandler.handler(), equals(GradleBuildStatus.retry));
+
+      final BufferLogger logger = context.get<Logger>();
+      expect(logger.errorText,
+        contains(
+          'Gradle threw an error while trying to update itself. '
+          'Retrying the update...'
+        )
+      );
+    });
+
+    testUsingContext('throws toolExit if gradle times out waiting for exclusive access to zip', () async {
+      const String errorMessage = '''
+Exception in thread "main" java.lang.RuntimeException: Timeout of 120000 reached waiting for exclusive access to file: /User/documents/gradle-5.6.2-all.zip
+	at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:61)
+	at org.gradle.wrapper.Install.createDist(Install.java:48)
+	at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128)
+	at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
+
+      expect(testErrorMessage(errorMessage, networkErrorHandler), isTrue);
+      expect(await networkErrorHandler.handler(), equals(GradleBuildStatus.retry));
+
+      final BufferLogger logger = context.get<Logger>();
+      expect(logger.errorText,
+        contains(
+          'Gradle threw an error while trying to update itself. '
+          'Retrying the update...'
+        )
+      );
+    });
+
+    testUsingContext('throws toolExit if remote host closes connection', () async {
+      const String errorMessage = '''
+Downloading https://services.gradle.org/distributions/gradle-5.6.2-all.zip
+Exception in thread "main" javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake
+	at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:994)
+	at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367)
+	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395)
+	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379)
+	at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
+	at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
+	at sun.net.www.protocol.http.HttpURLConnection.followRedirect0(HttpURLConnection.java:2729)
+	at sun.net.www.protocol.http.HttpURLConnection.followRedirect(HttpURLConnection.java:2641)
+	at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1824)
+	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1492)
+	at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:263)
+	at org.gradle.wrapper.Download.downloadInternal(Download.java:58)
+	at org.gradle.wrapper.Download.download(Download.java:44)
+	at org.gradle.wrapper.Install\$1.call(Install.java:61)
+	at org.gradle.wrapper.Install\$1.call(Install.java:48)
+	at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:65)
+	at org.gradle.wrapper.Install.createDist(Install.java:48)
+	at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128)
+	at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
+
+      expect(testErrorMessage(errorMessage, networkErrorHandler), isTrue);
+      expect(await networkErrorHandler.handler(), equals(GradleBuildStatus.retry));
+
+      final BufferLogger logger = context.get<Logger>();
+      expect(logger.errorText,
+        contains(
+          'Gradle threw an error while trying to update itself. '
+          'Retrying the update...'
+        )
+      );
+    });
+
+    testUsingContext('throws toolExit if file opening fails', () async {
+      const String errorMessage = r'''
+Downloading https://services.gradle.org/distributions/gradle-3.5.0-all.zip
+Exception in thread "main" java.io.FileNotFoundException: https://downloads.gradle-dn.com/distributions/gradle-3.5.0-all.zip
+	at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1890)
+	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1492)
+	at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:263)
+	at org.gradle.wrapper.Download.downloadInternal(Download.java:58)
+	at org.gradle.wrapper.Download.download(Download.java:44)
+	at org.gradle.wrapper.Install$1.call(Install.java:61)
+	at org.gradle.wrapper.Install$1.call(Install.java:48)
+	at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:65)
+	at org.gradle.wrapper.Install.createDist(Install.java:48)
+	at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128)
+	at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
+
+      expect(testErrorMessage(errorMessage, networkErrorHandler), isTrue);
+      expect(await networkErrorHandler.handler(), equals(GradleBuildStatus.retry));
+
+      final BufferLogger logger = context.get<Logger>();
+      expect(logger.errorText,
+        contains(
+          'Gradle threw an error while trying to update itself. '
+          'Retrying the update...'
+        )
+      );
+    });
+
+    testUsingContext('throws toolExit if the connection is reset', () async {
+      const String errorMessage = '''
+Downloading https://services.gradle.org/distributions/gradle-5.6.2-all.zip
+Exception in thread "main" java.net.SocketException: Connection reset
+	at java.net.SocketInputStream.read(SocketInputStream.java:210)
+	at java.net.SocketInputStream.read(SocketInputStream.java:141)
+	at sun.security.ssl.InputRecord.readFully(InputRecord.java:465)
+	at sun.security.ssl.InputRecord.readV3Record(InputRecord.java:593)
+	at sun.security.ssl.InputRecord.read(InputRecord.java:532)
+	at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:975)
+	at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367)
+	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395)
+	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379)
+	at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
+	at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
+	at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1564)
+	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1492)
+	at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:263)
+	at org.gradle.wrapper.Download.downloadInternal(Download.java:58)
+	at org.gradle.wrapper.Download.download(Download.java:44)
+	at org.gradle.wrapper.Install\$1.call(Install.java:61)
+	at org.gradle.wrapper.Install\$1.call(Install.java:48)
+	at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:65)
+	at org.gradle.wrapper.Install.createDist(Install.java:48)
+	at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128)
+	at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
+
+      expect(testErrorMessage(errorMessage, networkErrorHandler), isTrue);
+      expect(await networkErrorHandler.handler(), equals(GradleBuildStatus.retry));
+
+      final BufferLogger logger = context.get<Logger>();
+      expect(logger.errorText,
+        contains(
+          'Gradle threw an error while trying to update itself. '
+          'Retrying the update...'
+        )
+      );
+    });
+  });
+
+  group('permission errors', () {
+    testUsingContext('throws toolExit if gradle is missing execute permissions', () async {
+      const String errorMessage = '''
+Permission denied
+Command: /home/android/gradlew assembleRelease
+''';
+      expect(testErrorMessage(errorMessage, permissionDeniedErrorHandler), isTrue);
+      expect(await permissionDeniedErrorHandler.handler(), equals(GradleBuildStatus.exit));
+
+      final BufferLogger logger = context.get<Logger>();
+      expect(
+        logger.statusText,
+        contains('Gradle does not have permission to execute by your user.'),
+      );
+      expect(
+        logger.statusText,
+        contains(
+          'You should change the ownership of the project directory to your user, '
+          'or move the project to a directory with execute permissions.'
+        )
+      );
+    });
+  });
+
+  group('AndroidX', () {
+    final Usage mockUsage = MockUsage();
+
+    test('pattern', () {
+      expect(androidXFailureHandler.test(
+        'AAPT: error: resource android:attr/fontVariationSettings not found.'
+      ), isTrue);
+
+      expect(androidXFailureHandler.test(
+        'AAPT: error: resource android:attr/ttcIndex not found.'
+      ), isTrue);
+
+      expect(androidXFailureHandler.test(
+        'error: package android.support.annotation does not exist'
+      ), isTrue);
+
+      expect(androidXFailureHandler.test(
+        'import android.support.annotation.NonNull;'
+      ), isTrue);
+
+      expect(androidXFailureHandler.test(
+        'import androidx.annotation.NonNull;'
+      ), isTrue);
+
+      expect(androidXFailureHandler.test(
+        'Daemon:  AAPT2 aapt2-3.2.1-4818971-linux Daemon #0'
+      ), isTrue);
+    });
+
+    testUsingContext('handler - no plugins', () async {
+      final GradleBuildStatus status = await androidXFailureHandler
+        .handler(line: '', project: FlutterProject.current());
+
+      verify(mockUsage.sendEvent(
+        any,
+        any,
+        label: 'gradle--android-x-failure',
+        parameters: <String, String>{
+          'cd43': 'app-not-using-plugins',
+        },
+      )).called(1);
+
+      expect(status, equals(GradleBuildStatus.exit));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => MockProcessManager(),
+      Usage: () => mockUsage,
+    });
+
+    testUsingContext('handler - plugins and no AndroidX', () async {
+      fs.file('.flutter-plugins').createSync(recursive: true);
+
+      final GradleBuildStatus status = await androidXFailureHandler
+        .handler(
+          line: '',
+          project: FlutterProject.current(),
+          usesAndroidX: false,
+        );
+
+      final BufferLogger logger = context.get<Logger>();
+      expect(logger.statusText,
+        contains(
+          'AndroidX incompatibilities may have caused this build to fail. '
+          'Please migrate your app to AndroidX. See https://goo.gl/CP92wY.'
+        )
+      );
+      verify(mockUsage.sendEvent(
+        any,
+        any,
+        label: 'gradle--android-x-failure',
+        parameters: <String, String>{
+          'cd43': 'app-not-using-androidx',
+        },
+      )).called(1);
+
+      expect(status, equals(GradleBuildStatus.exit));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => MockProcessManager(),
+      Usage: () => mockUsage,
+    });
+
+    testUsingContext('handler - plugins, AndroidX, and AAR', () async {
+      fs.file('.flutter-plugins').createSync(recursive: true);
+
+      final GradleBuildStatus status = await androidXFailureHandler.handler(
+        line: '',
+        project: FlutterProject.current(),
+        usesAndroidX: true,
+        shouldBuildPluginAsAar: true,
+      );
+
+      verify(mockUsage.sendEvent(
+        any,
+        any,
+        label: 'gradle--android-x-failure',
+        parameters: <String, String>{
+          'cd43': 'using-jetifier',
+        },
+      )).called(1);
+
+      expect(status, equals(GradleBuildStatus.exit));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => MockProcessManager(),
+      Usage: () => mockUsage,
+    });
+
+    testUsingContext('handler - plugins, AndroidX, and no AAR', () async {
+      fs.file('.flutter-plugins').createSync(recursive: true);
+
+      final GradleBuildStatus status = await androidXFailureHandler.handler(
+        line: '',
+        project: FlutterProject.current(),
+        usesAndroidX: true,
+        shouldBuildPluginAsAar: false,
+      );
+
+      final BufferLogger logger = context.get<Logger>();
+      expect(logger.statusText,
+        contains(
+          'The built failed likely due to AndroidX incompatibilities in a plugin. '
+          'The tool is about to try using Jetfier to solve the incompatibility.'
+        )
+      );
+      verify(mockUsage.sendEvent(
+        any,
+        any,
+        label: 'gradle--android-x-failure',
+        parameters: <String, String>{
+          'cd43': 'not-using-jetifier',
+        },
+      )).called(1);
+      expect(status, equals(GradleBuildStatus.retryWithAarPlugins));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => MockProcessManager(),
+      Usage: () => mockUsage,
+    });
+  });
+
+  group('permission errors', () {
+    testUsingContext('pattern', () async {
+      const String errorMessage = '''
+Permission denied
+Command: /home/android/gradlew assembleRelease
+''';
+      expect(testErrorMessage(errorMessage, permissionDeniedErrorHandler), isTrue);
+    });
+
+    testUsingContext('handler', () async {
+      expect(await permissionDeniedErrorHandler.handler(), equals(GradleBuildStatus.exit));
+
+      final BufferLogger logger = context.get<Logger>();
+      expect(
+        logger.statusText,
+        contains('Gradle does not have permission to execute by your user.'),
+      );
+      expect(
+        logger.statusText,
+        contains(
+          'You should change the ownership of the project directory to your user, '
+          'or move the project to a directory with execute permissions.'
+        )
+      );
+    });
+  });
+
+  group('license not accepted', () {
+    test('pattern', () {
+      expect(
+        licenseNotAcceptedHandler.test(
+          'You have not accepted the license agreements of the following SDK components'
+        ),
+        isTrue,
+      );
+    });
+
+    testUsingContext('handler', () async {
+      await licenseNotAcceptedHandler.handler(
+        line: 'You have not accepted the license agreements of the following SDK components: [foo, bar]',
+        project: FlutterProject.current(),
+      );
+
+      final BufferLogger logger = context.get<Logger>();
+      expect(
+        logger.statusText,
+        contains(
+          'Unable to download needed Android SDK components, as the '
+          'following licenses have not been accepted:\n'
+          'foo, bar\n\n'
+          'To resolve this, please run the following command in a Terminal:\n'
+          'flutter doctor --android-licenses'
+        )
+      );
+    });
+  });
+
+  group('flavor undefined', () {
+    MockProcessManager mockProcessManager;
+
+    setUp(() {
+      mockProcessManager = MockProcessManager();
+    });
+
+    test('pattern', () {
+      expect(
+        flavorUndefinedHandler.test(
+          'Task assembleFooRelease not found in root project.'
+        ),
+        isTrue,
+      );
+      expect(
+        flavorUndefinedHandler.test(
+          'Task assembleBarRelease not found in root project.'
+        ),
+        isTrue,
+      );
+      expect(
+        flavorUndefinedHandler.test(
+          'Task assembleBar not found in root project.'
+        ),
+        isTrue,
+      );
+      expect(
+        flavorUndefinedHandler.test(
+          'Task assembleBar_foo not found in root project.'
+        ),
+        isTrue,
+      );
+    });
+
+    testUsingContext('handler - with flavor', () async {
+      when(mockProcessManager.run(
+        <String>[
+          'gradlew',
+          'app:tasks' ,
+          '--all',
+          '--console=auto',
+        ],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) async {
+        return ProcessResult(
+          1,
+          0,
+          '''
+assembleRelease
+assembleFlavor1
+assembleFlavor1Release
+assembleFlavor_2
+assembleFlavor_2Release
+assembleDebug
+assembleProfile
+assembles
+assembleFooTest
+          ''',
+          '',
+        );
+      });
+
+      await flavorUndefinedHandler.handler(
+        project: FlutterProject.current(),
+      );
+
+      final BufferLogger logger = context.get<Logger>();
+      expect(
+        logger.statusText,
+        contains(
+          'Gradle project does not define a task suitable '
+          'for the requested build.'
+        )
+      );
+      expect(
+        logger.statusText,
+        contains(
+          'The android/app/build.gradle file defines product '
+          'flavors: flavor1, flavor_2 '
+          'You must specify a --flavor option to select one of them.'
+        )
+      );
+    }, overrides: <Type, Generator>{
+      GradleUtils: () => FakeGradleUtils(),
+      Platform: () => fakePlatform('android'),
+      ProcessManager: () => mockProcessManager,
+    });
+
+    testUsingContext('handler - without flavor', () async {
+      when(mockProcessManager.run(
+        <String>[
+          'gradlew',
+          'app:tasks' ,
+          '--all',
+          '--console=auto',
+        ],
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) async {
+        return ProcessResult(
+          1,
+          0,
+          '''
+assembleRelease
+assembleDebug
+assembleProfile
+          ''',
+          '',
+        );
+      });
+
+      await flavorUndefinedHandler.handler(
+        project: FlutterProject.current(),
+      );
+
+      final BufferLogger logger = context.get<Logger>();
+      expect(
+        logger.statusText,
+        contains(
+          'Gradle project does not define a task suitable '
+          'for the requested build.'
+        )
+      );
+      expect(
+        logger.statusText,
+        contains(
+          'The android/app/build.gradle file does not define any custom product flavors. '
+          'You cannot use the --flavor option.'
+        )
+      );
+    }, overrides: <Type, Generator>{
+      GradleUtils: () => FakeGradleUtils(),
+      Platform: () => fakePlatform('android'),
+      ProcessManager: () => mockProcessManager,
+    });
+  });
+}
+
+class MockUsage extends Mock implements Usage {}
+
+bool testErrorMessage(String errorMessage, GradleHandledError error) {
+  return errorMessage
+    .split('\n')
+    .any((String line) => error.test(line));
+}
+
+Platform fakePlatform(String name) {
+  return FakePlatform
+    .fromPlatform(const LocalPlatform())
+    ..operatingSystem = name;
+}
+
+class FakeGradleUtils extends GradleUtils {
+  @override
+  String getExecutable(FlutterProject project) {
+    return 'gradlew';
+  }
+}
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 21ec43e..0571175 100644
--- a/packages/flutter_tools/test/general.shard/android/gradle_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/gradle_test.dart
@@ -8,79 +8,370 @@
 import 'package:flutter_tools/src/android/android_sdk.dart';
 import 'package:flutter_tools/src/android/android_studio.dart';
 import 'package:flutter_tools/src/android/gradle.dart';
-import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/android/gradle_utils.dart';
+import 'package:flutter_tools/src/android/gradle_errors.dart';
 import 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/base/context.dart';
 import 'package:flutter_tools/src/base/common.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/io.dart';
-import 'package:flutter_tools/src/base/os.dart';
+import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/base/terminal.dart';
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
 import 'package:flutter_tools/src/project.dart';
+import 'package:flutter_tools/src/reporting/reporting.dart';
 import 'package:mockito/mockito.dart';
 import 'package:platform/platform.dart';
 import 'package:process/process.dart';
 
 import '../../src/common.dart';
 import '../../src/context.dart';
+import '../../src/mocks.dart';
 import '../../src/pubspec_schema.dart';
 
 void main() {
   Cache.flutterRoot = getFlutterRoot();
-  group('gradle build', () {
-    test('do not crash if there is no Android SDK', () async {
-      Exception shouldBeToolExit;
-      try {
-        // We'd like to always set androidSdk to null and test updateLocalProperties. But that's
-        // currently impossible as the test is not hermetic. Luckily, our bots don't have Android
-        // SDKs yet so androidSdk should be null by default.
-        //
-        // This test is written to fail if our bots get Android SDKs in the future: shouldBeToolExit
-        // will be null and our expectation would fail. That would remind us to make these tests
-        // hermetic before adding Android SDKs to the bots.
-        updateLocalProperties(project: FlutterProject.current());
-      } on Exception catch (e) {
-        shouldBeToolExit = e;
-      }
-      // Ensure that we throw a meaningful ToolExit instead of a general crash.
-      expect(shouldBeToolExit, isToolExit);
+
+  group('build artifacts', () {
+    test('getApkDirectory in app projects', () {
+      final FlutterProject project = MockFlutterProject();
+      final AndroidProject androidProject = MockAndroidProject();
+      when(project.android).thenReturn(androidProject);
+      when(project.isModule).thenReturn(false);
+      when(androidProject.buildDirectory).thenReturn(fs.directory('foo'));
+
+      expect(
+        getApkDirectory(project).path,
+        equals(fs.path.join('foo', 'app', 'outputs', 'apk')),
+      );
     });
 
-    // Regression test for https://github.com/flutter/flutter/issues/34700
-    testUsingContext('Does not return nulls in apk list', () {
-      final GradleProject gradleProject = MockGradleProject();
-      const AndroidBuildInfo buildInfo = AndroidBuildInfo(BuildInfo.debug);
-      when(gradleProject.apkFilesFor(buildInfo)).thenReturn(<String>['not_real']);
-      when(gradleProject.apkDirectory).thenReturn(fs.currentDirectory);
+    test('getApkDirectory in module projects', () {
+      final FlutterProject project = MockFlutterProject();
+      final AndroidProject androidProject = MockAndroidProject();
+      when(project.android).thenReturn(androidProject);
+      when(project.isModule).thenReturn(true);
+      when(androidProject.buildDirectory).thenReturn(fs.directory('foo'));
 
-      expect(findApkFiles(gradleProject, buildInfo), <File>[]);
+      expect(
+        getApkDirectory(project).path,
+        equals(fs.path.join('foo', 'host', 'outputs', 'apk')),
+      );
+    });
+
+    test('getBundleDirectory in app projects', () {
+      final FlutterProject project = MockFlutterProject();
+      final AndroidProject androidProject = MockAndroidProject();
+      when(project.android).thenReturn(androidProject);
+      when(project.isModule).thenReturn(false);
+      when(androidProject.buildDirectory).thenReturn(fs.directory('foo'));
+
+      expect(
+        getBundleDirectory(project).path,
+        equals(fs.path.join('foo', 'app', 'outputs', 'bundle')),
+      );
+    });
+
+    test('getBundleDirectory in module projects', () {
+      final FlutterProject project = MockFlutterProject();
+      final AndroidProject androidProject = MockAndroidProject();
+      when(project.android).thenReturn(androidProject);
+      when(project.isModule).thenReturn(true);
+      when(androidProject.buildDirectory).thenReturn(fs.directory('foo'));
+
+      expect(
+        getBundleDirectory(project).path,
+        equals(fs.path.join('foo', 'host', 'outputs', 'bundle')),
+      );
+    });
+
+    test('getRepoDirectory', () {
+      expect(
+        getRepoDirectory(fs.directory('foo')).path,
+        equals(fs.path.join('foo','outputs', 'repo')),
+      );
+    });
+  });
+
+  group('gradle tasks', () {
+    test('assemble release', () {
+      expect(
+        getAssembleTaskFor(const BuildInfo(BuildMode.release, null)),
+        equals('assembleRelease'),
+      );
+      expect(
+        getAssembleTaskFor(const BuildInfo(BuildMode.release, 'flavorFoo')),
+        equals('assembleFlavorFooRelease'),
+      );
+    });
+
+    test('assemble debug', () {
+      expect(
+        getAssembleTaskFor(const BuildInfo(BuildMode.debug, null)),
+        equals('assembleDebug'),
+      );
+      expect(
+        getAssembleTaskFor(const BuildInfo(BuildMode.debug, 'flavorFoo')),
+        equals('assembleFlavorFooDebug'),
+      );
+    });
+
+    test('assemble profile', () {
+      expect(
+        getAssembleTaskFor(const BuildInfo(BuildMode.profile, null)),
+        equals('assembleProfile'),
+      );
+      expect(
+        getAssembleTaskFor(const BuildInfo(BuildMode.profile, 'flavorFoo')),
+        equals('assembleFlavorFooProfile'),
+      );
+    });
+  });
+
+  group('findBundleFile', () {
+    testUsingContext('Finds app bundle when flavor contains underscores in release mode', () {
+      final FlutterProject project = generateFakeAppBundle('foo_barRelease', 'app.aab');
+      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo_bar'));
+      expect(bundle, isNotNull);
+      expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barRelease', 'app.aab'));
     }, overrides: <Type, Generator>{
       FileSystem: () => MemoryFileSystem(),
       ProcessManager: () => FakeProcessManager.any(),
     });
 
-    test('androidXFailureRegex should match lines with likely AndroidX errors', () {
-      final List<String> nonMatchingLines = <String>[
-        ':app:preBuild UP-TO-DATE',
-        'BUILD SUCCESSFUL in 0s',
-        '',
-      ];
-      final List<String> matchingLines = <String>[
-        'AAPT: error: resource android:attr/fontVariationSettings not found.',
-        'AAPT: error: resource android:attr/ttcIndex not found.',
-        'error: package android.support.annotation does not exist',
-        'import android.support.annotation.NonNull;',
-        'import androidx.annotation.NonNull;',
-        'Daemon:  AAPT2 aapt2-3.2.1-4818971-linux Daemon #0',
-      ];
-      for (String m in nonMatchingLines) {
-        expect(androidXFailureRegex.hasMatch(m), isFalse);
-      }
-      for (String m in matchingLines) {
-        expect(androidXFailureRegex.hasMatch(m), isTrue);
-      }
+    testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in release mode', () {
+      final FlutterProject project = generateFakeAppBundle('fooRelease', 'app.aab');
+      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo'));
+      expect(bundle, isNotNull);
+      expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooRelease', 'app.aab'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Finds app bundle when no flavor is used in release mode', () {
+      final FlutterProject project = generateFakeAppBundle('release', 'app.aab');
+      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, null));
+      expect(bundle, isNotNull);
+      expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'release', 'app.aab'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Finds app bundle when flavor contains underscores in debug mode', () {
+      final FlutterProject project = generateFakeAppBundle('foo_barDebug', 'app.aab');
+      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo_bar'));
+      expect(bundle, isNotNull);
+      expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barDebug', 'app.aab'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in debug mode', () {
+      final FlutterProject project = generateFakeAppBundle('fooDebug', 'app.aab');
+      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo'));
+      expect(bundle, isNotNull);
+      expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooDebug', 'app.aab'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Finds app bundle when no flavor is used in debug mode', () {
+      final FlutterProject project = generateFakeAppBundle('debug', 'app.aab');
+      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, null));
+      expect(bundle, isNotNull);
+      expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'debug', 'app.aab'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Finds app bundle when flavor contains underscores in profile mode', () {
+      final FlutterProject project = generateFakeAppBundle('foo_barProfile', 'app.aab');
+      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo_bar'));
+      expect(bundle, isNotNull);
+      expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barProfile', 'app.aab'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in profile mode', () {
+      final FlutterProject project = generateFakeAppBundle('fooProfile', 'app.aab');
+      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo'));
+      expect(bundle, isNotNull);
+      expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooProfile', 'app.aab'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Finds app bundle when no flavor is used in profile mode', () {
+      final FlutterProject project = generateFakeAppBundle('profile', 'app.aab');
+      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, null));
+      expect(bundle, isNotNull);
+      expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'profile', 'app.aab'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Finds app bundle in release mode - Gradle 3.5', () {
+      final FlutterProject project = generateFakeAppBundle('release', 'app-release.aab');
+      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, null));
+      expect(bundle, isNotNull);
+      expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'release', 'app-release.aab'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Finds app bundle in profile mode - Gradle 3.5', () {
+      final FlutterProject project = generateFakeAppBundle('profile', 'app-profile.aab');
+      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, null));
+      expect(bundle, isNotNull);
+      expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'profile', 'app-profile.aab'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Finds app bundle in debug mode - Gradle 3.5', () {
+      final FlutterProject project = generateFakeAppBundle('debug', 'app-debug.aab');
+      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, null));
+      expect(bundle, isNotNull);
+      expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'debug', 'app-debug.aab'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Finds app bundle when flavor contains underscores in release mode - Gradle 3.5', () {
+      final FlutterProject project = generateFakeAppBundle('foo_barRelease', 'app-foo_bar-release.aab');
+      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo_bar'));
+      expect(bundle, isNotNull);
+      expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barRelease', 'app-foo_bar-release.aab'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Finds app bundle when flavor contains underscores in profile mode - Gradle 3.5', () {
+      final FlutterProject project = generateFakeAppBundle('foo_barProfile', 'app-foo_bar-profile.aab');
+      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo_bar'));
+      expect(bundle, isNotNull);
+      expect(bundle.path, fs.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barProfile', 'app-foo_bar-profile.aab'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Finds app bundle when flavor contains underscores in debug mode - Gradle 3.5', () {
+      final FlutterProject project = generateFakeAppBundle('foo_barDebug', 'app-foo_bar-debug.aab');
+      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo_bar'));
+      expect(bundle, isNotNull);
+      expect(bundle.path, fs.path.join('irrelevant','app', 'outputs', 'bundle', 'foo_barDebug', 'app-foo_bar-debug.aab'));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+  });
+
+  group('findApkFiles', () {
+    testUsingContext('Finds APK without flavor in release', () {
+      final FlutterProject project = MockFlutterProject();
+      final AndroidProject androidProject = MockAndroidProject();
+
+      when(project.android).thenReturn(androidProject);
+      when(project.isModule).thenReturn(false);
+      when(androidProject.buildDirectory).thenReturn(fs.directory('irrelevant'));
+
+      final Directory apkDirectory = fs.directory(fs.path.join('irrelevant', 'app', 'outputs', 'apk', 'release'));
+      apkDirectory.createSync(recursive: true);
+      apkDirectory.childFile('app-release.apk').createSync();
+
+      final Iterable<File> apks = findApkFiles(
+        project,
+        const AndroidBuildInfo(BuildInfo(BuildMode.release, '')),
+      );
+      expect(apks.isNotEmpty, isTrue);
+      expect(apks.first.path, equals(fs.path.join('irrelevant', 'app', 'outputs', 'apk', 'release', 'app-release.apk')));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Finds APK with flavor in release mode', () {
+      final FlutterProject project = MockFlutterProject();
+      final AndroidProject androidProject = MockAndroidProject();
+
+      when(project.android).thenReturn(androidProject);
+      when(project.isModule).thenReturn(false);
+      when(androidProject.buildDirectory).thenReturn(fs.directory('irrelevant'));
+
+      final Directory apkDirectory = fs.directory(fs.path.join('irrelevant', 'app', 'outputs', 'apk', 'release'));
+      apkDirectory.createSync(recursive: true);
+      apkDirectory.childFile('app-flavor1-release.apk').createSync();
+
+      final Iterable<File> apks = findApkFiles(
+        project,
+        const AndroidBuildInfo(BuildInfo(BuildMode.release, 'flavor1')),
+      );
+      expect(apks.isNotEmpty, isTrue);
+      expect(apks.first.path, equals(fs.path.join('irrelevant', 'app', 'outputs', 'apk', 'release', 'app-flavor1-release.apk')));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Finds APK with flavor in release mode - AGP v3', () {
+      final FlutterProject project = MockFlutterProject();
+      final AndroidProject androidProject = MockAndroidProject();
+
+      when(project.android).thenReturn(androidProject);
+      when(project.isModule).thenReturn(false);
+      when(androidProject.buildDirectory).thenReturn(fs.directory('irrelevant'));
+
+      final Directory apkDirectory = fs.directory(fs.path.join('irrelevant', 'app', 'outputs', 'apk', 'flavor1', 'release'));
+      apkDirectory.createSync(recursive: true);
+      apkDirectory.childFile('app-flavor1-release.apk').createSync();
+
+      final Iterable<File> apks = findApkFiles(
+        project,
+        const AndroidBuildInfo(BuildInfo(BuildMode.release, 'flavor1')),
+      );
+      expect(apks.isNotEmpty, isTrue);
+      expect(apks.first.path, equals(fs.path.join('irrelevant', 'app', 'outputs', 'apk', 'flavor1', 'release', 'app-flavor1-release.apk')));
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+  });
+
+  group('gradle build', () {
+    testUsingContext('do not crash if there is no Android SDK', () async {
+      expect(() {
+        updateLocalProperties(project: FlutterProject.current());
+      }, throwsToolExit(
+        message: '$warningMark No Android SDK found. Try setting the ANDROID_HOME environment variable.',
+      ));
+    }, overrides: <Type, Generator>{
+      AndroidSdk: () => null,
+    });
+
+    // Regression test for https://github.com/flutter/flutter/issues/34700
+    testUsingContext('Does not return nulls in apk list', () {
+      const AndroidBuildInfo buildInfo = AndroidBuildInfo(BuildInfo.debug);
+      expect(findApkFiles(FlutterProject.current(), buildInfo), <File>[]);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
     });
 
     test('androidXPluginWarningRegex should match lines with the AndroidX plugin warnings', () {
@@ -103,355 +394,6 @@
         expect(androidXPluginWarningRegex.hasMatch(m), isTrue);
       }
     });
-
-    test('ndkMessageFilter should only match lines without the error message', () {
-      final List<String> nonMatchingLines = <String>[
-        'NDK is missing a "platforms" directory.',
-        'If you are using NDK, verify the ndk.dir is set to a valid NDK directory.  It is currently set to /usr/local/company/home/username/Android/Sdk/ndk-bundle.',
-        'If you are not using NDK, unset the NDK variable from ANDROID_NDK_HOME or local.properties to remove this warning.',
-      ];
-      final List<String> matchingLines = <String>[
-        ':app:preBuild UP-TO-DATE',
-        'BUILD SUCCESSFUL in 0s',
-        '',
-        'Something NDK related mentioning ANDROID_NDK_HOME',
-      ];
-      for (String m in nonMatchingLines) {
-        expect(ndkMessageFilter.hasMatch(m), isFalse);
-      }
-      for (String m in matchingLines) {
-        expect(ndkMessageFilter.hasMatch(m), isTrue);
-      }
-    });
-
-    testUsingContext('Finds app bundle when flavor contains underscores in release mode', () {
-      final GradleProject gradleProject = generateFakeAppBundle('foo_barRelease', 'app.aab');
-      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, 'foo_bar'));
-      expect(bundle, isNotNull);
-      expect(bundle.path, '/foo_barRelease/app.aab');
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
-    testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in release mode', () {
-      final GradleProject gradleProject = generateFakeAppBundle('fooRelease', 'app.aab');
-      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, 'foo'));
-      expect(bundle, isNotNull);
-      expect(bundle.path, '/fooRelease/app.aab');
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
-    testUsingContext('Finds app bundle when no flavor is used in release mode', () {
-      final GradleProject gradleProject = generateFakeAppBundle('release', 'app.aab');
-      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, null));
-      expect(bundle, isNotNull);
-      expect(bundle.path, '/release/app.aab');
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
-    testUsingContext('Finds app bundle when flavor contains underscores in debug mode', () {
-      final GradleProject gradleProject = generateFakeAppBundle('foo_barDebug', 'app.aab');
-      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, 'foo_bar'));
-      expect(bundle, isNotNull);
-      expect(bundle.path, '/foo_barDebug/app.aab');
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
-    testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in debug mode', () {
-      final GradleProject gradleProject = generateFakeAppBundle('fooDebug', 'app.aab');
-      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, 'foo'));
-      expect(bundle, isNotNull);
-      expect(bundle.path, '/fooDebug/app.aab');
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
-    testUsingContext('Finds app bundle when no flavor is used in debug mode', () {
-      final GradleProject gradleProject = generateFakeAppBundle('debug', 'app.aab');
-      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, null));
-      expect(bundle, isNotNull);
-      expect(bundle.path, '/debug/app.aab');
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
-    testUsingContext('Finds app bundle when flavor contains underscores in profile mode', () {
-      final GradleProject gradleProject = generateFakeAppBundle('foo_barProfile', 'app.aab');
-      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, 'foo_bar'));
-      expect(bundle, isNotNull);
-      expect(bundle.path, '/foo_barProfile/app.aab');
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
-    testUsingContext('Finds app bundle when flavor doesn\'t contain underscores in profile mode', () {
-      final GradleProject gradleProject = generateFakeAppBundle('fooProfile', 'app.aab');
-      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, 'foo'));
-      expect(bundle, isNotNull);
-      expect(bundle.path, '/fooProfile/app.aab');
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
-    testUsingContext('Finds app bundle when no flavor is used in profile mode', () {
-      final GradleProject gradleProject = generateFakeAppBundle('profile', 'app.aab');
-      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, null));
-      expect(bundle, isNotNull);
-      expect(bundle.path, '/profile/app.aab');
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
-    testUsingContext('Finds app bundle in release mode - Gradle 3.5', () {
-      final GradleProject gradleProject = generateFakeAppBundle('release', 'app-release.aab');
-      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, null));
-      expect(bundle, isNotNull);
-      expect(bundle.path, '/release/app-release.aab');
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
-    testUsingContext('Finds app bundle in profile mode - Gradle 3.5', () {
-      final GradleProject gradleProject = generateFakeAppBundle('profile', 'app-profile.aab');
-      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, null));
-      expect(bundle, isNotNull);
-      expect(bundle.path, '/profile/app-profile.aab');
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
-    testUsingContext('Finds app bundle in debug mode - Gradle 3.5', () {
-      final GradleProject gradleProject = generateFakeAppBundle('debug', 'app-debug.aab');
-      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, null));
-      expect(bundle, isNotNull);
-      expect(bundle.path, '/debug/app-debug.aab');
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
-    testUsingContext('Finds app bundle when flavor contains underscores in release mode - Gradle 3.5', () {
-      final GradleProject gradleProject = generateFakeAppBundle('foo_barRelease', 'app-foo_bar-release.aab');
-      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.release, 'foo_bar'));
-      expect(bundle, isNotNull);
-      expect(bundle.path, '/foo_barRelease/app-foo_bar-release.aab');
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
-    testUsingContext('Finds app bundle when flavor contains underscores in profile mode - Gradle 3.5', () {
-      final GradleProject gradleProject = generateFakeAppBundle('foo_barProfile', 'app-foo_bar-profile.aab');
-      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.profile, 'foo_bar'));
-      expect(bundle, isNotNull);
-      expect(bundle.path, '/foo_barProfile/app-foo_bar-profile.aab');
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
-    testUsingContext('Finds app bundle when flavor contains underscores in debug mode - Gradle 3.5', () {
-      final GradleProject gradleProject = generateFakeAppBundle('foo_barDebug', 'app-foo_bar-debug.aab');
-      final File bundle = findBundleFile(gradleProject, const BuildInfo(BuildMode.debug, 'foo_bar'));
-      expect(bundle, isNotNull);
-      expect(bundle.path, '/foo_barDebug/app-foo_bar-debug.aab');
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-  });
-
-  group('gradle project', () {
-    GradleProject projectFrom(String properties, String tasks) => GradleProject.fromAppProperties(properties, tasks);
-
-    test('should extract build directory from app properties', () {
-      final GradleProject project = projectFrom('''
-someProperty: someValue
-buildDir: /Users/some/apps/hello/build/app
-someOtherProperty: someOtherValue
-      ''', '');
-      expect(
-        fs.path.normalize(project.apkDirectory.path),
-        fs.path.normalize('/Users/some/apps/hello/build/app/outputs/apk'),
-      );
-    });
-    test('should extract default build variants from app properties', () {
-      final GradleProject project = projectFrom('buildDir: /Users/some/apps/hello/build/app', '''
-someTask
-assemble
-assembleAndroidTest
-assembleDebug
-assembleProfile
-assembleRelease
-someOtherTask
-      ''');
-      expect(project.buildTypes, <String>['debug', 'profile', 'release']);
-      expect(project.productFlavors, isEmpty);
-    });
-    test('should extract custom build variants from app properties', () {
-      final GradleProject project = projectFrom('buildDir: /Users/some/apps/hello/build/app', '''
-someTask
-assemble
-assembleAndroidTest
-assembleDebug
-assembleFree
-assembleFreeAndroidTest
-assembleFreeDebug
-assembleFreeProfile
-assembleFreeRelease
-assemblePaid
-assemblePaidAndroidTest
-assemblePaidDebug
-assemblePaidProfile
-assemblePaidRelease
-assembleProfile
-assembleRelease
-someOtherTask
-      ''');
-      expect(project.buildTypes, <String>['debug', 'profile', 'release']);
-      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>[], '/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'], '/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>[], '/some/dir');
-      expect(project.apkFilesFor(
-        const AndroidBuildInfo(
-          BuildInfo.debug,
-          splitPerAbi: true,
-          targetArchs: <AndroidArch>[
-            AndroidArch.armeabi_v7a,
-            AndroidArch.arm64_v8a,
-          ],
-        )),
-        <String>[
-          'app-armeabi-v7a-debug.apk',
-          'app-arm64-v8a-debug.apk',
-        ]);
-
-      expect(project.apkFilesFor(
-        const AndroidBuildInfo(
-          BuildInfo.release,
-          splitPerAbi: true,
-          targetArchs: <AndroidArch>[
-            AndroidArch.armeabi_v7a,
-            AndroidArch.arm64_v8a,
-          ],
-        )),
-        <String>[
-          'app-armeabi-v7a-release.apk',
-          'app-arm64-v8a-release.apk',
-        ]);
-
-      expect(project.apkFilesFor(
-        const AndroidBuildInfo(
-          BuildInfo(BuildMode.release, 'unknown'),
-          splitPerAbi: true,
-          targetArchs: <AndroidArch>[
-            AndroidArch.armeabi_v7a,
-            AndroidArch.arm64_v8a,
-          ],
-        )).isEmpty, isTrue);
-    });
-    test('should provide apks for each ABI and flavored build types', () {
-      final GradleProject project = GradleProject(<String>['debug', 'profile', 'release'], <String>['free', 'paid'], '/some/dir');
-      expect(project.apkFilesFor(
-        const AndroidBuildInfo(
-          BuildInfo(BuildMode.debug, 'free'),
-          splitPerAbi: true,
-          targetArchs: <AndroidArch>[
-            AndroidArch.armeabi_v7a,
-            AndroidArch.arm64_v8a,
-          ],
-        )),
-        <String>[
-          'app-free-armeabi-v7a-debug.apk',
-          'app-free-arm64-v8a-debug.apk',
-        ]);
-
-      expect(project.apkFilesFor(
-        const AndroidBuildInfo(
-          BuildInfo(BuildMode.release, 'paid'),
-          splitPerAbi: true,
-          targetArchs: <AndroidArch>[
-            AndroidArch.armeabi_v7a,
-            AndroidArch.arm64_v8a,
-          ],
-        )),
-        <String>[
-          'app-paid-armeabi-v7a-release.apk',
-          'app-paid-arm64-v8a-release.apk',
-        ]);
-
-      expect(project.apkFilesFor(
-        const AndroidBuildInfo(
-          BuildInfo(BuildMode.release, 'unknown'),
-          splitPerAbi: true,
-          targetArchs: <AndroidArch>[
-            AndroidArch.armeabi_v7a,
-            AndroidArch.arm64_v8a,
-          ],
-        )).isEmpty, isTrue);
-    });
-    test('should provide assemble task name for default build types', () {
-      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'], '/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'], '/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>[], '/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'], '/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'], '/some/dir');
-      expect(project.bundleTaskFor(const BuildInfo(BuildMode.debug, 'randomFlavor')), 'bundleRandomFlavorDebug');
-    });
   });
 
   group('Config files', () {
@@ -461,7 +403,6 @@
     setUp(() {
       mockLogger = BufferLogger();
       tempDir = fs.systemTempDirectory.createTempSync('flutter_settings_aar_test.');
-
     });
 
     testUsingContext('create settings_aar.gradle when current settings.gradle loads plugins', () {
@@ -547,48 +488,6 @@
     });
   });
 
-  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;
@@ -861,477 +760,6 @@
     });
   });
 
-  group('Gradle failures', () {
-    MemoryFileSystem fs;
-    Directory tempDir;
-    Directory gradleWrapperDirectory;
-    MockProcessManager mockProcessManager;
-    String gradleBinary;
-
-    setUp(() {
-      fs = MemoryFileSystem();
-      tempDir = fs.systemTempDirectory.createTempSync('flutter_artifacts_test.');
-      gradleBinary = platform.isWindows ? 'gradlew.bat' : 'gradlew';
-      gradleWrapperDirectory = fs.directory(
-        fs.path.join(tempDir.path, 'bin', 'cache', 'artifacts', 'gradle_wrapper'));
-      gradleWrapperDirectory.createSync(recursive: true);
-      gradleWrapperDirectory
-        .childFile(gradleBinary)
-        .writeAsStringSync('irrelevant');
-      fs.currentDirectory
-        .childDirectory('android')
-        .createSync();
-      fs.currentDirectory
-        .childDirectory('android')
-        .childFile('gradle.properties')
-        .writeAsStringSync('irrelevant');
-      gradleWrapperDirectory
-        .childDirectory('gradle')
-        .childDirectory('wrapper')
-        .createSync(recursive: true);
-      gradleWrapperDirectory
-        .childDirectory('gradle')
-        .childDirectory('wrapper')
-        .childFile('gradle-wrapper.jar')
-        .writeAsStringSync('irrelevant');
-
-      mockProcessManager = MockProcessManager();
-    });
-
-    testUsingContext('throws toolExit if gradle fails while downloading', () async {
-      final List<String> cmd = <String>[
-        fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
-        '-v',
-      ];
-      const String errorMessage = '''
-Exception in thread "main" java.io.FileNotFoundException: https://downloads.gradle.org/distributions/gradle-4.1.1-all.zip
-at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1872)
-at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1474)
-at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254)
-at org.gradle.wrapper.Download.downloadInternal(Download.java:58)
-at org.gradle.wrapper.Download.download(Download.java:44)
-at org.gradle.wrapper.Install\$1.call(Install.java:61)
-at org.gradle.wrapper.Install\$1.call(Install.java:48)
-at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:65)
-at org.gradle.wrapper.Install.createDist(Install.java:48)
-at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128)
-at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
-      final ProcessException exception = ProcessException(
-        gradleBinary,
-        <String>['-v'],
-        errorMessage,
-        1,
-      );
-      when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment')))
-        .thenThrow(exception);
-      await expectLater(() async {
-        await checkGradleDependencies();
-      }, throwsToolExit(message: errorMessage));
-    }, overrides: <Type, Generator>{
-      Cache: () => Cache(rootOverride: tempDir),
-      FileSystem: () => fs,
-      ProcessManager: () => mockProcessManager,
-    });
-
-    testUsingContext('throw toolExit if gradle fails downloading with proxy error', () async {
-      final List<String> cmd = <String>[
-        fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
-        '-v',
-      ];
-      const String errorMessage = '''
-Exception in thread "main" java.io.IOException: Unable to tunnel through proxy. Proxy returns "HTTP/1.1 400 Bad Request"
-at sun.net.www.protocol.http.HttpURLConnection.doTunneling(HttpURLConnection.java:2124)
-at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:183)
-at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1546)
-at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1474)
-at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:254)
-at org.gradle.wrapper.Download.downloadInternal(Download.java:58)
-at org.gradle.wrapper.Download.download(Download.java:44)
-at org.gradle.wrapper.Install\$1.call(Install.java:61)
-at org.gradle.wrapper.Install\$1.call(Install.java:48)
-at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:65)
-at org.gradle.wrapper.Install.createDist(Install.java:48)
-at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128)
-at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
-      final ProcessException exception = ProcessException(
-        gradleBinary,
-        <String>['-v'],
-        errorMessage,
-        1,
-      );
-      when(mockProcessManager.run(cmd, environment: anyNamed('environment'), workingDirectory: null))
-        .thenThrow(exception);
-      await expectLater(() async {
-        await checkGradleDependencies();
-      }, throwsToolExit(message: errorMessage));
-    }, overrides: <Type, Generator>{
-      Cache: () => Cache(rootOverride: tempDir),
-      FileSystem: () => fs,
-      ProcessManager: () => mockProcessManager,
-    });
-
-    testUsingContext('throws toolExit if gradle is missing execute permissions. ', () async {
-      final List<String> cmd = <String>[
-        fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
-        '-v',
-      ];
-      final ProcessException exception = ProcessException(
-        gradleBinary,
-        <String>['-v'],
-        'Permission denied\nCommand: /home/android/gradlew -v',
-        1,
-      );
-      when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment')))
-        .thenThrow(exception);
-      await expectLater(() async {
-        await checkGradleDependencies();
-      }, throwsToolExit(message: 'does not have permission to execute by your user'));
-    }, overrides: <Type, Generator>{
-      Cache: () => Cache(rootOverride: tempDir),
-      FileSystem: () => fs,
-      ProcessManager: () => mockProcessManager,
-    });
-
-    testUsingContext('throws toolExit if gradle times out waiting for exclusive access to zip', () async {
-      final List<String> cmd = <String>[
-        fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
-        '-v',
-      ];
-      const String errorMessage = '''
-Exception in thread "main" java.lang.RuntimeException: Timeout of 120000 reached waiting for exclusive access to file: /User/documents/gradle-5.6.2-all.zip
-	at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:61)
-	at org.gradle.wrapper.Install.createDist(Install.java:48)
-	at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128)
-	at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
-      final ProcessException exception = ProcessException(
-        gradleBinary,
-        <String>['-v'],
-        errorMessage,
-        1,
-      );
-      when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment')))
-        .thenThrow(exception);
-      await expectLater(() async {
-        await checkGradleDependencies();
-      }, throwsToolExit(message: errorMessage));
-    }, overrides: <Type, Generator>{
-      Cache: () => Cache(rootOverride: tempDir),
-      FileSystem: () => fs,
-      ProcessManager: () => mockProcessManager,
-    });
-
-    testUsingContext('throws toolExit if gradle fails to unzip file', () async {
-      final List<String> cmd = <String>[
-        fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
-        '-v',
-      ];
-      const String errorMessage = '''
-Exception in thread "main" java.util.zip.ZipException: error in opening zip file /User/documents/gradle-5.6.2-all.zip
-	at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:61)
-	at org.gradle.wrapper.Install.createDist(Install.java:48)
-	at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128)
-	at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
-      final ProcessException exception = ProcessException(
-        gradleBinary,
-        <String>['-v'],
-        errorMessage,
-        1,
-      );
-      when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment')))
-        .thenThrow(exception);
-      await expectLater(() async {
-        await checkGradleDependencies();
-      }, throwsToolExit(message: errorMessage));
-    }, overrides: <Type, Generator>{
-      Cache: () => Cache(rootOverride: tempDir),
-      FileSystem: () => fs,
-      ProcessManager: () => mockProcessManager,
-    });
-
-    testUsingContext('throws toolExit if remote host closes connection', () async {
-      final List<String> cmd = <String>[
-        fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
-        '-v',
-      ];
-      const String errorMessage = '''
-Downloading https://services.gradle.org/distributions/gradle-5.6.2-all.zip
-
-
-Exception in thread "main" javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake
-	at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:994)
-	at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367)
-	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395)
-	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379)
-	at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
-	at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
-	at sun.net.www.protocol.http.HttpURLConnection.followRedirect0(HttpURLConnection.java:2729)
-	at sun.net.www.protocol.http.HttpURLConnection.followRedirect(HttpURLConnection.java:2641)
-	at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1824)
-	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1492)
-	at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:263)
-	at org.gradle.wrapper.Download.downloadInternal(Download.java:58)
-	at org.gradle.wrapper.Download.download(Download.java:44)
-	at org.gradle.wrapper.Install\$1.call(Install.java:61)
-	at org.gradle.wrapper.Install\$1.call(Install.java:48)
-	at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:65)
-	at org.gradle.wrapper.Install.createDist(Install.java:48)
-	at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128)
-	at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
-      final ProcessException exception = ProcessException(
-        gradleBinary,
-        <String>['-v'],
-        errorMessage,
-        1,
-      );
-      when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment')))
-        .thenThrow(exception);
-      await expectLater(() async {
-        await checkGradleDependencies();
-      }, throwsToolExit(message: errorMessage));
-    }, overrides: <Type, Generator>{
-      Cache: () => Cache(rootOverride: tempDir),
-      FileSystem: () => fs,
-      ProcessManager: () => mockProcessManager,
-    });
-
-    testUsingContext('throws toolExit if file opening fails', () async {
-      final List<String> cmd = <String>[
-        fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
-        '-v',
-      ];
-      const String errorMessage = r'''
-Downloading https://services.gradle.org/distributions/gradle-3.5.0-all.zip
-
-Exception in thread "main" java.io.FileNotFoundException: https://downloads.gradle-dn.com/distributions/gradle-3.5.0-all.zip
-	at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1890)
-	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1492)
-	at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:263)
-	at org.gradle.wrapper.Download.downloadInternal(Download.java:58)
-	at org.gradle.wrapper.Download.download(Download.java:44)
-	at org.gradle.wrapper.Install$1.call(Install.java:61)
-	at org.gradle.wrapper.Install$1.call(Install.java:48)
-	at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:65)
-	at org.gradle.wrapper.Install.createDist(Install.java:48)
-	at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128)
-	at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
-      final ProcessException exception = ProcessException(
-        gradleBinary,
-        <String>['-v'],
-        errorMessage,
-        1,
-      );
-      when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment')))
-        .thenThrow(exception);
-      await expectLater(() async {
-        await checkGradleDependencies();
-      }, throwsToolExit(message: errorMessage));
-    }, overrides: <Type, Generator>{
-      Cache: () => Cache(rootOverride: tempDir),
-      FileSystem: () => fs,
-      ProcessManager: () => mockProcessManager,
-    });
-
-    testUsingContext('throws toolExit if the connection is reset', () async {
-      final List<String> cmd = <String>[
-        fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
-        '-v',
-      ];
-      const String errorMessage = '''
-Downloading https://services.gradle.org/distributions/gradle-5.6.2-all.zip
-
-
-Exception in thread "main" java.net.SocketException: Connection reset
-	at java.net.SocketInputStream.read(SocketInputStream.java:210)
-	at java.net.SocketInputStream.read(SocketInputStream.java:141)
-	at sun.security.ssl.InputRecord.readFully(InputRecord.java:465)
-	at sun.security.ssl.InputRecord.readV3Record(InputRecord.java:593)
-	at sun.security.ssl.InputRecord.read(InputRecord.java:532)
-	at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:975)
-	at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1367)
-	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1395)
-	at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1379)
-	at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
-	at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
-	at sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1564)
-	at sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1492)
-	at sun.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:263)
-	at org.gradle.wrapper.Download.downloadInternal(Download.java:58)
-	at org.gradle.wrapper.Download.download(Download.java:44)
-	at org.gradle.wrapper.Install\$1.call(Install.java:61)
-	at org.gradle.wrapper.Install\$1.call(Install.java:48)
-	at org.gradle.wrapper.ExclusiveFileAccessManager.access(ExclusiveFileAccessManager.java:65)
-	at org.gradle.wrapper.Install.createDist(Install.java:48)
-	at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:128)
-	at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
-      final ProcessException exception = ProcessException(
-        gradleBinary,
-        <String>['-v'],
-        errorMessage,
-        1,
-      );
-      when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment')))
-        .thenThrow(exception);
-      await expectLater(() async {
-        await checkGradleDependencies();
-      }, throwsToolExit(message: errorMessage));
-    }, overrides: <Type, Generator>{
-      Cache: () => Cache(rootOverride: tempDir),
-      FileSystem: () => fs,
-      ProcessManager: () => mockProcessManager,
-    });
-
-     testUsingContext('throws toolExit if gradle exits abnormally', () async {
-      final List<String> cmd = <String>[
-        fs.path.join(fs.currentDirectory.path, 'android', gradleBinary),
-        '-v',
-      ];
-      const String errorMessage = '''
-ProcessException: Process exited abnormally:
-Exception in thread "main" java.lang.NullPointerException
-	at org.gradle.wrapper.BootstrapMainStarter.findLauncherJar(BootstrapMainStarter.java:34)
-	at org.gradle.wrapper.BootstrapMainStarter.start(BootstrapMainStarter.java:25)
-	at org.gradle.wrapper.WrapperExecutor.execute(WrapperExecutor.java:129)
-	at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)''';
-      final ProcessException exception = ProcessException(
-        gradleBinary,
-        <String>['-v'],
-        errorMessage,
-        1,
-      );
-      when(mockProcessManager.run(cmd, workingDirectory: anyNamed('workingDirectory'), environment: anyNamed('environment')))
-        .thenThrow(exception);
-      await expectLater(() async {
-        await checkGradleDependencies();
-      }, throwsToolExit(message: errorMessage));
-    }, overrides: <Type, Generator>{
-      Cache: () => Cache(rootOverride: tempDir),
-      FileSystem: () => fs,
-      ProcessManager: () => mockProcessManager,
-    });
-  });
-
-  group('injectGradleWrapperIfNeeded', () {
-    MemoryFileSystem memoryFileSystem;
-    Directory tempDir;
-    Directory gradleWrapperDirectory;
-
-    setUp(() {
-      memoryFileSystem = MemoryFileSystem();
-      tempDir = memoryFileSystem.systemTempDirectory.createTempSync('flutter_artifacts_test.');
-      gradleWrapperDirectory = memoryFileSystem.directory(
-          memoryFileSystem.path.join(tempDir.path, 'bin', 'cache', 'artifacts', 'gradle_wrapper'));
-      gradleWrapperDirectory.createSync(recursive: true);
-      gradleWrapperDirectory
-        .childFile('gradlew')
-        .writeAsStringSync('irrelevant');
-      gradleWrapperDirectory
-        .childDirectory('gradle')
-        .childDirectory('wrapper')
-        .createSync(recursive: true);
-      gradleWrapperDirectory
-        .childDirectory('gradle')
-        .childDirectory('wrapper')
-        .childFile('gradle-wrapper.jar')
-        .writeAsStringSync('irrelevant');
-    });
-
-    testUsingContext('Inject the wrapper when all files are missing', () {
-      final Directory sampleAppAndroid = fs.directory('/sample-app/android');
-      sampleAppAndroid.createSync(recursive: true);
-
-      injectGradleWrapperIfNeeded(sampleAppAndroid);
-
-      expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue);
-
-      expect(sampleAppAndroid
-        .childDirectory('gradle')
-        .childDirectory('wrapper')
-        .childFile('gradle-wrapper.jar')
-        .existsSync(), isTrue);
-
-      expect(sampleAppAndroid
-        .childDirectory('gradle')
-        .childDirectory('wrapper')
-        .childFile('gradle-wrapper.properties')
-        .existsSync(), isTrue);
-
-      expect(sampleAppAndroid
-        .childDirectory('gradle')
-        .childDirectory('wrapper')
-        .childFile('gradle-wrapper.properties')
-        .readAsStringSync(),
-            'distributionBase=GRADLE_USER_HOME\n'
-            'distributionPath=wrapper/dists\n'
-            'zipStoreBase=GRADLE_USER_HOME\n'
-            'zipStorePath=wrapper/dists\n'
-            'distributionUrl=https\\://services.gradle.org/distributions/gradle-5.6.2-all.zip\n');
-    }, overrides: <Type, Generator>{
-      Cache: () => Cache(rootOverride: tempDir),
-      FileSystem: () => memoryFileSystem,
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
-    testUsingContext('Inject the wrapper when some files are missing', () {
-      final Directory sampleAppAndroid = fs.directory('/sample-app/android');
-      sampleAppAndroid.createSync(recursive: true);
-
-      // There's an existing gradlew
-      sampleAppAndroid.childFile('gradlew').writeAsStringSync('existing gradlew');
-
-      injectGradleWrapperIfNeeded(sampleAppAndroid);
-
-      expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue);
-      expect(sampleAppAndroid.childFile('gradlew').readAsStringSync(),
-          equals('existing gradlew'));
-
-      expect(sampleAppAndroid
-        .childDirectory('gradle')
-        .childDirectory('wrapper')
-        .childFile('gradle-wrapper.jar')
-        .existsSync(), isTrue);
-
-      expect(sampleAppAndroid
-        .childDirectory('gradle')
-        .childDirectory('wrapper')
-        .childFile('gradle-wrapper.properties')
-        .existsSync(), isTrue);
-
-      expect(sampleAppAndroid
-        .childDirectory('gradle')
-        .childDirectory('wrapper')
-        .childFile('gradle-wrapper.properties')
-        .readAsStringSync(),
-            'distributionBase=GRADLE_USER_HOME\n'
-            'distributionPath=wrapper/dists\n'
-            'zipStoreBase=GRADLE_USER_HOME\n'
-            'zipStorePath=wrapper/dists\n'
-            'distributionUrl=https\\://services.gradle.org/distributions/gradle-5.6.2-all.zip\n');
-    }, overrides: <Type, Generator>{
-      Cache: () => Cache(rootOverride: tempDir),
-      FileSystem: () => memoryFileSystem,
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
-    testUsingContext('Gives executable permission to gradle', () {
-      final Directory sampleAppAndroid = fs.directory('/sample-app/android');
-      sampleAppAndroid.createSync(recursive: true);
-
-      // Make gradlew in the wrapper executable.
-      os.makeExecutable(gradleWrapperDirectory.childFile('gradlew'));
-
-      injectGradleWrapperIfNeeded(sampleAppAndroid);
-
-      final File gradlew = sampleAppAndroid.childFile('gradlew');
-      expect(gradlew.existsSync(), isTrue);
-      expect(gradlew.statSync().modeString().contains('x'), isTrue);
-    }, overrides: <Type, Generator>{
-      Cache: () => Cache(rootOverride: tempDir),
-      FileSystem: () => memoryFileSystem,
-      ProcessManager: () => FakeProcessManager.any(),
-      OperatingSystemUtils: () => OperatingSystemUtils(),
-    });
-  });
-
   group('migrateToR8', () {
     MemoryFileSystem memoryFileSystem;
 
@@ -1532,7 +960,8 @@
 plugin1=${plugin1.path}
 plugin2=${plugin2.path}
 ''');
-      final Directory buildDirectory = androidDirectory.childDirectory('build');
+      final Directory buildDirectory = androidDirectory
+        .childDirectory('build');
       buildDirectory
         .childDirectory('outputs')
         .childDirectory('repo')
@@ -1541,7 +970,7 @@
       await buildPluginsAsAar(
         FlutterProject.fromPath(androidDirectory.path),
         const AndroidBuildInfo(BuildInfo.release),
-        buildDirectory: buildDirectory.path,
+        buildDirectory: buildDirectory,
       );
 
       final String flutterRoot = fs.path.absolute(Cache.flutterRoot);
@@ -1584,6 +1013,8 @@
   });
 
   group('gradle build', () {
+    final Usage mockUsage = MockUsage();
+
     MockAndroidSdk mockAndroidSdk;
     MockAndroidStudio mockAndroidStudio;
     MockLocalEngineArtifacts mockArtifacts;
@@ -1623,6 +1054,399 @@
         .writeAsStringSync('irrelevant');
     });
 
+    testUsingContext('recognizes common errors - tool exit', () async {
+      final Process process = createMockProcess(
+        exitCode: 1,
+        stdout: 'irrelevant\nSome gradle message\nirrelevant',
+      );
+      when(mockProcessManager.start(any,
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment')))
+      .thenAnswer((_) => Future<Process>.value(process));
+
+      fs.directory('android')
+        .childFile('build.gradle')
+        .createSync(recursive: true);
+
+      fs.directory('android')
+        .childFile('gradle.properties')
+        .createSync(recursive: true);
+
+      fs.directory('android')
+        .childDirectory('app')
+        .childFile('build.gradle')
+        ..createSync(recursive: true)
+        ..writeAsStringSync('apply from: irrelevant/flutter.gradle');
+
+      bool handlerCalled = false;
+      await expectLater(() async {
+       await buildGradleApp(
+          project: FlutterProject.current(),
+          androidBuildInfo: const AndroidBuildInfo(
+            BuildInfo(
+              BuildMode.release,
+              null,
+            ),
+          ),
+          target: 'lib/main.dart',
+          isBuildingBundle: false,
+          localGradleErrors: <GradleHandledError>[
+            GradleHandledError(
+              test: (String line) {
+                return line.contains('Some gradle message');
+              },
+              handler: ({
+                String line,
+                FlutterProject project,
+                bool usesAndroidX,
+                bool shouldBuildPluginAsAar,
+              }) async {
+                handlerCalled = true;
+                return GradleBuildStatus.exit;
+              },
+              eventLabel: 'random-event-label',
+            ),
+          ],
+        );
+      },
+      throwsToolExit(
+        message: 'Gradle task assembleRelease failed with exit code 1'
+      ));
+
+      expect(handlerCalled, isTrue);
+
+      verify(mockUsage.sendEvent(
+        any,
+        any,
+        label: 'gradle--random-event-label-failure',
+        parameters: anyNamed('parameters'),
+      )).called(1);
+
+    }, overrides: <Type, Generator>{
+      AndroidSdk: () => mockAndroidSdk,
+      Cache: () => cache,
+      Platform: () => android,
+      FileSystem: () => fs,
+      ProcessManager: () => mockProcessManager,
+      Usage: () => mockUsage,
+    });
+
+    testUsingContext('recognizes common errors - retry build', () async {
+      when(mockProcessManager.start(any,
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment')))
+      .thenAnswer((_) {
+        final Process process = createMockProcess(
+          exitCode: 1,
+          stdout: 'irrelevant\nSome gradle message\nirrelevant',
+        );
+        return Future<Process>.value(process);
+      });
+
+      fs.directory('android')
+        .childFile('build.gradle')
+        .createSync(recursive: true);
+
+      fs.directory('android')
+        .childFile('gradle.properties')
+        .createSync(recursive: true);
+
+      fs.directory('android')
+        .childDirectory('app')
+        .childFile('build.gradle')
+        ..createSync(recursive: true)
+        ..writeAsStringSync('apply from: irrelevant/flutter.gradle');
+
+      int testFnCalled = 0;
+      await expectLater(() async {
+       await buildGradleApp(
+          project: FlutterProject.current(),
+          androidBuildInfo: const AndroidBuildInfo(
+            BuildInfo(
+              BuildMode.release,
+              null,
+            ),
+          ),
+          target: 'lib/main.dart',
+          isBuildingBundle: false,
+          localGradleErrors: <GradleHandledError>[
+            GradleHandledError(
+              test: (String line) {
+                if (line.contains('Some gradle message')) {
+                  testFnCalled++;
+                  return true;
+                }
+                return false;
+              },
+              handler: ({
+                String line,
+                FlutterProject project,
+                bool usesAndroidX,
+                bool shouldBuildPluginAsAar,
+              }) async {
+                return GradleBuildStatus.retry;
+              },
+              eventLabel: 'random-event-label',
+            ),
+          ],
+        );
+      }, throwsToolExit(
+        message: 'Gradle task assembleRelease failed with exit code 1'
+      ));
+
+      expect(testFnCalled, equals(2));
+
+      verify(mockUsage.sendEvent(
+        any,
+        any,
+        label: 'gradle--random-event-label-failure',
+        parameters: anyNamed('parameters'),
+      )).called(1);
+
+    }, overrides: <Type, Generator>{
+      AndroidSdk: () => mockAndroidSdk,
+      Cache: () => cache,
+      Platform: () => android,
+      FileSystem: () => fs,
+      ProcessManager: () => mockProcessManager,
+      Usage: () => mockUsage,
+    });
+
+    testUsingContext('logs success event after a sucessful retry', () async {
+      int testFnCalled = 0;
+      when(mockProcessManager.start(any,
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment')))
+      .thenAnswer((_) {
+        Process process;
+        if (testFnCalled == 0) {
+          process = createMockProcess(
+            exitCode: 1,
+            stdout: 'irrelevant\nSome gradle message\nirrelevant',
+          );
+        } else {
+          process = createMockProcess(
+            exitCode: 0,
+            stdout: 'irrelevant',
+          );
+        }
+        testFnCalled++;
+        return Future<Process>.value(process);
+      });
+
+      fs.directory('android')
+        .childFile('build.gradle')
+        .createSync(recursive: true);
+
+      fs.directory('android')
+        .childFile('gradle.properties')
+        .createSync(recursive: true);
+
+      fs.directory('android')
+        .childDirectory('app')
+        .childFile('build.gradle')
+        ..createSync(recursive: true)
+        ..writeAsStringSync('apply from: irrelevant/flutter.gradle');
+
+      fs.directory('build')
+        .childDirectory('app')
+        .childDirectory('outputs')
+        .childDirectory('apk')
+        .childDirectory('release')
+        .childFile('app-release.apk')
+        ..createSync(recursive: true);
+
+      await buildGradleApp(
+        project: FlutterProject.current(),
+        androidBuildInfo: const AndroidBuildInfo(
+          BuildInfo(
+            BuildMode.release,
+            null,
+          ),
+        ),
+        target: 'lib/main.dart',
+        isBuildingBundle: false,
+        localGradleErrors: <GradleHandledError>[
+          GradleHandledError(
+            test: (String line) {
+              return line.contains('Some gradle message');
+            },
+            handler: ({
+              String line,
+              FlutterProject project,
+              bool usesAndroidX,
+              bool shouldBuildPluginAsAar,
+            }) async {
+              return GradleBuildStatus.retry;
+            },
+            eventLabel: 'random-event-label',
+          ),
+        ],
+      );
+
+      verify(mockUsage.sendEvent(
+        any,
+        any,
+        label: 'gradle--random-event-label-success',
+        parameters: anyNamed('parameters'),
+      )).called(1);
+
+    }, overrides: <Type, Generator>{
+      AndroidSdk: () => mockAndroidSdk,
+      Cache: () => cache,
+      FileSystem: () => fs,
+      Platform: () => android,
+      ProcessManager: () => mockProcessManager,
+      Usage: () => mockUsage,
+    });
+
+    testUsingContext('recognizes common errors - retry build with AAR plugins', () async {
+      when(mockProcessManager.start(any,
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment')))
+      .thenAnswer((_) {
+        final Process process = createMockProcess(
+          exitCode: 1,
+          stdout: 'irrelevant\nSome gradle message\nirrelevant',
+        );
+        return Future<Process>.value(process);
+      });
+
+      fs.directory('android')
+        .childFile('build.gradle')
+        .createSync(recursive: true);
+
+      fs.directory('android')
+        .childFile('gradle.properties')
+        .createSync(recursive: true);
+
+      fs.directory('android')
+        .childDirectory('app')
+        .childFile('build.gradle')
+        ..createSync(recursive: true)
+        ..writeAsStringSync('apply from: irrelevant/flutter.gradle');
+
+      int testFnCalled = 0;
+      bool builtPluginAsAar = false;
+      await expectLater(() async {
+       await buildGradleApp(
+          project: FlutterProject.current(),
+          androidBuildInfo: const AndroidBuildInfo(
+            BuildInfo(
+              BuildMode.release,
+              null,
+            ),
+          ),
+          target: 'lib/main.dart',
+          isBuildingBundle: false,
+          localGradleErrors: <GradleHandledError>[
+            GradleHandledError(
+              test: (String line) {
+                if (line.contains('Some gradle message')) {
+                  testFnCalled++;
+                  return true;
+                }
+                return false;
+              },
+              handler: ({
+                String line,
+                FlutterProject project,
+                bool usesAndroidX,
+                bool shouldBuildPluginAsAar,
+              }) async {
+                if (testFnCalled == 2) {
+                  builtPluginAsAar = shouldBuildPluginAsAar;
+                }
+                return GradleBuildStatus.retryWithAarPlugins;
+              },
+              eventLabel: 'random-event-label',
+            ),
+          ],
+        );
+      }, throwsToolExit(
+        message: 'Gradle task assembleRelease failed with exit code 1'
+      ));
+
+      expect(testFnCalled, equals(2));
+      expect(builtPluginAsAar, isTrue);
+
+      verify(mockUsage.sendEvent(
+        any,
+        any,
+        label: 'gradle--random-event-label-failure',
+        parameters: anyNamed('parameters'),
+      )).called(1);
+
+    }, overrides: <Type, Generator>{
+      AndroidSdk: () => mockAndroidSdk,
+      Cache: () => cache,
+      Platform: () => android,
+      FileSystem: () => fs,
+      ProcessManager: () => mockProcessManager,
+      Usage: () => mockUsage,
+    });
+
+    testUsingContext('indicates that an APK has been built successfully', () async {
+      when(mockProcessManager.start(any,
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment')))
+      .thenAnswer((_) {
+        return Future<Process>.value(
+          createMockProcess(
+            exitCode: 0,
+            stdout: '',
+          ));
+      });
+
+      fs.directory('android')
+        .childFile('build.gradle')
+        .createSync(recursive: true);
+
+      fs.directory('android')
+        .childFile('gradle.properties')
+        .createSync(recursive: true);
+
+      fs.directory('android')
+        .childDirectory('app')
+        .childFile('build.gradle')
+        ..createSync(recursive: true)
+        ..writeAsStringSync('apply from: irrelevant/flutter.gradle');
+
+      fs.directory('build')
+        .childDirectory('app')
+        .childDirectory('outputs')
+        .childDirectory('apk')
+        .childDirectory('release')
+        .childFile('app-release.apk')
+        ..createSync(recursive: true);
+
+      await buildGradleApp(
+        project: FlutterProject.current(),
+        androidBuildInfo: const AndroidBuildInfo(
+          BuildInfo(
+            BuildMode.release,
+            null,
+          ),
+        ),
+        target: 'lib/main.dart',
+        isBuildingBundle: false,
+        localGradleErrors: <GradleHandledError>[],
+      );
+
+      final BufferLogger logger = context.get<Logger>();
+      expect(
+        logger.statusText,
+        contains('Built build/app/outputs/apk/release/app-release.apk (0.0MB)'),
+      );
+
+    }, overrides: <Type, Generator>{
+      AndroidSdk: () => mockAndroidSdk,
+      Cache: () => cache,
+      FileSystem: () => fs,
+      Platform: () => android,
+      ProcessManager: () => mockProcessManager,
+    });
+
     testUsingContext('build aar uses selected local engine', () async {
       when(mockArtifacts.getArtifactPath(Artifact.flutterFramework,
           platform: TargetPlatform.android_arm, mode: anyNamed('mode'))).thenReturn('engine');
@@ -1650,6 +1474,9 @@
       fs.file('path/to/project/.android/gradle.properties')
         .writeAsStringSync('irrelevant');
 
+      fs.file('path/to/project/.android/build.gradle')
+        .createSync(recursive: true);
+
       when(mockProcessManager.run(
           <String> ['/path/to/project/.android/gradlew', '-v'],
           workingDirectory: anyNamed('workingDirectory'),
@@ -1675,7 +1502,7 @@
       await buildGradleAar(
         androidBuildInfo: const AndroidBuildInfo(BuildInfo(BuildMode.release, null)),
         project: FlutterProject.current(),
-        outputDir: 'build/',
+        outputDir: fs.directory('build/'),
         target: '',
       );
 
@@ -1700,14 +1527,24 @@
 }
 
 /// Generates a fake app bundle at the location [directoryName]/[fileName].
-GradleProject generateFakeAppBundle(String directoryName, String fileName) {
-  final GradleProject gradleProject = MockGradleProject();
-  when(gradleProject.bundleDirectory).thenReturn(fs.currentDirectory);
+FlutterProject generateFakeAppBundle(String directoryName, String fileName) {
+  final FlutterProject project = MockFlutterProject();
+  final AndroidProject androidProject = MockAndroidProject();
 
-  final Directory aabDirectory = gradleProject.bundleDirectory.childDirectory(directoryName);
-  fs.directory(aabDirectory).createSync(recursive: true);
-  fs.file(fs.path.join(aabDirectory.path, fileName)).writeAsStringSync('irrelevant');
-  return gradleProject;
+  when(project.isModule).thenReturn(false);
+  when(project.android).thenReturn(androidProject);
+  when(androidProject.buildDirectory).thenReturn(fs.directory('irrelevant'));
+
+  final Directory bundleDirectory = getBundleDirectory(project);
+  bundleDirectory
+    .childDirectory(directoryName)
+    ..createSync(recursive: true);
+
+  bundleDirectory
+    .childDirectory(directoryName)
+    .childFile(fileName)
+    .createSync();
+  return project;
 }
 
 Platform fakePlatform(String name) {
@@ -1716,17 +1553,19 @@
 
 class FakeGradleUtils extends GradleUtils {
   @override
-  Future<String> getExecutable(FlutterProject project) async {
+  String getExecutable(FlutterProject project) {
     return 'gradlew';
   }
 }
 
 class MockAndroidSdk extends Mock implements AndroidSdk {}
+class MockAndroidProject extends Mock implements AndroidProject {}
 class MockAndroidStudio extends Mock implements AndroidStudio {}
 class MockDirectory extends Mock implements Directory {}
 class MockFile extends Mock implements File {}
-class MockGradleProject extends Mock implements GradleProject {}
+class MockFlutterProject extends Mock implements FlutterProject {}
 class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
 class MockProcessManager extends Mock implements ProcessManager {}
 class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
 class MockitoAndroidSdk extends Mock implements AndroidSdk {}
+class MockUsage extends Mock implements Usage {}
diff --git a/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart
new file mode 100644
index 0000000..d8b945e
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart
@@ -0,0 +1,137 @@
+// 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:file/memory.dart';
+import 'package:flutter_tools/src/android/gradle_utils.dart';
+import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/os.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:process/process.dart';
+
+import '../../src/common.dart';
+import '../../src/context.dart';
+
+void main() {
+   group('injectGradleWrapperIfNeeded', () {
+    MemoryFileSystem memoryFileSystem;
+    Directory tempDir;
+    Directory gradleWrapperDirectory;
+
+    setUp(() {
+      memoryFileSystem = MemoryFileSystem();
+      tempDir = memoryFileSystem.systemTempDirectory.createTempSync('flutter_artifacts_test.');
+      gradleWrapperDirectory = memoryFileSystem.directory(
+          memoryFileSystem.path.join(tempDir.path, 'bin', 'cache', 'artifacts', 'gradle_wrapper'));
+      gradleWrapperDirectory.createSync(recursive: true);
+      gradleWrapperDirectory
+        .childFile('gradlew')
+        .writeAsStringSync('irrelevant');
+      gradleWrapperDirectory
+        .childDirectory('gradle')
+        .childDirectory('wrapper')
+        .createSync(recursive: true);
+      gradleWrapperDirectory
+        .childDirectory('gradle')
+        .childDirectory('wrapper')
+        .childFile('gradle-wrapper.jar')
+        .writeAsStringSync('irrelevant');
+    });
+
+    testUsingContext('Inject the wrapper when all files are missing', () {
+      final Directory sampleAppAndroid = fs.directory('/sample-app/android');
+      sampleAppAndroid.createSync(recursive: true);
+
+      injectGradleWrapperIfNeeded(sampleAppAndroid);
+
+      expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue);
+
+      expect(sampleAppAndroid
+        .childDirectory('gradle')
+        .childDirectory('wrapper')
+        .childFile('gradle-wrapper.jar')
+        .existsSync(), isTrue);
+
+      expect(sampleAppAndroid
+        .childDirectory('gradle')
+        .childDirectory('wrapper')
+        .childFile('gradle-wrapper.properties')
+        .existsSync(), isTrue);
+
+      expect(sampleAppAndroid
+        .childDirectory('gradle')
+        .childDirectory('wrapper')
+        .childFile('gradle-wrapper.properties')
+        .readAsStringSync(),
+            'distributionBase=GRADLE_USER_HOME\n'
+            'distributionPath=wrapper/dists\n'
+            'zipStoreBase=GRADLE_USER_HOME\n'
+            'zipStorePath=wrapper/dists\n'
+            'distributionUrl=https\\://services.gradle.org/distributions/gradle-5.6.2-all.zip\n');
+    }, overrides: <Type, Generator>{
+      Cache: () => Cache(rootOverride: tempDir),
+      FileSystem: () => memoryFileSystem,
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Inject the wrapper when some files are missing', () {
+      final Directory sampleAppAndroid = fs.directory('/sample-app/android');
+      sampleAppAndroid.createSync(recursive: true);
+
+      // There's an existing gradlew
+      sampleAppAndroid.childFile('gradlew').writeAsStringSync('existing gradlew');
+
+      injectGradleWrapperIfNeeded(sampleAppAndroid);
+
+      expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue);
+      expect(sampleAppAndroid.childFile('gradlew').readAsStringSync(),
+          equals('existing gradlew'));
+
+      expect(sampleAppAndroid
+        .childDirectory('gradle')
+        .childDirectory('wrapper')
+        .childFile('gradle-wrapper.jar')
+        .existsSync(), isTrue);
+
+      expect(sampleAppAndroid
+        .childDirectory('gradle')
+        .childDirectory('wrapper')
+        .childFile('gradle-wrapper.properties')
+        .existsSync(), isTrue);
+
+      expect(sampleAppAndroid
+        .childDirectory('gradle')
+        .childDirectory('wrapper')
+        .childFile('gradle-wrapper.properties')
+        .readAsStringSync(),
+          'distributionBase=GRADLE_USER_HOME\n'
+          'distributionPath=wrapper/dists\n'
+          'zipStoreBase=GRADLE_USER_HOME\n'
+          'zipStorePath=wrapper/dists\n'
+          'distributionUrl=https\\://services.gradle.org/distributions/gradle-5.6.2-all.zip\n');
+    }, overrides: <Type, Generator>{
+      Cache: () => Cache(rootOverride: tempDir),
+      FileSystem: () => memoryFileSystem,
+      ProcessManager: () => FakeProcessManager.any(),
+    });
+
+    testUsingContext('Gives executable permission to gradle', () {
+      final Directory sampleAppAndroid = fs.directory('/sample-app/android');
+      sampleAppAndroid.createSync(recursive: true);
+
+      // Make gradlew in the wrapper executable.
+      os.makeExecutable(gradleWrapperDirectory.childFile('gradlew'));
+
+      injectGradleWrapperIfNeeded(sampleAppAndroid);
+
+      final File gradlew = sampleAppAndroid.childFile('gradlew');
+      expect(gradlew.existsSync(), isTrue);
+      expect(gradlew.statSync().modeString().contains('x'), isTrue);
+    }, overrides: <Type, Generator>{
+      Cache: () => Cache(rootOverride: tempDir),
+      FileSystem: () => memoryFileSystem,
+      ProcessManager: () => FakeProcessManager.any(),
+      OperatingSystemUtils: () => OperatingSystemUtils(),
+    });
+  });
+}
\ No newline at end of file
diff --git a/packages/flutter_tools/test/general.shard/cache_test.dart b/packages/flutter_tools/test/general.shard/cache_test.dart
index be5e4a9..dd9e2ac 100644
--- a/packages/flutter_tools/test/general.shard/cache_test.dart
+++ b/packages/flutter_tools/test/general.shard/cache_test.dart
@@ -11,7 +11,7 @@
 import 'package:platform/platform.dart';
 import 'package:process/process.dart';
 
-import 'package:flutter_tools/src/android/gradle.dart';
+import 'package:flutter_tools/src/android/gradle_utils.dart';
 import 'package:flutter_tools/src/base/common.dart';
 import 'package:flutter_tools/src/base/io.dart';
 import 'package:flutter_tools/src/cache.dart';
@@ -316,7 +316,7 @@
           expect(args[1], '-b');
           expect(args[2].endsWith('resolve_dependencies.gradle'), isTrue);
           expect(args[5], 'resolveDependencies');
-          expect(invocation.namedArguments[#environment], gradleEnv);
+          expect(invocation.namedArguments[#environment], gradleEnvironment);
           return Future<ProcessResult>.value(ProcessResult(0, 0, '', ''));
         });
 
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
index a93b50e..38d505d 100644
--- a/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/build_aar_test.dart
@@ -8,7 +8,6 @@
 import 'package:file/memory.dart';
 import 'package:flutter_tools/src/android/android_builder.dart';
 import 'package:flutter_tools/src/android/android_sdk.dart';
-import 'package:flutter_tools/src/android/gradle.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/platform.dart';
 import 'package:flutter_tools/src/cache.dart';
@@ -17,6 +16,7 @@
 import 'package:mockito/mockito.dart';
 import 'package:process/process.dart';
 
+import '../../src/android_common.dart';
 import '../../src/common.dart';
 import '../../src/context.dart';
 import '../../src/mocks.dart';
@@ -120,7 +120,9 @@
 
     group('AndroidSdk', () {
       testUsingContext('validateSdkWellFormed() not called, sdk reinitialized', () async {
-        final Directory gradleCacheDir = memoryFileSystem.directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')..createSync(recursive: true);
+        final Directory gradleCacheDir = memoryFileSystem
+          .directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')
+          ..createSync(recursive: true);
         gradleCacheDir.childFile(platform.isWindows ? 'gradlew.bat' : 'gradlew').createSync();
 
         tempDir.childFile('pubspec.yaml')
@@ -141,11 +143,31 @@
 ''');
         tempDir.childFile('.packages').createSync(recursive: true);
         final Directory androidDir = tempDir.childDirectory('android');
-        androidDir.childFile('build.gradle').createSync(recursive: true);
-        androidDir.childFile('gradle.properties').createSync(recursive: true);
-        androidDir.childDirectory('gradle').childDirectory('wrapper').childFile('gradle-wrapper.properties').createSync(recursive: true);
-        tempDir.childDirectory('build').childDirectory('outputs').childDirectory('repo').createSync(recursive: true);
-        tempDir.childDirectory('lib').childFile('main.dart').createSync(recursive: true);
+        androidDir
+          .childFile('build.gradle')
+          .createSync(recursive: true);
+        androidDir
+          .childDirectory('app')
+          .childFile('build.gradle')
+          ..createSync(recursive: true)
+          ..writeAsStringSync('apply from: irrelevant/flutter.gradle');
+        androidDir
+          .childFile('gradle.properties')
+          .createSync(recursive: true);
+        androidDir
+          .childDirectory('gradle')
+          .childDirectory('wrapper')
+          .childFile('gradle-wrapper.properties')
+          .createSync(recursive: true);
+        tempDir
+          .childDirectory('build')
+          .childDirectory('outputs')
+          .childDirectory('repo')
+          .createSync(recursive: true);
+        tempDir
+          .childDirectory('lib')
+          .childFile('main.dart')
+          .createSync(recursive: true);
         await runBuildAarCommand(tempDir.path);
 
         verifyNever(mockAndroidSdk.validateSdkWellFormed());
@@ -153,7 +175,6 @@
       },
       overrides: <Type, Generator>{
         AndroidSdk: () => mockAndroidSdk,
-        GradleUtils: () => GradleUtils(),
         ProcessManager: () => mockProcessManager,
         FileSystem: () => memoryFileSystem,
       });
diff --git a/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart b/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart
index fd063c1..4970b0b 100644
--- a/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart
@@ -8,7 +8,6 @@
 import 'package:file/memory.dart';
 import 'package:flutter_tools/src/android/android_builder.dart';
 import 'package:flutter_tools/src/android/android_sdk.dart';
-import 'package:flutter_tools/src/android/gradle.dart';
 import 'package:flutter_tools/src/base/context.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/logger.dart';
@@ -20,6 +19,7 @@
 import 'package:mockito/mockito.dart';
 import 'package:process/process.dart';
 
+import '../../src/android_common.dart';
 import '../../src/common.dart';
 import '../../src/context.dart';
 import '../../src/mocks.dart';
@@ -152,7 +152,10 @@
             platform.isWindows ? 'gradlew.bat' : 'gradlew');
       });
       testUsingContext('validateSdkWellFormed() not called, sdk reinitialized', () async {
-        final Directory gradleCacheDir = memoryFileSystem.directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')..createSync(recursive: true);
+        final Directory gradleCacheDir = memoryFileSystem
+          .directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')
+          ..createSync(recursive: true);
+
         gradleCacheDir.childFile(platform.isWindows ? 'gradlew.bat' : 'gradlew').createSync();
 
         tempDir.childFile('pubspec.yaml')
@@ -170,11 +173,31 @@
 ''');
         tempDir.childFile('.packages').createSync(recursive: true);
         final Directory androidDir = tempDir.childDirectory('android');
-        androidDir.childFile('build.gradle').createSync(recursive: true);
-        androidDir.childFile('gradle.properties').createSync(recursive: true);
-        androidDir.childDirectory('gradle').childDirectory('wrapper').childFile('gradle-wrapper.properties').createSync(recursive: true);
-        tempDir.childDirectory('build').childDirectory('outputs').childDirectory('repo').createSync(recursive: true);
-        tempDir.childDirectory('lib').childFile('main.dart').createSync(recursive: true);
+        androidDir
+          .childFile('build.gradle')
+          .createSync(recursive: true);
+        androidDir
+          .childDirectory('app')
+          .childFile('build.gradle')
+          ..createSync(recursive: true)
+          ..writeAsStringSync('apply from: irrelevant/flutter.gradle');
+        androidDir
+          .childFile('gradle.properties')
+          .createSync(recursive: true);
+        androidDir
+          .childDirectory('gradle')
+          .childDirectory('wrapper')
+          .childFile('gradle-wrapper.properties')
+          .createSync(recursive: true);
+        tempDir
+          .childDirectory('build')
+          .childDirectory('outputs')
+          .childDirectory('repo')
+          .createSync(recursive: true);
+        tempDir
+          .childDirectory('lib')
+          .childFile('main.dart')
+          .createSync(recursive: true);
         when(mockProcessManager.run(any,
           workingDirectory: anyNamed('workingDirectory'),
           environment: anyNamed('environment')))
@@ -182,7 +205,7 @@
 
         await expectLater(
           runBuildApkCommand(tempDir.path, arguments: <String>['--no-pub', '--flutter-root=/flutter_root']),
-          throwsToolExit(message: 'Gradle build failed: 1'),
+          throwsToolExit(message: 'Gradle task assembleRelease failed with exit code 1'),
         );
 
         verifyNever(mockAndroidSdk.validateSdkWellFormed());
@@ -190,7 +213,6 @@
       },
       overrides: <Type, Generator>{
         AndroidSdk: () => mockAndroidSdk,
-        GradleUtils: () => GradleUtils(),
         FileSystem: () => memoryFileSystem,
         ProcessManager: () => mockProcessManager,
       });
@@ -221,7 +243,6 @@
     overrides: <Type, Generator>{
       AndroidSdk: () => mockAndroidSdk,
       FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
-      GradleUtils: () => GradleUtils(),
       ProcessManager: () => mockProcessManager,
     });
 
@@ -252,7 +273,6 @@
     overrides: <Type, Generator>{
       AndroidSdk: () => mockAndroidSdk,
       FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
-      GradleUtils: () => GradleUtils(),
       ProcessManager: () => mockProcessManager,
     });
 
@@ -300,13 +320,12 @@
       verify(mockUsage.sendEvent(
         'build',
         'apk',
-        label: 'r8-failure',
+        label: 'gradle--r8-failure',
         parameters: anyNamed('parameters'),
       )).called(1);
     },
     overrides: <Type, Generator>{
       AndroidSdk: () => mockAndroidSdk,
-      GradleUtils: () => GradleUtils(),
       FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
       ProcessManager: () => mockProcessManager,
       Usage: () => mockUsage,
@@ -344,7 +363,7 @@
       }, throwsToolExit());
 
       final BufferLogger logger = context.get<Logger>();
-      expect(logger.statusText, contains('[!] Your app isn\'t using AndroidX'));
+      expect(logger.statusText, contains('Your app isn\'t using AndroidX'));
       expect(logger.statusText, contains(
         'To avoid potential build failures, you can quickly migrate your app by '
         'following the steps on https://goo.gl/CP92wY'
@@ -359,7 +378,6 @@
     },
     overrides: <Type, Generator>{
       AndroidSdk: () => mockAndroidSdk,
-      GradleUtils: () => GradleUtils(),
       FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
       ProcessManager: () => mockProcessManager,
       Usage: () => mockUsage,
@@ -414,7 +432,6 @@
     },
     overrides: <Type, Generator>{
       AndroidSdk: () => mockAndroidSdk,
-      GradleUtils: () => GradleUtils(),
       FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
       ProcessManager: () => mockProcessManager,
       Usage: () => mockUsage,
diff --git a/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart b/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart
index 7663156..8f79b4a 100644
--- a/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart
@@ -8,7 +8,6 @@
 import 'package:file/memory.dart';
 import 'package:flutter_tools/src/android/android_builder.dart';
 import 'package:flutter_tools/src/android/android_sdk.dart';
-import 'package:flutter_tools/src/android/gradle.dart';
 import 'package:flutter_tools/src/base/context.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/logger.dart';
@@ -20,6 +19,7 @@
 import 'package:mockito/mockito.dart';
 import 'package:process/process.dart';
 
+import '../../src/android_common.dart';
 import '../../src/common.dart';
 import '../../src/context.dart';
 import '../../src/mocks.dart';
@@ -139,7 +139,9 @@
       });
 
       testUsingContext('validateSdkWellFormed() not called, sdk reinitialized', () async {
-        final Directory gradleCacheDir = memoryFileSystem.directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')..createSync(recursive: true);
+        final Directory gradleCacheDir = memoryFileSystem
+          .directory('/flutter_root/bin/cache/artifacts/gradle_wrapper')
+          ..createSync(recursive: true);
         gradleCacheDir.childFile(platform.isWindows ? 'gradlew.bat' : 'gradlew').createSync();
 
         tempDir.childFile('pubspec.yaml')
@@ -158,10 +160,26 @@
         tempDir.childFile('.packages').createSync(recursive: true);
         final Directory androidDir = tempDir.childDirectory('android');
         androidDir.childFile('build.gradle').createSync(recursive: true);
-        androidDir.childFile('gradle.properties').createSync(recursive: true);
-        androidDir.childDirectory('gradle').childDirectory('wrapper').childFile('gradle-wrapper.properties').createSync(recursive: true);
-        tempDir.childDirectory('build').childDirectory('outputs').childDirectory('repo').createSync(recursive: true);
-        tempDir.childDirectory('lib').childFile('main.dart').createSync(recursive: true);
+        androidDir
+          .childDirectory('app')
+          .childFile('build.gradle')
+          ..createSync(recursive: true)
+          ..writeAsStringSync('apply from: irrelevant/flutter.gradle');
+        androidDir
+          .childFile('gradle.properties')
+          .createSync(recursive: true);
+        androidDir
+          .childDirectory('gradle')
+          .childDirectory('wrapper')
+          .childFile('gradle-wrapper.properties')
+          .createSync(recursive: true);
+        tempDir.childDirectory('build')
+          .childDirectory('outputs')
+          .childDirectory('repo')
+          .createSync(recursive: true);
+        tempDir.childDirectory('lib')
+          .childFile('main.dart')
+          .createSync(recursive: true);
         when(mockProcessManager.run(any,
           workingDirectory: anyNamed('workingDirectory'),
           environment: anyNamed('environment')))
@@ -169,7 +187,7 @@
 
         await expectLater(
           runBuildAppBundleCommand(tempDir.path, arguments: <String>['--no-pub', '--flutter-root=/flutter_root']),
-          throwsToolExit(message: 'Gradle build failed: 1'),
+          throwsToolExit(message: 'Gradle task bundleRelease failed with exit code 1'),
         );
 
         verifyNever(mockAndroidSdk.validateSdkWellFormed());
@@ -177,7 +195,6 @@
       },
       overrides: <Type, Generator>{
         AndroidSdk: () => mockAndroidSdk,
-        GradleUtils: () => GradleUtils(),
         ProcessManager: () => mockProcessManager,
         FileSystem: () => memoryFileSystem,
       });
@@ -210,7 +227,6 @@
     overrides: <Type, Generator>{
       AndroidSdk: () => mockAndroidSdk,
       FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
-      GradleUtils: () => GradleUtils(),
       ProcessManager: () => mockProcessManager,
     });
 
@@ -243,7 +259,6 @@
     overrides: <Type, Generator>{
       AndroidSdk: () => mockAndroidSdk,
       FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
-      GradleUtils: () => GradleUtils(),
       ProcessManager: () => mockProcessManager,
     });
 
@@ -291,13 +306,12 @@
       verify(mockUsage.sendEvent(
         'build',
         'appbundle',
-        label: 'r8-failure',
+        label: 'gradle--r8-failure',
         parameters: anyNamed('parameters'),
       )).called(1);
     },
     overrides: <Type, Generator>{
       AndroidSdk: () => mockAndroidSdk,
-      GradleUtils: () => GradleUtils(),
       FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
       ProcessManager: () => mockProcessManager,
       Usage: () => mockUsage,
@@ -335,7 +349,7 @@
       }, throwsToolExit());
 
       final BufferLogger logger = context.get<Logger>();
-      expect(logger.statusText, contains('[!] Your app isn\'t using AndroidX'));
+      expect(logger.statusText, contains('Your app isn\'t using AndroidX'));
       expect(logger.statusText, contains(
         'To avoid potential build failures, you can quickly migrate your app by '
         'following the steps on https://goo.gl/CP92wY'
@@ -350,7 +364,6 @@
     },
     overrides: <Type, Generator>{
       AndroidSdk: () => mockAndroidSdk,
-      GradleUtils: () => GradleUtils(),
       FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
       ProcessManager: () => mockProcessManager,
       Usage: () => mockUsage,
@@ -388,7 +401,7 @@
       }, throwsToolExit());
 
       final BufferLogger logger = context.get<Logger>();
-      expect(logger.statusText.contains('[!] Your app isn\'t using AndroidX'), isFalse);
+      expect(logger.statusText.contains('Your app isn\'t using AndroidX'), isFalse);
       expect(
         logger.statusText.contains(
           'To avoid potential build failures, you can quickly migrate your app by '
@@ -405,7 +418,6 @@
     },
     overrides: <Type, Generator>{
       AndroidSdk: () => mockAndroidSdk,
-      GradleUtils: () => GradleUtils(),
       FlutterProjectFactory: () => FakeFlutterProjectFactory(tempDir),
       ProcessManager: () => mockProcessManager,
       Usage: () => mockUsage,
diff --git a/packages/flutter_tools/test/src/android_common.dart b/packages/flutter_tools/test/src/android_common.dart
new file mode 100644
index 0000000..dda0afb
--- /dev/null
+++ b/packages/flutter_tools/test/src/android_common.dart
@@ -0,0 +1,34 @@
+// 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:meta/meta.dart';
+
+import 'package:flutter_tools/src/android/android_builder.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/project.dart';
+
+/// A fake implementation of [AndroidBuilder].
+class FakeAndroidBuilder implements AndroidBuilder {
+  @override
+  Future<void> buildAar({
+    @required FlutterProject project,
+    @required AndroidBuildInfo androidBuildInfo,
+    @required String target,
+    @required String outputDir,
+  }) async {}
+
+  @override
+  Future<void> buildApk({
+    @required FlutterProject project,
+    @required AndroidBuildInfo androidBuildInfo,
+    @required String target,
+  }) async {}
+
+  @override
+  Future<void> buildAab({
+    @required FlutterProject project,
+    @required AndroidBuildInfo androidBuildInfo,
+    @required String target,
+  }) async {}
+}