blob: 487c2232ef43ddf1f8352616ad5c1639f4fc0a07 [file] [log] [blame]
// Copyright 2017 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/ntp/google_landing_mediator.h"
#include "base/mac/bind_objc_block.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/cancelable_task_tracker.h"
#include "components/favicon/core/large_icon_service.h"
#include "components/favicon_base/fallback_icon_style.h"
#include "components/ntp_tiles/metrics.h"
#include "components/ntp_tiles/most_visited_sites.h"
#include "components/ntp_tiles/ntp_tile.h"
#include "components/rappor/rappor_service_impl.h"
#include "components/search_engines/template_url_service.h"
#include "components/search_engines/template_url_service_observer.h"
#include "ios/chrome/browser/application_context.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#include "ios/chrome/browser/favicon/ios_chrome_large_icon_cache_factory.h"
#include "ios/chrome/browser/favicon/ios_chrome_large_icon_service_factory.h"
#include "ios/chrome/browser/favicon/large_icon_cache.h"
#import "ios/chrome/browser/metrics/new_tab_page_uma.h"
#include "ios/chrome/browser/ntp_tiles/ios_most_visited_sites_factory.h"
#import "ios/chrome/browser/ntp_tiles/most_visited_sites_observer_bridge.h"
#include "ios/chrome/browser/reading_list/reading_list_model_factory.h"
#include "ios/chrome/browser/search_engines/template_url_service_factory.h"
#import "ios/chrome/browser/ui/browser_view_controller.h"
#import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h"
#import "ios/chrome/browser/ui/commands/generic_chrome_command.h"
#import "ios/chrome/browser/ui/ntp/google_landing_consumer.h"
#import "ios/chrome/browser/ui/ntp/notification_promo_whats_new.h"
#import "ios/chrome/browser/ui/toolbar/web_toolbar_controller.h"
#import "ios/chrome/browser/ui/url_loader.h"
#import "ios/chrome/browser/web_state_list/web_state_list.h"
#import "ios/chrome/browser/web_state_list/web_state_list_observer_bridge.h"
#include "ios/public/provider/chrome/browser/chrome_browser_provider.h"
#include "ios/public/provider/chrome/browser/voice/voice_search_provider.h"
#import "ios/shared/chrome/browser/ui/commands/command_dispatcher.h"
#include "ios/web/public/web_state/web_state.h"
#include "skia/ext/skia_utils_ios.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using base::UserMetricsAction;
namespace {
const CGFloat kFaviconMinSize = 32;
const NSInteger kMaxNumMostVisitedFavicons = 8;
} // namespace
@interface GoogleLandingMediator (UsedBySearchEngineObserver)
// Check to see if the logo visibility should change.
- (void)updateShowLogo;
@end
namespace google_landing {
// Observer used to hide the Google logo and doodle if the TemplateURLService
// changes.
class SearchEngineObserver : public TemplateURLServiceObserver {
public:
SearchEngineObserver(GoogleLandingMediator* owner,
TemplateURLService* urlService);
~SearchEngineObserver() override;
void OnTemplateURLServiceChanged() override;
private:
__weak GoogleLandingMediator* _owner;
TemplateURLService* _templateURLService; // weak
};
SearchEngineObserver::SearchEngineObserver(GoogleLandingMediator* owner,
TemplateURLService* urlService)
: _owner(owner), _templateURLService(urlService) {
_templateURLService->AddObserver(this);
}
SearchEngineObserver::~SearchEngineObserver() {
_templateURLService->RemoveObserver(this);
}
void SearchEngineObserver::OnTemplateURLServiceChanged() {
[_owner updateShowLogo];
}
} // namespace google_landing
@interface GoogleLandingMediator ()<GoogleLandingDataSource,
MostVisitedSitesObserving,
WebStateListObserving> {
// The ChromeBrowserState associated with this mediator.
ios::ChromeBrowserState* _browserState; // Weak.
// |YES| if impressions were logged already and shouldn't be logged again.
BOOL _recordedPageImpression;
// Controller to fetch and show doodles or a default Google logo.
id<LogoVendor> _doodleController;
// Listen for default search engine changes.
std::unique_ptr<google_landing::SearchEngineObserver> _observer;
TemplateURLService* _templateURLService; // weak
// A MostVisitedSites::Observer bridge object to get notified of most visited
// sites changes.
std::unique_ptr<ntp_tiles::MostVisitedSitesObserverBridge>
_mostVisitedObserverBridge;
std::unique_ptr<ntp_tiles::MostVisitedSites> _mostVisitedSites;
// Most visited data from the MostVisitedSites service currently in use.
ntp_tiles::NTPTilesVector _mostVisitedData;
// Most visited data from the MostVisitedSites service (copied upon receiving
// the callback), not yet used by the collection. It will be used after a user
// interaction.
ntp_tiles::NTPTilesVector _freshMostVisitedData;
// Most visited data used for logging the tiles impression. The data are
// copied when receiving the first non-empty data. This copy is used to make
// sure only the data received the first time are logged, and only once.
ntp_tiles::NTPTilesVector _mostVisitedDataForLogging;
// Observes the WebStateList so that this mediator can update the UI when the
// active WebState changes.
std::unique_ptr<WebStateListObserverBridge> _webStateListObserver;
// What's new promo.
std::unique_ptr<NotificationPromoWhatsNew> _notificationPromo;
// Used to cancel tasks for the LargeIconService.
base::CancelableTaskTracker _cancelable_task_tracker;
}
// Consumer to handle google landing update notifications.
@property(nonatomic, weak) id<GoogleLandingConsumer> consumer;
// The WebStateList that is being observed by this mediator.
@property(nonatomic, assign, readonly) WebStateList* webStateList;
// The dispatcher for this mediator.
@property(nonatomic, weak) id<ChromeExecuteCommand, UrlLoader> dispatcher;
// Perform initial setup.
- (void)setUp;
@end
@implementation GoogleLandingMediator
@synthesize webStateList = _webStateList;
@synthesize consumer = _consumer;
@synthesize dispatcher = _dispatcher;
- (instancetype)initWithConsumer:(id<GoogleLandingConsumer>)consumer
browserState:(ios::ChromeBrowserState*)browserState
dispatcher:(id<ChromeExecuteCommand, UrlLoader>)dispatcher
webStateList:(WebStateList*)webStateList {
self = [super init];
if (self) {
_consumer = consumer;
_browserState = browserState;
_dispatcher = dispatcher;
_webStateList = webStateList;
_webStateListObserver = base::MakeUnique<WebStateListObserverBridge>(self);
_webStateList->AddObserver(_webStateListObserver.get());
[self setUp];
}
return self;
}
- (void)shutdown {
_webStateList->RemoveObserver(_webStateListObserver.get());
[[NSNotificationCenter defaultCenter] removeObserver:self.consumer];
_observer.reset();
}
- (void)setUp {
[self.consumer setVoiceSearchIsEnabled:ios::GetChromeBrowserProvider()
->GetVoiceSearchProvider()
->IsVoiceSearchEnabled()];
[self.consumer
setMaximumMostVisitedSitesShown:[GoogleLandingMediator maxSitesShown]];
[self.consumer setTabCount:self.webStateList->count()];
web::WebState* webState = _webStateList->GetActiveWebState();
if (webState) {
web::NavigationManager* nav = webState->GetNavigationManager();
[self.consumer setCanGoForward:nav->CanGoForward()];
[self.consumer setCanGoBack:nav->CanGoBack()];
}
// Set up template URL service to listen for default search engine changes.
_templateURLService =
ios::TemplateURLServiceFactory::GetForBrowserState(_browserState);
_observer.reset(
new google_landing::SearchEngineObserver(self, _templateURLService));
_templateURLService->Load();
_doodleController = ios::GetChromeBrowserProvider()->CreateLogoVendor(
_browserState, self.dispatcher);
[_consumer setLogoVendor:_doodleController];
[self updateShowLogo];
// Set up most visited sites. This call may have the side effect of
// triggering -onMostVisitedURLsAvailable immediately, which can load the
// view before dataSource is set.
_mostVisitedSites =
IOSMostVisitedSitesFactory::NewForBrowserState(_browserState);
_mostVisitedObserverBridge.reset(
new ntp_tiles::MostVisitedSitesObserverBridge(self));
_mostVisitedSites->SetMostVisitedURLsObserver(
_mostVisitedObserverBridge.get(), [GoogleLandingMediator maxSitesShown]);
// Set up notifications;
NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter
addObserver:self.consumer
selector:@selector(locationBarBecomesFirstResponder)
name:ios_internal::kLocationBarBecomesFirstResponderNotification
object:nil];
[defaultCenter
addObserver:self.consumer
selector:@selector(locationBarResignsFirstResponder)
name:ios_internal::kLocationBarResignsFirstResponderNotification
object:nil];
// Set up what's new.
_notificationPromo.reset(
new NotificationPromoWhatsNew(GetApplicationContext()->GetLocalState()));
_notificationPromo->Init();
[self.consumer setPromoText:[base::SysUTF8ToNSString(
_notificationPromo->promo_text()) copy]];
[self.consumer setPromoIcon:_notificationPromo->icon()];
[self.consumer setPromoCanShow:_notificationPromo->CanShow()];
}
- (void)updateShowLogo {
BOOL showLogo = NO;
const TemplateURL* defaultURL =
_templateURLService->GetDefaultSearchProvider();
if (defaultURL) {
showLogo =
defaultURL->GetEngineType(_templateURLService->search_terms_data()) ==
SEARCH_ENGINE_GOOGLE;
}
[self.consumer setLogoIsShowing:showLogo];
}
+ (NSUInteger)maxSitesShown {
return kMaxNumMostVisitedFavicons;
}
#pragma mark - MostVisitedSitesObserving
- (void)onMostVisitedURLsAvailable:(const ntp_tiles::NTPTilesVector&)data {
if (_mostVisitedData.size() > 0) {
// If some content is already displayed to the user, do not update it to
// prevent updating the all the tiles without any action from the user.
_freshMostVisitedData = data;
return;
}
_mostVisitedData = data;
[self.consumer mostVisitedDataUpdated];
if (data.size() && !_recordedPageImpression) {
_recordedPageImpression = YES;
_mostVisitedDataForLogging = data;
ntp_tiles::metrics::RecordPageImpression(data.size());
}
}
- (void)onIconMadeAvailable:(const GURL&)siteUrl {
for (size_t i = 0; i < _mostVisitedData.size(); ++i) {
const ntp_tiles::NTPTile& ntpTile = _mostVisitedData[i];
if (ntpTile.url == siteUrl) {
[self.consumer mostVisitedIconMadeAvailableAtIndex:i];
break;
}
}
}
- (void)getFaviconForURL:(GURL)URL
size:(CGFloat)size
useCache:(BOOL)useCache
imageCallback:(void (^)(UIImage* favicon))imageCallback
fallbackCallback:(void (^)(UIColor* textColor,
UIColor* backgroundColor,
BOOL isDefaultColor))fallbackCallback {
__weak GoogleLandingMediator* weakSelf = self;
void (^faviconBlock)(const favicon_base::LargeIconResult&) = ^(
const favicon_base::LargeIconResult& result) {
ntp_tiles::TileVisualType tileType;
if (result.bitmap.is_valid()) {
scoped_refptr<base::RefCountedMemory> data =
result.bitmap.bitmap_data.get();
UIImage* favicon = [UIImage
imageWithData:[NSData dataWithBytes:data->front() length:data->size()]
scale:[UIScreen mainScreen].scale];
imageCallback(favicon);
tileType = ntp_tiles::TileVisualType::ICON_REAL;
} else if (result.fallback_icon_style) {
UIColor* backgroundColor = skia::UIColorFromSkColor(
result.fallback_icon_style->background_color);
UIColor* textColor =
skia::UIColorFromSkColor(result.fallback_icon_style->text_color);
BOOL isDefaultColor =
result.fallback_icon_style->is_default_background_color;
fallbackCallback(textColor, backgroundColor, isDefaultColor);
fallbackCallback(backgroundColor, textColor, isDefaultColor);
tileType = isDefaultColor ? ntp_tiles::TileVisualType::ICON_DEFAULT
: ntp_tiles::TileVisualType::ICON_COLOR;
}
GoogleLandingMediator* strongSelf = weakSelf;
if (strongSelf &&
(result.bitmap.is_valid() || result.fallback_icon_style)) {
[strongSelf largeIconCache]->SetCachedResult(URL, result);
}
};
if (useCache) {
std::unique_ptr<favicon_base::LargeIconResult> cached_result =
[self largeIconCache]->GetCachedResult(URL);
if (cached_result) {
faviconBlock(*cached_result);
}
}
CGFloat faviconSize = [UIScreen mainScreen].scale * size;
CGFloat faviconMinSize = [UIScreen mainScreen].scale * kFaviconMinSize;
[self largeIconService]->GetLargeIconOrFallbackStyle(
URL, faviconMinSize, faviconSize, base::BindBlockArc(faviconBlock),
&_cancelable_task_tracker);
}
#pragma mark - WebStateListObserving
- (void)webStateList:(WebStateList*)webStateList
didInsertWebState:(web::WebState*)webState
atIndex:(int)index {
[self.consumer setTabCount:self.webStateList->count()];
}
- (void)webStateList:(WebStateList*)webStateList
didDetachWebState:(web::WebState*)webState
atIndex:(int)atIndex {
[self.consumer setTabCount:self.webStateList->count()];
}
// If the actual webState associated with this mediator were passed in, this
// would not be necessary. However, since the active webstate can change when
// the new tab page is created (and animated in), listen for changes here and
// always display what's active.
- (void)webStateList:(WebStateList*)webStateList
didChangeActiveWebState:(web::WebState*)newWebState
oldWebState:(web::WebState*)oldWebState
atIndex:(int)atIndex
userAction:(BOOL)userAction {
if (newWebState) {
web::NavigationManager* nav = newWebState->GetNavigationManager();
[self.consumer setCanGoForward:nav->CanGoForward()];
[self.consumer setCanGoBack:nav->CanGoBack()];
}
}
#pragma mark - GoogleLandingDataSource
- (void)addBlacklistedURL:(const GURL&)url {
_mostVisitedSites->AddOrRemoveBlacklistedUrl(url, true);
[self useFreshData];
}
- (void)removeBlacklistedURL:(const GURL&)url {
_mostVisitedSites->AddOrRemoveBlacklistedUrl(url, false);
[self useFreshData];
}
- (ntp_tiles::NTPTile)mostVisitedAtIndex:(NSUInteger)index {
return _mostVisitedData[index];
}
- (NSUInteger)mostVisitedSize {
return _mostVisitedData.size();
}
- (void)logMostVisitedClick:(const NSUInteger)visitedIndex
tileType:(ntp_tiles::TileVisualType)tileType {
new_tab_page_uma::RecordAction(
_browserState, new_tab_page_uma::ACTION_OPENED_MOST_VISITED_ENTRY);
base::RecordAction(UserMetricsAction("MobileNTPMostVisited"));
const ntp_tiles::NTPTile& tile = _mostVisitedData[visitedIndex];
ntp_tiles::metrics::RecordTileClick(visitedIndex, tile.source, tileType);
}
- (ReadingListModel*)readingListModel {
return ReadingListModelFactory::GetForBrowserState(_browserState);
}
- (LargeIconCache*)largeIconCache {
return IOSChromeLargeIconCacheFactory::GetForBrowserState(_browserState);
}
- (favicon::LargeIconService*)largeIconService {
return IOSChromeLargeIconServiceFactory::GetForBrowserState(_browserState);
}
- (void)promoViewed {
DCHECK(_notificationPromo);
_notificationPromo->HandleViewed();
[self.consumer setPromoCanShow:_notificationPromo->CanShow()];
}
- (void)promoTapped {
DCHECK(_notificationPromo);
_notificationPromo->HandleClosed();
[self.consumer setPromoCanShow:_notificationPromo->CanShow()];
if (_notificationPromo->IsURLPromo()) {
[self.dispatcher webPageOrderedOpen:_notificationPromo->url()
referrer:web::Referrer()
inBackground:NO
appendTo:kCurrentTab];
return;
}
if (_notificationPromo->IsChromeCommand()) {
GenericChromeCommand* command = [[GenericChromeCommand alloc]
initWithTag:_notificationPromo->command_id()];
[self.dispatcher chromeExecuteCommand:command];
return;
}
NOTREACHED();
}
#pragma mark - Private
// If there is some fresh most visited tiles, they become the current tiles and
// the consumer gets notified.
- (void)useFreshData {
_mostVisitedData = _freshMostVisitedData;
[self.consumer mostVisitedDataUpdated];
}
// If it is the first time we see the favicon corresponding to |URL|, we log the
// |tileType| impression.
- (void)faviconOfType:(ntp_tiles::TileVisualType)tileType
fetchedForURL:(const GURL&)URL {
for (size_t i = 0; i < _mostVisitedDataForLogging.size(); ++i) {
ntp_tiles::NTPTile& ntpTile = _mostVisitedDataForLogging[i];
if (ntpTile.url == URL) {
ntp_tiles::metrics::RecordTileImpression(
i, ntpTile.source, tileType, URL,
GetApplicationContext()->GetRapporServiceImpl());
// Reset the URL to be sure to log the impression only once.
ntpTile.url = GURL();
break;
}
}
}
@end