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