From d899b03234bb6c503f68b8c913ad0ccee72a721e Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Wed, 2 Jun 2021 15:11:09 -0700 Subject: [PATCH 1/6] [iOSTextInput] fix potential dangling pointer access --- .../Source/FlutterTextInputPlugin.mm | 107 ++++++++++++------ .../Source/FlutterTextInputPluginTest.m | 22 +++- 2 files changed, 89 insertions(+), 40 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index 854a3ff7f4c1e..c6b576398c5fe 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -505,6 +505,9 @@ @implementation FlutterTextInputView { const char* _selectionAffinity; FlutterTextRange* _selectedTextRange; CGRect _cachedFirstRect; + // The view has reached end of life, and is no longer + // allowed to access its textInputDelegate. + BOOL _decommissioned; } @synthesize tokenizer = _tokenizer; @@ -535,6 +538,7 @@ - (instancetype)init { _returnKeyType = UIReturnKeyDone; _secureTextEntry = NO; _accessibilityEnabled = NO; + _decommissioned = NO; if (@available(iOS 11.0, *)) { _smartQuotesType = UITextSmartQuotesTypeYes; _smartDashesType = UITextSmartDashesTypeYes; @@ -545,6 +549,7 @@ - (instancetype)init { } - (void)configureWithDictionary:(NSDictionary*)configuration { + NSAssert(!_decommissioned, @"Attempt to reuse a decommissioned view, for %@", configuration); NSDictionary* inputType = configuration[kKeyboardType]; NSString* keyboardAppearance = configuration[kKeyboardAppearance]; NSDictionary* autofill = configuration[kAutofillProperties]; @@ -596,6 +601,23 @@ - (UITextContentType)textContentType { return _textContentType; } +- (id)textInputDelegate { + return _decommissioned ? nil : _textInputDelegate; +} + +// Declares that the view has reached end of life, and +// is no longer allowed to access its textInputDelegate. +// +// UIKit may retain this view (even after it's been removed +// from the view hierarchy) so that it may outlive the plugin/engine +// and _textInputDelegate will be a dangling pointer. + +// The text input plugin needs to call decommision to prevent that +// from happening. +- (void)decommision { + _decommissioned = YES; +} + - (void)dealloc { [_text release]; [_markedText release]; @@ -778,7 +800,8 @@ - (void)replaceRange:(UITextRange*)range withText:(NSString*)text { - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text { if (self.returnKeyType == UIReturnKeyDefault && [text isEqualToString:@"\n"]) { - [_textInputDelegate performAction:FlutterTextInputActionNewline withClient:_textInputClient]; + [self.textInputDelegate performAction:FlutterTextInputActionNewline + withClient:_textInputClient]; return YES; } @@ -819,7 +842,7 @@ - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)t break; } - [_textInputDelegate performAction:action withClient:_textInputClient]; + [self.textInputDelegate performAction:action withClient:_textInputClient]; return NO; } @@ -1062,9 +1085,9 @@ - (CGRect)firstRectForRange:(UITextRange*)range { return _cachedFirstRect; } - [_textInputDelegate showAutocorrectionPromptRectForStart:start - end:end - withClient:_textInputClient]; + [self.textInputDelegate showAutocorrectionPromptRectForStart:start + end:end + withClient:_textInputClient]; // TODO(cbracken) Implement. return CGRectZero; } @@ -1097,21 +1120,21 @@ - (UITextRange*)characterRangeAtPoint:(CGPoint)point { } - (void)beginFloatingCursorAtPoint:(CGPoint)point { - [_textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateStart - withClient:_textInputClient - withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}]; + [self.textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateStart + withClient:_textInputClient + withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}]; } - (void)updateFloatingCursorAtPoint:(CGPoint)point { - [_textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateUpdate - withClient:_textInputClient - withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}]; + [self.textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateUpdate + withClient:_textInputClient + withPosition:@{@"X" : @(point.x), @"Y" : @(point.y)}]; } - (void)endFloatingCursor { - [_textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateEnd - withClient:_textInputClient - withPosition:@{@"X" : @(0), @"Y" : @(0)}]; + [self.textInputDelegate updateFloatingCursor:FlutterFloatingCursorDragStateEnd + withClient:_textInputClient + withPosition:@{@"X" : @(0), @"Y" : @(0)}]; } #pragma mark - UIKeyInput Overrides @@ -1139,9 +1162,11 @@ - (void)updateEditingState { }; if (_textInputClient == 0 && _autofillId != nil) { - [_textInputDelegate updateEditingClient:_textInputClient withState:state withTag:_autofillId]; + [self.textInputDelegate updateEditingClient:_textInputClient + withState:state + withTag:_autofillId]; } else { - [_textInputDelegate updateEditingClient:_textInputClient withState:state]; + [self.textInputDelegate updateEditingClient:_textInputClient withState:state]; } } @@ -1259,8 +1284,6 @@ - (void)enableActiveViewAccessibility { @end @interface FlutterTextInputPlugin () -@property(nonatomic, strong) FlutterTextInputView* reusableInputView; - // The current password-autofillable input fields that have yet to be saved. @property(nonatomic, readonly) NSMutableDictionary* autofillContext; @@ -1278,11 +1301,11 @@ - (instancetype)init { self = [super init]; if (self) { - _reusableInputView = [[FlutterTextInputView alloc] init]; - _reusableInputView.secureTextEntry = NO; _autofillContext = [[NSMutableDictionary alloc] init]; - _activeView = [_reusableInputView retain]; _inputHider = [[FlutterTextInputViewAccessibilityHider alloc] init]; + // Initialize activeView with a dummy view to keep tests + // passing. + _activeView = [[FlutterTextInputView alloc] init]; } return self; @@ -1291,7 +1314,6 @@ - (instancetype)init { - (void)dealloc { [self hideTextInput]; _activeView.textInputDelegate = nil; - [_reusableInputView release]; [_activeView release]; [_inputHider release]; [_autofillContext release]; @@ -1398,14 +1420,14 @@ - (void)triggerAutofillSave:(BOOL)saveEntries { if (saveEntries) { // Make all the input fields in the autofill context visible, // then remove them to trigger autofill save. - [self cleanUpViewHierarchy:YES clearText:YES]; + [self cleanUpViewHierarchy:YES clearText:YES decommisionOnly:NO]; [_autofillContext removeAllObjects]; [self changeInputViewsAutofillVisibility:YES]; } else { [_autofillContext removeAllObjects]; } - [self cleanUpViewHierarchy:YES clearText:!saveEntries]; + [self cleanUpViewHierarchy:YES clearText:!saveEntries decommisionOnly:NO]; [self addToInputParentViewIfNeeded:_activeView]; } @@ -1414,9 +1436,11 @@ - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configur // Hide all input views from autofill, only make those in the new configuration visible // to autofill. [self changeInputViewsAutofillVisibility:NO]; + + // Update the current active view. switch (autofillTypeOf(configuration)) { case FlutterAutofillTypeNone: - self.activeView = [self updateAndShowReusableInputView:configuration]; + self.activeView = [self createInputViewWith:configuration]; break; case FlutterAutofillTypeRegular: // If the group does not involve password autofill, only install the @@ -1431,10 +1455,11 @@ - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configur isPasswordRelated:YES]; break; } - [_activeView setTextInputClient:client]; [_activeView reloadInputViews]; + // Decommission all views that will soon be removed. + [self cleanUpViewHierarchy:NO clearText:YES decommisionOnly:YES]; // Clean up views that no longer need to be in the view hierarchy, according to // the current autofill context. The "garbage" input views are already made // invisible to autofill and they can't `becomeFirstResponder`, we only remove @@ -1451,17 +1476,17 @@ - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configur // hints. This method re-configures and reuses an existing instance of input field // instead of creating a new one. // Also updates the current autofill context. -- (FlutterTextInputView*)updateAndShowReusableInputView:(NSDictionary*)configuration { +- (FlutterTextInputView*)createInputViewWith:(NSDictionary*)configuration { // It's possible that the configuration of this non-autofillable input view has // an autofill configuration without hints. If it does, remove it from the context. NSString* autofillId = autofillIdFromDictionary(configuration); if (autofillId) { [_autofillContext removeObjectForKey:autofillId]; } - - [_reusableInputView configureWithDictionary:configuration]; - [self addToInputParentViewIfNeeded:_reusableInputView]; - _reusableInputView.textInputDelegate = _textInputDelegate; + FlutterTextInputView* newView = [[FlutterTextInputView alloc] init]; + [newView configureWithDictionary:configuration]; + [self addToInputParentViewIfNeeded:newView]; + newView.textInputDelegate = _textInputDelegate; for (NSDictionary* field in configuration[kAssociatedAutofillFields]) { NSString* autofillId = autofillIdFromDictionary(field); @@ -1469,7 +1494,7 @@ - (FlutterTextInputView*)updateAndShowReusableInputView:(NSDictionary*)configura [_autofillContext removeObjectForKey:autofillId]; } } - return _reusableInputView; + return [newView autorelease]; } - (FlutterTextInputView*)updateAndShowAutofillViews:(NSArray*)fields @@ -1551,7 +1576,9 @@ - (UIView*)keyWindow { // context. May remove the active view too if includeActiveView is YES. // When clearText is YES, the text on the input fields will be set to empty before // they are removed from the view hierarchy, to avoid triggering autofill save. -- (void)cleanUpViewHierarchy:(BOOL)includeActiveView clearText:(BOOL)clearText { +- (void)cleanUpViewHierarchy:(BOOL)includeActiveView + clearText:(BOOL)clearText + decommisionOnly:(BOOL)decommisionOnly { for (UIView* view in self.textInputViews) { if ([view isKindOfClass:[FlutterTextInputView class]] && (includeActiveView || view != _activeView)) { @@ -1560,14 +1587,17 @@ - (void)cleanUpViewHierarchy:(BOOL)includeActiveView clearText:(BOOL)clearText { if (clearText) { [inputView replaceRangeLocal:NSMakeRange(0, inputView.text.length) withText:@""]; } - [view removeFromSuperview]; + [inputView decommision]; + if (!decommisionOnly) { + [inputView removeFromSuperview]; + } } } } } - (void)collectGarbageInputViews { - [self cleanUpViewHierarchy:NO clearText:YES]; + [self cleanUpViewHierarchy:NO clearText:YES decommisionOnly:NO]; } // Changes the visibility of every FlutterTextInputView currently in the @@ -1582,7 +1612,12 @@ - (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility { } // Resets the client id of every FlutterTextInputView in the view hierarchy -// to 0. Called when a new text input connection will be established. +// to 0. +// Called before establishing a new text input connection. +// For views in the current autofill context, they need to +// stay in the view hierachy but should not be allowed to +// send messages (other than autofill related ones) to the +// framework. - (void)resetAllClientIds { for (UIView* view in self.textInputViews) { if ([view isKindOfClass:[FlutterTextInputView class]]) { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m index 997daa2a0d48a..50b6d546c25fb 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m @@ -19,6 +19,7 @@ - (void)setEditableTransform:(NSArray*)matrix; - (void)setTextInputState:(NSDictionary*)state; - (void)setMarkedRect:(CGRect)markedRect; - (void)updateEditingState; +- (void)decommisson; - (BOOL)isVisibleToAutofill; @end @@ -252,6 +253,23 @@ - (void)testInputViewCrash { [activeView updateEditingState]; } +- (void)testNoDanglingEnginePointer { + NSDictionary* config = self.mutableTemplateCopy; + [self setClientId:123 configuration:config]; + + // We'll hold onto the current view and try to access the engine + // after changing the active view. + FlutterTextInputView* currentView = textInputPlugin.activeView; + [self setClientId:456 configuration:config]; + XCTAssertNotNil(currentView); + XCTAssertNotNil(textInputPlugin.activeView); + XCTAssertNotEqual(currentView, textInputPlugin.activeView); + + // Verify that the view can no longer access the engine + // instance. + XCTAssertNil(currentView.textInputDelegate); +} + - (void)ensureOnlyActiveViewCanBecomeFirstResponder { for (FlutterTextInputView* inputView in self.installedInputViews) { XCTAssertEqual(inputView.canBecomeFirstResponder, inputView == textInputPlugin.activeView); @@ -673,7 +691,6 @@ - (void)testCommitAutofillContext { [self ensureOnlyActiveViewCanBecomeFirstResponder]; [self commitAutofillContextAndVerify]; - XCTAssertNotEqual(textInputPlugin.textInputView, textInputPlugin.reusableInputView); [self ensureOnlyActiveViewCanBecomeFirstResponder]; // Install the password field again. @@ -689,14 +706,12 @@ - (void)testCommitAutofillContext { [self ensureOnlyActiveViewCanBecomeFirstResponder]; [self commitAutofillContextAndVerify]; - XCTAssertNotEqual(textInputPlugin.textInputView, textInputPlugin.reusableInputView); [self ensureOnlyActiveViewCanBecomeFirstResponder]; // Now switch to an input field that does not autofill. [self setClientId:125 configuration:self.mutableTemplateCopy]; XCTAssertEqual(self.viewsVisibleToAutofill.count, 0); - XCTAssertEqual(textInputPlugin.textInputView, textInputPlugin.reusableInputView); // The active view should still be installed so it doesn't get // deallocated. @@ -706,7 +721,6 @@ - (void)testCommitAutofillContext { [self ensureOnlyActiveViewCanBecomeFirstResponder]; [self commitAutofillContextAndVerify]; - XCTAssertEqual(textInputPlugin.textInputView, textInputPlugin.reusableInputView); [self ensureOnlyActiveViewCanBecomeFirstResponder]; } From 1a0e8aba16730a2bc49428c99b70c322c927d4d9 Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Wed, 2 Jun 2021 15:23:57 -0700 Subject: [PATCH 2/6] code comments --- .../framework/Source/FlutterTextInputPlugin.mm | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index c6b576398c5fe..b229d357f4fa1 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -609,11 +609,11 @@ - (UITextContentType)textContentType { // is no longer allowed to access its textInputDelegate. // // UIKit may retain this view (even after it's been removed -// from the view hierarchy) so that it may outlive the plugin/engine -// and _textInputDelegate will be a dangling pointer. +// from the view hierarchy) so that it may outlive the plugin/engine, +// in which case _textInputDelegate will become a dangling pointer. -// The text input plugin needs to call decommision to prevent that -// from happening. +// The text input plugin needs to call decommision when it should +// not have access to its FlutterTextInputDelegate any more. - (void)decommision { _decommissioned = YES; } @@ -1472,10 +1472,11 @@ - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configur [self performSelector:@selector(collectGarbageInputViews) withObject:nil afterDelay:0.1]; } -// Updates and shows an input field that is not password related and has no autofill -// hints. This method re-configures and reuses an existing instance of input field -// instead of creating a new one. -// Also updates the current autofill context. +// Creates and shows an input field that is not password related and has no autofill +// hints. This method returns a new FlutterTextInputView instance when called, since +// UIKit uses the identity of `UITextInput` instances (or the identity of the input +// views) to decide whether the IME's internal states should be reset. See: +// https://github.com/flutter/flutter/issues/79031 . - (FlutterTextInputView*)createInputViewWith:(NSDictionary*)configuration { // It's possible that the configuration of this non-autofillable input view has // an autofill configuration without hints. If it does, remove it from the context. From 69b5f4646a8f884ac75ccc3495654de6195c44bd Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Wed, 2 Jun 2021 15:25:33 -0700 Subject: [PATCH 3/6] [iOSTextInput] fix potential dangling pointer access --- .../ios/framework/Source/FlutterTextInputPluginTest.m | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m index 50b6d546c25fb..9e9407616ae17 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m @@ -253,6 +253,17 @@ - (void)testInputViewCrash { [activeView updateEditingState]; } +- (void)testDoNotReuseInputViews { + NSDictionary* config = self.mutableTemplateCopy; + [self setClientId:123 configuration:config]; + FlutterTextInputView* currentView = textInputPlugin.activeView; + [self setClientId:456 configuration:config]; + + XCTAssertNotNil(currentView); + XCTAssertNotNil(textInputPlugin.activeView); + XCTAssertNotEqual(currentView, textInputPlugin.activeView); +} + - (void)testNoDanglingEnginePointer { NSDictionary* config = self.mutableTemplateCopy; [self setClientId:123 configuration:config]; From 1633a8a16c036b97721ac05be1808cacf5531173 Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Wed, 2 Jun 2021 15:25:43 -0700 Subject: [PATCH 4/6] [iOSTextInput] fix potential dangling pointer access --- .../darwin/ios/framework/Source/FlutterTextInputPluginTest.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m index 9e9407616ae17..17fd7cb1c414d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m @@ -258,7 +258,7 @@ - (void)testDoNotReuseInputViews { [self setClientId:123 configuration:config]; FlutterTextInputView* currentView = textInputPlugin.activeView; [self setClientId:456 configuration:config]; - + XCTAssertNotNil(currentView); XCTAssertNotNil(textInputPlugin.activeView); XCTAssertNotEqual(currentView, textInputPlugin.activeView); From a5786e0eb579d77759e82e146f38405b162b553a Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Thu, 3 Jun 2021 17:16:37 -0700 Subject: [PATCH 5/6] review --- .../Source/FlutterTextInputPlugin.mm | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index b229d357f4fa1..ef95a76abad7d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -1420,14 +1420,14 @@ - (void)triggerAutofillSave:(BOOL)saveEntries { if (saveEntries) { // Make all the input fields in the autofill context visible, // then remove them to trigger autofill save. - [self cleanUpViewHierarchy:YES clearText:YES decommisionOnly:NO]; + [self cleanUpViewHierarchy:YES clearText:YES delayRemoval:NO]; [_autofillContext removeAllObjects]; [self changeInputViewsAutofillVisibility:YES]; } else { [_autofillContext removeAllObjects]; } - [self cleanUpViewHierarchy:YES clearText:!saveEntries decommisionOnly:NO]; + [self cleanUpViewHierarchy:YES clearText:!saveEntries delayRemoval:NO]; [self addToInputParentViewIfNeeded:_activeView]; } @@ -1458,18 +1458,17 @@ - (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configur [_activeView setTextInputClient:client]; [_activeView reloadInputViews]; - // Decommission all views that will soon be removed. - [self cleanUpViewHierarchy:NO clearText:YES decommisionOnly:YES]; // Clean up views that no longer need to be in the view hierarchy, according to // the current autofill context. The "garbage" input views are already made // invisible to autofill and they can't `becomeFirstResponder`, we only remove // them to free up resources and reduce the number of input views in the view // hierarchy. // - // This is scheduled on the runloop and delayed by 0.1s so we don't remove the + // The garbage views are decommissioned immediately, but the removeFromSuperview + // call is scheduled on the runloop and delayed by 0.1s so we don't remove the // text fields immediately (which seems to make the keyboard flicker). // See: https://github.com/flutter/flutter/issues/64628. - [self performSelector:@selector(collectGarbageInputViews) withObject:nil afterDelay:0.1]; + [self cleanUpViewHierarchy:NO clearText:YES delayRemoval:YES]; } // Creates and shows an input field that is not password related and has no autofill @@ -1573,13 +1572,21 @@ - (UIView*)keyWindow { return _inputHider.subviews; } -// Removes every installed input field, unless it's in the current autofill -// context. May remove the active view too if includeActiveView is YES. +// Decommisions (See the "decommision" method on FlutterTextInputView) and removes +// every installed input field, unless it's in the current autofill context. +// +// The active view will be decommisioned and removed from its superview too, if +// includeActiveView is YES. // When clearText is YES, the text on the input fields will be set to empty before // they are removed from the view hierarchy, to avoid triggering autofill save. +// If delayRemoval is true, removeFromSuperview will be scheduled on the runloop and +// will be delayed by 0.1s so we don't remove the text fields immediately (which seems +// to make the keyboard flicker). +// See: https://github.com/flutter/flutter/issues/64628. + - (void)cleanUpViewHierarchy:(BOOL)includeActiveView clearText:(BOOL)clearText - decommisionOnly:(BOOL)decommisionOnly { + delayRemoval:(BOOL)delayRemoval { for (UIView* view in self.textInputViews) { if ([view isKindOfClass:[FlutterTextInputView class]] && (includeActiveView || view != _activeView)) { @@ -1589,7 +1596,9 @@ - (void)cleanUpViewHierarchy:(BOOL)includeActiveView [inputView replaceRangeLocal:NSMakeRange(0, inputView.text.length) withText:@""]; } [inputView decommision]; - if (!decommisionOnly) { + if (delayRemoval) { + [inputView performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:0.1]; + } else { [inputView removeFromSuperview]; } } @@ -1597,10 +1606,6 @@ - (void)cleanUpViewHierarchy:(BOOL)includeActiveView } } -- (void)collectGarbageInputViews { - [self cleanUpViewHierarchy:NO clearText:YES decommisionOnly:NO]; -} - // Changes the visibility of every FlutterTextInputView currently in the // view hierarchy. - (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility { From 456c7e176a1191c554b0f5dab69b68cf6b64ca40 Mon Sep 17 00:00:00 2001 From: LongCat is Looong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Thu, 3 Jun 2021 17:49:14 -0700 Subject: [PATCH 6/6] fix tests --- .../Source/FlutterTextInputPluginTest.m | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m index 17fd7cb1c414d..5cd8defa9dd68 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m @@ -57,7 +57,9 @@ @interface FlutterTextInputPlugin () @property(nonatomic, readonly) NSMutableDictionary* autofillContext; -- (void)collectGarbageInputViews; +- (void)cleanUpViewHierarchy:(BOOL)includeActiveView + clearText:(BOOL)clearText + delayRemoval:(BOOL)delayRemoval; - (NSArray*)textInputViews; @end @@ -595,7 +597,7 @@ - (void)testAutofillContext { XCTAssertEqual(textInputPlugin.autofillContext.count, 2); - [textInputPlugin collectGarbageInputViews]; + [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO]; XCTAssertEqual(self.installedInputViews.count, 2); XCTAssertEqual(textInputPlugin.textInputView, textInputPlugin.autofillContext[@"field1"]); [self ensureOnlyActiveViewCanBecomeFirstResponder]; @@ -618,7 +620,7 @@ - (void)testAutofillContext { XCTAssertEqual(self.viewsVisibleToAutofill.count, 2); XCTAssertEqual(textInputPlugin.autofillContext.count, 3); - [textInputPlugin collectGarbageInputViews]; + [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO]; XCTAssertEqual(self.installedInputViews.count, 3); XCTAssertEqual(textInputPlugin.textInputView, textInputPlugin.autofillContext[@"field1"]); [self ensureOnlyActiveViewCanBecomeFirstResponder]; @@ -638,7 +640,7 @@ - (void)testAutofillContext { XCTAssertEqual(self.viewsVisibleToAutofill.count, 1); XCTAssertEqual(textInputPlugin.autofillContext.count, 3); - [textInputPlugin collectGarbageInputViews]; + [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO]; XCTAssertEqual(self.installedInputViews.count, 4); // Old autofill input fields are still installed and reused. @@ -657,7 +659,7 @@ - (void)testAutofillContext { XCTAssertEqual(self.viewsVisibleToAutofill.count, 1); XCTAssertEqual(textInputPlugin.autofillContext.count, 3); - [textInputPlugin collectGarbageInputViews]; + [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO]; XCTAssertEqual(self.installedInputViews.count, 4); // Old autofill input fields are still installed and reused. @@ -710,7 +712,7 @@ - (void)testCommitAutofillContext { [self setClientId:124 configuration:field3]; XCTAssertEqual(self.viewsVisibleToAutofill.count, 1); - [textInputPlugin collectGarbageInputViews]; + [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO]; XCTAssertEqual(self.installedInputViews.count, 3); XCTAssertEqual(textInputPlugin.autofillContext.count, 2); XCTAssertNotEqual(textInputPlugin.textInputView, nil); @@ -726,7 +728,7 @@ - (void)testCommitAutofillContext { // The active view should still be installed so it doesn't get // deallocated. - [textInputPlugin collectGarbageInputViews]; + [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO]; XCTAssertEqual(self.installedInputViews.count, 1); XCTAssertEqual(textInputPlugin.autofillContext.count, 0); [self ensureOnlyActiveViewCanBecomeFirstResponder]; @@ -823,7 +825,7 @@ - (void)testClearAutofillContextClearsSelection { XCTAssertEqual(self.installedInputViews.count, 2); - [textInputPlugin collectGarbageInputViews]; + [textInputPlugin cleanUpViewHierarchy:NO clearText:YES delayRemoval:NO]; XCTAssertEqual(self.installedInputViews.count, 1); // Verify the old input view is properly cleaned up.