blob: 47818209cbc1f8be9c0fe99ba7585db4d969173e [file] [log] [blame]
// Copyright 2018 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_handler.h"
#import <UIKit/UIKit.h>
#include "base/mac/foundation_util.h"
#import "components/autofill/core/browser/keyboard_accessory_metrics_logger.h"
#import "components/autofill/ios/browser/js_suggestion_manager.h"
#import "ios/chrome/browser/ui/util/uikit_ui_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace autofill {
NSString* const kFormSuggestionAssistButtonPreviousElement = @"previousTap";
NSString* const kFormSuggestionAssistButtonNextElement = @"nextTap";
NSString* const kFormSuggestionAssistButtonDone = @"done";
} // namespace autofill
namespace {
// Finds all views of a particular kind if class |aClass| in the subview
// hierarchy of the given |root| view.
NSArray* SubviewsWithClass(UIView* root, Class aClass) {
DCHECK(root);
NSMutableArray* viewsToExamine = [NSMutableArray arrayWithObject:root];
NSMutableArray* subviews = [NSMutableArray array];
while ([viewsToExamine count]) {
UIView* view = [viewsToExamine lastObject];
if ([view isKindOfClass:aClass])
[subviews addObject:view];
[viewsToExamine removeLastObject];
[viewsToExamine addObjectsFromArray:[view subviews]];
}
return subviews;
}
// Returns true if |item|'s action name contains |actionName|.
BOOL ItemActionMatchesName(UIBarButtonItem* item, NSString* actionName) {
SEL itemAction = [item action];
if (!itemAction)
return false;
NSString* itemActionName = NSStringFromSelector(itemAction);
// This doesn't do a strict string match for the action name.
return [itemActionName rangeOfString:actionName].location != NSNotFound;
}
// Finds all UIToolbarItems associated with a given UIToolbar |toolbar| with
// action selectors with a name that contains the action name specified by
// |actionName|.
NSArray* FindToolbarItemsForActionName(UIToolbar* toolbar,
NSString* actionName) {
NSMutableArray* toolbarItems = [NSMutableArray array];
for (UIBarButtonItem* item in [toolbar items]) {
if (ItemActionMatchesName(item, actionName))
[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 = SubviewsWithClass(root, [UIToolbar class]);
for (UIToolbar* toolbar in toolbars) {
[descendants
addObjectsFromArray:FindToolbarItemsForActionName(toolbar, actionName)];
}
return descendants;
}
// Finds all UIBarButtonItem(s) with action selectors of the name specified by
// |actionName| in the UITextInputAssistantItem passed.
NSArray* FindDescendantToolbarItemsForActionName(
UITextInputAssistantItem* inputAssistantItem,
NSString* actionName) {
NSMutableArray* toolbarItems = [NSMutableArray array];
NSMutableArray* buttonGroupsGroup = [[NSMutableArray alloc] init];
if (inputAssistantItem.leadingBarButtonGroups)
[buttonGroupsGroup addObject:inputAssistantItem.leadingBarButtonGroups];
if (inputAssistantItem.trailingBarButtonGroups)
[buttonGroupsGroup addObject:inputAssistantItem.trailingBarButtonGroups];
for (NSArray* buttonGroups in buttonGroupsGroup) {
for (UIBarButtonItemGroup* group in buttonGroups) {
NSArray* items = group.barButtonItems;
for (UIBarButtonItem* item in items) {
if (ItemActionMatchesName(item, actionName))
[toolbarItems addObject:item];
}
}
}
return toolbarItems;
}
} // namespace
@interface FormInputAccessoryViewHandler () {
// The frameId of the frame containing the form with the latest focus.
NSString* _lastFocusFormActivityWebFrameID;
}
// The frameId of the frame containing the form with the latest focus.
@property(nonatomic) NSString* lastFocusFormActivityWebFrameID;
@end
@implementation FormInputAccessoryViewHandler {
// Logs UMA metrics for the keyboard accessory.
std::unique_ptr<autofill::KeyboardAccessoryMetricsLogger>
_keyboardAccessoryMetricsLogger;
}
@synthesize JSSuggestionManager = _JSSuggestionManager;
- (instancetype)init {
self = [super init];
if (self) {
_keyboardAccessoryMetricsLogger.reset(
new autofill::KeyboardAccessoryMetricsLogger());
}
return self;
}
- (void)setLastFocusFormActivityWebFrameID:(NSString*)frameID {
_lastFocusFormActivityWebFrameID = frameID;
}
// 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] Because the buttons on the assist controls can change any
// time, this can break with any new iOS version.
- (BOOL)executeFormAssistAction:(NSString*)actionName {
NSArray* descendants = nil;
if (IsIPadIdiom()) {
// There is no input accessory view for iPads, instead Apple adds the assist
// controls to the UITextInputAssistantItem.
UIResponder* firstResponder = GetFirstResponder();
UITextInputAssistantItem* inputAssistantItem =
firstResponder.inputAssistantItem;
if (!inputAssistantItem)
return NO;
descendants =
FindDescendantToolbarItemsForActionName(inputAssistantItem, actionName);
} else {
UIResponder* firstResponder = GetFirstResponder();
UIView* inputAccessoryView = firstResponder.inputAccessoryView;
if (!inputAccessoryView)
return NO;
descendants =
FindDescendantToolbarItemsForActionName(inputAccessoryView, actionName);
}
if (![descendants count])
return NO;
UIBarButtonItem* item = descendants.firstObject;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[[item target] performSelector:[item action] withObject:item];
#pragma clang diagnostic pop
return YES;
}
- (void)reset {
_keyboardAccessoryMetricsLogger.reset(
new autofill::KeyboardAccessoryMetricsLogger());
}
#pragma mark - FormInputNavigator
- (void)closeKeyboardWithButtonPress {
[self closeKeyboardLoggingButtonPressed:YES];
}
- (void)closeKeyboardWithoutButtonPress {
[self closeKeyboardLoggingButtonPressed:NO];
}
- (void)selectPreviousElementWithButtonPress {
[self selectPreviousElementLoggingButtonPressed:YES];
}
- (void)selectPreviousElementWithoutButtonPress {
[self selectPreviousElementLoggingButtonPressed:NO];
}
- (void)selectNextElementWithButtonPress {
[self selectNextElementLoggingButtonPressed:YES];
}
- (void)selectNextElementWithoutButtonPress {
[self selectNextElementLoggingButtonPressed:NO];
}
- (void)fetchPreviousAndNextElementsPresenceWithCompletionHandler:
(void (^)(BOOL, BOOL))completionHandler {
DCHECK(completionHandler);
[_JSSuggestionManager
fetchPreviousAndNextElementsPresenceInFrameWithID:
_lastFocusFormActivityWebFrameID
completionHandler:completionHandler];
}
#pragma mark - Private
// Tries to close the keyboard sendind an action to the default accessory bar
// if that fails, fallbacks on JavaScript. Logs metrics if loggingButtonPressed
// is YES.
- (void)closeKeyboardLoggingButtonPressed:(BOOL)loggingButtonPressed {
NSString* actionName = autofill::kFormSuggestionAssistButtonDone;
BOOL performedAction = [self executeFormAssistAction:actionName];
if (!performedAction) {
// We could not find the built-in form assist controls, so try to focus
// the next or previous control using JavaScript.
[_JSSuggestionManager
closeKeyboardForFrameWithID:_lastFocusFormActivityWebFrameID];
}
if (loggingButtonPressed) {
_keyboardAccessoryMetricsLogger->OnCloseButtonPressed();
}
}
// Tries to focus on the next element sendind an action to the default accessory
// bar if that fails, fallbacks on JavaScript. Logs metrics if
// loggingButtonPressed is YES.
- (void)selectPreviousElementLoggingButtonPressed:(BOOL)loggingButtonPressed {
NSString* actionName = autofill::kFormSuggestionAssistButtonPreviousElement;
BOOL performedAction = [self executeFormAssistAction:actionName];
if (!performedAction) {
// We could not find the built-in form assist controls, so try to focus
// the next or previous control using JavaScript.
[_JSSuggestionManager
selectPreviousElementInFrameWithID:_lastFocusFormActivityWebFrameID];
}
if (loggingButtonPressed) {
_keyboardAccessoryMetricsLogger->OnPreviousButtonPressed();
}
}
// Tries to focus on the previous element sendind an action to the default
// accessory bar if that fails, fallbacks on JavaScript. Logs metrics if
// loggingButtonPressed is YES.
- (void)selectNextElementLoggingButtonPressed:(BOOL)loggingButtonPressed {
NSString* actionName = autofill::kFormSuggestionAssistButtonNextElement;
BOOL performedAction = [self executeFormAssistAction:actionName];
if (!performedAction) {
// We could not find the built-in form assist controls, so try to focus
// the next or previous control using JavaScript.
[_JSSuggestionManager
selectNextElementInFrameWithID:_lastFocusFormActivityWebFrameID];
}
if (loggingButtonPressed) {
_keyboardAccessoryMetricsLogger->OnNextButtonPressed();
}
}
@end