| Index: ios/chrome/browser/autofill/form_suggestion_controller.mm
|
| diff --git a/ios/chrome/browser/autofill/form_suggestion_controller.mm b/ios/chrome/browser/autofill/form_suggestion_controller.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..e3eb8aef26bee006458c8fe6e6330f6026f2c085
|
| --- /dev/null
|
| +++ b/ios/chrome/browser/autofill/form_suggestion_controller.mm
|
| @@ -0,0 +1,352 @@
|
| +// Copyright 2014 The Chromium Authors. All rights reserved.
|
| +// 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"
|
| +
|
| +#include "base/ios/weak_nsobject.h"
|
| +#include "base/mac/foundation_util.h"
|
| +#include "base/mac/scoped_block.h"
|
| +#include "base/mac/scoped_nsobject.h"
|
| +#include "base/memory/scoped_ptr.h"
|
| +#include "base/strings/sys_string_conversions.h"
|
| +#include "base/strings/utf_string_conversions.h"
|
| +#include "components/autofill/core/browser/autofill_popup_delegate.h"
|
| +#import "components/autofill/ios/browser/form_suggestion.h"
|
| +#import "ios/chrome/browser/autofill/form_input_accessory_view_controller.h"
|
| +#import "ios/chrome/browser/autofill/form_suggestion_provider.h"
|
| +#import "ios/chrome/browser/autofill/form_suggestion_view.h"
|
| +#import "ios/chrome/browser/passwords/password_generation_utils.h"
|
| +#include "ios/web/public/url_scheme_util.h"
|
| +#import "ios/web/public/web_state/crw_web_view_proxy.h"
|
| +#import "ios/web/public/web_state/js/crw_js_injection_receiver.h"
|
| +#import "ios/web/public/web_state/web_state.h"
|
| +
|
| +namespace {
|
| +
|
| +// Struct that describes suggestion state.
|
| +struct AutofillSuggestionState {
|
| + AutofillSuggestionState(const std::string& form_name,
|
| + const std::string& field_name,
|
| + const std::string& typed_value);
|
| + // The name of the form for autofill.
|
| + std::string form_name;
|
| + // The name of the field for autofill.
|
| + std::string field_name;
|
| + // The user-typed value in the field.
|
| + std::string typed_value;
|
| + // The suggestions for the form field. An array of |FormSuggestion|.
|
| + base::scoped_nsobject<NSArray> suggestions;
|
| +};
|
| +
|
| +AutofillSuggestionState::AutofillSuggestionState(const std::string& form_name,
|
| + const std::string& field_name,
|
| + const std::string& typed_value)
|
| + : form_name(form_name), field_name(field_name), typed_value(typed_value) {
|
| +}
|
| +
|
| +} // namespace
|
| +
|
| +@interface FormSuggestionController () <FormInputAccessoryViewProvider> {
|
| + // Form navigation delegate.
|
| + base::WeakNSProtocol<id<FormInputAccessoryViewDelegate>> _delegate;
|
| +
|
| + // Callback to update the accessory view.
|
| + base::mac::ScopedBlock<AccessoryViewReadyCompletion> completionHandler_;
|
| +
|
| + // Autofill suggestion state.
|
| + scoped_ptr<AutofillSuggestionState> _suggestionState;
|
| +
|
| + // Providers for suggestions, sorted according to the order in which
|
| + // they should be asked for suggestions, with highest priority in front.
|
| + base::scoped_nsobject<NSArray> _suggestionProviders;
|
| +
|
| + // Access to WebView from the CRWWebController.
|
| + base::scoped_nsprotocol<id<CRWWebViewProxy>> _webViewProxy;
|
| +}
|
| +
|
| +// Returns an autoreleased input accessory view that shows |suggestions|.
|
| +- (UIView*)suggestionViewWithSuggestions:(NSArray*)suggestions;
|
| +
|
| +// Updates keyboard for |suggestionState|.
|
| +- (void)updateKeyboard:(AutofillSuggestionState*)suggestionState;
|
| +
|
| +// Updates keyboard with |suggestions|.
|
| +- (void)updateKeyboardWithSuggestions:(NSArray*)suggestions;
|
| +
|
| +// Clears state in between page loads.
|
| +- (void)resetSuggestionState;
|
| +
|
| +// Finds a FormSuggestionProvider that can supply suggestions for the specified
|
| +// form, requests them, and updates the view accordingly.
|
| +- (void)retrieveSuggestionsForFormNamed:(const std::string&)formName
|
| + fieldName:(const std::string&)fieldName
|
| + type:(const std::string&)type
|
| + webState:(web::WebState*)webState;
|
| +
|
| +@end
|
| +
|
| +@implementation FormSuggestionController {
|
| + // Bridge to observe the web state from Objective-C.
|
| + scoped_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
|
| +
|
| + // Manager for FormSuggestion JavaScripts.
|
| + base::scoped_nsobject<JsSuggestionManager> _jsSuggestionManager;
|
| +
|
| + // The provider for the current set of suggestions.
|
| + __weak id<FormSuggestionProvider> _provider;
|
| +}
|
| +
|
| +- (instancetype)initWithWebState:(web::WebState*)webState
|
| + providers:(NSArray*)providers
|
| + JsSuggestionManager:(JsSuggestionManager*)jsSuggestionManager {
|
| + self = [super init];
|
| + if (self) {
|
| + _webStateObserverBridge.reset(
|
| + new web::WebStateObserverBridge(webState, self));
|
| + _webViewProxy.reset([webState->GetWebViewProxy() retain]);
|
| + _jsSuggestionManager.reset([jsSuggestionManager retain]);
|
| + _suggestionProviders.reset([providers copy]);
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +- (instancetype)initWithWebState:(web::WebState*)webState
|
| + providers:(NSArray*)providers {
|
| + JsSuggestionManager* jsSuggestionManager =
|
| + base::mac::ObjCCast<JsSuggestionManager>(
|
| + [webState->GetJSInjectionReceiver()
|
| + instanceOfClass:[JsSuggestionManager class]]);
|
| + return [self initWithWebState:webState
|
| + providers:providers
|
| + JsSuggestionManager:jsSuggestionManager];
|
| +}
|
| +
|
| +- (void)detachFromWebState {
|
| + _webStateObserverBridge.reset();
|
| +}
|
| +
|
| +#pragma mark -
|
| +#pragma mark CRWWebStateObserver
|
| +
|
| +- (void)webStateDestroyed:(web::WebState*)webState {
|
| + [self detachFromWebState];
|
| +}
|
| +
|
| +- (void)pageLoaded:(web::WebState*)webState {
|
| + [self processPage:webState];
|
| +}
|
| +
|
| +- (void)processPage:(web::WebState*)webState {
|
| + [self resetSuggestionState];
|
| +
|
| + web::URLVerificationTrustLevel trustLevel =
|
| + web::URLVerificationTrustLevel::kNone;
|
| + const GURL pageURL(webState->GetCurrentURL(&trustLevel));
|
| + if (trustLevel != web::URLVerificationTrustLevel::kAbsolute) {
|
| + DLOG(WARNING) << "Page load not handled on untrusted page";
|
| + return;
|
| + }
|
| +
|
| + if (web::UrlHasWebScheme(pageURL) && webState->ContentIsHTML())
|
| + [_jsSuggestionManager inject];
|
| +}
|
| +
|
| +- (void)setWebViewProxy:(id<CRWWebViewProxy>)webViewProxy {
|
| + _webViewProxy.reset([webViewProxy retain]);
|
| +}
|
| +
|
| +- (void)retrieveSuggestionsForFormNamed:(const std::string&)formName
|
| + fieldName:(const std::string&)fieldName
|
| + type:(const std::string&)type
|
| + webState:(web::WebState*)webState {
|
| + base::WeakNSObject<FormSuggestionController> weakSelf(self);
|
| + base::scoped_nsobject<NSString> strongFormName(
|
| + [base::SysUTF8ToNSString(formName) copy]);
|
| + base::scoped_nsobject<NSString> strongFieldName(
|
| + [base::SysUTF8ToNSString(fieldName) copy]);
|
| + base::scoped_nsobject<NSString> strongType(
|
| + [base::SysUTF8ToNSString(type) copy]);
|
| + base::scoped_nsobject<NSString> strongValue(
|
| + [base::SysUTF8ToNSString(_suggestionState.get()->typed_value) copy]);
|
| +
|
| + // 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.
|
| + base::scoped_nsobject<NSMutableArray> findProviderBlocks(
|
| + [[NSMutableArray alloc] init]);
|
| + for (NSUInteger i = 0; i < [_suggestionProviders count]; i++) {
|
| + base::mac::ScopedBlock<passwords::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.
|
| + base::scoped_nsobject<FormSuggestionController> strongSelf(
|
| + [weakSelf retain]);
|
| + if (!strongSelf)
|
| + return;
|
| + id<FormSuggestionProvider> provider =
|
| + strongSelf.get()->_suggestionProviders[i];
|
| + [provider checkIfSuggestionsAvailableForForm:strongFormName
|
| + field:strongFieldName
|
| + type:strongType
|
| + typedValue:strongValue
|
| + webState:webState
|
| + completionHandler:completion];
|
| + },
|
| + base::scoped_policy::RETAIN);
|
| + [findProviderBlocks addObject:block];
|
| + }
|
| +
|
| + // Once the suggestions are retrieved, update the suggestions UI.
|
| + SuggestionsReadyCompletion readyCompletion =
|
| + ^(NSArray* suggestions, id<FormSuggestionProvider> provider) {
|
| + [weakSelf onSuggestionsReady:suggestions provider:provider];
|
| + };
|
| +
|
| + // Once a provider is found, use it to retrieve suggestions.
|
| + passwords::PipelineCompletionBlock completion = ^(NSUInteger providerIndex) {
|
| + if (providerIndex == NSNotFound)
|
| + return;
|
| + base::scoped_nsobject<FormSuggestionController> strongSelf(
|
| + [weakSelf retain]);
|
| + if (!strongSelf)
|
| + return;
|
| + id<FormSuggestionProvider> provider =
|
| + strongSelf.get()->_suggestionProviders[providerIndex];
|
| + [provider retrieveSuggestionsForForm:strongFormName
|
| + field:strongFieldName
|
| + type:strongType
|
| + typedValue:strongValue
|
| + 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
|
| + // |onProviderFound|.
|
| + passwords::RunSearchPipeline(findProviderBlocks, completion);
|
| +}
|
| +
|
| +- (void)onSuggestionsReady:(NSArray*)suggestions
|
| + provider:(id<FormSuggestionProvider>)provider {
|
| + // TODO(ios): crbug.com/249916. If we can also pass in the form/field for
|
| + // which |sugguestions| 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.
|
| + // OnQueryFormFieldAutofill) and this method being called back. Results are
|
| + // therefore no longer relevant.
|
| + return;
|
| + }
|
| +
|
| + _provider = provider;
|
| + _suggestionState->suggestions.reset([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.reset([[NSArray alloc] init]);
|
| + [self updateKeyboard:_suggestionState.get()];
|
| +}
|
| +
|
| +- (void)updateKeyboard:(AutofillSuggestionState*)suggestionState {
|
| + if (!_suggestionState) {
|
| + if (completionHandler_)
|
| + completionHandler_.get()(nil, self);
|
| + } else {
|
| + [self updateKeyboardWithSuggestions:_suggestionState->suggestions];
|
| + }
|
| +}
|
| +
|
| +- (void)updateKeyboardWithSuggestions:(NSArray*)suggestions {
|
| + if (completionHandler_)
|
| + completionHandler_.get()([self suggestionViewWithSuggestions:suggestions],
|
| + self);
|
| +}
|
| +
|
| +- (UIView*)suggestionViewWithSuggestions:(NSArray*)suggestions {
|
| + base::scoped_nsobject<FormSuggestionView> view([[FormSuggestionView alloc]
|
| + initWithFrame:[_webViewProxy getKeyboardAccessory].frame
|
| + client:self
|
| + suggestions:suggestions]);
|
| + return view.autorelease();
|
| +}
|
| +
|
| +- (void)didSelectSuggestion:(FormSuggestion*)suggestion {
|
| + if (!_suggestionState)
|
| + return;
|
| +
|
| + // Send the suggestion to the provider and advance the cursor.
|
| + base::WeakNSObject<FormSuggestionController> weakSelf(self);
|
| + [_provider
|
| + didSelectSuggestion:suggestion
|
| + forField:base::SysUTF8ToNSString(_suggestionState->field_name)
|
| + form:base::SysUTF8ToNSString(_suggestionState->form_name)
|
| + completionHandler:^{
|
| + [[weakSelf accessoryViewDelegate] selectNextElement];
|
| + }];
|
| + _provider = nil;
|
| +}
|
| +
|
| +- (id<FormInputAccessoryViewProvider>)accessoryViewProvider {
|
| + return self;
|
| +}
|
| +
|
| +#pragma mark FormInputAccessoryViewProvider
|
| +
|
| +- (id<FormInputAccessoryViewDelegate>)accessoryViewDelegate {
|
| + return _delegate.get();
|
| +}
|
| +
|
| +- (void)setAccessoryViewDelegate:(id<FormInputAccessoryViewDelegate>)delegate {
|
| + _delegate.reset(delegate);
|
| +}
|
| +
|
| +- (void)checkIfAccessoryViewAvailableForFormNamed:(const std::string&)formName
|
| + fieldName:(const std::string&)fieldName
|
| + webState:(web::WebState*)webState
|
| + completionHandler:
|
| + (AccessoryViewAvailableCompletion)
|
| + completionHandler {
|
| + [self processPage:webState];
|
| + completionHandler(YES);
|
| +}
|
| +
|
| +- (void)retrieveAccessoryViewForFormNamed:(const std::string&)formName
|
| + fieldName:(const std::string&)fieldName
|
| + value:(const std::string&)value
|
| + type:(const std::string&)type
|
| + webState:(web::WebState*)webState
|
| + completionHandler:
|
| + (AccessoryViewReadyCompletion)completionHandler {
|
| + _suggestionState.reset(
|
| + new AutofillSuggestionState(formName, fieldName, value));
|
| + completionHandler([self suggestionViewWithSuggestions:@[]], self);
|
| + completionHandler_.reset([completionHandler copy]);
|
| + [self retrieveSuggestionsForFormNamed:formName
|
| + fieldName:fieldName
|
| + type:type
|
| + webState:webState];
|
| +}
|
| +
|
| +- (void)inputAccessoryViewControllerDidReset:
|
| + (FormInputAccessoryViewController*)controller {
|
| + completionHandler_.reset();
|
| + [self resetSuggestionState];
|
| +}
|
| +
|
| +- (void)resizeAccessoryView {
|
| + [self updateKeyboard:_suggestionState.get()];
|
| +}
|
| +
|
| +@end
|
|
|