Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
sxychenjing
engine
提交
d96371e0
E
engine
项目概览
sxychenjing
/
engine
与 Fork 源项目一致
从无法访问的项目Fork
通知
3
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
E
engine
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
未验证
提交
d96371e0
编写于
7月 28, 2020
作者:
L
LongCatIsLooong
提交者:
GitHub
7月 28, 2020
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Add autofill save for iOS and Android (#18643)
上级
db8c40b3
变更
5
隐藏空白更改
内联
并排
Showing
5 changed file
with
718 addition
and
144 deletion
+718
-144
shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java
...ter/embedding/engine/systemchannels/TextInputChannel.java
+16
-0
shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
...rm/android/io/flutter/plugin/editing/TextInputPlugin.java
+12
-0
shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java
...d/test/io/flutter/plugin/editing/TextInputPluginTest.java
+35
-0
shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
...orm/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
+390
-101
shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m
.../darwin/ios/framework/Source/FlutterTextInputPluginTest.m
+265
-43
未找到文件。
shell/platform/android/io/flutter/embedding/engine/systemchannels/TextInputChannel.java
浏览文件 @
d96371e0
...
...
@@ -112,6 +112,10 @@ public class TextInputChannel {
textInputMethodHandler
.
clearClient
();
result
.
success
(
null
);
break
;
case
"TextInput.finishAutofillContext"
:
textInputMethodHandler
.
finishAutofillContext
((
boolean
)
args
);
result
.
success
(
null
);
break
;
default
:
result
.
notImplemented
();
break
;
...
...
@@ -284,6 +288,18 @@ public class TextInputChannel {
*/
void
requestAutofill
();
/**
* Requests that the {@link AutofillManager} cancel or commit the current autofill context.
*
* <p>The method calls {@link android.view.autofill.AutofillManager#commit()} when {@code
* shouldSave} is true, and calls {@link android.view.autofill.AutofillManager#cancel()}
* otherwise.
*
* @param shouldSave whether the active autofill service should save the current user input for
* future use.
*/
void
finishAutofillContext
(
boolean
shouldSave
);
// TODO(mattcarroll): javadoc
void
setClient
(
int
textInputClientId
,
@NonNull
Configuration
configuration
);
...
...
shell/platform/android/io/flutter/plugin/editing/TextInputPlugin.java
浏览文件 @
d96371e0
...
...
@@ -82,6 +82,18 @@ public class TextInputPlugin {
notifyViewEntered
();
}
@Override
public
void
finishAutofillContext
(
boolean
shouldSave
)
{
if
(
Build
.
VERSION
.
SDK_INT
<
Build
.
VERSION_CODES
.
O
||
afm
==
null
)
{
return
;
}
if
(
shouldSave
)
{
afm
.
commit
();
}
else
{
afm
.
cancel
();
}
}
@Override
public
void
setClient
(
int
textInputClientId
,
TextInputChannel
.
Configuration
configuration
)
{
...
...
shell/platform/android/test/io/flutter/plugin/editing/TextInputPluginTest.java
浏览文件 @
d96371e0
...
...
@@ -70,6 +70,14 @@ public class TextInputPluginTest {
}
}
private
static
void
sendToBinaryMessageHandler
(
BinaryMessenger
.
BinaryMessageHandler
binaryMessageHandler
,
String
method
,
Object
args
)
{
MethodCall
methodCall
=
new
MethodCall
(
method
,
args
);
ByteBuffer
encodedMethodCall
=
JSONMethodCodec
.
INSTANCE
.
encodeMethodCall
(
methodCall
);
binaryMessageHandler
.
onMessage
(
(
ByteBuffer
)
encodedMethodCall
.
flip
(),
mock
(
BinaryMessenger
.
BinaryReply
.
class
));
}
@Test
public
void
textInputPlugin_RequestsReattachOnCreation
()
throws
JSONException
{
// Initialize a general TextInputPlugin.
...
...
@@ -531,6 +539,33 @@ public class TextInputPluginTest {
verify
(
children
[
0
]).
setDimens
(
anyInt
(),
anyInt
(),
anyInt
(),
anyInt
(),
geq
(
0
),
geq
(
0
));
}
@Test
public
void
respondsToInputChannelMessages
()
{
ArgumentCaptor
<
BinaryMessenger
.
BinaryMessageHandler
>
binaryMessageHandlerCaptor
=
ArgumentCaptor
.
forClass
(
BinaryMessenger
.
BinaryMessageHandler
.
class
);
DartExecutor
mockBinaryMessenger
=
mock
(
DartExecutor
.
class
);
TextInputChannel
.
TextInputMethodHandler
mockHandler
=
mock
(
TextInputChannel
.
TextInputMethodHandler
.
class
);
TextInputChannel
textInputChannel
=
new
TextInputChannel
(
mockBinaryMessenger
);
textInputChannel
.
setTextInputMethodHandler
(
mockHandler
);
verify
(
mockBinaryMessenger
,
times
(
1
))
.
setMessageHandler
(
any
(
String
.
class
),
binaryMessageHandlerCaptor
.
capture
());
BinaryMessenger
.
BinaryMessageHandler
binaryMessageHandler
=
binaryMessageHandlerCaptor
.
getValue
();
sendToBinaryMessageHandler
(
binaryMessageHandler
,
"TextInput.requestAutofill"
,
null
);
verify
(
mockHandler
,
times
(
1
)).
requestAutofill
();
sendToBinaryMessageHandler
(
binaryMessageHandler
,
"TextInput.finishAutofillContext"
,
true
);
verify
(
mockHandler
,
times
(
1
)).
finishAutofillContext
(
true
);
sendToBinaryMessageHandler
(
binaryMessageHandler
,
"TextInput.finishAutofillContext"
,
false
);
verify
(
mockHandler
,
times
(
1
)).
finishAutofillContext
(
false
);
}
@Implements
(
InputMethodManager
.
class
)
public
static
class
TestImm
extends
ShadowInputMethodManager
{
private
InputMethodSubtype
currentInputMethodSubtype
;
...
...
shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
浏览文件 @
d96371e0
...
...
@@ -11,6 +11,26 @@
static
const
char
_kTextAffinityDownstream
[]
=
"TextAffinity.downstream"
;
static
const
char
_kTextAffinityUpstream
[]
=
"TextAffinity.upstream"
;
#pragma mark - TextInputConfiguration Field Names
static
NSString
*
const
kSecureTextEntry
=
@"obscureText"
;
static
NSString
*
const
kKeyboardType
=
@"inputType"
;
static
NSString
*
const
kKeyboardAppearance
=
@"keyboardAppearance"
;
static
NSString
*
const
kInputAction
=
@"inputAction"
;
static
NSString
*
const
kSmartDashesType
=
@"smartDashesType"
;
static
NSString
*
const
kSmartQuotesType
=
@"smartQuotesType"
;
static
NSString
*
const
kAssociatedAutofillFields
=
@"fields"
;
// TextInputConfiguration.autofill and sub-field names
static
NSString
*
const
kAutofillProperties
=
@"autofill"
;
static
NSString
*
const
kAutofillId
=
@"uniqueIdentifier"
;
static
NSString
*
const
kAutofillEditingValue
=
@"editingValue"
;
static
NSString
*
const
kAutofillHints
=
@"hints"
;
static
NSString
*
const
kAutocorrectionType
=
@"autocorrect"
;
#pragma mark - Static Functions
static
UIKeyboardType
ToUIKeyboardType
(
NSDictionary
*
type
)
{
NSString
*
inputType
=
type
[
@"name"
];
if
([
inputType
isEqualToString
:
@"TextInputType.address"
])
...
...
@@ -209,9 +229,86 @@ static UITextContentType ToUITextContentType(NSArray<NSString*>* hints) {
return
hints
[
0
];
}
static
NSString
*
uniqueIdFromDictionary
(
NSDictionary
*
dictionary
)
{
NSDictionary
*
autofill
=
dictionary
[
@"autofill"
];
return
autofill
==
nil
?
nil
:
autofill
[
@"uniqueIdentifier"
];
// Retrieves the autofillId from an input field's configuration. Returns
// nil if the field is nil and the input field is not a password field.
static
NSString
*
autofillIdFromDictionary
(
NSDictionary
*
dictionary
)
{
NSDictionary
*
autofill
=
dictionary
[
kAutofillProperties
];
if
(
autofill
)
{
return
autofill
[
kAutofillId
];
}
// When autofill is nil, the field may still need an autofill id
// if the field is for password.
return
[
dictionary
[
kSecureTextEntry
]
boolValue
]
?
@"password"
:
nil
;
}
// There're 2 types of autofills on native iOS:
// - Regular autofill, includes contact information autofill and
// one-time-code autofill, takes place in the form of predictive
// text in the quick type bar. This type of autofill does not save
// user input.
// - Password autofill, includes automatic strong password and regular
// password autofill. The former happens automatically when a
// "new password" field is detected, and only that password field
// will be populated. The latter appears in the quick type bar when
// an eligible input field becomes the first responder, and may
// fill both the username and the password fields. iOS will attempt
// to save user input for both kinds of password fields.
typedef
NS_ENUM
(
NSInteger
,
FlutterAutofillType
)
{
// The field does not have autofillable content. Additionally if
// the field is currently in the autofill context, it will be
// removed from the context without triggering autofill save.
FlutterAutofillTypeNone
,
FlutterAutofillTypeRegular
,
FlutterAutofillTypePassword
,
};
static
BOOL
isFieldPasswordRelated
(
NSDictionary
*
configuration
)
{
if
(
@available
(
iOS
10.0
,
*
))
{
BOOL
isSecureTextEntry
=
[
configuration
[
kSecureTextEntry
]
boolValue
];
if
(
isSecureTextEntry
)
return
YES
;
if
(
!
autofillIdFromDictionary
(
configuration
))
{
return
NO
;
}
NSDictionary
*
autofill
=
configuration
[
kAutofillProperties
];
UITextContentType
contentType
=
ToUITextContentType
(
autofill
[
kAutofillHints
]);
if
(
@available
(
iOS
11.0
,
*
))
{
if
([
contentType
isEqualToString
:
UITextContentTypePassword
]
||
[
contentType
isEqualToString
:
UITextContentTypeUsername
])
{
return
YES
;
}
}
if
(
@available
(
iOS
12.0
,
*
))
{
if
([
contentType
isEqualToString
:
UITextContentTypeNewPassword
])
{
return
YES
;
}
}
}
return
NO
;
}
static
FlutterAutofillType
autofillTypeOf
(
NSDictionary
*
configuration
)
{
for
(
NSDictionary
*
field
in
configuration
[
kAssociatedAutofillFields
])
{
if
(
isFieldPasswordRelated
(
field
))
{
return
FlutterAutofillTypePassword
;
}
}
if
(
isFieldPasswordRelated
(
configuration
))
{
return
FlutterAutofillTypePassword
;
}
if
(
@available
(
iOS
10.0
,
*
))
{
NSDictionary
*
autofill
=
configuration
[
kAutofillProperties
];
UITextContentType
contentType
=
ToUITextContentType
(
autofill
[
kAutofillHints
]);
return
[
contentType
isEqualToString
:
@""
]
?
FlutterAutofillTypeNone
:
FlutterAutofillTypeRegular
;
}
return
FlutterAutofillTypeNone
;
}
#pragma mark - FlutterTextPosition
...
...
@@ -269,8 +366,54 @@ static NSString* uniqueIdFromDictionary(NSDictionary* dictionary) {
}
@end
// A FlutterTextInputView that masquerades as a UITextField, and forwards
// selectors it can't respond to to a shared UITextField instance.
//
// Relevant API docs claim that password autofill supports any custom view
// that adopts the UITextInput protocol, automatic strong password seems to
// currently only support UITextFields, and password saving only supports
// UITextFields and UITextViews, as of iOS 13.5.
@interface
FlutterSecureTextInputView
:
FlutterTextInputView
@property
(
nonatomic
,
strong
,
readonly
)
UITextField
*
textField
;
@end
@implementation
FlutterSecureTextInputView
{
UITextField
*
_textField
;
}
-
(
void
)
dealloc
{
[
_textField
release
];
[
super
dealloc
];
}
-
(
UITextField
*
)
textField
{
if
(
_textField
==
nil
)
{
_textField
=
[[[
UITextField
alloc
]
init
]
autorelease
];
}
return
_textField
;
}
-
(
BOOL
)
isKindOfClass
:(
Class
)
aClass
{
return
[
super
isKindOfClass
:
aClass
]
||
(
aClass
==
[
UITextField
class
]);
}
-
(
NSMethodSignature
*
)
methodSignatureForSelector
:(
SEL
)
aSelector
{
NSMethodSignature
*
signature
=
[
super
methodSignatureForSelector
:
aSelector
];
if
(
!
signature
)
{
signature
=
[
self
.
textField
methodSignatureForSelector
:
aSelector
];
}
return
signature
;
}
-
(
void
)
forwardInvocation
:(
NSInvocation
*
)
anInvocation
{
[
anInvocation
invokeWithTarget
:
self
.
textField
];
}
@end
@interface
FlutterTextInputView
()
@property
(
nonatomic
,
copy
)
NSString
*
autofillId
;
@property
(
nonatomic
)
BOOL
isVisibleToAutofill
;
@end
@implementation
FlutterTextInputView
{
...
...
@@ -311,6 +454,59 @@ static NSString* uniqueIdFromDictionary(NSDictionary* dictionary) {
return
self
;
}
-
(
void
)
configureWithDictionary
:(
NSDictionary
*
)
configuration
{
NSDictionary
*
inputType
=
configuration
[
kKeyboardType
];
NSString
*
keyboardAppearance
=
configuration
[
kKeyboardAppearance
];
NSDictionary
*
autofill
=
configuration
[
kAutofillProperties
];
self
.
secureTextEntry
=
[
configuration
[
kSecureTextEntry
]
boolValue
];
self
.
keyboardType
=
ToUIKeyboardType
(
inputType
);
self
.
keyboardType
=
UIKeyboardTypeNamePhonePad
;
self
.
returnKeyType
=
ToUIReturnKeyType
(
configuration
[
kInputAction
]);
self
.
autocapitalizationType
=
ToUITextAutoCapitalizationType
(
configuration
);
if
(
@available
(
iOS
11.0
,
*
))
{
NSString
*
smartDashesType
=
configuration
[
kSmartDashesType
];
// This index comes from the SmartDashesType enum in the framework.
bool
smartDashesIsDisabled
=
smartDashesType
&&
[
smartDashesType
isEqualToString
:
@"0"
];
self
.
smartDashesType
=
smartDashesIsDisabled
?
UITextSmartDashesTypeNo
:
UITextSmartDashesTypeYes
;
NSString
*
smartQuotesType
=
configuration
[
kSmartQuotesType
];
// This index comes from the SmartQuotesType enum in the framework.
bool
smartQuotesIsDisabled
=
smartQuotesType
&&
[
smartQuotesType
isEqualToString
:
@"0"
];
self
.
smartQuotesType
=
smartQuotesIsDisabled
?
UITextSmartQuotesTypeNo
:
UITextSmartQuotesTypeYes
;
}
if
([
keyboardAppearance
isEqualToString
:
@"Brightness.dark"
])
{
self
.
keyboardAppearance
=
UIKeyboardAppearanceDark
;
}
else
if
([
keyboardAppearance
isEqualToString
:
@"Brightness.light"
])
{
self
.
keyboardAppearance
=
UIKeyboardAppearanceLight
;
}
else
{
self
.
keyboardAppearance
=
UIKeyboardAppearanceDefault
;
}
NSString
*
autocorrect
=
configuration
[
kAutocorrectionType
];
self
.
autocorrectionType
=
autocorrect
&&
!
[
autocorrect
boolValue
]
?
UITextAutocorrectionTypeNo
:
UITextAutocorrectionTypeDefault
;
if
(
@available
(
iOS
10.0
,
*
))
{
self
.
autofillId
=
autofillIdFromDictionary
(
configuration
);
if
(
autofill
==
nil
)
{
self
.
textContentType
=
@""
;
}
else
{
self
.
textContentType
=
ToUITextContentType
(
autofill
[
kAutofillHints
]);
[
self
setTextInputState
:
autofill
[
kAutofillEditingValue
]];
NSAssert
(
_autofillId
,
@"The autofill configuration must contain an autofill id"
);
}
// The input field needs to be visible for the system autofill
// to find it.
self
.
isVisibleToAutofill
=
autofill
||
_secureTextEntry
;
}
}
-
(
UITextContentType
)
textContentType
{
return
_textContentType
;
}
-
(
void
)
dealloc
{
[
_text
release
];
[
_markedText
release
];
...
...
@@ -392,12 +588,26 @@ static NSString* uniqueIdFromDictionary(NSDictionary* dictionary) {
return
NSMakeRange
(
start
,
length
);
}
-
(
BOOL
)
isVisibleToAutofill
{
return
self
.
frame
.
size
.
width
>
0
&&
self
.
frame
.
size
.
height
>
0
;
}
// An input view is generally ignored by password autofill attempts, if it's
// not the first responder and is zero-sized. For input fields that are in the
// autofill context but do not belong to the current autofill group, setting
// their frames to CGRectZero prevents ios autofill from taking them into
// account.
-
(
void
)
setIsVisibleToAutofill
:(
BOOL
)
isVisibleToAutofill
{
self
.
frame
=
isVisibleToAutofill
?
CGRectMake
(
0
,
0
,
1
,
1
)
:
CGRectZero
;
}
#pragma mark - UIResponder Overrides
-
(
BOOL
)
canBecomeFirstResponder
{
// Only the currently focused input field can
// become the first responder. This prevents iOS
// from changing focus by itself.
// from changing focus by itself (the framework
// focus will be out of sync if that happens).
return
_textInputClient
!=
0
;
}
...
...
@@ -701,7 +911,7 @@ static NSString* uniqueIdFromDictionary(NSDictionary* dictionary) {
// physical keyboard.
-
(
CGRect
)
firstRectForRange
:(
UITextRange
*
)
range
{
// multi-stage text is handled
somewhere else
.
// multi-stage text is handled
in the framework
.
if
(
_markedTextRange
!=
nil
)
{
return
CGRectZero
;
}
...
...
@@ -845,9 +1055,11 @@ static NSString* uniqueIdFromDictionary(NSDictionary* dictionary) {
@end
@interface
FlutterTextInputPlugin
()
@property
(
nonatomic
,
retain
)
FlutterTextInputView
*
nonAutofillInputView
;
@property
(
nonatomic
,
retain
)
FlutterTextInputView
*
nonAutofillSecureInputView
;
@property
(
nonatomic
,
retain
)
NSMutableArray
<
FlutterTextInputView
*>*
inputViews
;
@property
(
nonatomic
,
strong
)
FlutterTextInputView
*
reusableInputView
;
// The current password-autofillable input fields that have yet to be saved.
@property
(
nonatomic
,
readonly
)
NSMutableDictionary
<
NSString
*
,
FlutterTextInputView
*>*
autofillContext
;
@property
(
nonatomic
,
assign
)
FlutterTextInputView
*
activeView
;
@end
...
...
@@ -859,13 +1071,10 @@ static NSString* uniqueIdFromDictionary(NSDictionary* dictionary) {
self
=
[
super
init
];
if
(
self
)
{
_nonAutofillInputView
=
[[
FlutterTextInputView
alloc
]
init
];
_nonAutofillInputView
.
secureTextEntry
=
NO
;
_nonAutofillSecureInputView
=
[[
FlutterTextInputView
alloc
]
init
];
_nonAutofillSecureInputView
.
secureTextEntry
=
YES
;
_inputViews
=
[[
NSMutableArray
alloc
]
init
];
_activeView
=
_nonAutofillInputView
;
_reusableInputView
=
[[
FlutterTextInputView
alloc
]
init
];
_reusableInputView
.
secureTextEntry
=
NO
;
_autofillContext
=
[[
NSMutableDictionary
alloc
]
init
];
_activeView
=
_reusableInputView
;
}
return
self
;
...
...
@@ -873,9 +1082,8 @@ static NSString* uniqueIdFromDictionary(NSDictionary* dictionary) {
-
(
void
)
dealloc
{
[
self
hideTextInput
];
[
_nonAutofillInputView
release
];
[
_nonAutofillSecureInputView
release
];
[
_inputViews
release
];
[
_reusableInputView
release
];
[
_autofillContext
release
];
[
super
dealloc
];
}
...
...
@@ -902,21 +1110,17 @@ static NSString* uniqueIdFromDictionary(NSDictionary* dictionary) {
}
else
if
([
method
isEqualToString
:
@"TextInput.clearClient"
])
{
[
self
clearTextInputClient
];
result
(
nil
);
}
else
if
([
method
isEqualToString
:
@"TextInput.finishAutofillContext"
])
{
[
self
triggerAutofillSave
:[
args
boolValue
]];
result
(
nil
);
}
else
{
result
(
FlutterMethodNotImplemented
);
}
}
-
(
void
)
showTextInput
{
UIWindow
*
keyWindow
=
[
UIApplication
sharedApplication
].
keyWindow
;
NSAssert
(
keyWindow
!=
nullptr
,
@"The application must have a key window since the keyboard client "
@"must be part of the responder chain to function"
);
_activeView
.
textInputDelegate
=
_textInputDelegate
;
if
(
_activeView
.
window
!=
keyWindow
)
{
[
keyWindow
addSubview
:
_activeView
];
}
[
self
addToKeyWindowIfNeeded
:
_activeView
];
[
_activeView
becomeFirstResponder
];
}
...
...
@@ -924,102 +1128,187 @@ static NSString* uniqueIdFromDictionary(NSDictionary* dictionary) {
[
_activeView
resignFirstResponder
];
}
-
(
void
)
triggerAutofillSave
:(
BOOL
)
saveEntries
{
[
self
hideTextInput
];
if
(
saveEntries
)
{
// Make all the input fields in the autofill context visible,
// then remove them to trigger autofill save.
[
self
cleanUpViewHierarchy
:
YES
clearText
:
YES
];
[
_autofillContext
removeAllObjects
];
[
self
changeInputViewsAutofillVisibility
:
YES
];
}
else
{
[
_autofillContext
removeAllObjects
];
}
[
self
cleanUpViewHierarchy
:
YES
clearText
:
!
saveEntries
];
[
self
addToKeyWindowIfNeeded
:
_activeView
];
}
-
(
void
)
setTextInputClient
:(
int
)
client
withConfiguration
:(
NSDictionary
*
)
configuration
{
UIWindow
*
keyWindow
=
[
UIApplication
sharedApplication
].
keyWindow
;
NSArray
*
fields
=
configuration
[
@"fields"
];
NSString
*
clientUniqueId
=
uniqueIdFromDictionary
(
configuration
);
bool
isSecureTextEntry
=
[
configuration
[
@"obscureText"
]
boolValue
];
// Hide all input views from autofill, only make those in the new configuration visible
// to autofill.
[
self
changeInputViewsAutofillVisibility
:
NO
];
switch
(
autofillTypeOf
(
configuration
))
{
case
FlutterAutofillTypeNone
:
_activeView
=
[
self
updateAndShowReusableInputView
:
configuration
];
break
;
case
FlutterAutofillTypeRegular
:
// If the group does not involve password autofill, only install the
// input view that's being focused.
_activeView
=
[
self
updateAndShowAutofillViews
:
nil
focusedField:
configuration
isPasswordRelated:
NO
];
break
;
case
FlutterAutofillTypePassword
:
_activeView
=
[
self
updateAndShowAutofillViews
:
configuration
[
kAssociatedAutofillFields
]
focusedField:
configuration
isPasswordRelated:
YES
];
break
;
}
if
(
fields
==
nil
)
{
_activeView
=
isSecureTextEntry
?
_nonAutofillSecureInputView
:
_nonAutofillInputView
;
[
FlutterTextInputPlugin
setupInputView
:
_activeView
withConfiguration
:
configuration
];
// Clean up views that should no longer be in the view hierarchy according to the
// updated autofill context.
[
self
cleanUpViewHierarchy
:
NO
clearText
:
YES
];
[
_activeView
setTextInputClient
:
client
];
[
_activeView
reloadInputViews
];
}
if
(
_activeView
.
window
!=
keyWindow
)
{
[
keyWindow
addSubview
:
_activeView
];
}
}
else
{
NSAssert
(
clientUniqueId
!=
nil
,
@"The client's unique id can't be null"
);
for
(
FlutterTextInputView
*
view
in
_inputViews
)
{
[
view
removeFromSuperview
];
}
// 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.
-
(
FlutterTextInputView
*
)
updateAndShowReusableInputView
:(
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
];
}
for
(
UIView
*
view
in
keyWindow
.
subviews
)
{
if
([
view
isKindOfClass
:[
FlutterTextInputView
class
]])
{
[
view
removeFromSuperview
];
}
[
_reusableInputView
configureWithDictionary
:
configuration
];
[
self
addToKeyWindowIfNeeded
:
_reusableInputView
];
_reusableInputView
.
textInputDelegate
=
_textInputDelegate
;
for
(
NSDictionary
*
field
in
configuration
[
kAssociatedAutofillFields
])
{
NSString
*
autofillId
=
autofillIdFromDictionary
(
field
);
if
(
autofillId
&&
autofillTypeOf
(
field
)
==
FlutterAutofillTypeNone
)
{
[
_autofillContext
removeObjectForKey
:
autofillId
];
}
}
return
_reusableInputView
;
}
[
_inputViews
removeAllObjects
];
-
(
FlutterTextInputView
*
)
updateAndShowAutofillViews
:(
NSArray
*
)
fields
focusedField
:(
NSDictionary
*
)
focusedField
isPasswordRelated
:(
BOOL
)
isPassword
{
FlutterTextInputView
*
focused
=
nil
;
NSString
*
focusedId
=
autofillIdFromDictionary
(
focusedField
);
NSAssert
(
focusedId
,
@"autofillId must not be null for the focused field: %@"
,
focusedField
);
for
(
NSDictionary
*
field
in
fields
)
{
FlutterTextInputView
*
newInputView
=
[[[
FlutterTextInputView
alloc
]
init
]
autorelease
];
newInputView
.
textInputDelegate
=
_textInputDelegate
;
[
_inputViews
addObject
:
newInputView
];
if
(
!
fields
)
{
// DO NOT push the current autofillable input fields to the context even
// if it's password-related, because it is not in an autofill group.
focused
=
[
self
getOrCreateAutofillableView
:
focusedField
isPasswordAutofill
:
isPassword
];
[
_autofillContext
removeObjectForKey
:
focusedId
];
}
NSString
*
autofillId
=
uniqueIdFromDictionary
(
field
);
newInputView
.
autofillId
=
autofillId
;
for
(
NSDictionary
*
field
in
fields
)
{
NSString
*
autofillId
=
autofillIdFromDictionary
(
field
);
NSAssert
(
autofillId
,
@"autofillId must not be null for field: %@"
,
field
);
if
([
clientUniqueId
isEqualToString
:
autofillId
])
{
_activeView
=
newInputView
;
}
BOOL
hasHints
=
autofillTypeOf
(
field
)
!=
FlutterAutofillTypeNone
;
BOOL
isFocused
=
[
focusedId
isEqualToString
:
autofillId
];
[
FlutterTextInputPlugin
setupInputView
:
newInputView
withConfiguration
:
field
];
[
keyWindow
addSubview
:
newInputView
];
if
(
isFocused
)
{
focused
=
[
self
getOrCreateAutofillableView
:
field
isPasswordAutofill
:
isPassword
];
}
if
(
hasHints
)
{
// Push the current input field to the context if it has hints.
_autofillContext
[
autofillId
]
=
isFocused
?
focused
:
[
self
getOrCreateAutofillableView
:
field
isPasswordAutofill:
isPassword
];
}
else
{
// Mark for deletion;
[
_autofillContext
removeObjectForKey
:
autofillId
];
}
}
[
_activeView
setTextInputClient
:
client
];
[
_activeView
reloadInputViews
];
}
NSAssert
(
focused
,
@"The current focused input view must not be nil."
);
return
focused
;
}
// Returns a new non-reusable input view (and put it into the view hierarchy), or get the
// view from the current autofill context, if an input view with the same autofill id
// already exists in the context.
// This is generally used for input fields that are autofillable (UIKit tracks these veiws
// for autofill purposes so they should not be reused for a different type of views).
-
(
FlutterTextInputView
*
)
getOrCreateAutofillableView
:(
NSDictionary
*
)
field
isPasswordAutofill
:(
BOOL
)
needsPasswordAutofill
{
NSString
*
autofillId
=
autofillIdFromDictionary
(
field
);
FlutterTextInputView
*
inputView
=
_autofillContext
[
autofillId
];
if
(
!
inputView
)
{
inputView
=
needsPasswordAutofill
?
[
FlutterSecureTextInputView
alloc
]
:
[
FlutterTextInputView
alloc
];
inputView
=
[[
inputView
init
]
autorelease
];
[
self
addToKeyWindowIfNeeded
:
inputView
];
}
+
(
void
)
setupInputView
:(
FlutterTextInputView
*
)
inputView
withConfiguration
:(
NSDictionary
*
)
configuration
{
NSDictionary
*
inputType
=
configuration
[
@"inputType"
];
NSString
*
keyboardAppearance
=
configuration
[
@"keyboardAppearance"
];
NSDictionary
*
autofill
=
configuration
[
@"autofill"
];
inputView
.
textInputDelegate
=
_textInputDelegate
;
[
inputView
configureWithDictionary
:
field
];
return
inputView
;
}
inputView
.
secureTextEntry
=
[
configuration
[
@"obscureText"
]
boolValue
];
inputView
.
keyboardType
=
ToUIKeyboardType
(
inputType
);
inputView
.
returnKeyType
=
ToUIReturnKeyType
(
configuration
[
@"inputAction"
]);
inputView
.
autocapitalizationType
=
ToUITextAutoCapitalizationType
(
configuration
);
// Removes every installed input field, unless it's in the current autofill
// 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 autofill save .
-
(
void
)
cleanUpViewHierarchy
:(
BOOL
)
includeActiveView
clearText
:(
BOOL
)
clearText
{
UIWindow
*
keyWindow
=
[
UIApplication
sharedApplication
].
keyWindow
;
NSAssert
(
keyWindow
!=
nullptr
,
@"The application must have a key window since the keyboard client "
@"must be part of the responder chain to function"
);
if
(
@available
(
iOS
11.0
,
*
))
{
NSString
*
smartDashesType
=
configuration
[
@"smartDashesType"
];
// This index comes from the SmartDashesType enum in the framework.
bool
smartDashesIsDisabled
=
smartDashesType
&&
[
smartDashesType
isEqualToString
:
@"0"
];
inputView
.
smartDashesType
=
smartDashesIsDisabled
?
UITextSmartDashesTypeNo
:
UITextSmartDashesTypeYes
;
NSString
*
smartQuotesType
=
configuration
[
@"smartQuotesType"
];
// This index comes from the SmartQuotesType enum in the framework.
bool
smartQuotesIsDisabled
=
smartQuotesType
&&
[
smartQuotesType
isEqualToString
:
@"0"
];
inputView
.
smartQuotesType
=
smartQuotesIsDisabled
?
UITextSmartQuotesTypeNo
:
UITextSmartQuotesTypeYes
;
}
if
([
keyboardAppearance
isEqualToString
:
@"Brightness.dark"
])
{
inputView
.
keyboardAppearance
=
UIKeyboardAppearanceDark
;
}
else
if
([
keyboardAppearance
isEqualToString
:
@"Brightness.light"
])
{
inputView
.
keyboardAppearance
=
UIKeyboardAppearanceLight
;
}
else
{
inputView
.
keyboardAppearance
=
UIKeyboardAppearanceDefault
;
for
(
UIView
*
view
in
keyWindow
.
subviews
)
{
if
([
view
isKindOfClass
:[
FlutterTextInputView
class
]]
&&
(
includeActiveView
||
view
!=
_activeView
))
{
FlutterTextInputView
*
inputView
=
(
FlutterTextInputView
*
)
view
;
if
(
_autofillContext
[
inputView
.
autofillId
]
!=
view
)
{
if
(
clearText
)
{
inputView
.
text
.
string
=
@""
;
}
[
view
removeFromSuperview
];
}
}
}
NSString
*
autocorrect
=
configuration
[
@"autocorrect"
];
inputView
.
autocorrectionType
=
autocorrect
&&
!
[
autocorrect
boolValue
]
?
UITextAutocorrectionTypeNo
:
UITextAutocorrectionTypeDefault
;
if
(
@available
(
iOS
10.0
,
*
))
{
if
(
autofill
==
nil
)
{
inputView
.
textContentType
=
@""
;
}
else
{
inputView
.
textContentType
=
ToUITextContentType
(
autofill
[
@"hints"
]);
[
inputView
setTextInputState
:
autofill
[
@"editingValue"
]];
// An input field needs to be visible in order to get
// autofilled when it's not the one that triggered
// autofill.
inputView
.
frame
=
CGRectMake
(
0
,
0
,
1
,
1
);
}
-
(
void
)
changeInputViewsAutofillVisibility
:(
BOOL
)
newVisibility
{
UIWindow
*
keyWindow
=
[
UIApplication
sharedApplication
].
keyWindow
;
NSAssert
(
keyWindow
!=
nullptr
,
@"The application must have a key window since the keyboard client "
@"must be part of the responder chain to function"
);
for
(
UIView
*
view
in
keyWindow
.
subviews
)
{
if
([
view
isKindOfClass
:[
FlutterTextInputView
class
]])
{
FlutterTextInputView
*
inputView
=
(
FlutterTextInputView
*
)
view
;
inputView
.
isVisibleToAutofill
=
newVisibility
;
}
}
}
-
(
void
)
addToKeyWindowIfNeeded
:(
FlutterTextInputView
*
)
inputView
{
UIWindow
*
keyWindow
=
[
UIApplication
sharedApplication
].
keyWindow
;
NSAssert
(
keyWindow
!=
nullptr
,
@"The application must have a key window since the keyboard client "
@"must be part of the responder chain to function"
);
if
(
inputView
.
window
!=
keyWindow
)
{
[
keyWindow
addSubview
:
inputView
];
}
}
-
(
void
)
setTextInputEditingState
:(
NSDictionary
*
)
state
{
if
([
_activeView
setTextInputState
:
state
])
{
[
_activeView
updateEditingState
];
...
...
shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m
浏览文件 @
d96371e0
...
...
@@ -10,14 +10,26 @@
FLUTTER_ASSERT_ARC
@interface
FlutterTextInputPluginTest
:
XCTestCase
@end
@interface
FlutterTextInputView
()
@property
(
nonatomic
,
copy
)
NSString
*
autofillId
;
-
(
void
)
setTextInputState
:(
NSDictionary
*
)
state
;
-
(
BOOL
)
isVisibleToAutofill
;
@end
@interface
FlutterTextInputPlugin
()
@property
(
nonatomic
,
strong
)
FlutterTextInputView
*
reusableInputView
;
@property
(
nonatomic
,
assign
)
FlutterTextInputView
*
activeView
;
@property
(
nonatomic
,
readonly
)
NSMutableDictionary
<
NSString
*
,
FlutterTextInputView
*>*
autofillContext
;
@end
@interface
FlutterTextInputPluginTest
:
XCTestCase
@end
@implementation
FlutterTextInputPluginTest
{
NSDictionary
*
_template
;
NSDictionary
*
_passwordTemplate
;
id
engine
;
FlutterTextInputPlugin
*
textInputPlugin
;
}
...
...
@@ -34,33 +46,93 @@ FLUTTER_ASSERT_ARC
[
engine
stopMocking
];
[[[[
textInputPlugin
textInputView
]
superview
]
subviews
]
makeObjectsPerformSelector:
@selector
(
removeFromSuperview
)];
[
super
tearDown
];
}
-
(
void
)
testSecureInput
{
NSDictionary
*
config
=
@{
@"inputType"
:
@{
@"name"
:
@"TextInuptType.text"
},
@"keyboardAppearance"
:
@"Brightness.light"
,
@"obscureText"
:
@YES
,
@"inputAction"
:
@"TextInputAction.unspecified"
,
@"smartDashesType"
:
@"0"
,
@"smartQuotesType"
:
@"0"
,
@"autocorrect"
:
@YES
};
-
(
void
)
setClientId
:(
int
)
clientId
configuration
:(
NSDictionary
*
)
config
{
FlutterMethodCall
*
setClientCall
=
[
FlutterMethodCall
methodCallWithMethodName
:
@"TextInput.setClient"
arguments:
@[
@123
,
config
]];
arguments:
@[
[
NSNumber
numberWithInt
:
clientId
],
config
]];
[
textInputPlugin
handleMethodCall
:
setClientCall
result:
^
(
id
_Nullable
result
){
}];
}
// Find all the FlutterTextInputViews we created.
NSArray
<
FlutterTextInputView
*>*
inputFields
=
[[[[
textInputPlugin
textInputView
]
superview
]
subviews
]
filteredArrayUsingPredicate:
[
NSPredicate
predicateWithFormat
:
@"class == %@"
,
-
(
void
)
commitAutofillContextAndVerify
{
FlutterMethodCall
*
methodCall
=
[
FlutterMethodCall
methodCallWithMethodName
:
@"TextInput.finishAutofillContext"
arguments:
@YES
];
[
textInputPlugin
handleMethodCall
:
methodCall
result:
^
(
id
_Nullable
result
){
}];
XCTAssertEqual
(
self
.
viewsVisibleToAutofill
.
count
,
[
textInputPlugin
.
activeView
isVisibleToAutofill
]
?
1
:
0
);
XCTAssertNotEqual
(
textInputPlugin
.
textInputView
,
nil
);
// The active view should still be installed so it doesn't get
// deallocated.
XCTAssertEqual
(
self
.
installedInputViews
.
count
,
1
);
XCTAssertEqual
(
textInputPlugin
.
autofillContext
.
count
,
0
);
}
-
(
NSMutableDictionary
*
)
mutableTemplateCopy
{
if
(
!
_template
)
{
_template
=
@{
@"inputType"
:
@{
@"name"
:
@"TextInuptType.text"
},
@"keyboardAppearance"
:
@"Brightness.light"
,
@"obscureText"
:
@NO
,
@"inputAction"
:
@"TextInputAction.unspecified"
,
@"smartDashesType"
:
@"0"
,
@"smartQuotesType"
:
@"0"
,
@"autocorrect"
:
@YES
};
}
return
[
_template
mutableCopy
];
}
-
(
NSMutableDictionary
*
)
mutablePasswordTemplateCopy
{
if
(
!
_passwordTemplate
)
{
_passwordTemplate
=
@{
@"inputType"
:
@{
@"name"
:
@"TextInuptType.text"
},
@"keyboardAppearance"
:
@"Brightness.light"
,
@"obscureText"
:
@YES
,
@"inputAction"
:
@"TextInputAction.unspecified"
,
@"smartDashesType"
:
@"0"
,
@"smartQuotesType"
:
@"0"
,
@"autocorrect"
:
@YES
};
}
return
[
_passwordTemplate
mutableCopy
];
}
-
(
NSArray
<
FlutterTextInputView
*>*
)
installedInputViews
{
UIWindow
*
keyWindow
=
[[[
UIApplication
sharedApplication
]
windows
]
filteredArrayUsingPredicate:
[
NSPredicate
predicateWithFormat
:
@"isKeyWindow == YES"
]]
.
firstObject
;
return
[
keyWindow
.
subviews
filteredArrayUsingPredicate:
[
NSPredicate
predicateWithFormat
:
@"self isKindOfClass: %@"
,
[
FlutterTextInputView
class
]]];
}
-
(
NSArray
<
FlutterTextInputView
*>*
)
viewsVisibleToAutofill
{
return
[
self
.
installedInputViews
filteredArrayUsingPredicate:
[
NSPredicate
predicateWithFormat
:
@"isVisibleToAutofill == YES"
]];
}
#pragma mark - Tests
-
(
void
)
testSecureInput
{
NSDictionary
*
config
=
self
.
mutableTemplateCopy
;
[
config
setValue
:
@"YES"
forKey
:
@"obscureText"
];
[
self
setClientId
:
123
configuration
:
config
];
// Find all the FlutterTextInputViews we created.
NSArray
<
FlutterTextInputView
*>*
inputFields
=
self
.
installedInputViews
;
// There are no autofill and the mock framework requested a secure entry. The first and only
// inserted FlutterTextInputView should be a secure text entry one.
...
...
@@ -75,6 +147,10 @@ FLUTTER_ASSERT_ARC
// The one FlutterTextInputView we inserted into the view hierarchy should be the text input
// plugin's active text input view.
XCTAssertEqual
(
inputView
,
textInputPlugin
.
textInputView
);
// Despite not given an id in configuration, inputView has
// an autofill id.
XCTAssert
(
inputView
.
autofillId
.
length
>
0
);
}
-
(
void
)
testTextChangesTriggerUpdateEditingClient
{
...
...
@@ -167,18 +243,9 @@ FLUTTER_ASSERT_ARC
}]]);
}
-
(
void
)
testAutofillInputViews
{
NSDictionary
*
template
=
@{
@"inputType"
:
@{
@"name"
:
@"TextInuptType.text"
},
@"keyboardAppearance"
:
@"Brightness.light"
,
@"obscureText"
:
@NO
,
@"inputAction"
:
@"TextInputAction.unspecified"
,
@"smartDashesType"
:
@"0"
,
@"smartQuotesType"
:
@"0"
,
@"autocorrect"
:
@YES
};
NSMutableDictionary
*
field1
=
[
template
mutableCopy
];
-
(
void
)
testAutofillContext
{
NSMutableDictionary
*
field1
=
self
.
mutableTemplateCopy
;
[
field1
setValue
:@{
@"uniqueIdentifier"
:
@"field1"
,
@"hints"
:
@[
@"hint1"
],
...
...
@@ -186,7 +253,7 @@ FLUTTER_ASSERT_ARC
}
forKey:
@"autofill"
];
NSMutableDictionary
*
field2
=
[
template
mutableCopy
]
;
NSMutableDictionary
*
field2
=
self
.
mutablePasswordTemplateCopy
;
[
field2
setValue
:@{
@"uniqueIdentifier"
:
@"field2"
,
@"hints"
:
@[
@"hint2"
],
...
...
@@ -197,21 +264,160 @@ FLUTTER_ASSERT_ARC
NSMutableDictionary
*
config
=
[
field1
mutableCopy
];
[
config
setValue
:@[
field1
,
field2
]
forKey
:
@"fields"
];
FlutterMethodCall
*
setClientCall
=
[
FlutterMethodCall
methodCallWithMethodName
:
@"TextInput.setClient"
arguments:
@[
@123
,
config
]];
[
self
setClientId
:
123
configuration
:
config
];
XCTAssertEqual
(
self
.
viewsVisibleToAutofill
.
count
,
2
);
[
textInputPlugin
handleMethodCall
:
setClientCall
result:
^
(
id
_Nullable
result
){
}];
XCTAssertEqual
(
textInputPlugin
.
autofillContext
.
count
,
2
);
XCTAssertEqual
(
self
.
installedInputViews
.
count
,
2
);
XCTAssertEqual
(
textInputPlugin
.
textInputView
,
textInputPlugin
.
autofillContext
[
@"field1"
]);
// The configuration changes.
NSMutableDictionary
*
field3
=
self
.
mutablePasswordTemplateCopy
;
[
field3
setValue
:@{
@"uniqueIdentifier"
:
@"field3"
,
@"hints"
:
@[
@"hint3"
],
@"editingValue"
:
@{
@"text"
:
@""
}
}
forKey:
@"autofill"
];
NSMutableDictionary
*
oldContext
=
textInputPlugin
.
autofillContext
;
// Replace field2 with field3.
[
config
setValue
:@[
field1
,
field3
]
forKey
:
@"fields"
];
[
self
setClientId
:
123
configuration
:
config
];
XCTAssertEqual
(
self
.
viewsVisibleToAutofill
.
count
,
2
);
XCTAssertEqual
(
textInputPlugin
.
autofillContext
.
count
,
3
);
XCTAssertEqual
(
self
.
installedInputViews
.
count
,
3
);
XCTAssertEqual
(
textInputPlugin
.
textInputView
,
textInputPlugin
.
autofillContext
[
@"field1"
]);
// Old autofill input fields are still installed and reused.
for
(
NSString
*
key
in
oldContext
.
allKeys
)
{
XCTAssertEqual
(
oldContext
[
key
],
textInputPlugin
.
autofillContext
[
key
]);
}
// Switch to a password field that has no contentType and is not in an AutofillGroup.
config
=
self
.
mutablePasswordTemplateCopy
;
oldContext
=
textInputPlugin
.
autofillContext
;
[
self
setClientId
:
124
configuration
:
config
];
XCTAssertEqual
(
self
.
viewsVisibleToAutofill
.
count
,
1
);
XCTAssertEqual
(
textInputPlugin
.
autofillContext
.
count
,
3
);
XCTAssertEqual
(
self
.
installedInputViews
.
count
,
4
);
// Old autofill input fields are still installed and reused.
for
(
NSString
*
key
in
oldContext
.
allKeys
)
{
XCTAssertEqual
(
oldContext
[
key
],
textInputPlugin
.
autofillContext
[
key
]);
}
// The active view should change.
XCTAssertNotEqual
(
textInputPlugin
.
textInputView
,
textInputPlugin
.
autofillContext
[
@"field1"
]);
// Switch to a similar password field, the previous field should be reused.
oldContext
=
textInputPlugin
.
autofillContext
;
[
self
setClientId
:
200
configuration
:
config
];
// Reuse the input view instance from the last time.
XCTAssertEqual
(
self
.
viewsVisibleToAutofill
.
count
,
1
);
XCTAssertEqual
(
textInputPlugin
.
autofillContext
.
count
,
3
);
XCTAssertEqual
(
self
.
installedInputViews
.
count
,
4
);
// Old autofill input fields are still installed and reused.
for
(
NSString
*
key
in
oldContext
.
allKeys
)
{
XCTAssertEqual
(
oldContext
[
key
],
textInputPlugin
.
autofillContext
[
key
]);
}
XCTAssertNotEqual
(
textInputPlugin
.
textInputView
,
textInputPlugin
.
autofillContext
[
@"field1"
]);
}
-
(
void
)
testCommitAutofillContext
{
NSMutableDictionary
*
field1
=
self
.
mutableTemplateCopy
;
[
field1
setValue
:@{
@"uniqueIdentifier"
:
@"field1"
,
@"hints"
:
@[
@"hint1"
],
@"editingValue"
:
@{
@"text"
:
@""
}
}
forKey:
@"autofill"
];
NSMutableDictionary
*
field2
=
self
.
mutablePasswordTemplateCopy
;
[
field2
setValue
:@{
@"uniqueIdentifier"
:
@"field2"
,
@"hints"
:
@[
@"hint2"
],
@"editingValue"
:
@{
@"text"
:
@""
}
}
forKey:
@"autofill"
];
NSMutableDictionary
*
field3
=
self
.
mutableTemplateCopy
;
[
field3
setValue
:@{
@"uniqueIdentifier"
:
@"field3"
,
@"hints"
:
@[
@"hint3"
],
@"editingValue"
:
@{
@"text"
:
@""
}
}
forKey:
@"autofill"
];
NSMutableDictionary
*
config
=
[
field1
mutableCopy
];
[
config
setValue
:@[
field1
,
field2
]
forKey
:
@"fields"
];
[
self
setClientId
:
123
configuration
:
config
];
XCTAssertEqual
(
self
.
viewsVisibleToAutofill
.
count
,
2
);
XCTAssertEqual
(
textInputPlugin
.
autofillContext
.
count
,
2
);
[
self
commitAutofillContextAndVerify
];
XCTAssertNotEqual
(
textInputPlugin
.
textInputView
,
textInputPlugin
.
reusableInputView
);
// Install the password field again.
[
self
setClientId
:
123
configuration
:
config
];
// Switch to a regular autofill group.
[
self
setClientId
:
124
configuration
:
field3
];
XCTAssertEqual
(
self
.
viewsVisibleToAutofill
.
count
,
1
);
XCTAssertEqual
(
self
.
installedInputViews
.
count
,
3
);
XCTAssertEqual
(
textInputPlugin
.
autofillContext
.
count
,
2
);
XCTAssertNotEqual
(
textInputPlugin
.
textInputView
,
nil
);
[
self
commitAutofillContextAndVerify
];
XCTAssertNotEqual
(
textInputPlugin
.
textInputView
,
textInputPlugin
.
reusableInputView
);
// 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.
XCTAssertEqual
(
self
.
installedInputViews
.
count
,
1
);
XCTAssertEqual
(
textInputPlugin
.
autofillContext
.
count
,
0
);
[
self
commitAutofillContextAndVerify
];
XCTAssertEqual
(
textInputPlugin
.
textInputView
,
textInputPlugin
.
reusableInputView
);
}
-
(
void
)
testAutofillInputViews
{
NSMutableDictionary
*
field1
=
self
.
mutableTemplateCopy
;
[
field1
setValue
:@{
@"uniqueIdentifier"
:
@"field1"
,
@"hints"
:
@[
@"hint1"
],
@"editingValue"
:
@{
@"text"
:
@""
}
}
forKey:
@"autofill"
];
NSMutableDictionary
*
field2
=
self
.
mutablePasswordTemplateCopy
;
[
field2
setValue
:@{
@"uniqueIdentifier"
:
@"field2"
,
@"hints"
:
@[
@"hint2"
],
@"editingValue"
:
@{
@"text"
:
@""
}
}
forKey:
@"autofill"
];
NSMutableDictionary
*
config
=
[
field1
mutableCopy
];
[
config
setValue
:@[
field1
,
field2
]
forKey
:
@"fields"
];
[
self
setClientId
:
123
configuration
:
config
];
// Find all the FlutterTextInputViews we created.
NSArray
<
FlutterTextInputView
*>*
inputFields
=
[[[[
textInputPlugin
textInputView
]
superview
]
subviews
]
filteredArrayUsingPredicate:
[
NSPredicate
predicateWithFormat
:
@"class == %@"
,
[
FlutterTextInputView
class
]]];
NSArray
<
FlutterTextInputView
*>*
inputFields
=
self
.
installedInputViews
;
// Both fields are installed and visible because it's a password group.
XCTAssertEqual
(
inputFields
.
count
,
2
);
XCTAssertEqual
(
self
.
viewsVisibleToAutofill
.
count
,
2
);
// Find the inactive autofillable input field.
FlutterTextInputView
*
inactiveView
=
inputFields
[
1
];
...
...
@@ -222,6 +428,22 @@ FLUTTER_ASSERT_ARC
OCMVerify
([
engine
updateEditingClient
:
0
withState
:[
OCMArg
isNotNil
]
withTag
:
@"field2"
]);
}
-
(
void
)
testPasswordAutofillHack
{
NSDictionary
*
config
=
self
.
mutableTemplateCopy
;
[
config
setValue
:
@"YES"
forKey
:
@"obscureText"
];
[
self
setClientId
:
123
configuration
:
config
];
// Find all the FlutterTextInputViews we created.
NSArray
<
FlutterTextInputView
*>*
inputFields
=
self
.
installedInputViews
;
FlutterTextInputView
*
inputView
=
inputFields
[
0
];
XCTAssert
([
inputView
isKindOfClass
:[
UITextField
class
]]);
// FlutterSecureTextInputView does not respond to font,
// but it should return the default UITextField.font.
XCTAssertNotEqual
([
inputView
performSelector
:
@selector
(
font
)],
nil
);
}
-
(
void
)
testAutocorrectionPromptRectAppears
{
FlutterTextInputView
*
inputView
=
[[
FlutterTextInputView
alloc
]
initWithFrame
:
CGRectZero
];
inputView
.
textInputDelegate
=
engine
;
...
...
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录