[cp][3.28] Combined CP for "workaround for non-tappable webview #56804" AND "enable the webview non tappable workaround by checking subviews recursively #57168" (#57176)

CP for 3 PRs: https://github.com/flutter/engine/pull/56804 and https://github.com/flutter/engine/pull/57168 and  https://github.com/flutter/engine/pull/57193

This is for 3.28.

Since the previous PR was not merged, so I combined the 2 PRs to make it easier to merge. 

*List which issues are fixed by this PR. You must list at least one issue.*
https://github.com/pichillilorenzo/flutter_inappwebview/issues/2415

*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn
index 4a0eeac..6020365 100644
--- a/shell/platform/darwin/ios/BUILD.gn
+++ b/shell/platform/darwin/ios/BUILD.gn
@@ -165,6 +165,7 @@
     "CoreVideo.framework",
     "IOSurface.framework",
     "QuartzCore.framework",
+    "WebKit.framework",
     "UIKit.framework",
   ]
   if (flutter_runtime_mode == "profile" || flutter_runtime_mode == "debug") {
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
index a0e140e..16ce9ba 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
@@ -4,6 +4,8 @@
 
 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
 
+#import <WebKit/WebKit.h>
+
 #include "flutter/display_list/effects/dl_image_filter.h"
 #include "flutter/fml/platform/darwin/cf_utils.h"
 #import "flutter/shell/platform/darwin/ios/ios_surface.h"
@@ -564,11 +566,48 @@
   self.delayingRecognizer.state = UIGestureRecognizerStateFailed;
 }
 
+- (BOOL)containsWebView:(UIView*)view remainingSubviewDepth:(int)remainingSubviewDepth {
+  if (remainingSubviewDepth < 0) {
+    return NO;
+  }
+  if ([view isKindOfClass:[WKWebView class]]) {
+    return YES;
+  }
+  for (UIView* subview in view.subviews) {
+    if ([self containsWebView:subview remainingSubviewDepth:remainingSubviewDepth - 1]) {
+      return YES;
+    }
+  }
+  return NO;
+}
+
 - (void)blockGesture {
   switch (_blockingPolicy) {
     case FlutterPlatformViewGestureRecognizersBlockingPolicyEager:
       // We block all other gesture recognizers immediately in this policy.
       self.delayingRecognizer.state = UIGestureRecognizerStateEnded;
+
+      // On iOS 18.2, WKWebView's internal recognizer likely caches the old state of its blocking
+      // recognizers (i.e. delaying recognizer), resulting in non-tappable links. See
+      // https://github.com/flutter/flutter/issues/158961. Removing and adding back the delaying
+      // recognizer solves the problem, possibly because UIKit notifies all the recognizers related
+      // to (blocking or blocked by) this recognizer. It is not possible to inject this workaround
+      // from the web view plugin level. Right now we only observe this issue for
+      // FlutterPlatformViewGestureRecognizersBlockingPolicyEager, but we should try it if a similar
+      // issue arises for the other policy.
+      if (@available(iOS 18.2, *)) {
+        // This workaround is designed for WKWebView only. The 1P web view plugin provides a
+        // WKWebView itself as the platform view. However, some 3P plugins provide wrappers of
+        // WKWebView instead. So we perform DFS to search the view hierarchy (with a depth limit).
+        // Passing a limit of 0 means only searching for platform view itself; Pass 1 to include its
+        // children as well, and so on. We should be conservative and start with a small number. The
+        // AdMob banner has a WKWebView at depth 7.
+        if ([self containsWebView:self.embeddedView remainingSubviewDepth:1]) {
+          [self removeGestureRecognizer:self.delayingRecognizer];
+          [self addGestureRecognizer:self.delayingRecognizer];
+        }
+      }
+
       break;
     case FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded:
       if (self.delayingRecognizer.touchedEndedWithoutBlocking) {
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
index 2a890cc..2402fec 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
@@ -6,6 +6,7 @@
 
 #import <OCMock/OCMock.h>
 #import <UIKit/UIKit.h>
+#import <WebKit/WebKit.h>
 #import <XCTest/XCTest.h>
 
 #include <memory>
@@ -25,7 +26,7 @@
 FLUTTER_ASSERT_ARC
 
 @class FlutterPlatformViewsTestMockPlatformView;
-__weak static FlutterPlatformViewsTestMockPlatformView* gMockPlatformView = nil;
+__weak static UIView* gMockPlatformView = nil;
 const float kFloatCompareEpsilon = 0.001;
 
 @interface FlutterPlatformViewsTestMockPlatformView : UIView
@@ -73,6 +74,9 @@
   self.viewCreated = YES;
 }
 
+- (void)dealloc {
+  gMockPlatformView = nil;
+}
 @end
 
 @interface FlutterPlatformViewsTestMockFlutterPlatformFactory
@@ -88,6 +92,49 @@
 
 @end
 
+@interface FlutterPlatformViewsTestMockWebView : NSObject <FlutterPlatformView>
+@property(nonatomic, strong) UIView* view;
+@property(nonatomic, assign) BOOL viewCreated;
+@end
+
+@implementation FlutterPlatformViewsTestMockWebView
+- (instancetype)init {
+  if (self = [super init]) {
+    _view = [[WKWebView alloc] init];
+    gMockPlatformView = _view;
+    _viewCreated = NO;
+  }
+  return self;
+}
+
+- (UIView*)view {
+  [self checkViewCreatedOnce];
+  return _view;
+}
+
+- (void)checkViewCreatedOnce {
+  if (self.viewCreated) {
+    abort();
+  }
+  self.viewCreated = YES;
+}
+
+- (void)dealloc {
+  gMockPlatformView = nil;
+}
+@end
+
+@interface FlutterPlatformViewsTestMockWebViewFactory : NSObject <FlutterPlatformViewFactory>
+@end
+
+@implementation FlutterPlatformViewsTestMockWebViewFactory
+- (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame
+                                   viewIdentifier:(int64_t)viewId
+                                        arguments:(id _Nullable)args {
+  return [[FlutterPlatformViewsTestMockWebView alloc] init];
+}
+@end
+
 @interface FlutterPlatformViewsTestNilFlutterPlatformFactory : NSObject <FlutterPlatformViewFactory>
 @end
 
@@ -100,6 +147,93 @@
 
 @end
 
+@interface FlutterPlatformViewsTestMockWrapperWebView : NSObject <FlutterPlatformView>
+@property(nonatomic, strong) UIView* view;
+@property(nonatomic, assign) BOOL viewCreated;
+@end
+
+@implementation FlutterPlatformViewsTestMockWrapperWebView
+- (instancetype)init {
+  if (self = [super init]) {
+    _view = [[UIView alloc] init];
+    [_view addSubview:[[WKWebView alloc] init]];
+    gMockPlatformView = _view;
+    _viewCreated = NO;
+  }
+  return self;
+}
+
+- (UIView*)view {
+  [self checkViewCreatedOnce];
+  return _view;
+}
+
+- (void)checkViewCreatedOnce {
+  if (self.viewCreated) {
+    abort();
+  }
+  self.viewCreated = YES;
+}
+
+- (void)dealloc {
+  gMockPlatformView = nil;
+}
+@end
+
+@interface FlutterPlatformViewsTestMockWrapperWebViewFactory : NSObject <FlutterPlatformViewFactory>
+@end
+
+@implementation FlutterPlatformViewsTestMockWrapperWebViewFactory
+- (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame
+                                   viewIdentifier:(int64_t)viewId
+                                        arguments:(id _Nullable)args {
+  return [[FlutterPlatformViewsTestMockWrapperWebView alloc] init];
+}
+@end
+
+@interface FlutterPlatformViewsTestMockNestedWrapperWebView : NSObject <FlutterPlatformView>
+@property(nonatomic, strong) UIView* view;
+@property(nonatomic, assign) BOOL viewCreated;
+@end
+
+@implementation FlutterPlatformViewsTestMockNestedWrapperWebView
+- (instancetype)init {
+  if (self = [super init]) {
+    _view = [[UIView alloc] init];
+    UIView* childView = [[UIView alloc] init];
+    [_view addSubview:childView];
+    [childView addSubview:[[WKWebView alloc] init]];
+    gMockPlatformView = _view;
+    _viewCreated = NO;
+  }
+  return self;
+}
+
+- (UIView*)view {
+  [self checkViewCreatedOnce];
+  return _view;
+}
+
+- (void)checkViewCreatedOnce {
+  if (self.viewCreated) {
+    abort();
+  }
+  self.viewCreated = YES;
+}
+@end
+
+@interface FlutterPlatformViewsTestMockNestedWrapperWebViewFactory
+    : NSObject <FlutterPlatformViewFactory>
+@end
+
+@implementation FlutterPlatformViewsTestMockNestedWrapperWebViewFactory
+- (NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame
+                                   viewIdentifier:(int64_t)viewId
+                                        arguments:(id _Nullable)args {
+  return [[FlutterPlatformViewsTestMockNestedWrapperWebView alloc] init];
+}
+@end
+
 namespace flutter {
 namespace {
 class FlutterPlatformViewsTestMockPlatformViewDelegate : public PlatformView::Delegate {
@@ -3048,6 +3182,258 @@
   [flutterPlatformViewsController reset];
 }
 
+- (void)
+    testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldRemoveAndAddBackDelayingRecognizerForWebView {
+  flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
+
+  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
+                               /*platform=*/GetDefaultTaskRunner(),
+                               /*raster=*/GetDefaultTaskRunner(),
+                               /*ui=*/GetDefaultTaskRunner(),
+                               /*io=*/GetDefaultTaskRunner());
+  FlutterPlatformViewsController* flutterPlatformViewsController =
+      [[FlutterPlatformViewsController alloc] init];
+  flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner();
+  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
+      /*delegate=*/mock_delegate,
+      /*rendering_api=*/mock_delegate.settings_.enable_impeller
+          ? flutter::IOSRenderingAPI::kMetal
+          : flutter::IOSRenderingAPI::kSoftware,
+      /*platform_views_controller=*/flutterPlatformViewsController,
+      /*task_runners=*/runners,
+      /*worker_task_runner=*/nil,
+      /*is_gpu_disabled_jsync_switch=*/std::make_shared<fml::SyncSwitch>());
+
+  FlutterPlatformViewsTestMockWebViewFactory* factory =
+      [[FlutterPlatformViewsTestMockWebViewFactory alloc] init];
+  [flutterPlatformViewsController
+                   registerViewFactory:factory
+                                withId:@"MockWebView"
+      gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager];
+  FlutterResult result = ^(id result) {
+  };
+  [flutterPlatformViewsController
+      onMethodCall:[FlutterMethodCall
+                       methodCallWithMethodName:@"create"
+                                      arguments:@{@"id" : @2, @"viewType" : @"MockWebView"}]
+            result:result];
+
+  XCTAssertNotNil(gMockPlatformView);
+
+  // Find touch inteceptor view
+  UIView* touchInteceptorView = gMockPlatformView;
+  while (touchInteceptorView != nil &&
+         ![touchInteceptorView isKindOfClass:[FlutterTouchInterceptingView class]]) {
+    touchInteceptorView = touchInteceptorView.superview;
+  }
+  XCTAssertNotNil(touchInteceptorView);
+
+  XCTAssert(touchInteceptorView.gestureRecognizers.count == 2);
+  UIGestureRecognizer* delayingRecognizer = touchInteceptorView.gestureRecognizers[0];
+  UIGestureRecognizer* forwardingRecognizer = touchInteceptorView.gestureRecognizers[1];
+
+  XCTAssert([delayingRecognizer isKindOfClass:[FlutterDelayingGestureRecognizer class]]);
+  XCTAssert([forwardingRecognizer isKindOfClass:[ForwardingGestureRecognizer class]]);
+
+  [(FlutterTouchInterceptingView*)touchInteceptorView blockGesture];
+
+  if (@available(iOS 18.2, *)) {
+    // Since we remove and add back delayingRecognizer, it would be reordered to the last.
+    XCTAssertEqual(touchInteceptorView.gestureRecognizers[0], forwardingRecognizer);
+    XCTAssertEqual(touchInteceptorView.gestureRecognizers[1], delayingRecognizer);
+  } else {
+    XCTAssertEqual(touchInteceptorView.gestureRecognizers[0], delayingRecognizer);
+    XCTAssertEqual(touchInteceptorView.gestureRecognizers[1], forwardingRecognizer);
+  }
+}
+
+- (void)
+    testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldRemoveAndAddBackDelayingRecognizerForWrapperWebView {
+  flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
+
+  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
+                               /*platform=*/GetDefaultTaskRunner(),
+                               /*raster=*/GetDefaultTaskRunner(),
+                               /*ui=*/GetDefaultTaskRunner(),
+                               /*io=*/GetDefaultTaskRunner());
+  FlutterPlatformViewsController* flutterPlatformViewsController =
+      [[FlutterPlatformViewsController alloc] init];
+  flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner();
+  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
+      /*delegate=*/mock_delegate,
+      /*rendering_api=*/mock_delegate.settings_.enable_impeller
+          ? flutter::IOSRenderingAPI::kMetal
+          : flutter::IOSRenderingAPI::kSoftware,
+      /*platform_views_controller=*/flutterPlatformViewsController,
+      /*task_runners=*/runners,
+      /*worker_task_runner=*/nil,
+      /*is_gpu_disabled_jsync_switch=*/std::make_shared<fml::SyncSwitch>());
+
+  FlutterPlatformViewsTestMockWrapperWebViewFactory* factory =
+      [[FlutterPlatformViewsTestMockWrapperWebViewFactory alloc] init];
+  [flutterPlatformViewsController
+                   registerViewFactory:factory
+                                withId:@"MockWrapperWebView"
+      gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager];
+  FlutterResult result = ^(id result) {
+  };
+  [flutterPlatformViewsController
+      onMethodCall:[FlutterMethodCall
+                       methodCallWithMethodName:@"create"
+                                      arguments:@{@"id" : @2, @"viewType" : @"MockWrapperWebView"}]
+            result:result];
+
+  XCTAssertNotNil(gMockPlatformView);
+
+  // Find touch inteceptor view
+  UIView* touchInteceptorView = gMockPlatformView;
+  while (touchInteceptorView != nil &&
+         ![touchInteceptorView isKindOfClass:[FlutterTouchInterceptingView class]]) {
+    touchInteceptorView = touchInteceptorView.superview;
+  }
+  XCTAssertNotNil(touchInteceptorView);
+
+  XCTAssert(touchInteceptorView.gestureRecognizers.count == 2);
+  UIGestureRecognizer* delayingRecognizer = touchInteceptorView.gestureRecognizers[0];
+  UIGestureRecognizer* forwardingRecognizer = touchInteceptorView.gestureRecognizers[1];
+
+  XCTAssert([delayingRecognizer isKindOfClass:[FlutterDelayingGestureRecognizer class]]);
+  XCTAssert([forwardingRecognizer isKindOfClass:[ForwardingGestureRecognizer class]]);
+
+  [(FlutterTouchInterceptingView*)touchInteceptorView blockGesture];
+
+  if (@available(iOS 18.2, *)) {
+    // Since we remove and add back delayingRecognizer, it would be reordered to the last.
+    XCTAssertEqual(touchInteceptorView.gestureRecognizers[0], forwardingRecognizer);
+    XCTAssertEqual(touchInteceptorView.gestureRecognizers[1], delayingRecognizer);
+  } else {
+    XCTAssertEqual(touchInteceptorView.gestureRecognizers[0], delayingRecognizer);
+    XCTAssertEqual(touchInteceptorView.gestureRecognizers[1], forwardingRecognizer);
+  }
+}
+
+- (void)
+    testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldNotRemoveAndAddBackDelayingRecognizerForNestedWrapperWebView {
+  flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
+
+  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
+                               /*platform=*/GetDefaultTaskRunner(),
+                               /*raster=*/GetDefaultTaskRunner(),
+                               /*ui=*/GetDefaultTaskRunner(),
+                               /*io=*/GetDefaultTaskRunner());
+  FlutterPlatformViewsController* flutterPlatformViewsController =
+      [[FlutterPlatformViewsController alloc] init];
+  flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner();
+  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
+      /*delegate=*/mock_delegate,
+      /*rendering_api=*/mock_delegate.settings_.enable_impeller
+          ? flutter::IOSRenderingAPI::kMetal
+          : flutter::IOSRenderingAPI::kSoftware,
+      /*platform_views_controller=*/flutterPlatformViewsController,
+      /*task_runners=*/runners,
+      /*worker_task_runner=*/nil,
+      /*is_gpu_disabled_jsync_switch=*/std::make_shared<fml::SyncSwitch>());
+
+  FlutterPlatformViewsTestMockNestedWrapperWebViewFactory* factory =
+      [[FlutterPlatformViewsTestMockNestedWrapperWebViewFactory alloc] init];
+  [flutterPlatformViewsController
+                   registerViewFactory:factory
+                                withId:@"MockNestedWrapperWebView"
+      gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager];
+  FlutterResult result = ^(id result) {
+  };
+  [flutterPlatformViewsController
+      onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"create"
+                                                     arguments:@{
+                                                       @"id" : @2,
+                                                       @"viewType" : @"MockNestedWrapperWebView"
+                                                     }]
+            result:result];
+
+  XCTAssertNotNil(gMockPlatformView);
+
+  // Find touch inteceptor view
+  UIView* touchInteceptorView = gMockPlatformView;
+  while (touchInteceptorView != nil &&
+         ![touchInteceptorView isKindOfClass:[FlutterTouchInterceptingView class]]) {
+    touchInteceptorView = touchInteceptorView.superview;
+  }
+  XCTAssertNotNil(touchInteceptorView);
+
+  XCTAssert(touchInteceptorView.gestureRecognizers.count == 2);
+  UIGestureRecognizer* delayingRecognizer = touchInteceptorView.gestureRecognizers[0];
+  UIGestureRecognizer* forwardingRecognizer = touchInteceptorView.gestureRecognizers[1];
+
+  XCTAssert([delayingRecognizer isKindOfClass:[FlutterDelayingGestureRecognizer class]]);
+  XCTAssert([forwardingRecognizer isKindOfClass:[ForwardingGestureRecognizer class]]);
+
+  [(FlutterTouchInterceptingView*)touchInteceptorView blockGesture];
+
+  XCTAssertEqual(touchInteceptorView.gestureRecognizers[0], delayingRecognizer);
+  XCTAssertEqual(touchInteceptorView.gestureRecognizers[1], forwardingRecognizer);
+}
+
+- (void)
+    testFlutterPlatformViewBlockGestureUnderEagerPolicyShouldNotRemoveAndAddBackDelayingRecognizerForNonWebView {
+  flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
+
+  flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
+                               /*platform=*/GetDefaultTaskRunner(),
+                               /*raster=*/GetDefaultTaskRunner(),
+                               /*ui=*/GetDefaultTaskRunner(),
+                               /*io=*/GetDefaultTaskRunner());
+  FlutterPlatformViewsController* flutterPlatformViewsController =
+      [[FlutterPlatformViewsController alloc] init];
+  flutterPlatformViewsController.taskRunner = GetDefaultTaskRunner();
+  auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
+      /*delegate=*/mock_delegate,
+      /*rendering_api=*/mock_delegate.settings_.enable_impeller
+          ? flutter::IOSRenderingAPI::kMetal
+          : flutter::IOSRenderingAPI::kSoftware,
+      /*platform_views_controller=*/flutterPlatformViewsController,
+      /*task_runners=*/runners,
+      /*worker_task_runner=*/nil,
+      /*is_gpu_disabled_jsync_switch=*/std::make_shared<fml::SyncSwitch>());
+
+  FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
+      [[FlutterPlatformViewsTestMockFlutterPlatformFactory alloc] init];
+  [flutterPlatformViewsController
+                   registerViewFactory:factory
+                                withId:@"MockFlutterPlatformView"
+      gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager];
+  FlutterResult result = ^(id result) {
+  };
+  [flutterPlatformViewsController
+      onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"create"
+                                                     arguments:@{
+                                                       @"id" : @2,
+                                                       @"viewType" : @"MockFlutterPlatformView"
+                                                     }]
+            result:result];
+
+  XCTAssertNotNil(gMockPlatformView);
+
+  // Find touch inteceptor view
+  UIView* touchInteceptorView = gMockPlatformView;
+  while (touchInteceptorView != nil &&
+         ![touchInteceptorView isKindOfClass:[FlutterTouchInterceptingView class]]) {
+    touchInteceptorView = touchInteceptorView.superview;
+  }
+  XCTAssertNotNil(touchInteceptorView);
+
+  XCTAssert(touchInteceptorView.gestureRecognizers.count == 2);
+  UIGestureRecognizer* delayingRecognizer = touchInteceptorView.gestureRecognizers[0];
+  UIGestureRecognizer* forwardingRecognizer = touchInteceptorView.gestureRecognizers[1];
+
+  XCTAssert([delayingRecognizer isKindOfClass:[FlutterDelayingGestureRecognizer class]]);
+  XCTAssert([forwardingRecognizer isKindOfClass:[ForwardingGestureRecognizer class]]);
+
+  [(FlutterTouchInterceptingView*)touchInteceptorView blockGesture];
+
+  XCTAssertEqual(touchInteceptorView.gestureRecognizers[0], delayingRecognizer);
+  XCTAssertEqual(touchInteceptorView.gestureRecognizers[1], forwardingRecognizer);
+}
+
 - (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashing {
   flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;