blob: ce8454c15368362d5a38a76e423efcdbbd35c67b [file] [log] [blame]
// Copyright 2012 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/find_in_page/find_in_page_controller.h"
#import <UIKit/UIKit.h>
#import <cmath>
#include <memory>
#import "base/logging.h"
#import "base/mac/foundation_util.h"
#import "ios/chrome/browser/find_in_page/find_in_page_model.h"
#import "ios/chrome/browser/find_in_page/js_findinpage_manager.h"
#include "ios/chrome/browser/metrics/ukm_url_recorder.h"
#import "ios/chrome/browser/web/dom_altering_lock.h"
#import "ios/web/public/web_state/js/crw_js_injection_receiver.h"
#import "ios/web/public/web_state/ui/crw_web_view_proxy.h"
#import "ios/web/public/web_state/ui/crw_web_view_scroll_view_proxy.h"
#import "ios/web/public/web_state/web_state.h"
#import "ios/web/public/web_state/web_state_observer_bridge.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
NSString* const kFindBarTextFieldWillBecomeFirstResponderNotification =
@"kFindBarTextFieldWillBecomeFirstResponderNotification";
NSString* const kFindBarTextFieldDidResignFirstResponderNotification =
@"kFindBarTextFieldDidResignFirstResponderNotification";
namespace {
// The delay (in secs) after which the find in page string will be pumped again.
const NSTimeInterval kRecurringPumpDelay = .01;
// Keeps find in page search term to be shared between different tabs. Never
// reset, not stored on disk.
static NSString* gSearchTerm;
}
@interface FindInPageController () <DOMAltering, CRWWebStateObserver>
// The web view's scroll view.
- (CRWWebViewScrollViewProxy*)webViewScrollView;
// Find in Page text field listeners.
- (void)findBarTextFieldWillBecomeFirstResponder:(NSNotification*)note;
- (void)findBarTextFieldDidResignFirstResponder:(NSNotification*)note;
// Keyboard listeners.
- (void)keyboardDidShow:(NSNotification*)note;
- (void)keyboardWillHide:(NSNotification*)note;
// Records UKM metric for Find in Page search matches.
- (void)logFindInPageSearchUKM;
// Constantly injects the find string in page until
// |disableFindInPageWithCompletionHandler:| is called or the find operation is
// complete. Calls |completionHandler| if the find operation is complete.
// |completionHandler| can be nil.
- (void)startPumpingWithCompletionHandler:(ProceduralBlock)completionHandler;
// Gives find in page more time to complete. Calls |completionHandler| with
// a BOOL indicating if the find operation was successful. |completionHandler|
// can be nil.
- (void)pumpFindStringInPageWithCompletionHandler:
(void (^)(BOOL))completionHandler;
// Processes the result of a single find in page pump. Calls |completionHandler|
// if pumping is done. Re-pumps if necessary.
- (void)processPumpResult:(BOOL)finished
scrollPoint:(CGPoint)scrollPoint
completionHandler:(ProceduralBlock)completionHandler;
// Prevent scrolling past the end of the page.
- (CGPoint)limitOverscroll:(CRWWebViewScrollViewProxy*)scrollViewProxy
atPoint:(CGPoint)point;
@end
@implementation FindInPageController {
// Object that manages find_in_page.js injection into the web view.
__weak JsFindinpageManager* _findInPageJsManager;
// Access to the web view from the web state.
id<CRWWebViewProxy> _webViewProxy;
// True when a find is in progress. Used to avoid running JavaScript during
// disable when there is nothing to clear.
BOOL _findStringStarted;
// The WebState this instance is observing. Will be null after
// -webStateDestroyed: has been called.
web::WebState* _webState;
// Bridge to observe the web state from Objective-C.
std::unique_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
}
@synthesize findInPageModel = _findInPageModel;
+ (void)setSearchTerm:(NSString*)string {
gSearchTerm = [string copy];
}
+ (NSString*)searchTerm {
return gSearchTerm;
}
- (id)initWithWebState:(web::WebState*)webState {
self = [super init];
if (self) {
DCHECK(webState);
_webState = webState;
_findInPageModel = [[FindInPageModel alloc] init];
_findInPageJsManager = base::mac::ObjCCastStrict<JsFindinpageManager>(
[_webState->GetJSInjectionReceiver()
instanceOfClass:[JsFindinpageManager class]]);
_findInPageJsManager.findInPageModel = _findInPageModel;
_webStateObserverBridge =
std::make_unique<web::WebStateObserverBridge>(self);
_webState->AddObserver(_webStateObserverBridge.get());
_webViewProxy = _webState->GetWebViewProxy();
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(findBarTextFieldWillBecomeFirstResponder:)
name:kFindBarTextFieldWillBecomeFirstResponderNotification
object:nil];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(findBarTextFieldDidResignFirstResponder:)
name:kFindBarTextFieldDidResignFirstResponderNotification
object:nil];
DOMAlteringLock::CreateForWebState(_webState);
}
return self;
}
- (void)dealloc {
if (_webState) {
_webState->RemoveObserver(_webStateObserverBridge.get());
_webStateObserverBridge.reset();
_webState = nullptr;
}
}
- (BOOL)canFindInPage {
return [_webViewProxy hasSearchableTextContent];
}
- (void)initFindInPage {
[_findInPageJsManager inject];
// Initialize the module with our frame size.
CGRect frame = [_webViewProxy bounds];
[_findInPageJsManager setWidth:frame.size.width height:frame.size.height];
}
- (CRWWebViewScrollViewProxy*)webViewScrollView {
return [_webViewProxy scrollViewProxy];
}
- (CGPoint)limitOverscroll:(CRWWebViewScrollViewProxy*)scrollViewProxy
atPoint:(CGPoint)point {
CGFloat contentHeight = scrollViewProxy.contentSize.height;
CGFloat frameHeight = scrollViewProxy.frame.size.height;
CGFloat maxScroll = std::max<CGFloat>(0, contentHeight - frameHeight);
if (point.y > maxScroll) {
point.y = maxScroll;
}
return point;
}
- (void)processPumpResult:(BOOL)finished
scrollPoint:(CGPoint)scrollPoint
completionHandler:(ProceduralBlock)completionHandler {
if (finished) {
scrollPoint = [self limitOverscroll:[_webViewProxy scrollViewProxy]
atPoint:scrollPoint];
[[_webViewProxy scrollViewProxy] setContentOffset:scrollPoint animated:YES];
if (completionHandler)
completionHandler();
} else {
[self performSelector:@selector(startPumpingWithCompletionHandler:)
withObject:completionHandler
afterDelay:kRecurringPumpDelay];
}
}
- (void)logFindInPageSearchUKM {
ukm::SourceId sourceID = ukm::GetSourceIdForWebStateDocument(_webState);
if (sourceID != ukm::kInvalidSourceId) {
ukm::builders::IOS_FindInPageSearchMatches(sourceID)
.SetHasMatches(_findInPageModel.matches > 0)
.Record(ukm::UkmRecorder::Get());
}
}
- (void)findStringInPage:(NSString*)query
completionHandler:(ProceduralBlock)completionHandler {
ProceduralBlockWithBool lockAction = ^(BOOL hasLock) {
if (!hasLock) {
if (completionHandler) {
completionHandler();
}
return;
}
// Cancel any previous pumping.
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self initFindInPage];
// Keep track of whether a find is in progress so to avoid running
// JavaScript during disable if unnecessary.
_findStringStarted = YES;
__weak FindInPageController* weakSelf = self;
[_findInPageJsManager findString:query
completionHandler:^(BOOL finished, CGPoint point) {
FindInPageController* strongSelf = weakSelf;
if (!strongSelf) {
return;
}
[strongSelf logFindInPageSearchUKM];
[strongSelf processPumpResult:finished
scrollPoint:point
completionHandler:completionHandler];
}];
};
DOMAlteringLock::FromWebState(_webState)->Acquire(self, lockAction);
}
- (void)startPumpingWithCompletionHandler:(ProceduralBlock)completionHandler {
__weak FindInPageController* weakSelf = self;
id completionHandlerBlock = ^void(BOOL findFinished) {
if (findFinished) {
// Pumping complete. Nothing else to do.
if (completionHandler)
completionHandler();
return;
}
// Further pumping is required.
[weakSelf performSelector:@selector(startPumpingWithCompletionHandler:)
withObject:completionHandler
afterDelay:kRecurringPumpDelay];
};
[self pumpFindStringInPageWithCompletionHandler:completionHandlerBlock];
}
- (void)pumpFindStringInPageWithCompletionHandler:
(void (^)(BOOL))completionHandler {
__weak FindInPageController* weakSelf = self;
[_findInPageJsManager pumpWithCompletionHandler:^(BOOL finished,
CGPoint point) {
FindInPageController* strongSelf = weakSelf;
if (finished) {
point = [strongSelf limitOverscroll:[strongSelf webViewScrollView]
atPoint:point];
[[strongSelf webViewScrollView] setContentOffset:point animated:YES];
}
completionHandler(finished);
}];
}
- (void)findNextStringInPageWithCompletionHandler:
(ProceduralBlock)completionHandler {
[self initFindInPage];
__weak FindInPageController* weakSelf = self;
[_findInPageJsManager nextMatchWithCompletionHandler:^(CGPoint point) {
FindInPageController* strongSelf = weakSelf;
point = [strongSelf limitOverscroll:[strongSelf webViewScrollView]
atPoint:point];
[[strongSelf webViewScrollView] setContentOffset:point animated:YES];
if (completionHandler)
completionHandler();
}];
}
// Highlight the previous search match, update model and scroll to match.
- (void)findPreviousStringInPageWithCompletionHandler:
(ProceduralBlock)completionHandler {
[self initFindInPage];
__weak FindInPageController* weakSelf = self;
[_findInPageJsManager previousMatchWithCompletionHandler:^(CGPoint point) {
FindInPageController* strongSelf = weakSelf;
point = [strongSelf limitOverscroll:[strongSelf webViewScrollView]
atPoint:point];
[[strongSelf webViewScrollView] setContentOffset:point animated:YES];
if (completionHandler)
completionHandler();
}];
}
// Remove highlights from the page and disable the model.
- (void)disableFindInPageWithCompletionHandler:
(ProceduralBlock)completionHandler {
if (![self canFindInPage]) {
if (completionHandler)
completionHandler();
return;
}
// Cancel any queued calls to |recurringPumpWithCompletionHandler|.
[NSObject cancelPreviousPerformRequestsWithTarget:self];
__weak FindInPageController* weakSelf = self;
ProceduralBlock handler = ^{
FindInPageController* strongSelf = weakSelf;
if (strongSelf && strongSelf->_webState) {
DOMAlteringLock::FromWebState(strongSelf->_webState)->Release(strongSelf);
}
if (completionHandler)
completionHandler();
};
// Only run JSFindInPageManager disable if there is a string in progress to
// avoid WKWebView crash on deallocation due to outstanding completion
// handler.
if (_findStringStarted) {
[_findInPageJsManager disableWithCompletionHandler:handler];
_findStringStarted = NO;
} else {
handler();
}
}
- (void)saveSearchTerm {
[[self class] setSearchTerm:[[self findInPageModel] text]];
}
- (void)restoreSearchTerm {
// Pasteboards always return nil in background:
if ([[UIApplication sharedApplication] applicationState] !=
UIApplicationStateActive) {
return;
}
NSString* term = [[self class] searchTerm];
[[self findInPageModel] updateQuery:(term ? term : @"") matches:0];
}
#pragma mark - Notification listeners
- (void)findBarTextFieldWillBecomeFirstResponder:(NSNotification*)note {
// Listen to the keyboard appearance notifications.
NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter addObserver:self
selector:@selector(keyboardDidShow:)
name:UIKeyboardDidShowNotification
object:nil];
[defaultCenter addObserver:self
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)findBarTextFieldDidResignFirstResponder:(NSNotification*)note {
// Resign from the keyboard appearance notifications on the next turn of the
// runloop.
dispatch_async(dispatch_get_main_queue(), ^{
NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter removeObserver:self
name:UIKeyboardDidShowNotification
object:nil];
[defaultCenter removeObserver:self
name:UIKeyboardWillHideNotification
object:nil];
});
}
- (void)keyboardDidShow:(NSNotification*)note {
NSDictionary* info = [note userInfo];
CGSize kbSize =
[[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
CGFloat kbHeight = kbSize.height;
UIEdgeInsets insets = UIEdgeInsetsZero;
insets.bottom = kbHeight;
[_webViewProxy registerInsets:insets forCaller:self];
}
- (void)keyboardWillHide:(NSNotification*)note {
[_webViewProxy unregisterInsetsForCaller:self];
}
- (void)detachFromWebState {
if (_webState) {
_webState->RemoveObserver(_webStateObserverBridge.get());
_webStateObserverBridge.reset();
_webState = nullptr;
}
}
#pragma mark - CRWWebStateObserver Methods
- (void)webStateDestroyed:(web::WebState*)webState {
DCHECK_EQ(_webState, webState);
[self detachFromWebState];
}
#pragma mark - DOMAltering Methods
- (BOOL)canReleaseDOMLock {
return NO;
}
- (void)releaseDOMLockWithCompletionHandler:(ProceduralBlock)completionHandler {
NOTREACHED();
}
@end