diff options
author | droger <droger@chromium.org> | 2015-03-23 10:51:03 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-03-23 17:51:56 +0000 |
commit | 69b5fc8fc036b5ade5e1498d368ef5c49fb74e48 (patch) | |
tree | 928eef46c2a61c848b40c8b2cabab66cfa2686a0 /ios/chrome | |
parent | 744256e2f3933feaa05c967541afac36825056fa (diff) | |
download | chromium_src-69b5fc8fc036b5ade5e1498d368ef5c49fb74e48.zip chromium_src-69b5fc8fc036b5ade5e1498d368ef5c49fb74e48.tar.gz chromium_src-69b5fc8fc036b5ade5e1498d368ef5c49fb74e48.tar.bz2 |
[iOS] Upstream files in //ios/chrome/browser/autofill
BUG=437508
Review URL: https://codereview.chromium.org/1022463002
Cr-Commit-Position: refs/heads/master@{#321805}
Diffstat (limited to 'ios/chrome')
21 files changed, 2188 insertions, 0 deletions
diff --git a/ios/chrome/DEPS b/ios/chrome/DEPS index a33b946..f809f8d 100644 --- a/ios/chrome/DEPS +++ b/ios/chrome/DEPS @@ -4,6 +4,8 @@ include_rules = [ "-ios/chrome", "+ios/chrome/grit", + "+components/autofill/core/browser", + "+components/autofill/ios/browser", "+components/dom_distiller/core", "+components/dom_distiller/ios", "+components/infobars/core", diff --git a/ios/chrome/browser/autofill/OWNERS b/ios/chrome/browser/autofill/OWNERS new file mode 100644 index 0000000..cdbac76f --- /dev/null +++ b/ios/chrome/browser/autofill/OWNERS @@ -0,0 +1 @@ +dconnelly@chromium.org diff --git a/ios/chrome/browser/autofill/autofill_agent_utils.h b/ios/chrome/browser/autofill/autofill_agent_utils.h new file mode 100644 index 0000000..d8f6c13 --- /dev/null +++ b/ios/chrome/browser/autofill/autofill_agent_utils.h @@ -0,0 +1,35 @@ +// Copyright 2013 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. +#ifndef IOS_CHROME_BROWSER_AUTOFILL_AUTOFILL_AGENT_UTILS_H_ +#define IOS_CHROME_BROWSER_AUTOFILL_AUTOFILL_AGENT_UTILS_H_ + +// TODO (sgrant): Switch to componentized version of this code when +// http://crbug/328070 is fixed. + +namespace autofill { +class FormStructure; +} + +namespace autofill_agent_util { + +// Determines if the |structure| has any fields that are of type +// autofill::CREDIT_CARD and thus asking for credit card info. +bool RequestingCreditCardInfo(const autofill::FormStructure* structure); + +// Returns true if one of the nodes in |structure| request information related +// to a billing address. +bool RequestFullBillingAddress(autofill::FormStructure* structure); + +// Returns true if one of the nodes in |structure| request information related +// to a shipping address. To determine this actually attempt to fill the form +// using an empty data model that tracks which fields are requested. +bool RequestShippingAddress(autofill::FormStructure* structure); + +// Returns true if one of the nodes in |structure| request information related +// to a phone number. +bool RequestPhoneNumber(autofill::FormStructure* structure); + +} // namespace autofill_agent_util + +#endif // IOS_CHROME_BROWSER_AUTOFILL_AUTOFILL_AGENT_UTILS_H_ diff --git a/ios/chrome/browser/autofill/autofill_agent_utils.mm b/ios/chrome/browser/autofill/autofill_agent_utils.mm new file mode 100644 index 0000000..33e7ddd --- /dev/null +++ b/ios/chrome/browser/autofill/autofill_agent_utils.mm @@ -0,0 +1,139 @@ +// Copyright 2013 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/autofill_agent_utils.h" + +#include "base/bind.h" +#include "base/macros.h" +#include "components/autofill/core/browser/autofill_manager.h" +#include "components/autofill/core/browser/detail_input.h" +#include "components/autofill/core/browser/dialog_section.h" +#include "components/autofill/core/browser/server_field_types_util.h" +#include "grit/components_strings.h" +#include "ios/chrome/browser/application_context.h" +#include "ui/base/l10n/l10n_util.h" + +// TODO (sgrant): Switch to componentized version of this code when +// http://crbug/328070 is fixed. +// This code was largely copied from autofill_dialog_controller_android.cc + +namespace { + +// Returns true if |input_type| in |section| is needed for |form_structure|. +bool IsSectionInputUsedInFormStructure( + autofill::DialogSection section, + autofill::ServerFieldType input_type, + const autofill::FormStructure& form_structure) { + autofill::DetailInput input; + input.length = autofill::DetailInput::SHORT; + input.type = input_type; + input.placeholder_text = base::string16(); + input.expand_weight = 0; + + for (size_t i = 0; i < form_structure.field_count(); ++i) { + const autofill::AutofillField* field = form_structure.field(i); + if (field && autofill::ServerTypeMatchesField(section, input.type, *field)) + return true; + } + return false; +} + +} // namespace + +namespace autofill_agent_util { + +// Determines if the |structure| has any fields that are of type +// autofill::CREDIT_CARD and thus asking for credit card info. +bool RequestingCreditCardInfo(const autofill::FormStructure* structure) { + DCHECK(structure); + + size_t field_count = structure->field_count(); + for (size_t i = 0; i < field_count; ++i) { + autofill::AutofillType type(structure->field(i)->Type().GetStorableType()); + if (type.group() == autofill::CREDIT_CARD) + return true; + } + + return false; +} + +// Returns true if one of the nodes in |structure| request information related +// to a billing address. +bool RequestFullBillingAddress(autofill::FormStructure* structure) { + const autofill::ServerFieldType fieldsToCheckFor[] = { + autofill::ADDRESS_BILLING_LINE1, + autofill::ADDRESS_BILLING_LINE2, + autofill::ADDRESS_BILLING_CITY, + autofill::ADDRESS_BILLING_STATE, + autofill::PHONE_BILLING_WHOLE_NUMBER}; + + for (size_t i = 0; i < arraysize(fieldsToCheckFor); ++i) { + if (IsSectionInputUsedInFormStructure(autofill::SECTION_BILLING, + fieldsToCheckFor[i], *structure)) { + return true; + } + } + + return false; +} + +// Return empty info string for fill fields method. +base::string16 ReturnEmptyInfo(const autofill::AutofillType& type) { + return base::string16(); +} + +// Returns true if one of the nodes in |structure| request information related +// to a shipping address. To determine this actually attempt to fill the form +// using an empty data model that tracks which fields are requested. +bool RequestShippingAddress(autofill::FormStructure* structure) { + // Country code is unused for iOS and Android, so it + // doesn't matter what's passed. + std::string country_code; + autofill::DetailInputs inputs; + // TODO(eugenebut): Clean up kShippingInputs definition, unify with + // android codebase. crbug.com/371074 + const autofill::DetailInput kShippingInputs[] = { + {autofill::DetailInput::LONG, autofill::NAME_FULL}, + {autofill::DetailInput::LONG, autofill::ADDRESS_HOME_LINE1}, + {autofill::DetailInput::LONG, autofill::ADDRESS_HOME_LINE2}, + {autofill::DetailInput::LONG, autofill::ADDRESS_HOME_CITY}, + {autofill::DetailInput::SHORT, + autofill::ADDRESS_HOME_STATE, + l10n_util::GetStringUTF16(IDS_AUTOFILL_FIELD_LABEL_STATE)}, + {autofill::DetailInput::SHORT_EOL, autofill::ADDRESS_HOME_ZIP}, + {autofill::DetailInput::NONE, autofill::ADDRESS_HOME_COUNTRY}, + }; + autofill::BuildInputs(kShippingInputs, arraysize(kShippingInputs), &inputs); + + // TODO(ios): [Merge r284576]: The 4th argument to FillFields() + // is the language code based on either the billing or shipping address. + // See implementation in upstream's autofill_dialog_controller_impl.cc + // AutofillDialogControllerImpl::MutableAddressLanguageCodeForSection() + // Temporarily using std::string here to complete merge. + // See http://crbug/363063. + return structure->FillFields( + autofill::TypesFromInputs(inputs), + base::Bind(autofill::ServerTypeMatchesField, autofill::SECTION_SHIPPING), + base::Bind(&ReturnEmptyInfo), std::string(), + GetApplicationContext()->GetApplicationLocale()); +} + +// Returns true if one of the nodes in |structure| request information related +// to a phone number. +bool RequestPhoneNumber(autofill::FormStructure* structure) { + if (IsSectionInputUsedInFormStructure(autofill::SECTION_BILLING, + autofill::PHONE_BILLING_WHOLE_NUMBER, + *structure)) { + return true; + } + + if (IsSectionInputUsedInFormStructure(autofill::SECTION_SHIPPING, + autofill::PHONE_HOME_WHOLE_NUMBER, + *structure)) { + return true; + } + + return false; +} +} diff --git a/ios/chrome/browser/autofill/form_input_accessory_view.h b/ios/chrome/browser/autofill/form_input_accessory_view.h new file mode 100644 index 0000000..b8992a5 --- /dev/null +++ b/ios/chrome/browser/autofill/form_input_accessory_view.h @@ -0,0 +1,29 @@ +// 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. + +#ifndef IOS_CHROME_BROWSER_AUTOFILL_FORM_INPUT_ACCESSORY_VIEW_H_ +#define IOS_CHROME_BROWSER_AUTOFILL_FORM_INPUT_ACCESSORY_VIEW_H_ + +#import <UIKit/UIKit.h> + +@protocol FormInputAccessoryViewDelegate; + +// Subview of the accessory view for web forms. Shows a custom view with form +// navigation controls above the keyboard. Subclassed to enable input clicks by +// way of the playInputClick method. +@interface FormInputAccessoryView : UIView<UIInputViewAudioFeedback> + +// Initializes with |frame| and |delegate| to show |customView|. If the size of +// |rightFrame| is non-zero, the view will have two parts: the left one has +// frame |leftFrame| and the right one has frame |rightFrame|. Otherwise the +// view will be shown in |leftFrame|. +- (instancetype)initWithFrame:(CGRect)frame + delegate:(id<FormInputAccessoryViewDelegate>)delegate + customView:(UIView*)customView + leftFrame:(CGRect)leftFrame + rightFrame:(CGRect)rightFrame; + +@end + +#endif // IOS_CHROME_BROWSER_AUTOFILL_FORM_INPUT_ACCESSORY_VIEW_H_ diff --git a/ios/chrome/browser/autofill/form_input_accessory_view.mm b/ios/chrome/browser/autofill/form_input_accessory_view.mm new file mode 100644 index 0000000..a794ab4 --- /dev/null +++ b/ios/chrome/browser/autofill/form_input_accessory_view.mm @@ -0,0 +1,371 @@ +// 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.h" + +#import <QuartzCore/QuartzCore.h> + +#include "base/i18n/rtl.h" +#include "base/ios/weak_nsobject.h" +#include "base/mac/scoped_nsobject.h" +#import "ios/chrome/browser/autofill/form_input_accessory_view_delegate.h" +#import "ios/chrome/browser/ui/image_util.h" +#include "ios/chrome/browser/ui/ui_util.h" + +namespace { + +// The alpha value of the background color. +const CGFloat kBackgroundColorAlpha = 1.0; + +// Horizontal margin around the custom view. +const CGFloat kCustomViewHorizontalMargin = 2; + +// The width of the previous and next buttons. +const CGFloat kNavigationButtonWidth = 44; + +// The width of the separators of the previous and next buttons. +const CGFloat kNavigationButtonSeparatorWidth = 1; + +// The width of the shadow part of the navigation area separator. +const CGFloat kNavigationAreaSeparatorShadowWidth = 2; + +// The width of the navigation area / custom view separator asset. +const CGFloat kNavigationAreaSeparatorWidth = 1; + +// Returns YES if the keyboard close button should be shown on the accessory. +BOOL ShouldShowCloseButton() { + return !IsIPadIdiom(); +} + +// Returns the width of navigation view. +CGFloat GetNavigationViewWidth() { + // The number of naviation buttons (includes close button if shown). + NSUInteger numberNavigationButtons = 2; + if (ShouldShowCloseButton()) + numberNavigationButtons++; + return numberNavigationButtons * kNavigationButtonWidth + + (numberNavigationButtons - 1) * kNavigationButtonSeparatorWidth + + kNavigationAreaSeparatorWidth; +} + +} // namespace + +@interface FormInputAccessoryView () + +// Initializes the view with the given |customView|. +// If the size of |rightFrame| is non-zero, the view will be split into two +// parts with |leftFrame| and |rightFrame|. Otherwise the Autofill view will +// be shown in |leftFrame|. +- (void)initializeViewWithCustomView:(UIView*)customView + leftFrame:(CGRect)leftFrame + rightFrame:(CGRect)rightFrame; + +// Returns a view that shows navigation buttons in the |frame|. +- (UIView*)viewForNavigationButtonsInFrame:(CGRect)frame; + +// Returns a navigation button for Autofill that has |normalImage| for state +// UIControlStateNormal, a |pressedImage| for states UIControlStateSelected and +// UIControlStateHighlighted, and an optional |disabledImage| for +// UIControlStateDisabled. +- (UIButton*)keyboardNavButtonWithNormalImage:(UIImage*)normalImage + pressedImage:(UIImage*)pressedImage + disabledImage:(UIImage*)disabledImage + target:(id)target + action:(SEL)action + enabled:(BOOL)enabled + originX:(CGFloat)originX + originY:(CGFloat)originY + height:(CGFloat)height; + +// Adds a background image to |view|. The supplied image is stretched to fit the +// space by stretching the content its horizontal and vertical centers. ++ (void)addBackgroundImageInView:(UIView*)view + withImageName:(NSString*)imageName; + +// Adds an image view in |view| with an image named |imageName| at +// (|originX|, 0). The width is |width| and the height is the height of |view|. ++ (void)addImageViewWithImageName:(NSString*)imageName + originX:(CGFloat)originX + originY:(CGFloat)originY + width:(CGFloat)width + inView:(UIView*)view; + +@end + +@implementation FormInputAccessoryView { + // The custom view that is displayed in the input accessory view. + base::scoped_nsobject<UIView> _customView; + + // Delegate of this view. + base::WeakNSProtocol<id<FormInputAccessoryViewDelegate>> _delegate; +} + +- (instancetype)initWithFrame:(CGRect)frame + delegate:(id<FormInputAccessoryViewDelegate>)delegate + customView:(UIView*)customView + leftFrame:(CGRect)leftFrame + rightFrame:(CGRect)rightFrame { + DCHECK(delegate); + self = [super initWithFrame:frame]; + if (self) { + _delegate.reset(delegate); + _customView.reset([customView retain]); + [self initializeViewWithCustomView:_customView + leftFrame:leftFrame + rightFrame:rightFrame]; + } + return self; +} + +#pragma mark - +#pragma mark UIInputViewAudioFeedback + +- (BOOL)enableInputClicksWhenVisible { + return YES; +} + +#pragma mark - +#pragma mark Private Methods + +- (void)initializeViewWithCustomView:(UIView*)customView + leftFrame:(CGRect)leftFrame + rightFrame:(CGRect)rightFrame { + UIView* customViewContainer = [[[UIView alloc] init] autorelease]; + [self addSubview:customViewContainer]; + UIView* navView = [[[UIView alloc] init] autorelease]; + [self addSubview:navView]; + + bool splitKeyboard = CGRectGetWidth(rightFrame) != 0; + BOOL isRTL = base::i18n::IsRTL(); + + // The computed frame for |customView|. + CGRect customViewFrame; + // Frame of a subview of |navView| in which navigation buttons will be shown. + CGRect navFrame = CGRectZero; + if (splitKeyboard) { + NSString* navViewBackgroundImageName = nil; + NSString* customViewContainerBackgroundImageName = nil; + NSUInteger navFrameOriginX = 0; + if (isRTL) { + navView.frame = leftFrame; + navViewBackgroundImageName = @"autofill_keyboard_background_left"; + customViewContainer.frame = rightFrame; + customViewContainerBackgroundImageName = + @"autofill_keyboard_background_right"; + // Navigation buttons will be shown on the left side. + navFrameOriginX = 0; + } else { + customViewContainer.frame = leftFrame; + customViewContainerBackgroundImageName = + @"autofill_keyboard_background_left"; + navView.frame = rightFrame; + navViewBackgroundImageName = @"autofill_keyboard_background_right"; + // Navigation buttons will be shown on the right side. + navFrameOriginX = + CGRectGetWidth(navView.frame) - GetNavigationViewWidth(); + } + + [[self class] + addBackgroundImageInView:customViewContainer + withImageName:customViewContainerBackgroundImageName]; + [[self class] addBackgroundImageInView:navView + withImageName:navViewBackgroundImageName]; + + // For RTL, the custom view is the right view; the padding should be at the + // left side of this view. Otherwise, the custom view is the left view + // and the space is at the right side. + customViewFrame = CGRectMake(isRTL ? kCustomViewHorizontalMargin : 0, 0, + CGRectGetWidth(customViewContainer.bounds) - + kCustomViewHorizontalMargin, + CGRectGetHeight(customViewContainer.bounds)); + navFrame = CGRectMake(navFrameOriginX, 0, GetNavigationViewWidth(), + CGRectGetHeight(navView.frame)); + } else { + NSUInteger navViewFrameOriginX = 0; + NSUInteger customViewContainerFrameOrginX = 0; + if (isRTL) { + navViewFrameOriginX = kNavigationAreaSeparatorShadowWidth; + customViewContainerFrameOrginX = GetNavigationViewWidth(); + } else { + navViewFrameOriginX = + CGRectGetWidth(leftFrame) - GetNavigationViewWidth(); + } + + customViewContainer.frame = + CGRectMake(customViewContainerFrameOrginX, 0, + CGRectGetWidth(leftFrame) - GetNavigationViewWidth() + + kNavigationAreaSeparatorShadowWidth, + CGRectGetHeight(leftFrame)); + navView.frame = CGRectMake(navViewFrameOriginX, 0, GetNavigationViewWidth(), + CGRectGetHeight(leftFrame)); + + customViewFrame = customViewContainer.bounds; + navFrame = navView.bounds; + [[self class] addBackgroundImageInView:self + withImageName:@"autofill_keyboard_background"]; + } + + [customView setFrame:customViewFrame]; + [customViewContainer addSubview:customView]; + [navView addSubview:[self viewForNavigationButtonsInFrame:navFrame]]; +} + +UIImage* ButtonImage(NSString* name) { + UIImage* rawImage = [UIImage imageNamed:name]; + return StretchableImageFromUIImage(rawImage, 1, 0); +} + +- (UIView*)viewForNavigationButtonsInFrame:(CGRect)frame { + UIView* navView = [[[UIView alloc] initWithFrame:frame] autorelease]; + + BOOL isRTL = base::i18n::IsRTL(); + + // Vertical space is left for a dividing line. + CGFloat firstRow = 1; + + CGFloat currentX = 0; + + // Navigation view is at the right side if not RTL. Add a left separator in + // this case. + if (!isRTL) { + [[self class] addImageViewWithImageName:@"autofill_left_sep" + originX:currentX + originY:firstRow + width:kNavigationAreaSeparatorWidth + inView:navView]; + currentX = kNavigationAreaSeparatorWidth; + } + + UIButton* previousButton = [self + keyboardNavButtonWithNormalImage:ButtonImage(@"autofill_prev") + pressedImage:ButtonImage(@"autofill_prev_pressed") + disabledImage:ButtonImage(@"autofill_prev_inactive") + target:_delegate + action:@selector(selectPreviousElement) + enabled:NO + originX:currentX + originY:firstRow + height:CGRectGetHeight(frame)]; + [navView addSubview:previousButton]; + currentX += kNavigationButtonWidth; + + // Add internal separator. + [[self class] addImageViewWithImageName:@"autofill_middle_sep" + originX:currentX + originY:firstRow + width:kNavigationButtonSeparatorWidth + inView:navView]; + currentX += kNavigationButtonSeparatorWidth; + + UIButton* nextButton = [self + keyboardNavButtonWithNormalImage:ButtonImage(@"autofill_next") + pressedImage:ButtonImage(@"autofill_next_pressed") + disabledImage:ButtonImage(@"autofill_next_inactive") + target:_delegate + action:@selector(selectNextElement) + enabled:NO + originX:currentX + originY:firstRow + height:CGRectGetHeight(frame)]; + [navView addSubview:nextButton]; + currentX += kNavigationButtonWidth; + + [_delegate fetchPreviousAndNextElementsPresenceWithCompletionHandler: + ^(BOOL hasPreviousElement, BOOL hasNextElement) { + previousButton.enabled = hasPreviousElement; + nextButton.enabled = hasNextElement; + }]; + + if (ShouldShowCloseButton()) { + // Add internal separator. + [[self class] addImageViewWithImageName:@"autofill_middle_sep" + originX:currentX + originY:firstRow + width:kNavigationButtonSeparatorWidth + inView:navView]; + currentX += kNavigationButtonSeparatorWidth; + + [navView addSubview:[self + keyboardNavButtonWithNormalImage:ButtonImage(@"autofill_close") + pressedImage:ButtonImage(@"autofill_close_pressed") + disabledImage:nil + target:_delegate + action:@selector(closeKeyboard) + enabled:YES + originX:currentX + originY:firstRow + height:CGRectGetHeight(frame)]]; + currentX += kNavigationButtonWidth; + } + + // Navigation view is at the left side for RTL. Add a right separator in + // this case. + if (isRTL) { + [[self class] addImageViewWithImageName:@"autofill_right_sep" + originX:currentX + originY:firstRow + width:kNavigationAreaSeparatorWidth + inView:navView]; + } + + return navView; +} + +- (UIButton*)keyboardNavButtonWithNormalImage:(UIImage*)normalImage + pressedImage:(UIImage*)pressedImage + disabledImage:(UIImage*)disabledImage + target:(id)target + action:(SEL)action + enabled:(BOOL)enabled + originX:(CGFloat)originX + originY:(CGFloat)originY + height:(CGFloat)height { + UIButton* button = [UIButton buttonWithType:UIButtonTypeCustom]; + + button.frame = + CGRectMake(originX, originY, kNavigationButtonWidth, height - originY); + + [button setBackgroundImage:normalImage forState:UIControlStateNormal]; + [button setBackgroundImage:pressedImage forState:UIControlStateSelected]; + [button setBackgroundImage:pressedImage forState:UIControlStateHighlighted]; + if (disabledImage) + [button setBackgroundImage:disabledImage forState:UIControlStateDisabled]; + + CALayer* layer = [button layer]; + layer.borderWidth = 0; + layer.borderColor = [[UIColor blackColor] CGColor]; + button.enabled = enabled; + [button addTarget:target + action:action + forControlEvents:UIControlEventTouchUpInside]; + return button; +} + ++ (void)addBackgroundImageInView:(UIView*)view + withImageName:(NSString*)imageName { + UIImage* backgroundImage = StretchableImageNamed(imageName); + + UIImageView* backgroundImageView = + [[[UIImageView alloc] initWithFrame:view.bounds] autorelease]; + [backgroundImageView setImage:backgroundImage]; + [backgroundImageView setAlpha:kBackgroundColorAlpha]; + [view addSubview:backgroundImageView]; + [view sendSubviewToBack:backgroundImageView]; +} + ++ (void)addImageViewWithImageName:(NSString*)imageName + originX:(CGFloat)originX + originY:(CGFloat)originY + width:(CGFloat)width + inView:(UIView*)view { + UIImage* image = + StretchableImageFromUIImage([UIImage imageNamed:imageName], 0, 0); + base::scoped_nsobject<UIImageView> imageView( + [[UIImageView alloc] initWithImage:image]); + [imageView setFrame:CGRectMake(originX, originY, width, + CGRectGetHeight(view.bounds) - originY)]; + [view addSubview:imageView]; +} + +@end diff --git a/ios/chrome/browser/autofill/form_input_accessory_view_controller.h b/ios/chrome/browser/autofill/form_input_accessory_view_controller.h new file mode 100644 index 0000000..1ecb8ef --- /dev/null +++ b/ios/chrome/browser/autofill/form_input_accessory_view_controller.h @@ -0,0 +1,95 @@ +// 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. + +#ifndef IOS_CHROME_BROWSER_AUTOFILL_FORM_INPUT_ACCESSORY_VIEW_CONTROLLER_H_ +#define IOS_CHROME_BROWSER_AUTOFILL_FORM_INPUT_ACCESSORY_VIEW_CONTROLLER_H_ + +#import <UIKit/UIKit.h> + +#import "ios/chrome/browser/autofill/form_input_accessory_view_delegate.h" +#import "ios/web/public/web_state/web_state_observer_bridge.h" + +@protocol CRWWebViewProxy; + +namespace ios_internal { +namespace autofill { +extern NSString* const kFormSuggestionAssistButtonPreviousElement; +extern NSString* const kFormSuggestionAssistButtonNextElement; +extern NSString* const kFormSuggestionAssistButtonDone; +} // namespace autofill +} // namespace ios_internal + +@protocol FormInputAccessoryViewProvider; +@class FormInputAccessoryViewController; + +// Block type to indicate that a FormInputAccessoryViewProvider has an accessory +// view to provide. +typedef void (^AccessoryViewAvailableCompletion)( + BOOL inputAccessoryViewAvailable); + +// Block type to provide an accessory view asynchronously. +typedef void (^AccessoryViewReadyCompletion)( + UIView* view, + id<FormInputAccessoryViewProvider> provider); + +// Represents an object that can provide a custom keyboard input accessory view. +@protocol FormInputAccessoryViewProvider<NSObject> + +// A delegate for form navigation. +@property(nonatomic, assign) + id<FormInputAccessoryViewDelegate> accessoryViewDelegate; + +// Determines asynchronously if this provider has a view available for the +// specified form/field and invokes |completionHandler| with the answer. +- (void)checkIfAccessoryViewAvailableForFormNamed:(const std::string&)formName + fieldName:(const std::string&)fieldName + webState:(web::WebState*)webState + completionHandler: + (AccessoryViewAvailableCompletion) + completionHandler; + +// Asynchronously retrieves an accessory view from this provider for the +// specified form/field and returns it via |completionHandler|. +- (void)retrieveAccessoryViewForFormNamed:(const std::string&)formName + fieldName:(const std::string&)fieldName + value:(const std::string&)value + type:(const std::string&)type + webState:(web::WebState*)webState + completionHandler: + (AccessoryViewReadyCompletion)completionHandler; + +// Notifies this provider that the accessory view is going away. +- (void)inputAccessoryViewControllerDidReset: + (FormInputAccessoryViewController*)controller; + +// Notifies this provider that the accessory view frame is changing. If the +// view provided by this provider needs to change, the updated view should be +// returned using the completion received in +// |retrieveAccessoryViewForForm:field:value:type:webState:completionHandler:|. +- (void)resizeAccessoryView; + +@end + +// Creates and manages a custom input accessory view while the user is +// interacting with a form. Also handles hiding and showing the default +// accessory view elements. +@interface FormInputAccessoryViewController + : NSObject<CRWWebStateObserver, FormInputAccessoryViewDelegate> + +// Initializes a new controller with the specified |providers| of input +// accessory views. +- (instancetype)initWithWebState:(web::WebState*)webState + providers:(NSArray*)providers; + +// Hides the default input accessory view and replaces it with one that shows +// |customView| and form navigation controls. +- (void)showCustomInputAccessoryView:(UIView*)customView; + +// Restores the default input accessory view, removing (if necessary) any +// previously-added custom view. +- (void)restoreDefaultInputAccessoryView; + +@end + +#endif // IOS_CHROME_BROWSER_AUTOFILL_FORM_INPUT_ACCESSORY_VIEW_CONTROLLER_H_ 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 0000000..8f82627 --- /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 diff --git a/ios/chrome/browser/autofill/form_input_accessory_view_delegate.h b/ios/chrome/browser/autofill/form_input_accessory_view_delegate.h new file mode 100644 index 0000000..38c6f18 --- /dev/null +++ b/ios/chrome/browser/autofill/form_input_accessory_view_delegate.h @@ -0,0 +1,32 @@ +// 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. + +#ifndef IOS_CHROME_BROWSER_AUTOFILL_FORM_INPUT_ACCESSORY_VIEW_DELEGATE_H_ +#define IOS_CHROME_BROWSER_AUTOFILL_FORM_INPUT_ACCESSORY_VIEW_DELEGATE_H_ + +#import <Foundation/Foundation.h> + +// Handles user interaction with a FormInputAccessoryView. +@protocol FormInputAccessoryViewDelegate<NSObject> + +// Called when the close button is clicked. +- (void)closeKeyboard; + +// Called when the previous button is clicked. +- (void)selectPreviousElement; + +// Called when the next button is clicked. +- (void)selectNextElement; + +// Called when updating the keyboard view. Checks if the page contains a next +// and a previous element. +// |completionHandler| is called with 2 BOOLs, the first indicating if a +// previous element was found, and the second indicating if a next element was +// found. |completionHandler| cannot be nil. +- (void)fetchPreviousAndNextElementsPresenceWithCompletionHandler: + (void (^)(BOOL, BOOL))completionHandler; + +@end + +#endif // IOS_CHROME_BROWSER_AUTOFILL_FORM_INPUT_ACCESSORY_VIEW_DELEGATE_H_ diff --git a/ios/chrome/browser/autofill/form_suggestion_controller.h b/ios/chrome/browser/autofill/form_suggestion_controller.h new file mode 100644 index 0000000..6cf39c7 --- /dev/null +++ b/ios/chrome/browser/autofill/form_suggestion_controller.h @@ -0,0 +1,60 @@ +// 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. + +#ifndef IOS_CHROME_BROWSER_AUTOFILL_FORM_SUGGESTION_CONTROLLER_H_ +#define IOS_CHROME_BROWSER_AUTOFILL_FORM_SUGGESTION_CONTROLLER_H_ + +#import <UIKit/UIKit.h> + +#include <string> + +#import "components/autofill/ios/browser/js_suggestion_manager.h" +#import "ios/chrome/browser/autofill/form_suggestion_view.h" +#import "ios/chrome/browser/autofill/form_suggestion_view_client.h" +#import "ios/web/public/web_state/web_state_observer_bridge.h" + +namespace web { +class WebState; +} + +@protocol CRWWebViewProxy; +@protocol FormInputAccessoryViewProvider; + +// Handles form focus events and presents input suggestions. +@interface FormSuggestionController + : NSObject<CRWWebStateObserver, FormSuggestionViewClient> + +// Initializes a new FormSuggestionController with the specified WebState and a +// list of FormSuggestionProviders. +// When suggestions are required for an input field, the |providers| will be +// asked (in order) if they can handle the field; the first provider to return +// YES from [FormSuggestionProvider canProviderSuggestionsForForm:field:] will +// be expected to provide those suggestions using [FormSuggestionProvider +// retrieveSuggestionsForForm:field:withCompletion:]. +- (instancetype)initWithWebState:(web::WebState*)webState + providers:(NSArray*)providers; + +// Instructs the controller to detach itself from the WebState. +- (void)detachFromWebState; + +// Provides an input accessory view for form suggestions. +@property(nonatomic, readonly) + id<FormInputAccessoryViewProvider> accessoryViewProvider; + +@end + +@interface FormSuggestionController (ForTesting) + +// Initializes a new controller in the same way as the public initializer, but +// supports specifying a JsSuggestionManager for testing. +- (instancetype)initWithWebState:(web::WebState*)webState + providers:(NSArray*)providers + JsSuggestionManager:(JsSuggestionManager*)jsSuggestionManager; + +// Overrides the web view proxy. +- (void)setWebViewProxy:(id<CRWWebViewProxy>)webViewProxy; + +@end + +#endif // IOS_CHROME_BROWSER_AUTOFILL_FORM_SUGGESTION_CONTROLLER_H_ diff --git a/ios/chrome/browser/autofill/form_suggestion_controller.mm b/ios/chrome/browser/autofill/form_suggestion_controller.mm new file mode 100644 index 0000000..e3eb8ae --- /dev/null +++ b/ios/chrome/browser/autofill/form_suggestion_controller.mm @@ -0,0 +1,352 @@ +// 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_suggestion_controller.h" + +#include "base/ios/weak_nsobject.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" +#include "components/autofill/core/browser/autofill_popup_delegate.h" +#import "components/autofill/ios/browser/form_suggestion.h" +#import "ios/chrome/browser/autofill/form_input_accessory_view_controller.h" +#import "ios/chrome/browser/autofill/form_suggestion_provider.h" +#import "ios/chrome/browser/autofill/form_suggestion_view.h" +#import "ios/chrome/browser/passwords/password_generation_utils.h" +#include "ios/web/public/url_scheme_util.h" +#import "ios/web/public/web_state/crw_web_view_proxy.h" +#import "ios/web/public/web_state/js/crw_js_injection_receiver.h" +#import "ios/web/public/web_state/web_state.h" + +namespace { + +// Struct that describes suggestion state. +struct AutofillSuggestionState { + AutofillSuggestionState(const std::string& form_name, + const std::string& field_name, + const std::string& typed_value); + // The name of the form for autofill. + std::string form_name; + // The name of the field for autofill. + std::string field_name; + // The user-typed value in the field. + std::string typed_value; + // The suggestions for the form field. An array of |FormSuggestion|. + base::scoped_nsobject<NSArray> suggestions; +}; + +AutofillSuggestionState::AutofillSuggestionState(const std::string& form_name, + const std::string& field_name, + const std::string& typed_value) + : form_name(form_name), field_name(field_name), typed_value(typed_value) { +} + +} // namespace + +@interface FormSuggestionController () <FormInputAccessoryViewProvider> { + // Form navigation delegate. + base::WeakNSProtocol<id<FormInputAccessoryViewDelegate>> _delegate; + + // Callback to update the accessory view. + base::mac::ScopedBlock<AccessoryViewReadyCompletion> completionHandler_; + + // Autofill suggestion state. + scoped_ptr<AutofillSuggestionState> _suggestionState; + + // Providers for suggestions, sorted according to the order in which + // they should be asked for suggestions, with highest priority in front. + base::scoped_nsobject<NSArray> _suggestionProviders; + + // Access to WebView from the CRWWebController. + base::scoped_nsprotocol<id<CRWWebViewProxy>> _webViewProxy; +} + +// Returns an autoreleased input accessory view that shows |suggestions|. +- (UIView*)suggestionViewWithSuggestions:(NSArray*)suggestions; + +// Updates keyboard for |suggestionState|. +- (void)updateKeyboard:(AutofillSuggestionState*)suggestionState; + +// Updates keyboard with |suggestions|. +- (void)updateKeyboardWithSuggestions:(NSArray*)suggestions; + +// Clears state in between page loads. +- (void)resetSuggestionState; + +// Finds a FormSuggestionProvider that can supply suggestions for the specified +// form, requests them, and updates the view accordingly. +- (void)retrieveSuggestionsForFormNamed:(const std::string&)formName + fieldName:(const std::string&)fieldName + type:(const std::string&)type + webState:(web::WebState*)webState; + +@end + +@implementation FormSuggestionController { + // Bridge to observe the web state from Objective-C. + scoped_ptr<web::WebStateObserverBridge> _webStateObserverBridge; + + // Manager for FormSuggestion JavaScripts. + base::scoped_nsobject<JsSuggestionManager> _jsSuggestionManager; + + // The provider for the current set of suggestions. + __weak id<FormSuggestionProvider> _provider; +} + +- (instancetype)initWithWebState:(web::WebState*)webState + providers:(NSArray*)providers + JsSuggestionManager:(JsSuggestionManager*)jsSuggestionManager { + self = [super init]; + if (self) { + _webStateObserverBridge.reset( + new web::WebStateObserverBridge(webState, self)); + _webViewProxy.reset([webState->GetWebViewProxy() retain]); + _jsSuggestionManager.reset([jsSuggestionManager retain]); + _suggestionProviders.reset([providers copy]); + } + return self; +} + +- (instancetype)initWithWebState:(web::WebState*)webState + providers:(NSArray*)providers { + JsSuggestionManager* jsSuggestionManager = + base::mac::ObjCCast<JsSuggestionManager>( + [webState->GetJSInjectionReceiver() + instanceOfClass:[JsSuggestionManager class]]); + return [self initWithWebState:webState + providers:providers + JsSuggestionManager:jsSuggestionManager]; +} + +- (void)detachFromWebState { + _webStateObserverBridge.reset(); +} + +#pragma mark - +#pragma mark CRWWebStateObserver + +- (void)webStateDestroyed:(web::WebState*)webState { + [self detachFromWebState]; +} + +- (void)pageLoaded:(web::WebState*)webState { + [self processPage:webState]; +} + +- (void)processPage:(web::WebState*)webState { + [self resetSuggestionState]; + + web::URLVerificationTrustLevel trustLevel = + web::URLVerificationTrustLevel::kNone; + const GURL pageURL(webState->GetCurrentURL(&trustLevel)); + if (trustLevel != web::URLVerificationTrustLevel::kAbsolute) { + DLOG(WARNING) << "Page load not handled on untrusted page"; + return; + } + + if (web::UrlHasWebScheme(pageURL) && webState->ContentIsHTML()) + [_jsSuggestionManager inject]; +} + +- (void)setWebViewProxy:(id<CRWWebViewProxy>)webViewProxy { + _webViewProxy.reset([webViewProxy retain]); +} + +- (void)retrieveSuggestionsForFormNamed:(const std::string&)formName + fieldName:(const std::string&)fieldName + type:(const std::string&)type + webState:(web::WebState*)webState { + base::WeakNSObject<FormSuggestionController> weakSelf(self); + base::scoped_nsobject<NSString> strongFormName( + [base::SysUTF8ToNSString(formName) copy]); + base::scoped_nsobject<NSString> strongFieldName( + [base::SysUTF8ToNSString(fieldName) copy]); + base::scoped_nsobject<NSString> strongType( + [base::SysUTF8ToNSString(type) copy]); + base::scoped_nsobject<NSString> strongValue( + [base::SysUTF8ToNSString(_suggestionState.get()->typed_value) copy]); + + // Build a block for each provider that will invoke its completion with YES + // if the provider can provide suggestions for the specified form/field/type + // and NO otherwise. + base::scoped_nsobject<NSMutableArray> findProviderBlocks( + [[NSMutableArray alloc] init]); + for (NSUInteger i = 0; i < [_suggestionProviders 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. + // |_suggestionProviders| is immutable, so the subscripting is + // always valid. + base::scoped_nsobject<FormSuggestionController> strongSelf( + [weakSelf retain]); + if (!strongSelf) + return; + id<FormSuggestionProvider> provider = + strongSelf.get()->_suggestionProviders[i]; + [provider checkIfSuggestionsAvailableForForm:strongFormName + field:strongFieldName + type:strongType + typedValue:strongValue + webState:webState + completionHandler:completion]; + }, + base::scoped_policy::RETAIN); + [findProviderBlocks addObject:block]; + } + + // Once the suggestions are retrieved, update the suggestions UI. + SuggestionsReadyCompletion readyCompletion = + ^(NSArray* suggestions, id<FormSuggestionProvider> provider) { + [weakSelf onSuggestionsReady:suggestions provider:provider]; + }; + + // Once a provider is found, use it to retrieve suggestions. + passwords::PipelineCompletionBlock completion = ^(NSUInteger providerIndex) { + if (providerIndex == NSNotFound) + return; + base::scoped_nsobject<FormSuggestionController> strongSelf( + [weakSelf retain]); + if (!strongSelf) + return; + id<FormSuggestionProvider> provider = + strongSelf.get()->_suggestionProviders[providerIndex]; + [provider retrieveSuggestionsForForm:strongFormName + field:strongFieldName + type:strongType + typedValue:strongValue + 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, completion); +} + +- (void)onSuggestionsReady:(NSArray*)suggestions + provider:(id<FormSuggestionProvider>)provider { + // TODO(ios): crbug.com/249916. If we can also pass in the form/field for + // which |sugguestions| are, we should check here if |suggestions| are for + // the current active element. If not, reset |_suggestionState|. + if (!_suggestionState) { + // The suggestion state was reset in between the call to Autofill API (e.g. + // OnQueryFormFieldAutofill) and this method being called back. Results are + // therefore no longer relevant. + return; + } + + _provider = provider; + _suggestionState->suggestions.reset([suggestions copy]); + [self updateKeyboard:_suggestionState.get()]; +} + +- (void)resetSuggestionState { + _provider = nil; + _suggestionState.reset(); +} + +- (void)clearSuggestions { + // Note that other parts of the suggestionsState are not reset. + if (!_suggestionState.get()) + return; + _suggestionState->suggestions.reset([[NSArray alloc] init]); + [self updateKeyboard:_suggestionState.get()]; +} + +- (void)updateKeyboard:(AutofillSuggestionState*)suggestionState { + if (!_suggestionState) { + if (completionHandler_) + completionHandler_.get()(nil, self); + } else { + [self updateKeyboardWithSuggestions:_suggestionState->suggestions]; + } +} + +- (void)updateKeyboardWithSuggestions:(NSArray*)suggestions { + if (completionHandler_) + completionHandler_.get()([self suggestionViewWithSuggestions:suggestions], + self); +} + +- (UIView*)suggestionViewWithSuggestions:(NSArray*)suggestions { + base::scoped_nsobject<FormSuggestionView> view([[FormSuggestionView alloc] + initWithFrame:[_webViewProxy getKeyboardAccessory].frame + client:self + suggestions:suggestions]); + return view.autorelease(); +} + +- (void)didSelectSuggestion:(FormSuggestion*)suggestion { + if (!_suggestionState) + return; + + // Send the suggestion to the provider and advance the cursor. + base::WeakNSObject<FormSuggestionController> weakSelf(self); + [_provider + didSelectSuggestion:suggestion + forField:base::SysUTF8ToNSString(_suggestionState->field_name) + form:base::SysUTF8ToNSString(_suggestionState->form_name) + completionHandler:^{ + [[weakSelf accessoryViewDelegate] selectNextElement]; + }]; + _provider = nil; +} + +- (id<FormInputAccessoryViewProvider>)accessoryViewProvider { + return self; +} + +#pragma mark FormInputAccessoryViewProvider + +- (id<FormInputAccessoryViewDelegate>)accessoryViewDelegate { + return _delegate.get(); +} + +- (void)setAccessoryViewDelegate:(id<FormInputAccessoryViewDelegate>)delegate { + _delegate.reset(delegate); +} + +- (void)checkIfAccessoryViewAvailableForFormNamed:(const std::string&)formName + fieldName:(const std::string&)fieldName + webState:(web::WebState*)webState + completionHandler: + (AccessoryViewAvailableCompletion) + completionHandler { + [self processPage:webState]; + completionHandler(YES); +} + +- (void)retrieveAccessoryViewForFormNamed:(const std::string&)formName + fieldName:(const std::string&)fieldName + value:(const std::string&)value + type:(const std::string&)type + webState:(web::WebState*)webState + completionHandler: + (AccessoryViewReadyCompletion)completionHandler { + _suggestionState.reset( + new AutofillSuggestionState(formName, fieldName, value)); + completionHandler([self suggestionViewWithSuggestions:@[]], self); + completionHandler_.reset([completionHandler copy]); + [self retrieveSuggestionsForFormNamed:formName + fieldName:fieldName + type:type + webState:webState]; +} + +- (void)inputAccessoryViewControllerDidReset: + (FormInputAccessoryViewController*)controller { + completionHandler_.reset(); + [self resetSuggestionState]; +} + +- (void)resizeAccessoryView { + [self updateKeyboard:_suggestionState.get()]; +} + +@end diff --git a/ios/chrome/browser/autofill/form_suggestion_label.h b/ios/chrome/browser/autofill/form_suggestion_label.h new file mode 100644 index 0000000..44a784b --- /dev/null +++ b/ios/chrome/browser/autofill/form_suggestion_label.h @@ -0,0 +1,28 @@ +// Copyright 2013 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. + +#ifndef IOS_CHROME_BROWSER_AUTOFILL_FORM_SUGGESTION_LABEL_H_ +#define IOS_CHROME_BROWSER_AUTOFILL_FORM_SUGGESTION_LABEL_H_ + +#import <UIKit/UIKit.h> + +#include "base/ios/weak_nsobject.h" +#include "base/mac/scoped_nsobject.h" + +@class FormSuggestion; +@protocol FormSuggestionViewClient; + +// Class for Autofill suggestion in the customized keyboard. +@interface FormSuggestionLabel : UIView + +// Designated initializer. Initializes with |proposedFrame| and |client| for +// |suggestion|. Its width will be adjusted according to the length of +// |suggestion| and width in |proposedFrame| is ignored. +- (id)initWithSuggestion:(FormSuggestion*)suggestion + proposedFrame:(CGRect)proposedFrame + client:(id<FormSuggestionViewClient>)client; + +@end + +#endif // IOS_CHROME_BROWSER_AUTOFILL_FORM_SUGGESTION_LABEL_H_ diff --git a/ios/chrome/browser/autofill/form_suggestion_label.mm b/ios/chrome/browser/autofill/form_suggestion_label.mm new file mode 100644 index 0000000..60e9022 --- /dev/null +++ b/ios/chrome/browser/autofill/form_suggestion_label.mm @@ -0,0 +1,238 @@ +// Copyright 2013 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_suggestion_label.h" + +#include <cmath> + +#import <QuartzCore/QuartzCore.h> + +#include "base/strings/sys_string_conversions.h" +#include "components/autofill/core/browser/credit_card.h" +#import "components/autofill/ios/browser/form_suggestion.h" +#import "ios/chrome/browser/autofill/form_suggestion_view_client.h" +#include "ios/chrome/browser/ui/ui_util.h" + +namespace { + +// The button corner radius. +const CGFloat kCornerRadius = 3.0f; + +// The width of the border in the button background image. +const CGFloat kBorderWidth = 1.0f; + +// Font size of button titles. +const CGFloat kIpadFontSize = 15.0f; +const CGFloat kIphoneFontSize = 13.0f; + +// The grayscale value of the color object. +const CGFloat kTitleColor = 51.0f / 255.0f; + +// The alpha value of the suggestion's description label. +const CGFloat kDescriptionLabelAlpha = 0.54f; + +// The edge inset for background image. +const CGFloat kBackgroundImageEdgeInsetSize = 8.0f; +// The space between items in the label. +const CGFloat kSpacing = 8.0f; + +// Structure that record the image for each icon. +struct IconImageMap { + const char* const icon_name; + NSString* image_name; +}; + +const IconImageMap kCreditCardIconImageMap[] = { + {autofill::kAmericanExpressCard, @"autofill_card_american_express"}, + {autofill::kDiscoverCard, @"autofill_card_discover"}, + {autofill::kMasterCard, @"autofill_card_mastercard"}, + {autofill::kVisaCard, @"autofill_card_visa"}, + {autofill::kDinersCard, @"autofill_card_diners"}, + {autofill::kGenericCard, @"autofill_card_generic"}, + {autofill::kJCBCard, @"autofill_card_jcb"}, + {autofill::kUnionPay, @"autofill_card_unionpay"}, +}; + +// Creates a label with the given |text| and |alpha| suitable for use in a +// suggestion button in the keyboard accessory view. +UILabel* TextLabel(NSString* text, CGFloat alpha) { + base::scoped_nsobject<UILabel> label([[UILabel alloc] init]); + [label setText:text]; + UIFont* font = IsIPadIdiom() ? [UIFont systemFontOfSize:kIpadFontSize] + : [UIFont systemFontOfSize:kIphoneFontSize]; + [label setFont:font]; + [label setTextColor:[UIColor colorWithWhite:kTitleColor alpha:alpha]]; + [label setBackgroundColor:[UIColor clearColor]]; + [label sizeToFit]; + return label.autorelease(); +} + +} // namespace + +@interface FormSuggestionLabel () + +@property(nonatomic, readonly) UIColor* normalBackgroundColor; +@property(nonatomic, readonly) UIColor* pressedBackgroundColor; + +// Returns the color generated from the image named |imageName| resized to +// |rect|. +- (UIColor*)backgroundColorFromImageNamed:(NSString*)imageName + inRect:(CGRect)rect; +// Returns the name of the image for credit card icon. ++ (NSString*)imageNameForCreditCardIcon:(NSString*)icon; +@end + +@implementation FormSuggestionLabel { + // Client of this view. + base::WeakNSProtocol<id<FormSuggestionViewClient>> client_; + base::scoped_nsobject<FormSuggestion> suggestion_; + + // Background color when the label is not pressed. + base::scoped_nsobject<UIColor> normalBackgroundColor_; + // Background color when the label is pressed. + base::scoped_nsobject<UIColor> pressedBackgroundColor_; +} + +- (id)initWithSuggestion:(FormSuggestion*)suggestion + proposedFrame:(CGRect)proposedFrame + client:(id<FormSuggestionViewClient>)client { + // TODO(jimblackler): implement sizeThatFits: and layoutSubviews, and perform + // layout in those methods instead of in the designated initializer. + self = [super initWithFrame:CGRectZero]; + if (self) { + suggestion_.reset([suggestion retain]); + client_.reset(client); + + const CGFloat frameHeight = CGRectGetHeight(proposedFrame); + CGFloat currentX = kBorderWidth + kSpacing; + + UIImage* iconImage = [UIImage imageNamed: + [FormSuggestionLabel imageNameForCreditCardIcon:suggestion.icon]]; + if (iconImage) { + UIImageView* iconView = + [[[UIImageView alloc] initWithImage:iconImage] autorelease]; + const CGFloat iconY = + std::floor((frameHeight - iconImage.size.height) / 2.0f); + iconView.frame = CGRectMake(currentX, iconY, iconImage.size.width, + iconImage.size.height); + [self addSubview:iconView]; + currentX += CGRectGetWidth(iconView.frame) + kSpacing; + } + + UILabel* label = TextLabel(suggestion.value, 1.0f); + const CGFloat labelY = + std::floor(frameHeight / 2.0f - CGRectGetMidY(label.frame)); + label.frame = CGRectMake(currentX, labelY, CGRectGetWidth(label.frame), + CGRectGetHeight(label.frame)); + [self addSubview:label]; + currentX += CGRectGetWidth(label.frame) + kSpacing; + + if (suggestion.displayDescription) { + UILabel* description = + TextLabel(suggestion.displayDescription, kDescriptionLabelAlpha); + const CGFloat descriptionY = + std::floor(frameHeight / 2.0f - CGRectGetMidY(description.frame)); + description.frame = + CGRectMake(currentX, descriptionY, CGRectGetWidth(description.frame), + CGRectGetHeight(description.frame)); + [self addSubview:description]; + currentX += CGRectGetWidth(description.frame) + kSpacing; + } + + currentX += kBorderWidth; + + self.frame = CGRectMake(proposedFrame.origin.x, proposedFrame.origin.y, + currentX, proposedFrame.size.height); + [self setBackgroundColor:[self normalBackgroundColor]]; + [[self layer] setCornerRadius:kCornerRadius]; + + [self setClipsToBounds:YES]; + [self setIsAccessibilityElement:YES]; + [self setAccessibilityLabel:suggestion.value]; + [self setUserInteractionEnabled:YES]; + } + + return self; +} + +- (id)initWithFrame:(CGRect)frame { + NOTREACHED(); + return nil; +} + +- (void)layoutSubviews { + // Resets the colors so the size will be updated in their getters. + normalBackgroundColor_.reset(); + pressedBackgroundColor_.reset(); + [super layoutSubviews]; +} + +#pragma mark - +#pragma mark UIResponder + +- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { + [self setBackgroundColor:self.pressedBackgroundColor]; +} + +- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { + [self setBackgroundColor:self.normalBackgroundColor]; +} + +- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { + [self setBackgroundColor:self.normalBackgroundColor]; + [client_ didSelectSuggestion:suggestion_]; +} + +#pragma mark - +#pragma mark Private + +- (UIColor*)normalBackgroundColor { + if (!normalBackgroundColor_) { + normalBackgroundColor_.reset( + [[self backgroundColorFromImageNamed:@"autofill_button" + inRect:self.bounds] retain]); + } + return normalBackgroundColor_; +} + +- (UIColor*)pressedBackgroundColor { + if (!pressedBackgroundColor_) { + pressedBackgroundColor_.reset( + [[self backgroundColorFromImageNamed:@"autofill_button_pressed" + inRect:self.bounds] retain]); + } + return pressedBackgroundColor_; +} + +- (UIColor*)backgroundColorFromImageNamed:(NSString*)imageName + inRect:(CGRect)rect { + UIEdgeInsets edgeInsets = UIEdgeInsetsMake( + kBackgroundImageEdgeInsetSize, kBackgroundImageEdgeInsetSize, + kBackgroundImageEdgeInsetSize, kBackgroundImageEdgeInsetSize); + UIImage* image = + [[UIImage imageNamed:imageName] resizableImageWithCapInsets:edgeInsets]; + + UIGraphicsBeginImageContextWithOptions(rect.size, NO, + UIScreen.mainScreen.scale); + [image drawInRect:rect]; + UIImage* resizedImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return [UIColor colorWithPatternImage:resizedImage]; +} + ++ (NSString*)imageNameForCreditCardIcon:(NSString*)icon { + if (!icon || [icon length] == 0) { + return nil; + } + std::string iconName(base::SysNSStringToUTF8(icon)); + for (size_t i = 0; i < arraysize(kCreditCardIconImageMap); ++i) { + if (iconName.compare(kCreditCardIconImageMap[i].icon_name) == 0) { + return kCreditCardIconImageMap[i].image_name; + } + } + return nil; +} + +@end diff --git a/ios/chrome/browser/autofill/form_suggestion_provider.h b/ios/chrome/browser/autofill/form_suggestion_provider.h new file mode 100644 index 0000000..09ba946 --- /dev/null +++ b/ios/chrome/browser/autofill/form_suggestion_provider.h @@ -0,0 +1,55 @@ +// 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. + +#ifndef IOS_CHROME_BROWSER_AUTOFILL_FORM_SUGGESTION_PROVIDER_H_ +#define IOS_CHROME_BROWSER_AUTOFILL_FORM_SUGGESTION_PROVIDER_H_ + +#import "components/autofill/ios/browser/form_suggestion.h" + +@protocol FormSuggestionProvider; + +namespace web { +class WebState; +} // namespace web + +typedef void (^SuggestionsAvailableCompletion)(BOOL suggestionsAvailable); +typedef void (^SuggestionsReadyCompletion)(NSArray* suggestions, + id<FormSuggestionProvider> delegate); +typedef void (^SuggestionHandledCompletion)(void); + +// Provides user-selectable suggestions for an input field of a web form +// and handles user interaction with those suggestions. +@protocol FormSuggestionProvider<NSObject> + +// Determines whether the receiver can provide suggestions for the specified +// |form| and |field|, returning the result using the provided |completion|. +// |typedValue| contains the text that the user has typed into the field so far. +- (void)checkIfSuggestionsAvailableForForm:(NSString*)formName + field:(NSString*)fieldName + type:(NSString*)type + typedValue:(NSString*)typedValue + webState:(web::WebState*)webState + completionHandler: + (SuggestionsAvailableCompletion)completion; + +// Retrieves suggestions for the specified |form| and |field| and returns them +// using the provided |completion|. |typedValue| contains the text that the +// user has typed into the field so far. +- (void)retrieveSuggestionsForForm:(NSString*)formName + field:(NSString*)fieldName + type:(NSString*)type + typedValue:(NSString*)typedValue + webState:(web::WebState*)webState + completionHandler:(SuggestionsReadyCompletion)completion; + +// Handles user selection of a suggestion for the specified form and +// field, invoking |completion| when finished. +- (void)didSelectSuggestion:(FormSuggestion*)suggestion + forField:(NSString*)fieldName + form:(NSString*)formName + completionHandler:(SuggestionHandledCompletion)completion; + +@end + +#endif // IOS_CHROME_BROWSER_AUTOFILL_FORM_SUGGESTION_PROVIDER_H_ diff --git a/ios/chrome/browser/autofill/form_suggestion_view.h b/ios/chrome/browser/autofill/form_suggestion_view.h new file mode 100644 index 0000000..9a8c33c --- /dev/null +++ b/ios/chrome/browser/autofill/form_suggestion_view.h @@ -0,0 +1,28 @@ +// Copyright 2013 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. + +#ifndef IOS_CHROME_BROWSER_AUTOFILL_FORM_SUGGESTION_VIEW_H_ +#define IOS_CHROME_BROWSER_AUTOFILL_FORM_SUGGESTION_VIEW_H_ + +#import <UIKit/UIKit.h> + +#include "base/ios/weak_nsobject.h" + +@protocol FormSuggestionViewClient; + +// A scrollable view for displaying user-selectable autofill form suggestions. +@interface FormSuggestionView : UIScrollView<UIInputViewAudioFeedback> + +// Initializes with |frame| and |client| to show |suggestions|. +- (instancetype)initWithFrame:(CGRect)frame + client:(id<FormSuggestionViewClient>)client + suggestions:(NSArray*)suggestions; + +@end + +@interface FormSuggestionView (ForTesting) +@property(nonatomic, readonly) NSArray* suggestions; +@end + +#endif // IOS_CHROME_BROWSER_AUTOFILL_FORM_SUGGESTION_VIEW_H_ diff --git a/ios/chrome/browser/autofill/form_suggestion_view.mm b/ios/chrome/browser/autofill/form_suggestion_view.mm new file mode 100644 index 0000000..4ac0467 --- /dev/null +++ b/ios/chrome/browser/autofill/form_suggestion_view.mm @@ -0,0 +1,97 @@ +// Copyright 2013 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_suggestion_view.h" + +#include "base/i18n/rtl.h" +#include "base/mac/scoped_nsobject.h" +#import "ios/chrome/browser/autofill/form_suggestion_label.h" +#import "ios/chrome/browser/autofill/form_suggestion_view_client.h" + +namespace { + +// Vertical margin between suggestions and the edge of the suggestion content +// frame. +const CGFloat kSuggestionVerticalMargin = 4; + +// Horizontal margin around suggestions (i.e. between suggestions, and between +// the end suggestions and the suggestion content frame). +const CGFloat kSuggestionHorizontalMargin = 2; + +} // namespace + +@implementation FormSuggestionView { + // The FormSuggestions that are displayed by this view. + base::scoped_nsobject<NSArray> _suggestions; +} + +- (instancetype)initWithFrame:(CGRect)frame + client:(id<FormSuggestionViewClient>)client + suggestions:(NSArray*)suggestions { + self = [super initWithFrame:frame]; + if (self) { + _suggestions.reset([suggestions copy]); + + self.showsVerticalScrollIndicator = NO; + self.showsHorizontalScrollIndicator = NO; + self.bounces = NO; + self.canCancelContentTouches = YES; + + // Total height occupied by the label content, padding, border and margin. + const CGFloat labelHeight = + CGRectGetHeight(frame) - kSuggestionVerticalMargin * 2; + + BOOL isRTL = base::i18n::IsRTL(); + + NSUInteger suggestionCount = [_suggestions count]; + // References to labels. These references are used to adjust the labels' + // positions if they don't take up the whole suggestion view area for RTL. + base::scoped_nsobject<NSMutableArray> labels( + [[NSMutableArray alloc] initWithCapacity:suggestionCount]); + __block CGFloat currentX = kSuggestionHorizontalMargin; + void (^setupBlock)(FormSuggestion* suggestion, NSUInteger idx, BOOL* stop) = + ^(FormSuggestion* suggestion, NSUInteger idx, BOOL* stop) { + // FormSuggestionLabel will adjust the width, so here 0 is used for + // the width. + CGRect proposedFrame = + CGRectMake(currentX, kSuggestionVerticalMargin, 0, labelHeight); + base::scoped_nsobject<UIView> label( + [[FormSuggestionLabel alloc] initWithSuggestion:suggestion + proposedFrame:proposedFrame + client:client]); + [self addSubview:label]; + [labels addObject:label]; + currentX += + CGRectGetWidth([label frame]) + kSuggestionHorizontalMargin; + }; + [_suggestions enumerateObjectsWithOptions:(isRTL ? NSEnumerationReverse : 0) + usingBlock:setupBlock]; + + if (isRTL) { + if (currentX < CGRectGetWidth(frame)) { + self.contentSize = frame.size; + // Offsets labels for right alignment. + CGFloat offset = CGRectGetWidth(frame) - currentX; + for (UIView* label in labels.get()) { + label.frame = CGRectOffset(label.frame, offset, 0); + } + } else { + self.contentSize = CGSizeMake(currentX, CGRectGetHeight(frame)); + // Sets the visible rectangle so suggestions at the right end are + // initially visible. + CGRect initRect = {{currentX - CGRectGetWidth(frame), 0}, frame.size}; + [self scrollRectToVisible:initRect animated:NO]; + } + } else { + self.contentSize = CGSizeMake(currentX, CGRectGetHeight(frame)); + } + } + return self; +} + +- (NSArray*)suggestions { + return _suggestions.get(); +} + +@end diff --git a/ios/chrome/browser/autofill/form_suggestion_view_client.h b/ios/chrome/browser/autofill/form_suggestion_view_client.h new file mode 100644 index 0000000..a3af25f --- /dev/null +++ b/ios/chrome/browser/autofill/form_suggestion_view_client.h @@ -0,0 +1,20 @@ +// Copyright 2013 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. + +#ifndef IOS_CHROME_BROWSER_AUTOFILL_FORM_SUGGESTION_VIEW_CLIENT_H_ +#define IOS_CHROME_BROWSER_AUTOFILL_FORM_SUGGESTION_VIEW_CLIENT_H_ + +#import <Foundation/Foundation.h> + +@class FormSuggestion; + +// Handles user interaction with a FormSuggestionView. +@protocol FormSuggestionViewClient<NSObject> + +// Called when a suggestion is selected. +- (void)didSelectSuggestion:(FormSuggestion*)suggestion; + +@end + +#endif // IOS_CHROME_BROWSER_AUTOFILL_FORM_SUGGESTION_VIEW_CLIENT_H_ diff --git a/ios/chrome/browser/passwords/OWNERS b/ios/chrome/browser/passwords/OWNERS new file mode 100644 index 0000000..cdbac76f --- /dev/null +++ b/ios/chrome/browser/passwords/OWNERS @@ -0,0 +1 @@ +dconnelly@chromium.org diff --git a/ios/chrome/browser/passwords/password_generation_utils.h b/ios/chrome/browser/passwords/password_generation_utils.h new file mode 100644 index 0000000..63e485b --- /dev/null +++ b/ios/chrome/browser/passwords/password_generation_utils.h @@ -0,0 +1,29 @@ +// 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. + +#ifndef IOS_CHROME_BROWSER_PASSWORDS_PASSWORD_GENERATION_UTILS_H_ +#define IOS_CHROME_BROWSER_PASSWORDS_PASSWORD_GENERATION_UTILS_H_ + +#import <Foundation/Foundation.h> +#import <CoreGraphics/CoreGraphics.h> + +namespace passwords { + +// Returns the frame determined by moving |inner_frame| inside |outer_frame| +// for the password generation input accessory views. +CGRect GetGenerationAccessoryFrame(CGRect outer_frame, CGRect inner_frame); + +// Block types for |RunSearchPipeline|. +typedef void (^PipelineBlock)(void (^completion)(BOOL)); +typedef void (^PipelineCompletionBlock)(NSUInteger index); + +// Executes each PipelineBlock in |blocks| in order until one invokes its +// completion with YES, in which case |on_complete| will be invoked with the +// |index| of the succeeding block, or until they all invoke their completions +// with NO, in which case |on_complete| will be invoked with NSNotFound. +void RunSearchPipeline(NSArray* blocks, PipelineCompletionBlock on_complete); + +} // namespace passwords + +#endif // IOS_CHROME_BROWSER_PASSWORDS_PASSWORD_GENERATION_UTILS_H_ diff --git a/ios/chrome/browser/passwords/password_generation_utils.mm b/ios/chrome/browser/passwords/password_generation_utils.mm new file mode 100644 index 0000000..7f1abd4 --- /dev/null +++ b/ios/chrome/browser/passwords/password_generation_utils.mm @@ -0,0 +1,50 @@ +// 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. + +#include "ios/chrome/browser/passwords/password_generation_utils.h" + +#include "base/i18n/rtl.h" +#include "ios/chrome/browser/ui/ui_util.h" + +namespace passwords { + +namespace { + +const CGFloat kPadding = IsIPadIdiom() ? 16 : 8; + +// The actual implementation of |RunPipeline| that begins with the first block +// in |blocks|. +void RunSearchPipeline(NSArray* blocks, + PipelineCompletionBlock on_complete, + NSUInteger from_index) { + if (from_index == [blocks count]) { + on_complete(NSNotFound); + return; + } + PipelineBlock block = blocks[from_index]; + block(^(BOOL success) { + if (success) + on_complete(from_index); + else + RunSearchPipeline(blocks, on_complete, from_index + 1); + }); +} + +} // namespace + +CGRect GetGenerationAccessoryFrame(CGRect outer_frame, CGRect inner_frame) { + CGFloat x = kPadding; + if (base::i18n::IsRTL()) + x = CGRectGetWidth(outer_frame) - CGRectGetWidth(inner_frame) - kPadding; + const CGFloat y = + (CGRectGetHeight(outer_frame) - CGRectGetHeight(inner_frame)) / 2.0; + inner_frame.origin = CGPointMake(x, y); + return inner_frame; +} + +void RunSearchPipeline(NSArray* blocks, PipelineCompletionBlock on_complete) { + RunSearchPipeline(blocks, on_complete, 0); +} + +} // namespace passwords diff --git a/ios/chrome/ios_chrome.gyp b/ios/chrome/ios_chrome.gyp index bd89b5c..5334f35 100644 --- a/ios/chrome/ios_chrome.gyp +++ b/ios/chrome/ios_chrome.gyp @@ -15,6 +15,8 @@ ], 'dependencies': [ '../../base/base.gyp:base', + '../../components/components.gyp:autofill_core_browser', + '../../components/components.gyp:autofill_ios_browser', '../../components/components.gyp:dom_distiller_core', '../../components/components.gyp:dom_distiller_ios', '../../components/components.gyp:infobars_core', @@ -53,6 +55,21 @@ 'browser/application_context_impl.h', 'browser/arch_util.cc', 'browser/arch_util.h', + 'browser/autofill/autofill_agent_utils.h', + 'browser/autofill/autofill_agent_utils.mm', + 'browser/autofill/form_input_accessory_view.h', + 'browser/autofill/form_input_accessory_view.mm', + 'browser/autofill/form_input_accessory_view_controller.h', + 'browser/autofill/form_input_accessory_view_controller.mm', + 'browser/autofill/form_input_accessory_view_delegate.h', + 'browser/autofill/form_suggestion_controller.h', + 'browser/autofill/form_suggestion_controller.mm', + 'browser/autofill/form_suggestion_label.h', + 'browser/autofill/form_suggestion_label.mm', + 'browser/autofill/form_suggestion_provider.h', + 'browser/autofill/form_suggestion_view.h', + 'browser/autofill/form_suggestion_view.mm', + 'browser/autofill/form_suggestion_view_client.h', 'browser/browser_state/browser_state_otr_helper.cc', 'browser/browser_state/browser_state_otr_helper.h', 'browser/chrome_switches.cc', @@ -81,6 +98,8 @@ 'browser/infobars/infobar_utils.mm', 'browser/net/image_fetcher.h', 'browser/net/image_fetcher.mm', + 'browser/passwords/password_generation_utils.h', + 'browser/passwords/password_generation_utils.mm', 'browser/pref_names.cc', 'browser/pref_names.h', 'browser/snapshots/snapshot_cache.h', |