Revert "Fix broken channel icon in chrome://help on CrOS" and try again
[chromium-blink-merge.git] / ios / chrome / browser / autofill / form_input_accessory_view_controller.mm
blobe628cc9d6b6a0800894cc490fd01e7a57f370e49
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"
23 #include "url/gurl.h"
25 namespace autofill {
26 NSString* const kFormSuggestionAssistButtonPreviousElement = @"previousTap";
27 NSString* const kFormSuggestionAssistButtonNextElement = @"nextTap";
28 NSString* const kFormSuggestionAssistButtonDone = @"done";
29 CGFloat const kInputAccessoryHeight = 44.0f;
30 }  // namespace autofill
32 namespace {
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) {
37   DCHECK(root);
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]];
48   }
50   return descendants;
53 // Finds all UIToolbarItems associated with a given UIToolbar |toolbar| with
54 // action selectors with a name that containts the action name specified by
55 // |actionName|.
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];
62     if (!itemAction)
63       continue;
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];
69   }
71   return toolbarItems;
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) {
82     [descendants
83         addObjectsFromArray:FindToolbarItemsForActionName(toolbar, actionName)];
84   }
86   return descendants;
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,
101                                   CGRect* leftFrame,
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)
111     return false;
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;
119   } else {
120     *rightFrame = first_frame;
121     *leftFrame = second_frame;
122   }
123   return true;
126 }  // namespace
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
146 // otherwise. [HACK]
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.
161 - (void)reset;
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;
169 @end
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
189   // forms.
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 {
213   self = [super init];
214   if (self) {
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;
221   }
222   return self;
225 - (void)wasShown {
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]
231         addObserver:self
232            selector:@selector(keyboardWillOrDidChangeFrame:)
233                name:UIKeyboardWillChangeFrameNotification
234              object:nil];
235     [[NSNotificationCenter defaultCenter]
236         addObserver:self
237            selector:@selector(textInputDidBeginEditing:)
238                name:UITextFieldTextDidBeginEditingNotification
239              object:nil];
240     [[NSNotificationCenter defaultCenter]
241         addObserver:self
242            selector:@selector(textInputDidBeginEditing:)
243                name:UITextViewTextDidBeginEditingNotification
244              object:nil];
245   }
246   [[NSNotificationCenter defaultCenter]
247       addObserver:self
248          selector:@selector(keyboardWillOrDidChangeFrame:)
249              name:UIKeyboardDidChangeFrameNotification
250            object:nil];
251   [[NSNotificationCenter defaultCenter]
252       addObserver:self
253          selector:@selector(keyboardDidHide:)
254              name:UIKeyboardDidHideNotification
255            object:nil];
258 - (void)wasHidden {
259   [_customAccessoryView removeFromSuperview];
260   [[NSNotificationCenter defaultCenter] removeObserver:self];
263 - (void)dealloc {
264   [[NSNotificationCenter defaultCenter] removeObserver:self];
265   [super dealloc];
268 - (web::WebState*)webState {
269   return _webStateObserverBridge ? _webStateObserverBridge->web_state()
270                                  : nullptr;
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;
282     }
283   }
286 - (void)showCustomInputAccessoryView:(UIView*)view {
287   DCHECK(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)
295                 .size.height == 0 ||
296         CGRectEqualToRect(_keyboardFrame, CGRectZero)) {
297       _customAccessoryView.reset();
298       return;
299     }
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();
309         return;
310       }
311     }
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];
322   } else {
323     // On all other versions, the custom view replaces the default UI of the
324     // inputAccessoryView.
325     [self restoreDefaultInputAccessoryView];
326     CGRect leftFrame;
327     CGRect rightFrame;
328     UIView* inputAccessoryView = [self.webViewProxy getKeyboardAccessory];
329     if (ComputeFramesOfKeyboardParts(inputAccessoryView, &leftFrame,
330                                      &rightFrame)) {
331       [self hideSubviewsInOriginalAccessoryView:inputAccessoryView];
332       _customAccessoryView.reset([[FormInputAccessoryView alloc]
333           initWithFrame:inputAccessoryView.frame
334                delegate:self
335              customView:view
336               leftFrame:leftFrame
337              rightFrame:rightFrame]);
338       [inputAccessoryView addSubview:_customAccessoryView];
339     }
340   }
343 - (void)restoreDefaultInputAccessoryView {
344   [_customAccessoryView removeFromSuperview];
345   _customAccessoryView.reset();
346   for (UIView* subview in _hiddenOriginalSubviews.get()) {
347     subview.hidden = NO;
348   }
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];
361     }];
362   }
365 - (BOOL)executeFormAssistAction:(NSString*)actionName {
366   UIView* inputAccessoryView = [self.webViewProxy getKeyboardAccessory];
367   if (!inputAccessoryView)
368     return NO;
370   NSArray* descendants =
371       FindDescendantToolbarItemsForActionName(inputAccessoryView, actionName);
373   if (![descendants count])
374     return NO;
376   UIBarButtonItem* item = descendants[0];
377   [[item target] performSelector:[item action] withObject:item];
378   return YES;
381 - (void)runBlockAllowingKeyboardDisplay:(ProceduralBlock)block {
382   DCHECK([UIWebView
383       instancesRespondToSelector:@selector(keyboardDisplayRequiresUserAction)]);
385   BOOL originalValue = [self.webViewProxy keyboardDisplayRequiresUserAction];
386   [self.webViewProxy setKeyboardDisplayRequiresUserAction:NO];
387   block();
388   [self.webViewProxy setKeyboardDisplayRequiresUserAction:originalValue];
391 #pragma mark -
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];
403     }];
404   }
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];
416     }];
417   }
420 - (void)fetchPreviousAndNextElementsPresenceWithCompletionHandler:
421         (void (^)(BOOL, BOOL))completionHandler {
422   DCHECK(completionHandler);
423   [_JSSuggestionManager
424       fetchPreviousAndNextElementsPresenceWithCompletionHandler:
425           completionHandler];
428 #pragma mark -
429 #pragma mark CRWWebStateObserver
431 - (void)webStateDidLoadPage:(web::WebState*)webState {
432   [self reset];
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
440                                  keyCode:(int)keyCode
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()) {
446     [self reset];
447     return;
448   }
450   if ((type == "blur" || type == "change")) {
451     return;
452   }
454   [self retrieveAccessoryViewForForm:formName
455                                field:fieldName
456                                value:value
457                                 type:type
458                             webState:webState];
461 - (void)webStateDestroyed:(web::WebState*)webState {
462   [self reset];
463   _webStateObserverBridge.reset();
466 - (void)reset {
467   if (_currentProvider) {
468     [_currentProvider inputAccessoryViewControllerDidReset:self];
469     _currentProvider.reset();
470   }
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
487   // and NO otherwise.
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(
497               [weakSelf retain]);
498           if (!strongSelf)
499             return;
500           id<FormInputAccessoryViewProvider> provider =
501               strongSelf.get()->_providers[i];
502           [provider checkIfAccessoryViewIsAvailableForFormNamed:strongFormName
503                                                       fieldName:strongFieldName
504                                                        webState:webState
505                                               completionHandler:completion];
506         },
507         base::scoped_policy::RETAIN);
508     [findProviderBlocks addObject:block];
509   }
511   // Once the view is retrieved, update the UI.
512   AccessoryViewReadyCompletion readyCompletion =
513       ^(UIView* accessoryView, id<FormInputAccessoryViewProvider> provider) {
514         base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
515             [weakSelf retain]);
516         if (!strongSelf || !strongSelf.get()->_currentProvider)
517           return;
518         DCHECK_EQ(strongSelf.get()->_currentProvider.get(), provider);
519         [provider setAccessoryViewDelegate:strongSelf];
520         [strongSelf showCustomInputAccessoryView:accessoryView];
521       };
523   // Once a provider is found, use it to retrieve the accessory view.
524   passwords::PipelineCompletionBlock onProviderFound =
525       ^(NSUInteger providerIndex) {
526         if (providerIndex == NSNotFound) {
527           [weakSelf reset];
528           return;
529         }
530         base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
531             [weakSelf retain]);
532         if (!strongSelf || ![strongSelf webState])
533           return;
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
542                                         value:strongValue
543                                          type:strongType
544                                      webState:webState
545                      accessoryViewUpdateBlock:readyCompletion];
546       };
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)
557     return nil;
559   UIWindow* window = windows[1];
560   for (UIView* subview in window.subviews) {
561     if ([NSStringFromClass([subview class]) rangeOfString:@"PeripheralHost"]
562             .location != NSNotFound) {
563       return subview;
564     }
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) {
570           return subsubview;
571         }
572       }
573     }
574   }
576   return nil;
579 - (void)keyboardWillOrDidChangeFrame:(NSNotification*)notification {
580   if (!self.webState || !_currentProvider)
581     return;
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))) {
588     return;
589   }
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 {
598   [self reset];
601 - (void)keyboardDidHide:(NSNotification*)notification {
602   _keyboardFrame = CGRectZero;
605 @end