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"
24 namespace ios_internal {
26 NSString* const kFormSuggestionAssistButtonPreviousElement = @"previousTap";
27 NSString* const kFormSuggestionAssistButtonNextElement = @"nextTap";
28 NSString* const kFormSuggestionAssistButtonDone = @"done";
29 } // namespace autofill
30 } // namespace ios_internal
34 // Finds all views of a particular kind if class |klass| in the subview
35 // hierarchy of the given |root| view.
36 NSArray* FindDescendantsOfClass(UIView* root, Class klass) {
38 NSMutableArray* viewsToExamine = [NSMutableArray arrayWithObject:root];
39 NSMutableArray* descendants = [NSMutableArray array];
41 while ([viewsToExamine count]) {
42 UIView* view = [viewsToExamine lastObject];
43 if ([view isKindOfClass:klass])
44 [descendants addObject:view];
46 [viewsToExamine removeLastObject];
47 [viewsToExamine addObjectsFromArray:[view subviews]];
53 // Finds all UIToolbarItems associated with a given UIToolbar |toolbar| with
54 // action selectors with a name that containts the action name specified by
56 NSArray* FindToolbarItemsForActionName(UIToolbar* toolbar,
57 NSString* actionName) {
58 NSMutableArray* toolbarItems = [NSMutableArray array];
60 for (UIBarButtonItem* item in [toolbar items]) {
61 SEL itemAction = [item action];
64 NSString* itemActionName = NSStringFromSelector(itemAction);
66 // We don't do a strict string match for the action name.
67 if ([itemActionName rangeOfString:actionName].location != NSNotFound)
68 [toolbarItems addObject:item];
74 // Finds all UIToolbarItem(s) with action selectors of the name specified by
75 // |actionName| in any UIToolbars in the view hierarchy below |root|.
76 NSArray* FindDescendantToolbarItemsForActionName(UIView* root,
77 NSString* actionName) {
78 NSMutableArray* descendants = [NSMutableArray array];
80 NSArray* toolbars = FindDescendantsOfClass(root, [UIToolbar class]);
81 for (UIToolbar* toolbar in toolbars) {
83 addObjectsFromArray:FindToolbarItemsForActionName(toolbar, actionName)];
89 // Computes the frame of each part of the accessory view of the keyboard. It is
90 // assumed that the keyboard has either two parts (when it is split) or one part
91 // (when it is merged).
93 // If there are two parts, the frame of the left part is returned in
94 // |leftFrame| and the frame of the right part is returned in |rightFrame|.
95 // If there is only one part, the frame is returned in |leftFrame| and
96 // |rightFrame| has size zero.
98 // Heuristics are used to compute this information. It returns true if the
99 // number of |inputAccessoryView.subviews| is not 2.
100 bool ComputeFramesOfKeyboardParts(UIView* inputAccessoryView,
102 CGRect* rightFrame) {
103 // It is observed (on iOS 6) there are always two subviews in the original
104 // input accessory view. When the keyboard is split, each subview represents
105 // one part of the accesssary view of the keyboard. When the keyboard is
106 // merged, one subview has the same frame as that of the whole accessory view
107 // and the other has zero size with the screen width as origin.x.
108 // The computation here is based on this observation.
109 NSArray* subviews = inputAccessoryView.subviews;
110 if (subviews.count != 2)
113 CGRect first_frame = static_cast<UIView*>(subviews[0]).frame;
114 CGRect second_frame = static_cast<UIView*>(subviews[1]).frame;
115 if (CGRectGetMinX(first_frame) < CGRectGetMinX(second_frame) ||
116 CGRectGetWidth(second_frame) == 0) {
117 *leftFrame = first_frame;
118 *rightFrame = second_frame;
120 *rightFrame = first_frame;
121 *leftFrame = second_frame;
128 @interface FormInputAccessoryViewController ()
130 // Allows injection of the JsSuggestionManager.
131 - (instancetype)initWithWebState:(web::WebState*)webState
132 JSSuggestionManager:(JsSuggestionManager*)JSSuggestionManager
133 providers:(NSArray*)providers;
135 // Called when the keyboard did change frame.
136 - (void)keyboardDidChangeFrame:(NSNotification*)notification;
138 // Called when the keyboard is dismissed.
139 - (void)keyboardDidHide:(NSNotification*)notification;
141 // Hides the subviews in |accessoryView|.
142 - (void)hideSubviewsInOriginalAccessoryView:(UIView*)accessoryView;
144 // Attempts to execute/tap/send-an-event-to the iOS built-in "next" and
145 // "previous" form assist controls. Returns NO if this attempt failed, YES
147 - (BOOL)executeFormAssistAction:(NSString*)actionName;
149 // Runs |block| while allowing the keyboard to be displayed as a result of focus
150 // changes caused by |block|.
151 - (void)runBlockAllowingKeyboardDisplay:(ProceduralBlock)block;
153 // Asynchronously retrieves an accessory view from |_providers|.
154 - (void)retrieveAccessoryViewForForm:(const std::string&)formName
155 field:(const std::string&)fieldName
156 value:(const std::string&)value
157 type:(const std::string&)type
158 webState:(web::WebState*)webState;
160 // Clears the current custom accessory view and restores the default.
163 // The current web state.
164 @property(nonatomic, readonly) web::WebState* webState;
166 // The current web view proxy.
167 @property(nonatomic, readonly) id<CRWWebViewProxy> webViewProxy;
171 @implementation FormInputAccessoryViewController {
172 // Bridge to observe the web state from Objective-C.
173 scoped_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
175 // Last registered keyboard rectangle.
176 CGRect _keyboardFrame;
178 // The custom view that should be shown in the input accessory view.
179 base::scoped_nsobject<UIView> _customAccessoryView;
181 // The JS manager for interacting with the underlying form.
182 base::scoped_nsobject<JsSuggestionManager> _JSSuggestionManager;
184 // The original subviews in keyboard accessory view that were originally not
185 // hidden but were hidden when showing Autofill suggestions.
186 base::scoped_nsobject<NSMutableArray> _hiddenOriginalSubviews;
188 // The objects that can provide a custom input accessory view while filling
190 base::scoped_nsobject<NSArray> _providers;
192 // The object that manages the currently-shown custom accessory view.
193 base::WeakNSProtocol<id<FormInputAccessoryViewProvider>> _currentProvider;
196 - (instancetype)initWithWebState:(web::WebState*)webState
197 providers:(NSArray*)providers {
198 JsSuggestionManager* suggestionManager =
199 base::mac::ObjCCastStrict<JsSuggestionManager>(
200 [webState->GetJSInjectionReceiver()
201 instanceOfClass:[JsSuggestionManager class]]);
202 return [self initWithWebState:webState
203 JSSuggestionManager:suggestionManager
204 providers:providers];
207 - (instancetype)initWithWebState:(web::WebState*)webState
208 JSSuggestionManager:(JsSuggestionManager*)JSSuggestionManager
209 providers:(NSArray*)providers {
212 _JSSuggestionManager.reset([JSSuggestionManager retain]);
213 _hiddenOriginalSubviews.reset([[NSMutableArray alloc] init]);
214 _webStateObserverBridge.reset(
215 new web::WebStateObserverBridge(webState, self));
216 _providers.reset([providers copy]);
217 // There is no defined relation on the timing of JavaScript events and
218 // keyboard showing up. So it is necessary to listen to the keyboard
219 // notification to make sure the keyboard is updated.
220 [[NSNotificationCenter defaultCenter]
222 selector:@selector(keyboardDidChangeFrame:)
223 name:UIKeyboardDidChangeFrameNotification
225 [[NSNotificationCenter defaultCenter]
227 selector:@selector(keyboardDidHide:)
228 name:UIKeyboardDidHideNotification
235 [[NSNotificationCenter defaultCenter] removeObserver:self];
239 - (web::WebState*)webState {
240 return _webStateObserverBridge ? _webStateObserverBridge->web_state()
244 - (id<CRWWebViewProxy>)webViewProxy {
245 return self.webState ? self.webState->GetWebViewProxy() : nil;
248 - (void)hideSubviewsInOriginalAccessoryView:(UIView*)accessoryView {
249 for (UIView* subview in [accessoryView subviews]) {
250 if (!subview.hidden) {
251 [_hiddenOriginalSubviews addObject:subview];
252 subview.hidden = YES;
257 - (void)showCustomInputAccessoryView:(UIView*)view {
258 [self restoreDefaultInputAccessoryView];
261 UIView* inputAccessoryView = [self.webViewProxy getKeyboardAccessory];
262 if (ComputeFramesOfKeyboardParts(inputAccessoryView, &leftFrame,
264 [self hideSubviewsInOriginalAccessoryView:inputAccessoryView];
265 _customAccessoryView.reset(
266 [[FormInputAccessoryView alloc] initWithFrame:inputAccessoryView.frame
270 rightFrame:rightFrame]);
271 [inputAccessoryView addSubview:_customAccessoryView];
275 - (void)restoreDefaultInputAccessoryView {
276 [_customAccessoryView removeFromSuperview];
277 _customAccessoryView.reset();
278 for (UIView* subview in _hiddenOriginalSubviews.get()) {
281 [_hiddenOriginalSubviews removeAllObjects];
284 - (void)closeKeyboard {
285 BOOL performedAction =
286 [self executeFormAssistAction:ios_internal::autofill::
287 kFormSuggestionAssistButtonDone];
289 if (!performedAction) {
290 // We could not find the built-in form assist controls, so try to focus
291 // the next or previous control using JavaScript.
292 [self runBlockAllowingKeyboardDisplay:^{
293 [_JSSuggestionManager closeKeyboard];
298 - (BOOL)executeFormAssistAction:(NSString*)actionName {
299 UIView* inputAccessoryView = [self.webViewProxy getKeyboardAccessory];
300 if (!inputAccessoryView)
303 NSArray* descendants =
304 FindDescendantToolbarItemsForActionName(inputAccessoryView, actionName);
306 if (![descendants count])
309 UIBarButtonItem* item = descendants[0];
310 [[item target] performSelector:[item action] withObject:item];
314 - (void)runBlockAllowingKeyboardDisplay:(ProceduralBlock)block {
316 instancesRespondToSelector:@selector(keyboardDisplayRequiresUserAction)]);
318 BOOL originalValue = [self.webViewProxy keyboardDisplayRequiresUserAction];
319 [self.webViewProxy setKeyboardDisplayRequiresUserAction:NO];
321 [self.webViewProxy setKeyboardDisplayRequiresUserAction:originalValue];
325 #pragma mark FormInputAccessoryViewDelegate
327 - (void)selectPreviousElement {
328 BOOL performedAction = [self
329 executeFormAssistAction:ios_internal::autofill::
330 kFormSuggestionAssistButtonPreviousElement];
331 if (!performedAction) {
332 // We could not find the built-in form assist controls, so try to focus
333 // the next or previous control using JavaScript.
334 [self runBlockAllowingKeyboardDisplay:^{
335 [_JSSuggestionManager selectPreviousElement];
340 - (void)selectNextElement {
341 BOOL performedAction =
342 [self executeFormAssistAction:ios_internal::autofill::
343 kFormSuggestionAssistButtonNextElement];
345 if (!performedAction) {
346 // We could not find the built-in form assist controls, so try to focus
347 // the next or previous control using JavaScript.
348 [self runBlockAllowingKeyboardDisplay:^{
349 [_JSSuggestionManager selectNextElement];
354 - (void)fetchPreviousAndNextElementsPresenceWithCompletionHandler:
355 (void (^)(BOOL, BOOL))completionHandler {
356 DCHECK(completionHandler);
357 [_JSSuggestionManager
358 fetchPreviousAndNextElementsPresenceWithCompletionHandler:
363 #pragma mark CRWWebStateObserver
365 - (void)webStateDidLoadPage:(web::WebState*)webState {
369 - (void)webState:(web::WebState*)webState
370 didRegisterFormActivityWithFormNamed:(const std::string&)formName
371 fieldName:(const std::string&)fieldName
372 type:(const std::string&)type
373 value:(const std::string&)value
375 inputMissing:(BOOL)inputMissing {
376 web::URLVerificationTrustLevel trustLevel;
377 const GURL pageURL(webState->GetCurrentURL(&trustLevel));
378 if (inputMissing || trustLevel != web::URLVerificationTrustLevel::kAbsolute ||
379 !web::UrlHasWebScheme(pageURL) || !webState->ContentIsHTML()) {
384 if ((type == "blur" || type == "change")) {
388 [self retrieveAccessoryViewForForm:formName
395 - (void)webStateDestroyed:(web::WebState*)webState {
397 _webStateObserverBridge.reset();
401 if (_currentProvider) {
402 [_currentProvider inputAccessoryViewControllerDidReset:self];
403 _currentProvider.reset();
405 [self restoreDefaultInputAccessoryView];
408 - (void)retrieveAccessoryViewForForm:(const std::string&)formName
409 field:(const std::string&)fieldName
410 value:(const std::string&)value
411 type:(const std::string&)type
412 webState:(web::WebState*)webState {
413 base::WeakNSObject<FormInputAccessoryViewController> weakSelf(self);
414 std::string strongFormName = formName;
415 std::string strongFieldName = fieldName;
416 std::string strongValue = value;
417 std::string strongType = type;
419 // Build a block for each provider that will invoke its completion with YES
420 // if the provider can provide an accessory view for the specified form/field
422 base::scoped_nsobject<NSMutableArray> findProviderBlocks(
423 [[NSMutableArray alloc] init]);
424 for (NSUInteger i = 0; i < [_providers count]; i++) {
425 base::mac::ScopedBlock<passwords::PipelineBlock> block(
426 ^(void (^completion)(BOOL success)) {
427 // Access all the providers through |self| to guarantee that both
428 // |self| and all the providers exist when the block is executed.
429 // |_providers| is immutable, so the subscripting is always valid.
430 base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
434 id<FormInputAccessoryViewProvider> provider =
435 strongSelf.get()->_providers[i];
436 [provider checkIfAccessoryViewIsAvailableForFormNamed:strongFormName
437 fieldName:strongFieldName
439 completionHandler:completion];
441 base::scoped_policy::RETAIN);
442 [findProviderBlocks addObject:block];
445 // Once the view is retrieved, update the UI.
446 AccessoryViewReadyCompletion readyCompletion =
447 ^(UIView* accessoryView, id<FormInputAccessoryViewProvider> provider) {
448 base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
450 if (!strongSelf || !strongSelf.get()->_currentProvider)
452 DCHECK_EQ(strongSelf.get()->_currentProvider.get(), provider);
453 [provider setAccessoryViewDelegate:strongSelf];
454 [strongSelf showCustomInputAccessoryView:accessoryView];
457 // Once a provider is found, use it to retrieve the accessory view.
458 passwords::PipelineCompletionBlock onProviderFound =
459 ^(NSUInteger providerIndex) {
460 if (providerIndex == NSNotFound) {
464 base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
466 if (!strongSelf || ![strongSelf webState])
468 id<FormInputAccessoryViewProvider> provider =
469 strongSelf.get()->_providers[providerIndex];
470 [strongSelf.get()->_currentProvider
471 inputAccessoryViewControllerDidReset:self];
472 strongSelf.get()->_currentProvider.reset(provider);
473 [strongSelf.get()->_currentProvider
474 retrieveAccessoryViewForFormNamed:strongFormName
475 fieldName:strongFieldName
479 accessoryViewUpdateBlock:readyCompletion];
482 // Run all the blocks in |findProviderBlocks| until one invokes its
483 // completion with YES. The first one to do so will be passed to
484 // |onProviderFound|.
485 passwords::RunSearchPipeline(findProviderBlocks, onProviderFound);
488 - (void)keyboardDidChangeFrame:(NSNotification*)notification {
489 if (!self.webState || !_currentProvider)
491 CGRect keyboardFrame =
492 [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
493 // With iOS8 (beta) this method can be called even when the rect has not
494 // changed. When this is detected we exit early.
495 if (CGRectEqualToRect(CGRectIntegral(_keyboardFrame),
496 CGRectIntegral(keyboardFrame))) {
499 _keyboardFrame = keyboardFrame;
500 [_currentProvider resizeAccessoryView];
503 - (void)keyboardDidHide:(NSNotification*)notification {
504 _keyboardFrame = CGRectZero;