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_input_accessory_view_controller.h"
7 #include "base/ios/block_types.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 #import "components/autofill/ios/browser/js_suggestion_manager.h"
15 #import "ios/chrome/browser/autofill/form_input_accessory_view.h"
16 #import "ios/chrome/browser/passwords/password_generation_utils.h"
17 #include "ios/web/public/test/crw_test_js_injection_receiver.h"
18 #include "ios/web/public/url_scheme_util.h"
19 #import "ios/web/public/web_state/crw_web_view_proxy.h"
20 #include "ios/web/public/web_state/url_verification_constants.h"
21 #include "ios/web/public/web_state/web_state.h"
25 NSString* const kFormSuggestionAssistButtonPreviousElement = @"previousTap";
26 NSString* const kFormSuggestionAssistButtonNextElement = @"nextTap";
27 NSString* const kFormSuggestionAssistButtonDone = @"done";
28 } // namespace autofill
32 // Finds all views of a particular kind if class |klass| in the subview
33 // hierarchy of the given |root| view.
34 NSArray* FindDescendantsOfClass(UIView* root, Class klass) {
36 NSMutableArray* viewsToExamine = [NSMutableArray arrayWithObject:root];
37 NSMutableArray* descendants = [NSMutableArray array];
39 while ([viewsToExamine count]) {
40 UIView* view = [viewsToExamine lastObject];
41 if ([view isKindOfClass:klass])
42 [descendants addObject:view];
44 [viewsToExamine removeLastObject];
45 [viewsToExamine addObjectsFromArray:[view subviews]];
51 // Finds all UIToolbarItems associated with a given UIToolbar |toolbar| with
52 // action selectors with a name that containts the action name specified by
54 NSArray* FindToolbarItemsForActionName(UIToolbar* toolbar,
55 NSString* actionName) {
56 NSMutableArray* toolbarItems = [NSMutableArray array];
58 for (UIBarButtonItem* item in [toolbar items]) {
59 SEL itemAction = [item action];
62 NSString* itemActionName = NSStringFromSelector(itemAction);
64 // We don't do a strict string match for the action name.
65 if ([itemActionName rangeOfString:actionName].location != NSNotFound)
66 [toolbarItems addObject:item];
72 // Finds all UIToolbarItem(s) with action selectors of the name specified by
73 // |actionName| in any UIToolbars in the view hierarchy below |root|.
74 NSArray* FindDescendantToolbarItemsForActionName(UIView* root,
75 NSString* actionName) {
76 NSMutableArray* descendants = [NSMutableArray array];
78 NSArray* toolbars = FindDescendantsOfClass(root, [UIToolbar class]);
79 for (UIToolbar* toolbar in toolbars) {
81 addObjectsFromArray:FindToolbarItemsForActionName(toolbar, actionName)];
87 // Computes the frame of each part of the accessory view of the keyboard. It is
88 // assumed that the keyboard has either two parts (when it is split) or one part
89 // (when it is merged).
91 // If there are two parts, the frame of the left part is returned in
92 // |leftFrame| and the frame of the right part is returned in |rightFrame|.
93 // If there is only one part, the frame is returned in |leftFrame| and
94 // |rightFrame| has size zero.
96 // Heuristics are used to compute this information. It returns true if the
97 // number of |inputAccessoryView.subviews| is not 2.
98 bool ComputeFramesOfKeyboardParts(UIView* inputAccessoryView,
100 CGRect* rightFrame) {
101 // It is observed (on iOS 6) there are always two subviews in the original
102 // input accessory view. When the keyboard is split, each subview represents
103 // one part of the accesssary view of the keyboard. When the keyboard is
104 // merged, one subview has the same frame as that of the whole accessory view
105 // and the other has zero size with the screen width as origin.x.
106 // The computation here is based on this observation.
107 NSArray* subviews = inputAccessoryView.subviews;
108 if (subviews.count != 2)
111 CGRect first_frame = static_cast<UIView*>(subviews[0]).frame;
112 CGRect second_frame = static_cast<UIView*>(subviews[1]).frame;
113 if (CGRectGetMinX(first_frame) < CGRectGetMinX(second_frame) ||
114 CGRectGetWidth(second_frame) == 0) {
115 *leftFrame = first_frame;
116 *rightFrame = second_frame;
118 *rightFrame = first_frame;
119 *leftFrame = second_frame;
126 @interface FormInputAccessoryViewController ()
128 // Allows injection of the JsSuggestionManager.
129 - (instancetype)initWithWebState:(web::WebState*)webState
130 JSSuggestionManager:(JsSuggestionManager*)JSSuggestionManager
131 providers:(NSArray*)providers;
133 // Called when the keyboard did change frame.
134 - (void)keyboardDidChangeFrame:(NSNotification*)notification;
136 // Called when the keyboard is dismissed.
137 - (void)keyboardDidHide:(NSNotification*)notification;
139 // Hides the subviews in |accessoryView|.
140 - (void)hideSubviewsInOriginalAccessoryView:(UIView*)accessoryView;
142 // Attempts to execute/tap/send-an-event-to the iOS built-in "next" and
143 // "previous" form assist controls. Returns NO if this attempt failed, YES
145 - (BOOL)executeFormAssistAction:(NSString*)actionName;
147 // Runs |block| while allowing the keyboard to be displayed as a result of focus
148 // changes caused by |block|.
149 - (void)runBlockAllowingKeyboardDisplay:(ProceduralBlock)block;
151 // Asynchronously retrieves an accessory view from |_providers|.
152 - (void)retrieveAccessoryViewForForm:(const std::string&)formName
153 field:(const std::string&)fieldName
154 value:(const std::string&)value
155 type:(const std::string&)type
156 webState:(web::WebState*)webState;
158 // Clears the current custom accessory view and restores the default.
161 // The current web state.
162 @property(nonatomic, readonly) web::WebState* webState;
164 // The current web view proxy.
165 @property(nonatomic, readonly) id<CRWWebViewProxy> webViewProxy;
169 @implementation FormInputAccessoryViewController {
170 // Bridge to observe the web state from Objective-C.
171 scoped_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
173 // Last registered keyboard rectangle.
174 CGRect _keyboardFrame;
176 // The custom view that should be shown in the input accessory view.
177 base::scoped_nsobject<UIView> _customAccessoryView;
179 // The JS manager for interacting with the underlying form.
180 base::scoped_nsobject<JsSuggestionManager> _JSSuggestionManager;
182 // The original subviews in keyboard accessory view that were originally not
183 // hidden but were hidden when showing Autofill suggestions.
184 base::scoped_nsobject<NSMutableArray> _hiddenOriginalSubviews;
186 // The objects that can provide a custom input accessory view while filling
188 base::scoped_nsobject<NSArray> _providers;
190 // The object that manages the currently-shown custom accessory view.
191 base::WeakNSProtocol<id<FormInputAccessoryViewProvider>> _currentProvider;
194 - (instancetype)initWithWebState:(web::WebState*)webState
195 providers:(NSArray*)providers {
196 JsSuggestionManager* suggestionManager =
197 base::mac::ObjCCastStrict<JsSuggestionManager>(
198 [webState->GetJSInjectionReceiver()
199 instanceOfClass:[JsSuggestionManager class]]);
200 return [self initWithWebState:webState
201 JSSuggestionManager:suggestionManager
202 providers:providers];
205 - (instancetype)initWithWebState:(web::WebState*)webState
206 JSSuggestionManager:(JsSuggestionManager*)JSSuggestionManager
207 providers:(NSArray*)providers {
210 _JSSuggestionManager.reset([JSSuggestionManager retain]);
211 _hiddenOriginalSubviews.reset([[NSMutableArray alloc] init]);
212 _webStateObserverBridge.reset(
213 new web::WebStateObserverBridge(webState, self));
214 _providers.reset([providers copy]);
215 // There is no defined relation on the timing of JavaScript events and
216 // keyboard showing up. So it is necessary to listen to the keyboard
217 // notification to make sure the keyboard is updated.
218 [[NSNotificationCenter defaultCenter]
220 selector:@selector(keyboardDidChangeFrame:)
221 name:UIKeyboardDidChangeFrameNotification
223 [[NSNotificationCenter defaultCenter]
225 selector:@selector(keyboardDidHide:)
226 name:UIKeyboardDidHideNotification
233 [[NSNotificationCenter defaultCenter] removeObserver:self];
237 - (web::WebState*)webState {
238 return _webStateObserverBridge ? _webStateObserverBridge->web_state()
242 - (id<CRWWebViewProxy>)webViewProxy {
243 return self.webState ? self.webState->GetWebViewProxy() : nil;
246 - (void)hideSubviewsInOriginalAccessoryView:(UIView*)accessoryView {
247 for (UIView* subview in [accessoryView subviews]) {
248 if (!subview.hidden) {
249 [_hiddenOriginalSubviews addObject:subview];
250 subview.hidden = YES;
255 - (void)showCustomInputAccessoryView:(UIView*)view {
256 [self restoreDefaultInputAccessoryView];
259 UIView* inputAccessoryView = [self.webViewProxy getKeyboardAccessory];
260 if (ComputeFramesOfKeyboardParts(inputAccessoryView, &leftFrame,
262 [self hideSubviewsInOriginalAccessoryView:inputAccessoryView];
263 _customAccessoryView.reset(
264 [[FormInputAccessoryView alloc] initWithFrame:inputAccessoryView.frame
268 rightFrame:rightFrame]);
269 [inputAccessoryView addSubview:_customAccessoryView];
273 - (void)restoreDefaultInputAccessoryView {
274 [_customAccessoryView removeFromSuperview];
275 _customAccessoryView.reset();
276 for (UIView* subview in _hiddenOriginalSubviews.get()) {
279 [_hiddenOriginalSubviews removeAllObjects];
282 - (void)closeKeyboard {
283 BOOL performedAction =
284 [self executeFormAssistAction:autofill::kFormSuggestionAssistButtonDone];
286 if (!performedAction) {
287 // We could not find the built-in form assist controls, so try to focus
288 // the next or previous control using JavaScript.
289 [self runBlockAllowingKeyboardDisplay:^{
290 [_JSSuggestionManager closeKeyboard];
295 - (BOOL)executeFormAssistAction:(NSString*)actionName {
296 UIView* inputAccessoryView = [self.webViewProxy getKeyboardAccessory];
297 if (!inputAccessoryView)
300 NSArray* descendants =
301 FindDescendantToolbarItemsForActionName(inputAccessoryView, actionName);
303 if (![descendants count])
306 UIBarButtonItem* item = descendants[0];
307 [[item target] performSelector:[item action] withObject:item];
311 - (void)runBlockAllowingKeyboardDisplay:(ProceduralBlock)block {
313 instancesRespondToSelector:@selector(keyboardDisplayRequiresUserAction)]);
315 BOOL originalValue = [self.webViewProxy keyboardDisplayRequiresUserAction];
316 [self.webViewProxy setKeyboardDisplayRequiresUserAction:NO];
318 [self.webViewProxy setKeyboardDisplayRequiresUserAction:originalValue];
322 #pragma mark FormInputAccessoryViewDelegate
324 - (void)selectPreviousElement {
325 BOOL performedAction =
326 [self executeFormAssistAction:
327 autofill::kFormSuggestionAssistButtonPreviousElement];
328 if (!performedAction) {
329 // We could not find the built-in form assist controls, so try to focus
330 // the next or previous control using JavaScript.
331 [self runBlockAllowingKeyboardDisplay:^{
332 [_JSSuggestionManager selectPreviousElement];
337 - (void)selectNextElement {
338 BOOL performedAction = [self
339 executeFormAssistAction:autofill::kFormSuggestionAssistButtonNextElement];
341 if (!performedAction) {
342 // We could not find the built-in form assist controls, so try to focus
343 // the next or previous control using JavaScript.
344 [self runBlockAllowingKeyboardDisplay:^{
345 [_JSSuggestionManager selectNextElement];
350 - (void)fetchPreviousAndNextElementsPresenceWithCompletionHandler:
351 (void (^)(BOOL, BOOL))completionHandler {
352 DCHECK(completionHandler);
353 [_JSSuggestionManager
354 fetchPreviousAndNextElementsPresenceWithCompletionHandler:
359 #pragma mark CRWWebStateObserver
361 - (void)webStateDidLoadPage:(web::WebState*)webState {
365 - (void)webState:(web::WebState*)webState
366 didRegisterFormActivityWithFormNamed:(const std::string&)formName
367 fieldName:(const std::string&)fieldName
368 type:(const std::string&)type
369 value:(const std::string&)value
371 inputMissing:(BOOL)inputMissing {
372 web::URLVerificationTrustLevel trustLevel;
373 const GURL pageURL(webState->GetCurrentURL(&trustLevel));
374 if (inputMissing || trustLevel != web::URLVerificationTrustLevel::kAbsolute ||
375 !web::UrlHasWebScheme(pageURL) || !webState->ContentIsHTML()) {
380 if ((type == "blur" || type == "change")) {
384 [self retrieveAccessoryViewForForm:formName
391 - (void)webStateDestroyed:(web::WebState*)webState {
393 _webStateObserverBridge.reset();
397 if (_currentProvider) {
398 [_currentProvider inputAccessoryViewControllerDidReset:self];
399 _currentProvider.reset();
401 [self restoreDefaultInputAccessoryView];
404 - (void)retrieveAccessoryViewForForm:(const std::string&)formName
405 field:(const std::string&)fieldName
406 value:(const std::string&)value
407 type:(const std::string&)type
408 webState:(web::WebState*)webState {
409 base::WeakNSObject<FormInputAccessoryViewController> weakSelf(self);
410 std::string strongFormName = formName;
411 std::string strongFieldName = fieldName;
412 std::string strongValue = value;
413 std::string strongType = type;
415 // Build a block for each provider that will invoke its completion with YES
416 // if the provider can provide an accessory view for the specified form/field
418 base::scoped_nsobject<NSMutableArray> findProviderBlocks(
419 [[NSMutableArray alloc] init]);
420 for (NSUInteger i = 0; i < [_providers count]; i++) {
421 base::mac::ScopedBlock<passwords::PipelineBlock> block(
422 ^(void (^completion)(BOOL success)) {
423 // Access all the providers through |self| to guarantee that both
424 // |self| and all the providers exist when the block is executed.
425 // |_providers| is immutable, so the subscripting is always valid.
426 base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
430 id<FormInputAccessoryViewProvider> provider =
431 strongSelf.get()->_providers[i];
432 [provider checkIfAccessoryViewIsAvailableForFormNamed:strongFormName
433 fieldName:strongFieldName
435 completionHandler:completion];
437 base::scoped_policy::RETAIN);
438 [findProviderBlocks addObject:block];
441 // Once the view is retrieved, update the UI.
442 AccessoryViewReadyCompletion readyCompletion =
443 ^(UIView* accessoryView, id<FormInputAccessoryViewProvider> provider) {
444 base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
446 if (!strongSelf || !strongSelf.get()->_currentProvider)
448 DCHECK_EQ(strongSelf.get()->_currentProvider.get(), provider);
449 [provider setAccessoryViewDelegate:strongSelf];
450 [strongSelf showCustomInputAccessoryView:accessoryView];
453 // Once a provider is found, use it to retrieve the accessory view.
454 passwords::PipelineCompletionBlock onProviderFound =
455 ^(NSUInteger providerIndex) {
456 if (providerIndex == NSNotFound) {
460 base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
462 if (!strongSelf || ![strongSelf webState])
464 id<FormInputAccessoryViewProvider> provider =
465 strongSelf.get()->_providers[providerIndex];
466 [strongSelf.get()->_currentProvider
467 inputAccessoryViewControllerDidReset:self];
468 strongSelf.get()->_currentProvider.reset(provider);
469 [strongSelf.get()->_currentProvider
470 retrieveAccessoryViewForFormNamed:strongFormName
471 fieldName:strongFieldName
475 accessoryViewUpdateBlock:readyCompletion];
478 // Run all the blocks in |findProviderBlocks| until one invokes its
479 // completion with YES. The first one to do so will be passed to
480 // |onProviderFound|.
481 passwords::RunSearchPipeline(findProviderBlocks, onProviderFound);
484 - (void)keyboardDidChangeFrame:(NSNotification*)notification {
485 if (!self.webState || !_currentProvider)
487 CGRect keyboardFrame =
488 [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
489 // With iOS8 (beta) this method can be called even when the rect has not
490 // changed. When this is detected we exit early.
491 if (CGRectEqualToRect(CGRectIntegral(_keyboardFrame),
492 CGRectIntegral(keyboardFrame))) {
495 _keyboardFrame = keyboardFrame;
496 [_currentProvider resizeAccessoryView];
499 - (void)keyboardDidHide:(NSNotification*)notification {
500 _keyboardFrame = CGRectZero;