blob: d339d2a39a2327d0311807c784f55f4225549843 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/autofill/form_suggestion_controller.h"
#import <memory>
#import "base/mac/foundation_util.h"
#import "base/metrics/histogram_functions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "components/autofill/core/browser/ui/autofill_popup_delegate.h"
#import "components/autofill/core/browser/ui/popup_types.h"
#import "components/autofill/ios/browser/form_suggestion.h"
#import "components/autofill/ios/browser/form_suggestion_provider.h"
#import "components/autofill/ios/form_util/form_activity_params.h"
#import "components/prefs/pref_service.h"
#import "ios/chrome/browser/autofill/form_input_navigator.h"
#import "ios/chrome/browser/autofill/form_input_suggestions_provider.h"
#import "ios/chrome/browser/shared/model/browser_state/chrome_browser_state.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/web/common/url_scheme_util.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/ui/crw_web_view_proxy.h"
#import "ios/web/public/web_state.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using autofill::FieldRendererId;
using autofill::FormRendererId;
// Block types for `RunSearchPipeline`.
using PipelineBlock = void (^)(void (^completion)(BOOL));
using PipelineCompletionBlock = void (^)(NSUInteger index);
namespace {
// Struct that describes suggestion state.
struct AutofillSuggestionState {
AutofillSuggestionState(const std::string& form_name,
FormRendererId unique_form_id,
const std::string& field_identifier,
FieldRendererId unique_field_id,
const std::string& frame_identifier,
const std::string& typed_value);
// The name of the form for autofill.
std::string form_name;
// The unique numeric identifier of the form for autofill.
FormRendererId unique_form_id;
// The identifier of the field for autofill.
std::string field_identifier;
// The unique numeric identifier of the field for autofill.
FieldRendererId unique_field_id;
// The identifier of the frame for autofill.
std::string frame_identifier;
// The user-typed value in the field.
std::string typed_value;
// The suggestions for the form field. An array of `FormSuggestion`.
NSArray* suggestions;
};
AutofillSuggestionState::AutofillSuggestionState(
const std::string& form_name,
FormRendererId unique_form_id,
const std::string& field_identifier,
FieldRendererId unique_field_id,
const std::string& frame_identifier,
const std::string& typed_value)
: form_name(form_name),
unique_form_id(unique_form_id),
field_identifier(field_identifier),
unique_field_id(unique_field_id),
frame_identifier(frame_identifier),
typed_value(typed_value) {}
// Executes each PipelineBlock in `blocks` in order until one invokes its
// completion with YES, in which case `on_complete` will be invoked with the
// `index` of the succeeding block, or until they all invoke their completions
// with NO, in which case `on_complete` will be invoked with NSNotFound.
void RunSearchPipeline(NSArray<PipelineBlock>* blocks,
PipelineCompletionBlock on_complete,
NSUInteger from_index = 0) {
if (from_index == [blocks count]) {
on_complete(NSNotFound);
return;
}
PipelineBlock block = blocks[from_index];
block(^(BOOL success) {
if (success) {
on_complete(from_index);
} else {
RunSearchPipeline(blocks, on_complete, from_index + 1);
}
});
}
} // namespace
@interface FormSuggestionController () {
// Callback to update the accessory view.
FormSuggestionsReadyCompletion _accessoryViewUpdateBlock;
// Autofill suggestion state.
std::unique_ptr<AutofillSuggestionState> _suggestionState;
// Providers for suggestions, sorted according to the order in which
// they should be asked for suggestions, with highest priority in front.
NSArray* _suggestionProviders;
// Access to WebView from the CRWWebController.
id<CRWWebViewProxy> _webViewProxy;
}
// Unique id of the last request.
@property(nonatomic, assign) NSUInteger requestIdentifier;
// Updates keyboard for `suggestionState`.
- (void)updateKeyboard:(AutofillSuggestionState*)suggestionState;
// Updates keyboard with `suggestions`.
- (void)updateKeyboardWithSuggestions:(NSArray*)suggestions;
// Clears state in between page loads.
- (void)resetSuggestionState;
@end
@implementation FormSuggestionController {
// The WebState this instance is observing. Will be null after
// -webStateDestroyed: has been called.
web::WebState* _webState;
// Bridge to observe the web state from Objective-C.
std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
// The provider for the current set of suggestions.
__weak id<FormSuggestionProvider> _provider;
}
@synthesize formInputNavigator = _formInputNavigator;
- (instancetype)initWithWebState:(web::WebState*)webState
providers:(NSArray*)providers {
self = [super init];
if (self) {
DCHECK(webState);
_webState = webState;
_webStateObserverBridge =
std::make_unique<web::WebStateObserverBridge>(self);
_webState->AddObserver(_webStateObserverBridge.get());
_webViewProxy = webState->GetWebViewProxy();
_suggestionProviders = [providers copy];
}
return self;
}
- (void)dealloc {
if (_webState) {
_webState->RemoveObserver(_webStateObserverBridge.get());
_webStateObserverBridge.reset();
_webState = nullptr;
}
}
- (void)detachFromWebState {
if (_webState) {
_webState->RemoveObserver(_webStateObserverBridge.get());
_webStateObserverBridge.reset();
_webState = nullptr;
}
}
#pragma mark - CRWWebStateObserver
- (void)webStateDestroyed:(web::WebState*)webState {
DCHECK_EQ(_webState, webState);
[self detachFromWebState];
}
- (void)webState:(web::WebState*)webState didLoadPageWithSuccess:(BOOL)success {
DCHECK_EQ(_webState, webState);
[self processPage:webState];
}
- (void)processPage:(web::WebState*)webState {
[self resetSuggestionState];
}
- (void)setWebViewProxy:(id<CRWWebViewProxy>)webViewProxy {
_webViewProxy = webViewProxy;
}
- (void)retrieveSuggestionsForForm:(const autofill::FormActivityParams&)params
webState:(web::WebState*)webState {
self.requestIdentifier += 1;
NSUInteger requestIdentifier = self.requestIdentifier;
__weak FormSuggestionController* weakSelf = self;
FormSuggestionProviderQuery* formQuery = [[FormSuggestionProviderQuery alloc]
initWithFormName:base::SysUTF8ToNSString(params.form_name)
uniqueFormID:params.unique_form_id
fieldIdentifier:base::SysUTF8ToNSString(params.field_identifier)
uniqueFieldID:params.unique_field_id
fieldType:base::SysUTF8ToNSString(params.field_type)
type:base::SysUTF8ToNSString(params.type)
typedValue:base::SysUTF8ToNSString(
_suggestionState.get()->typed_value)
frameID:base::SysUTF8ToNSString(params.frame_id)];
BOOL hasUserGesture = params.has_user_gesture;
// Build a block for each provider that will invoke its completion with YES
// if the provider can provide suggestions for the specified form/field/type
// and NO otherwise.
NSMutableArray* findProviderBlocks = [[NSMutableArray alloc] init];
for (NSUInteger i = 0; i < [_suggestionProviders count]; i++) {
PipelineBlock block = ^(void (^completion)(BOOL success)) {
// Access all the providers through `self` to guarantee that both
// `self` and all the providers exist when the block is executed.
// `_suggestionProviders` is immutable, so the subscripting is
// always valid.
FormSuggestionController* strongSelf = weakSelf;
if (!strongSelf)
return;
id<FormSuggestionProvider> provider = strongSelf->_suggestionProviders[i];
[provider checkIfSuggestionsAvailableForForm:formQuery
hasUserGesture:hasUserGesture
webState:webState
completionHandler:completion];
};
[findProviderBlocks addObject:block];
}
// Once the suggestions are retrieved, update the suggestions UI.
SuggestionsReadyCompletion readyCompletion =
^(NSArray<FormSuggestion*>* suggestions,
id<FormSuggestionProvider> provider) {
[weakSelf onSuggestionsReady:suggestions provider:provider];
};
// Once a provider is found, use it to retrieve suggestions.
PipelineCompletionBlock completion = ^(NSUInteger providerIndex) {
// Ignore outdated results.
if (weakSelf.requestIdentifier != requestIdentifier) {
return;
}
if (providerIndex == NSNotFound) {
[weakSelf onNoSuggestionsAvailable];
return;
}
FormSuggestionController* strongSelf = weakSelf;
if (!strongSelf)
return;
id<FormSuggestionProvider> provider =
strongSelf->_suggestionProviders[providerIndex];
[provider retrieveSuggestionsForForm:formQuery
webState:webState
completionHandler:readyCompletion];
};
// Run all the blocks in `findProviderBlocks` until one invokes its
// completion with YES. The first one to do so will be passed to
// `completion`.
RunSearchPipeline(findProviderBlocks, completion);
}
- (void)onNoSuggestionsAvailable {
// Check the update block hasn't been reset while waiting for suggestions.
if (!_accessoryViewUpdateBlock) {
return;
}
_accessoryViewUpdateBlock(@[], self);
}
- (void)onSuggestionsReady:(NSArray<FormSuggestion*>*)suggestions
provider:(id<FormSuggestionProvider>)provider {
// TODO(ios): crbug.com/249916. If we can also pass in the form/field for
// which `suggestions` are, we should check here if `suggestions` are for
// the current active element. If not, reset `_suggestionState`.
if (!_suggestionState) {
// The suggestion state was reset in between the call to Autofill API (e.g.
// OnAskForValuesToFill) and this method being called back. Results are
// therefore no longer relevant.
return;
}
_provider = provider;
_suggestionState->suggestions = [suggestions copy];
[self updateKeyboard:_suggestionState.get()];
}
- (void)resetSuggestionState {
_provider = nil;
_suggestionState.reset();
}
- (void)clearSuggestions {
// Note that other parts of the suggestionsState are not reset.
if (!_suggestionState.get())
return;
_suggestionState->suggestions = [[NSArray alloc] init];
[self updateKeyboard:_suggestionState.get()];
}
- (void)updateKeyboard:(AutofillSuggestionState*)suggestionState {
if (!suggestionState) {
if (_accessoryViewUpdateBlock)
_accessoryViewUpdateBlock(nil, self);
} else {
[self updateKeyboardWithSuggestions:suggestionState->suggestions];
}
}
- (void)updateKeyboardWithSuggestions:(NSArray<FormSuggestion*>*)suggestions {
if (_accessoryViewUpdateBlock) {
_accessoryViewUpdateBlock(suggestions, self);
}
}
- (void)didSelectSuggestion:(FormSuggestion*)suggestion {
// If a suggestion was selected, reset the password bottom sheet dismiss count
// to 0.
[self resetPasswordBottomSheetDismissCount];
if (!_suggestionState)
return;
// Send the suggestion to the provider. Upon completion advance the cursor
// for single-field Autofill, or close the keyboard for full-form Autofill.
__weak FormSuggestionController* weakSelf = self;
[_provider
didSelectSuggestion:suggestion
form:base::SysUTF8ToNSString(_suggestionState->form_name)
uniqueFormID:_suggestionState->unique_form_id
fieldIdentifier:base::SysUTF8ToNSString(
_suggestionState->field_identifier)
uniqueFieldID:_suggestionState->unique_field_id
frameID:base::SysUTF8ToNSString(
_suggestionState->frame_identifier)
completionHandler:^{
[[weakSelf formInputNavigator] closeKeyboardWithoutButtonPress];
}];
}
#pragma mark - FormInputSuggestionsProvider
- (void)retrieveSuggestionsForForm:(const autofill::FormActivityParams&)params
webState:(web::WebState*)webState
accessoryViewUpdateBlock:
(FormSuggestionsReadyCompletion)accessoryViewUpdateBlock {
[self processPage:webState];
_suggestionState.reset(new AutofillSuggestionState(
params.form_name, params.unique_form_id, params.field_identifier,
params.unique_field_id, params.frame_id, params.value));
_accessoryViewUpdateBlock = [accessoryViewUpdateBlock copy];
[self retrieveSuggestionsForForm:params webState:webState];
}
- (void)inputAccessoryViewControllerDidReset {
_accessoryViewUpdateBlock = nil;
[self resetSuggestionState];
}
- (SuggestionProviderType)type {
return _provider ? _provider.type : SuggestionProviderTypeUnknown;
}
- (autofill::PopupType)suggestionType {
return _provider ? _provider.suggestionType
: autofill::PopupType::kUnspecified;
}
#pragma mark - Private
// Resets the password bottom sheet dismiss count to 0.
- (void)resetPasswordBottomSheetDismissCount {
ChromeBrowserState* browserState =
_webState
? ChromeBrowserState::FromBrowserState(_webState->GetBrowserState())
: nullptr;
if (browserState) {
int dismissCount = browserState->GetPrefs()->GetInteger(
prefs::kIosPasswordBottomSheetDismissCount);
browserState->GetPrefs()->SetInteger(
prefs::kIosPasswordBottomSheetDismissCount, 0);
if (dismissCount > 0) {
// Log how many times the bottom sheet had been dismissed before being
// re-enabled.
static constexpr int kHistogramMin = 1;
static constexpr int kHistogramMax = 4;
static constexpr size_t kHistogramBuckets = 3;
base::UmaHistogramCustomCounts(
"IOS.ResetDismissCount.Password.BottomSheet", dismissCount,
kHistogramMin, kHistogramMax, kHistogramBuckets);
}
}
}
@end