Upstreaming browser/ui/uikit_ui_util from iOS.
[chromium-blink-merge.git] / ios / chrome / browser / autofill / form_suggestion_controller.mm
blob317f6af71713c98c5a46827a50af26a8e8f25866
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/weak_nsobject.h"
8 #include "base/mac/foundation_util.h"
9 #include "base/mac/scoped_block.h"
10 #include "base/mac/scoped_nsobject.h"
11 #include "base/memory/scoped_ptr.h"
12 #include "base/strings/sys_string_conversions.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "components/autofill/core/browser/autofill_popup_delegate.h"
15 #include "components/autofill/ios/browser/autofill_field_trial_ios.h"
16 #import "components/autofill/ios/browser/form_suggestion.h"
17 #import "ios/chrome/browser/autofill/form_input_accessory_view_controller.h"
18 #import "ios/chrome/browser/autofill/form_suggestion_provider.h"
19 #import "ios/chrome/browser/autofill/form_suggestion_view.h"
20 #import "ios/chrome/browser/passwords/password_generation_utils.h"
21 #include "ios/web/public/url_scheme_util.h"
22 #import "ios/web/public/web_state/crw_web_view_proxy.h"
23 #import "ios/web/public/web_state/js/crw_js_injection_receiver.h"
24 #import "ios/web/public/web_state/web_state.h"
26 namespace {
28 // Struct that describes suggestion state.
29 struct AutofillSuggestionState {
30   AutofillSuggestionState(const std::string& form_name,
31                           const std::string& field_name,
32                           const std::string& typed_value);
33   // The name of the form for autofill.
34   std::string form_name;
35   // The name of the field for autofill.
36   std::string field_name;
37   // The user-typed value in the field.
38   std::string typed_value;
39   // The suggestions for the form field. An array of |FormSuggestion|.
40   base::scoped_nsobject<NSArray> suggestions;
43 AutofillSuggestionState::AutofillSuggestionState(const std::string& form_name,
44                                                  const std::string& field_name,
45                                                  const std::string& typed_value)
46     : form_name(form_name), field_name(field_name), typed_value(typed_value) {
49 }  // namespace
51 @interface FormSuggestionController () <FormInputAccessoryViewProvider> {
52   // Form navigation delegate.
53   base::WeakNSProtocol<id<FormInputAccessoryViewDelegate>> _delegate;
55   // Callback to update the accessory view.
56   base::mac::ScopedBlock<AccessoryViewReadyCompletion>
57       accessoryViewUpdateBlock_;
59   // Autofill suggestion state.
60   scoped_ptr<AutofillSuggestionState> _suggestionState;
62   // Providers for suggestions, sorted according to the order in which
63   // they should be asked for suggestions, with highest priority in front.
64   base::scoped_nsobject<NSArray> _suggestionProviders;
66   // Access to WebView from the CRWWebController.
67   base::scoped_nsprotocol<id<CRWWebViewProxy>> _webViewProxy;
70 // Returns an autoreleased input accessory view that shows |suggestions|.
71 - (UIView*)suggestionViewWithSuggestions:(NSArray*)suggestions;
73 // Updates keyboard for |suggestionState|.
74 - (void)updateKeyboard:(AutofillSuggestionState*)suggestionState;
76 // Updates keyboard with |suggestions|.
77 - (void)updateKeyboardWithSuggestions:(NSArray*)suggestions;
79 // Clears state in between page loads.
80 - (void)resetSuggestionState;
82 // Finds a FormSuggestionProvider that can supply suggestions for the specified
83 // form, requests them, and updates the view accordingly.
84 - (void)retrieveSuggestionsForFormNamed:(const std::string&)formName
85                               fieldName:(const std::string&)fieldName
86                                    type:(const std::string&)type
87                                webState:(web::WebState*)webState;
89 @end
91 @implementation FormSuggestionController {
92   // Bridge to observe the web state from Objective-C.
93   scoped_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
95   // Manager for FormSuggestion JavaScripts.
96   base::scoped_nsobject<JsSuggestionManager> _jsSuggestionManager;
98   // The provider for the current set of suggestions.
99   __weak id<FormSuggestionProvider> _provider;
102 - (instancetype)initWithWebState:(web::WebState*)webState
103                        providers:(NSArray*)providers
104              JsSuggestionManager:(JsSuggestionManager*)jsSuggestionManager {
105   self = [super init];
106   if (self) {
107     _webStateObserverBridge.reset(
108         new web::WebStateObserverBridge(webState, self));
109     _webViewProxy.reset([webState->GetWebViewProxy() retain]);
110     _jsSuggestionManager.reset([jsSuggestionManager retain]);
111     _suggestionProviders.reset([providers copy]);
112   }
113   return self;
116 - (instancetype)initWithWebState:(web::WebState*)webState
117                        providers:(NSArray*)providers {
118   JsSuggestionManager* jsSuggestionManager =
119       base::mac::ObjCCast<JsSuggestionManager>(
120           [webState->GetJSInjectionReceiver()
121               instanceOfClass:[JsSuggestionManager class]]);
122   return [self initWithWebState:webState
123                       providers:providers
124             JsSuggestionManager:jsSuggestionManager];
127 - (void)detachFromWebState {
128   _webStateObserverBridge.reset();
131 #pragma mark -
132 #pragma mark CRWWebStateObserver
134 - (void)webStateDestroyed:(web::WebState*)webState {
135   [self detachFromWebState];
138 - (void)webStateDidLoadPage:(web::WebState*)webState {
139   [self processPage:webState];
142 - (void)processPage:(web::WebState*)webState {
143   [self resetSuggestionState];
145   web::URLVerificationTrustLevel trustLevel =
146       web::URLVerificationTrustLevel::kNone;
147   const GURL pageURL(webState->GetCurrentURL(&trustLevel));
148   if (trustLevel != web::URLVerificationTrustLevel::kAbsolute) {
149     DLOG(WARNING) << "Page load not handled on untrusted page";
150     return;
151   }
153   if (web::UrlHasWebScheme(pageURL) && webState->ContentIsHTML())
154     [_jsSuggestionManager inject];
157 - (void)setWebViewProxy:(id<CRWWebViewProxy>)webViewProxy {
158   _webViewProxy.reset([webViewProxy retain]);
161 - (void)retrieveSuggestionsForFormNamed:(const std::string&)formName
162                               fieldName:(const std::string&)fieldName
163                                    type:(const std::string&)type
164                                webState:(web::WebState*)webState {
165   base::WeakNSObject<FormSuggestionController> weakSelf(self);
166   base::scoped_nsobject<NSString> strongFormName(
167       [base::SysUTF8ToNSString(formName) copy]);
168   base::scoped_nsobject<NSString> strongFieldName(
169       [base::SysUTF8ToNSString(fieldName) copy]);
170   base::scoped_nsobject<NSString> strongType(
171       [base::SysUTF8ToNSString(type) copy]);
172   base::scoped_nsobject<NSString> strongValue(
173       [base::SysUTF8ToNSString(_suggestionState.get()->typed_value) copy]);
175   // Build a block for each provider that will invoke its completion with YES
176   // if the provider can provide suggestions for the specified form/field/type
177   // and NO otherwise.
178   base::scoped_nsobject<NSMutableArray> findProviderBlocks(
179       [[NSMutableArray alloc] init]);
180   for (NSUInteger i = 0; i < [_suggestionProviders count]; i++) {
181     base::mac::ScopedBlock<passwords::PipelineBlock> block(
182         ^(void (^completion)(BOOL success)) {
183           // Access all the providers through |self| to guarantee that both
184           // |self| and all the providers exist when the block is executed.
185           // |_suggestionProviders| is immutable, so the subscripting is
186           // always valid.
187           base::scoped_nsobject<FormSuggestionController> strongSelf(
188               [weakSelf retain]);
189           if (!strongSelf)
190             return;
191           id<FormSuggestionProvider> provider =
192               strongSelf.get()->_suggestionProviders[i];
193           [provider checkIfSuggestionsAvailableForForm:strongFormName
194                                                  field:strongFieldName
195                                                   type:strongType
196                                             typedValue:strongValue
197                                               webState:webState
198                                      completionHandler:completion];
199         },
200         base::scoped_policy::RETAIN);
201     [findProviderBlocks addObject:block];
202   }
204   // Once the suggestions are retrieved, update the suggestions UI.
205   SuggestionsReadyCompletion readyCompletion =
206       ^(NSArray* suggestions, id<FormSuggestionProvider> provider) {
207         [weakSelf onSuggestionsReady:suggestions provider:provider];
208       };
210   // Once a provider is found, use it to retrieve suggestions.
211   passwords::PipelineCompletionBlock completion = ^(NSUInteger providerIndex) {
212     if (providerIndex == NSNotFound)
213       return;
214     base::scoped_nsobject<FormSuggestionController> strongSelf(
215         [weakSelf retain]);
216     if (!strongSelf)
217       return;
218     id<FormSuggestionProvider> provider =
219         strongSelf.get()->_suggestionProviders[providerIndex];
220     [provider retrieveSuggestionsForForm:strongFormName
221                                    field:strongFieldName
222                                     type:strongType
223                               typedValue:strongValue
224                                 webState:webState
225                        completionHandler:readyCompletion];
226   };
228   // Run all the blocks in |findProviderBlocks| until one invokes its
229   // completion with YES. The first one to do so will be passed to
230   // |onProviderFound|.
231   passwords::RunSearchPipeline(findProviderBlocks, completion);
234 - (void)onSuggestionsReady:(NSArray*)suggestions
235                   provider:(id<FormSuggestionProvider>)provider {
236   // TODO(ios): crbug.com/249916. If we can also pass in the form/field for
237   // which |sugguestions| are, we should check here if |suggestions| are for
238   // the current active element. If not, reset |_suggestionState|.
239   if (!_suggestionState) {
240     // The suggestion state was reset in between the call to Autofill API (e.g.
241     // OnQueryFormFieldAutofill) and this method being called back. Results are
242     // therefore no longer relevant.
243     return;
244   }
246   _provider = provider;
247   _suggestionState->suggestions.reset([suggestions copy]);
248   [self updateKeyboard:_suggestionState.get()];
251 - (void)resetSuggestionState {
252   _provider = nil;
253   _suggestionState.reset();
256 - (void)clearSuggestions {
257   // Note that other parts of the suggestionsState are not reset.
258   if (!_suggestionState.get())
259     return;
260   _suggestionState->suggestions.reset([[NSArray alloc] init]);
261   [self updateKeyboard:_suggestionState.get()];
264 - (void)updateKeyboard:(AutofillSuggestionState*)suggestionState {
265   if (!_suggestionState) {
266     if (accessoryViewUpdateBlock_)
267       accessoryViewUpdateBlock_.get()(nil, self);
268   } else {
269     [self updateKeyboardWithSuggestions:_suggestionState->suggestions];
270   }
273 - (void)updateKeyboardWithSuggestions:(NSArray*)suggestions {
274   if (accessoryViewUpdateBlock_) {
275     accessoryViewUpdateBlock_.get()(
276         [self suggestionViewWithSuggestions:suggestions], self);
277   }
280 - (UIView*)suggestionViewWithSuggestions:(NSArray*)suggestions {
281   base::scoped_nsobject<FormSuggestionView> view([[FormSuggestionView alloc]
282       initWithFrame:[_webViewProxy getKeyboardAccessory].frame
283              client:self
284         suggestions:suggestions]);
285   return view.autorelease();
288 - (void)didSelectSuggestion:(FormSuggestion*)suggestion {
289   if (!_suggestionState)
290     return;
292   // Send the suggestion to the provider. Upon completion advance the cursor
293   // for single-field Autofill, or close the keyboard for full-form Autofill.
294   base::WeakNSObject<FormSuggestionController> weakSelf(self);
295   [_provider
296       didSelectSuggestion:suggestion
297                  forField:base::SysUTF8ToNSString(_suggestionState->field_name)
298                      form:base::SysUTF8ToNSString(_suggestionState->form_name)
299         completionHandler:^{
300           if (autofill::AutofillFieldTrialIOS::IsFullFormAutofillEnabled())
301             [[weakSelf accessoryViewDelegate] closeKeyboard];
302           else
303             [[weakSelf accessoryViewDelegate] selectNextElement];
304         }];
305   _provider = nil;
308 - (id<FormInputAccessoryViewProvider>)accessoryViewProvider {
309   return self;
312 #pragma mark FormInputAccessoryViewProvider
314 - (id<FormInputAccessoryViewDelegate>)accessoryViewDelegate {
315   return _delegate.get();
318 - (void)setAccessoryViewDelegate:(id<FormInputAccessoryViewDelegate>)delegate {
319   _delegate.reset(delegate);
322 - (void)
323     checkIfAccessoryViewIsAvailableForFormNamed:(const std::string&)formName
324                                       fieldName:(const std::string&)fieldName
325                                        webState:(web::WebState*)webState
326                               completionHandler:
327                                   (AccessoryViewAvailableCompletion)
328                                       completionHandler {
329   [self processPage:webState];
330   completionHandler(YES);
333 - (void)retrieveAccessoryViewForFormNamed:(const std::string&)formName
334                                 fieldName:(const std::string&)fieldName
335                                     value:(const std::string&)value
336                                      type:(const std::string&)type
337                                  webState:(web::WebState*)webState
338                  accessoryViewUpdateBlock:
339                      (AccessoryViewReadyCompletion)accessoryViewUpdateBlock {
340   _suggestionState.reset(
341       new AutofillSuggestionState(formName, fieldName, value));
342   accessoryViewUpdateBlock([self suggestionViewWithSuggestions:@[]], self);
343   accessoryViewUpdateBlock_.reset([accessoryViewUpdateBlock copy]);
344   [self retrieveSuggestionsForFormNamed:formName
345                               fieldName:fieldName
346                                    type:type
347                                webState:webState];
350 - (void)inputAccessoryViewControllerDidReset:
351         (FormInputAccessoryViewController*)controller {
352   accessoryViewUpdateBlock_.reset();
353   [self resetSuggestionState];
356 - (void)resizeAccessoryView {
357   [self updateKeyboard:_suggestionState.get()];
360 @end