blob: 5879bee8f56047511e828f04c63ae84f4935c32e [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/form_input_accessory_mediator.h"
#include "base/ios/block_types.h"
#include "base/ios/ios_util.h"
#include "base/mac/foundation_util.h"
#include "base/metrics/histogram_functions.h"
#import "base/strings/sys_string_conversions.h"
#include "components/autofill/core/browser/personal_data_manager.h"
#include "components/autofill/core/common/autofill_features.h"
#import "components/autofill/ios/browser/form_suggestion.h"
#import "components/autofill/ios/browser/form_suggestion_provider.h"
#import "components/autofill/ios/browser/personal_data_manager_observer_bridge.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/app/application_delegate/app_state.h"
#import "ios/chrome/browser/autofill/form_input_accessory_view_handler.h"
#import "ios/chrome/browser/autofill/form_input_suggestions_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/autofill/manual_fill/passwords_fetcher.h"
#import "ios/chrome/browser/passwords/password_generation_utils.h"
#import "ios/chrome/browser/ui/autofill/form_input_accessory/form_input_accessory_consumer.h"
#import "ios/chrome/browser/ui/autofill/form_input_accessory/form_input_accessory_view.h"
#import "ios/chrome/browser/ui/commands/security_alert_commands.h"
#import "ios/chrome/browser/ui/coordinators/chrome_coordinator.h"
#import "ios/chrome/browser/ui/default_promo/default_browser_utils.h"
#import "ios/chrome/browser/ui/ui_feature_flags.h"
#import "ios/chrome/browser/ui/util/keyboard_observer_helper.h"
#import "ios/chrome/browser/ui/util/ui_util.h"
#import "ios/chrome/browser/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/web_state_list/web_state_list.h"
#include "ios/chrome/common/ui/reauthentication/reauthentication_event.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_module.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/web/common/url_scheme_util.h"
#include "ios/web/public/js_messaging/web_frame.h"
#include "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/web_state.h"
#include "ui/base/l10n/l10n_util_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using base::UmaHistogramEnumeration;
@interface FormInputAccessoryMediator () <AppStateObserver,
FormActivityObserver,
FormInputAccessoryViewDelegate,
CRWWebStateObserver,
KeyboardObserverHelperConsumer,
PasswordFetcherDelegate,
PersonalDataManagerObserver,
WebStateListObserving>
// The main consumer for this mediator.
@property(nonatomic, weak) id<FormInputAccessoryConsumer> consumer;
// The delegate for this object.
@property(nonatomic, weak) id<FormInputAccessoryMediatorDelegate> delegate;
// The object that manages the currently-shown custom accessory view.
@property(nonatomic, weak) id<FormInputSuggestionsProvider> currentProvider;
// YES if the first responder is valid.
@property(nonatomic, assign) BOOL firstResponderIsValid;
// 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;
// The object that provides suggestions while filling forms.
@property(nonatomic, weak) id<FormInputSuggestionsProvider> provider;
// The password fetcher used to know if passwords are available and update the
// consumer accordingly.
@property(nonatomic, strong) PasswordFetcher* passwordFetcher;
// Whether suggestions are disabled.
@property(nonatomic, assign) BOOL suggestionsDisabled;
// YES if the latest form activity was made in a form that supports the
// accessory.
@property(nonatomic, assign) BOOL validActivityForAccessoryView;
// The WebState this instance is observing. Can be null.
@property(nonatomic, assign) web::WebState* webState;
// Contains information about the application state, for example the last window
// that was tapped.
@property(nonatomic, weak) AppState* appState;
// Reauthentication Module used for re-authentication.
@property(nonatomic, strong) ReauthenticationModule* reauthenticationModule;
// Used to present alerts.
@property(nonatomic, weak) id<SecurityAlertCommands> securityAlertHandler;
@end
@implementation FormInputAccessoryMediator {
// The WebStateList this instance is observing in order to update the
// active WebState.
WebStateList* _webStateList;
// Personal data manager to be observed.
autofill::PersonalDataManager* _personalDataManager;
// C++ to ObjC bridge for PersonalDataManagerObserver.
std::unique_ptr<autofill::PersonalDataManagerObserverBridge>
_personalDataManagerObserver;
// 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;
// The last seen valid params of a form before retrieving suggestions. Or
// empty if |_hasLastSeenParams| is NO.
autofill::FormActivityParams _lastSeenParams;
// If YES |_lastSeenParams| is valid.
BOOL _hasLastSeenParams;
}
- (instancetype)
initWithConsumer:(id<FormInputAccessoryConsumer>)consumer
delegate:(id<FormInputAccessoryMediatorDelegate>)delegate
webStateList:(WebStateList*)webStateList
personalDataManager:(autofill::PersonalDataManager*)personalDataManager
passwordStore:
(scoped_refptr<password_manager::PasswordStore>)passwordStore
appState:(AppState*)appState
securityAlertHandler:(id<SecurityAlertCommands>)securityAlertHandler
reauthenticationModule:(ReauthenticationModule*)reauthenticationModule {
self = [super init];
if (self) {
_consumer = consumer;
_consumer.navigationDelegate = self;
_delegate = delegate;
if (webStateList) {
_webStateList = webStateList;
_webStateListObserver =
std::make_unique<WebStateListObserverBridge>(self);
_webStateList->AddObserver(_webStateListObserver.get());
web::WebState* webState = webStateList->GetActiveWebState();
if (webState) {
_webState = webState;
FormSuggestionTabHelper* tabHelper =
FormSuggestionTabHelper::FromWebState(webState);
if (tabHelper) {
_provider = tabHelper->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.webState = _webState;
NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter addObserver:self
selector:@selector(handleTextInputDidBeginEditing:)
name:UITextFieldTextDidBeginEditingNotification
object:nil];
[defaultCenter addObserver:self
selector:@selector(handleTextInputDidEndEditing:)
name:UITextFieldTextDidEndEditingNotification
object:nil];
[defaultCenter addObserver:self
selector:@selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[defaultCenter addObserver:self
selector:@selector(windowDidBecomeKey:)
name:UIWindowDidBecomeKeyNotification
object:nil];
_keyboardObserver = [[KeyboardObserverHelper alloc] init];
_keyboardObserver.consumer = self;
// In BVC unit tests the password store doesn't exist. Skip creating the
// fetcher.
// TODO:(crbug.com/878388) Remove this workaround.
if (passwordStore) {
_passwordFetcher =
[[PasswordFetcher alloc] initWithPasswordStore:passwordStore
delegate:self
URL:GURL::EmptyGURL()];
}
if (personalDataManager) {
_personalDataManager = personalDataManager;
_personalDataManagerObserver.reset(
new autofill::PersonalDataManagerObserverBridge(self));
personalDataManager->AddObserver(_personalDataManagerObserver.get());
// TODO:(crbug.com/845472) Add earl grey test to verify the credit card
// button is hidden when local cards are saved and then
// kAutofillCreditCardEnabled is changed to disabled.
consumer.creditCardButtonHidden =
personalDataManager->GetCreditCards().empty();
consumer.addressButtonHidden =
personalDataManager->GetProfilesToSuggest().empty();
} else {
consumer.creditCardButtonHidden = YES;
consumer.addressButtonHidden = YES;
}
_appState = appState;
if (!base::ios::IsRunningOnIOS14OrLater()) {
[_appState addObserver:self];
}
_reauthenticationModule = reauthenticationModule;
_securityAlertHandler = securityAlertHandler;
}
return self;
}
- (void)dealloc {
[self disconnect];
}
- (void)disconnect {
_formActivityObserverBridge.reset();
if (_personalDataManager && _personalDataManagerObserver.get()) {
_personalDataManager->RemoveObserver(_personalDataManagerObserver.get());
_personalDataManagerObserver.reset();
}
if (_webState) {
_webState->RemoveObserver(_webStateObserverBridge.get());
_webStateObserverBridge.reset();
_webState = nullptr;
}
if (_webStateList) {
_webStateList->RemoveObserver(_webStateListObserver.get());
_webStateListObserver.reset();
_webStateList = nullptr;
}
if (!base::ios::IsRunningOnIOS14OrLater()) {
[_appState removeObserver:self];
}
}
- (void)detachFromWebState {
[self reset];
if (_webState) {
_webState->RemoveObserver(_webStateObserverBridge.get());
_webStateObserverBridge.reset();
_webState = nullptr;
_formActivityObserverBridge.reset();
}
}
- (BOOL)lastFocusedFieldWasPassword {
return _lastSeenParams.field_type == autofill::kPasswordFieldType;
}
#pragma mark - KeyboardObserverHelperConsumer
- (void)keyboardDidStayOnScreen {
[self.consumer removeAnimationsOnKeyboardView];
}
- (void)keyboardWillChangeToState:(KeyboardState)keyboardState {
if (keyboardState.isVisible) {
[self verifyFirstResponderAndUpdateCustomKeyboardView];
[self updateSuggestionsIfNeeded];
}
[self.consumer keyboardWillChangeToState:keyboardState];
if (!keyboardState.isVisible) {
[self.delegate mediatorDidDetectKeyboardHide:self];
}
}
#pragma mark - FormActivityObserver
- (void)webState:(web::WebState*)webState
didRegisterFormActivity:(const autofill::FormActivityParams&)params
inFrame:(web::WebFrame*)frame {
DCHECK_EQ(_webState, webState);
self.validActivityForAccessoryView = NO;
// Return early if |params| is not complete.
if (params.input_missing) {
return;
}
// Return early if the URL can't be verified.
web::URLVerificationTrustLevel trustLevel;
const GURL pageURL(webState->GetCurrentURL(&trustLevel));
if (trustLevel != web::URLVerificationTrustLevel::kAbsolute) {
return;
}
// Return early, pause and reset if the url is not HTML.
if (!web::UrlHasWebScheme(pageURL) || !webState->ContentIsHTML()) {
[self pauseCustomKeyboardView];
[self reset];
return;
}
// Return early and reset if frame is missing or can't call JS.
if (!frame || !frame->CanCallJavaScriptFunction()) {
[self reset];
return;
}
self.validActivityForAccessoryView = YES;
[self continueCustomKeyboardView];
NSString* frameID;
if (frame) {
frameID = base::SysUTF8ToNSString(frame->GetFrameId());
}
DCHECK(frameID.length);
[self.formInputAccessoryHandler setLastFocusFormActivityWebFrameID:frameID];
[self synchronizeNavigationControls];
// Don't look for suggestions in the next events.
if (params.type == "blur" || params.type == "change" ||
params.type == "form_changed") {
return;
}
_lastSeenParams = params;
_hasLastSeenParams = YES;
[self.consumer prepareToShowSuggestions];
[self retrieveSuggestionsForForm:params webState:webState];
}
#pragma mark - FormInputAccessoryViewDelegate
- (void)formInputAccessoryViewDidTapNextButton:(FormInputAccessoryView*)sender {
[self.formInputAccessoryHandler selectNextElementWithButtonPress];
}
- (void)formInputAccessoryViewDidTapPreviousButton:
(FormInputAccessoryView*)sender {
[self.formInputAccessoryHandler selectPreviousElementWithButtonPress];
}
- (void)formInputAccessoryViewDidTapCloseButton:
(FormInputAccessoryView*)sender {
[self.formInputAccessoryHandler closeKeyboardWithButtonPress];
}
#pragma mark - CRWWebStateObserver
- (void)webStateWasShown:(web::WebState*)webState {
DCHECK_EQ(_webState, webState);
[self continueCustomKeyboardView];
[self updateSuggestionsIfNeeded];
}
- (void)webStateWasHidden:(web::WebState*)webState {
DCHECK_EQ(_webState, webState);
[self pauseCustomKeyboardView];
}
- (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:(ActiveWebStateChangeReason)reason {
[self reset];
[self updateWithNewWebState:newWebState];
}
#pragma mark - Public
- (void)disableSuggestions {
self.suggestionsDisabled = YES;
}
- (void)enableSuggestions {
self.suggestionsDisabled = NO;
[self updateSuggestionsIfNeeded];
}
#pragma mark - Setters
- (void)setCurrentProvider:(id<FormInputSuggestionsProvider>)currentProvider {
if (_currentProvider == currentProvider) {
return;
}
[_currentProvider inputAccessoryViewControllerDidReset];
_currentProvider = currentProvider;
_currentProvider.formInputNavigator = self.formInputAccessoryHandler;
}
#pragma mark - Private
- (void)updateSuggestionsIfNeeded {
if (_hasLastSeenParams && _webState) {
[self retrieveSuggestionsForForm:_lastSeenParams webState:_webState];
}
}
// Tells the consumer to pause the custom keyboard view.
- (void)pauseCustomKeyboardView {
[self.consumer pauseCustomKeyboardView];
}
// Tells the consumer to continue the custom keyboard view if the last activity
// is valid, the web state is visible, and there is no other text input.
- (void)continueCustomKeyboardView {
// Return early if the form is not a supported one.
if (!self.validActivityForAccessoryView) {
return;
}
// Return early if the current webstate is not visible.
if (!self.webState || !self.webState->IsVisible()) {
return;
}
// Return early if the current input is not valid.
if (!self.firstResponderIsValid) {
return;
}
[self.consumer continueCustomKeyboardView];
}
// Update the status of the consumer form navigation buttons to match the
// handler state.
- (void)synchronizeNavigationControls {
__weak __typeof(self) weakSelf = self;
[self.formInputAccessoryHandler
fetchPreviousAndNextElementsPresenceWithCompletionHandler:^(
bool previousButtonEnabled, bool nextButtonEnabled) {
weakSelf.consumer.formInputNextButtonEnabled = nextButtonEnabled;
weakSelf.consumer.formInputPreviousButtonEnabled =
previousButtonEnabled;
}];
}
// Updates the accessory mediator with the passed web state, its JS suggestion
// manager and the registered provider. If nullptr 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);
FormSuggestionTabHelper* tabHelper =
FormSuggestionTabHelper::FromWebState(webState);
if (tabHelper) {
self.provider = tabHelper->GetAccessoryViewProvider();
}
_formInputAccessoryHandler.webState = webState;
} else {
self.webState = nullptr;
self.provider = nil;
}
}
// Resets the current provider, the consumer view and the navigation handler. As
// well as reenables suggestions.
- (void)reset {
_lastSeenParams = autofill::FormActivityParams();
_hasLastSeenParams = NO;
[self.consumer restoreOriginalKeyboardView];
[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)retrieveSuggestionsForForm:(const autofill::FormActivityParams&)params
webState:(web::WebState*)webState {
DCHECK_EQ(webState, self.webState);
DCHECK(_hasLastSeenParams);
__weak id<FormInputSuggestionsProvider> provider = self.provider;
__weak __typeof(self) weakSelf = self;
[provider
retrieveSuggestionsForForm:params
webState:self.webState
accessoryViewUpdateBlock:^(NSArray<FormSuggestion*>* suggestions,
id<FormInputSuggestionsProvider> provider) {
// No suggestions found, return.
if (!suggestions) {
return;
}
[weakSelf updateWithProvider:provider suggestions:suggestions];
}];
}
// Post the passed |suggestionView| to the consumer. In case suggestions are
// disabled, it's keep for later.
- (void)updateWithProvider:(id<FormInputSuggestionsProvider>)provider
suggestions:(NSArray<FormSuggestion*>*)suggestions {
// If the suggestions are disabled, post this view with no suggestions to the
// consumer. This allows the navigation buttons be in sync.
if (self.suggestionsDisabled) {
return;
} else {
// If suggestions are enabled update |currentProvider|.
self.currentProvider = provider;
// Post it to the consumer.
[self.consumer showAccessorySuggestions:suggestions];
if (suggestions.count) {
if (provider.type == SuggestionProviderTypeAutofill) {
LogLikelyInterestedDefaultBrowserUserActivity(
DefaultPromoTypeMadeForIOS);
}
}
}
}
// Inform the delegate that the app went to the background.
- (void)applicationDidEnterBackground:(NSNotification*)notification {
[self.delegate mediatorDidDetectMovingToBackground:self];
}
- (void)windowDidBecomeKey:(NSNotification*)notification {
[self verifyFirstResponderAndUpdateCustomKeyboardView];
}
// Verifies that the first responder is a child of WKWebView and that is is not
// a child of SSOSignInViewController. Pause or try to continue the keyboard
// custom view depending on the validity of the first responder.
- (void)verifyFirstResponderAndUpdateCustomKeyboardView {
if (!self.webState) {
self.firstResponderIsValid = NO;
[self pauseCustomKeyboardView];
return;
}
BOOL ancestorIsSSOSignInViewController = NO;
BOOL ancestorIsWkWebView = NO;
UIView* webStateContainerView = self.webState->GetView();
BOOL webStateInKeyWindow = webStateContainerView.window.isKeyWindow;
if (!base::ios::IsRunningOnIOS14OrLater()) {
// This is a workaround for a bug in iOS multiwindow, in which you can touch
// a webView without the window getting the keyboard focus. The result is
// that you focus a field in the new window gains focus, but keyboard typing
// continue to happen in the other window.
// TODO(crbug.com/1109124): Remove this workaround.
webStateInKeyWindow =
webStateInKeyWindow &&
webStateContainerView.window == self.appState.lastTappedWindow;
}
if (webStateInKeyWindow) {
UIResponder* firstResponder = GetFirstResponder();
while (firstResponder) {
if ([firstResponder isKindOfClass:NSClassFromString(@"WKWebView")]) {
ancestorIsWkWebView = YES;
}
if ([firstResponder
isKindOfClass:NSClassFromString(@"SSOSignInViewController")]) {
ancestorIsSSOSignInViewController = YES;
break;
}
firstResponder = firstResponder.nextResponder;
}
}
self.firstResponderIsValid = webStateInKeyWindow && ancestorIsWkWebView &&
!ancestorIsSSOSignInViewController;
if (self.firstResponderIsValid) {
[self continueCustomKeyboardView];
} else {
[self pauseCustomKeyboardView];
}
}
#pragma mark - Keyboard Notifications
// When any text field or text view (e.g. omnibox, settings search bar)
// begins editing, pause the consumer so it doesn't present the custom view over
// the keyboard.
- (void)handleTextInputDidBeginEditing:(NSNotification*)notification {
self.firstResponderIsValid = NO;
[self pauseCustomKeyboardView];
}
// When any text field or text view (e.g. omnibox, settings, card unmask dialog)
// ends editing, continue presenting.
- (void)handleTextInputDidEndEditing:(NSNotification*)notification {
[self verifyFirstResponderAndUpdateCustomKeyboardView];
}
#pragma mark - FormSuggestionClient
- (void)didSelectSuggestion:(FormSuggestion*)formSuggestion {
UmaHistogramEnumeration("IOS.Reauth.Password.Autofill",
ReauthenticationEvent::kAttempt);
__weak __typeof(self) weakSelf = self;
auto suggestionHandler = ^() {
if (weakSelf.currentProvider.type == SuggestionProviderTypePassword) {
LogLikelyInterestedDefaultBrowserUserActivity(DefaultPromoTypeStaySafe);
}
[weakSelf.currentProvider didSelectSuggestion:formSuggestion];
};
if (!formSuggestion.requiresReauth) {
UmaHistogramEnumeration("IOS.Reauth.Password.Autofill",
ReauthenticationEvent::kSuccess);
suggestionHandler();
return;
}
if ([self.reauthenticationModule canAttemptReauth]) {
NSString* reason = l10n_util::GetNSString(IDS_IOS_AUTOFILL_REAUTH_REASON);
auto completionHandler = ^(ReauthenticationResult result) {
if (result != ReauthenticationResult::kFailure) {
UmaHistogramEnumeration("IOS.Reauth.Password.Autofill",
ReauthenticationEvent::kSuccess);
suggestionHandler();
} else {
UmaHistogramEnumeration("IOS.Reauth.Password.Autofill",
ReauthenticationEvent::kFailure);
}
};
[self.reauthenticationModule
attemptReauthWithLocalizedReason:reason
canReusePreviousAuth:YES
handler:completionHandler];
} else {
UmaHistogramEnumeration("IOS.Reauth.Password.Autofill",
ReauthenticationEvent::kMissingPasscode);
suggestionHandler();
}
}
#pragma mark - PasswordFetcherDelegate
- (void)passwordFetcher:(PasswordFetcher*)passwordFetcher
didFetchPasswords:
(std::vector<std::unique_ptr<password_manager::PasswordForm>>)
passwords {
self.consumer.passwordButtonHidden = passwords.empty();
}
#pragma mark - PersonalDataManagerObserver
- (void)onPersonalDataChanged {
DCHECK(_personalDataManager);
self.consumer.creditCardButtonHidden =
_personalDataManager->GetCreditCards().empty();
self.consumer.addressButtonHidden =
_personalDataManager->GetProfilesToSuggest().empty();
}
#pragma mark - AppStateObserver
- (void)appState:(AppState*)appState lastTappedWindowChanged:(UIWindow*)window {
[self verifyFirstResponderAndUpdateCustomKeyboardView];
}
#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)injectProvider:(id<FormInputSuggestionsProvider>)provider {
self.provider = provider;
}
@end