Upstreaming browser/ui/uikit_ui_util from iOS.
[chromium-blink-merge.git] / ios / chrome / browser / autofill / form_input_accessory_view_controller.mm
blobcbcbe2684685215c8b97b6cf1ac084d11566113e
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"
22 #include "url/gurl.h"
24 namespace autofill {
25 NSString* const kFormSuggestionAssistButtonPreviousElement = @"previousTap";
26 NSString* const kFormSuggestionAssistButtonNextElement = @"nextTap";
27 NSString* const kFormSuggestionAssistButtonDone = @"done";
28 }  // namespace autofill
30 namespace {
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) {
35   DCHECK(root);
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]];
46   }
48   return descendants;
51 // Finds all UIToolbarItems associated with a given UIToolbar |toolbar| with
52 // action selectors with a name that containts the action name specified by
53 // |actionName|.
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];
60     if (!itemAction)
61       continue;
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];
67   }
69   return toolbarItems;
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) {
80     [descendants
81         addObjectsFromArray:FindToolbarItemsForActionName(toolbar, actionName)];
82   }
84   return descendants;
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,
99                                   CGRect* leftFrame,
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)
109     return false;
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;
117   } else {
118     *rightFrame = first_frame;
119     *leftFrame = second_frame;
120   }
121   return true;
124 }  // namespace
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
144 // otherwise. [HACK]
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.
159 - (void)reset;
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;
167 @end
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
187   // forms.
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 {
208   self = [super init];
209   if (self) {
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]
219         addObserver:self
220            selector:@selector(keyboardDidChangeFrame:)
221                name:UIKeyboardDidChangeFrameNotification
222              object:nil];
223     [[NSNotificationCenter defaultCenter]
224         addObserver:self
225            selector:@selector(keyboardDidHide:)
226                name:UIKeyboardDidHideNotification
227              object:nil];
228   }
229   return self;
232 - (void)dealloc {
233   [[NSNotificationCenter defaultCenter] removeObserver:self];
234   [super dealloc];
237 - (web::WebState*)webState {
238   return _webStateObserverBridge ? _webStateObserverBridge->web_state()
239                                  : nullptr;
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;
251     }
252   }
255 - (void)showCustomInputAccessoryView:(UIView*)view {
256   [self restoreDefaultInputAccessoryView];
257   CGRect leftFrame;
258   CGRect rightFrame;
259   UIView* inputAccessoryView = [self.webViewProxy getKeyboardAccessory];
260   if (ComputeFramesOfKeyboardParts(inputAccessoryView, &leftFrame,
261                                    &rightFrame)) {
262     [self hideSubviewsInOriginalAccessoryView:inputAccessoryView];
263     _customAccessoryView.reset(
264         [[FormInputAccessoryView alloc] initWithFrame:inputAccessoryView.frame
265                                              delegate:self
266                                            customView:view
267                                             leftFrame:leftFrame
268                                            rightFrame:rightFrame]);
269     [inputAccessoryView addSubview:_customAccessoryView];
270   }
273 - (void)restoreDefaultInputAccessoryView {
274   [_customAccessoryView removeFromSuperview];
275   _customAccessoryView.reset();
276   for (UIView* subview in _hiddenOriginalSubviews.get()) {
277     subview.hidden = NO;
278   }
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];
291     }];
292   }
295 - (BOOL)executeFormAssistAction:(NSString*)actionName {
296   UIView* inputAccessoryView = [self.webViewProxy getKeyboardAccessory];
297   if (!inputAccessoryView)
298     return NO;
300   NSArray* descendants =
301       FindDescendantToolbarItemsForActionName(inputAccessoryView, actionName);
303   if (![descendants count])
304     return NO;
306   UIBarButtonItem* item = descendants[0];
307   [[item target] performSelector:[item action] withObject:item];
308   return YES;
311 - (void)runBlockAllowingKeyboardDisplay:(ProceduralBlock)block {
312   DCHECK([UIWebView
313       instancesRespondToSelector:@selector(keyboardDisplayRequiresUserAction)]);
315   BOOL originalValue = [self.webViewProxy keyboardDisplayRequiresUserAction];
316   [self.webViewProxy setKeyboardDisplayRequiresUserAction:NO];
317   block();
318   [self.webViewProxy setKeyboardDisplayRequiresUserAction:originalValue];
321 #pragma mark -
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];
333     }];
334   }
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];
346     }];
347   }
350 - (void)fetchPreviousAndNextElementsPresenceWithCompletionHandler:
351         (void (^)(BOOL, BOOL))completionHandler {
352   DCHECK(completionHandler);
353   [_JSSuggestionManager
354       fetchPreviousAndNextElementsPresenceWithCompletionHandler:
355           completionHandler];
358 #pragma mark -
359 #pragma mark CRWWebStateObserver
361 - (void)webStateDidLoadPage:(web::WebState*)webState {
362   [self reset];
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
370                                  keyCode:(int)keyCode
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()) {
376     [self reset];
377     return;
378   }
380   if ((type == "blur" || type == "change")) {
381     return;
382   }
384   [self retrieveAccessoryViewForForm:formName
385                                field:fieldName
386                                value:value
387                                 type:type
388                             webState:webState];
391 - (void)webStateDestroyed:(web::WebState*)webState {
392   [self reset];
393   _webStateObserverBridge.reset();
396 - (void)reset {
397   if (_currentProvider) {
398     [_currentProvider inputAccessoryViewControllerDidReset:self];
399     _currentProvider.reset();
400   }
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
417   // and NO otherwise.
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(
427               [weakSelf retain]);
428           if (!strongSelf)
429             return;
430           id<FormInputAccessoryViewProvider> provider =
431               strongSelf.get()->_providers[i];
432           [provider checkIfAccessoryViewIsAvailableForFormNamed:strongFormName
433                                                       fieldName:strongFieldName
434                                                        webState:webState
435                                               completionHandler:completion];
436         },
437         base::scoped_policy::RETAIN);
438     [findProviderBlocks addObject:block];
439   }
441   // Once the view is retrieved, update the UI.
442   AccessoryViewReadyCompletion readyCompletion =
443       ^(UIView* accessoryView, id<FormInputAccessoryViewProvider> provider) {
444         base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
445             [weakSelf retain]);
446         if (!strongSelf || !strongSelf.get()->_currentProvider)
447           return;
448         DCHECK_EQ(strongSelf.get()->_currentProvider.get(), provider);
449         [provider setAccessoryViewDelegate:strongSelf];
450         [strongSelf showCustomInputAccessoryView:accessoryView];
451       };
453   // Once a provider is found, use it to retrieve the accessory view.
454   passwords::PipelineCompletionBlock onProviderFound =
455       ^(NSUInteger providerIndex) {
456         if (providerIndex == NSNotFound) {
457           [weakSelf reset];
458           return;
459         }
460         base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
461             [weakSelf retain]);
462         if (!strongSelf || ![strongSelf webState])
463           return;
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
472                                         value:strongValue
473                                          type:strongType
474                                      webState:webState
475                      accessoryViewUpdateBlock:readyCompletion];
476       };
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)
486     return;
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))) {
493     return;
494   }
495   _keyboardFrame = keyboardFrame;
496   [_currentProvider resizeAccessoryView];
499 - (void)keyboardDidHide:(NSNotification*)notification {
500   _keyboardFrame = CGRectZero;
503 @end