Don't show supervised user as "already on this device" while they're being imported.
[chromium-blink-merge.git] / ios / chrome / browser / autofill / form_input_accessory_view_controller.mm
blobc05d591726936243b4278aa39233bc88d55211a0
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 ios_internal {
25 namespace autofill {
26 NSString* const kFormSuggestionAssistButtonPreviousElement = @"previousTap";
27 NSString* const kFormSuggestionAssistButtonNextElement = @"nextTap";
28 NSString* const kFormSuggestionAssistButtonDone = @"done";
29 }  // namespace autofill
30 }  // namespace ios_internal
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 true 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 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
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   // 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 {
210   self = [super init];
211   if (self) {
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]
221         addObserver:self
222            selector:@selector(keyboardDidChangeFrame:)
223                name:UIKeyboardDidChangeFrameNotification
224              object:nil];
225     [[NSNotificationCenter defaultCenter]
226         addObserver:self
227            selector:@selector(keyboardDidHide:)
228                name:UIKeyboardDidHideNotification
229              object:nil];
230   }
231   return self;
234 - (void)dealloc {
235   [[NSNotificationCenter defaultCenter] removeObserver:self];
236   [super dealloc];
239 - (web::WebState*)webState {
240   return _webStateObserverBridge ? _webStateObserverBridge->web_state()
241                                  : nullptr;
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;
253     }
254   }
257 - (void)showCustomInputAccessoryView:(UIView*)view {
258   [self restoreDefaultInputAccessoryView];
259   CGRect leftFrame;
260   CGRect rightFrame;
261   UIView* inputAccessoryView = [self.webViewProxy getKeyboardAccessory];
262   if (ComputeFramesOfKeyboardParts(inputAccessoryView, &leftFrame,
263                                    &rightFrame)) {
264     [self hideSubviewsInOriginalAccessoryView:inputAccessoryView];
265     _customAccessoryView.reset(
266         [[FormInputAccessoryView alloc] initWithFrame:inputAccessoryView.frame
267                                              delegate:self
268                                            customView:view
269                                             leftFrame:leftFrame
270                                            rightFrame:rightFrame]);
271     [inputAccessoryView addSubview:_customAccessoryView];
272   }
275 - (void)restoreDefaultInputAccessoryView {
276   [_customAccessoryView removeFromSuperview];
277   _customAccessoryView.reset();
278   for (UIView* subview in _hiddenOriginalSubviews.get()) {
279     subview.hidden = NO;
280   }
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];
294     }];
295   }
298 - (BOOL)executeFormAssistAction:(NSString*)actionName {
299   UIView* inputAccessoryView = [self.webViewProxy getKeyboardAccessory];
300   if (!inputAccessoryView)
301     return NO;
303   NSArray* descendants =
304       FindDescendantToolbarItemsForActionName(inputAccessoryView, actionName);
306   if (![descendants count])
307     return NO;
309   UIBarButtonItem* item = descendants[0];
310   [[item target] performSelector:[item action] withObject:item];
311   return YES;
314 - (void)runBlockAllowingKeyboardDisplay:(ProceduralBlock)block {
315   DCHECK([UIWebView
316       instancesRespondToSelector:@selector(keyboardDisplayRequiresUserAction)]);
318   BOOL originalValue = [self.webViewProxy keyboardDisplayRequiresUserAction];
319   [self.webViewProxy setKeyboardDisplayRequiresUserAction:NO];
320   block();
321   [self.webViewProxy setKeyboardDisplayRequiresUserAction:originalValue];
324 #pragma mark -
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];
336     }];
337   }
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];
350     }];
351   }
354 - (void)fetchPreviousAndNextElementsPresenceWithCompletionHandler:
355         (void (^)(BOOL, BOOL))completionHandler {
356   DCHECK(completionHandler);
357   [_JSSuggestionManager
358       fetchPreviousAndNextElementsPresenceWithCompletionHandler:
359           completionHandler];
362 #pragma mark -
363 #pragma mark CRWWebStateObserver
365 - (void)webStateDidLoadPage:(web::WebState*)webState {
366   [self reset];
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
374                                  keyCode:(int)keyCode
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()) {
380     [self reset];
381     return;
382   }
384   if ((type == "blur" || type == "change")) {
385     return;
386   }
388   [self retrieveAccessoryViewForForm:formName
389                                field:fieldName
390                                value:value
391                                 type:type
392                             webState:webState];
395 - (void)webStateDestroyed:(web::WebState*)webState {
396   [self reset];
397   _webStateObserverBridge.reset();
400 - (void)reset {
401   if (_currentProvider) {
402     [_currentProvider inputAccessoryViewControllerDidReset:self];
403     _currentProvider.reset();
404   }
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
421   // and NO otherwise.
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(
431               [weakSelf retain]);
432           if (!strongSelf)
433             return;
434           id<FormInputAccessoryViewProvider> provider =
435               strongSelf.get()->_providers[i];
436           [provider checkIfAccessoryViewIsAvailableForFormNamed:strongFormName
437                                                       fieldName:strongFieldName
438                                                        webState:webState
439                                               completionHandler:completion];
440         },
441         base::scoped_policy::RETAIN);
442     [findProviderBlocks addObject:block];
443   }
445   // Once the view is retrieved, update the UI.
446   AccessoryViewReadyCompletion readyCompletion =
447       ^(UIView* accessoryView, id<FormInputAccessoryViewProvider> provider) {
448         base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
449             [weakSelf retain]);
450         if (!strongSelf || !strongSelf.get()->_currentProvider)
451           return;
452         DCHECK_EQ(strongSelf.get()->_currentProvider.get(), provider);
453         [provider setAccessoryViewDelegate:strongSelf];
454         [strongSelf showCustomInputAccessoryView:accessoryView];
455       };
457   // Once a provider is found, use it to retrieve the accessory view.
458   passwords::PipelineCompletionBlock onProviderFound =
459       ^(NSUInteger providerIndex) {
460         if (providerIndex == NSNotFound) {
461           [weakSelf reset];
462           return;
463         }
464         base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
465             [weakSelf retain]);
466         if (!strongSelf || ![strongSelf webState])
467           return;
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
476                                         value:strongValue
477                                          type:strongType
478                                      webState:webState
479                      accessoryViewUpdateBlock:readyCompletion];
480       };
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)
490     return;
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))) {
497     return;
498   }
499   _keyboardFrame = keyboardFrame;
500   [_currentProvider resizeAccessoryView];
503 - (void)keyboardDidHide:(NSNotification*)notification {
504   _keyboardFrame = CGRectZero;
507 @end