// Copyright 2014 The Flutter 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:process/process.dart';
import 'package:unified_analytics/unified_analytics.dart';

import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/os.dart';
import '../base/platform.dart';
import '../base/utils.dart';
import '../base/version.dart';
import '../base/version_range.dart';
import '../build_info.dart';
import '../cache.dart';
import '../globals.dart' as globals;
import '../project.dart';
import 'android_sdk.dart';

// These are the versions used in the project templates.
//
// In general, Flutter aims to default to the latest stable version.
// However, this currently requires to migrate existing integration tests to the
// latest supported values.
//
// Please see the README before changing any of these values.

// See https://gradle.org/releases
const templateDefaultGradleVersion = '8.12';

// When bumping, also update:
//  * ndkVersion constant in this file
//  * ndkVersion in FlutterExtension in packages/flutter_tools/gradle/src/main/kotlin/FlutterExtension.kt
//  * AGP version constants in packages/flutter_tools/gradle/build.gradle.kts
//  * AGP test constants in packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt
// See https://mvnrepository.com/artifact/com.android.tools.build/gradle
const templateAndroidGradlePluginVersion = '8.9.1';
const templateAndroidGradlePluginVersionForModule = '8.9.1';

// See https://kotlinlang.org/docs/releases.html#release-details
const templateKotlinGradlePluginVersion = '2.1.0';

// The Flutter Gradle Plugin is only applied to app projects, and modules that
// are built from source using (`include_flutter.groovy`). The remaining
// projects are: plugins, and modules compiled as AARs. In modules, the
// ephemeral directory `.android` is always regenerated after `flutter pub get`,
// so new versions are picked up after a Flutter upgrade.
//
// Please see the README before changing any of these values.
const compileSdkVersion = '36';
const minSdkVersion = '24';
const targetSdkVersion = '36';
const ndkVersion = '27.0.12077973';

// Update these when new major versions of Java are supported by new Gradle
// versions that we support.
// Source of truth: https://docs.gradle.org/current/userguide/compatibility.html
const oneMajorVersionHigherJavaVersion = '26';

// Update this when new versions of Gradle come out including minor versions
// and should correspond to the maximum Gradle version we test in CI.
//
// Supported here means supported by the tooling for
// flutter analyze --suggestions and does not imply broader flutter support.
const maxKnownAndSupportedGradleVersion = '9.1.0';

// Update this with new KGP versions come out including minor versions.
//
// Supported here means supported by the tooling for
// flutter analyze --suggestions and does not imply broader flutter support.
const maxKnownAndSupportedKgpVersion = '2.2.20';

// Update this when new versions of AGP come out.
//
// Supported here means tooling is aware of this version's Java <-> AGP
// compatibility.
@visibleForTesting
const maxKnownAndSupportedAgpVersion = '9.0';

// Update this when new versions of AGP come out.
const maxKnownAgpVersion = '9.0';

// Supported here means tooling is aware of this versions
// Java <-> AGP compatibility and does not imply broader flutter support.
// For use in flutter see the code in:
// flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt
@visibleForTesting
const oldestConsideredAgpVersion = '3.3.0';

// Supported here means tooling is aware of this versions
// gradle compatibility and does not imply broader flutter support.
@visibleForTesting
const oldestConsideredGradleVersion = '4.10.1';

// Supported here means tooling is aware of this versions
// gradle/AGP compatibility and does not imply broader flutter support.
@visibleForTesting
const oldestDocumentedKgpCompatabilityVersion = '1.6.20';

// Oldest documented version of AGP that has a listed minimum
// compatible Java version.
const oldestDocumentedJavaAgpCompatibilityVersion = '4.2';

// Constant used in [_buildAndroidGradlePluginRegExp] and
// [_settingsAndroidGradlePluginRegExp] and [_kotlinGradlePluginRegExpFromId]
// to identify the version section.
const _versionGroupName = 'version';

// AGP can be defined in the dependencies block of [build.gradle] or [build.gradle.kts].
// Expected content (covers both classpath and compileOnly cases):
// Groovy DSL with single quotes - 'com.android.tools.build:gradle:{{agpVersion}}'
// Groovy DSL with double quotes - "com.android.tools.build:gradle:{{agpVersion}}"
// Kotlin DSL - ("com.android.tools.build.gradle:{{agpVersion}}")
// ?<version> is used to name the version group which helps with extraction.
final _androidGradlePluginRegExpFromDependencies = RegExp(
  r"""[^\/]*\s*((\bclasspath\b)|(\bcompileOnly\b))\s*\(?['"]com\.android\.tools\.build:gradle:(?<version>\d+(\.\d+){1,2})\)?""",
  multiLine: true,
);

// AGP can be defined in the plugins block of [build.gradle],
// [build.gradle.kts], [settings.gradle], or [settings.gradle.kts].
// Expected content:
// Groovy DSL with single quotes - id 'com.android.application' version '{{agpVersion}}'
// Groovy DSL with double quotes - id "com.android.application" version "{{agpVersion}}"
// Kotlin DSL - id("com.android.application") version "{{agpVersion}}"
// ?<version> is used to name the version group which helps with extraction.
final _androidGradlePluginRegExpFromId = RegExp(
  r"""[^\/]*s*id\s*\(?['"]com\.android\.application['"]\)?\s+version\s+['"](?<version>\d+(\.\d+){1,2})\)?""",
  multiLine: true,
);

// KGP is defined in several places this code only checks in plugins block
// of [settings.gradle] and [settings.gradle.kts].
// Expected content:
// Groovy DSL - id "org.jetbrains.kotlin.android" version "{{kgpVersion}}"
// Kotlin DSL - id("org.jetbrains.kotlin.android") version "{{kgpVersion}}"
// ?<version> is used to name the version group which helps with extraction.
final _kotlinGradlePluginRegExpFromId = RegExp(
  r"""[^\/]*s*id\s*\(?['"]org\.jetbrains\.kotlin\.android['"]\)?\s+version\s+['"](?<version>\d+(\.\d+){1,2})\)?""",
  multiLine: true,
);

// Expected content format (with lines above and below).
// Version can have 2 or 3 numbers.
// 'distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip'
// '^\s*' protects against commented out lines.
final distributionUrlRegex = RegExp(r'^\s*distributionUrl\s*=\s*.*\.zip', multiLine: true);

// Modified version of the gradle distribution url match designed to only match
// gradle.org urls so that we can guarantee any modifications to the url
// still points to a hosted zip.
final gradleOrgVersionMatch = RegExp(
  r'^\s*distributionUrl\s*=\s*https\\://services\.gradle\.org/distributions/gradle-((?:\d|\.)+)-(.*)\.zip',
  multiLine: true,
);

// This matches uncommented minSdkVersion lines in the module-level build.gradle
// file which have minSdkVersion 16, 17, 18, 19, 20, 21, 22, 23 set with space sytax,
// equals syntax and when using minSdk or minSdkVersion.
// Matches uncommented minSdkVersion lines using equals syntax (=)
final tooOldMinSdkVersionEqualsMatch = RegExp(
  r'(?<=^\s*)minSdk(Version)?\s*=\s*(1[6789]|2[0123])(?=\s*(?://|$))',
  multiLine: true,
);

// Matches uncommented minSdkVersion lines using space syntax (no =)
final tooOldMinSdkVersionSpaceMatch = RegExp(
  r'(?<=^\s*)minSdk(Version)?\s+(1[6789]|2[0123])(?=\s*(?://|$))',
  multiLine: true,
);

// From https://docs.gradle.org/current/userguide/command_line_interface.html#command_line_interface
// Flag to print the versions for gradle, kotlin dsl, groovy, etc.
const gradleVersionsFlag = r'--version';

// Directory under android/ that gradle uses to store gradle information.
// Regularly used with [gradleWrapperDirectory] and
// [gradleWrapperPropertiesFilename].
// Different from the directory of gradle files stored in
// `_cache.getArtifactDirectory('gradle_wrapper')`
const gradleDirectoryName = 'gradle';
const gradleWrapperDirectoryName = 'wrapper';
const gradleWrapperPropertiesFilename = 'gradle-wrapper.properties';

/// Provides utilities to run a Gradle task, such as finding the Gradle executable
/// or constructing a Gradle project.
class GradleUtils {
  GradleUtils({
    required Platform platform,
    required Logger logger,
    required Cache cache,
    required OperatingSystemUtils operatingSystemUtils,
  }) : _platform = platform,
       _logger = logger,
       _cache = cache,
       _operatingSystemUtils = operatingSystemUtils;

  final Cache _cache;
  final Platform _platform;
  final Logger _logger;
  final OperatingSystemUtils _operatingSystemUtils;

  /// 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;
    injectGradleWrapperIfNeeded(androidDir);

    final File gradle = androidDir.childFile(getGradlewFileName(_platform));

    if (gradle.existsSync()) {
      _logger.printTrace('Using gradle from ${gradle.absolute.path}.');
      // If the Gradle executable doesn't have execute permission,
      // then attempt to set it.
      _operatingSystemUtils.makeExecutable(gradle);
      return gradle.absolute.path;
    }
    throwToolExit(
      'Unable to locate gradlew script. Please check that ${gradle.path} '
      'exists or that ${gradle.dirname} can be read.',
    );
  }

  /// Injects the Gradle wrapper files if any of these files don't exist in [directory].
  void injectGradleWrapperIfNeeded(Directory directory) {
    copyDirectory(
      _cache.getArtifactDirectory('gradle_wrapper'),
      directory,
      shouldCopyFile: (File sourceFile, File destinationFile) {
        // Don't override the existing files in the project.
        return !destinationFile.existsSync();
      },
      onFileCopied: (File source, File dest) {
        _operatingSystemUtils.makeExecutable(dest);
      },
    );
    // Add the `gradle-wrapper.properties` file if it doesn't exist.
    final Directory propertiesDirectory = directory
        .childDirectory(gradleDirectoryName)
        .childDirectory(gradleWrapperDirectoryName);
    final File propertiesFile = propertiesDirectory.childFile(gradleWrapperPropertiesFilename);

    if (propertiesFile.existsSync()) {
      return;
    }
    propertiesDirectory.createSync(recursive: true);
    final String gradleVersion = getGradleVersionForAndroidPlugin(directory, _logger);
    final propertyContents =
        '''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersion-all.zip
''';
    propertiesFile.writeAsStringSync(propertyContents);
  }
}

/// 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`,
/// `build.gradle.kts`, `settings.gradle`, or `settings.gradle.kts` file within
/// the project's Android directory.
String getGradleVersionForAndroidPlugin(Directory directory, Logger logger) {
  final String? androidPluginVersion = getAgpVersion(directory, logger);
  if (androidPluginVersion == null) {
    logger.printTrace(
      'AGP version cannot be determined, assuming Gradle version: $templateDefaultGradleVersion',
    );
    return templateDefaultGradleVersion;
  }
  return getGradleVersionFor(androidPluginVersion);
}

/// Returns the gradle file from the top level directory.
/// The returned file is not guaranteed to be present.
File getGradleWrapperFile(Directory directory) {
  return directory
      .childDirectory(gradleDirectoryName)
      .childDirectory(gradleWrapperDirectoryName)
      .childFile(gradleWrapperPropertiesFilename);
}

/// Parses the gradle wrapper distribution url to return a string containing
/// the version number.
///
/// Expected input is of the form '...gradle-7.4.2-all.zip', and the output
/// would be of the form '7.4.2'.
String? parseGradleVersionFromDistributionUrl(String? distributionUrl) {
  if (distributionUrl == null) {
    return null;
  }
  final List<String> zipParts = distributionUrl.split('-');
  if (zipParts.length < 2) {
    return null;
  }
  return zipParts[1];
}

/// Returns either the gradle-wrapper.properties value from the passed in
/// [directory] or if not present the version available in local path.
///
/// If gradle version is not found null is returned.
/// [directory] should be an android directory with a build.gradle file.
Future<String?> getGradleVersion(
  Directory directory,
  Logger logger,
  ProcessManager processManager,
) async {
  final File propertiesFile = getGradleWrapperFile(directory);

  if (propertiesFile.existsSync()) {
    final String wrapperFileContent = propertiesFile.readAsStringSync();

    final RegExpMatch? distributionUrl = distributionUrlRegex.firstMatch(wrapperFileContent);
    if (distributionUrl != null) {
      final String? gradleVersion = parseGradleVersionFromDistributionUrl(distributionUrl.group(0));
      if (gradleVersion != null) {
        return gradleVersion;
      } else {
        // Did not find gradle zip url. Likely this is a bug in our parsing.
        logger.printWarning(_formatParseWarning(wrapperFileContent, type: 'gradle'));
      }
    } else {
      // If no distributionUrl log then treat as if there was no propertiesFile.
      logger.printTrace(
        '$propertiesFile does not provide a Gradle version falling back to system gradle.',
      );
    }
  } else {
    // Could not find properties file.
    logger.printTrace('$propertiesFile does not exist falling back to system gradle');
  }
  // System installed Gradle version.
  // TODO(reidbaker): Modify this gradle execution to use gradlew.
  if (processManager.canRun('gradle')) {
    final gradleVersionsVerbose =
        (await processManager.run(<String>['gradle', gradleVersionsFlag])).stdout as String;
    // Expected format:
    /*

------------------------------------------------------------
Gradle 7.6
------------------------------------------------------------

Build time:   2022-11-25 13:35:10 UTC
Revision:     daece9dbc5b79370cc8e4fd6fe4b2cd400e150a8

Kotlin:       1.7.10
Groovy:       3.0.13
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          17.0.6 (Homebrew 17.0.6+0)
OS:           Mac OS X 13.2.1 aarch64
    */
    // Observation shows that the version can have 2 or 3 numbers.
    // Inner parentheticals `(\.\d+)?` denote the optional third value.
    // Outer parentheticals `Gradle (...)` denote a grouping used to extract
    // the version number.
    final gradleVersionRegex = RegExp(r'Gradle\s+(\d+\.\d+(?:\.\d+)?)');
    final RegExpMatch? version = gradleVersionRegex.firstMatch(gradleVersionsVerbose);
    if (version == null) {
      // Most likely a bug in our parse implementation/regex.
      logger.printWarning(_formatParseWarning(gradleVersionsVerbose, type: 'gradle'));
      return null;
    }
    return version.group(1);
  } else {
    logger.printTrace('Could not run system gradle');
    return null;
  }
}

/// Returns the Kotlin Gradle Plugin (KGP) version that the current project
/// depends on if found, `null` otherwise.
/// [androidDirectory] should be an android directory with a `build.gradle` file.
Future<String?> getKgpVersion(
  Directory androidDirectory,
  Logger logger,
  ProcessManager processManager,
) async {
  // Maintainers of the kotlin dsl and the kotlin gradle plugin are different.
  //
  // Android Docs refer to the kotlin gradle plugin with either the full name or KGP.
  // Kotlin docs refer to the kotlin gradle plugin as kotlin android plugin.
  //
  // gradle --version or ./gradlew --version will print the kotlin dsl version.
  // This version normally changes with the version of gradle.
  // https://github.com/gradle/gradle/blob/cefbee263181a924ac4efcaace6bda97a55bc0f7/platforms/core-runtime/gradle-cli/src/main/java/org/gradle/launcher/cli/DefaultCommandLineActionFactory.java#L260
  // This vesion is NOT the version of KGP that the project uses.
  //
  // Instead the kgpVersion task is a custom flutter task dynamiclly added that can
  // print the kgp version if gradle can run successfuly.

  if (processManager.canRun('./gradlew', workingDirectory: androidDirectory.path)) {
    final ProcessResult command = await processManager.run(<String>[
      './gradlew',
      'kgpVersion',
      '-q',
    ], workingDirectory: androidDirectory.path);
    if (command.exitCode == 0) {
      final kgpVersionOutput = command.stdout as String;

      // See expected output defined in
      // flutter/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt addTaskForKGPVersion
      final kotlinVersionRegex = RegExp(r'KGP Version:\s+(\d+\.\d+(?:\.\d+)?)');
      final RegExpMatch? version = kotlinVersionRegex.firstMatch(kgpVersionOutput);
      if (version != null) {
        return version.group(1);
      }
      // Most likely a bug in our parse implementation/regex.
      logger.printWarning(_formatParseWarning(kgpVersionOutput, type: 'kotlin'));
    } else {
      logger.printTrace('Non zero exit code from gradle task kgpVersion.');
    }
  } else {
    logger.printTrace('Could not run gradle task kgpVersion.');
  }

  // Project valiation code is regularly run on projects that can not build.
  // Because of that this code also attempts to search through known template
  // locations for kotlin versions.

  logger.printTrace('Checking settings for kgp version.');
  File settingsFile = androidDirectory.childFile('settings.gradle');
  if (!settingsFile.existsSync()) {
    settingsFile = androidDirectory.childFile('settings.gradle.kts');
  }

  if (settingsFile.existsSync()) {
    final String settingsFileContent = settingsFile.readAsStringSync();
    final RegExpMatch? settingsMatch = _kotlinGradlePluginRegExpFromId.firstMatch(
      settingsFileContent,
    );

    if (settingsMatch != null) {
      final String? kgpVersion = settingsMatch.namedGroup(_versionGroupName);
      logger.printTrace('$settingsFile provides KGP version: $kgpVersion');
      return kgpVersion;
    }
  } else {
    logger.printTrace('No settings.gradle.kts');
  }

  return null;
}

/// Returns the Android Gradle Plugin (AGP) version that the current project
/// depends on when found, null otherwise.
///
/// The Android plugin version is specified in the `build.gradle`,
/// `build.gradle.kts`, `settings.gradle, or `settings.gradle.kts`
/// files within the project's Android directory ([androidDirectory]).
String? getAgpVersion(Directory androidDirectory, Logger logger) {
  File buildFile = androidDirectory.childFile('build.gradle');
  if (!buildFile.existsSync()) {
    buildFile = androidDirectory.childFile('build.gradle.kts');
  }
  if (!buildFile.existsSync()) {
    logger.printTrace('Cannot find build.gradle/build.gradle.kts in $androidDirectory');
    return null;
  }
  final String buildFileContent = buildFile.readAsStringSync();
  final RegExpMatch? buildMatchClasspath = _androidGradlePluginRegExpFromDependencies.firstMatch(
    buildFileContent,
  );
  if (buildMatchClasspath != null) {
    final String? androidPluginVersion = buildMatchClasspath.namedGroup(_versionGroupName);
    logger.printTrace('$buildFile provides AGP version from classpath: $androidPluginVersion');
    return androidPluginVersion;
  }
  final RegExpMatch? buildMatchId = _androidGradlePluginRegExpFromId.firstMatch(buildFileContent);
  if (buildMatchId != null) {
    final String? androidPluginVersion = buildMatchId.namedGroup(_versionGroupName);
    logger.printTrace('$buildFile provides AGP version from plugin id: $androidPluginVersion');
    return androidPluginVersion;
  }

  logger.printTrace("$buildFile doesn't provide an AGP version. Checking settings.");
  File settingsFile = androidDirectory.childFile('settings.gradle');
  if (!settingsFile.existsSync()) {
    settingsFile = androidDirectory.childFile('settings.gradle.kts');
  }
  if (!settingsFile.existsSync()) {
    logger.printTrace('Cannot find settings.gradle/settings.gradle.kts in $androidDirectory');
    return null;
  }
  final String settingsFileContent = settingsFile.readAsStringSync();
  final RegExpMatch? settingsMatch = _androidGradlePluginRegExpFromId.firstMatch(
    settingsFileContent,
  );

  if (settingsMatch != null) {
    final String? androidPluginVersion = settingsMatch.namedGroup(_versionGroupName);
    logger.printTrace('$settingsFile provides AGP version: $androidPluginVersion');
    return androidPluginVersion;
  }
  logger.printTrace("$settingsFile doesn't provide an AGP version.");
  return null;
}

String _formatParseWarning(String content, {required String type}) {
  return 'Could not parse $type version from: \n'
      '$content \n'
      'If there is a version please look for an existing bug '
      'https://github.com/flutter/flutter/issues/'
      ' and if one does not exist file a new issue.';
}

// Validate that KGP and Gradle are compatible with each other.
//
// Returns true if versions are compatible.
// Null or empty Gradle or KGP version returns false.
// If compatibility cannot be evaluated returns false.
// If versions are newer than the max known version a warning is logged and true
// returned.
//
// Source of truth found here:
// https://kotlinlang.org/docs/gradle-configure-project.html#apply-the-plugin
bool validateGradleAndKGP(Logger logger, {required String? kgpV, required String? gradleV}) {
  if (gradleV == null || kgpV == null || gradleV.isEmpty || kgpV.isEmpty) {
    logger.printTrace('Gradle or KGP version unknown ($gradleV, $kgpV).');
    return false;
  }

  if (isWithinVersionRange(gradleV, min: '0.0', max: oldestConsideredGradleVersion)) {
    logger.printTrace(
      'Gradle version $gradleV older than oldest considered $oldestConsideredGradleVersion',
    );
    return false;
  }

  if (isWithinVersionRange(
    kgpV,
    min: maxKnownAndSupportedKgpVersion,
    max: '100.100',
    inclusiveMin: false,
  )) {
    logger.printTrace(
      'Newer than known KGP version ($kgpV), gradle ($gradleV).'
      '\n Treating as valid configuration.',
    );
    return true;
  }

  // https://kotlinlang.org/docs/gradle-configure-project.html#apply-the-plugin
  // Documenation is non continuous, past versions are known to the
  // publishers of KGP. When covering version ranges beyond what is documented
  // add a comment with the documented value.
  // Continuous KGP version handling is prefered in case an emergency patch to a
  // past release is shipped this code will assume the version range that is closest.

  // Documented max is 2.20, using 2.2.29 covers patch versions.
  if (isWithinVersionRange(kgpV, min: '2.2.20', max: '2.2.29')) {
    // Documented max is 8.14, using 8.14.99 non inclusive covers patch versions.
    return isWithinVersionRange(gradleV, min: '7.6.3', max: '8.14.99', inclusiveMax: false);
  }
  if (isWithinVersionRange(kgpV, min: '2.2.0', max: '2.2.10')) {
    // Documented max is 8.14, using 8.14.99 non inclusive covers patch versions.
    return isWithinVersionRange(gradleV, min: '7.6.3', max: '8.14.99', inclusiveMax: false);
  }
  if (isWithinVersionRange(kgpV, min: '2.1.20', max: '2.1.21')) {
    // Documented max is 8.12.1, using 8.12.99 non inclusive covers patch versions.
    return isWithinVersionRange(gradleV, min: '7.6.3', max: '8.12.99', inclusiveMax: false);
  }
  if (isWithinVersionRange(kgpV, min: '2.1.0', max: '2.1.10')) {
    // Documented max is 8.10, using 8.11 non inclusive covers patch versions.
    return isWithinVersionRange(gradleV, min: '7.6.3', max: '8.11', inclusiveMax: false);
  }
  // Documented max is 2.0.21.
  if (isWithinVersionRange(kgpV, min: '2.0.20', max: '2.1', inclusiveMax: false)) {
    // Documented max is 8.5, using 8.9 non inclusive covers patch versions.
    // Kotlin Multiplatform can throw warnings on 8.8.
    return isWithinVersionRange(gradleV, min: '6.8.3', max: '8.9', inclusiveMax: false);
  }
  if (isWithinVersionRange(kgpV, min: '2.0', max: '2.0.20', inclusiveMax: false)) {
    // Documented max is 8.5, using 8.6 non inclusive covers patch versions.
    return isWithinVersionRange(gradleV, min: '6.8.3', max: '8.6', inclusiveMax: false);
  }
  // Documented max is 1.9.25.
  if (isWithinVersionRange(kgpV, min: '1.9.20', max: '2.0', inclusiveMax: false)) {
    return isWithinVersionRange(gradleV, min: '6.8.3', max: '8.1.1');
  }
  // Documented max is 1.9.10.
  if (isWithinVersionRange(kgpV, min: '1.8.20', max: '1.9.20', inclusiveMax: false)) {
    return isWithinVersionRange(gradleV, min: '6.8.3', max: '7.6.0');
  }
  // Documented max is 1.8.11.
  if (isWithinVersionRange(kgpV, min: '1.8.0', max: '1.8.20', inclusiveMax: false)) {
    return isWithinVersionRange(gradleV, min: '6.8.3', max: '7.3.3');
  }
  // Documented max is 1.7.22.
  if (isWithinVersionRange(kgpV, min: '1.7.20', max: '1.8.0', inclusiveMax: false)) {
    return isWithinVersionRange(gradleV, min: '6.7.1', max: '7.1.1');
  }
  // Documented max is 1.7.10.
  if (isWithinVersionRange(kgpV, min: '1.7.0', max: '1.7.20', inclusiveMax: false)) {
    return isWithinVersionRange(gradleV, min: '6.7.1', max: '7.0.2');
  }
  // Documented max is 1.6.21.
  if (isWithinVersionRange(
    kgpV,
    min: oldestDocumentedKgpCompatabilityVersion,
    max: '1.7.0',
    inclusiveMax: false,
  )) {
    return isWithinVersionRange(gradleV, min: '6.1.1', max: '7.0.2');
  }

  logger.printTrace('Unknown KGP-Gradle compatibility, KGP: $kgpV, Gradle: $gradleV');
  return false;
}

// Validate that KGP and AGP are compatible with each other.
//
// Returns true if versions are compatible.
// Null or empty KGP or AGP version returns false.
// If compatibility cannot be evaluated returns false.
// If versions are newer than the max known version a warning is logged and true
// returned.
//
// Source of truth found here:
// https://kotlinlang.org/docs/gradle-configure-project.html#apply-the-plugin
bool validateAgpAndKgp(Logger logger, {required String? kgpV, required String? agpV}) {
  if (agpV == null || kgpV == null || agpV.isEmpty || kgpV.isEmpty) {
    logger.printTrace('KGP or AGP version unknown ($kgpV, $agpV).');
    return false;
  }

  if (isWithinVersionRange(agpV, min: '0.0', max: oldestConsideredAgpVersion)) {
    logger.printTrace(
      'AGP version ($agpV) older than oldest supported $oldestConsideredAgpVersion.',
    );
  }
  const maxKnownAgpVersionWithFullKotinSupport = '8.7.2';

  if (isWithinVersionRange(
        kgpV,
        min: maxKnownAndSupportedKgpVersion,
        max: '100.100',
        inclusiveMin: false,
      ) ||
      isWithinVersionRange(
        agpV,
        min: maxKnownAgpVersionWithFullKotinSupport,
        max: '100.100',
        inclusiveMin: false,
      )) {
    logger.printTrace(
      'Newer than known KGP version ($kgpV), AGP ($agpV).'
      '\n Treating as valid configuration.',
    );
    return true;
  }

  // https://kotlinlang.org/docs/gradle-configure-project.html#apply-the-plugin
  // Documenation is non continuous, past versions are known to the
  // publishers of KGP. When covering version ranges beyond what is documented
  // add a comment with the documented value.
  // Continuous KGP version handling is prefered in case an emergency patch to a
  // past release is shipped this code will assume the version range that is closest.

  // Documented max is 2.2.20
  if (isWithinVersionRange(kgpV, min: '2.2.20', max: '2.2.29')) {
    // Documented max is 8.11.1
    return isWithinVersionRange(agpV, min: '7.3.1', max: '8.12', inclusiveMax: false);
  }
  // Documented max is 2.2.10
  if (isWithinVersionRange(kgpV, min: '2.2.0', max: '2.2.19')) {
    // Documented max is 8.10.0
    return isWithinVersionRange(agpV, min: '7.3.1', max: '8.11', inclusiveMax: false);
  }
  // Documented max is 2.1.21
  if (isWithinVersionRange(kgpV, min: '2.1.0', max: '2.1.21')) {
    return isWithinVersionRange(agpV, min: '7.3.1', max: '8.7.2');
  }
  // Documented max is 2.0.21
  if (isWithinVersionRange(kgpV, min: '2.0.20', max: '2.1.0', inclusiveMax: false)) {
    // Documented max is 8.5.
    return isWithinVersionRange(agpV, min: '7.1.3', max: '8.6', inclusiveMax: false);
  }
  // Documented max is 2.0.0.
  if (isWithinVersionRange(kgpV, min: '2.0.0', max: '2.0.20', inclusiveMax: false)) {
    return isWithinVersionRange(agpV, min: '7.1.3', max: '8.3.1');
  }
  // Documented max is 1.9.25
  if (isWithinVersionRange(kgpV, min: '1.9.20', max: '2.0.0', inclusiveMax: false)) {
    return isWithinVersionRange(agpV, min: '4.2.2', max: '8.1.0');
  }
  // Documented max is 1.9.10
  if (isWithinVersionRange(kgpV, min: '1.9.0', max: '1.9.20', inclusiveMax: false)) {
    return isWithinVersionRange(agpV, min: '4.2.2', max: '7.4.0');
  }
  // Documented max is 1.8.22
  if (isWithinVersionRange(kgpV, min: '1.8.20', max: '1.9', inclusiveMax: false)) {
    return isWithinVersionRange(agpV, min: '4.1.3', max: '7.4.0');
  }
  // Documented max is 1.8.11
  if (isWithinVersionRange(kgpV, min: '1.8.0', max: '1.8.20', inclusiveMax: false)) {
    return isWithinVersionRange(agpV, min: '4.1.3', max: '7.2.1');
  }
  // Documented max is 1.7.22
  if (isWithinVersionRange(kgpV, min: '1.7.20', max: '1.8.0', inclusiveMax: false)) {
    return isWithinVersionRange(agpV, min: '3.6.4', max: '7.0.4');
  }
  // Documented max is 1.7.10
  // Documented gap between 1.6.21 and 1.7.0.
  if (isWithinVersionRange(kgpV, min: '1.6.20', max: '1.7.20', inclusiveMax: false)) {
    return isWithinVersionRange(agpV, min: '3.4.3', max: '7.0.2');
  }

  logger.printTrace('Unknown KGP-Gradle compatibility, KGP: $kgpV, AGP: $agpV');
  return false;
}

// Validate that Gradle version and AGP are compatible with each other.
//
// Returns true if versions are compatible.
// Null Gradle version or AGP version returns false.
// If compatibility cannot be evaluated returns false.
// If versions are newer than the max known version a warning is logged and true
// returned.
//
// Source of truth found here:
// https://developer.android.com/studio/releases/gradle-plugin#updating-gradle
// AGP has a minimum version of gradle required but no max starting at
// AGP version 2.3.0+.
bool validateGradleAndAgp(Logger logger, {required String? gradleV, required String? agpV}) {
  if (gradleV == null || agpV == null) {
    logger.printTrace('Gradle version or AGP version unknown ($gradleV, $agpV).');
    return false;
  }

  // First check if versions are too old.
  if (isWithinVersionRange(
    agpV,
    min: '0.0',
    max: oldestConsideredAgpVersion,
    inclusiveMax: false,
  )) {
    logger.printTrace('AGP Version: $agpV is too old.');
    return false;
  }
  if (isWithinVersionRange(
    gradleV,
    min: '0.0',
    max: oldestConsideredGradleVersion,
    inclusiveMax: false,
  )) {
    logger.printTrace('Gradle Version: $gradleV is too old.');
    return false;
  }

  // Check highest supported version before checking unknown versions.
  // https://developer.android.com/build/releases/agp-preview#android-gradle-plugin-compatibility
  if (isWithinVersionRange(agpV, min: '9.0', max: '9.0.99')) {
    return isWithinVersionRange(gradleV, min: '9.0', max: maxKnownAndSupportedGradleVersion);
  }
  // Check if versions are newer than the max known versions.
  if (isWithinVersionRange(agpV, min: maxKnownAndSupportedAgpVersion, max: '100.100')) {
    // Assume versions we do not know about are valid but log.
    final bool validGradle = isWithinVersionRange(gradleV, min: '9.1.0', max: '100.00');
    logger.printTrace(
      'Newer than known AGP version ($agpV), gradle ($gradleV).'
      '\n Treating as valid configuration.',
    );
    return validGradle;
  }

  // Begin Known Gradle <-> AGP validation.
  if (isWithinVersionRange(agpV, min: '8.13.0', max: '8.13.99')) {
    return isWithinVersionRange(gradleV, min: '8.13', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '8.12.0', max: '8.12.99')) {
    return isWithinVersionRange(gradleV, min: '8.13', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '8.11.0', max: '8.11.99')) {
    return isWithinVersionRange(gradleV, min: '8.13', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '8.10.0', max: '8.10.99')) {
    return isWithinVersionRange(gradleV, min: '8.11.1', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '8.9.0', max: '8.9.99')) {
    return isWithinVersionRange(gradleV, min: '8.11.1', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '8.8.0', max: '8.8.99')) {
    return isWithinVersionRange(gradleV, min: '8.10.2', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '8.7.0', max: '8.7.99')) {
    return isWithinVersionRange(gradleV, min: '8.9', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '8.6.0', max: '8.6.99')) {
    return isWithinVersionRange(gradleV, min: '8.7', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '8.5.0', max: '8.5.99')) {
    return isWithinVersionRange(gradleV, min: '8.7', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '8.4.0', max: '8.4.99')) {
    return isWithinVersionRange(gradleV, min: '8.6', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '8.3.0', max: '8.3.99')) {
    return isWithinVersionRange(gradleV, min: '8.4', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '8.2.0', max: '8.2.99')) {
    return isWithinVersionRange(gradleV, min: '8.2', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '8.0.0', max: '8.1.99')) {
    return isWithinVersionRange(gradleV, min: '8.0', max: maxKnownAndSupportedGradleVersion);
  }
  // Max agp here is a made up version to contain all 7.4 changes.
  if (isWithinVersionRange(agpV, min: '7.4', max: '7.5')) {
    return isWithinVersionRange(gradleV, min: '7.5', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '7.3', max: '7.4', inclusiveMax: false)) {
    return isWithinVersionRange(gradleV, min: '7.4', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '7.2', max: '7.3', inclusiveMax: false)) {
    return isWithinVersionRange(gradleV, min: '7.3.3', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '7.1', max: '7.2', inclusiveMax: false)) {
    return isWithinVersionRange(gradleV, min: '7.2', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '7.0', max: '7.1', inclusiveMax: false)) {
    return isWithinVersionRange(gradleV, min: '7.0', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '4.2.0', max: '7.0', inclusiveMax: false)) {
    return isWithinVersionRange(gradleV, min: '6.7.1', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '4.1.0', max: '4.2.0', inclusiveMax: false)) {
    return isWithinVersionRange(gradleV, min: '6.5', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '4.0.0', max: '4.1.0', inclusiveMax: false)) {
    return isWithinVersionRange(gradleV, min: '6.1.1', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '3.6.0', max: '3.6.4')) {
    return isWithinVersionRange(gradleV, min: '5.6.4', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '3.5.0', max: '3.5.4')) {
    return isWithinVersionRange(gradleV, min: '5.4.1', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '3.4.0', max: '3.4.3')) {
    return isWithinVersionRange(gradleV, min: '5.1.1', max: maxKnownAndSupportedGradleVersion);
  }
  if (isWithinVersionRange(agpV, min: '3.3.0', max: '3.3.3')) {
    return isWithinVersionRange(gradleV, min: '4.10.1', max: maxKnownAndSupportedGradleVersion);
  }

  logger.printTrace('Unknown Gradle-AGP compatibility, Gradle: $gradleV, AGP: $agpV');
  return false;
}

/// Validate that the [javaVersion] and [gradleVersion] are compatible with
/// each other.
///
/// Source of truth:
/// https://docs.gradle.org/current/userguide/compatibility.html#java
bool validateJavaAndGradle(
  Logger logger, {
  required String? javaVersion,
  required String? gradleVersion,
}) {
  // https://docs.gradle.org/current/userguide/compatibility.html#java
  const oldestConsideredJavaVersion = '1.8';
  const oldestDocumentedJavaGradleCompatibility = '2.0';

  // Begin Java <-> Gradle validation.

  if (javaVersion == null || gradleVersion == null) {
    logger.printTrace('Java version or Gradle version unknown ($javaVersion, $gradleVersion).');
    return false;
  }

  // First check if versions are too old.
  if (isWithinVersionRange(
    javaVersion,
    min: '1.1',
    max: oldestConsideredJavaVersion,
    inclusiveMax: false,
  )) {
    logger.printTrace('Java Version: $javaVersion is too old.');
    return false;
  }
  if (isWithinVersionRange(
    gradleVersion,
    min: '0.0',
    max: oldestDocumentedJavaGradleCompatibility,
    inclusiveMax: false,
  )) {
    logger.printTrace('Gradle Version: $gradleVersion is too old.');
    return false;
  }

  // Check if versions are newer than the max supported versions.
  if (isWithinVersionRange(javaVersion, min: oneMajorVersionHigherJavaVersion, max: '100.100')) {
    // Assume versions Java versions newer than [maxSupportedJavaVersion]
    // required a higher gradle version.
    final bool validGradle = isWithinVersionRange(
      gradleVersion,
      min: maxKnownAndSupportedGradleVersion,
      max: '100.00',
    );
    logger.printWarning(
      'Newer than known valid Java version ($javaVersion), gradle ($gradleVersion).'
      '\n Treating as valid configuration.',
    );
    return validGradle;
  }

  // Begin known Java <-> Gradle evaluation.
  for (final JavaGradleCompat data in _javaGradleCompatList) {
    if (isWithinVersionRange(
      javaVersion,
      min: data.javaMin,
      max: data.javaMax,
      inclusiveMax: false,
    )) {
      // Null max value should be treated as unbounded on the max side.
      return isWithinVersionRange(
        gradleVersion,
        min: data.gradleMin,
        max: data.gradleMax ?? '100.100',
      );
    }
  }

  logger.printTrace(
    'Unknown Java-Gradle compatibility, Java: $javaVersion, Gradle: $gradleVersion',
  );
  return false;
}

/// Returns compatibility information for the valid range of Gradle versions for
/// the specified Java version.
///
/// Returns null when the tooling has not documented the compatible Gradle
/// versions for the Java version (either the version is too old or too new). If
/// this seems like a mistake, the caller may need to update the
/// [_javaGradleCompatList] detailing Java/Gradle compatibility.
JavaGradleCompat? getValidGradleVersionRangeForJavaVersion(Logger logger, {required String javaV}) {
  for (final JavaGradleCompat data in _javaGradleCompatList) {
    if (isWithinVersionRange(javaV, min: data.javaMin, max: data.javaMax, inclusiveMax: false)) {
      return data;
    }
  }

  logger.printTrace('Unable to determine valid Gradle version range for Java version $javaV.');
  return null;
}

/// Validate that the specified Java and Android Gradle Plugin (AGP) versions are
/// compatible with each other.
///
/// Returns true when the specified Java and AGP versions are
/// definitely compatible; True if AGP is newer than known, otherwise, false. In addition,
/// this will return false when either a null Java or AGP version is provided.
///
/// Source of truth are the AGP release notes:
/// https://developer.android.com/build/releases/gradle-plugin
bool validateJavaAndAgp(Logger logger, {required String? javaV, required String? agpV}) {
  if (javaV == null || agpV == null) {
    logger.printTrace('Java version or AGP version unknown ($javaV, $agpV).');
    return false;
  }

  // Check if AGP version is too old to perform validation.
  if (isWithinVersionRange(
    agpV,
    min: '1.0',
    max: oldestDocumentedJavaAgpCompatibilityVersion,
    inclusiveMax: false,
  )) {
    logger.printTrace('AGP Version: $agpV is too old to determine Java compatibility.');
    return false;
  }

  if (isWithinVersionRange(
    agpV,
    min: maxKnownAndSupportedAgpVersion,
    max: '100.100',
    inclusiveMin: false,
  )) {
    logger.printTrace('AGP Version: $agpV is too new to determine Java compatibility.');
    return true;
  }

  // Begin known Java <-> AGP evaluation.
  for (final JavaAgpCompat data in _javaAgpCompatList) {
    if (isWithinVersionRange(agpV, min: data.agpMin, max: data.agpMax)) {
      return isWithinVersionRange(javaV, min: data.javaMin, max: '100.100');
    }
  }

  logger.printTrace('Unknown Java-AGP compatibility $javaV, $agpV');
  return false;
}

/// Returns compatibility information concerning the minimum AGP
/// version for the specified Java version.
JavaAgpCompat? getMinimumAgpVersionForJavaVersion(Logger logger, {required String javaV}) {
  for (final JavaAgpCompat data in _javaAgpCompatList) {
    if (isWithinVersionRange(javaV, min: data.javaMin, max: '100.100')) {
      return data;
    }
  }

  logger.printTrace('Unable to determine minimum AGP version for specified Java version.');
  return null;
}

/// Returns valid Java range for specified Gradle and AGP versions.
///
/// Assumes that gradleV and agpV are compatible versions.
VersionRange getJavaVersionFor({required String gradleV, required String agpV}) {
  // Find minimum Java version based on AGP compatibility.
  String? minJavaVersion;
  for (final JavaAgpCompat data in _javaAgpCompatList) {
    if (isWithinVersionRange(agpV, min: data.agpMin, max: data.agpMax)) {
      minJavaVersion = data.javaMin;
    }
  }

  // Find maximum Java version based on Gradle compatibility.
  String? maxJavaVersion;
  for (final JavaGradleCompat data in _javaGradleCompatList.reversed) {
    if (isWithinVersionRange(
      gradleV,
      min: data.gradleMin,
      max: maxKnownAndSupportedGradleVersion,
    )) {
      maxJavaVersion = data.javaMax;
    }
  }

  return VersionRange(minJavaVersion, maxJavaVersion);
}

/// 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 agpV) {
  final compatList = <GradleForAgp>[
    GradleForAgp(agpMin: '1.0.0', agpMax: '1.1.3', minRequiredGradle: '2.3'),
    GradleForAgp(agpMin: '1.2.0', agpMax: '1.3.1', minRequiredGradle: '2.9'),
    GradleForAgp(agpMin: '1.5.0', agpMax: '1.5.0', minRequiredGradle: '2.2.1'),
    GradleForAgp(agpMin: '2.0.0', agpMax: '2.1.2', minRequiredGradle: '2.13'),
    GradleForAgp(agpMin: '2.1.3', agpMax: '2.2.3', minRequiredGradle: '2.14.1'),
    GradleForAgp(agpMin: '2.3.0', agpMax: '2.9.9', minRequiredGradle: '3.3'),
    GradleForAgp(agpMin: '3.0.0', agpMax: '3.0.9', minRequiredGradle: '4.1'),
    GradleForAgp(agpMin: '3.1.0', agpMax: '3.1.9', minRequiredGradle: '4.4'),
    GradleForAgp(agpMin: '3.2.0', agpMax: '3.2.1', minRequiredGradle: '4.6'),
    GradleForAgp(agpMin: '3.3.0', agpMax: '3.3.2', minRequiredGradle: '4.10.2'),
    GradleForAgp(agpMin: '3.4.0', agpMax: '3.5.0', minRequiredGradle: '5.6.2'),
    GradleForAgp(agpMin: '4.0.0', agpMax: '4.1.0', minRequiredGradle: '6.7'),
    // 7.5 is a made up value to include everything through 7.4.*
    GradleForAgp(agpMin: '7.0.0', agpMax: '7.5', minRequiredGradle: '7.5'),
    // Use 0 and 99 as a patch values to signify every AGP patch version with
    // that major and minor version.
    GradleForAgp(agpMin: '8.0.0', agpMax: '8.1.99', minRequiredGradle: '8.0'),
    GradleForAgp(agpMin: '8.2.0', agpMax: '8.2.99', minRequiredGradle: '8.2'),
    GradleForAgp(agpMin: '8.3.0', agpMax: '8.3.99', minRequiredGradle: '8.4'),
    GradleForAgp(agpMin: '8.4.0', agpMax: '8.4.99', minRequiredGradle: '8.6'),
    GradleForAgp(agpMin: '8.5.0', agpMax: '8.6.99', minRequiredGradle: '8.7'),
    GradleForAgp(agpMin: '8.7.0', agpMax: '8.7.99', minRequiredGradle: '8.9'),
    GradleForAgp(agpMin: '8.8.0', agpMax: '8.8.99', minRequiredGradle: '8.10.2'),
    GradleForAgp(agpMin: '8.9.0', agpMax: '8.9.99', minRequiredGradle: '8.11.1'),
    GradleForAgp(agpMin: '8.10.0', agpMax: '8.10.99', minRequiredGradle: '8.11.1'),
    GradleForAgp(agpMin: '8.11.0', agpMax: '8.11.99', minRequiredGradle: '8.13'),
    GradleForAgp(agpMin: '8.12.0', agpMax: '8.12.99', minRequiredGradle: '8.13'),
    GradleForAgp(agpMin: '8.13.0', agpMax: '8.13.99', minRequiredGradle: '8.13'),
    GradleForAgp(agpMin: '9.0', agpMax: '9.0.99', minRequiredGradle: '9.0.0'),
    // Assume if AGP is newer than this code knows about return the highest gradle
    // version we know about.
    GradleForAgp(
      agpMin: maxKnownAgpVersion,
      agpMax: maxKnownAgpVersion,
      minRequiredGradle: maxKnownAndSupportedGradleVersion,
    ),
  ];
  for (final data in compatList) {
    if (isWithinVersionRange(agpV, min: data.agpMin, max: data.agpMax)) {
      return data.minRequiredGradle;
    }
  }
  if (isWithinVersionRange(agpV, min: maxKnownAgpVersion, max: '100.100')) {
    return maxKnownAndSupportedGradleVersion;
  }
  throwToolExit('Unsupported Android Plugin version: $agpV.');
}

/// 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 && globals.androidSdk == null) {
    exitWithNoSdkMessage();
  }
  final File localProperties = project.android.localPropertiesFile;
  var 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;
  }

  final AndroidSdk? androidSdk = globals.androidSdk;
  if (androidSdk != null) {
    changeIfNecessary('sdk.dir', globals.fsUtils.escapePath(androidSdk.directory.path));
  }

  changeIfNecessary('flutter.sdk', globals.fsUtils.escapePath(Cache.flutterRoot!));
  if (buildInfo != null) {
    changeIfNecessary('flutter.buildMode', buildInfo.modeName);
    final String? buildName = validatedBuildNameForPlatform(
      TargetPlatform.android_arm,
      buildInfo.buildName ?? project.manifest.buildName,
      globals.logger,
    );
    changeIfNecessary('flutter.versionName', buildName);
    final String? buildNumber = validatedBuildNumberForPlatform(
      TargetPlatform.android_arm,
      buildInfo.buildNumber ?? project.manifest.buildNumber,
      globals.logger,
    );
    changeIfNecessary('flutter.versionCode', buildNumber);
  }

  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 settings = SettingsFile();
  final AndroidSdk? androidSdk = globals.androidSdk;
  if (androidSdk != null) {
    settings.values['sdk.dir'] = globals.fsUtils.escapePath(androidSdk.directory.path);
  }
  settings.writeContents(properties);
}

void exitWithNoSdkMessage() {
  globals.analytics.send(
    Event.flutterBuildInfo(
      label: 'unsupported-project',
      buildType: 'gradle',
      error: 'android-sdk-not-found',
    ),
  );
  throwToolExit(
    '${globals.logger.terminal.warningMark} No Android SDK found. '
    'Try setting the ANDROID_HOME environment variable.',
  );
}

// Data class to hold normal/defined Java <-> Gradle compatibility criteria.
//
// The [javaMax] is exclusive in terms of supporting the noted [gradleMin],
// whereas [javaMin] is inclusive.
// Gradle Max is not required and when unset should mean unbound.
@immutable
class JavaGradleCompat {
  const JavaGradleCompat({
    required this.javaMin,
    required this.javaMax,
    required this.gradleMin,
    this.gradleMax,
  });

  final String javaMin;
  final String javaMax;
  final String gradleMin;
  final String? gradleMax;

  @override
  bool operator ==(Object other) =>
      other is JavaGradleCompat &&
      other.javaMin == javaMin &&
      other.javaMax == javaMax &&
      other.gradleMin == gradleMin &&
      other.gradleMax == gradleMax;

  @override
  int get hashCode => Object.hash(javaMin, javaMax, gradleMin, gradleMax);

  @override
  String toString() {
    return '$javaMin, $javaMax, $gradleMin, $gradleMax';
  }
}

// Data class to hold defined Java <-> AGP compatibility criteria.
//
// The [agpMin] and [agpMax] are inclusive in terms of having the
// noted [javaMin] and [javaDefault] versions.
@immutable
class JavaAgpCompat {
  const JavaAgpCompat({
    required this.javaMin,
    required this.javaDefault,
    required this.agpMin,
    required this.agpMax,
  });

  final String javaMin;
  final String javaDefault;
  final String agpMin;
  final String agpMax;

  @override
  bool operator ==(Object other) =>
      other is JavaAgpCompat &&
      other.javaMin == javaMin &&
      other.javaDefault == javaDefault &&
      other.agpMin == agpMin &&
      other.agpMax == agpMax;

  @override
  int get hashCode => Object.hash(javaMin, javaDefault, agpMin, agpMax);
}

class GradleForAgp {
  GradleForAgp({required this.agpMin, required this.agpMax, required this.minRequiredGradle});

  final String agpMin;
  final String agpMax;
  final String minRequiredGradle;
}

// Returns gradlew file name based on the platform.
String getGradlewFileName(Platform platform) {
  if (platform.isWindows) {
    return 'gradlew.bat';
  } else {
    return 'gradlew';
  }
}

@visibleForTesting
const maxGradleVersionForJavaPre17 = '8.14.100';

/// List of compatible Java/Gradle versions.
///
/// Should be updated when a new version of Java is supported by a new version
/// of Gradle, as https://docs.gradle.org/current/userguide/compatibility.html
/// details.
var _javaGradleCompatList = const <JavaGradleCompat>[
  JavaGradleCompat(javaMin: '25', javaMax: '26', gradleMin: '9.1.0'),
  JavaGradleCompat(javaMin: '24', javaMax: '25', gradleMin: '8.14'),
  JavaGradleCompat(javaMin: '23', javaMax: '24', gradleMin: '8.10'),
  JavaGradleCompat(javaMin: '22', javaMax: '23', gradleMin: '8.7'),
  JavaGradleCompat(javaMin: '21', javaMax: '22', gradleMin: '8.4'),
  JavaGradleCompat(javaMin: '20', javaMax: '21', gradleMin: '8.1'),
  JavaGradleCompat(javaMin: '19', javaMax: '20', gradleMin: '7.6'),
  JavaGradleCompat(javaMin: '18', javaMax: '19', gradleMin: '7.5'),
  JavaGradleCompat(javaMin: '17', javaMax: '18', gradleMin: '7.3'),
  JavaGradleCompat(
    javaMin: '16',
    javaMax: '17',
    gradleMin: '7.0',
    gradleMax: maxGradleVersionForJavaPre17,
  ),
  JavaGradleCompat(
    javaMin: '15',
    javaMax: '16',
    gradleMin: '6.7',
    gradleMax: maxGradleVersionForJavaPre17,
  ),
  JavaGradleCompat(
    javaMin: '14',
    javaMax: '15',
    gradleMin: '6.3',
    gradleMax: maxGradleVersionForJavaPre17,
  ),
  JavaGradleCompat(
    javaMin: '13',
    javaMax: '14',
    gradleMin: '6.0',
    gradleMax: maxGradleVersionForJavaPre17,
  ),
  JavaGradleCompat(
    javaMin: '12',
    javaMax: '13',
    gradleMin: '5.4',
    gradleMax: maxGradleVersionForJavaPre17,
  ),
  JavaGradleCompat(
    javaMin: '11',
    javaMax: '12',
    gradleMin: '5.0',
    gradleMax: maxGradleVersionForJavaPre17,
  ),
  // 1.11 is a made up java version to cover everything in 1.10.*
  JavaGradleCompat(
    javaMin: '1.10',
    javaMax: '1.11',
    gradleMin: '4.7',
    gradleMax: maxGradleVersionForJavaPre17,
  ),
  JavaGradleCompat(
    javaMin: '1.9',
    javaMax: '1.10',
    gradleMin: '4.3',
    gradleMax: maxGradleVersionForJavaPre17,
  ),
  JavaGradleCompat(
    javaMin: '1.8',
    javaMax: '1.9',
    gradleMin: '2.0',
    gradleMax: maxGradleVersionForJavaPre17,
  ),
];

// List of compatible Java/AGP versions, where agpMax versions are inclusive.
//
// Should be updated whenever a new version of AGP is released as
// https://developer.android.com/build/releases/gradle-plugin details.
var _javaAgpCompatList = const <JavaAgpCompat>[
  JavaAgpCompat(
    javaMin: '17',
    javaDefault: '17',
    agpMin: '8.0',
    agpMax: maxKnownAndSupportedAgpVersion,
  ),
  JavaAgpCompat(javaMin: '11', javaDefault: '11', agpMin: '7.0', agpMax: '7.4'),
  JavaAgpCompat(
    // You may use JDK 1.7 with AGP 4.2, but we treat 1.8 as the default since
    // it is used by default for this AGP version and lower versions of Java
    // are deprecated for executing Gradle.
    javaMin: '1.8',
    javaDefault: '1.8',
    agpMin: '4.2',
    agpMax: '4.2',
  ),
];
