blob: b86d500b503edb265deb6d6dbc89dbc21a519957 [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/ui/autofill/form_input_accessory_mediator.h"
#include "base/ios/block_types.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_block.h"
#include "components/autofill/core/common/autofill_features.h"
#import "components/autofill/ios/browser/js_suggestion_manager.h"
#import "components/autofill/ios/form_util/form_activity_observer_bridge.h"
#include "components/autofill/ios/form_util/form_activity_params.h"
#import "ios/chrome/browser/autofill/form_input_accessory_consumer.h"
#import "ios/chrome/browser/autofill/form_input_accessory_view_handler.h"
#import "ios/chrome/browser/autofill/form_input_accessory_view_provider.h"
#import "ios/chrome/browser/autofill/form_suggestion_tab_helper.h"
#import "ios/chrome/browser/autofill/form_suggestion_view.h"
#import "ios/chrome/browser/passwords/password_generation_utils.h"
#import "ios/chrome/browser/ui/autofill/manual_fill/keyboard_observer_helper.h"
#import "ios/chrome/browser/ui/autofill/manual_fill/manual_fill_accessory_view_controller.h"
#import "ios/chrome/browser/ui/coordinators/chrome_coordinator.h"
#include "ios/chrome/browser/ui/ui_util.h"
#import "ios/chrome/browser/web_state_list/web_state_list.h"
#import "ios/web/public/url_scheme_util.h"
#import "ios/web/public/web_state/js/crw_js_injection_receiver.h"
#include "ios/web/public/web_state/web_state.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@interface FormInputAccessoryMediator ()<FormActivityObserver,
CRWWebStateObserver,
KeyboardObserverHelperDelegate,
WebStateListObserving>
// The JS manager for interacting with the underlying form.
@property(nonatomic, weak) JsSuggestionManager* JSSuggestionManager;
// The main consumer for this mediator.
@property(nonatomic, weak) id<FormInputAccessoryConsumer> consumer;
// The object that manages the currently-shown custom accessory view.
@property(nonatomic, weak) id<FormInputAccessoryViewProvider> currentProvider;
// The form input handler. This is in charge of form navigation.
@property(nonatomic, strong)
FormInputAccessoryViewHandler* formInputAccessoryHandler;
// The observer to determine when the keyboard dissapears and when it stays.
@property(nonatomic, strong) KeyboardObserverHelper* keyboardObserver;
// Last seen provider. Used to reenable suggestions.
@property(nonatomic, weak) id<FormInputAccessoryViewProvider> lastProvider;
// Last seen suggestions. Used to reenable suggestions.
@property(nonatomic, strong) UIView* lastSuggestionView;
// Whether suggestions are disabled.
@property(nonatomic, assign) BOOL suggestionsDisabled;
// The objects that can provide a custom input accessory view while filling
// forms.
@property(nonatomic, copy)
NSArray<id<FormInputAccessoryViewProvider>>* providers;
// The WebState this instance is observing. Can be null.
@property(nonatomic, assign) web::WebState* webState;
@end
@implementation FormInputAccessoryMediator {
// The WebStateList this instance is observing in order to update the
// active WebState.
WebStateList* _webStateList;
// Bridge to observe the web state list from Objective-C.
std::unique_ptr<WebStateListObserverBridge> _webStateListObserver;
// Bridge to observe the web state from Objective-C.
std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
// Bridge to observe form activity in |_webState|.
std::unique_ptr<autofill::FormActivityObserverBridge>
_formActivityObserverBridge;
// Whether suggestions have previously been shown.
BOOL _suggestionsHaveBeenShown;
}
- (instancetype)initWithConsumer:(id<FormInputAccessoryConsumer>)consumer
webStateList:(WebStateList*)webStateList {
self = [super init];
if (self) {
_consumer = consumer;
if (webStateList) {
_webStateList = webStateList;
_webStateListObserver =
std::make_unique<WebStateListObserverBridge>(self);
_webStateList->AddObserver(_webStateListObserver.get());
web::WebState* webState = webStateList->GetActiveWebState();
if (webState) {
_webState = webState;
CRWJSInjectionReceiver* injectionReceiver =
webState->GetJSInjectionReceiver();
_JSSuggestionManager = base::mac::ObjCCastStrict<JsSuggestionManager>(
[injectionReceiver instanceOfClass:[JsSuggestionManager class]]);
_providers = @[ FormSuggestionTabHelper::FromWebState(webState)
->GetAccessoryViewProvider() ];
_formActivityObserverBridge =
std::make_unique<autofill::FormActivityObserverBridge>(_webState,
self);
_webStateObserverBridge =
std::make_unique<web::WebStateObserverBridge>(self);
webState->AddObserver(_webStateObserverBridge.get());
}
}
_formInputAccessoryHandler = [[FormInputAccessoryViewHandler alloc] init];
_formInputAccessoryHandler.JSSuggestionManager = _JSSuggestionManager;
NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter addObserver:self
selector:@selector(handleTextInputDidBeginEditing:)
name:UITextFieldTextDidBeginEditingNotification
object:nil];
_keyboardObserver = [[KeyboardObserverHelper alloc] init];
_keyboardObserver.delegate = self;
}
return self;
}
- (void)dealloc {
if (_webState) {
_webState->RemoveObserver(_webStateObserverBridge.get());
_webStateObserverBridge.reset();
_webState = nullptr;
_formActivityObserverBridge.reset();
}
if (_webStateList) {
_webStateList->RemoveObserver(_webStateListObserver.get());
_webStateListObserver.reset();
_webStateList = nullptr;
}
_formActivityObserverBridge.reset();
}
- (void)detachFromWebState {
[self reset];
if (_webState) {
_webState->RemoveObserver(_webStateObserverBridge.get());
_webStateObserverBridge.reset();
_webState = nullptr;
_formActivityObserverBridge.reset();
}
}
#pragma mark - KeyboardObserverHelperDelegate
- (void)keyboardDidStayOnScreen {
[self.consumer removeAnimationsOnKeyboardView];
}
- (void)keyboardDidHide {
[self reset];
}
#pragma mark - FormActivityObserver
- (void)webState:(web::WebState*)webState
didRegisterFormActivity:(const autofill::FormActivityParams&)params
inFrame:(web::WebFrame*)frame {
DCHECK_EQ(_webState, webState);
web::URLVerificationTrustLevel trustLevel;
const GURL pageURL(webState->GetCurrentURL(&trustLevel));
if (params.input_missing ||
trustLevel != web::URLVerificationTrustLevel::kAbsolute ||
!web::UrlHasWebScheme(pageURL) || !webState->ContentIsHTML()) {
[self reset];
return;
}
if (params.type == "blur" || params.type == "change" ||
params.type == "form_changed") {
return;
}
[self retrieveAccessoryViewForForm:params webState:webState];
}
#pragma mark - CRWWebStateObserver
- (void)webStateWasHidden:(web::WebState*)webState {
DCHECK_EQ(_webState, webState);
// On some iPhone with newers iOS (>11.3) when a view controller is presented,
// i.e. "all passwords", after dismissing it the keyboard appears and the last
// element is focused. Different devices were not consistent with minor
// versions changes. On iPad it always stays dismissed. It is important to
// reset on iPads because the accessory will stay without the keyboard, due
// how the it is added, On iPhones it will be hidden and reset when other text
// element gets the focus. On iPad the keyboard stays dismissed.
if (IsIPadIdiom()) {
[self reset];
}
}
- (void)webState:(web::WebState*)webState didLoadPageWithSuccess:(BOOL)success {
DCHECK_EQ(_webState, webState);
[self reset];
}
- (void)webStateDestroyed:(web::WebState*)webState {
DCHECK_EQ(_webState, webState);
[self detachFromWebState];
}
#pragma mark - CRWWebStateListObserver
- (void)webStateList:(WebStateList*)webStateList
didChangeActiveWebState:(web::WebState*)newWebState
oldWebState:(web::WebState*)oldWebState
atIndex:(int)atIndex
reason:(int)reason {
[self updateWithNewWebState:newWebState];
}
#pragma mark - Public
- (void)disableSuggestions {
self.suggestionsDisabled = YES;
[self updateWithProvider:nil suggestionView:nil];
}
- (void)enableSuggestions {
self.suggestionsDisabled = NO;
if (self.lastProvider && self.lastSuggestionView) {
[self updateWithProvider:self.lastProvider
suggestionView:self.lastSuggestionView];
}
}
#pragma mark - Setters
- (void)setCurrentProvider:(id<FormInputAccessoryViewProvider>)currentProvider {
if (_currentProvider == currentProvider) {
return;
}
[_currentProvider inputAccessoryViewControllerDidReset];
_currentProvider = currentProvider;
[_currentProvider setAccessoryViewDelegate:self.formInputAccessoryHandler];
}
#pragma mark - Private
// Updates the accessory mediator with the passed web state, its JS suggestion
// manager and the registered providers. If NULL is passed it will instead clear
// those properties in the mediator.
- (void)updateWithNewWebState:(web::WebState*)webState {
[self detachFromWebState];
if (webState) {
self.webState = webState;
_webStateObserverBridge =
std::make_unique<web::WebStateObserverBridge>(self);
webState->AddObserver(_webStateObserverBridge.get());
_formActivityObserverBridge =
std::make_unique<autofill::FormActivityObserverBridge>(webState, self);
CRWJSInjectionReceiver* injectionReceiver =
webState->GetJSInjectionReceiver();
self.JSSuggestionManager = base::mac::ObjCCastStrict<JsSuggestionManager>(
[injectionReceiver instanceOfClass:[JsSuggestionManager class]]);
self.providers = @[ FormSuggestionTabHelper::FromWebState(webState)
->GetAccessoryViewProvider() ];
_formInputAccessoryHandler.JSSuggestionManager = self.JSSuggestionManager;
} else {
self.webState = NULL;
self.JSSuggestionManager = nil;
self.providers = @[];
}
}
// Resets the current provider, the consumer view and the navigation handler. As
// well as reenables suggestions.
- (void)reset {
[self.consumer restoreKeyboardView];
[self.manualFillAccessoryViewController reset];
[self.formInputAccessoryHandler reset];
self.suggestionsDisabled = NO;
self.currentProvider = nil;
}
// Asynchronously queries the providers for an accessory view. Sends it to
// the consumer if found.
- (void)retrieveAccessoryViewForForm:(const autofill::FormActivityParams&)params
webState:(web::WebState*)webState {
DCHECK_EQ(webState, self.webState);
// TODO(crbug.com/845472): refactor this overly complex code. There is
// always at max one provider in _providers.
// 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.
NSMutableArray* findProviderBlocks = [[NSMutableArray alloc] init];
for (id<FormInputAccessoryViewProvider> provider in _providers) {
passwords::PipelineBlock findProviderBlock =
[self queryViewBlockForProvider:provider params:params];
[findProviderBlocks addObject:findProviderBlock];
}
// 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, ^(NSUInteger providerIndex){
// No need to do anything if no suggestions
// are found. The provider will
// update with an empty suggestions array.
});
}
// Returns a pipeline block used to search for a provider with the current form
// params.
- (passwords::PipelineBlock)
queryViewBlockForProvider:(id<FormInputAccessoryViewProvider>)provider
params:(autofill::FormActivityParams)params {
__weak __typeof(self) weakSelf = self;
return ^(void (^completion)(BOOL success)) {
FormInputAccessoryMediator* strongSelf = weakSelf;
if (!strongSelf) {
return;
}
AccessoryViewReadyCompletion accessoryViewReadyCompletion =
[self accessoryViewReadyBlockWithCompletion:completion];
[provider retrieveAccessoryViewForForm:params
webState:strongSelf.webState
accessoryViewUpdateBlock:accessoryViewReadyCompletion];
};
}
// Returns a block setting up the provider and the view returned. It calls the
// passed completion with NO if the view found is invalid. With YES otherwise.
- (AccessoryViewReadyCompletion)accessoryViewReadyBlockWithCompletion:
(void (^)(BOOL success))completion {
__weak __typeof(self) weakSelf = self;
return ^(UIView* accessoryView, id<FormInputAccessoryViewProvider> provider) {
// View is nil, tell the pipeline to continue searching.
if (!accessoryView) {
completion(NO);
return;
}
// Once the view is retrieved, tell the pipeline to stop and
// update the UI.
completion(YES);
[weakSelf updateWithProvider:provider suggestionView:accessoryView];
};
}
// Post the passed |suggestionView| to the consumer. In case suggestions are
// disabled, it's keep for later.
- (void)updateWithProvider:(id<FormInputAccessoryViewProvider>)provider
suggestionView:(UIView*)suggestionView {
// If the povider is valid, save the view and the provider for later. This is
// used to restore the state when re-enabling suggestions.
if (provider) {
self.lastSuggestionView = suggestionView;
self.lastProvider = provider;
}
// If the suggestions are disabled, post this view with no suggestions to the
// consumer. This allows the navigation buttons be in sync.
UIView* consumerView = suggestionView;
if (self.suggestionsDisabled) {
consumerView = [[FormSuggestionView alloc] initWithFrame:CGRectZero
client:nil
suggestions:@[]];
} else {
// If suggestions are enabled update |currentProvider|.
self.currentProvider = provider;
}
// If Manual Fallback is enabled, add its view after the suggestions.
if (autofill::features::IsPasswordManualFallbackEnabled()) {
FormSuggestionView* formSuggestionView =
base::mac::ObjCCast<FormSuggestionView>(consumerView);
formSuggestionView.trailingView =
self.manualFillAccessoryViewController.view;
}
// Post it to the consumer.
[self.consumer showCustomInputAccessoryView:consumerView
navigationDelegate:self.formInputAccessoryHandler];
}
// When any text field or text view (e.g. omnibox, settings, card unmask dialog)
// begins editing, reset ourselves so that we don't present our custom view over
// the keyboard.
- (void)handleTextInputDidBeginEditing:(NSNotification*)notification {
[self reset];
}
#pragma mark - Tests
- (void)injectWebState:(web::WebState*)webState {
[self detachFromWebState];
_webState = webState;
if (!_webState) {
return;
}
_webStateObserverBridge = std::make_unique<web::WebStateObserverBridge>(self);
_webState->AddObserver(_webStateObserverBridge.get());
_formActivityObserverBridge =
std::make_unique<autofill::FormActivityObserverBridge>(_webState, self);
}
- (void)injectSuggestionManager:(JsSuggestionManager*)JSSuggestionManager {
_JSSuggestionManager = JSSuggestionManager;
_formInputAccessoryHandler.JSSuggestionManager = _JSSuggestionManager;
}
- (void)injectProviders:
(NSArray<id<FormInputAccessoryViewProvider>>*)providers {
self.providers = providers;
}
@end