[flutter_releases] Flutter beta 2.7.0-3.1.pre Engine Cherrypicks (#29363)

* [ci.yaml] Disable framework tests on release branches (#29213)

* [iOS] Fix duplicated keys when typing quickly on HW keyboard (#29113)

* [iOS] Fix duplicated keys when typing quickly on HW keyboard

* Address review (refactor constant)

* Change message loop constant name

* Apply suggestions from code review

Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com>

Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com>

Co-authored-by: Casey Hillers <chillers@google.com>
Co-authored-by: Callum Moffat <smartercallum@gmail.com>
Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com>
diff --git a/.ci.yaml b/.ci.yaml
index e3c3d59..fff9e5c 100644
--- a/.ci.yaml
+++ b/.ci.yaml
@@ -178,6 +178,9 @@
 
   - name: Linux Framework Smoke Tests
     recipe: engine/framework_smoke
+    enabled_branches:
+      - main
+      - master
     timeout: 60
     scheduler: luci
 
@@ -221,6 +224,9 @@
 
   - name: Linux Web Framework tests
     recipe: engine/web_engine_framework
+    enabled_branches:
+      - main
+      - master
     properties:
       add_recipes_cq: "true"
       framework: "true"
diff --git a/fml/platform/darwin/message_loop_darwin.h b/fml/platform/darwin/message_loop_darwin.h
index fb4c178..fb43ca5 100644
--- a/fml/platform/darwin/message_loop_darwin.h
+++ b/fml/platform/darwin/message_loop_darwin.h
@@ -16,6 +16,12 @@
 namespace fml {
 
 class MessageLoopDarwin : public MessageLoopImpl {
+ public:
+  // A custom CFRunLoop mode used when processing flutter messages,
+  // so that the CFRunLoop can be run without being interrupted by UIKit,
+  // while still being able to receive and be interrupted by framework messages.
+  static CFStringRef kMessageLoopCFRunLoopMode;
+
  private:
   std::atomic_bool running_;
   CFRef<CFRunLoopTimerRef> delayed_wake_timer_;
diff --git a/fml/platform/darwin/message_loop_darwin.mm b/fml/platform/darwin/message_loop_darwin.mm
index ca714c8..969ff8b 100644
--- a/fml/platform/darwin/message_loop_darwin.mm
+++ b/fml/platform/darwin/message_loop_darwin.mm
@@ -13,6 +13,8 @@
 
 static constexpr CFTimeInterval kDistantFuture = 1.0e10;
 
+CFStringRef MessageLoopDarwin::kMessageLoopCFRunLoopMode = CFSTR("fmlMessageLoop");
+
 MessageLoopDarwin::MessageLoopDarwin()
     : running_(false), loop_((CFRunLoopRef)CFRetain(CFRunLoopGetCurrent())) {
   FML_DCHECK(loop_ != nullptr);
@@ -29,11 +31,14 @@
                            &timer_context /* context */));
   FML_DCHECK(delayed_wake_timer_ != nullptr);
   CFRunLoopAddTimer(loop_, delayed_wake_timer_, kCFRunLoopCommonModes);
+  // This mode will be used by FlutterKeyboardManager.
+  CFRunLoopAddTimer(loop_, delayed_wake_timer_, kMessageLoopCFRunLoopMode);
 }
 
 MessageLoopDarwin::~MessageLoopDarwin() {
   CFRunLoopTimerInvalidate(delayed_wake_timer_);
   CFRunLoopRemoveTimer(loop_, delayed_wake_timer_, kCFRunLoopCommonModes);
+  CFRunLoopRemoveTimer(loop_, delayed_wake_timer_, kMessageLoopCFRunLoopMode);
 }
 
 void MessageLoopDarwin::Run() {
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.mm b/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.mm
index f575307..4d8d7b9 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.mm
@@ -4,6 +4,9 @@
 
 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.h"
 #include "flutter/fml/memory/weak_ptr.h"
+#include "flutter/fml/platform/darwin/message_loop_darwin.h"
+
+static constexpr CFTimeInterval kDistantFuture = 1.0e10;
 
 @interface FlutterKeyboardManager ()
 
@@ -102,7 +105,10 @@
       // framework. Once the completeCallback is called, this run loop will exit
       // and the main one will resume. The completeCallback MUST be called, or
       // the app will get stuck in this run loop indefinitely.
-      CFRunLoopRun();
+      //
+      // We need to run in this mode so that UIKit doesn't give us new
+      // events until we are done processing this one.
+      CFRunLoopRunInMode(fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode, kDistantFuture, NO);
       break;
     }
     case UIPressPhaseChanged:
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManagerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManagerTest.mm
index 0eafd55..2586844 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManagerTest.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManagerTest.mm
@@ -8,6 +8,7 @@
 #import <XCTest/XCTest.h>
 #include <_types/_uint32_t.h>
 
+#include "flutter/fml/platform/darwin/message_loop_darwin.h"
 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h"
 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.h"
@@ -59,7 +60,8 @@
 
 namespace {
 
-typedef void (^KeyCallbackSetter)(FlutterAsyncKeyCallback callback);
+typedef void (^KeyCallbackSetter)(FlutterUIPressProxy* press, FlutterAsyncKeyCallback callback)
+    API_AVAILABLE(ios(13.4));
 typedef BOOL (^BoolGetter)();
 
 }  // namespace
@@ -100,11 +102,14 @@
       OCMStrictProtocolMock(@protocol(FlutterKeyPrimaryResponder));
   OCMStub([mock handlePress:[OCMArg any] callback:[OCMArg any]])
       .andDo((^(NSInvocation* invocation) {
+        FlutterUIPressProxy* press;
         FlutterAsyncKeyCallback callback;
+        [invocation getArgument:&press atIndex:2];
         [invocation getArgument:&callback atIndex:3];
-        CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^() {
-          callbackSetter(callback);
-        });
+        CFRunLoopPerformBlock(CFRunLoopGetCurrent(),
+                              fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode, ^() {
+                                callbackSetter(press, callback);
+                              });
       }));
   return mock;
 }
@@ -149,7 +154,8 @@
   FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init];
   __block BOOL primaryResponse = FALSE;
   __block int callbackCount = 0;
-  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterAsyncKeyCallback callback) {
+  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
+                                                            FlutterAsyncKeyCallback callback) {
              callbackCount++;
              callback(primaryResponse);
            }]];
@@ -181,14 +187,16 @@
 
   __block BOOL callback1Response = FALSE;
   __block int callback1Count = 0;
-  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterAsyncKeyCallback callback) {
+  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
+                                                            FlutterAsyncKeyCallback callback) {
              callback1Count++;
              callback(callback1Response);
            }]];
 
   __block BOOL callback2Response = FALSE;
   __block int callback2Count = 0;
-  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterAsyncKeyCallback callback) {
+  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
+                                                            FlutterAsyncKeyCallback callback) {
              callback2Count++;
              callback(callback2Response);
            }]];
@@ -242,7 +250,8 @@
 
   __block BOOL primaryResponse = FALSE;
   __block int callbackCount = 0;
-  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterAsyncKeyCallback callback) {
+  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
+                                                            FlutterAsyncKeyCallback callback) {
              callbackCount++;
              callback(primaryResponse);
            }]];
@@ -291,4 +300,73 @@
   XCTAssertFalse(completeHandled);
 }
 
+- (void)testEventsProcessedSequentially API_AVAILABLE(ios(13.4)) {
+  constexpr UIKeyboardHIDUsage keyId1 = (UIKeyboardHIDUsage)0x50;
+  constexpr UIKeyboardHIDUsage keyId2 = (UIKeyboardHIDUsage)0x51;
+  FlutterUIPressProxy* event1 = keyDownEvent(keyId1);
+  FlutterUIPressProxy* event2 = keyDownEvent(keyId2);
+  __block FlutterAsyncKeyCallback key1Callback;
+  __block FlutterAsyncKeyCallback key2Callback;
+  __block bool key1Handled = true;
+  __block bool key2Handled = true;
+
+  FlutterKeyboardManager* manager = [[FlutterKeyboardManager alloc] init];
+  [manager addPrimaryResponder:[self mockPrimaryResponder:^(FlutterUIPressProxy* press,
+                                                            FlutterAsyncKeyCallback callback) {
+             if (press == event1) {
+               key1Callback = callback;
+             } else if (press == event2) {
+               key2Callback = callback;
+             }
+           }]];
+
+  // Add both presses into the main CFRunLoop queue
+  CFRunLoopTimerRef timer0 = CFRunLoopTimerCreateWithHandler(
+      kCFAllocatorDefault, CFAbsoluteTimeGetCurrent(), 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
+        [manager handlePress:event1
+                  nextAction:^() {
+                    key1Handled = false;
+                  }];
+      });
+  CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer0, kCFRunLoopCommonModes);
+  CFRunLoopTimerRef timer1 = CFRunLoopTimerCreateWithHandler(
+      kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 1, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
+        // key1 should be completely finished by now
+        XCTAssertFalse(key1Handled);
+        [manager handlePress:event2
+                  nextAction:^() {
+                    key2Handled = false;
+                  }];
+        // End the nested CFRunLoop
+        CFRunLoopStop(CFRunLoopGetCurrent());
+      });
+  CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer1, kCFRunLoopCommonModes);
+
+  // Add the callbacks to the CFRunLoop with mode kMessageLoopCFRunLoopMode
+  // This allows them to interrupt the loop started within handlePress
+  CFRunLoopTimerRef timer2 = CFRunLoopTimerCreateWithHandler(
+      kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 2, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
+        // No processing should be done on key2 yet
+        XCTAssertTrue(key1Callback != nil);
+        XCTAssertTrue(key2Callback == nil);
+        key1Callback(false);
+      });
+  CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer2,
+                    fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode);
+  CFRunLoopTimerRef timer3 = CFRunLoopTimerCreateWithHandler(
+      kCFAllocatorDefault, CFAbsoluteTimeGetCurrent() + 3, 0, 0, 0, ^(CFRunLoopTimerRef timerRef) {
+        // Both keys should be processed by now
+        XCTAssertTrue(key1Callback != nil);
+        XCTAssertTrue(key2Callback != nil);
+        key2Callback(false);
+      });
+  CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer3,
+                    fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode);
+
+  // Start a nested CFRunLoop so we can wait for both presses to complete before exiting the test
+  CFRunLoopRun();
+  XCTAssertFalse(key2Handled);
+  XCTAssertFalse(key1Handled);
+}
+
 @end
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm
index 0ee9487..a06d19b 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm
@@ -5,6 +5,7 @@
 #import <OCMock/OCMock.h>
 #import <XCTest/XCTest.h>
 
+#include "flutter/fml/platform/darwin/message_loop_darwin.h"
 #import "flutter/lib/ui/window/viewport_metrics.h"
 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h"
 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
@@ -59,9 +60,10 @@
   // NSAssert(callback != nullptr, @"Invalid callback");
   // Response is async, so we have to post it to the run loop instead of calling
   // it directly.
-  CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^() {
-    callback(true, userData);
-  });
+  CFRunLoopPerformBlock(CFRunLoopGetCurrent(), fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode,
+                        ^() {
+                          callback(true, userData);
+                        });
 }
 @end
 
@@ -763,9 +765,10 @@
   // Response is async, so we have to post it to the run loop instead of calling
   // it directly.
   self.messageSent = message;
-  CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^() {
-    callback(replyMessage);
-  });
+  CFRunLoopPerformBlock(CFRunLoopGetCurrent(), fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode,
+                        ^() {
+                          callback(replyMessage);
+                        });
 }
 
 - (void)testValidKeyUpEvent API_AVAILABLE(ios(13.4)) {