Print message when HttpException is thrown after running `flutter run` (#37440)

diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 88ab5dc..cd618a3 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -512,6 +512,23 @@
   }
 }
 
+// Issue: https://github.com/flutter/flutter/issues/33050
+// Matches the following patterns:
+//    HttpException: Connection closed before full header was received, uri = *
+//    HttpException: , uri = *
+final RegExp kAndroidQHttpConnectionClosedExp = RegExp(r'^HttpException\:.+\, uri \=.+$');
+
+/// Returns `true` if any of the devices is running Android Q.
+Future<bool> hasDeviceRunningAndroidQ(List<FlutterDevice> flutterDevices) async {
+  for (FlutterDevice flutterDevice in flutterDevices) {
+    final String sdkNameAndVersion = await flutterDevice.device.sdkNameAndVersion;
+    if (sdkNameAndVersion != null && sdkNameAndVersion.startsWith('Android 10')) {
+      return true;
+    }
+  }
+  return false;
+}
+
 // Shared code between different resident application runners.
 abstract class ResidentRunner {
   ResidentRunner(
diff --git a/packages/flutter_tools/lib/src/run_cold.dart b/packages/flutter_tools/lib/src/run_cold.dart
index 6a79afc..12b494c 100644
--- a/packages/flutter_tools/lib/src/run_cold.dart
+++ b/packages/flutter_tools/lib/src/run_cold.dart
@@ -131,6 +131,13 @@
       await connectToServiceProtocol();
     } catch (error) {
       printError('Error connecting to the service protocol: $error');
+      // https://github.com/flutter/flutter/issues/33050
+      // TODO(blasten): Remove this check once https://issuetracker.google.com/issues/132325318 has been fixed.
+      if (await hasDeviceRunningAndroidQ(flutterDevices) &&
+          error.toString().contains(kAndroidQHttpConnectionClosedExp)) {
+        printStatus('🔨 If you are using an emulator running Android Q Beta, consider using an emulator running API level 29 or lower.');
+        printStatus('Learn more about the status of this issue on https://issuetracker.google.com/issues/132325318');
+      }
       return 2;
     }
     for (FlutterDevice device in flutterDevices) {
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index 7a8105d..4b09b2b 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -155,6 +155,13 @@
       );
     } catch (error) {
       printError('Error connecting to the service protocol: $error');
+      // https://github.com/flutter/flutter/issues/33050
+      // TODO(blasten): Remove this check once https://issuetracker.google.com/issues/132325318 has been fixed.
+      if (await hasDeviceRunningAndroidQ(flutterDevices) &&
+          error.toString().contains(kAndroidQHttpConnectionClosedExp)) {
+        printStatus('🔨 If you are using an emulator running Android Q Beta, consider using an emulator running API level 29 or lower.');
+        printStatus('Learn more about the status of this issue on https://issuetracker.google.com/issues/132325318.');
+      }
       return 2;
     }
 
diff --git a/packages/flutter_tools/test/general.shard/cold_test.dart b/packages/flutter_tools/test/general.shard/cold_test.dart
new file mode 100644
index 0000000..93f8b2f
--- /dev/null
+++ b/packages/flutter_tools/test/general.shard/cold_test.dart
@@ -0,0 +1,114 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/logger.dart';
+import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/compile.dart';
+import 'package:flutter_tools/src/device.dart';
+import 'package:flutter_tools/src/resident_runner.dart';
+import 'package:flutter_tools/src/run_cold.dart';
+import 'package:flutter_tools/src/vmservice.dart';
+import 'package:meta/meta.dart';
+import 'package:mockito/mockito.dart';
+
+import '../src/common.dart';
+import '../src/context.dart';
+import '../src/mocks.dart';
+
+void main() {
+  group('cold attach', () {
+    MockResidentCompiler residentCompiler;
+    BufferLogger mockLogger;
+
+    setUp(() {
+      mockLogger = BufferLogger();
+      residentCompiler = MockResidentCompiler();
+    });
+
+    testUsingContext('Prints message when HttpException is thrown - 1', () async {
+      final MockDevice mockDevice = MockDevice();
+      when(mockDevice.supportsHotReload).thenReturn(true);
+      when(mockDevice.supportsHotRestart).thenReturn(false);
+      when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester);
+      when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation _) async => 'Android 10');
+
+      final List<FlutterDevice> devices = <FlutterDevice>[
+        TestFlutterDevice(
+          device: mockDevice,
+          generator: residentCompiler,
+          exception: const HttpException('Connection closed before full header was received, '
+              'uri = http://127.0.0.1:63394/5ZmLv8A59xY=/ws')
+        ),
+      ];
+
+      final int exitCode = await ColdRunner(devices,
+        debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+      ).attach();
+      expect(exitCode, 2);
+      expect(mockLogger.statusText, contains('If you are using an emulator running Android Q Beta, '
+          'consider using an emulator running API level 29 or lower.'));
+      expect(mockLogger.statusText, contains('Learn more about the status of this issue on '
+          'https://issuetracker.google.com/issues/132325318'));
+    }, overrides: <Type, Generator>{
+      Logger: () => mockLogger,
+    });
+
+    testUsingContext('Prints message when HttpException is thrown - 2', () async {
+      final MockDevice mockDevice = MockDevice();
+      when(mockDevice.supportsHotReload).thenReturn(true);
+      when(mockDevice.supportsHotRestart).thenReturn(false);
+      when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester);
+      when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation _) async => 'Android 10');
+
+      final List<FlutterDevice> devices = <FlutterDevice>[
+        TestFlutterDevice(
+          device: mockDevice,
+          generator: residentCompiler,
+          exception: const HttpException(', uri = http://127.0.0.1:63394/5ZmLv8A59xY=/ws')
+        ),
+      ];
+
+      final int exitCode = await ColdRunner(devices,
+        debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+      ).attach();
+      expect(exitCode, 2);
+      expect(mockLogger.statusText, contains('If you are using an emulator running Android Q Beta, '
+          'consider using an emulator running API level 29 or lower.'));
+      expect(mockLogger.statusText, contains('Learn more about the status of this issue on '
+          'https://issuetracker.google.com/issues/132325318'));
+    }, overrides: <Type, Generator>{
+      Logger: () => mockLogger,
+    });
+  });
+}
+
+class MockDevice extends Mock implements Device {
+  MockDevice() {
+    when(isSupported()).thenReturn(true);
+  }
+}
+
+class TestFlutterDevice extends FlutterDevice {
+  TestFlutterDevice({
+    @required Device device,
+    @required this.exception,
+    @required ResidentCompiler generator
+  })  : assert(exception != null),
+        super(device, buildMode: BuildMode.debug, generator: generator, trackWidgetCreation: false);
+
+  /// The exception to throw when the connect method is called.
+  final Exception exception;
+
+  @override
+  Future<void> connect({
+    ReloadSources reloadSources,
+    Restart restart,
+    CompileExpression compileExpression,
+  }) async {
+    throw exception;
+  }
+}
diff --git a/packages/flutter_tools/test/general.shard/hot_test.dart b/packages/flutter_tools/test/general.shard/hot_test.dart
index 304fcf7..2853c19 100644
--- a/packages/flutter_tools/test/general.shard/hot_test.dart
+++ b/packages/flutter_tools/test/general.shard/hot_test.dart
@@ -5,11 +5,15 @@
 import 'dart:async';
 
 import 'package:flutter_tools/src/artifacts.dart';
+import 'package:flutter_tools/src/base/io.dart';
+import 'package:flutter_tools/src/base/logger.dart';
 import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/compile.dart';
 import 'package:flutter_tools/src/devfs.dart';
 import 'package:flutter_tools/src/device.dart';
 import 'package:flutter_tools/src/resident_runner.dart';
 import 'package:flutter_tools/src/run_hot.dart';
+import 'package:flutter_tools/src/vmservice.dart';
 import 'package:meta/meta.dart';
 import 'package:mockito/mockito.dart';
 
@@ -262,6 +266,77 @@
       });
     });
   });
+
+  group('hot attach', () {
+    MockResidentCompiler residentCompiler = MockResidentCompiler();
+    BufferLogger mockLogger;
+    MockLocalEngineArtifacts mockArtifacts;
+
+    setUp(() {
+      residentCompiler = MockResidentCompiler();
+      mockLogger = BufferLogger();
+      mockArtifacts = MockLocalEngineArtifacts();
+    });
+
+    testUsingContext('Prints message when HttpException is thrown - 1', () async {
+      final MockDevice mockDevice = MockDevice();
+      when(mockDevice.supportsHotReload).thenReturn(true);
+      when(mockDevice.supportsHotRestart).thenReturn(false);
+      when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester);
+      when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation _) async => 'Android 10');
+
+      final List<FlutterDevice> devices = <FlutterDevice>[
+        TestFlutterDevice(
+          device: mockDevice,
+          generator: residentCompiler,
+          exception: const HttpException('Connection closed before full header was received, '
+              'uri = http://127.0.0.1:63394/5ZmLv8A59xY=/ws')
+        ),
+      ];
+
+      final int exitCode = await HotRunner(devices,
+        debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+      ).attach();
+      expect(exitCode, 2);
+      expect(mockLogger.statusText, contains('If you are using an emulator running Android Q Beta, '
+          'consider using an emulator running API level 29 or lower.'));
+      expect(mockLogger.statusText, contains('Learn more about the status of this issue on '
+          'https://issuetracker.google.com/issues/132325318'));
+    }, overrides: <Type, Generator>{
+      Artifacts: () => mockArtifacts,
+      Logger: () => mockLogger,
+      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
+    });
+
+    testUsingContext('Prints message when HttpException is thrown - 2', () async {
+      final MockDevice mockDevice = MockDevice();
+      when(mockDevice.supportsHotReload).thenReturn(true);
+      when(mockDevice.supportsHotRestart).thenReturn(false);
+      when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester);
+      when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation _) async => 'Android 10');
+
+      final List<FlutterDevice> devices = <FlutterDevice>[
+        TestFlutterDevice(
+          device: mockDevice,
+          generator: residentCompiler,
+          exception: const HttpException(', uri = http://127.0.0.1:63394/5ZmLv8A59xY=/ws')
+        ),
+      ];
+
+      final int exitCode = await HotRunner(devices,
+        debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
+      ).attach();
+      expect(exitCode, 2);
+      expect(mockLogger.statusText, contains('If you are using an emulator running Android Q Beta, '
+          'consider using an emulator running API level 29 or lower.'));
+      expect(mockLogger.statusText, contains('Learn more about the status of this issue on '
+          'https://issuetracker.google.com/issues/132325318'));
+    }, overrides: <Type, Generator>{
+      Artifacts: () => mockArtifacts,
+      Logger: () => mockLogger,
+      HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true),
+    });
+  });
 }
 
 class MockDevFs extends Mock implements DevFS {}
@@ -274,6 +349,27 @@
   }
 }
 
+class TestFlutterDevice extends FlutterDevice {
+  TestFlutterDevice({
+    @required Device device,
+    @required this.exception,
+    @required ResidentCompiler generator
+  })  : assert(exception != null),
+        super(device, buildMode: BuildMode.debug, generator: generator, trackWidgetCreation: false);
+
+  /// The exception to throw when the connect method is called.
+  final Exception exception;
+
+  @override
+  Future<void> connect({
+    ReloadSources reloadSources,
+    Restart restart,
+    CompileExpression compileExpression,
+  }) async {
+    throw exception;
+  }
+}
+
 class TestHotRunnerConfig extends HotRunnerConfig {
   TestHotRunnerConfig({@required this.successfulSetup});
   bool successfulSetup;