Send progress notifications to clients during hot reload / hot restart (#112455)

diff --git a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart
index 1f1f79a..3d05858 100644
--- a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart
+++ b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter.dart
@@ -140,6 +140,13 @@
   Future<void> attachImpl() async {
     final FlutterAttachRequestArguments args = this.args as FlutterAttachRequestArguments;
 
+    final DapProgressReporter progress = startProgressNotification(
+      'launch',
+      'Flutter',
+      message: 'Attaching…',
+    );
+    unawaited(appStartedCompleter.future.then((_) => progress.end()));
+
     final String? vmServiceUri = args.vmServiceUri;
     final List<String> toolArgs = <String>[
       'attach',
@@ -255,6 +262,13 @@
   Future<void> launchImpl() async {
     final FlutterLaunchRequestArguments args = this.args as FlutterLaunchRequestArguments;
 
+    final DapProgressReporter progress = startProgressNotification(
+      'launch',
+      'Flutter',
+      message: 'Launching…',
+    );
+    unawaited(appStartedCompleter.future.then((_) => progress.end()));
+
     final List<String> toolArgs = <String>[
       'run',
       '--machine',
@@ -593,6 +607,14 @@
     bool fullRestart, [
     String? reason,
   ]) async {
+    final String progressId = fullRestart ? 'hotRestart' : 'hotReload';
+    final String progressMessage = fullRestart ? 'Hot restarting…' : 'Hot reloading…';
+    final DapProgressReporter progress = startProgressNotification(
+      progressId,
+      'Flutter',
+      message: progressMessage,
+    );
+
     try {
       await sendFlutterRequest('app.restart', <String, Object?>{
         'appId': appId,
@@ -605,6 +627,9 @@
       final String action = fullRestart ? 'Hot Restart' : 'Hot Reload';
       sendOutput('console', 'Failed to $action: $error');
     }
+    finally {
+      progress.end();
+    }
   }
 
   void _sendServiceExtensionStateChanged(vm.ExtensionData? extensionData) {
diff --git a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter_args.dart b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter_args.dart
index 85947f7..cc58f38 100644
--- a/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter_args.dart
+++ b/packages/flutter_tools/lib/src/debug_adapters/flutter_adapter_args.dart
@@ -27,6 +27,7 @@
     super.evaluateGettersInDebugViews,
     super.evaluateToStringInDebugViews,
     super.sendLogsToClient,
+    super.sendCustomProgressEvents,
   });
 
   FlutterAttachRequestArguments.fromMap(super.obj)
@@ -99,6 +100,7 @@
     super.evaluateGettersInDebugViews,
     super.evaluateToStringInDebugViews,
     super.sendLogsToClient,
+    super.sendCustomProgressEvents,
   });
 
   FlutterLaunchRequestArguments.fromMap(super.obj)
diff --git a/packages/flutter_tools/test/integration.shard/debug_adapter/flutter_adapter_test.dart b/packages/flutter_tools/test/integration.shard/debug_adapter/flutter_adapter_test.dart
index 7074760..f92ee3a 100644
--- a/packages/flutter_tools/test/integration.shard/debug_adapter/flutter_adapter_test.dart
+++ b/packages/flutter_tools/test/integration.shard/debug_adapter/flutter_adapter_test.dart
@@ -218,6 +218,43 @@
       await dap.client.terminate();
     });
 
+    testWithoutContext('sends progress notifications during hot reload', () async {
+      final BasicProject project = BasicProject();
+      await project.setUpIn(tempDir);
+
+      // Launch the app and wait for it to print "topLevelFunction".
+      await Future.wait(<Future<void>>[
+        dap.client.stdoutOutput.firstWhere((String output) => output.startsWith('topLevelFunction')),
+        dap.client.initialize(supportsProgressReporting: true),
+        dap.client.launch(
+              cwd: project.dir.path,
+              noDebug: true,
+              toolArgs: <String>['-d', 'flutter-tester'],
+            ),
+      ], eagerError: true);
+
+      // Capture progress events during a reload.
+      final Future<List<Event>> progressEventsFuture = dap.client.progressEvents().toList();
+      await dap.client.hotReload();
+      await dap.client.terminate();
+
+      // Verify the progress events.
+      final List<Event> progressEvents = await progressEventsFuture;
+      expect(progressEvents, hasLength(2));
+
+      final List<String> eventKinds = progressEvents.map((Event event) => event.event).toList();
+      expect(eventKinds, <String>['progressStart', 'progressEnd']);
+
+      final List<Map<String, Object?>> eventBodies = progressEvents.map((Event event) => event.body).cast<Map<String, Object?>>().toList();
+      final ProgressStartEventBody start = ProgressStartEventBody.fromMap(eventBodies[0]);
+      final ProgressEndEventBody end = ProgressEndEventBody.fromMap(eventBodies[1]);
+      expect(start.progressId, isNotNull);
+      expect(start.title, 'Flutter');
+      expect(start.message, 'Hot reloading…');
+      expect(end.progressId, start.progressId);
+      expect(end.message, isNull);
+    });
+
     testWithoutContext('can hot restart', () async {
       final BasicProject project = BasicProject();
       await project.setUpIn(tempDir);
@@ -255,6 +292,43 @@
       await dap.client.terminate();
     });
 
+    testWithoutContext('sends progress notifications during hot restart', () async {
+      final BasicProject project = BasicProject();
+      await project.setUpIn(tempDir);
+
+      // Launch the app and wait for it to print "topLevelFunction".
+      await Future.wait(<Future<void>>[
+        dap.client.stdoutOutput.firstWhere((String output) => output.startsWith('topLevelFunction')),
+        dap.client.initialize(supportsProgressReporting: true),
+        dap.client.launch(
+              cwd: project.dir.path,
+              noDebug: true,
+              toolArgs: <String>['-d', 'flutter-tester'],
+            ),
+      ], eagerError: true);
+
+      // Capture progress events during a restart.
+      final Future<List<Event>> progressEventsFuture = dap.client.progressEvents().toList();
+      await dap.client.hotRestart();
+      await dap.client.terminate();
+
+      // Verify the progress events.
+      final List<Event> progressEvents = await progressEventsFuture;
+      expect(progressEvents, hasLength(2));
+
+      final List<String> eventKinds = progressEvents.map((Event event) => event.event).toList();
+      expect(eventKinds, <String>['progressStart', 'progressEnd']);
+
+      final List<Map<String, Object?>> eventBodies = progressEvents.map((Event event) => event.body).cast<Map<String, Object?>>().toList();
+      final ProgressStartEventBody start = ProgressStartEventBody.fromMap(eventBodies[0]);
+      final ProgressEndEventBody end = ProgressEndEventBody.fromMap(eventBodies[1]);
+      expect(start.progressId, isNotNull);
+      expect(start.title, 'Flutter');
+      expect(start.message, 'Hot restarting…');
+      expect(end.progressId, start.progressId);
+      expect(end.message, isNull);
+    });
+
     testWithoutContext('can hot restart when exceptions occur on outgoing isolates', () async {
       final BasicProjectThatThrows project = BasicProjectThatThrows();
       await project.setUpIn(tempDir);
diff --git a/packages/flutter_tools/test/integration.shard/debug_adapter/test_client.dart b/packages/flutter_tools/test/integration.shard/debug_adapter/test_client.dart
index e1f823a..1bb15b3 100644
--- a/packages/flutter_tools/test/integration.shard/debug_adapter/test_client.dart
+++ b/packages/flutter_tools/test/integration.shard/debug_adapter/test_client.dart
@@ -83,6 +83,12 @@
     return _eventController.stream.where((Event e) => e.event == event);
   }
 
+  /// Returns a stream of progress events.
+  Stream<Event> progressEvents() {
+    const Set<String> progressEvents = <String>{'progressStart', 'progressUpdate', 'progressEnd'};
+    return _eventController.stream.where((Event e) => progressEvents.contains(e.event));
+  }
+
   /// Returns a stream of custom 'dart.serviceExtensionAdded' events.
   Stream<Map<String, Object?>> get serviceExtensionAddedEvents =>
       events('dart.serviceExtensionAdded')
@@ -116,12 +122,14 @@
   Future<Response> initialize({
     String exceptionPauseMode = 'None',
     bool? supportsRunInTerminalRequest,
+    bool? supportsProgressReporting,
   }) async {
     final List<ProtocolMessage> responses = await Future.wait(<Future<ProtocolMessage>>[
       event('initialized'),
       sendRequest(InitializeRequestArguments(
         adapterID: 'test',
         supportsRunInTerminalRequest: supportsRunInTerminalRequest,
+        supportsProgressReporting: supportsProgressReporting,
       )),
       sendRequest(
         SetExceptionBreakpointsArguments(