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 |