Log additional Android build failures (#43941)

diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index dff67bd..ed11fff 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -156,6 +156,7 @@
 /// Tries to create `settings_aar.gradle` in an app project by removing the subprojects
 /// from the existing `settings.gradle` file. This operation will fail if the existing
 /// `settings.gradle` file has local edits.
+@visibleForTesting
 void createSettingsAarGradle(Directory androidDirectory) {
   final File newSettingsFile = androidDirectory.childFile('settings_aar.gradle');
   if (newSettingsFile.existsSync()) {
@@ -232,6 +233,7 @@
   if (!_isSupportedVersion(project.android)) {
     _exitWithUnsupportedProjectMessage();
   }
+  final Directory buildDirectory = project.android.buildDirectory;
 
   final bool usesAndroidX = isAppUsingAndroidX(project.android.hostAppGradleRoot);
   if (usesAndroidX) {
@@ -255,7 +257,7 @@
     await buildPluginsAsAar(
       project,
       androidBuildInfo,
-      buildDirectory: project.android.buildDirectory.childDirectory('app'),
+      buildDirectory: buildDirectory.childDirectory('app'),
     );
   }
 
@@ -340,7 +342,7 @@
           return null;
         }
         if (detectedGradleError != null) {
-          // Pipe stdout/sterr from Gradle.
+          // Pipe stdout/stderr from Gradle.
           return line;
         }
         for (final GradleHandledError gradleError in localGradleErrors) {
@@ -351,7 +353,7 @@
             break;
           }
         }
-        // Pipe stdout/sterr from Gradle.
+        // Pipe stdout/stderr from Gradle.
         return line;
       },
     );
@@ -363,7 +365,7 @@
 
   if (exitCode != 0) {
     if (detectedGradleError == null) {
-      BuildEvent('gradle--unkown-failure').send();
+      BuildEvent('gradle-unkown-failure').send();
       throwToolExit(
         'Gradle task $assembleTask failed with exit code $exitCode',
         exitCode: exitCode,
@@ -377,7 +379,7 @@
       );
 
       if (retries >= 1) {
-        final String successEventLabel = 'gradle--${detectedGradleError.eventLabel}-success';
+        final String successEventLabel = 'gradle-${detectedGradleError.eventLabel}-success';
         switch (status) {
           case GradleBuildStatus.retry:
             await buildGradleApp(
@@ -407,7 +409,7 @@
             // noop.
         }
       }
-      BuildEvent('gradle--${detectedGradleError.eventLabel}-failure').send();
+      BuildEvent('gradle-${detectedGradleError.eventLabel}-failure').send();
       throwToolExit(
         'Gradle task $assembleTask failed with exit code $exitCode',
         exitCode: exitCode,
@@ -417,10 +419,6 @@
 
   if (isBuildingBundle) {
     final File bundleFile = findBundleFile(project, buildInfo);
-    if (bundleFile == null) {
-      throwToolExit('Gradle build failed to produce an Android bundle package.');
-    }
-
     final String appSize = (buildInfo.mode == BuildMode.debug)
       ? '' // Don't display the size when building a debug variant.
       : ' (${getSizeAsMB(bundleFile.lengthSync())})';
@@ -433,10 +431,6 @@
   }
   // Gradle produced an APK.
   final Iterable<File> apkFiles = findApkFiles(project, androidBuildInfo);
-  if (apkFiles.isEmpty) {
-    throwToolExit('Gradle build failed to produce an Android package.');
-  }
-
   final Directory apkDirectory = getApkDirectory(project);
   // Copy the first APK to app.apk, so `flutter run` can find it.
   // TODO(egarciad): Handle multiple APKs.
@@ -640,7 +634,7 @@
     } on ToolExit {
       // Log the entire plugin entry in `.flutter-plugins` since it
       // includes the plugin name and the version.
-      BuildEvent('plugin-aar-failure', eventError: plugin).send();
+      BuildEvent('gradle-plugin-aar-failure', eventError: plugin).send();
       throwToolExit('The plugin $pluginName could not be built due to the issue above.');
     }
   }
@@ -650,14 +644,11 @@
 @visibleForTesting
 Iterable<File> findApkFiles(
   FlutterProject project,
-  AndroidBuildInfo androidBuildInfo)
-{
+  AndroidBuildInfo androidBuildInfo,
+) {
   final Iterable<String> apkFileNames = _apkFilesFor(androidBuildInfo);
-  if (apkFileNames.isEmpty) {
-    return const <File>[];
-  }
   final Directory apkDirectory = getApkDirectory(project);
-  return apkFileNames.expand<File>((String apkFileName) {
+  final Iterable<File> apks = apkFileNames.expand<File>((String apkFileName) {
     File apkFile = apkDirectory.childFile(apkFileName);
     if (apkFile.existsSync()) {
       return <File>[apkFile];
@@ -682,6 +673,13 @@
     }
     return const <File>[];
   });
+  if (apks.isEmpty) {
+    _exitWithExpectedFileNotFound(
+      project: project,
+      fileExtension: '.apk',
+    );
+  }
+  return apks;
 }
 
 @visibleForTesting
@@ -716,5 +714,31 @@
       return bundleFile;
     }
   }
+  _exitWithExpectedFileNotFound(
+    project: project,
+    fileExtension: '.aab',
+  );
   return null;
 }
+
+/// Throws a [ToolExit] exception and logs the event.
+void _exitWithExpectedFileNotFound({
+  @required FlutterProject project,
+  @required String fileExtension,
+}) {
+  assert(project != null);
+  assert(fileExtension != null);
+
+  final String androidGradlePluginVersion =
+      getGradleVersionForAndroidPlugin(project.android.hostAppGradleRoot);
+  BuildEvent('gradle-expected-file-not-found',
+    settings:
+      'androidGradlePluginVersion: $androidGradlePluginVersion, '
+      'fileExtension: $fileExtension'
+    ).send();
+  throwToolExit(
+    'Gradle build failed to produce an $fileExtension file. '
+    'It\'s likely that this file was generated under ${project.android.buildDirectory.path}, '
+    'but the tool couldn\'t find it.'
+  );
+}
diff --git a/packages/flutter_tools/lib/src/android/gradle_errors.dart b/packages/flutter_tools/lib/src/android/gradle_errors.dart
index bcec1a7..ac94bd1 100644
--- a/packages/flutter_tools/lib/src/android/gradle_errors.dart
+++ b/packages/flutter_tools/lib/src/android/gradle_errors.dart
@@ -14,7 +14,7 @@
 typedef GradleErrorTest = bool Function(String);
 
 /// A Gradle error handled by the tool.
-class GradleHandledError{
+class GradleHandledError {
   const GradleHandledError({
     this.test,
     this.handler,
@@ -33,14 +33,14 @@
     bool shouldBuildPluginAsAar,
   }) handler;
 
-  /// The [BuildEvent] label is named gradle--[eventLabel].
+  /// The [BuildEvent] label is named gradle-[eventLabel].
   /// If not empty, the build event is logged along with
   /// additional metadata such as the attempt number.
   final String eventLabel;
 }
 
 /// The status of the Gradle build.
-enum GradleBuildStatus{
+enum GradleBuildStatus {
   /// The tool cannot recover from the failure and should exit.
   exit,
   /// The tool can retry the exact same build.
@@ -180,7 +180,7 @@
       // If the app doesn't use any plugin, then it's unclear where
       // the incompatibility is coming from.
       BuildEvent(
-        'gradle--android-x-failure',
+        'gradle-android-x-failure',
         eventError: 'app-not-using-plugins',
       ).send();
     }
@@ -192,7 +192,7 @@
         'Please migrate your app to AndroidX. See https://goo.gl/CP92wY.'
       );
       BuildEvent(
-        'gradle--android-x-failure',
+        'gradle-android-x-failure',
         eventError: 'app-not-using-androidx',
       ).send();
     }
@@ -201,7 +201,7 @@
       // by this point the app is using AndroidX, the plugins are built as
       // AARs, Jetifier translated Support libraries for AndroidX equivalents.
       BuildEvent(
-        'gradle--android-x-failure',
+        'gradle-android-x-failure',
         eventError: 'using-jetifier',
       ).send();
     }
@@ -211,7 +211,7 @@
         'The tool is about to try using Jetfier to solve the incompatibility.'
       );
       BuildEvent(
-        'gradle--android-x-failure',
+        'gradle-android-x-failure',
         eventError: 'not-using-jetifier',
       ).send();
       return GradleBuildStatus.retryWithAarPlugins;
diff --git a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart
index aca7919..8e05d75 100644
--- a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart
@@ -279,7 +279,7 @@
       verify(mockUsage.sendEvent(
         any,
         any,
-        label: 'gradle--android-x-failure',
+        label: 'gradle-android-x-failure',
         parameters: <String, String>{
           'cd43': 'app-not-using-plugins',
         },
@@ -312,7 +312,7 @@
       verify(mockUsage.sendEvent(
         any,
         any,
-        label: 'gradle--android-x-failure',
+        label: 'gradle-android-x-failure',
         parameters: <String, String>{
           'cd43': 'app-not-using-androidx',
         },
@@ -338,7 +338,7 @@
       verify(mockUsage.sendEvent(
         any,
         any,
-        label: 'gradle--android-x-failure',
+        label: 'gradle-android-x-failure',
         parameters: <String, String>{
           'cd43': 'using-jetifier',
         },
@@ -371,7 +371,7 @@
       verify(mockUsage.sendEvent(
         any,
         any,
-        label: 'gradle--android-x-failure',
+        label: 'gradle-android-x-failure',
         parameters: <String, String>{
           'cd43': 'not-using-jetifier',
         },
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 0571175..ae81bdf 100644
--- a/packages/flutter_tools/test/general.shard/android/gradle_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/gradle_test.dart
@@ -132,6 +132,8 @@
   });
 
   group('findBundleFile', () {
+    final Usage mockUsage = MockUsage();
+
     testUsingContext('Finds app bundle when flavor contains underscores in release mode', () {
       final FlutterProject project = generateFakeAppBundle('foo_barRelease', 'app.aab');
       final File bundle = findBundleFile(project, const BuildInfo(BuildMode.release, 'foo_bar'));
@@ -281,9 +283,39 @@
       FileSystem: () => MemoryFileSystem(),
       ProcessManager: () => FakeProcessManager.any(),
     });
+
+    testUsingContext('aab not found', () {
+      final FlutterProject project = FlutterProject.current();
+      expect(
+        () {
+          findBundleFile(project, const BuildInfo(BuildMode.debug, 'foo_bar'));
+        },
+        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.'
+        )
+      );
+      verify(
+        mockUsage.sendEvent(
+          any,
+          any,
+          label: 'gradle-expected-file-not-found',
+          parameters: const <String, String> {
+            'cd37': 'androidGradlePluginVersion: 5.6.2, fileExtension: .aab',
+          },
+        ),
+      ).called(1);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+      Usage: () => mockUsage,
+    });
   });
 
   group('findApkFiles', () {
+    final Usage mockUsage = MockUsage();
+
     testUsingContext('Finds APK without flavor in release', () {
       final FlutterProject project = MockFlutterProject();
       final AndroidProject androidProject = MockAndroidProject();
@@ -352,6 +384,37 @@
       FileSystem: () => MemoryFileSystem(),
       ProcessManager: () => FakeProcessManager.any(),
     });
+
+    testUsingContext('apk not found', () {
+      final FlutterProject project = FlutterProject.current();
+      expect(
+        () {
+          findApkFiles(
+            project,
+            const AndroidBuildInfo(BuildInfo(BuildMode.debug, 'foo_bar')),
+          );
+        },
+        throwsToolExit(
+          message:
+            'Gradle build failed to produce an .apk file. It\'s likely that this file '
+            'was generated under ${project.android.buildDirectory.path}, but the tool couldn\'t find it.'
+        )
+      );
+      verify(
+        mockUsage.sendEvent(
+          any,
+          any,
+          label: 'gradle-expected-file-not-found',
+          parameters: const <String, String> {
+            'cd37': 'androidGradlePluginVersion: 5.6.2, fileExtension: .apk',
+          },
+        ),
+      ).called(1);
+    }, overrides: <Type, Generator>{
+      FileSystem: () => MemoryFileSystem(),
+      ProcessManager: () => FakeProcessManager.any(),
+      Usage: () => mockUsage,
+    });
   });
 
   group('gradle build', () {
@@ -365,15 +428,6 @@
       AndroidSdk: () => null,
     });
 
-    // Regression test for https://github.com/flutter/flutter/issues/34700
-    testUsingContext('Does not return nulls in apk list', () {
-      const AndroidBuildInfo buildInfo = AndroidBuildInfo(BuildInfo.debug);
-      expect(findApkFiles(FlutterProject.current(), buildInfo), <File>[]);
-    }, overrides: <Type, Generator>{
-      FileSystem: () => MemoryFileSystem(),
-      ProcessManager: () => FakeProcessManager.any(),
-    });
-
     test('androidXPluginWarningRegex should match lines with the AndroidX plugin warnings', () {
       final List<String> nonMatchingLines = <String>[
         ':app:preBuild UP-TO-DATE',
@@ -1118,7 +1172,7 @@
       verify(mockUsage.sendEvent(
         any,
         any,
-        label: 'gradle--random-event-label-failure',
+        label: 'gradle-random-event-label-failure',
         parameters: anyNamed('parameters'),
       )).called(1);
 
@@ -1199,7 +1253,7 @@
       verify(mockUsage.sendEvent(
         any,
         any,
-        label: 'gradle--random-event-label-failure',
+        label: 'gradle-random-event-label-failure',
         parameters: anyNamed('parameters'),
       )).called(1);
 
@@ -1287,7 +1341,7 @@
       verify(mockUsage.sendEvent(
         any,
         any,
-        label: 'gradle--random-event-label-success',
+        label: 'gradle-random-event-label-success',
         parameters: anyNamed('parameters'),
       )).called(1);
 
@@ -1373,7 +1427,7 @@
       verify(mockUsage.sendEvent(
         any,
         any,
-        label: 'gradle--random-event-label-failure',
+        label: 'gradle-random-event-label-failure',
         parameters: anyNamed('parameters'),
       )).called(1);
 
diff --git a/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart b/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart
index 4970b0b..ca40523 100644
--- a/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/build_apk_test.dart
@@ -320,7 +320,7 @@
       verify(mockUsage.sendEvent(
         'build',
         'apk',
-        label: 'gradle--r8-failure',
+        label: 'gradle-r8-failure',
         parameters: anyNamed('parameters'),
       )).called(1);
     },
diff --git a/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart b/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart
index 8f79b4a..6b2b3e0 100644
--- a/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart
+++ b/packages/flutter_tools/test/general.shard/commands/build_appbundle_test.dart
@@ -306,7 +306,7 @@
       verify(mockUsage.sendEvent(
         'build',
         'appbundle',
-        label: 'gradle--r8-failure',
+        label: 'gradle-r8-failure',
         parameters: anyNamed('parameters'),
       )).called(1);
     },