// 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.

// @dart = 2.8

import 'package:archive/archive.dart';
import 'package:file/memory.dart';
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/android/gradle_errors.dart';
import 'package:flutter_tools/src/android/gradle_utils.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/common.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/base/process.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/globals.dart' as globals;
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/reporting.dart';
import 'package:mockito/mockito.dart';

import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/mocks.dart';

void main() {
  Cache.flutterRoot = getFlutterRoot();

  group('build artifacts', () {
    FileSystem fileSystem;

    setUp(() {
      fileSystem = MemoryFileSystem.test();
    });

    testWithoutContext('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(fileSystem.directory('foo'));

      expect(
        getApkDirectory(project).path,
        equals(fileSystem.path.join('foo', 'app', 'outputs', 'flutter-apk')),
      );
    });

    testWithoutContext('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(fileSystem.directory('foo'));

      expect(
        getApkDirectory(project).path,
        equals(fileSystem.path.join('foo', 'host', 'outputs', 'apk')),
      );
    });

    testWithoutContext('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(fileSystem.directory('foo'));

      expect(
        getBundleDirectory(project).path,
        equals(fileSystem.path.join('foo', 'app', 'outputs', 'bundle')),
      );
    });

    testWithoutContext('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(fileSystem.directory('foo'));

      expect(
        getBundleDirectory(project).path,
        equals(fileSystem.path.join('foo', 'host', 'outputs', 'bundle')),
      );
    });

    testWithoutContext('getRepoDirectory', () {
      expect(
        getRepoDirectory(fileSystem.directory('foo')).path,
        equals(fileSystem.path.join('foo','outputs', 'repo')),
      );
    });
  });

  group('gradle tasks', () {
    testWithoutContext('assemble release', () {
      expect(
        getAssembleTaskFor(const BuildInfo(BuildMode.release, null, treeShakeIcons: false)),
        equals('assembleRelease'),
      );
      expect(
        getAssembleTaskFor(const BuildInfo(BuildMode.release, 'flavorFoo', treeShakeIcons: false)),
        equals('assembleFlavorFooRelease'),
      );
    });

    testWithoutContext('assemble debug', () {
      expect(
        getAssembleTaskFor(BuildInfo.debug),
        equals('assembleDebug'),
      );
      expect(
        getAssembleTaskFor(const BuildInfo(BuildMode.debug, 'flavorFoo', treeShakeIcons: false)),
        equals('assembleFlavorFooDebug'),
      );
    });

    testWithoutContext('assemble profile', () {
      expect(
        getAssembleTaskFor(const BuildInfo(BuildMode.profile, null, treeShakeIcons: false)),
        equals('assembleProfile'),
      );
      expect(
        getAssembleTaskFor(const BuildInfo(BuildMode.profile, 'flavorFoo', treeShakeIcons: false)),
        equals('assembleFlavorFooProfile'),
      );
    });
  });

  group('findBundleFile', () {
    FileSystem fileSystem;
    TestUsage testUsage;

    setUp(() {
      fileSystem = MemoryFileSystem.test();
      testUsage = TestUsage();
    });

    testWithoutContext('Finds app bundle when flavor contains underscores in release mode', () {
      final FlutterProject project = generateFakeAppBundle('foo_barRelease', 'app.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo_bar', treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barRelease', 'app.aab'));
    });

    testWithoutContext('Finds app bundle when flavor contains underscores and uppercase letters in release mode', () {
      final FlutterProject project = generateFakeAppBundle('foo_barRelease', 'app.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo_Bar', treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barRelease', 'app.aab'));
    });

    testWithoutContext("Finds app bundle when flavor doesn't contain underscores in release mode", () {
      final FlutterProject project = generateFakeAppBundle('fooRelease', 'app.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo', treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooRelease', 'app.aab'));
    });

    testWithoutContext("Finds app bundle when flavor doesn't contain underscores but contains uppercase letters in release mode", () {
      final FlutterProject project = generateFakeAppBundle('fooaRelease', 'app.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'fooA', treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooaRelease', 'app.aab'));
    });

    testWithoutContext('Finds app bundle when no flavor is used in release mode', () {
      final FlutterProject project = generateFakeAppBundle('release', 'app.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, null, treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'release', 'app.aab'));
    });

    testWithoutContext('Finds app bundle when flavor contains underscores in debug mode', () {
      final FlutterProject project = generateFakeAppBundle('foo_barDebug', 'app.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo_bar', treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barDebug', 'app.aab'));
    });

    testWithoutContext('Finds app bundle when flavor contains underscores and uppercase letters in debug mode', () {
      final FlutterProject project = generateFakeAppBundle('foo_barDebug', 'app.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo_Bar', treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barDebug', 'app.aab'));
    });

    testWithoutContext("Finds app bundle when flavor doesn't contain underscores in debug mode", () {
      final FlutterProject project = generateFakeAppBundle('fooDebug', 'app.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo', treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooDebug', 'app.aab'));
    });

    testWithoutContext("Finds app bundle when flavor doesn't contain underscores but contains uppercase letters in debug mode", () {
      final FlutterProject project = generateFakeAppBundle('fooaDebug', 'app.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'fooA', treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooaDebug', 'app.aab'));
    });

    testWithoutContext('Finds app bundle when no flavor is used in debug mode', () {
      final FlutterProject project = generateFakeAppBundle('debug', 'app.aab', fileSystem);
      final File bundle = findBundleFile(project, BuildInfo.debug);
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'debug', 'app.aab'));
    });

    testWithoutContext('Finds app bundle when flavor contains underscores in profile mode', () {
      final FlutterProject project = generateFakeAppBundle('foo_barProfile', 'app.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo_bar', treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barProfile', 'app.aab'));
    });

    testWithoutContext('Finds app bundle when flavor contains underscores and uppercase letters in profile mode', () {
      final FlutterProject project = generateFakeAppBundle('foo_barProfile', 'app.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo_Bar', treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barProfile', 'app.aab'));
    });

    testWithoutContext("Finds app bundle when flavor doesn't contain underscores in profile mode", () {
      final FlutterProject project = generateFakeAppBundle('fooProfile', 'app.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo', treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooProfile', 'app.aab'));
    });

    testWithoutContext("Finds app bundle when flavor doesn't contain underscores but contains uppercase letters in profile mode", () {
      final FlutterProject project = generateFakeAppBundle('fooaProfile', 'app.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'fooA', treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'fooaProfile', 'app.aab'));
    });

    testWithoutContext('Finds app bundle when no flavor is used in profile mode', () {
      final FlutterProject project = generateFakeAppBundle('profile', 'app.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, null, treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'profile', 'app.aab'));
    });

    testWithoutContext('Finds app bundle in release mode - Gradle 3.5', () {
      final FlutterProject project = generateFakeAppBundle('release', 'app-release.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, null, treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'release', 'app-release.aab'));
    });

    testWithoutContext('Finds app bundle in profile mode - Gradle 3.5', () {
      final FlutterProject project = generateFakeAppBundle('profile', 'app-profile.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, null, treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'profile', 'app-profile.aab'));
    });

    testWithoutContext('Finds app bundle in debug mode - Gradle 3.5', () {
      final FlutterProject project = generateFakeAppBundle('debug', 'app-debug.aab', fileSystem);
      final File bundle = findBundleFile(project, BuildInfo.debug);
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'debug', 'app-debug.aab'));
    });

    testWithoutContext('Finds app bundle when flavor contains underscores in release mode - Gradle 3.5', () {
      final FlutterProject project = generateFakeAppBundle('foo_barRelease', 'app-foo_bar-release.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo_bar', treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barRelease', 'app-foo_bar-release.aab'));
    });

    testWithoutContext('Finds app bundle when flavor contains underscores and uppercase letters in release mode - Gradle 3.5', () {
      final FlutterProject project = generateFakeAppBundle('foo_barRelease', 'app-foo_bar-release.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo_Bar', treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barRelease', 'app-foo_bar-release.aab'));
    });

    testWithoutContext('Finds app bundle when flavor contains underscores in profile mode - Gradle 3.5', () {
      final FlutterProject project = generateFakeAppBundle('foo_barProfile', 'app-foo_bar-profile.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.profile, 'foo_bar', treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant', 'app', 'outputs', 'bundle', 'foo_barProfile', 'app-foo_bar-profile.aab'));
    });

    testWithoutContext('Finds app bundle when flavor contains underscores and uppercase letters in debug mode - Gradle 3.5', () {
      final FlutterProject project = generateFakeAppBundle('foo_barDebug', 'app-foo_bar-debug.aab', fileSystem);
      final File bundle = findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo_Bar', treeShakeIcons: false));
      expect(bundle, isNotNull);
      expect(bundle.path, fileSystem.path.join('irrelevant','app', 'outputs', 'bundle', 'foo_barDebug', 'app-foo_bar-debug.aab'));
    });

    testUsingContext('aab not found', () {
      final FlutterProject project = FlutterProject.current();
      expect(
        () {
          findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo_bar', treeShakeIcons: false));
        },
        throwsToolExit(
          message:
            "Gradle build failed to produce an .aab file. It's likely that this file "
            "was generated under ${project.android.buildDirectory.path}, but the tool couldn't find it."
        )
      );
      expect(testUsage.events, contains(
        const TestUsageEvent(
          'build',
          'unspecified',
          label: 'gradle-expected-file-not-found',
          parameters: <String, String> {
            'cd37': 'androidGradlePluginVersion: 6.7, fileExtension: .aab',
          },
        ),
      ));
    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem.test(),
      ProcessManager: () => FakeProcessManager.any(),
      Usage: () => testUsage,
    });
  });

  group('listApkPaths', () {
    testWithoutContext('Finds APK without flavor in release', () {
      final Iterable<String> apks = listApkPaths(
        const AndroidBuildInfo(BuildInfo(BuildMode.release, '', treeShakeIcons: false)),
      );

      expect(apks, <String>['app-release.apk']);
    });

    testWithoutContext('Finds APK with flavor in release mode', () {
      final Iterable<String> apks = listApkPaths(
        const AndroidBuildInfo(BuildInfo(BuildMode.release, 'flavor1', treeShakeIcons: false)),
      );

      expect(apks, <String>['app-flavor1-release.apk']);
    });

    testWithoutContext('Finds APK with flavor in release mode', () {
      final Iterable<String> apks = listApkPaths(
        const AndroidBuildInfo(BuildInfo(BuildMode.release, 'flavorA', treeShakeIcons: false)),
      );

      expect(apks, <String>['app-flavora-release.apk']);
    });

    testWithoutContext('Finds APK with flavor in release mode - AGP v3', () {
      final Iterable<String> apks = listApkPaths(
        const AndroidBuildInfo(BuildInfo(BuildMode.release, 'flavor1', treeShakeIcons: false)),
      );

      expect(apks, <String>['app-flavor1-release.apk']);
    });

    testWithoutContext('Finds APK with split-per-abi', () {
      final Iterable<String> apks = listApkPaths(
        const AndroidBuildInfo(BuildInfo(BuildMode.release, 'flavor1', treeShakeIcons: false), splitPerAbi: true),
      );

      expect(apks, unorderedEquals(<String>[
        'app-armeabi-v7a-flavor1-release.apk',
        'app-arm64-v8a-flavor1-release.apk',
        'app-x86_64-flavor1-release.apk',
      ]));
    });

    testWithoutContext('Finds APK with split-per-abi when flavor contains uppercase letters', () {
      final Iterable<String> apks = listApkPaths(
        const AndroidBuildInfo(BuildInfo(BuildMode.release, 'flavorA', treeShakeIcons: false), splitPerAbi: true),
      );

      expect(apks, unorderedEquals(<String>[
        'app-armeabi-v7a-flavora-release.apk',
        'app-arm64-v8a-flavora-release.apk',
        'app-x86_64-flavora-release.apk',
      ]));
    });

  });

  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_SDK_ROOT environment variable.',
      ));
    }, overrides: <Type, Generator>{
      AndroidSdk: () => null,
    });

    test('androidXPluginWarningRegex should match lines with the AndroidX plugin warnings', () {
      final List<String> nonMatchingLines = <String>[
        ':app:preBuild UP-TO-DATE',
        'BUILD SUCCESSFUL in 0s',
        'Generic plugin AndroidX text',
        '',
      ];
      final List<String> matchingLines = <String>[
        '*********************************************************************************************************************************',
        "WARNING: This version of image_picker will break your Android build if it or its dependencies aren't compatible with AndroidX.",
        'See https://goo.gl/CP92wY for more information on the problem and how to fix it.',
        'This warning prints for all Android build failures. The real root cause of the error may be unrelated.',
      ];
      for (final String m in nonMatchingLines) {
        expect(androidXPluginWarningRegex.hasMatch(m), isFalse);
      }
      for (final String m in matchingLines) {
        expect(androidXPluginWarningRegex.hasMatch(m), isTrue);
      }
    });
  });

  group('Config files', () {
    Directory tempDir;

    setUp(() {
      tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_settings_aar_test.');
    });

    testUsingContext('create settings_aar.gradle when current settings.gradle loads plugins', () {
      const String currentSettingsGradle = r'''
include ':app'

def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()

def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if (pluginsFile.exists()) {
    pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}

plugins.each { name, path ->
    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
    if (pluginDirectory.exists()) {
        include ":$name"
        project(":$name").projectDir = pluginDirectory
    }
}
''';

      const String settingsAarFile = '''
include ':app'
''';

      tempDir.childFile('settings.gradle').writeAsStringSync(currentSettingsGradle);

      final String toolGradlePath = globals.fs.path.join(
          globals.fs.path.absolute(Cache.flutterRoot),
          'packages',
          'flutter_tools',
          'gradle');
      globals.fs.directory(toolGradlePath).createSync(recursive: true);
      globals.fs.file(globals.fs.path.join(toolGradlePath, 'settings.gradle.legacy_versions'))
          .writeAsStringSync(currentSettingsGradle);

      globals.fs.file(globals.fs.path.join(toolGradlePath, 'settings_aar.gradle.tmpl'))
          .writeAsStringSync(settingsAarFile);

      createSettingsAarGradle(tempDir);

      expect(testLogger.statusText, contains('created successfully'));
      expect(tempDir.childFile('settings_aar.gradle').existsSync(), isTrue);

    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem.test(),
      ProcessManager: () => FakeProcessManager.any(),
    });

    testUsingContext("create settings_aar.gradle when current settings.gradle doesn't load plugins", () {
      const String currentSettingsGradle = '''
include ':app'
''';

      const String settingsAarFile = '''
include ':app'
''';

      tempDir.childFile('settings.gradle').writeAsStringSync(currentSettingsGradle);

      final String toolGradlePath = globals.fs.path.join(
          globals.fs.path.absolute(Cache.flutterRoot),
          'packages',
          'flutter_tools',
          'gradle');
      globals.fs.directory(toolGradlePath).createSync(recursive: true);
      globals.fs.file(globals.fs.path.join(toolGradlePath, 'settings.gradle.legacy_versions'))
          .writeAsStringSync(currentSettingsGradle);

      globals.fs.file(globals.fs.path.join(toolGradlePath, 'settings_aar.gradle.tmpl'))
          .writeAsStringSync(settingsAarFile);

      createSettingsAarGradle(tempDir);

      expect(testLogger.statusText, contains('created successfully'));
      expect(tempDir.childFile('settings_aar.gradle').existsSync(), isTrue);

    }, overrides: <Type, Generator>{
      FileSystem: () => MemoryFileSystem.test(),
      ProcessManager: () => FakeProcessManager.any(),
    });
  });

  group('Gradle local.properties', () {
    MockLocalEngineArtifacts mockArtifacts;
    MockProcessManager mockProcessManager;
    FakePlatform android;
    FileSystem fs;

    setUp(() {
      fs = MemoryFileSystem.test();
      mockArtifacts = MockLocalEngineArtifacts();
      mockProcessManager = MockProcessManager();
      android = fakePlatform('android');
    });

    void testUsingAndroidContext(String description, dynamic testMethod()) {
      testUsingContext(description, testMethod, overrides: <Type, Generator>{
        Artifacts: () => mockArtifacts,
        Platform: () => android,
        FileSystem: () => fs,
        ProcessManager: () => mockProcessManager,
      });
    }

    String propertyFor(String key, File file) {
      final Iterable<String> result = file.readAsLinesSync()
          .where((String line) => line.startsWith('$key='))
          .map((String line) => line.split('=')[1]);
      return result.isEmpty ? null : result.first;
    }

    Future<void> checkBuildVersion({
      String manifest,
      BuildInfo buildInfo,
      String expectedBuildName,
      String expectedBuildNumber,
    }) async {
      when(mockArtifacts.getArtifactPath(
        Artifact.flutterFramework,
        platform: TargetPlatform.android_arm,
        mode: anyNamed('mode'),
        environmentType: anyNamed('environmentType'),
      )).thenReturn('engine');
      when(mockArtifacts.engineOutPath).thenReturn(globals.fs.path.join('out', 'android_arm'));

      final File manifestFile = globals.fs.file('path/to/project/pubspec.yaml');
      manifestFile.createSync(recursive: true);
      manifestFile.writeAsStringSync(manifest);


      updateLocalProperties(
        project: FlutterProject.fromPath('path/to/project'),
        buildInfo: buildInfo,
        requireAndroidSdk: false,
      );

      final File localPropertiesFile = globals.fs.file('path/to/project/android/local.properties');
      expect(propertyFor('flutter.versionName', localPropertiesFile), expectedBuildName);
      expect(propertyFor('flutter.versionCode', localPropertiesFile), expectedBuildNumber);
    }

    testUsingAndroidContext('extract build name and number from pubspec.yaml', () async {
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';

      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false);
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '1',
      );
    });

    testUsingAndroidContext('extract build name from pubspec.yaml', () async {
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, treeShakeIcons: false);
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: null,
      );
    });

    testUsingAndroidContext('allow build info to override build name', () async {
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', treeShakeIcons: false);
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '1',
      );
    });

    testUsingAndroidContext('allow build info to override build number', () async {
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildNumber: '3', treeShakeIcons: false);
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.0',
        expectedBuildNumber: '3',
      );
    });

    testUsingAndroidContext('allow build info to override build name and number', () async {
      const String manifest = '''
name: test
version: 1.0.0+1
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false);
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

    testUsingAndroidContext('allow build info to override build name and set number', () async {
      const String manifest = '''
name: test
version: 1.0.0
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false);
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

    testUsingAndroidContext('allow build info to set build name and number', () async {
      const String manifest = '''
name: test
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      const BuildInfo buildInfo = BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false);
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: buildInfo,
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
    });

    testUsingAndroidContext('allow build info to unset build name and number', () async {
      const String manifest = '''
name: test
dependencies:
  flutter:
    sdk: flutter
flutter:
''';
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: const BuildInfo(BuildMode.release, null, buildName: null, buildNumber: null, treeShakeIcons: false),
        expectedBuildName: null,
        expectedBuildNumber: null,
      );
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: const BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: '3', treeShakeIcons: false),
        expectedBuildName: '1.0.2',
        expectedBuildNumber: '3',
      );
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: const BuildInfo(BuildMode.release, null, buildName: '1.0.3', buildNumber: '4', treeShakeIcons: false),
        expectedBuildName: '1.0.3',
        expectedBuildNumber: '4',
      );
      // Values don't get unset.
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: null,
        expectedBuildName: '1.0.3',
        expectedBuildNumber: '4',
      );
      // Values get unset.
      await checkBuildVersion(
        manifest: manifest,
        buildInfo: const BuildInfo(BuildMode.release, null, buildName: null, buildNumber: null, treeShakeIcons: false),
        expectedBuildName: null,
        expectedBuildNumber: null,
      );
    });
  });

  group('gradle version', () {
    testWithoutContext('should be compatible with the Android plugin version', () {
      // Granular versions.
      expect(getGradleVersionFor('1.0.0'), '2.3');
      expect(getGradleVersionFor('1.0.1'), '2.3');
      expect(getGradleVersionFor('1.0.2'), '2.3');
      expect(getGradleVersionFor('1.0.4'), '2.3');
      expect(getGradleVersionFor('1.0.8'), '2.3');
      expect(getGradleVersionFor('1.1.0'), '2.3');
      expect(getGradleVersionFor('1.1.2'), '2.3');
      expect(getGradleVersionFor('1.1.2'), '2.3');
      expect(getGradleVersionFor('1.1.3'), '2.3');
      // Version Ranges.
      expect(getGradleVersionFor('1.2.0'), '2.9');
      expect(getGradleVersionFor('1.3.1'), '2.9');

      expect(getGradleVersionFor('1.5.0'), '2.2.1');

      expect(getGradleVersionFor('2.0.0'), '2.13');
      expect(getGradleVersionFor('2.1.2'), '2.13');

      expect(getGradleVersionFor('2.1.3'), '2.14.1');
      expect(getGradleVersionFor('2.2.3'), '2.14.1');

      expect(getGradleVersionFor('2.3.0'), '3.3');

      expect(getGradleVersionFor('3.0.0'), '4.1');

      expect(getGradleVersionFor('3.1.0'), '4.4');

      expect(getGradleVersionFor('3.2.0'), '4.6');
      expect(getGradleVersionFor('3.2.1'), '4.6');

      expect(getGradleVersionFor('3.3.0'), '4.10.2');
      expect(getGradleVersionFor('3.3.2'), '4.10.2');

      expect(getGradleVersionFor('3.4.0'), '5.6.2');
      expect(getGradleVersionFor('3.5.0'), '5.6.2');

      expect(getGradleVersionFor('4.0.0'), '6.7');
      expect(getGradleVersionFor('4.1.0'), '6.7');
    });

    testWithoutContext('throws on unsupported versions', () {
      expect(() => getGradleVersionFor('3.6.0'),
          throwsA(predicate<Exception>((Exception e) => e is ToolExit)));
    });
  });

  group('isAppUsingAndroidX', () {
    FileSystem fs;

    setUp(() {
      fs = MemoryFileSystem.test();
    });

    testUsingContext('returns true when the project is using AndroidX', () async {
      final Directory androidDirectory = globals.fs.systemTempDirectory.createTempSync('flutter_android.');

      androidDirectory
        .childFile('gradle.properties')
        .writeAsStringSync('android.useAndroidX=true');

      expect(isAppUsingAndroidX(androidDirectory), isTrue);

    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
      ProcessManager: () => FakeProcessManager.any(),
    });

    testUsingContext('returns false when the project is not using AndroidX', () async {
      final Directory androidDirectory = globals.fs.systemTempDirectory.createTempSync('flutter_android.');

      androidDirectory
        .childFile('gradle.properties')
        .writeAsStringSync('android.useAndroidX=false');

      expect(isAppUsingAndroidX(androidDirectory), isFalse);

    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
      ProcessManager: () => FakeProcessManager.any(),
    });

    testUsingContext('returns false when gradle.properties does not exist', () async {
      final Directory androidDirectory = globals.fs.systemTempDirectory.createTempSync('flutter_android.');

      expect(isAppUsingAndroidX(androidDirectory), isFalse);

    }, overrides: <Type, Generator>{
      FileSystem: () => fs,
      ProcessManager: () => FakeProcessManager.any(),
    });
  });

  group('buildPluginsAsAar', () {
    FileSystem fs;
    FakeProcessManager fakeProcessManager;
    MockAndroidSdk mockAndroidSdk;

    setUp(() {
      fs = MemoryFileSystem.test();
      fakeProcessManager = FakeProcessManager.list(<FakeCommand>[]);
      mockAndroidSdk = MockAndroidSdk();
      when(mockAndroidSdk.directory).thenReturn('irrelevant');
    });

    testUsingContext('calls gradle', () async {
      final Directory androidDirectory = globals.fs.directory('android.');
      androidDirectory.createSync();
      androidDirectory
        .childFile('pubspec.yaml')
        .writeAsStringSync('name: irrelevant');

      final Directory plugin1 = globals.fs.directory('plugin1.');
      plugin1
        ..createSync()
        ..childFile('pubspec.yaml')
        .writeAsStringSync('''
name: irrelevant
flutter:
  plugin:
    androidPackage: irrelevant
''');

      plugin1
        .childDirectory('android')
        .childFile('build.gradle')
        .createSync(recursive: true);

      final Directory plugin2 = globals.fs.directory('plugin2.');
      plugin2
        ..createSync()
        ..childFile('pubspec.yaml')
        .writeAsStringSync('''
name: irrelevant
flutter:
  plugin:
    androidPackage: irrelevant
''');

      plugin2
        .childDirectory('android')
        .childFile('build.gradle')
        .createSync(recursive: true);

      androidDirectory
        .childFile('.flutter-plugins')
        .writeAsStringSync('''
plugin1=${plugin1.path}
plugin2=${plugin2.path}
''');
      final Directory buildDirectory = androidDirectory
        .childDirectory('build');
      buildDirectory
        .childDirectory('outputs')
        .childDirectory('repo')
        .createSync(recursive: true);

      final String flutterRoot = globals.fs.path.absolute(Cache.flutterRoot);
      final String initScript = globals.fs.path.join(
        flutterRoot,
        'packages',
        'flutter_tools',
        'gradle',
        'aar_init_script.gradle',
      );

      fakeProcessManager
        ..addCommand(FakeCommand(
          command: <String>[
            'gradlew',
            '-I=$initScript',
            '-Pflutter-root=$flutterRoot',
            '-Poutput-dir=${buildDirectory.path}',
            '-Pis-plugin=true',
            '-PbuildNumber=1.0',
            '-q',
            '-Pdart-obfuscation=false',
            '-Ptrack-widget-creation=false',
            '-Ptree-shake-icons=true',
            '-Ptarget-platform=android-arm,android-arm64,android-x64',
            'assembleAarRelease',
          ],
          workingDirectory: plugin1.childDirectory('android').path,
        ))
        ..addCommand(FakeCommand(
          command: <String>[
            'gradlew',
            '-I=$initScript',
            '-Pflutter-root=$flutterRoot',
            '-Poutput-dir=${buildDirectory.path}',
            '-Pis-plugin=true',
            '-PbuildNumber=1.0',
            '-q',
            '-Pdart-obfuscation=false',
            '-Ptrack-widget-creation=false',
            '-Ptree-shake-icons=true',
            '-Ptarget-platform=android-arm,android-arm64,android-x64',
            'assembleAarRelease',
          ],
          workingDirectory: plugin2.childDirectory('android').path,
        ));

      await buildPluginsAsAar(
        FlutterProject.fromPath(androidDirectory.path),
        const AndroidBuildInfo(BuildInfo(
          BuildMode.release,
          '',
          treeShakeIcons: true,
          dartObfuscation: true,
          buildNumber: '2.0'
        )),
        buildDirectory: buildDirectory,
      );
      expect(fakeProcessManager.hasRemainingExpectations, isFalse);
    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      FileSystem: () => fs,
      ProcessManager: () => fakeProcessManager,
      GradleUtils: () => FakeGradleUtils(),
    });

    testUsingContext('skips plugin without a android/build.gradle file', () async {
      final Directory androidDirectory = globals.fs.directory('android.');
      androidDirectory.createSync();
      androidDirectory
        .childFile('pubspec.yaml')
        .writeAsStringSync('name: irrelevant');

      final Directory plugin1 = globals.fs.directory('plugin1.');
      plugin1
        ..createSync()
        ..childFile('pubspec.yaml')
        .writeAsStringSync('''
name: irrelevant
flutter:
  plugin:
    androidPackage: irrelevant
''');

      androidDirectory
        .childFile('.flutter-plugins')
        .writeAsStringSync('''
plugin1=${plugin1.path}
''');
      // Create an empty android directory.
      // https://github.com/flutter/flutter/issues/46898
      plugin1.childDirectory('android').createSync();

      final Directory buildDirectory = androidDirectory.childDirectory('build');

      buildDirectory
        .childDirectory('outputs')
        .childDirectory('repo')
        .createSync(recursive: true);

      await buildPluginsAsAar(
        FlutterProject.fromPath(androidDirectory.path),
        const AndroidBuildInfo(BuildInfo.release),
        buildDirectory: buildDirectory,
      );
      expect(fakeProcessManager.hasRemainingExpectations, isFalse);
    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      FileSystem: () => fs,
      ProcessManager: () => fakeProcessManager,
      GradleUtils: () => FakeGradleUtils(),
    });
  });

  group('gradle build', () {
    TestUsage testUsage;
    MockAndroidSdk mockAndroidSdk;
    MockAndroidStudio mockAndroidStudio;
    MockLocalEngineArtifacts mockArtifacts;
    MockProcessManager mockProcessManager;
    FakePlatform android;
    FileSystem fileSystem;
    FileSystemUtils fileSystemUtils;
    Cache cache;

    setUp(() {
      testUsage = TestUsage();
      fileSystem = MemoryFileSystem.test();
      fileSystemUtils = MockFileSystemUtils();
      mockAndroidSdk = MockAndroidSdk();
      mockAndroidStudio = MockAndroidStudio();
      mockArtifacts = MockLocalEngineArtifacts();
      mockProcessManager = MockProcessManager();
      android = fakePlatform('android');

      when(mockAndroidSdk.directory).thenReturn('irrelevant');

      final Directory rootDirectory = fileSystem.currentDirectory;
      cache = Cache.test(
        rootOverride: rootDirectory,
        fileSystem: fileSystem,
      );

      final Directory gradleWrapperDirectory = rootDirectory
          .childDirectory('bin')
          .childDirectory('cache')
          .childDirectory('artifacts')
          .childDirectory('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('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));

      fileSystem.directory('android')
        .childFile('build.gradle')
        .createSync(recursive: true);

      fileSystem.directory('android')
        .childFile('gradle.properties')
        .createSync(recursive: true);

      fileSystem.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,
              treeShakeIcons: false,
            ),
          ),
          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);

      expect(testUsage.events, contains(
        const TestUsageEvent(
          'build',
          'unspecified',
          label: 'gradle-random-event-label-failure',
          parameters: <String, String>{},
        ),
      ));
    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      Cache: () => cache,
      Platform: () => android,
      FileSystem: () => fileSystem,
      ProcessManager: () => mockProcessManager,
      Usage: () => testUsage,
    });

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

      fileSystem.directory('android')
        .childFile('build.gradle')
        .createSync(recursive: true);

      fileSystem.directory('android')
        .childFile('gradle.properties')
        .createSync(recursive: true);

      fileSystem.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,
              treeShakeIcons: false,
            ),
          ),
          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));
      expect(testUsage.events, contains(
        const TestUsageEvent(
          'build',
          'unspecified',
          label: 'gradle-random-event-label-failure',
          parameters: <String, String>{},
        ),
      ));
    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      Cache: () => cache,
      Platform: () => android,
      FileSystem: () => fileSystem,
      ProcessManager: () => mockProcessManager,
      Usage: () => testUsage,
    });

    testUsingContext('recognizes process exceptions - tool exit', () async {
      when(mockProcessManager.start(any,
        workingDirectory: anyNamed('workingDirectory'),
        environment: anyNamed('environment')))
      .thenThrow(const ProcessException('', <String>[], 'Some gradle message'));

      fileSystem.directory('android')
        .childFile('build.gradle')
        .createSync(recursive: true);

      fileSystem.directory('android')
        .childFile('gradle.properties')
        .createSync(recursive: true);

      fileSystem.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,
              treeShakeIcons: false,
            ),
          ),
          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);

      expect(testUsage.events, contains(
        const TestUsageEvent(
          'build',
          'unspecified',
          label: 'gradle-random-event-label-failure',
          parameters: <String, String>{},
        ),
      ));
    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      Cache: () => cache,
      Platform: () => android,
      FileSystem: () => fileSystem,
      ProcessManager: () => mockProcessManager,
      Usage: () => testUsage,
    });

    testUsingContext('rethrows unrecognized ProcessException', () async {
      when(mockProcessManager.start(any,
        workingDirectory: anyNamed('workingDirectory'),
        environment: anyNamed('environment')))
      .thenThrow(const ProcessException('', <String>[], 'Unrecognized'));

      fileSystem.directory('android')
        .childFile('build.gradle')
        .createSync(recursive: true);

      fileSystem.directory('android')
        .childFile('gradle.properties')
        .createSync(recursive: true);

      fileSystem.directory('android')
        .childDirectory('app')
        .childFile('build.gradle')
        ..createSync(recursive: true)
        ..writeAsStringSync('apply from: irrelevant/flutter.gradle');

      await expectLater(() async {
       await buildGradleApp(
          project: FlutterProject.current(),
          androidBuildInfo: const AndroidBuildInfo(
            BuildInfo(
              BuildMode.release,
              null,
              treeShakeIcons: false,
            ),
          ),
          target: 'lib/main.dart',
          isBuildingBundle: false,
          localGradleErrors: const <GradleHandledError>[],
        );
      },
      throwsA(isA<ProcessException>()));

    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      Cache: () => cache,
      Platform: () => android,
      FileSystem: () => fileSystem,
      ProcessManager: () => mockProcessManager,
    });

    testUsingContext('logs success event after a successful 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);
      });

      fileSystem.directory('android')
        .childFile('build.gradle')
        .createSync(recursive: true);

      fileSystem.directory('android')
        .childFile('gradle.properties')
        .createSync(recursive: true);

      fileSystem.directory('android')
        .childDirectory('app')
        .childFile('build.gradle')
        ..createSync(recursive: true)
        ..writeAsStringSync('apply from: irrelevant/flutter.gradle');

      fileSystem.directory('build')
        .childDirectory('app')
        .childDirectory('outputs')
        .childDirectory('flutter-apk')
        .childFile('app-release.apk')
        .createSync(recursive: true);

      await buildGradleApp(
        project: FlutterProject.current(),
        androidBuildInfo: const AndroidBuildInfo(
          BuildInfo(
            BuildMode.release,
            null,
            treeShakeIcons: false,
          ),
        ),
        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',
          ),
        ],
      );

      expect(testUsage.events, contains(
        const TestUsageEvent(
          'build',
          'unspecified',
          label: 'gradle-random-event-label-success',
          parameters: <String, String>{},
        ),
      ));
    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      Cache: () => cache,
      FileSystem: () => fileSystem,
      Platform: () => android,
      ProcessManager: () => mockProcessManager,
      Usage: () => testUsage,
    });

    testUsingContext('performs code size analysis and sends analytics', () async {
      when(mockProcessManager.start(any,
        workingDirectory: anyNamed('workingDirectory'),
        environment: anyNamed('environment')))
      .thenAnswer((_) {
        return Future<Process>.value(createMockProcess(
          exitCode: 0,
          stdout: 'irrelevant',
        ));
      });

      fileSystem.directory('android')
        .childFile('build.gradle')
        .createSync(recursive: true);

      fileSystem.directory('android')
        .childFile('gradle.properties')
        .createSync(recursive: true);

      fileSystem.directory('android')
        .childDirectory('app')
        .childFile('build.gradle')
        ..createSync(recursive: true)
        ..writeAsStringSync('apply from: irrelevant/flutter.gradle');

      final Archive archive = Archive()
        ..addFile(ArchiveFile('AndroidManifest.xml', 100,  List<int>.filled(100, 0)))
        ..addFile(ArchiveFile('META-INF/CERT.RSA', 10,  List<int>.filled(10, 0)))
        ..addFile(ArchiveFile('META-INF/CERT.SF', 10,  List<int>.filled(10, 0)))
        ..addFile(ArchiveFile('lib/arm64-v8a/libapp.so', 50,  List<int>.filled(50, 0)))
        ..addFile(ArchiveFile('lib/arm64-v8a/libflutter.so', 50, List<int>.filled(50, 0)));

      fileSystem.directory('build')
        .childDirectory('app')
        .childDirectory('outputs')
        .childDirectory('flutter-apk')
        .childFile('app-release.apk')
        ..createSync(recursive: true)
        ..writeAsBytesSync(ZipEncoder().encode(archive));

      fileSystem.file('foo/snapshot.arm64-v8a.json')
        ..createSync(recursive: true)
        ..writeAsStringSync(r'''
[
  {
    "l": "dart:_internal",
    "c": "SubListIterable",
    "n": "[Optimized] skip",
    "s": 2400
  }
]''');
      fileSystem.file('foo/trace.arm64-v8a.json')
        ..createSync(recursive: true)
        ..writeAsStringSync('{}');

      await buildGradleApp(
        project: FlutterProject.current(),
        androidBuildInfo: const AndroidBuildInfo(
          BuildInfo(
            BuildMode.release,
            null,
            treeShakeIcons: false,
            codeSizeDirectory: 'foo',
          ),
          targetArchs: <AndroidArch>[AndroidArch.arm64_v8a],
        ),
        target: 'lib/main.dart',
        isBuildingBundle: false,
        localGradleErrors: <GradleHandledError>[],
      );

      expect(testUsage.events, contains(
        const TestUsageEvent(
          'code-size-analysis',
          'apk',
        ),
      ));
    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      Cache: () => cache,
      FileSystem: () => fileSystem,
      Platform: () => android,
      ProcessManager: () => mockProcessManager,
      Usage: () => testUsage,
    });

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

      fileSystem.directory('android')
        .childFile('build.gradle')
        .createSync(recursive: true);

      fileSystem.directory('android')
        .childFile('gradle.properties')
        .createSync(recursive: true);

      fileSystem.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,
              treeShakeIcons: false,
            ),
          ),
          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);

      expect(testUsage.events, contains(
        const TestUsageEvent(
          'build',
          'unspecified',
          label: 'gradle-random-event-label-failure',
          parameters: <String, String>{},
        ),
      ));
    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      Cache: () => cache,
      Platform: () => android,
      FileSystem: () => fileSystem,
      ProcessManager: () => mockProcessManager,
      Usage: () => testUsage,
    });

    testUsingContext('indicates that an APK has been built successfully', () async {
      fileSystem.directory('android')
        .childFile('build.gradle')
        .createSync(recursive: true);

      fileSystem.directory('android')
        .childFile('gradle.properties')
        .createSync(recursive: true);

      fileSystem.directory('android')
        .childDirectory('app')
        .childFile('build.gradle')
        ..createSync(recursive: true)
        ..writeAsStringSync('apply from: irrelevant/flutter.gradle');

      fileSystem.directory('build')
        .childDirectory('app')
        .childDirectory('outputs')
        .childDirectory('flutter-apk')
        .childFile('app-release.apk')
        .createSync(recursive: true);

      await buildGradleApp(
        project: FlutterProject.current(),
        androidBuildInfo: const AndroidBuildInfo(
          BuildInfo(
            BuildMode.release,
            null,
            treeShakeIcons: false,
          ),
        ),
        target: 'lib/main.dart',
        isBuildingBundle: false,
        localGradleErrors: const <GradleHandledError>[],
      );

      expect(
        testLogger.statusText,
        contains('Built build/app/outputs/flutter-apk/app-release.apk (0.0MB)'),
      );

    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      Cache: () => cache,
      FileSystem: () => fileSystem,
      Platform: () => android,
      ProcessManager: () => FakeProcessManager.any(),
    });

    testUsingContext("doesn't indicate how to consume an AAR when printHowToConsumeAaar is false", () async {
      final File manifestFile = fileSystem.file('pubspec.yaml');
      manifestFile.createSync(recursive: true);
      manifestFile.writeAsStringSync('''
        flutter:
          module:
            androidPackage: com.example.test
        '''
      );

      fileSystem.file('.android/gradlew').createSync(recursive: true);

      fileSystem.file('.android/gradle.properties')
        .writeAsStringSync('irrelevant');

      fileSystem.file('.android/build.gradle')
        .createSync(recursive: true);

      // Let any process start. Assert after.
      when(mockProcessManager.run(
        any,
        environment: anyNamed('environment'),
        workingDirectory: anyNamed('workingDirectory'),
      )).thenAnswer((_) async => ProcessResult(1, 0, '', ''));

      fileSystem.directory('build/outputs/repo').createSync(recursive: true);

      await buildGradleAar(
        androidBuildInfo: const AndroidBuildInfo(BuildInfo(BuildMode.release, null, treeShakeIcons: false)),
        project: FlutterProject.current(),
        outputDirectory: fileSystem.directory('build/'),
        target: '',
        buildNumber: '1.0',
      );

      expect(
        testLogger.statusText,
        contains('Built build/outputs/repo'),
      );
      expect(
        testLogger.statusText.contains('Consuming the Module'),
        isFalse,
      );

    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      AndroidStudio: () => mockAndroidStudio,
      Cache: () => cache,
      Platform: () => android,
      FileSystem: () => fileSystem,
      ProcessManager: () => mockProcessManager,
    });

    testUsingContext('build apk uses selected local engine,the engine abi is arm', () async {
      when(mockArtifacts.getArtifactPath(
        Artifact.flutterFramework,
        platform: TargetPlatform.android_arm,
        mode: anyNamed('mode'),
        environmentType: anyNamed('environmentType'),
      )).thenReturn('engine');
      when(mockArtifacts.engineOutPath).thenReturn(fileSystem.path.join('out', 'android_arm'));

      fileSystem.file('out/android_arm/flutter_embedding_release.pom')
        ..createSync(recursive: true)
        ..writeAsStringSync('''
<?xml version="1.0" encoding="UTF-8"?>
<project>
  <version>1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b</version>
  <dependencies>
  </dependencies>
</project>
''');
      fileSystem.file('out/android_arm/armeabi_v7a_release.pom').createSync(recursive: true);
      fileSystem.file('out/android_arm/armeabi_v7a_release.jar').createSync(recursive: true);
      fileSystem.file('out/android_arm/armeabi_v7a_release.maven-metadata.xml').createSync(recursive: true);
      fileSystem.file('out/android_arm/flutter_embedding_release.jar').createSync(recursive: true);
      fileSystem.file('out/android_arm/flutter_embedding_release.pom').createSync(recursive: true);
      fileSystem.file('out/android_arm/flutter_embedding_release.maven-metadata.xml').createSync(recursive: true);

      fileSystem.file('android/gradlew').createSync(recursive: true);

      fileSystem.directory('android')
        .childFile('gradle.properties')
        .createSync(recursive: true);

      fileSystem.file('android/build.gradle')
        .createSync(recursive: true);

      fileSystem.directory('android')
        .childDirectory('app')
        .childFile('build.gradle')
        ..createSync(recursive: true)
        ..writeAsStringSync('apply from: irrelevant/flutter.gradle');

      // Let any process start. Assert after.
      when(mockProcessManager.run(
        any,
        environment: anyNamed('environment'),
        workingDirectory: anyNamed('workingDirectory'),
      )).thenAnswer((_) async => ProcessResult(1, 0, '', ''));

      when(mockProcessManager.start(any,
        workingDirectory: anyNamed('workingDirectory'),
        environment: anyNamed('environment')))
      .thenAnswer((_) {
        return Future<Process>.value(
          createMockProcess(
            exitCode: 1,
          )
        );
      });

      await expectLater(() async {
        await buildGradleApp(
          project: FlutterProject.current(),
          androidBuildInfo: const AndroidBuildInfo(
            BuildInfo(
              BuildMode.release,
              null,
              treeShakeIcons: false,
            ),
          ),
          target: 'lib/main.dart',
          isBuildingBundle: false,
          localGradleErrors: const <GradleHandledError>[],
        );
      }, throwsToolExit());

      final List<String> actualGradlewCall = verify(
        mockProcessManager.start(
          captureAny,
          environment: anyNamed('environment'),
          workingDirectory: anyNamed('workingDirectory')
        ),
      ).captured.last as List<String>;

      expect(actualGradlewCall, contains('/android/gradlew'));
      expect(actualGradlewCall, contains('-Plocal-engine-out=out/android_arm'));
      expect(actualGradlewCall, contains('-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0'));
      expect(actualGradlewCall, contains('-Plocal-engine-build-mode=release'));

    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      AndroidStudio: () => mockAndroidStudio,
      Artifacts: () => mockArtifacts,
      Cache: () => cache,
      Platform: () => android,
      FileSystem: () => fileSystem,
      ProcessManager: () => mockProcessManager,
    });

    testUsingContext(
        'build apk uses selected local engine,the engine abi is arm64', () async {
      when(mockArtifacts.getArtifactPath(
        Artifact.flutterFramework,
        platform: anyNamed('platform'),
        mode: anyNamed('mode'),
        environmentType: anyNamed('environmentType'),
      )).thenReturn('engine');
      when(mockArtifacts.engineOutPath).thenReturn(fileSystem.path.join('out', 'android_arm64'));

      fileSystem.file('out/android_arm64/flutter_embedding_release.pom')
        ..createSync(recursive: true)
        ..writeAsStringSync('''
<?xml version="1.0" encoding="UTF-8"?>
<project>
  <version>1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b</version>
  <dependencies>
  </dependencies>
</project>
''');
      fileSystem.file('out/android_arm64/arm64_v8a_release.pom').createSync(recursive: true);
      fileSystem.file('out/android_arm64/arm64_v8a_release.jar').createSync(recursive: true);
      fileSystem.file('out/android_arm64/arm64_v8a_release.maven-metadata.xml').createSync(recursive: true);
      fileSystem.file('out/android_arm64/flutter_embedding_release.jar').createSync(recursive: true);
      fileSystem.file('out/android_arm64/flutter_embedding_release.pom').createSync(recursive: true);
      fileSystem.file('out/android_arm64/flutter_embedding_release.maven-metadata.xml').createSync(recursive: true);

      fileSystem.file('android/gradlew').createSync(recursive: true);

      fileSystem.directory('android')
          .childFile('gradle.properties')
          .createSync(recursive: true);

      fileSystem.file('android/build.gradle')
          .createSync(recursive: true);

      fileSystem.directory('android')
          .childDirectory('app')
          .childFile('build.gradle')
        ..createSync(recursive: true)
        ..writeAsStringSync('apply from: irrelevant/flutter.gradle');

      // Let any process start. Assert after.
      when(mockProcessManager.run(
        any,
        environment: anyNamed('environment'),
        workingDirectory: anyNamed('workingDirectory'),
      )).thenAnswer((_) async => ProcessResult(1, 0, '', ''));

      when(mockProcessManager.start(any,
          workingDirectory: anyNamed('workingDirectory'),
          environment: anyNamed('environment')))
          .thenAnswer((_) {
        return Future<Process>.value(
            createMockProcess(
              exitCode: 1,
            )
        );
      });

      await expectLater(() async {
        await buildGradleApp(
          project: FlutterProject.current(),
          androidBuildInfo: const AndroidBuildInfo(
            BuildInfo(
              BuildMode.release,
              null,
              treeShakeIcons: false,
            ),
          ),
          target: 'lib/main.dart',
          isBuildingBundle: false,
          localGradleErrors: const <GradleHandledError>[],
        );
      }, throwsToolExit());

      final List<String> actualGradlewCall = verify(
        mockProcessManager.start(
            captureAny,
            environment: anyNamed('environment'),
            workingDirectory: anyNamed('workingDirectory')
        ),
      ).captured.last as List<String>;

      expect(actualGradlewCall, contains('/android/gradlew'));
      expect(actualGradlewCall, contains('-Plocal-engine-out=out/android_arm64'));
      expect(actualGradlewCall, contains('-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0'));
      expect(actualGradlewCall, contains('-Plocal-engine-build-mode=release'));

    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      AndroidStudio: () => mockAndroidStudio,
      Artifacts: () => mockArtifacts,
      Cache: () => cache,
      Platform: () => android,
      FileSystem: () => fileSystem,
      ProcessManager: () => mockProcessManager,
    });

    testUsingContext(
        'build apk uses selected local engine,the engine abi is x86', () async {
      when(mockArtifacts.getArtifactPath(
        Artifact.flutterFramework,
        platform: anyNamed('platform'),
        mode: anyNamed('mode'),
        environmentType: anyNamed('environmentType'),
      )).thenReturn('engine');
      when(mockArtifacts.engineOutPath).thenReturn(fileSystem.path.join('out', 'android_x86'));

      fileSystem.file('out/android_x86/flutter_embedding_release.pom')
        ..createSync(recursive: true)
        ..writeAsStringSync('''
<?xml version="1.0" encoding="UTF-8"?>
<project>
  <version>1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b</version>
  <dependencies>
  </dependencies>
</project>
''');
      fileSystem.file('out/android_x86/x86_release.pom').createSync(recursive: true);
      fileSystem.file('out/android_x86/x86_release.jar').createSync(recursive: true);
      fileSystem.file('out/android_x86/x86_release.maven-metadata.xml').createSync(recursive: true);
      fileSystem.file('out/android_x86/flutter_embedding_release.jar').createSync(recursive: true);
      fileSystem.file('out/android_x86/flutter_embedding_release.pom').createSync(recursive: true);
      fileSystem.file('out/android_x86/flutter_embedding_release.maven-metadata.xml').createSync(recursive: true);

      fileSystem.file('android/gradlew').createSync(recursive: true);

      fileSystem.directory('android')
          .childFile('gradle.properties')
          .createSync(recursive: true);

      fileSystem.file('android/build.gradle')
          .createSync(recursive: true);

      fileSystem.directory('android')
          .childDirectory('app')
          .childFile('build.gradle')
        ..createSync(recursive: true)
        ..writeAsStringSync('apply from: irrelevant/flutter.gradle');

      // Let any process start. Assert after.
      when(mockProcessManager.run(
        any,
        environment: anyNamed('environment'),
        workingDirectory: anyNamed('workingDirectory'),
      )).thenAnswer((_) async => ProcessResult(1, 0, '', ''));

      when(mockProcessManager.start(any,
          workingDirectory: anyNamed('workingDirectory'),
          environment: anyNamed('environment')))
          .thenAnswer((_) {
        return Future<Process>.value(
            createMockProcess(
              exitCode: 1,
            )
        );
      });

      await expectLater(() async {
        await buildGradleApp(
          project: FlutterProject.current(),
          androidBuildInfo: const AndroidBuildInfo(
            BuildInfo(
              BuildMode.release,
              null,
              treeShakeIcons: false,
            ),
          ),
          target: 'lib/main.dart',
          isBuildingBundle: false,
          localGradleErrors: const <GradleHandledError>[],
        );
      }, throwsToolExit());

      final List<String> actualGradlewCall = verify(
        mockProcessManager.start(
            captureAny,
            environment: anyNamed('environment'),
            workingDirectory: anyNamed('workingDirectory')
        ),
      ).captured.last as List<String>;

      expect(actualGradlewCall, contains('/android/gradlew'));
      expect(actualGradlewCall, contains('-Plocal-engine-out=out/android_x86'));
      expect(actualGradlewCall, contains('-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0'));
      expect(actualGradlewCall, contains('-Plocal-engine-build-mode=release'));

    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      AndroidStudio: () => mockAndroidStudio,
      Artifacts: () => mockArtifacts,
      Cache: () => cache,
      Platform: () => android,
      FileSystem: () => fileSystem,
      ProcessManager: () => mockProcessManager,
    });

    testUsingContext(
        'build apk uses selected local engine,the engine abi is x64', () async {
      when(mockArtifacts.getArtifactPath(
        Artifact.flutterFramework,
        platform: anyNamed('platform'),
        mode: anyNamed('mode'),
        environmentType: anyNamed('environmentType'),
      )).thenReturn('engine');
      when(mockArtifacts.engineOutPath).thenReturn(fileSystem.path.join('out', 'android_x64'));

      fileSystem.file('out/android_x64/flutter_embedding_release.pom')
        ..createSync(recursive: true)
        ..writeAsStringSync('''
<?xml version="1.0" encoding="UTF-8"?>
<project>
  <version>1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b</version>
  <dependencies>
  </dependencies>
</project>
''');
      fileSystem.file('out/android_x64/x86_64_release.pom').createSync(recursive: true);
      fileSystem.file('out/android_x64/x86_64_release.jar').createSync(recursive: true);
      fileSystem.file('out/android_x64/x86_64_release.maven-metadata.xml').createSync(recursive: true);
      fileSystem.file('out/android_x64/flutter_embedding_release.jar').createSync(recursive: true);
      fileSystem.file('out/android_x64/flutter_embedding_release.pom').createSync(recursive: true);
      fileSystem.file('out/android_x64/flutter_embedding_release.maven-metadata.xml').createSync(recursive: true);

      fileSystem.file('android/gradlew').createSync(recursive: true);

      fileSystem.directory('android')
          .childFile('gradle.properties')
          .createSync(recursive: true);

      fileSystem.file('android/build.gradle')
          .createSync(recursive: true);

      fileSystem.directory('android')
          .childDirectory('app')
          .childFile('build.gradle')
        ..createSync(recursive: true)
        ..writeAsStringSync('apply from: irrelevant/flutter.gradle');

      // Let any process start. Assert after.
      when(mockProcessManager.run(
        any,
        environment: anyNamed('environment'),
        workingDirectory: anyNamed('workingDirectory'),
      )).thenAnswer((_) async => ProcessResult(1, 0, '', ''));

      when(mockProcessManager.start(any,
          workingDirectory: anyNamed('workingDirectory'),
          environment: anyNamed('environment')))
          .thenAnswer((_) {
        return Future<Process>.value(
            createMockProcess(
              exitCode: 1,
            )
        );
      });

      await expectLater(() async {
        await buildGradleApp(
          project: FlutterProject.current(),
          androidBuildInfo: const AndroidBuildInfo(
            BuildInfo(
              BuildMode.release,
              null,
              treeShakeIcons: false,
            ),
          ),
          target: 'lib/main.dart',
          isBuildingBundle: false,
          localGradleErrors: const <GradleHandledError>[],
        );
      }, throwsToolExit());

      final List<String> actualGradlewCall = verify(
        mockProcessManager.start(
            captureAny,
            environment: anyNamed('environment'),
            workingDirectory: anyNamed('workingDirectory')
        ),
      ).captured.last as List<String>;

      expect(actualGradlewCall, contains('/android/gradlew'));
      expect(actualGradlewCall, contains('-Plocal-engine-out=out/android_x64'));
      expect(actualGradlewCall, contains('-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0'));
      expect(actualGradlewCall, contains('-Plocal-engine-build-mode=release'));

    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      AndroidStudio: () => mockAndroidStudio,
      Artifacts: () => mockArtifacts,
      Cache: () => cache,
      Platform: () => android,
      FileSystem: () => fileSystem,
      ProcessManager: () => mockProcessManager,
    });

    testUsingContext('honors --no-android-gradle-daemon setting', () async {
      (globals.processManager as FakeProcessManager).addCommand(
        const FakeCommand(command: <String>[
          '/android/gradlew',
          '-q',
          '--no-daemon',
          '-Ptarget-platform=android-arm,android-arm64,android-x64',
          '-Ptarget=lib/main.dart',
          '-Pdart-obfuscation=false',
          '-Ptrack-widget-creation=false',
          '-Ptree-shake-icons=false',
          'assembleRelease'
        ],
      ));
      fileSystem.file('android/gradlew').createSync(recursive: true);

      fileSystem.directory('android')
          .childFile('gradle.properties')
          .createSync(recursive: true);

      fileSystem.file('android/build.gradle')
          .createSync(recursive: true);

      fileSystem.directory('android')
          .childDirectory('app')
          .childFile('build.gradle')
        ..createSync(recursive: true)
        ..writeAsStringSync('apply from: irrelevant/flutter.gradle');

      await expectLater(() async {
        await buildGradleApp(
          project: FlutterProject.current(),
          androidBuildInfo: const AndroidBuildInfo(
            BuildInfo(
              BuildMode.release,
              null,
              treeShakeIcons: false,
              androidGradleDaemon: false,
            ),
          ),
          target: 'lib/main.dart',
          isBuildingBundle: false,
          localGradleErrors: const <GradleHandledError>[],
        );
      }, throwsToolExit());
    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      AndroidStudio: () => mockAndroidStudio,
      Artifacts: () => Artifacts.test(),
      Cache: () => cache,
      Platform: () => android,
      FileSystem: () => fileSystem,
      ProcessManager: () => FakeProcessManager.list(<FakeCommand>[]),
    });

    testUsingContext('build aar uses selected local engine,the engine abi is arm', () async {
      when(mockArtifacts.getArtifactPath(
        Artifact.flutterFramework,
        platform: TargetPlatform.android_arm,
        mode: anyNamed('mode'),
        environmentType: anyNamed('environmentType'),
      )).thenReturn('engine');
      when(mockArtifacts.engineOutPath).thenReturn(fileSystem.path.join('out', 'android_arm'));

      fileSystem.file('out/android_arm/flutter_embedding_release.pom')
        ..createSync(recursive: true)
        ..writeAsStringSync('''
<?xml version="1.0" encoding="UTF-8"?>
<project>
  <version>1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b</version>
  <dependencies>
  </dependencies>
</project>
''');
      fileSystem.file('out/android_arm/armeabi_v7a_release.pom').createSync(recursive: true);
      fileSystem.file('out/android_arm/armeabi_v7a_release.jar').createSync(recursive: true);
      fileSystem.file('out/android_arm/armeabi_v7a_release.maven-metadata.xml').createSync(recursive: true);
      fileSystem.file('out/android_arm/flutter_embedding_release.jar').createSync(recursive: true);
      fileSystem.file('out/android_arm/flutter_embedding_release.pom').createSync(recursive: true);
      fileSystem.file('out/android_arm/flutter_embedding_release.maven-metadata.xml').createSync(recursive: true);

      final File manifestFile = fileSystem.file('pubspec.yaml');
      manifestFile.createSync(recursive: true);
      manifestFile.writeAsStringSync('''
        flutter:
          module:
            androidPackage: com.example.test
        '''
      );

      fileSystem.directory('.android/gradle')
        .createSync(recursive: true);

      fileSystem.directory('.android/gradle/wrapper')
        .createSync(recursive: true);

      fileSystem.file('.android/gradlew').createSync(recursive: true);

      fileSystem.file('.android/gradle.properties')
        .writeAsStringSync('irrelevant');

      fileSystem.file('.android/build.gradle')
        .createSync(recursive: true);

      // Let any process start. Assert after.
      when(mockProcessManager.run(
        any,
        environment: anyNamed('environment'),
        workingDirectory: anyNamed('workingDirectory'),
      )).thenAnswer((_) async => ProcessResult(1, 0, '', ''));

      fileSystem.directory('build/outputs/repo').createSync(recursive: true);

      when(fileSystemUtils.copyDirectorySync(any, any)).thenReturn(null);

      await buildGradleAar(
        androidBuildInfo: const AndroidBuildInfo(BuildInfo(BuildMode.release, null, treeShakeIcons: false)),
        project: FlutterProject.current(),
        outputDirectory: fileSystem.directory('build/'),
        target: '',
        buildNumber: '2.0',
      );

      final List<String> actualGradlewCall = verify(
        mockProcessManager.run(
          captureAny,
          environment: anyNamed('environment'),
          workingDirectory: anyNamed('workingDirectory'),
        ),
      ).captured.last as List<String>;

      expect(actualGradlewCall, contains('/.android/gradlew'));
      expect(actualGradlewCall, contains('-Plocal-engine-out=out/android_arm'));
      expect(actualGradlewCall, contains('-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0'));
      expect(actualGradlewCall, contains('-Plocal-engine-build-mode=release'));
      expect(actualGradlewCall, contains('-PbuildNumber=2.0'));

      // Verify the local engine repo is copied into the generated Maven repo.
      final List<dynamic> copyDirectoryArguments = verify(
        fileSystemUtils.copyDirectorySync(captureAny, captureAny)
      ).captured;

      expect(copyDirectoryArguments.length, 2);
      expect((copyDirectoryArguments.first as Directory).path, '/.tmp_rand0/flutter_tool_local_engine_repo.rand0');
      expect((copyDirectoryArguments.last as Directory).path, 'build/outputs/repo');

    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      AndroidStudio: () => mockAndroidStudio,
      Artifacts: () => mockArtifacts,
      Cache: () => cache,
      Platform: () => android,
      FileSystem: () => fileSystem,
      FileSystemUtils: () => fileSystemUtils,
      ProcessManager: () => mockProcessManager,
    });

    testUsingContext(
        'build aar uses selected local engine,the engine abi is arm64', () async {
      when(mockArtifacts.getArtifactPath(
        Artifact.flutterFramework,
        platform: anyNamed('platform'),
        mode: anyNamed('mode'),
        environmentType: anyNamed('environmentType'),
      )).thenReturn('engine');
      when(mockArtifacts.engineOutPath).thenReturn(fileSystem.path.join('out', 'android_arm64'));

      fileSystem.file('out/android_arm64/flutter_embedding_release.pom')
        ..createSync(recursive: true)
        ..writeAsStringSync('''
<?xml version="1.0" encoding="UTF-8"?>
<project>
  <version>1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b</version>
  <dependencies>
  </dependencies>
</project>
''');
      fileSystem.file('out/android_arm64/arm64_v8a_release.pom').createSync(recursive: true);
      fileSystem.file('out/android_arm64/arm64_v8a_release.jar').createSync(recursive: true);
      fileSystem.file('out/android_arm64/arm64_v8a_release.maven-metadata.xml').createSync(recursive: true);
      fileSystem.file('out/android_arm64/flutter_embedding_release.jar').createSync(recursive: true);
      fileSystem.file('out/android_arm64/flutter_embedding_release.pom').createSync(recursive: true);
      fileSystem.file('out/android_arm64/flutter_embedding_release.maven-metadata.xml').createSync(recursive: true);

      final File manifestFile = fileSystem.file('pubspec.yaml');
      manifestFile.createSync(recursive: true);
      manifestFile.writeAsStringSync('''
        flutter:
          module:
            androidPackage: com.example.test
        '''
      );

      fileSystem.directory('.android/gradle')
          .createSync(recursive: true);

      fileSystem.directory('.android/gradle/wrapper')
          .createSync(recursive: true);

      fileSystem.file('.android/gradlew').createSync(recursive: true);

      fileSystem.file('.android/gradle.properties')
          .writeAsStringSync('irrelevant');

      fileSystem.file('.android/build.gradle')
          .createSync(recursive: true);

      // Let any process start. Assert after.
      when(mockProcessManager.run(
        any,
        environment: anyNamed('environment'),
        workingDirectory: anyNamed('workingDirectory'),
      )).thenAnswer((_) async => ProcessResult(1, 0, '', ''));

      fileSystem.directory('build/outputs/repo').createSync(recursive: true);

      when(fileSystemUtils.copyDirectorySync(any, any)).thenReturn(null);

      await buildGradleAar(
        androidBuildInfo: const AndroidBuildInfo(
            BuildInfo(BuildMode.release, null, treeShakeIcons: false)),
        project: FlutterProject.current(),
        outputDirectory: fileSystem.directory('build/'),
        target: '',
        buildNumber: '2.0',
      );

      final List<String> actualGradlewCall = verify(
        mockProcessManager.run(
          captureAny,
          environment: anyNamed('environment'),
          workingDirectory: anyNamed('workingDirectory'),
        ),
      ).captured.last as List<String>;

      expect(actualGradlewCall, contains('/.android/gradlew'));
      expect(actualGradlewCall, contains('-Plocal-engine-out=out/android_arm64'));
      expect(actualGradlewCall, contains('-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0'));
      expect(actualGradlewCall, contains('-Plocal-engine-build-mode=release'));
      expect(actualGradlewCall, contains('-PbuildNumber=2.0'));

      // Verify the local engine repo is copied into the generated Maven repo.
      final List<dynamic> copyDirectoryArguments = verify(
          fileSystemUtils.copyDirectorySync(captureAny, captureAny)
      ).captured;

      expect(copyDirectoryArguments.length, 2);
      expect((copyDirectoryArguments.first as Directory).path, '/.tmp_rand0/flutter_tool_local_engine_repo.rand0');
      expect((copyDirectoryArguments.last as Directory).path, 'build/outputs/repo');

    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      AndroidStudio: () => mockAndroidStudio,
      Artifacts: () => mockArtifacts,
      Cache: () => cache,
      Platform: () => android,
      FileSystem: () => fileSystem,
      FileSystemUtils: () => fileSystemUtils,
      ProcessManager: () => mockProcessManager,
    });

    testUsingContext(
        'build aar uses selected local engine,the engine abi is x86', () async {
      when(mockArtifacts.getArtifactPath(
        Artifact.flutterFramework,
        platform: anyNamed('platform'),
        mode: anyNamed('mode'),
        environmentType: anyNamed('environmentType'),
      )).thenReturn('engine');
      when(mockArtifacts.engineOutPath).thenReturn(fileSystem.path.join('out', 'android_x86'));

      fileSystem.file('out/android_x86/flutter_embedding_release.pom')
        ..createSync(recursive: true)
        ..writeAsStringSync('''
<?xml version="1.0" encoding="UTF-8"?>
<project>
  <version>1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b</version>
  <dependencies>
  </dependencies>
</project>
''');
      fileSystem.file('out/android_x86/x86_release.pom').createSync(recursive: true);
      fileSystem.file('out/android_x86/x86_release.jar').createSync(recursive: true);
      fileSystem.file('out/android_x86/x86_release.maven-metadata.xml').createSync(recursive: true);
      fileSystem.file('out/android_x86/flutter_embedding_release.jar').createSync(recursive: true);
      fileSystem.file('out/android_x86/flutter_embedding_release.pom').createSync(recursive: true);
      fileSystem.file('out/android_x86/flutter_embedding_release.maven-metadata.xml').createSync(recursive: true);

      final File manifestFile = fileSystem.file('pubspec.yaml');
      manifestFile.createSync(recursive: true);
      manifestFile.writeAsStringSync('''
        flutter:
          module:
            androidPackage: com.example.test
        '''
      );

      fileSystem.directory('.android/gradle')
          .createSync(recursive: true);

      fileSystem.directory('.android/gradle/wrapper')
          .createSync(recursive: true);

      fileSystem.file('.android/gradlew').createSync(recursive: true);

      fileSystem.file('.android/gradle.properties')
          .writeAsStringSync('irrelevant');

      fileSystem.file('.android/build.gradle')
          .createSync(recursive: true);

      // Let any process start. Assert after.
      when(mockProcessManager.run(
        any,
        environment: anyNamed('environment'),
        workingDirectory: anyNamed('workingDirectory'),
      )).thenAnswer((_) async => ProcessResult(1, 0, '', ''));

      fileSystem.directory('build/outputs/repo').createSync(recursive: true);

      when(fileSystemUtils.copyDirectorySync(any, any)).thenReturn(null);

      await buildGradleAar(
        androidBuildInfo: const AndroidBuildInfo(
            BuildInfo(BuildMode.release, null, treeShakeIcons: false)),
        project: FlutterProject.current(),
        outputDirectory: fileSystem.directory('build/'),
        target: '',
        buildNumber: '2.0',
      );

      final List<String> actualGradlewCall = verify(
        mockProcessManager.run(
          captureAny,
          environment: anyNamed('environment'),
          workingDirectory: anyNamed('workingDirectory'),
        ),
      ).captured.last as List<String>;

      expect(actualGradlewCall, contains('/.android/gradlew'));
      expect(actualGradlewCall, contains('-Plocal-engine-out=out/android_x86'));
      expect(actualGradlewCall, contains('-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0'));
      expect(actualGradlewCall, contains('-Plocal-engine-build-mode=release'));
      expect(actualGradlewCall, contains('-PbuildNumber=2.0'));

      // Verify the local engine repo is copied into the generated Maven repo.
      final List<dynamic> copyDirectoryArguments = verify(
          fileSystemUtils.copyDirectorySync(captureAny, captureAny)
      ).captured;

      expect(copyDirectoryArguments.length, 2);
      expect((copyDirectoryArguments.first as Directory).path, '/.tmp_rand0/flutter_tool_local_engine_repo.rand0');
      expect((copyDirectoryArguments.last as Directory).path, 'build/outputs/repo');

    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      AndroidStudio: () => mockAndroidStudio,
      Artifacts: () => mockArtifacts,
      Cache: () => cache,
      Platform: () => android,
      FileSystem: () => fileSystem,
      FileSystemUtils: () => fileSystemUtils,
      ProcessManager: () => mockProcessManager,
    });

    testUsingContext(
        'build aar uses selected local engine,the engine abi is x64', () async {
      when(mockArtifacts.getArtifactPath(
        Artifact.flutterFramework,
        platform: anyNamed('platform'),
        mode: anyNamed('mode'),
        environmentType: anyNamed('environmentType'),
      )).thenReturn('engine');
      when(mockArtifacts.engineOutPath).thenReturn(fileSystem.path.join('out', 'android_x64'));

      fileSystem.file('out/android_x64/flutter_embedding_release.pom')
        ..createSync(recursive: true)
        ..writeAsStringSync('''
<?xml version="1.0" encoding="UTF-8"?>
<project>
  <version>1.0.0-73fd6b049a80bcea2db1f26c7cee434907cd188b</version>
  <dependencies>
  </dependencies>
</project>
''');
      fileSystem.file('out/android_x64/x86_64_release.pom').createSync(recursive: true);
      fileSystem.file('out/android_x64/x86_64_release.jar').createSync(recursive: true);
      fileSystem.file('out/android_x64/x86_64_release.maven-metadata.xml').createSync(recursive: true);
      fileSystem.file('out/android_x64/flutter_embedding_release.jar').createSync(recursive: true);
      fileSystem.file('out/android_x64/flutter_embedding_release.pom').createSync(recursive: true);
      fileSystem.file('out/android_x64/flutter_embedding_release.maven-metadata.xml').createSync(recursive: true);

      final File manifestFile = fileSystem.file('pubspec.yaml');
      manifestFile.createSync(recursive: true);
      manifestFile.writeAsStringSync('''
        flutter:
          module:
            androidPackage: com.example.test
        '''
      );

      fileSystem.directory('.android/gradle')
          .createSync(recursive: true);

      fileSystem.directory('.android/gradle/wrapper')
          .createSync(recursive: true);

      fileSystem.file('.android/gradlew').createSync(recursive: true);

      fileSystem.file('.android/gradle.properties')
          .writeAsStringSync('irrelevant');

      fileSystem.file('.android/build.gradle')
          .createSync(recursive: true);

      // Let any process start. Assert after.
      when(mockProcessManager.run(
        any,
        environment: anyNamed('environment'),
        workingDirectory: anyNamed('workingDirectory'),
      )).thenAnswer((_) async => ProcessResult(1, 0, '', ''));

      fileSystem.directory('build/outputs/repo').createSync(recursive: true);

      when(fileSystemUtils.copyDirectorySync(any, any)).thenReturn(null);

      await buildGradleAar(
        androidBuildInfo: const AndroidBuildInfo(
            BuildInfo(BuildMode.release, null, treeShakeIcons: false)),
        project: FlutterProject.current(),
        outputDirectory: fileSystem.directory('build/'),
        target: '',
        buildNumber: '2.0',
      );

      final List<String> actualGradlewCall = verify(
        mockProcessManager.run(
          captureAny,
          environment: anyNamed('environment'),
          workingDirectory: anyNamed('workingDirectory'),
        ),
      ).captured.last as List<String>;

      expect(actualGradlewCall, contains('/.android/gradlew'));
      expect(actualGradlewCall, contains('-Plocal-engine-out=out/android_x64'));
      expect(actualGradlewCall, contains('-Plocal-engine-repo=/.tmp_rand0/flutter_tool_local_engine_repo.rand0'));
      expect(actualGradlewCall, contains('-Plocal-engine-build-mode=release'));
      expect(actualGradlewCall, contains('-PbuildNumber=2.0'));

      // Verify the local engine repo is copied into the generated Maven repo.
      final List<dynamic> copyDirectoryArguments = verify(
          fileSystemUtils.copyDirectorySync(captureAny, captureAny)
      ).captured;

      expect(copyDirectoryArguments.length, 2);
      expect((copyDirectoryArguments.first as Directory).path, '/.tmp_rand0/flutter_tool_local_engine_repo.rand0');
      expect((copyDirectoryArguments.last as Directory).path, 'build/outputs/repo');

    }, overrides: <Type, Generator>{
      AndroidSdk: () => mockAndroidSdk,
      AndroidStudio: () => mockAndroidStudio,
      Artifacts: () => mockArtifacts,
      Cache: () => cache,
      Platform: () => android,
      FileSystem: () => fileSystem,
      FileSystemUtils: () => fileSystemUtils,
      ProcessManager: () => mockProcessManager,
    });
  });

  group('printHowToConsumeAar', () {
    BufferLogger logger;
    FileSystem fileSystem;

    setUp(() {
      logger = BufferLogger.test();
      fileSystem = MemoryFileSystem.test();
    });

    testWithoutContext('stdout contains release, debug and profile', () async {
      printHowToConsumeAar(
        buildModes: const <String>{'release', 'debug', 'profile'},
        androidPackage: 'com.mycompany',
        repoDirectory: fileSystem.directory('build/'),
        buildNumber: '2.2',
        logger: logger,
        fileSystem: fileSystem,
      );

      expect(
        logger.statusText,
        contains(
          '\n'
          'Consuming the Module\n'
          '  1. Open <host>/app/build.gradle\n'
          '  2. Ensure you have the repositories configured, otherwise add them:\n'
          '\n'
          '      String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"\n'
          '      repositories {\n'
          '        maven {\n'
          "            url 'build/'\n"
          '        }\n'
          '        maven {\n'
          '            url "\$storageUrl/download.flutter.io"\n'
          '        }\n'
          '      }\n'
          '\n'
          '  3. Make the host app depend on the Flutter module:\n'
          '\n'
          '    dependencies {\n'
          "      releaseImplementation 'com.mycompany:flutter_release:2.2'\n"
          "      debugImplementation 'com.mycompany:flutter_debug:2.2'\n"
          "      profileImplementation 'com.mycompany:flutter_profile:2.2'\n"
          '    }\n'
          '\n'
          '\n'
          '  4. Add the `profile` build type:\n'
          '\n'
          '    android {\n'
          '      buildTypes {\n'
          '        profile {\n'
          '          initWith debug\n'
          '        }\n'
          '      }\n'
          '    }\n'
          '\n'
          'To learn more, visit https://flutter.dev/go/build-aar\n'
        )
      );
    });

    testWithoutContext('stdout contains release', () async {
      printHowToConsumeAar(
        buildModes: const <String>{'release'},
        androidPackage: 'com.mycompany',
        repoDirectory: fileSystem.directory('build/'),
        logger: logger,
        fileSystem: fileSystem,
      );

      expect(
        logger.statusText,
        contains(
          '\n'
          'Consuming the Module\n'
          '  1. Open <host>/app/build.gradle\n'
          '  2. Ensure you have the repositories configured, otherwise add them:\n'
          '\n'
          '      String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"\n'
          '      repositories {\n'
          '        maven {\n'
          "            url 'build/'\n"
          '        }\n'
          '        maven {\n'
          '            url "\$storageUrl/download.flutter.io"\n'
          '        }\n'
          '      }\n'
          '\n'
          '  3. Make the host app depend on the Flutter module:\n'
          '\n'
          '    dependencies {\n'
          "      releaseImplementation 'com.mycompany:flutter_release:1.0'\n"
          '    }\n'
          '\n'
          'To learn more, visit https://flutter.dev/go/build-aar\n'
        )
      );
    });

    testWithoutContext('stdout contains debug', () async {
      printHowToConsumeAar(
        buildModes: const <String>{'debug'},
        androidPackage: 'com.mycompany',
        repoDirectory: fileSystem.directory('build/'),
        logger: logger,
        fileSystem: fileSystem,
      );

      expect(
        logger.statusText,
        contains(
          '\n'
          'Consuming the Module\n'
          '  1. Open <host>/app/build.gradle\n'
          '  2. Ensure you have the repositories configured, otherwise add them:\n'
          '\n'
          '      String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"\n'
          '      repositories {\n'
          '        maven {\n'
          "            url 'build/'\n"
          '        }\n'
          '        maven {\n'
          '            url "\$storageUrl/download.flutter.io"\n'
          '        }\n'
          '      }\n'
          '\n'
          '  3. Make the host app depend on the Flutter module:\n'
          '\n'
          '    dependencies {\n'
          "      debugImplementation 'com.mycompany:flutter_debug:1.0'\n"
          '    }\n'
          '\n'
          'To learn more, visit https://flutter.dev/go/build-aar\n'
        )
      );
    });

    testWithoutContext('stdout contains profile', () async {
      printHowToConsumeAar(
        buildModes: const <String>{'profile'},
        androidPackage: 'com.mycompany',
        repoDirectory: fileSystem.directory('build/'),
        buildNumber: '1.0',
        logger: logger,
        fileSystem: fileSystem,
      );

      expect(
        logger.statusText,
        contains(
          '\n'
          'Consuming the Module\n'
          '  1. Open <host>/app/build.gradle\n'
          '  2. Ensure you have the repositories configured, otherwise add them:\n'
          '\n'
          '      String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"\n'
          '      repositories {\n'
          '        maven {\n'
          "            url 'build/'\n"
          '        }\n'
          '        maven {\n'
          '            url "\$storageUrl/download.flutter.io"\n'
          '        }\n'
          '      }\n'
          '\n'
          '  3. Make the host app depend on the Flutter module:\n'
          '\n'
          '    dependencies {\n'
          "      profileImplementation 'com.mycompany:flutter_profile:1.0'\n"
          '    }\n'
          '\n'
          '\n'
          '  4. Add the `profile` build type:\n'
          '\n'
          '    android {\n'
          '      buildTypes {\n'
          '        profile {\n'
          '          initWith debug\n'
          '        }\n'
          '      }\n'
          '    }\n'
          '\n'
          'To learn more, visit https://flutter.dev/go/build-aar\n'
        )
      );
    });
  });

  test('Current settings.gradle is in our legacy settings.gradle file set', () {
    // If this test fails, you probably edited templates/app/android.tmpl.
    // That's fine, but you now need to add a copy of that file to gradle/settings.gradle.legacy_versions, separated
    // from the previous versions by a line that just says ";EOF".
    final File templateSettingsDotGradle = globals.fs.file(globals.fs.path.join(Cache.flutterRoot, 'packages', 'flutter_tools', 'templates', 'app', 'android.tmpl', 'settings.gradle'));
    final File legacySettingsDotGradleFiles = globals.fs.file(globals.fs.path.join(Cache.flutterRoot, 'packages','flutter_tools', 'gradle', 'settings.gradle.legacy_versions'));
    expect(
      legacySettingsDotGradleFiles.readAsStringSync().split(';EOF').map<String>((String body) => body.trim()),
      contains(templateSettingsDotGradle.readAsStringSync().trim()),
    );
  }, skip: true); // TODO(jonahwilliams): This is an integration test and should be moved to the integration shard.
}

/// Generates a fake app bundle at the location [directoryName]/[fileName].
FlutterProject generateFakeAppBundle(String directoryName, String fileName, FileSystem fileSystem) {
  final FlutterProject project = MockFlutterProject();
  final AndroidProject androidProject = MockAndroidProject();

  when(project.isModule).thenReturn(false);
  when(project.android).thenReturn(androidProject);
  when(androidProject.buildDirectory).thenReturn(fileSystem.directory('irrelevant'));

  final Directory bundleDirectory = getBundleDirectory(project);
  bundleDirectory
    .childDirectory(directoryName)
    .createSync(recursive: true);

  bundleDirectory
    .childDirectory(directoryName)
    .childFile(fileName)
    .createSync();
  return project;
}

FakePlatform fakePlatform(String name) {
  return FakePlatform(
    environment: <String, String>{'HOME': '/path/to/home'},
    operatingSystem: name,
    stdoutSupportsAnsi: false,
  );
}

class FakeGradleUtils extends GradleUtils {
  @override
  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 MockFileSystemUtils extends Mock implements FileSystemUtils {}
class MockFlutterProject extends Mock implements FlutterProject {}
class MockLocalEngineArtifacts extends Mock implements LocalEngineArtifacts {}
class MockProcessManager extends Mock implements ProcessManager {}