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/ios/ios_util.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 #import "components/autofill/ios/browser/js_suggestion_manager.h"
14 #import "ios/chrome/browser/autofill/form_input_accessory_view.h"
15 #import "ios/chrome/browser/autofill/form_suggestion_view.h"
16 #import "ios/chrome/browser/passwords/password_generation_utils.h"
17 #include "ios/chrome/browser/ui/ui_util.h"
18 #include "ios/web/public/test/crw_test_js_injection_receiver.h"
19 #include "ios/web/public/url_scheme_util.h"
20 #import "ios/web/public/web_state/crw_web_view_proxy.h"
21 #include "ios/web/public/web_state/url_verification_constants.h"
22 #include "ios/web/public/web_state/web_state.h"
26 NSString* const kFormSuggestionAssistButtonPreviousElement = @"previousTap";
27 NSString* const kFormSuggestionAssistButtonNextElement = @"nextTap";
28 NSString* const kFormSuggestionAssistButtonDone = @"done";
29 CGFloat const kInputAccessoryHeight = 44.0f;
30 } // namespace autofill
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 false 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 will or did change frame.
136 - (void)keyboardWillOrDidChangeFrame:(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 // Whether suggestions have previously been shown.
193 BOOL _suggestionsHaveBeenShown;
195 // The object that manages the currently-shown custom accessory view.
196 base::WeakNSProtocol<id<FormInputAccessoryViewProvider>> _currentProvider;
199 - (instancetype)initWithWebState:(web::WebState*)webState
200 providers:(NSArray*)providers {
201 JsSuggestionManager* suggestionManager =
202 base::mac::ObjCCastStrict<JsSuggestionManager>(
203 [webState->GetJSInjectionReceiver()
204 instanceOfClass:[JsSuggestionManager class]]);
205 return [self initWithWebState:webState
206 JSSuggestionManager:suggestionManager
207 providers:providers];
210 - (instancetype)initWithWebState:(web::WebState*)webState
211 JSSuggestionManager:(JsSuggestionManager*)JSSuggestionManager
212 providers:(NSArray*)providers {
215 _JSSuggestionManager.reset([JSSuggestionManager retain]);
216 _hiddenOriginalSubviews.reset([[NSMutableArray alloc] init]);
217 _webStateObserverBridge.reset(
218 new web::WebStateObserverBridge(webState, self));
219 _providers.reset([providers copy]);
220 _suggestionsHaveBeenShown = NO;
226 // There is no defined relation on the timing of JavaScript events and
227 // keyboard showing up. So it is necessary to listen to the keyboard
228 // notification to make sure the keyboard is updated.
229 if (base::ios::IsRunningOnIOS9OrLater() && IsIPadIdiom()) {
230 [[NSNotificationCenter defaultCenter]
232 selector:@selector(keyboardWillOrDidChangeFrame:)
233 name:UIKeyboardWillChangeFrameNotification
235 [[NSNotificationCenter defaultCenter]
237 selector:@selector(textInputDidBeginEditing:)
238 name:UITextFieldTextDidBeginEditingNotification
240 [[NSNotificationCenter defaultCenter]
242 selector:@selector(textInputDidBeginEditing:)
243 name:UITextViewTextDidBeginEditingNotification
246 [[NSNotificationCenter defaultCenter]
248 selector:@selector(keyboardWillOrDidChangeFrame:)
249 name:UIKeyboardDidChangeFrameNotification
251 [[NSNotificationCenter defaultCenter]
253 selector:@selector(keyboardDidHide:)
254 name:UIKeyboardDidHideNotification
259 [_customAccessoryView removeFromSuperview];
260 [[NSNotificationCenter defaultCenter] removeObserver:self];
264 [[NSNotificationCenter defaultCenter] removeObserver:self];
268 - (web::WebState*)webState {
269 return _webStateObserverBridge ? _webStateObserverBridge->web_state()
273 - (id<CRWWebViewProxy>)webViewProxy {
274 return self.webState ? self.webState->GetWebViewProxy() : nil;
277 - (void)hideSubviewsInOriginalAccessoryView:(UIView*)accessoryView {
278 for (UIView* subview in [accessoryView subviews]) {
279 if (!subview.hidden) {
280 [_hiddenOriginalSubviews addObject:subview];
281 subview.hidden = YES;
286 - (void)showCustomInputAccessoryView:(UIView*)view {
288 if (base::ios::IsRunningOnIOS9OrLater() && IsIPadIdiom()) {
289 // On iPads running iOS 9 or later, there's no inputAccessoryView available
290 // so we attach the custom view directly to the keyboard view instead.
291 [_customAccessoryView removeFromSuperview];
293 // If the keyboard isn't visible don't show the custom view.
294 if (CGRectIntersection([UIScreen mainScreen].bounds, _keyboardFrame)
296 CGRectEqualToRect(_keyboardFrame, CGRectZero)) {
297 _customAccessoryView.reset();
301 // If this is a form suggestion view and no suggestions have been triggered
302 // yet, don't show the custom view.
303 FormSuggestionView* formSuggestionView =
304 base::mac::ObjCCastStrict<FormSuggestionView>(view);
305 if (formSuggestionView) {
306 int numSuggestions = [[formSuggestionView suggestions] count];
307 if (!_suggestionsHaveBeenShown && numSuggestions == 0) {
308 _customAccessoryView.reset();
312 _suggestionsHaveBeenShown = YES;
314 CGFloat height = autofill::kInputAccessoryHeight;
315 CGRect frame = CGRectMake(_keyboardFrame.origin.x, -height,
316 _keyboardFrame.size.width, height);
317 _customAccessoryView.reset(
318 [[FormInputAccessoryView alloc] initWithFrame:frame customView:view]);
319 UIView* keyboardView = [self getKeyboardView];
320 DCHECK(keyboardView);
321 [keyboardView addSubview:_customAccessoryView];
323 // On all other versions, the custom view replaces the default UI of the
324 // inputAccessoryView.
325 [self restoreDefaultInputAccessoryView];
328 UIView* inputAccessoryView = [self.webViewProxy getKeyboardAccessory];
329 if (ComputeFramesOfKeyboardParts(inputAccessoryView, &leftFrame,
331 [self hideSubviewsInOriginalAccessoryView:inputAccessoryView];
332 _customAccessoryView.reset([[FormInputAccessoryView alloc]
333 initWithFrame:inputAccessoryView.frame
337 rightFrame:rightFrame]);
338 [inputAccessoryView addSubview:_customAccessoryView];
343 - (void)restoreDefaultInputAccessoryView {
344 [_customAccessoryView removeFromSuperview];
345 _customAccessoryView.reset();
346 for (UIView* subview in _hiddenOriginalSubviews.get()) {
349 [_hiddenOriginalSubviews removeAllObjects];
352 - (void)closeKeyboard {
353 BOOL performedAction =
354 [self executeFormAssistAction:autofill::kFormSuggestionAssistButtonDone];
356 if (!performedAction) {
357 // We could not find the built-in form assist controls, so try to focus
358 // the next or previous control using JavaScript.
359 [self runBlockAllowingKeyboardDisplay:^{
360 [_JSSuggestionManager closeKeyboard];
365 - (BOOL)executeFormAssistAction:(NSString*)actionName {
366 UIView* inputAccessoryView = [self.webViewProxy getKeyboardAccessory];
367 if (!inputAccessoryView)
370 NSArray* descendants =
371 FindDescendantToolbarItemsForActionName(inputAccessoryView, actionName);
373 if (![descendants count])
376 UIBarButtonItem* item = descendants[0];
377 [[item target] performSelector:[item action] withObject:item];
381 - (void)runBlockAllowingKeyboardDisplay:(ProceduralBlock)block {
383 instancesRespondToSelector:@selector(keyboardDisplayRequiresUserAction)]);
385 BOOL originalValue = [self.webViewProxy keyboardDisplayRequiresUserAction];
386 [self.webViewProxy setKeyboardDisplayRequiresUserAction:NO];
388 [self.webViewProxy setKeyboardDisplayRequiresUserAction:originalValue];
392 #pragma mark FormInputAccessoryViewDelegate
394 - (void)selectPreviousElement {
395 BOOL performedAction =
396 [self executeFormAssistAction:
397 autofill::kFormSuggestionAssistButtonPreviousElement];
398 if (!performedAction) {
399 // We could not find the built-in form assist controls, so try to focus
400 // the next or previous control using JavaScript.
401 [self runBlockAllowingKeyboardDisplay:^{
402 [_JSSuggestionManager selectPreviousElement];
407 - (void)selectNextElement {
408 BOOL performedAction = [self
409 executeFormAssistAction:autofill::kFormSuggestionAssistButtonNextElement];
411 if (!performedAction) {
412 // We could not find the built-in form assist controls, so try to focus
413 // the next or previous control using JavaScript.
414 [self runBlockAllowingKeyboardDisplay:^{
415 [_JSSuggestionManager selectNextElement];
420 - (void)fetchPreviousAndNextElementsPresenceWithCompletionHandler:
421 (void (^)(BOOL, BOOL))completionHandler {
422 DCHECK(completionHandler);
423 [_JSSuggestionManager
424 fetchPreviousAndNextElementsPresenceWithCompletionHandler:
429 #pragma mark CRWWebStateObserver
431 - (void)webStateDidLoadPage:(web::WebState*)webState {
435 - (void)webState:(web::WebState*)webState
436 didRegisterFormActivityWithFormNamed:(const std::string&)formName
437 fieldName:(const std::string&)fieldName
438 type:(const std::string&)type
439 value:(const std::string&)value
441 inputMissing:(BOOL)inputMissing {
442 web::URLVerificationTrustLevel trustLevel;
443 const GURL pageURL(webState->GetCurrentURL(&trustLevel));
444 if (inputMissing || trustLevel != web::URLVerificationTrustLevel::kAbsolute ||
445 !web::UrlHasWebScheme(pageURL) || !webState->ContentIsHTML()) {
450 if ((type == "blur" || type == "change")) {
454 [self retrieveAccessoryViewForForm:formName
461 - (void)webStateDestroyed:(web::WebState*)webState {
463 _webStateObserverBridge.reset();
467 if (_currentProvider) {
468 [_currentProvider inputAccessoryViewControllerDidReset:self];
469 _currentProvider.reset();
471 [self restoreDefaultInputAccessoryView];
474 - (void)retrieveAccessoryViewForForm:(const std::string&)formName
475 field:(const std::string&)fieldName
476 value:(const std::string&)value
477 type:(const std::string&)type
478 webState:(web::WebState*)webState {
479 base::WeakNSObject<FormInputAccessoryViewController> weakSelf(self);
480 std::string strongFormName = formName;
481 std::string strongFieldName = fieldName;
482 std::string strongValue = value;
483 std::string strongType = type;
485 // Build a block for each provider that will invoke its completion with YES
486 // if the provider can provide an accessory view for the specified form/field
488 base::scoped_nsobject<NSMutableArray> findProviderBlocks(
489 [[NSMutableArray alloc] init]);
490 for (NSUInteger i = 0; i < [_providers count]; i++) {
491 base::mac::ScopedBlock<passwords::PipelineBlock> block(
492 ^(void (^completion)(BOOL success)) {
493 // Access all the providers through |self| to guarantee that both
494 // |self| and all the providers exist when the block is executed.
495 // |_providers| is immutable, so the subscripting is always valid.
496 base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
500 id<FormInputAccessoryViewProvider> provider =
501 strongSelf.get()->_providers[i];
502 [provider checkIfAccessoryViewIsAvailableForFormNamed:strongFormName
503 fieldName:strongFieldName
505 completionHandler:completion];
507 base::scoped_policy::RETAIN);
508 [findProviderBlocks addObject:block];
511 // Once the view is retrieved, update the UI.
512 AccessoryViewReadyCompletion readyCompletion =
513 ^(UIView* accessoryView, id<FormInputAccessoryViewProvider> provider) {
514 base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
516 if (!strongSelf || !strongSelf.get()->_currentProvider)
518 DCHECK_EQ(strongSelf.get()->_currentProvider.get(), provider);
519 [provider setAccessoryViewDelegate:strongSelf];
520 [strongSelf showCustomInputAccessoryView:accessoryView];
523 // Once a provider is found, use it to retrieve the accessory view.
524 passwords::PipelineCompletionBlock onProviderFound =
525 ^(NSUInteger providerIndex) {
526 if (providerIndex == NSNotFound) {
530 base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
532 if (!strongSelf || ![strongSelf webState])
534 id<FormInputAccessoryViewProvider> provider =
535 strongSelf.get()->_providers[providerIndex];
536 [strongSelf.get()->_currentProvider
537 inputAccessoryViewControllerDidReset:self];
538 strongSelf.get()->_currentProvider.reset(provider);
539 [strongSelf.get()->_currentProvider
540 retrieveAccessoryViewForFormNamed:strongFormName
541 fieldName:strongFieldName
545 accessoryViewUpdateBlock:readyCompletion];
548 // Run all the blocks in |findProviderBlocks| until one invokes its
549 // completion with YES. The first one to do so will be passed to
550 // |onProviderFound|.
551 passwords::RunSearchPipeline(findProviderBlocks, onProviderFound);
554 - (UIView*)getKeyboardView {
555 NSArray* windows = [UIApplication sharedApplication].windows;
556 if (windows.count < 2)
559 UIWindow* window = windows[1];
560 for (UIView* subview in window.subviews) {
561 if ([NSStringFromClass([subview class]) rangeOfString:@"PeripheralHost"]
562 .location != NSNotFound) {
565 if ([NSStringFromClass([subview class]) rangeOfString:@"SetContainer"]
566 .location != NSNotFound) {
567 for (UIView* subsubview in subview.subviews) {
568 if ([NSStringFromClass([subsubview class]) rangeOfString:@"SetHost"]
569 .location != NSNotFound) {
579 - (void)keyboardWillOrDidChangeFrame:(NSNotification*)notification {
580 if (!self.webState || !_currentProvider)
582 CGRect keyboardFrame =
583 [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
584 // With iOS8 (beta) this method can be called even when the rect has not
585 // changed. When this is detected we exit early.
586 if (CGRectEqualToRect(CGRectIntegral(_keyboardFrame),
587 CGRectIntegral(keyboardFrame))) {
590 _keyboardFrame = keyboardFrame;
591 [_currentProvider resizeAccessoryView];
594 // On iPads running iOS 9 or later, when any text field or text view (e.g.
595 // omnibox, settings, card unmask dialog) begins editing, reset ourselves so
596 // that we don't present our custom view over the keyboard.
597 - (void)textInputDidBeginEditing:(NSNotification*)notification {
601 - (void)keyboardDidHide:(NSNotification*)notification {
602 _keyboardFrame = CGRectZero;