| Index: ios/chrome/browser/autofill/form_input_accessory_view_controller.mm
|
| diff --git a/ios/chrome/browser/autofill/form_input_accessory_view_controller.mm b/ios/chrome/browser/autofill/form_input_accessory_view_controller.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..8f82627b5b5bbff5330b9b73aced068e1911c2a6
|
| --- /dev/null
|
| +++ b/ios/chrome/browser/autofill/form_input_accessory_view_controller.mm
|
| @@ -0,0 +1,507 @@
|
| +// Copyright 2014 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +#import "ios/chrome/browser/autofill/form_input_accessory_view_controller.h"
|
| +
|
| +#include "base/ios/block_types.h"
|
| +#include "base/mac/foundation_util.h"
|
| +#include "base/mac/scoped_block.h"
|
| +#include "base/mac/scoped_nsobject.h"
|
| +#include "base/memory/scoped_ptr.h"
|
| +#include "base/strings/sys_string_conversions.h"
|
| +#include "base/strings/utf_string_conversions.h"
|
| +#import "components/autofill/ios/browser/js_suggestion_manager.h"
|
| +#import "ios/chrome/browser/autofill/form_input_accessory_view.h"
|
| +#import "ios/chrome/browser/passwords/password_generation_utils.h"
|
| +#include "ios/web/public/test/crw_test_js_injection_receiver.h"
|
| +#include "ios/web/public/url_scheme_util.h"
|
| +#import "ios/web/public/web_state/crw_web_view_proxy.h"
|
| +#include "ios/web/public/web_state/url_verification_constants.h"
|
| +#include "ios/web/public/web_state/web_state.h"
|
| +#include "url/gurl.h"
|
| +
|
| +namespace ios_internal {
|
| +namespace autofill {
|
| +NSString* const kFormSuggestionAssistButtonPreviousElement = @"previousTap";
|
| +NSString* const kFormSuggestionAssistButtonNextElement = @"nextTap";
|
| +NSString* const kFormSuggestionAssistButtonDone = @"done";
|
| +} // namespace autofill
|
| +} // namespace ios_internal
|
| +
|
| +namespace {
|
| +
|
| +// Finds all views of a particular kind if class |klass| in the subview
|
| +// hierarchy of the given |root| view.
|
| +NSArray* FindDescendantsOfClass(UIView* root, Class klass) {
|
| + DCHECK(root);
|
| + NSMutableArray* viewsToExamine = [NSMutableArray arrayWithObject:root];
|
| + NSMutableArray* descendants = [NSMutableArray array];
|
| +
|
| + while ([viewsToExamine count]) {
|
| + UIView* view = [viewsToExamine lastObject];
|
| + if ([view isKindOfClass:klass])
|
| + [descendants addObject:view];
|
| +
|
| + [viewsToExamine removeLastObject];
|
| + [viewsToExamine addObjectsFromArray:[view subviews]];
|
| + }
|
| +
|
| + return descendants;
|
| +}
|
| +
|
| +// Finds all UIToolbarItems associated with a given UIToolbar |toolbar| with
|
| +// action selectors with a name that containts the action name specified by
|
| +// |actionName|.
|
| +NSArray* FindToolbarItemsForActionName(UIToolbar* toolbar,
|
| + NSString* actionName) {
|
| + NSMutableArray* toolbarItems = [NSMutableArray array];
|
| +
|
| + for (UIBarButtonItem* item in [toolbar items]) {
|
| + SEL itemAction = [item action];
|
| + if (!itemAction)
|
| + continue;
|
| + NSString* itemActionName = NSStringFromSelector(itemAction);
|
| +
|
| + // We don't do a strict string match for the action name.
|
| + if ([itemActionName rangeOfString:actionName].location != NSNotFound)
|
| + [toolbarItems addObject:item];
|
| + }
|
| +
|
| + return toolbarItems;
|
| +}
|
| +
|
| +// Finds all UIToolbarItem(s) with action selectors of the name specified by
|
| +// |actionName| in any UIToolbars in the view hierarchy below |root|.
|
| +NSArray* FindDescendantToolbarItemsForActionName(UIView* root,
|
| + NSString* actionName) {
|
| + NSMutableArray* descendants = [NSMutableArray array];
|
| +
|
| + NSArray* toolbars = FindDescendantsOfClass(root, [UIToolbar class]);
|
| + for (UIToolbar* toolbar in toolbars) {
|
| + [descendants
|
| + addObjectsFromArray:FindToolbarItemsForActionName(toolbar, actionName)];
|
| + }
|
| +
|
| + return descendants;
|
| +}
|
| +
|
| +// Computes the frame of each part of the accessory view of the keyboard. It is
|
| +// assumed that the keyboard has either two parts (when it is split) or one part
|
| +// (when it is merged).
|
| +//
|
| +// If there are two parts, the frame of the left part is returned in
|
| +// |leftFrame| and the frame of the right part is returned in |rightFrame|.
|
| +// If there is only one part, the frame is returned in |leftFrame| and
|
| +// |rightFrame| has size zero.
|
| +//
|
| +// Heuristics are used to compute this information. It returns true if the
|
| +// number of |inputAccessoryView.subviews| is not 2.
|
| +bool ComputeFramesOfKeyboardParts(UIView* inputAccessoryView,
|
| + CGRect* leftFrame,
|
| + CGRect* rightFrame) {
|
| + // It is observed (on iOS 6) there are always two subviews in the original
|
| + // input accessory view. When the keyboard is split, each subview represents
|
| + // one part of the accesssary view of the keyboard. When the keyboard is
|
| + // merged, one subview has the same frame as that of the whole accessory view
|
| + // and the other has zero size with the screen width as origin.x.
|
| + // The computation here is based on this observation.
|
| + NSArray* subviews = inputAccessoryView.subviews;
|
| + if (subviews.count != 2)
|
| + return false;
|
| +
|
| + CGRect first_frame = static_cast<UIView*>(subviews[0]).frame;
|
| + CGRect second_frame = static_cast<UIView*>(subviews[1]).frame;
|
| + if (CGRectGetMinX(first_frame) < CGRectGetMinX(second_frame) ||
|
| + CGRectGetWidth(second_frame) == 0) {
|
| + *leftFrame = first_frame;
|
| + *rightFrame = second_frame;
|
| + } else {
|
| + *rightFrame = first_frame;
|
| + *leftFrame = second_frame;
|
| + }
|
| + return true;
|
| +}
|
| +
|
| +} // namespace
|
| +
|
| +@interface FormInputAccessoryViewController ()
|
| +
|
| +// Allows injection of the JsSuggestionManager.
|
| +- (instancetype)initWithWebState:(web::WebState*)webState
|
| + JSSuggestionManager:(JsSuggestionManager*)JSSuggestionManager
|
| + providers:(NSArray*)providers;
|
| +
|
| +// Called when the keyboard did change frame.
|
| +- (void)keyboardDidChangeFrame:(NSNotification*)notification;
|
| +
|
| +// Called when the keyboard is dismissed.
|
| +- (void)keyboardDidHide:(NSNotification*)notification;
|
| +
|
| +// Hides the subviews in |accessoryView|.
|
| +- (void)hideSubviewsInOriginalAccessoryView:(UIView*)accessoryView;
|
| +
|
| +// Attempts to execute/tap/send-an-event-to the iOS built-in "next" and
|
| +// "previous" form assist controls. Returns NO if this attempt failed, YES
|
| +// otherwise. [HACK]
|
| +- (BOOL)executeFormAssistAction:(NSString*)actionName;
|
| +
|
| +// Runs |block| while allowing the keyboard to be displayed as a result of focus
|
| +// changes caused by |block|.
|
| +- (void)runBlockAllowingKeyboardDisplay:(ProceduralBlock)block;
|
| +
|
| +// Asynchronously retrieves an accessory view from |_providers|.
|
| +- (void)retrieveAccessoryViewForForm:(const std::string&)formName
|
| + field:(const std::string&)fieldName
|
| + value:(const std::string&)value
|
| + type:(const std::string&)type
|
| + webState:(web::WebState*)webState;
|
| +
|
| +// Clears the current custom accessory view and restores the default.
|
| +- (void)reset;
|
| +
|
| +// The current web state.
|
| +@property(nonatomic, readonly) web::WebState* webState;
|
| +
|
| +// The current web view proxy.
|
| +@property(nonatomic, readonly) id<CRWWebViewProxy> webViewProxy;
|
| +
|
| +@end
|
| +
|
| +@implementation FormInputAccessoryViewController {
|
| + // Bridge to observe the web state from Objective-C.
|
| + scoped_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
|
| +
|
| + // Last registered keyboard rectangle.
|
| + CGRect _keyboardFrame;
|
| +
|
| + // The custom view that should be shown in the input accessory view.
|
| + base::scoped_nsobject<UIView> _customAccessoryView;
|
| +
|
| + // The JS manager for interacting with the underlying form.
|
| + base::scoped_nsobject<JsSuggestionManager> _JSSuggestionManager;
|
| +
|
| + // The original subviews in keyboard accessory view that were originally not
|
| + // hidden but were hidden when showing Autofill suggestions.
|
| + base::scoped_nsobject<NSMutableArray> _hiddenOriginalSubviews;
|
| +
|
| + // The objects that can provide a custom input accessory view while filling
|
| + // forms.
|
| + base::scoped_nsobject<NSArray> _providers;
|
| +
|
| + // The object that manages the currently-shown custom accessory view.
|
| + base::WeakNSProtocol<id<FormInputAccessoryViewProvider>> _currentProvider;
|
| +}
|
| +
|
| +- (instancetype)initWithWebState:(web::WebState*)webState
|
| + providers:(NSArray*)providers {
|
| + JsSuggestionManager* suggestionManager =
|
| + base::mac::ObjCCastStrict<JsSuggestionManager>(
|
| + [webState->GetJSInjectionReceiver()
|
| + instanceOfClass:[JsSuggestionManager class]]);
|
| + return [self initWithWebState:webState
|
| + JSSuggestionManager:suggestionManager
|
| + providers:providers];
|
| +}
|
| +
|
| +- (instancetype)initWithWebState:(web::WebState*)webState
|
| + JSSuggestionManager:(JsSuggestionManager*)JSSuggestionManager
|
| + providers:(NSArray*)providers {
|
| + self = [super init];
|
| + if (self) {
|
| + _JSSuggestionManager.reset([JSSuggestionManager retain]);
|
| + _hiddenOriginalSubviews.reset([[NSMutableArray alloc] init]);
|
| + _webStateObserverBridge.reset(
|
| + new web::WebStateObserverBridge(webState, self));
|
| + _providers.reset([providers copy]);
|
| + // There is no defined relation on the timing of JavaScript events and
|
| + // keyboard showing up. So it is necessary to listen to the keyboard
|
| + // notification to make sure the keyboard is updated.
|
| + [[NSNotificationCenter defaultCenter]
|
| + addObserver:self
|
| + selector:@selector(keyboardDidChangeFrame:)
|
| + name:UIKeyboardDidChangeFrameNotification
|
| + object:nil];
|
| + [[NSNotificationCenter defaultCenter]
|
| + addObserver:self
|
| + selector:@selector(keyboardDidHide:)
|
| + name:UIKeyboardDidHideNotification
|
| + object:nil];
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +- (void)dealloc {
|
| + [[NSNotificationCenter defaultCenter] removeObserver:self];
|
| + [super dealloc];
|
| +}
|
| +
|
| +- (web::WebState*)webState {
|
| + return _webStateObserverBridge ? _webStateObserverBridge->web_state()
|
| + : nullptr;
|
| +}
|
| +
|
| +- (id<CRWWebViewProxy>)webViewProxy {
|
| + return self.webState ? self.webState->GetWebViewProxy() : nil;
|
| +}
|
| +
|
| +- (void)hideSubviewsInOriginalAccessoryView:(UIView*)accessoryView {
|
| + for (UIView* subview in [accessoryView subviews]) {
|
| + if (!subview.hidden) {
|
| + [_hiddenOriginalSubviews addObject:subview];
|
| + subview.hidden = YES;
|
| + }
|
| + }
|
| +}
|
| +
|
| +- (void)showCustomInputAccessoryView:(UIView*)view {
|
| + [self restoreDefaultInputAccessoryView];
|
| + CGRect leftFrame;
|
| + CGRect rightFrame;
|
| + UIView* inputAccessoryView = [self.webViewProxy getKeyboardAccessory];
|
| + if (ComputeFramesOfKeyboardParts(inputAccessoryView, &leftFrame,
|
| + &rightFrame)) {
|
| + [self hideSubviewsInOriginalAccessoryView:inputAccessoryView];
|
| + _customAccessoryView.reset(
|
| + [[FormInputAccessoryView alloc] initWithFrame:inputAccessoryView.frame
|
| + delegate:self
|
| + customView:view
|
| + leftFrame:leftFrame
|
| + rightFrame:rightFrame]);
|
| + [inputAccessoryView addSubview:_customAccessoryView];
|
| + }
|
| +}
|
| +
|
| +- (void)restoreDefaultInputAccessoryView {
|
| + [_customAccessoryView removeFromSuperview];
|
| + _customAccessoryView.reset();
|
| + for (UIView* subview in _hiddenOriginalSubviews.get()) {
|
| + subview.hidden = NO;
|
| + }
|
| + [_hiddenOriginalSubviews removeAllObjects];
|
| +}
|
| +
|
| +- (void)closeKeyboard {
|
| + BOOL performedAction =
|
| + [self executeFormAssistAction:ios_internal::autofill::
|
| + kFormSuggestionAssistButtonDone];
|
| +
|
| + if (!performedAction) {
|
| + // We could not find the built-in form assist controls, so try to focus
|
| + // the next or previous control using JavaScript.
|
| + [self runBlockAllowingKeyboardDisplay:^{
|
| + [_JSSuggestionManager closeKeyboard];
|
| + }];
|
| + }
|
| +}
|
| +
|
| +- (BOOL)executeFormAssistAction:(NSString*)actionName {
|
| + UIView* inputAccessoryView = [self.webViewProxy getKeyboardAccessory];
|
| + if (!inputAccessoryView)
|
| + return NO;
|
| +
|
| + NSArray* descendants =
|
| + FindDescendantToolbarItemsForActionName(inputAccessoryView, actionName);
|
| +
|
| + if (![descendants count])
|
| + return NO;
|
| +
|
| + UIBarButtonItem* item = descendants[0];
|
| + [[item target] performSelector:[item action] withObject:item];
|
| + return YES;
|
| +}
|
| +
|
| +- (void)runBlockAllowingKeyboardDisplay:(ProceduralBlock)block {
|
| + DCHECK([UIWebView
|
| + instancesRespondToSelector:@selector(keyboardDisplayRequiresUserAction)]);
|
| +
|
| + BOOL originalValue = [self.webViewProxy keyboardDisplayRequiresUserAction];
|
| + [self.webViewProxy setKeyboardDisplayRequiresUserAction:NO];
|
| + block();
|
| + [self.webViewProxy setKeyboardDisplayRequiresUserAction:originalValue];
|
| +}
|
| +
|
| +#pragma mark -
|
| +#pragma mark FormInputAccessoryViewDelegate
|
| +
|
| +- (void)selectPreviousElement {
|
| + BOOL performedAction = [self
|
| + executeFormAssistAction:ios_internal::autofill::
|
| + kFormSuggestionAssistButtonPreviousElement];
|
| + if (!performedAction) {
|
| + // We could not find the built-in form assist controls, so try to focus
|
| + // the next or previous control using JavaScript.
|
| + [self runBlockAllowingKeyboardDisplay:^{
|
| + [_JSSuggestionManager selectPreviousElement];
|
| + }];
|
| + }
|
| +}
|
| +
|
| +- (void)selectNextElement {
|
| + BOOL performedAction =
|
| + [self executeFormAssistAction:ios_internal::autofill::
|
| + kFormSuggestionAssistButtonNextElement];
|
| +
|
| + if (!performedAction) {
|
| + // We could not find the built-in form assist controls, so try to focus
|
| + // the next or previous control using JavaScript.
|
| + [self runBlockAllowingKeyboardDisplay:^{
|
| + [_JSSuggestionManager selectNextElement];
|
| + }];
|
| + }
|
| +}
|
| +
|
| +- (void)fetchPreviousAndNextElementsPresenceWithCompletionHandler:
|
| + (void (^)(BOOL, BOOL))completionHandler {
|
| + DCHECK(completionHandler);
|
| + [_JSSuggestionManager
|
| + fetchPreviousAndNextElementsPresenceWithCompletionHandler:
|
| + completionHandler];
|
| +}
|
| +
|
| +#pragma mark -
|
| +#pragma mark CRWWebStateObserver
|
| +
|
| +- (void)pageLoaded:(web::WebState*)webState {
|
| + [self reset];
|
| +}
|
| +
|
| +- (void)formActivity:(web::WebState*)webState
|
| + formName:(const std::string&)formName
|
| + fieldName:(const std::string&)fieldName
|
| + type:(const std::string&)type
|
| + value:(const std::string&)value
|
| + keyCode:(int)keyCode
|
| + error:(BOOL)error {
|
| + web::URLVerificationTrustLevel trustLevel;
|
| + const GURL pageURL(webState->GetCurrentURL(&trustLevel));
|
| + if (error || trustLevel != web::URLVerificationTrustLevel::kAbsolute ||
|
| + !web::UrlHasWebScheme(pageURL) || !webState->ContentIsHTML()) {
|
| + [self reset];
|
| + return;
|
| + }
|
| +
|
| + if ((type == "blur" || type == "change")) {
|
| + return;
|
| + }
|
| +
|
| + [self retrieveAccessoryViewForForm:formName
|
| + field:fieldName
|
| + value:value
|
| + type:type
|
| + webState:webState];
|
| +}
|
| +
|
| +- (void)webStateDestroyed:(web::WebState*)webState {
|
| + [self reset];
|
| + _webStateObserverBridge.reset();
|
| +}
|
| +
|
| +- (void)reset {
|
| + if (_currentProvider) {
|
| + [_currentProvider inputAccessoryViewControllerDidReset:self];
|
| + _currentProvider.reset();
|
| + }
|
| + [self restoreDefaultInputAccessoryView];
|
| +}
|
| +
|
| +- (void)retrieveAccessoryViewForForm:(const std::string&)formName
|
| + field:(const std::string&)fieldName
|
| + value:(const std::string&)value
|
| + type:(const std::string&)type
|
| + webState:(web::WebState*)webState {
|
| + base::WeakNSObject<FormInputAccessoryViewController> weakSelf(self);
|
| + std::string strongFormName = formName;
|
| + std::string strongFieldName = fieldName;
|
| + std::string strongValue = value;
|
| + std::string strongType = type;
|
| +
|
| + // Build a block for each provider that will invoke its completion with YES
|
| + // if the provider can provide an accessory view for the specified form/field
|
| + // and NO otherwise.
|
| + base::scoped_nsobject<NSMutableArray> findProviderBlocks(
|
| + [[NSMutableArray alloc] init]);
|
| + for (NSUInteger i = 0; i < [_providers count]; i++) {
|
| + base::mac::ScopedBlock<passwords::PipelineBlock> block(
|
| + ^(void (^completion)(BOOL success)) {
|
| + // Access all the providers through |self| to guarantee that both
|
| + // |self| and all the providers exist when the block is executed.
|
| + // |_providers| is immutable, so the subscripting is always valid.
|
| + base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
|
| + [weakSelf retain]);
|
| + if (!strongSelf)
|
| + return;
|
| + id<FormInputAccessoryViewProvider> provider =
|
| + strongSelf.get()->_providers[i];
|
| + [provider checkIfAccessoryViewAvailableForFormNamed:strongFormName
|
| + fieldName:strongFieldName
|
| + webState:webState
|
| + completionHandler:completion];
|
| + },
|
| + base::scoped_policy::RETAIN);
|
| + [findProviderBlocks addObject:block];
|
| + }
|
| +
|
| + // Once the view is retrieved, update the UI.
|
| + AccessoryViewReadyCompletion readyCompletion =
|
| + ^(UIView* accessoryView, id<FormInputAccessoryViewProvider> provider) {
|
| + base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
|
| + [weakSelf retain]);
|
| + if (!strongSelf || !strongSelf.get()->_currentProvider)
|
| + return;
|
| + DCHECK_EQ(strongSelf.get()->_currentProvider.get(), provider);
|
| + [provider setAccessoryViewDelegate:strongSelf];
|
| + [strongSelf showCustomInputAccessoryView:accessoryView];
|
| + };
|
| +
|
| + // Once a provider is found, use it to retrieve the accessory view.
|
| + passwords::PipelineCompletionBlock onProviderFound =
|
| + ^(NSUInteger providerIndex) {
|
| + if (providerIndex == NSNotFound) {
|
| + [weakSelf reset];
|
| + return;
|
| + }
|
| + base::scoped_nsobject<FormInputAccessoryViewController> strongSelf(
|
| + [weakSelf retain]);
|
| + if (!strongSelf || ![strongSelf webState])
|
| + return;
|
| + id<FormInputAccessoryViewProvider> provider =
|
| + strongSelf.get()->_providers[providerIndex];
|
| + [strongSelf.get()->_currentProvider
|
| + inputAccessoryViewControllerDidReset:self];
|
| + strongSelf.get()->_currentProvider.reset(provider);
|
| + [strongSelf.get()->_currentProvider
|
| + retrieveAccessoryViewForFormNamed:strongFormName
|
| + fieldName:strongFieldName
|
| + value:strongValue
|
| + type:strongType
|
| + webState:webState
|
| + completionHandler:readyCompletion];
|
| + };
|
| +
|
| + // Run all the blocks in |findProviderBlocks| until one invokes its
|
| + // completion with YES. The first one to do so will be passed to
|
| + // |onProviderFound|.
|
| + passwords::RunSearchPipeline(findProviderBlocks, onProviderFound);
|
| +}
|
| +
|
| +- (void)keyboardDidChangeFrame:(NSNotification*)notification {
|
| + if (!self.webState || !_currentProvider)
|
| + return;
|
| + CGRect keyboardFrame =
|
| + [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
| + // With iOS8 (beta) this method can be called even when the rect has not
|
| + // changed. When this is detected we exit early.
|
| + if (CGRectEqualToRect(CGRectIntegral(_keyboardFrame),
|
| + CGRectIntegral(keyboardFrame))) {
|
| + return;
|
| + }
|
| + _keyboardFrame = keyboardFrame;
|
| + [_currentProvider resizeAccessoryView];
|
| +}
|
| +
|
| +- (void)keyboardDidHide:(NSNotification*)notification {
|
| + _keyboardFrame = CGRectZero;
|
| +}
|
| +
|
| +@end
|
|
|