Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CP] Workaround iOS text input crash for emoji+Korean text #36807

Merged
merged 2 commits into from
Oct 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -71,6 +73,19 @@

#pragma mark - Static Functions

// Determine if the character at `range` of `text` is an emoji.
static BOOL IsEmoji(NSString* text, NSRange charRange) {
UChar32 codePoint;
BOOL gotCodePoint = [text getBytes:&codePoint
maxLength:sizeof(codePoint)
usedLength:NULL
encoding:NSUTF32StringEncoding
options:kNilOptions
range:charRange
remainingRange:NULL];
return gotCodePoint && u_hasBinaryProperty(codePoint, UCHAR_EMOJI);
}

// "TextInputType.none" is a made-up input type that's typically
// used when there's an in-app virtual keyboard. If
// "TextInputType.none" is specified, disable the system
Expand Down Expand Up @@ -713,6 +728,10 @@ @interface FlutterTextInputView ()
@property(nonatomic, assign) CGRect markedRect;
@property(nonatomic) BOOL isVisibleToAutofill;
@property(nonatomic, assign) BOOL accessibilityEnabled;
// The composed character that is temporarily removed by the keyboard API.
// This is cleared at the start of each keyboard interaction. (Enter a character, delete a character
// etc)
@property(nonatomic, copy) NSString* temporarilyDeletedComposedCharacter;

- (void)setEditableTransform:(NSArray*)matrix;
@end
Expand Down Expand Up @@ -899,6 +918,8 @@ - (void)dealloc {
[_markedTextStyle release];
[_textContentType release];
[_textInteraction release];
[_temporarilyDeletedComposedCharacter release];
_temporarilyDeletedComposedCharacter = nil;
[super dealloc];
}

Expand Down Expand Up @@ -1243,6 +1264,10 @@ - (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
}

- (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text {
// `temporarilyDeletedComposedCharacter` should only be used during a single text change session.
// So it needs to be cleared at the start of each text editting session.
self.temporarilyDeletedComposedCharacter = nil;

if (self.returnKeyType == UIReturnKeyDefault && [text isEqualToString:@"\n"]) {
[self.textInputDelegate flutterTextInputView:self
performAction:FlutterTextInputActionNewline
Expand Down Expand Up @@ -1830,6 +1855,15 @@ - (BOOL)hasText {
}

- (void)insertText:(NSString*)text {
if (self.temporarilyDeletedComposedCharacter.length > 0 && text.length == 1 && !text.UTF8String &&
[text characterAtIndex:0] == [self.temporarilyDeletedComposedCharacter characterAtIndex:0]) {
// Workaround for https://github.com/flutter/flutter/issues/111494
// TODO(cyanglaz): revert this workaround if when flutter supports a minimum iOS version which
// this bug is fixed by Apple.
text = self.temporarilyDeletedComposedCharacter;
self.temporarilyDeletedComposedCharacter = nil;
}

NSMutableArray<FlutterTextSelectionRect*>* copiedRects =
[[NSMutableArray alloc] initWithCapacity:[_selectionRects count]];
NSAssert([_selectedTextRange.start isKindOfClass:[FlutterTextPosition class]],
Expand Down Expand Up @@ -1896,12 +1930,29 @@ - (void)deleteBackward {
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);
if (IsEmoji(self.text, charRange)) {
newRange = NSMakeRange(charRange.location, oldRange.location - charRange.location);
}

_selectedTextRange = [[FlutterTextRange rangeWithNSRange:newRange] copy];
[oldSelectedRange release];
}
}

if (!_selectedTextRange.isEmpty) {
// Cache the last deleted emoji to use for an iOS bug where the next
// insertion corrupts the emoji characters.
// See: https://github.com/flutter/flutter/issues/111494#issuecomment-1248441346
if (IsEmoji(self.text, _selectedTextRange.range)) {
NSString* deletedText = [self.text substringWithRange:_selectedTextRange.range];
NSRange deleteFirstCharacterRange = fml::RangeForCharacterAtIndex(deletedText, 0);
self.temporarilyDeletedComposedCharacter =
[deletedText substringWithRange:deleteFirstCharacterRange];
}
[self replaceRange:_selectedTextRange withText:@""];
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,100 @@ - (void)testStandardEditActions {
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, @"");
}

// This tests the workaround to fix an iOS 16 bug
// See: https://github.com/flutter/flutter/issues/111494
- (void)testSystemOnlyAddingPartialComposedCharacter {
NSDictionary* config = self.mutableTemplateCopy;
[self setClientId:123 configuration:config];
NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
FlutterTextInputView* inputView = inputFields[0];

[inputView insertText:@"👨‍👩‍👧‍👦"];
[inputView deleteBackward];

// Insert the first unichar in the emoji.
[inputView insertText:[@"👨‍👩‍👧‍👦" substringWithRange:NSMakeRange(0, 1)]];
[inputView insertText:@"아"];

XCTAssertEqualObjects(inputView.text, @"👨‍👩‍👧‍👦아");

// Deleting 아.
[inputView deleteBackward];
// 👨‍👩‍👧‍👦 should be the current string.

[inputView insertText:@"😀"];
[inputView deleteBackward];
// Insert the first unichar in the emoji.
[inputView insertText:[@"😀" substringWithRange:NSMakeRange(0, 1)]];
[inputView insertText:@"아"];
XCTAssertEqualObjects(inputView.text, @"👨‍👩‍👧‍👦😀아");

// Deleting 아.
[inputView deleteBackward];
// 👨‍👩‍👧‍👦😀 should be the current string.

[inputView deleteBackward];
// Insert the first unichar in the emoji.
[inputView insertText:[@"😀" substringWithRange:NSMakeRange(0, 1)]];
[inputView insertText:@"아"];

XCTAssertEqualObjects(inputView.text, @"👨‍👩‍👧‍👦😀아");
}

- (void)testCachedComposedCharacterClearedAtKeyboardInteraction {
NSDictionary* config = self.mutableTemplateCopy;
[self setClientId:123 configuration:config];
NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
FlutterTextInputView* inputView = inputFields[0];

[inputView insertText:@"👨‍👩‍👧‍👦"];
[inputView deleteBackward];
[inputView shouldChangeTextInRange:OCMClassMock([UITextRange class]) replacementText:@""];

// Insert the first unichar in the emoji.
NSString* brokenEmoji = [@"👨‍👩‍👧‍👦" substringWithRange:NSMakeRange(0, 1)];
[inputView insertText:brokenEmoji];
[inputView insertText:@"아"];

NSString* finalText = [NSString stringWithFormat:@"%@아", brokenEmoji];
XCTAssertEqualObjects(inputView.text, finalText);
}

- (void)testPastingNonTextDisallowed {
NSDictionary* config = self.mutableTemplateCopy;
[self setClientId:123 configuration:config];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down