Add haptic notifications support. (#177721)

Closes https://github.com/flutter/flutter/issues/150029

### Description
- Adds `successNotification`, `warningNotification` and
`errorNotification` haptics to the framework
- Adds `UINotificationFeedbackTypeSuccess`,
`UINotificationFeedbackTypeWarning` and
`UINotificationFeedbackTypeError` haptics support on iOS
- Adds `HapticFeedbackConstants.CONFIRM` and
`HapticFeedbackConstants.REJECT` haptics support on Android
- Adds tests


| iOS | Android | Web |
|:-:|:-:|:-:|
| UINotificationFeedbackTypeSuccess | HapticFeedbackConstants.CONFIRM |
20ms vibration |
| UINotificationFeedbackTypeWarning |
HapticFeedbackConstants.KEYBOARD_TAP | 20ms vibration |
| UINotificationFeedbackTypeError | HapticFeedbackConstants.REJECT |
30ms vibration |

## Pre-launch Checklist

- [X] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [X] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [X] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [X] I signed the [CLA].
- [X] I listed at least one issue that this PR fixes in the description
above.
- [X] I updated/added relevant documentation (doc comments with `///`).
- [X] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [X] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [X] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart
index e9f54aa..c868219 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart
@@ -681,6 +681,9 @@
       'HapticFeedbackType.mediumImpact' => vibrateMediumImpact,
       'HapticFeedbackType.heavyImpact' => vibrateHeavyImpact,
       'HapticFeedbackType.selectionClick' => vibrateSelectionClick,
+      'HapticFeedbackType.successNotification' => vibrateMediumImpact,
+      'HapticFeedbackType.warningNotification' => vibrateMediumImpact,
+      'HapticFeedbackType.errorNotification' => vibrateHeavyImpact,
       _ => vibrateLongPress,
     };
   }
diff --git a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java
index 1f9c105..d25068e 100644
--- a/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java
+++ b/engine/src/flutter/shell/platform/android/io/flutter/embedding/engine/systemchannels/PlatformChannel.java
@@ -592,7 +592,10 @@
     LIGHT_IMPACT("HapticFeedbackType.lightImpact"),
     MEDIUM_IMPACT("HapticFeedbackType.mediumImpact"),
     HEAVY_IMPACT("HapticFeedbackType.heavyImpact"),
-    SELECTION_CLICK("HapticFeedbackType.selectionClick");
+    SELECTION_CLICK("HapticFeedbackType.selectionClick"),
+    SUCCESS_NOTIFICATION("HapticFeedbackType.successNotification"),
+    WARNING_NOTIFICATION("HapticFeedbackType.warningNotification"),
+    ERROR_NOTIFICATION("HapticFeedbackType.errorNotification");
 
     @NonNull
     static HapticFeedbackType fromValue(@Nullable String encodedName) throws NoSuchFieldException {
diff --git a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java
index 8315fa3..3c3ceea 100644
--- a/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java
+++ b/engine/src/flutter/shell/platform/android/io/flutter/plugin/platform/PlatformPlugin.java
@@ -209,6 +209,21 @@
       case SELECTION_CLICK:
         view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
         break;
+      case SUCCESS_NOTIFICATION:
+        if (Build.VERSION.SDK_INT >= API_LEVELS.API_30) {
+          view.performHapticFeedback(HapticFeedbackConstants.CONFIRM);
+        }
+        break;
+      case WARNING_NOTIFICATION:
+        if (Build.VERSION.SDK_INT >= API_LEVELS.API_30) {
+          view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
+        }
+        break;
+      case ERROR_NOTIFICATION:
+        if (Build.VERSION.SDK_INT >= API_LEVELS.API_30) {
+          view.performHapticFeedback(HapticFeedbackConstants.REJECT);
+        }
+        break;
     }
   }
 
diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java
index 9d2abb0..423176c 100644
--- a/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java
+++ b/engine/src/flutter/shell/platform/android/test/io/flutter/plugin/platform/PlatformPluginTest.java
@@ -15,6 +15,7 @@
 import static org.junit.Assert.fail;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.mockStatic;
@@ -33,6 +34,7 @@
 import android.content.res.AssetFileDescriptor;
 import android.net.Uri;
 import android.os.Build;
+import android.view.HapticFeedbackConstants;
 import android.view.View;
 import android.view.Window;
 import android.view.WindowInsetsController;
@@ -752,4 +754,106 @@
     assertEquals(sendToIntent.getType(), "text/plain");
     assertEquals(sendToIntent.getStringExtra(Intent.EXTRA_TEXT), expectedContent);
   }
+
+  @Config(sdk = API_LEVELS.API_29)
+  @Test
+  public void vibrateHapticFeedbackWhenApiLevelIsLessThan30() {
+    View fakeDecorView = mock(View.class);
+    Window fakeWindow = mock(Window.class);
+    Activity mockActivity = mock(Activity.class);
+    when(fakeWindow.getDecorView()).thenReturn(fakeDecorView);
+    when(mockActivity.getWindow()).thenReturn(fakeWindow);
+    PlatformPlugin platformPlugin = new PlatformPlugin(mockActivity, mockPlatformChannel);
+
+    platformPlugin.mPlatformMessageHandler.vibrateHapticFeedback(
+        PlatformChannel.HapticFeedbackType.STANDARD);
+    verify(fakeDecorView).performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+    clearInvocations(fakeDecorView);
+
+    platformPlugin.mPlatformMessageHandler.vibrateHapticFeedback(
+        PlatformChannel.HapticFeedbackType.LIGHT_IMPACT);
+    verify(fakeDecorView).performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+    clearInvocations(fakeDecorView);
+
+    platformPlugin.mPlatformMessageHandler.vibrateHapticFeedback(
+        PlatformChannel.HapticFeedbackType.MEDIUM_IMPACT);
+    verify(fakeDecorView).performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
+    clearInvocations(fakeDecorView);
+
+    platformPlugin.mPlatformMessageHandler.vibrateHapticFeedback(
+        PlatformChannel.HapticFeedbackType.HEAVY_IMPACT);
+    verify(fakeDecorView).performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
+    clearInvocations(fakeDecorView);
+
+    platformPlugin.mPlatformMessageHandler.vibrateHapticFeedback(
+        PlatformChannel.HapticFeedbackType.SELECTION_CLICK);
+    verify(fakeDecorView).performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+    clearInvocations(fakeDecorView);
+
+    platformPlugin.mPlatformMessageHandler.vibrateHapticFeedback(
+        PlatformChannel.HapticFeedbackType.SUCCESS_NOTIFICATION);
+    verify(fakeDecorView, never()).performHapticFeedback(HapticFeedbackConstants.CONFIRM);
+    clearInvocations(fakeDecorView);
+
+    platformPlugin.mPlatformMessageHandler.vibrateHapticFeedback(
+        PlatformChannel.HapticFeedbackType.WARNING_NOTIFICATION);
+    verify(fakeDecorView, never()).performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
+    clearInvocations(fakeDecorView);
+
+    platformPlugin.mPlatformMessageHandler.vibrateHapticFeedback(
+        PlatformChannel.HapticFeedbackType.ERROR_NOTIFICATION);
+    verify(fakeDecorView, never()).performHapticFeedback(HapticFeedbackConstants.REJECT);
+    clearInvocations(fakeDecorView);
+  }
+
+  @Config(minSdk = API_LEVELS.API_30)
+  @Test
+  public void vibrateHapticFeedbackWhenApiLevelIsHigherOrEquals30() {
+    View fakeDecorView = mock(View.class);
+    Window fakeWindow = mock(Window.class);
+    Activity mockActivity = mock(Activity.class);
+    when(fakeWindow.getDecorView()).thenReturn(fakeDecorView);
+    when(mockActivity.getWindow()).thenReturn(fakeWindow);
+    PlatformPlugin platformPlugin = new PlatformPlugin(mockActivity, mockPlatformChannel);
+
+    platformPlugin.mPlatformMessageHandler.vibrateHapticFeedback(
+        PlatformChannel.HapticFeedbackType.STANDARD);
+    verify(fakeDecorView).performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+    clearInvocations(fakeDecorView);
+
+    platformPlugin.mPlatformMessageHandler.vibrateHapticFeedback(
+        PlatformChannel.HapticFeedbackType.LIGHT_IMPACT);
+    verify(fakeDecorView).performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+    clearInvocations(fakeDecorView);
+
+    platformPlugin.mPlatformMessageHandler.vibrateHapticFeedback(
+        PlatformChannel.HapticFeedbackType.MEDIUM_IMPACT);
+    verify(fakeDecorView).performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
+    clearInvocations(fakeDecorView);
+
+    platformPlugin.mPlatformMessageHandler.vibrateHapticFeedback(
+        PlatformChannel.HapticFeedbackType.HEAVY_IMPACT);
+    verify(fakeDecorView).performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
+    clearInvocations(fakeDecorView);
+
+    platformPlugin.mPlatformMessageHandler.vibrateHapticFeedback(
+        PlatformChannel.HapticFeedbackType.SELECTION_CLICK);
+    verify(fakeDecorView).performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+    clearInvocations(fakeDecorView);
+
+    platformPlugin.mPlatformMessageHandler.vibrateHapticFeedback(
+        PlatformChannel.HapticFeedbackType.SUCCESS_NOTIFICATION);
+    verify(fakeDecorView).performHapticFeedback(HapticFeedbackConstants.CONFIRM);
+    clearInvocations(fakeDecorView);
+
+    platformPlugin.mPlatformMessageHandler.vibrateHapticFeedback(
+        PlatformChannel.HapticFeedbackType.WARNING_NOTIFICATION);
+    verify(fakeDecorView).performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
+    clearInvocations(fakeDecorView);
+
+    platformPlugin.mPlatformMessageHandler.vibrateHapticFeedback(
+        PlatformChannel.HapticFeedbackType.ERROR_NOTIFICATION);
+    verify(fakeDecorView).performHapticFeedback(HapticFeedbackConstants.REJECT);
+    clearInvocations(fakeDecorView);
+  }
 }
diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm
index 1adf1bd..808d87a 100644
--- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm
+++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm
@@ -265,6 +265,15 @@
     [[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleHeavy] impactOccurred];
   } else if ([@"HapticFeedbackType.selectionClick" isEqualToString:feedbackType]) {
     [[[UISelectionFeedbackGenerator alloc] init] selectionChanged];
+  } else if ([@"HapticFeedbackType.successNotification" isEqualToString:feedbackType]) {
+    [[[UINotificationFeedbackGenerator alloc] init]
+        notificationOccurred:UINotificationFeedbackTypeSuccess];
+  } else if ([@"HapticFeedbackType.warningNotification" isEqualToString:feedbackType]) {
+    [[[UINotificationFeedbackGenerator alloc] init]
+        notificationOccurred:UINotificationFeedbackTypeWarning];
+  } else if ([@"HapticFeedbackType.errorNotification" isEqualToString:feedbackType]) {
+    [[[UINotificationFeedbackGenerator alloc] init]
+        notificationOccurred:UINotificationFeedbackTypeError];
   }
 }
 
diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm
index 83f54d7..7fd8d8c 100644
--- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm
+++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPluginTest.mm
@@ -23,6 +23,7 @@
 - (void)showLookUpViewController:(NSString*)term;
 - (void)showShareViewController:(NSString*)content;
 - (void)playSystemSound:(NSString*)soundType;
+- (void)vibrateHapticFeedback:(NSString*)feedbackType;
 @end
 
 @interface UIViewController ()
@@ -312,6 +313,24 @@
   [self waitForExpectationsWithTimeout:1 handler:nil];
 }
 
+- (void)testHapticFeedbackVibrate {
+  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:nil];
+  XCTestExpectation* invokeExpectation =
+      [self expectationWithDescription:@"HapticFeedback.vibrate invoked"];
+  FlutterPlatformPlugin* plugin = [[FlutterPlatformPlugin alloc] initWithEngine:engine];
+  FlutterPlatformPlugin* mockPlugin = OCMPartialMock(plugin);
+
+  FlutterMethodCall* methodCall =
+      [FlutterMethodCall methodCallWithMethodName:@"HapticFeedback.vibrate"
+                                        arguments:@"HapticFeedbackType.lightImpact"];
+  FlutterResult result = ^(id result) {
+    OCMVerify([mockPlugin vibrateHapticFeedback:@"HapticFeedbackType.lightImpact"]);
+    [invokeExpectation fulfill];
+  };
+  [mockPlugin handleMethodCall:methodCall result:result];
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+}
+
 - (void)testViewControllerBasedStatusBarHiddenUpdate {
   id bundleMock = OCMPartialMock([NSBundle mainBundle]);
   OCMStub([bundleMock objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"])
diff --git a/packages/flutter/lib/src/services/haptic_feedback.dart b/packages/flutter/lib/src/services/haptic_feedback.dart
index 7105fb4..1ec0434 100644
--- a/packages/flutter/lib/src/services/haptic_feedback.dart
+++ b/packages/flutter/lib/src/services/haptic_feedback.dart
@@ -93,4 +93,59 @@
       'HapticFeedbackType.selectionClick',
     );
   }
+
+  /// Provides a haptic feedback indicating that a task or action has completed
+  /// successfully.
+  ///
+  /// On iOS, this uses a `UINotificationFeedbackGenerator` with
+  /// `UINotificationFeedbackTypeSuccess`.
+  ///
+  /// On Android, this uses `HapticFeedbackConstants.CONFIRM` on API levels 30
+  /// and above. This call has no effects on Android API levels below 30.
+  ///
+  /// {@template flutter.services.HapticFeedback.notification}
+  /// See also:
+  ///
+  ///  * [Human Interface Guidelines Playing Haptics](https://developer.apple.com/design/human-interface-guidelines/playing-haptics#Notification)
+  /// {@endtemplate}
+  static Future<void> successNotification() async {
+    await SystemChannels.platform.invokeMethod<void>(
+      'HapticFeedback.vibrate',
+      'HapticFeedbackType.successNotification',
+    );
+  }
+
+  /// Provides a haptic feedback indicating that a task or action has produced
+  /// a warning.
+  ///
+  /// On iOS, this uses a `UINotificationFeedbackGenerator` with
+  /// `UINotificationFeedbackTypeWarning`.
+  ///
+  /// On Android, this uses `HapticFeedbackConstants.KEYBOARD_TAP` on API
+  /// levels 30 and above. This call has no effects on Android API levels below
+  /// 30.
+  ///
+  /// {@macro flutter.services.HapticFeedback.notification}
+  static Future<void> warningNotification() async {
+    await SystemChannels.platform.invokeMethod<void>(
+      'HapticFeedback.vibrate',
+      'HapticFeedbackType.warningNotification',
+    );
+  }
+
+  /// Provides a haptic feedback indicating that a task or action has failed.
+  ///
+  /// On iOS, this uses a `UINotificationFeedbackGenerator` with
+  /// `UINotificationFeedbackTypeError`.
+  ///
+  /// On Android, this uses `HapticFeedbackConstants.REJECT` on API levels 30
+  /// and above. This call has no effects on Android API levels below 30.
+  ///
+  /// {@macro flutter.services.HapticFeedback.notification}
+  static Future<void> errorNotification() async {
+    await SystemChannels.platform.invokeMethod<void>(
+      'HapticFeedback.vibrate',
+      'HapticFeedbackType.errorNotification',
+    );
+  }
 }
diff --git a/packages/flutter/test/services/haptic_feedback_test.dart b/packages/flutter/test/services/haptic_feedback_test.dart
index baefeae..d471d7e 100644
--- a/packages/flutter/test/services/haptic_feedback_test.dart
+++ b/packages/flutter/test/services/haptic_feedback_test.dart
@@ -55,5 +55,17 @@
       HapticFeedback.selectionClick,
       'HapticFeedbackType.selectionClick',
     );
+    await callAndVerifyHapticFunction(
+      HapticFeedback.successNotification,
+      'HapticFeedbackType.successNotification',
+    );
+    await callAndVerifyHapticFunction(
+      HapticFeedback.warningNotification,
+      'HapticFeedbackType.warningNotification',
+    );
+    await callAndVerifyHapticFunction(
+      HapticFeedback.errorNotification,
+      'HapticFeedbackType.errorNotification',
+    );
   });
 }