Add missing files in the Gradle wrapper directory (#39145)

diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index 9f5953d..2b14a1a 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -268,13 +268,10 @@
   final File gradle = directory.childFile(
     platform.isWindows ? 'gradlew.bat' : 'gradlew',
   );
-
   if (gradle.existsSync()) {
-    os.makeExecutable(gradle);
     return gradle.absolute.path;
-  } else {
-    return null;
   }
+  return null;
 }
 
 Future<String> _ensureGradle(FlutterProject project) async {
@@ -286,12 +283,12 @@
 // of validating the Gradle executable. This may take several seconds.
 Future<String> _initializeGradle(FlutterProject project) async {
   final Directory android = project.android.hostAppGradleRoot;
-  final Status status = logger.startProgress('Initializing gradle...', timeout: timeoutConfiguration.slowOperation);
-  String gradle = _locateGradlewExecutable(android);
-  if (gradle == null) {
-    injectGradleWrapper(android);
-    gradle = _locateGradlewExecutable(android);
-  }
+  final Status status = logger.startProgress('Initializing gradle...',
+      timeout: timeoutConfiguration.slowOperation);
+
+  injectGradleWrapperIfNeeded(android);
+
+  final String gradle = _locateGradlewExecutable(android);
   if (gradle == null)
     throwToolExit('Unable to locate gradlew script');
   printTrace('Using gradle from $gradle.');
@@ -302,11 +299,25 @@
   return gradle;
 }
 
-/// Injects the Gradle wrapper into the specified directory.
-void injectGradleWrapper(Directory directory) {
-  copyDirectorySync(cache.getArtifactDirectory('gradle_wrapper'), directory);
-  _locateGradlewExecutable(directory);
-  final File propertiesFile = directory.childFile(fs.path.join('gradle', 'wrapper', 'gradle-wrapper.properties'));
+/// Injects the Gradle wrapper files if any of these files don't exist in [directory].
+void injectGradleWrapperIfNeeded(Directory directory) {
+  copyDirectorySync(
+    cache.getArtifactDirectory('gradle_wrapper'),
+    directory,
+    shouldCopyFile: (File sourceFile, File destinationFile) {
+      // Don't override the existing files in the project.
+      return !destinationFile.existsSync();
+    },
+    onFileCopied: (File sourceFile, File destinationFile) {
+      final String modes = sourceFile.statSync().modeString();
+      if (modes != null && modes.contains('x')) {
+        os.makeExecutable(destinationFile);
+      }
+    },
+  );
+  // Add the `gradle-wrapper.properties` file if it doesn't exist.
+  final File propertiesFile = directory.childFile(
+      fs.path.join('gradle', 'wrapper', 'gradle-wrapper.properties'));
   if (!propertiesFile.existsSync()) {
     final String gradleVersion = getGradleVersionForAndroidPlugin(directory);
     propertiesFile.writeAsStringSync('''
diff --git a/packages/flutter_tools/lib/src/base/file_system.dart b/packages/flutter_tools/lib/src/base/file_system.dart
index 1048e32..3f7dadb 100644
--- a/packages/flutter_tools/lib/src/base/file_system.dart
+++ b/packages/flutter_tools/lib/src/base/file_system.dart
@@ -64,11 +64,18 @@
   }
 }
 
-/// Recursively copies `srcDir` to `destDir`, invoking [onFileCopied] if
-/// specified for each source/destination file pair.
+/// Creates `destDir` if needed, then recursively copies `srcDir` to `destDir`,
+/// invoking [onFileCopied], if specified, for each source/destination file pair.
 ///
-/// Creates `destDir` if needed.
-void copyDirectorySync(Directory srcDir, Directory destDir, [ void onFileCopied(File srcFile, File destFile) ]) {
+/// Skips files if [shouldCopyFile] returns `false`.
+void copyDirectorySync(
+  Directory srcDir,
+  Directory destDir,
+  {
+    bool shouldCopyFile(File srcFile, File destFile),
+    void onFileCopied(File srcFile, File destFile),
+  }
+) {
   if (!srcDir.existsSync())
     throw Exception('Source directory "${srcDir.path}" does not exist, nothing to copy');
 
@@ -79,11 +86,18 @@
     final String newPath = destDir.fileSystem.path.join(destDir.path, entity.basename);
     if (entity is File) {
       final File newFile = destDir.fileSystem.file(newPath);
+      if (shouldCopyFile != null && !shouldCopyFile(entity, newFile)) {
+        continue;
+      }
       newFile.writeAsBytesSync(entity.readAsBytesSync());
       onFileCopied?.call(entity, newFile);
     } else if (entity is Directory) {
       copyDirectorySync(
-        entity, destDir.fileSystem.directory(newPath));
+        entity,
+        destDir.fileSystem.directory(newPath),
+        shouldCopyFile: shouldCopyFile,
+        onFileCopied: onFileCopied,
+      );
     } else {
       throw Exception('${entity.path} is neither File nor Directory');
     }
diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart
index c29458a..3dee11a 100644
--- a/packages/flutter_tools/lib/src/commands/create.dart
+++ b/packages/flutter_tools/lib/src/commands/create.dart
@@ -628,7 +628,7 @@
     copyDirectorySync(
       cache.getArtifactDirectory('gradle_wrapper'),
       project.android.hostAppGradleRoot,
-      (File sourceFile, File destinationFile) {
+      onFileCopied: (File sourceFile, File destinationFile) {
         filesCreated++;
         final String modes = sourceFile.statSync().modeString();
         if (modes != null && modes.contains('x')) {
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart
index dc2552c..e33f679 100644
--- a/packages/flutter_tools/lib/src/project.dart
+++ b/packages/flutter_tools/lib/src/project.dart
@@ -580,7 +580,7 @@
     _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_common'), _editableHostAppDirectory);
     _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_editable'), _editableHostAppDirectory);
     _overwriteFromTemplate(fs.path.join('module', 'android', 'gradle'), _editableHostAppDirectory);
-    gradle.injectGradleWrapper(_editableHostAppDirectory);
+    gradle.injectGradleWrapperIfNeeded(_editableHostAppDirectory);
     gradle.writeLocalProperties(_editableHostAppDirectory.childFile('local.properties'));
     await injectPlugins(parent);
   }
@@ -593,7 +593,7 @@
     _deleteIfExistsSync(ephemeralDirectory);
     _overwriteFromTemplate(fs.path.join('module', 'android', 'library'), ephemeralDirectory);
     _overwriteFromTemplate(fs.path.join('module', 'android', 'gradle'), ephemeralDirectory);
-    gradle.injectGradleWrapper(ephemeralDirectory);
+    gradle.injectGradleWrapperIfNeeded(ephemeralDirectory);
   }
 
   void _overwriteFromTemplate(String path, Directory target) {
diff --git a/packages/flutter_tools/test/general.shard/android/gradle_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_test.dart
index b33bbc0..313055f 100644
--- a/packages/flutter_tools/test/general.shard/android/gradle_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/gradle_test.dart
@@ -13,6 +13,7 @@
 import 'package:flutter_tools/src/artifacts.dart';
 import 'package:flutter_tools/src/base/common.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
+import 'package:flutter_tools/src/base/os.dart';
 import 'package:flutter_tools/src/build_info.dart';
 import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
@@ -848,6 +849,125 @@
     });
   });
 
+  group('injectGradleWrapperIfNeeded', () {
+    MemoryFileSystem memoryFileSystem;
+    Directory tempDir;
+    Directory gradleWrapperDirectory;
+
+    setUp(() {
+      memoryFileSystem = MemoryFileSystem();
+      tempDir = memoryFileSystem.systemTempDirectory.createTempSync('artifacts_test.');
+      gradleWrapperDirectory = memoryFileSystem.directory(
+          memoryFileSystem.path.join(tempDir.path, 'bin', 'cache', 'artifacts', 'gradle_wrapper'));
+      gradleWrapperDirectory.createSync(recursive: true);
+      gradleWrapperDirectory
+        .childFile('gradlew')
+        .writeAsStringSync('irrelevant');
+      gradleWrapperDirectory
+        .childDirectory('gradle')
+        .childDirectory('wrapper')
+        .createSync(recursive: true);
+      gradleWrapperDirectory
+        .childDirectory('gradle')
+        .childDirectory('wrapper')
+        .childFile('gradle-wrapper.jar')
+        .writeAsStringSync('irrelevant');
+    });
+
+    testUsingContext('Inject the wrapper when all files are missing', () {
+      final Directory sampleAppAndroid = fs.directory('/sample-app/android');
+      sampleAppAndroid.createSync(recursive: true);
+
+      injectGradleWrapperIfNeeded(sampleAppAndroid);
+
+      expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue);
+
+      expect(sampleAppAndroid
+        .childDirectory('gradle')
+        .childDirectory('wrapper')
+        .childFile('gradle-wrapper.jar')
+        .existsSync(), isTrue);
+
+      expect(sampleAppAndroid
+        .childDirectory('gradle')
+        .childDirectory('wrapper')
+        .childFile('gradle-wrapper.properties')
+        .existsSync(), isTrue);
+
+      expect(sampleAppAndroid
+        .childDirectory('gradle')
+        .childDirectory('wrapper')
+        .childFile('gradle-wrapper.properties')
+        .readAsStringSync(),
+            'distributionBase=GRADLE_USER_HOME\n'
+            'distributionPath=wrapper/dists\n'
+            'zipStoreBase=GRADLE_USER_HOME\n'
+            'zipStorePath=wrapper/dists\n'
+            'distributionUrl=https\\://services.gradle.org/distributions/gradle-4.10.2-all.zip\n');
+    }, overrides: <Type, Generator>{
+      Cache: () => Cache(rootOverride: tempDir),
+      FileSystem: () => memoryFileSystem,
+    });
+
+    testUsingContext('Inject the wrapper when some files are missing', () {
+      final Directory sampleAppAndroid = fs.directory('/sample-app/android');
+      sampleAppAndroid.createSync(recursive: true);
+
+      // There's an existing gradlew
+      sampleAppAndroid.childFile('gradlew').writeAsStringSync('existing gradlew');
+
+      injectGradleWrapperIfNeeded(sampleAppAndroid);
+
+      expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue);
+      expect(sampleAppAndroid.childFile('gradlew').readAsStringSync(),
+          equals('existing gradlew'));
+
+      expect(sampleAppAndroid
+        .childDirectory('gradle')
+        .childDirectory('wrapper')
+        .childFile('gradle-wrapper.jar')
+        .existsSync(), isTrue);
+
+      expect(sampleAppAndroid
+        .childDirectory('gradle')
+        .childDirectory('wrapper')
+        .childFile('gradle-wrapper.properties')
+        .existsSync(), isTrue);
+
+      expect(sampleAppAndroid
+        .childDirectory('gradle')
+        .childDirectory('wrapper')
+        .childFile('gradle-wrapper.properties')
+        .readAsStringSync(),
+            'distributionBase=GRADLE_USER_HOME\n'
+            'distributionPath=wrapper/dists\n'
+            'zipStoreBase=GRADLE_USER_HOME\n'
+            'zipStorePath=wrapper/dists\n'
+            'distributionUrl=https\\://services.gradle.org/distributions/gradle-4.10.2-all.zip\n');
+    }, overrides: <Type, Generator>{
+      Cache: () => Cache(rootOverride: tempDir),
+      FileSystem: () => memoryFileSystem,
+    });
+
+    testUsingContext('Gives executable permission to gradle', () {
+      final Directory sampleAppAndroid = fs.directory('/sample-app/android');
+      sampleAppAndroid.createSync(recursive: true);
+
+      // Make gradlew in the wrapper executable.
+      os.makeExecutable(gradleWrapperDirectory.childFile('gradlew'));
+
+      injectGradleWrapperIfNeeded(sampleAppAndroid);
+
+      final File gradlew = sampleAppAndroid.childFile('gradlew');
+      expect(gradlew.existsSync(), isTrue);
+      expect(gradlew.statSync().modeString().contains('x'), isTrue);
+    }, overrides: <Type, Generator>{
+      Cache: () => Cache(rootOverride: tempDir),
+      FileSystem: () => memoryFileSystem,
+      OperatingSystemUtils: () => OperatingSystemUtils(),
+    });
+  });
+
   group('gradle build', () {
     MockAndroidSdk mockAndroidSdk;
     MockAndroidStudio mockAndroidStudio;
@@ -855,6 +975,7 @@
     MockProcessManager mockProcessManager;
     FakePlatform android;
     FileSystem fs;
+    Cache cache;
 
     setUp(() {
       fs = MemoryFileSystem();
@@ -863,6 +984,28 @@
       mockArtifacts = MockLocalEngineArtifacts();
       mockProcessManager = MockProcessManager();
       android = fakePlatform('android');
+
+      final Directory tempDir = fs.systemTempDirectory.createTempSync('artifacts_test.');
+      cache = Cache(rootOverride: tempDir);
+
+      final Directory gradleWrapperDirectory = tempDir
+          .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('build aar uses selected local engine', () async {
@@ -928,6 +1071,7 @@
         AndroidSdk: () => mockAndroidSdk,
         AndroidStudio: () => mockAndroidStudio,
         Artifacts: () => mockArtifacts,
+        Cache: () => cache,
         ProcessManager: () => mockProcessManager,
         Platform: () => android,
         FileSystem: () => fs,
diff --git a/packages/flutter_tools/test/general.shard/application_package_test.dart b/packages/flutter_tools/test/general.shard/application_package_test.dart
index 31ce93f..c2fbedb 100644
--- a/packages/flutter_tools/test/general.shard/application_package_test.dart
+++ b/packages/flutter_tools/test/general.shard/application_package_test.dart
@@ -39,17 +39,20 @@
     AndroidSdk sdk;
     ProcessManager mockProcessManager;
     MemoryFileSystem fs;
+    Cache mockCache;
     File gradle;
     final Map<Type, Generator> overrides = <Type, Generator>{
       AndroidSdk: () => sdk,
       ProcessManager: () => mockProcessManager,
       FileSystem: () => fs,
+      Cache: () => mockCache,
     };
 
     setUp(() async {
       sdk = MockitoAndroidSdk();
       mockProcessManager = MockitoProcessManager();
       fs = MemoryFileSystem();
+      mockCache = MockCache();
       Cache.flutterRoot = '../..';
       when(sdk.licensesAvailable).thenReturn(true);
       when(mockProcessManager.canRun(any)).thenReturn(true);
@@ -100,6 +103,14 @@
         platform.isWindows ? 'gradlew.bat' : 'gradlew',
       )..createSync(recursive: true);
 
+      final Directory gradleWrapperDir = fs.systemTempDirectory.createTempSync('gradle_wrapper.');
+      when(mockCache.getArtifactDirectory('gradle_wrapper')).thenReturn(gradleWrapperDir);
+
+      fs.directory(gradleWrapperDir.childDirectory('gradle').childDirectory('wrapper'))
+          .createSync(recursive: true);
+      fs.file(fs.path.join(gradleWrapperDir.path, 'gradlew')).writeAsStringSync('irrelevant');
+      fs.file(fs.path.join(gradleWrapperDir.path, 'gradlew.bat')).writeAsStringSync('irrelevant');
+
       await ApplicationPackageFactory.instance.getPackageForPlatform(
         TargetPlatform.android_arm,
         applicationBinary: fs.file('app.apk'),
@@ -606,4 +617,5 @@
 {"CFBundleIdentifier": "fooBundleId"}
 ''';
 
+class MockCache extends Mock implements Cache {}
 class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils { }
diff --git a/packages/flutter_tools/test/general.shard/base/file_system_test.dart b/packages/flutter_tools/test/general.shard/base/file_system_test.dart
index 09126ac..e0a4155 100644
--- a/packages/flutter_tools/test/general.shard/base/file_system_test.dart
+++ b/packages/flutter_tools/test/general.shard/base/file_system_test.dart
@@ -58,6 +58,29 @@
       // There's still 3 things in the original directory as there were initially.
       expect(sourceMemoryFs.directory(sourcePath).listSync().length, 3);
     });
+
+    testUsingContext('Skip files if shouldCopyFile returns false', () {
+      final Directory origin = fs.directory('/origin');
+      origin.createSync();
+      fs.file(fs.path.join('origin', 'a.txt')).writeAsStringSync('irrelevant');
+      fs.directory('/origin/nested').createSync();
+      fs.file(fs.path.join('origin', 'nested', 'a.txt')).writeAsStringSync('irrelevant');
+      fs.file(fs.path.join('origin', 'nested', 'b.txt')).writeAsStringSync('irrelevant');
+
+      final Directory destination = fs.directory('/destination');
+      copyDirectorySync(origin, destination, shouldCopyFile: (File origin, File dest) {
+        return origin.basename == 'b.txt';
+      });
+
+      expect(destination.existsSync(), isTrue);
+      expect(destination.childDirectory('nested').existsSync(), isTrue);
+      expect(destination.childDirectory('nested').childFile('b.txt').existsSync(), isTrue);
+
+      expect(destination.childFile('a.txt').existsSync(), isFalse);
+      expect(destination.childDirectory('nested').childFile('a.txt').existsSync(), isFalse);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+    });
   });
 
   group('canonicalizePath', () {