Fix an issue that deleting an emoji may crash the app (#34508)
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
index 0e2c656..2d33fb9 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
@@ -7,6 +7,8 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
+#include "unicode/uchar.h"
+
#include "flutter/fml/logging.h"
#include "flutter/fml/platform/darwin/string_range_sanitization.h"
@@ -1896,6 +1898,22 @@
NSRange oldRange = ((FlutterTextRange*)oldSelectedRange).range;
if (oldRange.location > 0) {
NSRange newRange = NSMakeRange(oldRange.location - 1, 1);
+
+ // We should check if the last character is a part of emoji.
+ // If so, we must delete the entire emoji to prevent the text from being malformed.
+ NSRange charRange = fml::RangeForCharacterAtIndex(self.text, oldRange.location - 1);
+ UChar32 codePoint;
+ BOOL gotCodePoint = [self.text getBytes:&codePoint
+ maxLength:sizeof(codePoint)
+ usedLength:NULL
+ encoding:NSUTF32StringEncoding
+ options:kNilOptions
+ range:charRange
+ remainingRange:NULL];
+ if (gotCodePoint && u_hasBinaryProperty(codePoint, UCHAR_EMOJI)) {
+ newRange = NSMakeRange(charRange.location, oldRange.location - charRange.location);
+ }
+
_selectedTextRange = [[FlutterTextRange rangeWithNSRange:newRange] copy];
[oldSelectedRange release];
}
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm
index bd656cb..bec7028 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm
@@ -406,6 +406,41 @@
XCTAssertEqualObjects(substring, @"bbbbaaaabbbbaaaa");
}
+- (void)testDeletingBackward {
+ NSDictionary* config = self.mutableTemplateCopy;
+ [self setClientId:123 configuration:config];
+ NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
+ FlutterTextInputView* inputView = inputFields[0];
+
+ [inputView insertText:@"αΉπ text π₯°π¨π©π§π¦πΊπ³ΰΈΰΈ΅ "];
+ [inputView deleteBackward];
+ [inputView deleteBackward];
+
+ // Thai vowel is removed.
+ XCTAssertEqualObjects(inputView.text, @"αΉπ text π₯°π¨π©π§π¦πΊπ³ΰΈ");
+ [inputView deleteBackward];
+ XCTAssertEqualObjects(inputView.text, @"αΉπ text π₯°π¨π©π§π¦πΊπ³");
+ [inputView deleteBackward];
+ XCTAssertEqualObjects(inputView.text, @"αΉπ text π₯°π¨π©π§π¦");
+ [inputView deleteBackward];
+ XCTAssertEqualObjects(inputView.text, @"αΉπ text π₯°");
+ [inputView deleteBackward];
+
+ XCTAssertEqualObjects(inputView.text, @"αΉπ text ");
+ [inputView deleteBackward];
+ [inputView deleteBackward];
+ [inputView deleteBackward];
+ [inputView deleteBackward];
+ [inputView deleteBackward];
+ [inputView deleteBackward];
+
+ XCTAssertEqualObjects(inputView.text, @"αΉπ");
+ [inputView deleteBackward];
+ XCTAssertEqualObjects(inputView.text, @"αΉ");
+ [inputView deleteBackward];
+ XCTAssertEqualObjects(inputView.text, @"");
+}
+
- (void)testPastingNonTextDisallowed {
NSDictionary* config = self.mutableTemplateCopy;
[self setClientId:123 configuration:config];
diff --git a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/xcshareddata/xcschemes/IosUnitTests.xcscheme b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/xcshareddata/xcschemes/IosUnitTests.xcscheme
index 48aa290..b1341fc 100644
--- a/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/xcshareddata/xcschemes/IosUnitTests.xcscheme
+++ b/testing/ios/IosUnitTests/IosUnitTests.xcodeproj/xcshareddata/xcschemes/IosUnitTests.xcscheme
@@ -78,6 +78,12 @@
ReferencedContainer = "container:IosUnitTests.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
+ <CommandLineArguments>
+ <CommandLineArgument
+ argument = "--icu-data-file-path=Frameworks/Flutter.framework/icudtl.dat"
+ isEnabled = "YES">
+ </CommandLineArgument>
+ </CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"