// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:crypto/crypto.dart';
import 'package:meta/meta.dart';
import 'package:xml/xml.dart' as xml;

import '../android/android_sdk.dart';
import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../base/terminal.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../cache.dart';
import '../flutter_manifest.dart';
import '../globals.dart';
import '../project.dart';
import '../reporting/reporting.dart';
import 'android_sdk.dart';
import 'gradle_errors.dart';
import 'gradle_utils.dart';

/// 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');
}

/// 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');
}

/// The directory where the repo is generated.
/// Only applicable to AARs.
Directory getRepoDirectory(Directory buildDirectory) {
  return buildDirectory
    .childDirectory('outputs')
    .childDirectory('repo');
}

/// 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)}';
}

/// Returns the task to build an APK.
@visibleForTesting
String getAssembleTaskFor(BuildInfo buildInfo) {
  return _taskFor('assemble', buildInfo);
}

/// 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()) {
    return false;
  }
  final File appGradle = project.hostAppGradleRoot.childFile(
      fs.path.join('app', 'build.gradle'));
  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 false;
}

/// Returns the apk file created by [buildGradleProject]
Future<File> getGradleAppOut(AndroidProject androidProject) async {
  if (!_isSupportedVersion(androidProject)) {
    _exitWithUnsupportedProjectMessage();
  }
  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 FlutterProject flutterProject = FlutterProject.current();
  await processUtils.run(<String>[
      gradleUtils.getExecutable(flutterProject),
      'dependencies',
    ],
    throwOnError: true,
    workingDirectory: flutterProject.android.hostAppGradleRoot.path,
    environment: gradleEnvironment,
  );
  androidSdk.reinitialize();
  progress.stop();
}

/// Tries to create `settings_aar.gradle` in an app project by removing the subprojects
/// from the existing `settings.gradle` file. This operation will fail if the existing
/// `settings.gradle` file has local edits.
@visibleForTesting
void createSettingsAarGradle(Directory androidDirectory) {
  final File newSettingsFile = androidDirectory.childFile('settings_aar.gradle');
  if (newSettingsFile.existsSync()) {
    return;
  }
  final File currentSettingsFile = androidDirectory.childFile('settings.gradle');
  if (!currentSettingsFile.existsSync()) {
    return;
  }
  final String currentFileContent = currentSettingsFile.readAsStringSync();

  final String newSettingsRelativeFile = fs.path.relative(newSettingsFile.path);
  final Status status = logger.startProgress('✏️  Creating `$newSettingsRelativeFile`...',
      timeout: timeoutConfiguration.fastOperation);

  final String flutterRoot = fs.path.absolute(Cache.flutterRoot);
  final File deprecatedFile = fs.file(fs.path.join(flutterRoot, 'packages','flutter_tools',
      'gradle', 'deprecated_settings.gradle'));
  assert(deprecatedFile.existsSync());
  final String settingsAarContent = fs.file(fs.path.join(flutterRoot, 'packages','flutter_tools',
      'gradle', 'settings_aar.gradle.tmpl')).readAsStringSync();

  // Get the `settings.gradle` content variants that should be patched.
  final List<String> existingVariants = deprecatedFile.readAsStringSync().split(';EOF');
  existingVariants.add(settingsAarContent);

  bool exactMatch = false;
  for (String fileContentVariant in existingVariants) {
    if (currentFileContent.trim() == fileContentVariant.trim()) {
      exactMatch = true;
      break;
    }
  }
  if (!exactMatch) {
    status.cancel();
    printStatus('$warningMark Flutter tried to create the file `$newSettingsRelativeFile`, but failed.');
    // Print how to manually update the file.
    printStatus(fs.file(fs.path.join(flutterRoot, 'packages','flutter_tools',
        'gradle', 'manual_migration_settings.gradle.md')).readAsStringSync());
    throwToolExit('Please create the file and run this command again.');
  }
  // Copy the new file.
  newSettingsFile.writeAsStringSync(settingsAarContent);
  status.stop();
  printStatus('$successMark `$newSettingsRelativeFile` created successfully.');
}

/// Builds an app.
///
/// * [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 {
  if (androidSdk == null) {
    exitWithNoSdkMessage();
  }
  if (!project.android.isUsingGradle) {
    _exitWithProjectNotUsingGradleMessage();
  }
  if (!_isSupportedVersion(project.android)) {
    _exitWithUnsupportedProjectMessage();
  }
  final Directory buildDirectory = project.android.buildDirectory;

  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);

  if (shouldBuildPluginAsAar) {
    // Create a settings.gradle that doesn't import the plugins as subprojects.
    createSettingsAarGradle(project.android.hostAppGradleRoot);
    await buildPluginsAsAar(
      project,
      androidBuildInfo,
      buildDirectory: 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;
    final Directory localEngineRepo = _getLocalEngineRepo(
      engineOutPath: localEngineArtifacts.engineOutPath,
      androidBuildInfo: androidBuildInfo,
    );
    printTrace(
      'Using local engine: ${localEngineArtifacts.engineOutPath}\n'
      'Local Maven repo: ${localEngineRepo.path}'
    );
    command.add('-Plocal-engine-repo=${localEngineRepo.path}');
    command.add('-Plocal-engine-build-mode=${buildInfo.modeName}');
    command.add('-Plocal-engine-out=${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);

  GradleHandledError detectedGradleError;
  String detectedGradleErrorLine;
  String consumeLog(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/stderr 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/stderr from Gradle.
    return line;
  }

  final Stopwatch sw = Stopwatch()..start();
  int exitCode = 1;
  try {
    exitCode = await processUtils.stream(
      command,
      workingDirectory: project.android.hostAppGradleRoot.path,
      allowReentrantFlutter: true,
      environment: gradleEnvironment,
      mapFunction: consumeLog,
    );
  } on ProcessException catch(exception) {
    consumeLog(exception.toString());
    // Rethrow the exception if the error isn't handled by any of the
    // `localGradleErrors`.
    if (detectedGradleError == null) {
      rethrow;
    }
  } 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);
    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);
  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.
/// * [outputDir] is the destination of the artifacts,
Future<void> buildGradleAar({
  @required FlutterProject project,
  @required AndroidBuildInfo androidBuildInfo,
  @required String target,
  @required Directory outputDirectory,
}) async {
  assert(project != null);
  assert(target != null);
  assert(androidBuildInfo != null);
  assert(outputDirectory != null);

  if (androidSdk == null) {
    exitWithNoSdkMessage();
  }
  final FlutterManifest manifest = project.manifest;
  if (!manifest.isModule && !manifest.isPlugin) {
    throwToolExit('AARs can only be built for plugin or module projects.');
  }

  final String aarTask = getAarTaskFor(androidBuildInfo.buildInfo);
  final Status status = logger.startProgress(
    'Running Gradle task \'$aarTask\'...',
    timeout: timeoutConfiguration.slowOperation,
    multilineOutput: true,
  );

  final String flutterRoot = fs.path.absolute(Cache.flutterRoot);
  final String initScript = fs.path.join(
    flutterRoot,
    'packages',
    'flutter_tools',
    'gradle',
    'aar_init_script.gradle',
  );
  final List<String> command = <String>[
    gradleUtils.getExecutable(project),
    '-I=$initScript',
    '-Pflutter-root=$flutterRoot',
    '-Poutput-dir=${outputDirectory.path}',
    '-Pis-plugin=${manifest.isPlugin}',
  ];

  if (target != null && target.isNotEmpty) {
    command.add('-Ptarget=$target');
  }

  if (androidBuildInfo.targetArchs.isNotEmpty) {
    final String targetPlatforms = androidBuildInfo.targetArchs
        .map(getPlatformNameForAndroidArch).join(',');
    command.add('-Ptarget-platform=$targetPlatforms');
  }
  if (artifacts is LocalEngineArtifacts) {
    final LocalEngineArtifacts localEngineArtifacts = artifacts;
    final Directory localEngineRepo = _getLocalEngineRepo(
      engineOutPath: localEngineArtifacts.engineOutPath,
      androidBuildInfo: androidBuildInfo,
    );
    printTrace(
      'Using local engine: ${localEngineArtifacts.engineOutPath}\n'
      'Local Maven repo: ${localEngineRepo.path}'
    );
    command.add('-Plocal-engine-repo=${localEngineRepo.path}');
    command.add('-Plocal-engine-build-mode=${androidBuildInfo.buildInfo.modeName}');
    command.add('-Plocal-engine-out=${localEngineArtifacts.engineOutPath}');
  }

  command.add(aarTask);

  final Stopwatch sw = Stopwatch()..start();
  RunResult result;
  try {
    result = await processUtils.run(
      command,
      workingDirectory: project.android.hostAppGradleRoot.path,
      allowReentrantFlutter: true,
      environment: gradleEnvironment,
    );
  } finally {
    status.stop();
  }
  flutterUsage.sendTiming('build', 'gradle-aar', sw.elapsed);

  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,
    );
  }
  final Directory repoDirectory = getRepoDirectory(outputDirectory);
  if (!repoDirectory.existsSync()) {
    printStatus(result.stdout, wrap: false);
    printError(result.stderr, wrap: false);
    throwToolExit(
      'Gradle task $aarTask failed to produce $repoDirectory.',
      exitCode: exitCode,
    );
  }
  printStatus(
    '$successMark Built ${fs.path.relative(repoDirectory.path)}.',
    color: TerminalColor.green,
  );
}

/// Prints how to consume the AAR from a host app.
void printHowToConsumeAar({
  @required Set<String> buildModes,
  @required String androidPackage,
  @required Directory repoDirectory,
}) {
  assert(buildModes != null && buildModes.isNotEmpty);
  assert(androidPackage != null);
  assert(repoDirectory != null);

  printStatus('''

${terminal.bolden('Consuming the Module')}
  1. Open ${fs.path.join('<host>', 'app', 'build.gradle')}
  2. Ensure you have the repositories configured, otherwise add them:

      repositories {
        maven {
            url '${repoDirectory.path}'
        }
        maven {
            url 'http://download.flutter.io'
        }
      }

  3. Make the host app depend on the Flutter module:

    dependencies {''');

  for (String buildMode in buildModes) {
    printStatus('''
      ${buildMode}Implementation '$androidPackage:flutter_$buildMode:1.0''');
  }

printStatus('''
    }
''');

  if (buildModes.contains('profile')) {
    printStatus('''

  4. Add the `profile` build type:

    android {
      buildTypes {
        profile {
          initWith debug
        }
      }
    }
''');
  }

printStatus('To learn more, visit https://flutter.dev/go/build-aar''');
}

String _hex(List<int> bytes) {
  final StringBuffer result = StringBuffer();
  for (int part in bytes) {
    result.write('${part < 16 ? '0' : ''}${part.toRadixString(16)}');
  }
  return result.toString();
}

String _calculateSha(File file) {
  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', 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', sw.elapsed);
  return sha;
}

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.',
  );
}

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.'
  );
}

/// Returns [true] if the current app uses AndroidX.
// TODO(egarciad): https://github.com/flutter/flutter/issues/40800
// Remove `FlutterManifest.usesAndroidX` and provide a unified `AndroidProject.usesAndroidX`.
bool isAppUsingAndroidX(Directory androidDirectory) {
  final File properties = androidDirectory.childFile('gradle.properties');
  if (!properties.existsSync()) {
    return false;
  }
  return properties.readAsStringSync().contains('android.useAndroidX=true');
}

/// Builds the plugins as AARs.
@visibleForTesting
Future<void> buildPluginsAsAar(
  FlutterProject flutterProject,
  AndroidBuildInfo androidBuildInfo, {
  Directory buildDirectory,
}) async {
  final File flutterPluginFile = flutterProject.flutterPluginsFile;
  if (!flutterPluginFile.existsSync()) {
    return;
  }
  final List<String> plugins = flutterPluginFile.readAsStringSync().split('\n');
  for (String plugin in plugins) {
    final List<String> pluginParts = plugin.split('=');
    if (pluginParts.length != 2) {
      continue;
    }
    final Directory pluginDirectory = fs.directory(pluginParts.last);
    assert(pluginDirectory.existsSync());

    final String pluginName = pluginParts.first;
    logger.printStatus('Building plugin $pluginName...');
    try {
      await buildGradleAar(
        project: FlutterProject.fromDirectory(pluginDirectory),
        androidBuildInfo: const AndroidBuildInfo(
          BuildInfo(
            BuildMode.release, // Plugins are built as release.
            null, // Plugins don't define flavors.
          ),
        ),
        target: '',
        outputDirectory: buildDirectory,
      );
    } on ToolExit {
      // Log the entire plugin entry in `.flutter-plugins` since it
      // includes the plugin name and the version.
      BuildEvent('gradle-plugin-aar-failure', eventError: plugin).send();
      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(
  FlutterProject project,
  AndroidBuildInfo androidBuildInfo,
) {
  final Iterable<String> apkFileNames = _apkFilesFor(androidBuildInfo);
  final Directory apkDirectory = getApkDirectory(project);
  final Iterable<File> apks = apkFileNames.expand<File>((String apkFileName) {
    File apkFile = apkDirectory.childFile(apkFileName);
    if (apkFile.existsSync()) {
      return <File>[apkFile];
    }
    final BuildInfo buildInfo = androidBuildInfo.buildInfo;
    final String modeName = camelCase(buildInfo.modeName);
    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 = apkDirectory
        .childDirectory(buildInfo.flavor)
        .childDirectory(modeName)
        .childFile(apkFileName);
      if (apkFile.existsSync()) {
        return <File>[apkFile];
      }
    }
    return const <File>[];
  });
  if (apks.isEmpty) {
    _exitWithExpectedFileNotFound(
      project: project,
      fileExtension: '.apk',
    );
  }
  return apks;
}

@visibleForTesting
File findBundleFile(FlutterProject project, BuildInfo buildInfo) {
  final List<File> fileCandidates = <File>[
    getBundleDirectory(project)
      .childDirectory(camelCase(buildInfo.modeName))
      .childFile('app.aab'),
    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(
      getBundleDirectory(project)
        .childDirectory('${buildInfo.flavor}${camelCase('_' + buildInfo.modeName)}')
        .childFile('app.aab'));

    // The Android Gradle plugin 3.5.0 adds the flavor name to file name.
    // 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(
      getBundleDirectory(project)
        .childDirectory('${buildInfo.flavor}${camelCase('_' + buildInfo.modeName)}')
        .childFile('app-${buildInfo.flavor}-${buildInfo.modeName}.aab'));
  }
  for (final File bundleFile in fileCandidates) {
    if (bundleFile.existsSync()) {
      return bundleFile;
    }
  }
  _exitWithExpectedFileNotFound(
    project: project,
    fileExtension: '.aab',
  );
  return null;
}

/// Throws a [ToolExit] exception and logs the event.
void _exitWithExpectedFileNotFound({
  @required FlutterProject project,
  @required String fileExtension,
}) {
  assert(project != null);
  assert(fileExtension != null);

  final String androidGradlePluginVersion =
      getGradleVersionForAndroidPlugin(project.android.hostAppGradleRoot);
  BuildEvent('gradle-expected-file-not-found',
    settings:
      'androidGradlePluginVersion: $androidGradlePluginVersion, '
      'fileExtension: $fileExtension'
    ).send();
  throwToolExit(
    'Gradle build failed to produce an $fileExtension file. '
    'It\'s likely that this file was generated under ${project.android.buildDirectory.path}, '
    'but the tool couldn\'t find it.'
  );
}

void _createSymlink(String targetPath, String linkPath) {
  final File targetFile = fs.file(targetPath);
  if (!targetFile.existsSync()) {
    throwToolExit('The file $targetPath wasn\'t found in the local engine out directory.');
  }
  final File linkFile = fs.file(linkPath);
  final Link symlink = linkFile.parent.childLink(linkFile.basename);
  try {
    symlink.createSync(targetPath, recursive: true);
  } on FileSystemException catch (exception) {
    throwToolExit(
      'Failed to create the symlink $linkPath->$targetPath: $exception'
    );
  }
}

String _getLocalArtifactVersion(String pomPath) {
  final File pomFile = fs.file(pomPath);
  if (!pomFile.existsSync()) {
    throwToolExit('The file $pomPath wasn\'t found in the local engine out directory.');
  }
  xml.XmlDocument document;
  try {
    document = xml.parse(pomFile.readAsStringSync());
  } on xml.XmlParserException {
    throwToolExit(
      'Error parsing $pomPath. Please ensure that this is a valid XML document.'
    );
  } on FileSystemException {
    throwToolExit(
      'Error reading $pomPath. Please ensure that you have read permission to this'
      'file and try again.');
  }
  final Iterable<xml.XmlElement> project = document.findElements('project');
  assert(project.isNotEmpty);
  for (xml.XmlElement versionElement in document.findAllElements('version')) {
    if (versionElement.parent == project.first) {
      return versionElement.text;
    }
  }
  throwToolExit('Error while parsing the <version> element from $pomPath');
  return null;
}

/// Returns the local Maven repository for a local engine build.
/// For example, if the engine is built locally at <home>/engine/src/out/android_release_unopt
/// This method generates symlinks in the temp directory to the engine artifacts
/// following the convention specified on https://maven.apache.org/pom.html#Repositories
Directory _getLocalEngineRepo({
  @required String engineOutPath,
  @required AndroidBuildInfo androidBuildInfo,
}) {
  assert(engineOutPath != null);
  assert(androidBuildInfo != null);

  final String abi = getEnumName(androidBuildInfo.targetArchs.first);
  final Directory localEngineRepo = fs.systemTempDirectory
    .createTempSync('flutter_tool_local_engine_repo.');

  // Remove the local engine repo before the tool exits.
  addShutdownHook(
    () => localEngineRepo.deleteSync(recursive: true),
    ShutdownStage.CLEANUP,
  );

  final String buildMode = androidBuildInfo.buildInfo.modeName;
  final String artifactVersion = _getLocalArtifactVersion(
    fs.path.join(
      engineOutPath,
      'flutter_embedding_$buildMode.pom',
    )
  );
  for (String artifact in const <String>['pom', 'jar']) {
    // The Android embedding artifacts.
    _createSymlink(
      fs.path.join(
        engineOutPath,
        'flutter_embedding_$buildMode.$artifact',
      ),
      fs.path.join(
        localEngineRepo.path,
        'io',
        'flutter',
        'flutter_embedding_$buildMode',
        artifactVersion,
        'flutter_embedding_$buildMode-$artifactVersion.$artifact',
      ),
    );
    // The engine artifacts (libflutter.so).
    _createSymlink(
      fs.path.join(
        engineOutPath,
        '${abi}_$buildMode.$artifact',
      ),
      fs.path.join(
        localEngineRepo.path,
        'io',
        'flutter',
        '${abi}_$buildMode',
        artifactVersion,
        '${abi}_$buildMode-$artifactVersion.$artifact',
      ),
    );
  }
  return localEngineRepo;
}
