[CP (beta)] [ios17][text_input]fix ios 17.0 keyboard freeze when switching languages (#48041)

CP for https://github.com/flutter/engine/pull/47566

*List which issues are fixed by this PR. You must list at least one issue.*

https://github.com/flutter/flutter/issues/134716

*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/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
index f02ab85..09946dc 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
@@ -602,7 +602,7 @@
     case UITextGranularityLine:
       // The default UITextInputStringTokenizer does not handle line granularity
       // correctly. We need to implement our own line tokenizer.
-      result = [self lineEnclosingPosition:position];
+      result = [self lineEnclosingPosition:position inDirection:direction];
       break;
     case UITextGranularityCharacter:
     case UITextGranularityWord:
@@ -618,7 +618,21 @@
   return result;
 }
 
-- (UITextRange*)lineEnclosingPosition:(UITextPosition*)position {
+- (UITextRange*)lineEnclosingPosition:(UITextPosition*)position
+                          inDirection:(UITextDirection)direction {
+  // TODO(hellohuanlin): remove iOS 17 check. The same logic should apply to older iOS version.
+  if (@available(iOS 17.0, *)) {
+    // According to the API doc if the text position is at a text-unit boundary, it is considered
+    // enclosed only if the next position in the given direction is entirely enclosed. Link:
+    // https://developer.apple.com/documentation/uikit/uitextinputtokenizer/1614464-rangeenclosingposition?language=objc
+    FlutterTextPosition* flutterPosition = (FlutterTextPosition*)position;
+    if (flutterPosition.index > _textInputView.text.length ||
+        (flutterPosition.index == _textInputView.text.length &&
+         direction == UITextStorageDirectionForward)) {
+      return nil;
+    }
+  }
+
   // Gets the first line break position after the input position.
   NSString* textAfter = [_textInputView
       textInRange:[_textInputView textRangeFromPosition:position
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm
index f18357a..8a6cc6b 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm
@@ -2660,6 +2660,54 @@
   XCTAssertEqual(range.range.length, 20u);
 }
 
+- (void)testFlutterTokenizerLineEnclosingEndOfDocumentInBackwardDirectionShouldNotReturnNil {
+  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
+  [inputView insertText:@"0123456789\n012345"];
+  id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
+
+  FlutterTextRange* range =
+      (FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
+                                           withGranularity:UITextGranularityLine
+                                               inDirection:UITextStorageDirectionBackward];
+  XCTAssertEqual(range.range.location, 11u);
+  XCTAssertEqual(range.range.length, 6u);
+}
+
+- (void)testFlutterTokenizerLineEnclosingEndOfDocumentInForwardDirectionShouldReturnNilOnIOS17 {
+  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
+  [inputView insertText:@"0123456789\n012345"];
+  id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
+
+  FlutterTextRange* range =
+      (FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument]
+                                           withGranularity:UITextGranularityLine
+                                               inDirection:UITextStorageDirectionForward];
+  if (@available(iOS 17.0, *)) {
+    XCTAssertNil(range);
+  } else {
+    XCTAssertEqual(range.range.location, 11u);
+    XCTAssertEqual(range.range.length, 6u);
+  }
+}
+
+- (void)testFlutterTokenizerLineEnclosingOutOfRangePositionShouldReturnNilOnIOS17 {
+  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
+  [inputView insertText:@"0123456789\n012345"];
+  id<UITextInputTokenizer> tokenizer = [inputView tokenizer];
+
+  FlutterTextPosition* position = [FlutterTextPosition positionWithIndex:100];
+  FlutterTextRange* range =
+      (FlutterTextRange*)[tokenizer rangeEnclosingPosition:position
+                                           withGranularity:UITextGranularityLine
+                                               inDirection:UITextStorageDirectionForward];
+  if (@available(iOS 17.0, *)) {
+    XCTAssertNil(range);
+  } else {
+    XCTAssertEqual(range.range.location, 0u);
+    XCTAssertEqual(range.range.length, 0u);
+  }
+}
+
 - (void)testFlutterTextInputPluginRetainsFlutterTextInputView {
   FlutterViewController* flutterViewController = [[FlutterViewController alloc] init];
   FlutterTextInputPlugin* myInputPlugin = [[FlutterTextInputPlugin alloc] initWithDelegate:engine];