Reland "Allow for gradle downloading missing SDK assets" (#28097) (#28355)

* Allow for gradle downloading missing SDK assets if SDK licenses are present.

*  Improvements for windows testing
diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart
index 73aa44b..45e46bc 100644
--- a/packages/flutter_tools/lib/src/android/android_device.dart
+++ b/packages/flutter_tools/lib/src/android/android_device.dart
@@ -372,7 +372,7 @@
     if (buildInfo.targetPlatform == null && devicePlatform == TargetPlatform.android_arm64)
       buildInfo = buildInfo.withTargetPlatform(TargetPlatform.android_arm64);
 
-    if (!prebuiltApplication) {
+    if (!prebuiltApplication || androidSdk.licensesAvailable && androidSdk.latestVersion == null) {
       printTrace('Building APK');
       final FlutterProject project = await FlutterProject.current();
       await buildApk(
diff --git a/packages/flutter_tools/lib/src/android/android_sdk.dart b/packages/flutter_tools/lib/src/android/android_sdk.dart
index 0bb5823..73432f0 100644
--- a/packages/flutter_tools/lib/src/android/android_sdk.dart
+++ b/packages/flutter_tools/lib/src/android/android_sdk.dart
@@ -263,7 +263,7 @@
 
 class AndroidSdk {
   AndroidSdk(this.directory, [this.ndk]) {
-    _init();
+    reinitialize();
   }
 
   static const String _javaHomeEnvironmentVariable = 'JAVA_HOME';
@@ -278,6 +278,23 @@
   List<AndroidSdkVersion> _sdkVersions;
   AndroidSdkVersion _latestVersion;
 
+  /// Whether the `platform-tools` directory exists in the Android SDK.
+  ///
+  /// It is possible to have an Android SDK folder that is missing this with
+  /// the expectation that it will be downloaded later, e.g. by gradle or the
+  /// sdkmanager. The [licensesAvailable] property should be used to determine
+  /// whether the licenses are at least possibly accepted.
+  bool get platformToolsAvailable => fs.directory(fs.path.join(directory, 'platform-tools')).existsSync();
+
+  /// Whether the `licenses` directory exists in the Android SDK.
+  ///
+  /// The existence of this folder normally indicates that the SDK licenses have
+  /// been accepted, e.g. via the sdkmanager, Android Studio, or by copying them
+  /// from another workstation such as in CI scenarios. If these files are valid
+  /// gradle or the sdkmanager will be able to download and use other parts of
+  /// the SDK on demand.
+  bool get licensesAvailable => fs.directory(fs.path.join(directory, 'licenses')).existsSync();
+
   static AndroidSdk locateAndroidSdk() {
     String findAndroidHomeDir() {
       String androidHomeDir;
@@ -348,14 +365,14 @@
   }
 
   static bool validSdkDirectory(String dir) {
-    return fs.isDirectorySync(fs.path.join(dir, 'platform-tools'));
+    return fs.isDirectorySync(fs.path.join(dir, 'licenses'));
   }
 
   List<AndroidSdkVersion> get sdkVersions => _sdkVersions;
 
   AndroidSdkVersion get latestVersion => _latestVersion;
 
-  String get adbPath => getPlatformToolsPath('adb');
+  String get adbPath => getPlatformToolsPath(platform.isWindows ? 'adb.exe' : 'adb');
 
   String get emulatorPath => getEmulatorPath();
 
@@ -376,8 +393,8 @@
   /// Validate the Android SDK. This returns an empty list if there are no
   /// issues; otherwise, it returns a list of issues found.
   List<String> validateSdkWellFormed() {
-    if (!processManager.canRun(adbPath))
-      return <String>['Android SDK file not found: $adbPath.'];
+    if (adbPath == null || !processManager.canRun(adbPath))
+      return <String>['Android SDK file not found: ${adbPath ?? 'adb'}.'];
 
     if (sdkVersions.isEmpty || latestVersion == null) {
       final StringBuffer msg = StringBuffer('No valid Android SDK platforms found in ${_platformsDir.path}.');
@@ -396,7 +413,10 @@
   }
 
   String getPlatformToolsPath(String binaryName) {
-    return fs.path.join(directory, 'platform-tools', binaryName);
+    final String path = fs.path.join(directory, 'platform-tools', binaryName);
+    if (fs.file(path).existsSync())
+      return path;
+    return null;
   }
 
   String getEmulatorPath() {
@@ -420,7 +440,11 @@
     return null;
   }
 
-  void _init() {
+  /// Sets up various paths used internally.
+  ///
+  /// This method should be called in a case where the tooling may have updated
+  /// SDK artifacts, such as after running a gradle build.
+  void reinitialize() {
     List<Version> buildTools = <Version>[]; // 19.1.0, 22.0.1, ...
 
     final Directory buildToolsDir = fs.directory(fs.path.join(directory, 'build-tools'));
diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart
index d7376fb..5da192c 100644
--- a/packages/flutter_tools/lib/src/android/android_workflow.dart
+++ b/packages/flutter_tools/lib/src/android/android_workflow.dart
@@ -105,6 +105,11 @@
       return ValidationResult(ValidationType.missing, messages);
     }
 
+    if (androidSdk.licensesAvailable && !androidSdk.platformToolsAvailable) {
+      messages.add(ValidationMessage.hint(userMessages.androidSdkLicenseOnly(kAndroidHome)));
+      return ValidationResult(ValidationType.partial, messages);
+    }
+
     messages.add(ValidationMessage(userMessages.androidSdkLocation(androidSdk.directory)));
 
     messages.add(ValidationMessage(androidSdk.ndk == null
@@ -249,7 +254,9 @@
       }
     }
 
-    _ensureCanRunSdkManager();
+    if (!_canRunSdkManager()) {
+      return LicensesAccepted.unknown;
+    }
 
     final Process process = await runCommand(
       <String>[androidSdk.sdkManagerPath, '--licenses'],
@@ -279,7 +286,9 @@
       return false;
     }
 
-    _ensureCanRunSdkManager();
+    if (!_canRunSdkManager()) {
+      throwToolExit(userMessages.androidMissingSdkManager(androidSdk.sdkManagerPath));
+    }
 
     final Version sdkManagerVersion = Version.parse(androidSdk.sdkManagerVersion);
     if (sdkManagerVersion == null || sdkManagerVersion.major < 26) {
@@ -306,10 +315,9 @@
     return exitCode == 0;
   }
 
-  static void _ensureCanRunSdkManager() {
+  static bool _canRunSdkManager() {
     assert(androidSdk != null);
     final String sdkManagerPath = androidSdk.sdkManagerPath;
-    if (!processManager.canRun(sdkManagerPath))
-      throwToolExit(userMessages.androidMissingSdkManager(sdkManagerPath));
+    return processManager.canRun(sdkManagerPath);
   }
 }
diff --git a/packages/flutter_tools/lib/src/android/apk.dart b/packages/flutter_tools/lib/src/android/apk.dart
index 46de467..4a0ded0 100644
--- a/packages/flutter_tools/lib/src/android/apk.dart
+++ b/packages/flutter_tools/lib/src/android/apk.dart
@@ -8,7 +8,6 @@
 
 import '../base/common.dart';
 import '../build_info.dart';
-import '../globals.dart';
 import '../project.dart';
 
 import 'android_sdk.dart';
@@ -32,18 +31,11 @@
   if (androidSdk == null)
     throwToolExit('No Android SDK found. Try setting the ANDROID_SDK_ROOT environment variable.');
 
-  final List<String> validationResult = androidSdk.validateSdkWellFormed();
-  if (validationResult.isNotEmpty) {
-    for (String message in validationResult) {
-      printError(message, wrap: false);
-    }
-    throwToolExit('Try re-installing or updating your Android SDK.');
-  }
-
-  return buildGradleProject(
+  await buildGradleProject(
     project: project,
     buildInfo: buildInfo,
     target: target,
     isBuildingBundle: false
   );
+  androidSdk.reinitialize();
 }
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index a51f6eb..60fb758 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -112,6 +112,21 @@
   return _cachedGradleProject;
 }
 
+/// Runs `gradlew dependencies`, ensuring that dependencies are resolved and
+/// potentially downloaded.
+Future<void> checkGradleDependencies() async {
+  final Status progress = logger.startProgress('Ensuring gradle dependencies are up to date...', timeout: kSlowOperation);
+  final FlutterProject flutterProject = await FlutterProject.current();
+  final String gradle = await _ensureGradle(flutterProject);
+  await runCheckedAsync(
+    <String>[gradle, 'dependencies'],
+    workingDirectory: flutterProject.android.hostAppGradleRoot.path,
+    environment: _gradleEnv,
+  );
+  androidSdk.reinitialize();
+  progress.stop();
+}
+
 // Note: Dependencies are resolved and possibly downloaded as a side-effect
 // of calculating the app properties using Gradle. This may take minutes.
 Future<GradleProject> _readGradleProject() async {
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart
index e274519..3822ea2 100644
--- a/packages/flutter_tools/lib/src/application_package.dart
+++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -34,6 +34,9 @@
       case TargetPlatform.android_arm64:
       case TargetPlatform.android_x64:
       case TargetPlatform.android_x86:
+        if (androidSdk?.licensesAvailable == true  && androidSdk.latestVersion == null) {
+          await checkGradleDependencies();
+        }
         return applicationBinary == null
             ? await AndroidApk.fromAndroidProject((await FlutterProject.current()).android)
             : AndroidApk.fromApk(applicationBinary);
diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart
index f7ceef6..230e7e4 100644
--- a/packages/flutter_tools/lib/src/base/user_messages.dart
+++ b/packages/flutter_tools/lib/src/base/user_messages.dart
@@ -45,6 +45,15 @@
   String androidCantRunJavaBinary(String javaBinary) => 'Cannot execute $javaBinary to determine the version';
   String get androidUnknownJavaVersion => 'Could not determine java version';
   String androidJavaVersion(String javaVersion) => 'Java version $javaVersion';
+  String androidSdkLicenseOnly(String envKey) =>
+      'Android SDK contains licenses only.\n'
+      'Your first build of an Android application will take longer than usual, '
+      'while gradle downloads the missing components. This functionality will '
+      'only work if the licenses in the licenses folder in $envKey are valid.\n'
+      'If the Android SDK has been installed to another location, set $envKey to that location.\n'
+      'You may also want to add it to your PATH environment variable.\n\n'
+      'Certain features, such as `flutter emulators` and `flutter devices`, will '
+      'not work without the currently missing SDK components.';
   String androidBadSdkDir(String envKey, String homeDir) =>
       '$envKey = $homeDir\n'
       'but Android SDK not found at this location.';
@@ -53,7 +62,7 @@
       'Install Android Studio from: https://developer.android.com/studio/index.html\n'
       'On first launch it will assist you in installing the Android SDK components.\n'
       '(or visit https://flutter.io/setup/#android-setup for detailed instructions).\n'
-      'If Android SDK has been installed to a custom location, set $envKey to that location.\n'
+      'If the Android SDK has been installed to a custom location, set $envKey to that location.\n'
       'You may also want to add it to your PATH environment variable.\n';
   String androidSdkLocation(String directory) => 'Android SDK at $directory';
   String androidSdkPlatformToolsVersion(String platform, String tools) =>
@@ -75,7 +84,11 @@
   String get androidLicensesAll => 'All Android licenses accepted.';
   String get androidLicensesSome => 'Some Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses';
   String get androidLicensesNone => 'Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses';
-  String get androidLicensesUnknown => 'Android license status unknown.';
+  String get androidLicensesUnknown =>
+      'Android license status unknown.\n'
+      'Try re-installing or updating your Android SDK Manager.\n'
+      'See https://developer.android.com/studio/#downloads or visit '
+      'https://flutter.io/setup/#android-setup for detailed instructions.';
   String androidSdkManagerOutdated(String managerPath) =>
       'A newer version of the Android SDK is required. To update, run:\n'
       '$managerPath --update\n';
diff --git a/packages/flutter_tools/test/android/android_device_test.dart b/packages/flutter_tools/test/android/android_device_test.dart
index b6988f5..d7945e5 100644
--- a/packages/flutter_tools/test/android/android_device_test.dart
+++ b/packages/flutter_tools/test/android/android_device_test.dart
@@ -27,16 +27,23 @@
   });
 
   group('getAdbDevices', () {
+    final MockProcessManager mockProcessManager = MockProcessManager();
     testUsingContext('throws on missing adb path', () {
       final Directory sdkDir = MockAndroidSdk.createSdkDirectory();
       Config.instance.setValue('android-sdk', sdkDir.path);
 
       final File adbExe = fs.file(getAdbPath(androidSdk));
-      adbExe.deleteSync();
+      when(mockProcessManager.runSync(
+        <String>[adbExe.path, 'devices', '-l'],
+      ))
+      .thenAnswer(
+        (_) => throw ArgumentError(adbExe.path),
+      );
       expect(() => getAdbDevices(), throwsToolExit(message: RegExp('Unable to run "adb".*${adbExe.path}')));
     }, overrides: <Type, Generator>{
       AndroidSdk: () => MockAndroidSdk(),
       FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => mockProcessManager,
     });
 
     testUsingContext('physical devices', () {
diff --git a/packages/flutter_tools/test/android/android_sdk_test.dart b/packages/flutter_tools/test/android/android_sdk_test.dart
index 9ef1711..0a7578b 100644
--- a/packages/flutter_tools/test/android/android_sdk_test.dart
+++ b/packages/flutter_tools/test/android/android_sdk_test.dart
@@ -225,12 +225,13 @@
     bool withSdkManager = true,
   }) {
     final Directory dir = fs.systemTempDirectory.createTempSync('flutter_mock_android_sdk.');
+    final String exe = platform.isWindows ? '.exe' : '';
+    _createSdkFile(dir, 'licenses/dummy');
+    _createSdkFile(dir, 'platform-tools/adb$exe');
 
-    _createSdkFile(dir, 'platform-tools/adb');
-
-    _createSdkFile(dir, 'build-tools/sda/aapt');
-    _createSdkFile(dir, 'build-tools/af/aapt');
-    _createSdkFile(dir, 'build-tools/ljkasd/aapt');
+    _createSdkFile(dir, 'build-tools/sda/aapt$exe');
+    _createSdkFile(dir, 'build-tools/af/aapt$exe');
+    _createSdkFile(dir, 'build-tools/ljkasd/aapt$exe');
 
     _createSdkFile(dir, 'platforms/android-22/android.jar');
     _createSdkFile(dir, 'platforms/android-23/android.jar');
diff --git a/packages/flutter_tools/test/android/android_workflow_test.dart b/packages/flutter_tools/test/android/android_workflow_test.dart
index 4764b1b..c988474 100644
--- a/packages/flutter_tools/test/android/android_workflow_test.dart
+++ b/packages/flutter_tools/test/android/android_workflow_test.dart
@@ -42,11 +42,12 @@
     return (List<String> command) => MockProcess(stdout: stdoutStream);
   }
 
-  testUsingContext('licensesAccepted throws if cannot run sdkmanager', () async {
+  testUsingContext('licensesAccepted returns LicensesAccepted.unknown if cannot run sdkmanager', () async {
     processManager.succeed = false;
     when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager');
     final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator();
-    expect(licenseValidator.licensesAccepted, throwsToolExit());
+    final LicensesAccepted licenseStatus = await licenseValidator.licensesAccepted;
+    expect(licenseStatus, LicensesAccepted.unknown);
   }, overrides: <Type, Generator>{
     AndroidSdk: () => sdk,
     FileSystem: () => fs,
@@ -178,8 +179,27 @@
     Stdio: () => stdio,
   });
 
+  testUsingContext('detects license-only SDK installation', () async {
+    when(sdk.licensesAvailable).thenReturn(true);
+    when(sdk.platformToolsAvailable).thenReturn(false);
+    final ValidationResult validationResult = await AndroidValidator().validate();
+    expect(validationResult.type, ValidationType.partial);
+    expect(
+      validationResult.messages.last.message,
+      userMessages.androidSdkLicenseOnly(kAndroidHome),
+    );
+  }, overrides: <Type, Generator>{
+    AndroidSdk: () => sdk,
+    FileSystem: () => fs,
+    Platform: () => FakePlatform()..environment = <String, String>{'HOME': '/home/me'},
+    ProcessManager: () => processManager,
+    Stdio: () => stdio,
+  });
+
   testUsingContext('detects minium required SDK and buildtools', () async {
     final AndroidSdkVersion mockSdkVersion = MockAndroidSdkVersion();
+    when(sdk.licensesAvailable).thenReturn(true);
+    when(sdk.platformToolsAvailable).thenReturn(true);
 
     // Test with invalid SDK and build tools
     when(mockSdkVersion.sdkLevel).thenReturn(26);
diff --git a/packages/flutter_tools/test/application_package_test.dart b/packages/flutter_tools/test/application_package_test.dart
index 48a1396..099e8a2 100644
--- a/packages/flutter_tools/test/application_package_test.dart
+++ b/packages/flutter_tools/test/application_package_test.dart
@@ -3,18 +3,24 @@
 // found in the LICENSE file.
 
 import 'dart:convert';
+import 'dart:io' show ProcessResult;
 
 import 'package:file/file.dart';
 import 'package:file/memory.dart';
 import 'package:flutter_tools/src/base/platform.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/cache.dart';
+import 'package:flutter_tools/src/project.dart';
 import 'package:mockito/mockito.dart';
 
 import 'package:flutter_tools/src/application_package.dart';
+import 'package:flutter_tools/src/android/android_sdk.dart';
 import 'package:flutter_tools/src/base/context.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/base/os.dart';
 import 'package:flutter_tools/src/ios/ios_workflow.dart';
+import 'package:process/process.dart';
 
 import 'src/common.dart';
 import 'src/context.dart';
@@ -24,7 +30,78 @@
   Platform: _kNoColorTerminalPlatform,
 };
 
+class MockitoProcessManager extends Mock implements ProcessManager {}
+class MockitoAndroidSdk extends Mock implements AndroidSdk {}
+class MockitoAndroidSdkVersion extends Mock implements AndroidSdkVersion {}
+
 void main() {
+  group('Apk with partial Android SDK works', () {
+    AndroidSdk sdk;
+    ProcessManager mockProcessManager;
+    MemoryFileSystem fs;
+    File gradle;
+    final Map<Type, Generator> overrides = <Type, Generator>{
+      AndroidSdk: () => sdk,
+      ProcessManager: () => mockProcessManager,
+      FileSystem: () => fs,
+    };
+
+    setUp(() async {
+      sdk = MockitoAndroidSdk();
+      mockProcessManager = MockitoProcessManager();
+      fs = MemoryFileSystem();
+      Cache.flutterRoot = '../..';
+      when(sdk.licensesAvailable).thenReturn(true);
+      when(mockProcessManager.canRun(any)).thenReturn(true);
+      when(mockProcessManager.run(
+        any,
+        workingDirectory: anyNamed('workingDirectory'),
+        environment: anyNamed('environment'),
+      )).thenAnswer((_) async => ProcessResult(1, 0, 'stdout', 'stderr'));
+      when(mockProcessManager.runSync(any)).thenReturn(ProcessResult(1, 0, 'stdout', 'stderr'));
+      final FlutterProject project = await FlutterProject.current();
+      gradle = fs.file(project.android.hostAppGradleRoot.childFile(
+        platform.isWindows ? 'gradlew.bat' : 'gradlew',
+      ).path)..createSync(recursive: true);
+    });
+
+    testUsingContext('Licenses available, build tools not, apk exists', () async {
+      when(sdk.latestVersion).thenReturn(null);
+      final FlutterProject project = await FlutterProject.current();
+      final File gradle = project.android.hostAppGradleRoot.childFile(
+        platform.isWindows ? 'gradlew.bat' : 'gradlew',
+      )..createSync(recursive: true);
+
+      await ApplicationPackageFactory.instance.getPackageForPlatform(
+        TargetPlatform.android_arm,
+        applicationBinary: fs.file('app.apk'),
+      );
+      verify(
+        mockProcessManager.run(
+          argThat(equals(<String>[gradle.path, 'dependencies'])),
+          workingDirectory: anyNamed('workingDirectory'),
+          environment: anyNamed('environment'),
+        ),
+      ).called(1);
+    }, overrides: overrides);
+
+    testUsingContext('Licenses available, build tools available, does not call gradle dependencies', () async {
+      final AndroidSdkVersion sdkVersion = MockitoAndroidSdkVersion();
+      when(sdk.latestVersion).thenReturn(sdkVersion);
+
+      await ApplicationPackageFactory.instance.getPackageForPlatform(
+        TargetPlatform.android_arm,
+      );
+      verifyNever(
+        mockProcessManager.run(
+          argThat(equals(<String>[gradle.path, 'dependencies'])),
+          workingDirectory: anyNamed('workingDirectory'),
+          environment: anyNamed('environment'),
+        ),
+      );
+    }, overrides: overrides);
+  });
+
   group('ApkManifestData', () {
     test('Parses manifest with an Activity that has enabled set to true, action set to android.intent.action.MAIN and category set to android.intent.category.LAUNCHER', () {
       final ApkManifestData data = ApkManifestData.parseFromXmlDump(_aaptDataWithExplicitEnabledAndMainLauncherActivity);
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart
index 015b96e..0150d4c 100644
--- a/packages/flutter_tools/test/src/mocks.dart
+++ b/packages/flutter_tools/test/src/mocks.dart
@@ -11,6 +11,7 @@
 import 'package:flutter_tools/src/application_package.dart';
 import 'package:flutter_tools/src/base/file_system.dart' hide IOSink;
 import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/platform.dart';
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/compile.dart';
 import 'package:flutter_tools/src/devfs.dart';
@@ -44,16 +45,26 @@
     int ndkVersion = 16,
     bool withNdkSysroot = false,
     bool withSdkManager = true,
+    bool withPlatformTools = true,
+    bool withBuildTools = true,
   }) {
     final Directory dir = fs.systemTempDirectory.createTempSync('flutter_mock_android_sdk.');
+    final String exe = platform.isWindows ? '.exe' : '';
+    final String bat = platform.isWindows ? '.bat' : '';
 
-    _createSdkFile(dir, 'platform-tools/adb');
+    _createDir(dir, 'licenses');
 
-    _createSdkFile(dir, 'build-tools/19.1.0/aapt');
-    _createSdkFile(dir, 'build-tools/22.0.1/aapt');
-    _createSdkFile(dir, 'build-tools/23.0.2/aapt');
-    if (withAndroidN)
-      _createSdkFile(dir, 'build-tools/24.0.0-preview/aapt');
+    if (withPlatformTools) {
+      _createSdkFile(dir, 'platform-tools/adb$exe');
+    }
+
+    if (withBuildTools) {
+      _createSdkFile(dir, 'build-tools/19.1.0/aapt$exe');
+      _createSdkFile(dir, 'build-tools/22.0.1/aapt$exe');
+      _createSdkFile(dir, 'build-tools/23.0.2/aapt$exe');
+      if (withAndroidN)
+        _createSdkFile(dir, 'build-tools/24.0.0-preview/aapt$exe');
+    }
 
     _createSdkFile(dir, 'platforms/android-22/android.jar');
     _createSdkFile(dir, 'platforms/android-23/android.jar');
@@ -63,7 +74,7 @@
     }
 
     if (withSdkManager)
-      _createSdkFile(dir, 'tools/bin/sdkmanager');
+      _createSdkFile(dir, 'tools/bin/sdkmanager$bat');
 
     if (withNdkDir != null) {
       final String ndkToolchainBin = fs.path.join(