Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ios / chrome / browser / autofill / form_suggestion_controller.mm
blobf2457601a034d42408e1d4720da9e1a859a6c935
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "ios/chrome/browser/autofill/form_suggestion_controller.h"
7 #include "base/ios/ios_util.h"
8 #include "base/ios/weak_nsobject.h"
9 #include "base/mac/foundation_util.h"
10 #include "base/mac/scoped_block.h"
11 #include "base/mac/scoped_nsobject.h"
12 #include "base/memory/scoped_ptr.h"
13 #include "base/strings/sys_string_conversions.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "components/autofill/core/browser/autofill_popup_delegate.h"
16 #include "components/autofill/ios/browser/autofill_field_trial_ios.h"
17 #import "components/autofill/ios/browser/form_suggestion.h"
18 #import "ios/chrome/browser/autofill/form_input_accessory_view_controller.h"
19 #import "ios/chrome/browser/autofill/form_suggestion_provider.h"
20 #import "ios/chrome/browser/autofill/form_suggestion_view.h"
21 #import "ios/chrome/browser/passwords/password_generation_utils.h"
22 #include "ios/chrome/browser/ui/ui_util.h"
23 #include "ios/web/public/url_scheme_util.h"
24 #import "ios/web/public/web_state/crw_web_view_proxy.h"
25 #import "ios/web/public/web_state/js/crw_js_injection_receiver.h"
26 #import "ios/web/public/web_state/web_state.h"
28 namespace {
30 // Struct that describes suggestion state.
31 struct AutofillSuggestionState {
32   AutofillSuggestionState(const std::string& form_name,
33                           const std::string& field_name,
34                           const std::string& typed_value);
35   // The name of the form for autofill.
36   std::string form_name;
37   // The name of the field for autofill.
38   std::string field_name;
39   // The user-typed value in the field.
40   std::string typed_value;
41   // The suggestions for the form field. An array of |FormSuggestion|.
42   base::scoped_nsobject<NSArray> suggestions;
45 AutofillSuggestionState::AutofillSuggestionState(const std::string& form_name,
46                                                  const std::string& field_name,
47                                                  const std::string& typed_value)
48     : form_name(form_name), field_name(field_name), typed_value(typed_value) {
51 }  // namespace
53 @interface FormSuggestionController () <FormInputAccessoryViewProvider> {
54   // Form navigation delegate.
55   base::WeakNSProtocol<id<FormInputAccessoryViewDelegate>> _delegate;
57   // Callback to update the accessory view.
58   base::mac::ScopedBlock<AccessoryViewReadyCompletion>
59       accessoryViewUpdateBlock_;
61   // Autofill suggestion state.
62   scoped_ptr<AutofillSuggestionState> _suggestionState;
64   // Providers for suggestions, sorted according to the order in which
65   // they should be asked for suggestions, with highest priority in front.
66   base::scoped_nsobject<NSArray> _suggestionProviders;
68   // Access to WebView from the CRWWebController.
69   base::scoped_nsprotocol<id<CRWWebViewProxy>> _webViewProxy;
72 // Returns an autoreleased input accessory view that shows |suggestions|.
73 - (UIView*)suggestionViewWithSuggestions:(NSArray*)suggestions;
75 // Updates keyboard for |suggestionState|.
76 - (void)updateKeyboard:(AutofillSuggestionState*)suggestionState;
78 // Updates keyboard with |suggestions|.
79 - (void)updateKeyboardWithSuggestions:(NSArray*)suggestions;
81 // Clears state in between page loads.
82 - (void)resetSuggestionState;
84 // Finds a FormSuggestionProvider that can supply suggestions for the specified
85 // form, requests them, and updates the view accordingly.
86 - (void)retrieveSuggestionsForFormNamed:(const std::string&)formName
87                               fieldName:(const std::string&)fieldName
88                                    type:(const std::string&)type
89                                webState:(web::WebState*)webState;
91 @end
93 @implementation FormSuggestionController {
94   // Bridge to observe the web state from Objective-C.
95   scoped_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
97   // Manager for FormSuggestion JavaScripts.
98   base::scoped_nsobject<JsSuggestionManager> _jsSuggestionManager;
100   // The provider for the current set of suggestions.
101   __weak id<FormSuggestionProvider> _provider;
104 - (instancetype)initWithWebState:(web::WebState*)webState
105                        providers:(NSArray*)providers
106              JsSuggestionManager:(JsSuggestionManager*)jsSuggestionManager {
107   self = [super init];
108   if (self) {
109     _webStateObserverBridge.reset(
110         new web::WebStateObserverBridge(webState, self));
111     _webViewProxy.reset([webState->GetWebViewProxy() retain]);
112     _jsSuggestionManager.reset([jsSuggestionManager retain]);
113     _suggestionProviders.reset([providers copy]);
114   }
115   return self;
118 - (instancetype)initWithWebState:(web::WebState*)webState
119                        providers:(NSArray*)providers {
120   JsSuggestionManager* jsSuggestionManager =
121       base::mac::ObjCCast<JsSuggestionManager>(
122           [webState->GetJSInjectionReceiver()
123               instanceOfClass:[JsSuggestionManager class]]);
124   return [self initWithWebState:webState
125                       providers:providers
126             JsSuggestionManager:jsSuggestionManager];
129 - (void)detachFromWebState {
130   _webStateObserverBridge.reset();
133 #pragma mark -
134 #pragma mark CRWWebStateObserver
136 - (void)webStateDestroyed:(web::WebState*)webState {
137   [self detachFromWebState];
140 - (void)webStateDidLoadPage:(web::WebState*)webState {
141   [self processPage:webState];
144 - (void)processPage:(web::WebState*)webState {
145   [self resetSuggestionState];
147   web::URLVerificationTrustLevel trustLevel =
148       web::URLVerificationTrustLevel::kNone;
149   const GURL pageURL(webState->GetCurrentURL(&trustLevel));
150   if (trustLevel != web::URLVerificationTrustLevel::kAbsolute) {
151     DLOG(WARNING) << "Page load not handled on untrusted page";
152     return;
153   }
155   if (web::UrlHasWebScheme(pageURL) && webState->ContentIsHTML())
156     [_jsSuggestionManager inject];
159 - (void)setWebViewProxy:(id<CRWWebViewProxy>)webViewProxy {
160   _webViewProxy.reset([webViewProxy retain]);
163 - (void)retrieveSuggestionsForFormNamed:(const std::string&)formName
164                               fieldName:(const std::string&)fieldName
165                                    type:(const std::string&)type
166                                webState:(web::WebState*)webState {
167   base::WeakNSObject<FormSuggestionController> weakSelf(self);
168   base::scoped_nsobject<NSString> strongFormName(
169       [base::SysUTF8ToNSString(formName) copy]);
170   base::scoped_nsobject<NSString> strongFieldName(
171       [base::SysUTF8ToNSString(fieldName) copy]);
172   base::scoped_nsobject<NSString> strongType(
173       [base::SysUTF8ToNSString(type) copy]);
174   base::scoped_nsobject<NSString> strongValue(
175       [base::SysUTF8ToNSString(_suggestionState.get()->typed_value) copy]);
177   // Build a block for each provider that will invoke its completion with YES
178   // if the provider can provide suggestions for the specified form/field/type
179   // and NO otherwise.
180   base::scoped_nsobject<NSMutableArray> findProviderBlocks(
181       [[NSMutableArray alloc] init]);
182   for (NSUInteger i = 0; i < [_suggestionProviders count]; i++) {
183     base::mac::ScopedBlock<passwords::PipelineBlock> block(
184         ^(void (^completion)(BOOL success)) {
185           // Access all the providers through |self| to guarantee that both
186           // |self| and all the providers exist when the block is executed.
187           // |_suggestionProviders| is immutable, so the subscripting is
188           // always valid.
189           base::scoped_nsobject<FormSuggestionController> strongSelf(
190               [weakSelf retain]);
191           if (!strongSelf)
192             return;
193           id<FormSuggestionProvider> provider =
194               strongSelf.get()->_suggestionProviders[i];
195           [provider checkIfSuggestionsAvailableForForm:strongFormName
196                                                  field:strongFieldName
197                                                   type:strongType
198                                             typedValue:strongValue
199                                               webState:webState
200                                      completionHandler:completion];
201         },
202         base::scoped_policy::RETAIN);
203     [findProviderBlocks addObject:block];
204   }
206   // Once the suggestions are retrieved, update the suggestions UI.
207   SuggestionsReadyCompletion readyCompletion =
208       ^(NSArray* suggestions, id<FormSuggestionProvider> provider) {
209         [weakSelf onSuggestionsReady:suggestions provider:provider];
210       };
212   // Once a provider is found, use it to retrieve suggestions.
213   passwords::PipelineCompletionBlock completion = ^(NSUInteger providerIndex) {
214     if (providerIndex == NSNotFound)
215       return;
216     base::scoped_nsobject<FormSuggestionController> strongSelf(
217         [weakSelf retain]);
218     if (!strongSelf)
219       return;
220     id<FormSuggestionProvider> provider =
221         strongSelf.get()->_suggestionProviders[providerIndex];
222     [provider retrieveSuggestionsForForm:strongFormName
223                                    field:strongFieldName
224                                     type:strongType
225                               typedValue:strongValue
226                                 webState:webState
227                        completionHandler:readyCompletion];
228   };
230   // Run all the blocks in |findProviderBlocks| until one invokes its
231   // completion with YES. The first one to do so will be passed to
232   // |onProviderFound|.
233   passwords::RunSearchPipeline(findProviderBlocks, completion);
236 - (void)onSuggestionsReady:(NSArray*)suggestions
237                   provider:(id<FormSuggestionProvider>)provider {
238   // TODO(ios): crbug.com/249916. If we can also pass in the form/field for
239   // which |suggestions| are, we should check here if |suggestions| are for
240   // the current active element. If not, reset |_suggestionState|.
241   if (!_suggestionState) {
242     // The suggestion state was reset in between the call to Autofill API (e.g.
243     // OnQueryFormFieldAutofill) and this method being called back. Results are
244     // therefore no longer relevant.
245     return;
246   }
248   _provider = provider;
249   _suggestionState->suggestions.reset([suggestions copy]);
250   [self updateKeyboard:_suggestionState.get()];
253 - (void)resetSuggestionState {
254   _provider = nil;
255   _suggestionState.reset();
258 - (void)clearSuggestions {
259   // Note that other parts of the suggestionsState are not reset.
260   if (!_suggestionState.get())
261     return;
262   _suggestionState->suggestions.reset([[NSArray alloc] init]);
263   [self updateKeyboard:_suggestionState.get()];
266 - (void)updateKeyboard:(AutofillSuggestionState*)suggestionState {
267   if (!suggestionState) {
268     if (accessoryViewUpdateBlock_)
269       accessoryViewUpdateBlock_.get()(nil, self);
270   } else {
271     [self updateKeyboardWithSuggestions:suggestionState->suggestions];
272   }
275 - (void)updateKeyboardWithSuggestions:(NSArray*)suggestions {
276   if (accessoryViewUpdateBlock_) {
277     accessoryViewUpdateBlock_.get()(
278         [self suggestionViewWithSuggestions:suggestions], self);
279   }
282 - (UIView*)suggestionViewWithSuggestions:(NSArray*)suggestions {
283   CGRect frame = [_webViewProxy keyboardAccessory].frame;
284   // Force the desired height on iPads running iOS 9 or later where the height
285   // of the inputAccessoryView is 0.
286   if (base::ios::IsRunningOnIOS9OrLater() && IsIPadIdiom()) {
287     frame.size.height = autofill::kInputAccessoryHeight;
288   }
289   base::scoped_nsobject<FormSuggestionView> view([[FormSuggestionView alloc]
290       initWithFrame:frame
291              client:self
292         suggestions:suggestions]);
293   return view.autorelease();
296 - (void)didSelectSuggestion:(FormSuggestion*)suggestion {
297   if (!_suggestionState)
298     return;
300   // Send the suggestion to the provider. Upon completion advance the cursor
301   // for single-field Autofill, or close the keyboard for full-form Autofill.
302   base::WeakNSObject<FormSuggestionController> weakSelf(self);
303   [_provider
304       didSelectSuggestion:suggestion
305                  forField:base::SysUTF8ToNSString(_suggestionState->field_name)
306                      form:base::SysUTF8ToNSString(_suggestionState->form_name)
307         completionHandler:^{
308           if (autofill::AutofillFieldTrialIOS::IsFullFormAutofillEnabled()) {
309             [[weakSelf accessoryViewDelegate] closeKeyboardWithoutButtonPress];
310           } else {
311             [[weakSelf accessoryViewDelegate]
312                 selectNextElementWithoutButtonPress];
313           }
314         }];
315   _provider = nil;
318 - (id<FormInputAccessoryViewProvider>)accessoryViewProvider {
319   return self;
322 #pragma mark FormInputAccessoryViewProvider
324 - (id<FormInputAccessoryViewDelegate>)accessoryViewDelegate {
325   return _delegate.get();
328 - (void)setAccessoryViewDelegate:(id<FormInputAccessoryViewDelegate>)delegate {
329   _delegate.reset(delegate);
332 - (void)
333     checkIfAccessoryViewIsAvailableForFormNamed:(const std::string&)formName
334                                       fieldName:(const std::string&)fieldName
335                                        webState:(web::WebState*)webState
336                               completionHandler:
337                                   (AccessoryViewAvailableCompletion)
338                                       completionHandler {
339   [self processPage:webState];
340   completionHandler(YES);
343 - (void)retrieveAccessoryViewForFormNamed:(const std::string&)formName
344                                 fieldName:(const std::string&)fieldName
345                                     value:(const std::string&)value
346                                      type:(const std::string&)type
347                                  webState:(web::WebState*)webState
348                  accessoryViewUpdateBlock:
349                      (AccessoryViewReadyCompletion)accessoryViewUpdateBlock {
350   _suggestionState.reset(
351       new AutofillSuggestionState(formName, fieldName, value));
352   accessoryViewUpdateBlock([self suggestionViewWithSuggestions:@[]], self);
353   accessoryViewUpdateBlock_.reset([accessoryViewUpdateBlock copy]);
354   [self retrieveSuggestionsForFormNamed:formName
355                               fieldName:fieldName
356                                    type:type
357                                webState:webState];
360 - (void)inputAccessoryViewControllerDidReset:
361         (FormInputAccessoryViewController*)controller {
362   accessoryViewUpdateBlock_.reset();
363   [self resetSuggestionState];
366 - (void)resizeAccessoryView {
367   [self updateKeyboard:_suggestionState.get()];
370 - (BOOL)getLogKeyboardAccessoryMetrics {
371   return YES;
374 @end