Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(576)

Unified Diff: ios/chrome/browser/autofill/form_input_accessory_view_controller.mm

Issue 1022463002: [iOS] Upstream files in //ios/chrome/browser/autofill (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Rebase Created 5 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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

Powered by Google App Engine
This is Rietveld 408576698