blob: 37988ac553e5bc68869e3151521ab01f4c0c039d [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/content_suggestions/content_suggestions_coordinator.h"
#include "base/mac/foundation_util.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/scoped_observer.h"
#include "components/feed/core/shared_prefs/pref_names.h"
#include "components/ntp_snippets/content_suggestions_service.h"
#include "components/ntp_snippets/pref_names.h"
#include "components/ntp_snippets/remote/remote_suggestions_scheduler.h"
#include "components/ntp_tiles/most_visited_sites.h"
#include "components/prefs/pref_service.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#include "ios/chrome/browser/drag_and_drop/drag_and_drop_flag.h"
#import "ios/chrome/browser/drag_and_drop/url_drag_drop_handler.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/main/browser.h"
#include "ios/chrome/browser/ntp_snippets/ios_chrome_content_suggestions_service_factory.h"
#include "ios/chrome/browser/ntp_tiles/ios_most_visited_sites_factory.h"
#include "ios/chrome/browser/pref_names.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/signin/authentication_service_factory.h"
#include "ios/chrome/browser/signin/identity_manager_factory.h"
#import "ios/chrome/browser/ui/alert_coordinator/action_sheet_coordinator.h"
#import "ios/chrome/browser/ui/commands/application_commands.h"
#import "ios/chrome/browser/ui/commands/browser_commands.h"
#import "ios/chrome/browser/ui/commands/command_dispatcher.h"
#import "ios/chrome/browser/ui/commands/open_new_tab_command.h"
#import "ios/chrome/browser/ui/content_suggestions/cells/content_suggestions_most_visited_item.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_data_sink.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_feature.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_header_synchronizer.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_header_view_controller.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_mediator.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_menu_provider.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_metrics_recorder.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_view_controller.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_view_controller_audience.h"
#import "ios/chrome/browser/ui/content_suggestions/discover_feed_menu_commands.h"
#import "ios/chrome/browser/ui/content_suggestions/ntp_home_constant.h"
#import "ios/chrome/browser/ui/content_suggestions/ntp_home_mediator.h"
#import "ios/chrome/browser/ui/content_suggestions/ntp_home_metrics.h"
#import "ios/chrome/browser/ui/content_suggestions/theme_change_delegate.h"
#import "ios/chrome/browser/ui/menu/action_factory.h"
#import "ios/chrome/browser/ui/menu/menu_histograms.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_header_constants.h"
#import "ios/chrome/browser/ui/ntp/notification_promo_whats_new.h"
#import "ios/chrome/browser/ui/overscroll_actions/overscroll_actions_controller.h"
#import "ios/chrome/browser/ui/settings/utils/pref_backed_boolean.h"
#import "ios/chrome/browser/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/url_loading/url_loading_browser_agent.h"
#import "ios/chrome/browser/url_loading/url_loading_params.h"
#import "ios/chrome/browser/voice/voice_search_availability.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/public/provider/chrome/browser/chrome_browser_provider.h"
#import "ios/public/provider/chrome/browser/discover_feed/discover_feed_provider.h"
#include "ui/base/l10n/l10n_util_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@interface ContentSuggestionsCoordinator () <
ContentSuggestionsMenuProvider,
ContentSuggestionsViewControllerAudience,
DiscoverFeedMenuCommands,
OverscrollActionsControllerDelegate,
ThemeChangeDelegate,
URLDropDelegate> {
// Helper object managing the availability of the voice search feature.
VoiceSearchAvailability _voiceSearchAvailability;
}
@property(nonatomic, strong)
ContentSuggestionsViewController* suggestionsViewController;
@property(nonatomic, strong)
ContentSuggestionsMediator* contentSuggestionsMediator;
@property(nonatomic, strong)
ContentSuggestionsHeaderSynchronizer* headerCollectionInteractionHandler;
@property(nonatomic, strong) ContentSuggestionsMetricsRecorder* metricsRecorder;
@property(nonatomic, strong) NTPHomeMediator* NTPMediator;
@property(nonatomic, strong) UIViewController* discoverFeedViewController;
@property(nonatomic, strong) URLDragDropHandler* dragDropHandler;
@property(nonatomic, strong) ActionSheetCoordinator* alertCoordinator;
// Redefined as readwrite.
@property(nonatomic, strong, readwrite)
ContentSuggestionsHeaderViewController* headerController;
@property(nonatomic, strong) PrefBackedBoolean* contentSuggestionsVisible;
@end
@implementation ContentSuggestionsCoordinator
- (void)start {
DCHECK(self.browser);
if (self.visible) {
// Prevent this coordinator from being started twice in a row
return;
}
_visible = YES;
ntp_snippets::ContentSuggestionsService* contentSuggestionsService =
IOSChromeContentSuggestionsServiceFactory::GetForBrowserState(
self.browser->GetBrowserState());
contentSuggestionsService->remote_suggestions_scheduler()
->OnSuggestionsSurfaceOpened();
contentSuggestionsService->user_classifier()->OnEvent(
ntp_snippets::UserClassifier::Metric::NTP_OPENED);
contentSuggestionsService->user_classifier()->OnEvent(
ntp_snippets::UserClassifier::Metric::SUGGESTIONS_SHOWN);
PrefService* prefs =
ChromeBrowserState::FromBrowserState(self.browser->GetBrowserState())
->GetPrefs();
bool contentSuggestionsEnabled =
prefs->GetBoolean(prefs::kArticlesForYouEnabled);
self.contentSuggestionsVisible = [[PrefBackedBoolean alloc]
initWithPrefService:prefs
prefName:feed::prefs::kArticlesListVisible];
if (contentSuggestionsEnabled) {
if ([self.contentSuggestionsVisible value]) {
ntp_home::RecordNTPImpression(ntp_home::REMOTE_SUGGESTIONS);
} else {
ntp_home::RecordNTPImpression(ntp_home::REMOTE_COLLAPSED);
}
} else {
ntp_home::RecordNTPImpression(ntp_home::LOCAL_SUGGESTIONS);
}
self.NTPMediator = [[NTPHomeMediator alloc]
initWithWebState:self.webState
templateURLService:ios::TemplateURLServiceFactory::
GetForBrowserState(
self.browser->GetBrowserState())
URLLoader:UrlLoadingBrowserAgent::FromBrowser(self.browser)
authService:AuthenticationServiceFactory::GetForBrowserState(
self.browser->GetBrowserState())
identityManager:IdentityManagerFactory::GetForBrowserState(
self.browser->GetBrowserState())
logoVendor:ios::GetChromeBrowserProvider()->CreateLogoVendor(
self.browser, self.webState)
voiceSearchAvailability:&_voiceSearchAvailability];
self.NTPMediator.browser = self.browser;
self.headerController = [[ContentSuggestionsHeaderViewController alloc] init];
// TODO(crbug.com/1045047): Use HandlerForProtocol after commands protocol
// clean up.
self.headerController.dispatcher =
static_cast<id<ApplicationCommands, BrowserCommands, OmniboxCommands,
FakeboxFocuser>>(self.browser->GetCommandDispatcher());
self.headerController.commandHandler = self.NTPMediator;
self.headerController.delegate = self.NTPMediator;
self.headerController.readingListModel =
ReadingListModelFactory::GetForBrowserState(
self.browser->GetBrowserState());
self.headerController.toolbarDelegate = self.toolbarDelegate;
favicon::LargeIconService* largeIconService =
IOSChromeLargeIconServiceFactory::GetForBrowserState(
self.browser->GetBrowserState());
LargeIconCache* cache = IOSChromeLargeIconCacheFactory::GetForBrowserState(
self.browser->GetBrowserState());
std::unique_ptr<ntp_tiles::MostVisitedSites> mostVisitedFactory =
IOSMostVisitedSitesFactory::NewForBrowserState(
self.browser->GetBrowserState());
ReadingListModel* readingListModel =
ReadingListModelFactory::GetForBrowserState(
self.browser->GetBrowserState());
self.discoverFeedViewController = ios::GetChromeBrowserProvider()
->GetDiscoverFeedProvider()
->NewFeedViewController(self.browser);
// TODO(crbug.com/1085419): Once the CollectionView is cleanly exposed, remove
// this loop.
for (UIView* view in self.discoverFeedViewController.view.subviews) {
if ([view isKindOfClass:[UICollectionView class]]) {
UICollectionView* feedView = static_cast<UICollectionView*>(view);
feedView.bounces = false;
feedView.alwaysBounceVertical = false;
}
}
self.contentSuggestionsMediator = [[ContentSuggestionsMediator alloc]
initWithContentService:contentSuggestionsService
largeIconService:largeIconService
largeIconCache:cache
mostVisitedSite:std::move(mostVisitedFactory)
readingListModel:readingListModel
prefService:prefs
discoverFeed:self.discoverFeedViewController];
self.contentSuggestionsMediator.commandHandler = self.NTPMediator;
self.contentSuggestionsMediator.headerProvider = self.headerController;
self.contentSuggestionsMediator.contentArticlesExpanded =
self.contentSuggestionsVisible;
self.headerController.promoCanShow =
[self.contentSuggestionsMediator notificationPromo]->CanShow();
self.metricsRecorder = [[ContentSuggestionsMetricsRecorder alloc] init];
self.metricsRecorder.delegate = self.contentSuggestionsMediator;
self.suggestionsViewController = [[ContentSuggestionsViewController alloc]
initWithStyle:CollectionViewControllerStyleDefault];
[self.suggestionsViewController
setDataSource:self.contentSuggestionsMediator];
self.suggestionsViewController.suggestionCommandHandler = self.NTPMediator;
self.suggestionsViewController.audience = self;
self.suggestionsViewController.overscrollDelegate = self;
self.suggestionsViewController.themeChangeDelegate = self;
self.suggestionsViewController.metricsRecorder = self.metricsRecorder;
id<SnackbarCommands> dispatcher = HandlerForProtocol(
self.browser->GetCommandDispatcher(), SnackbarCommands);
self.suggestionsViewController.dispatcher = dispatcher;
self.suggestionsViewController.discoverFeedMenuHandler = self;
if (@available(iOS 13.0, *)) {
self.suggestionsViewController.menuProvider = self;
}
self.NTPMediator.consumer = self.headerController;
// TODO(crbug.com/1045047): Use HandlerForProtocol after commands protocol
// clean up.
self.NTPMediator.dispatcher =
static_cast<id<ApplicationCommands, BrowserCommands, OmniboxCommands,
SnackbarCommands>>(self.browser->GetCommandDispatcher());
self.NTPMediator.NTPMetrics = [[NTPHomeMetrics alloc]
initWithBrowserState:self.browser->GetBrowserState()
webState:self.webState];
self.NTPMediator.metricsRecorder = self.metricsRecorder;
self.NTPMediator.suggestionsViewController = self.suggestionsViewController;
self.NTPMediator.suggestionsMediator = self.contentSuggestionsMediator;
self.NTPMediator.suggestionsService = contentSuggestionsService;
[self.NTPMediator setUp];
[self.suggestionsViewController addChildViewController:self.headerController];
[self.headerController
didMoveToParentViewController:self.suggestionsViewController];
self.headerCollectionInteractionHandler =
[[ContentSuggestionsHeaderSynchronizer alloc]
initWithCollectionController:self.suggestionsViewController
headerController:self.headerController];
self.NTPMediator.headerCollectionInteractionHandler =
self.headerCollectionInteractionHandler;
if (DragAndDropIsEnabled()) {
self.dragDropHandler = [[URLDragDropHandler alloc] init];
self.dragDropHandler.dropDelegate = self;
[self.suggestionsViewController.collectionView
addInteraction:[[UIDropInteraction alloc]
initWithDelegate:self.dragDropHandler]];
}
}
- (void)stop {
[self.NTPMediator shutdown];
self.NTPMediator = nil;
[self.contentSuggestionsMediator disconnect];
self.contentSuggestionsMediator = nil;
self.headerController = nil;
_visible = NO;
}
- (UIViewController*)viewController {
return self.suggestionsViewController;
}
#pragma mark - ContentSuggestionsViewControllerAudience
- (void)promoShown {
NotificationPromoWhatsNew* notificationPromo =
[self.contentSuggestionsMediator notificationPromo];
notificationPromo->HandleViewed();
[self.headerController setPromoCanShow:notificationPromo->CanShow()];
}
#pragma mark - OverscrollActionsControllerDelegate
- (void)overscrollActionsController:(OverscrollActionsController*)controller
didTriggerAction:(OverscrollAction)action {
// TODO(crbug.com/1045047): Use HandlerForProtocol after commands protocol
// clean up.
id<ApplicationCommands, BrowserCommands, OmniboxCommands, SnackbarCommands>
handler = static_cast<id<ApplicationCommands, BrowserCommands,
OmniboxCommands, SnackbarCommands>>(
self.browser->GetCommandDispatcher());
switch (action) {
case OverscrollAction::NEW_TAB: {
[handler openURLInNewTab:[OpenNewTabCommand command]];
} break;
case OverscrollAction::CLOSE_TAB: {
[handler closeCurrentTab];
base::RecordAction(base::UserMetricsAction("OverscrollActionCloseTab"));
} break;
case OverscrollAction::REFRESH:
[self reload];
break;
case OverscrollAction::NONE:
NOTREACHED();
break;
}
}
- (BOOL)shouldAllowOverscrollActionsForOverscrollActionsController:
(OverscrollActionsController*)controller {
return YES;
}
- (UIView*)toolbarSnapshotViewForOverscrollActionsController:
(OverscrollActionsController*)controller {
return
[[self.headerController toolBarView] snapshotViewAfterScreenUpdates:NO];
}
- (UIView*)headerViewForOverscrollActionsController:
(OverscrollActionsController*)controller {
return self.suggestionsViewController.view;
}
- (CGFloat)headerInsetForOverscrollActionsController:
(OverscrollActionsController*)controller {
return 0;
}
- (CGFloat)headerHeightForOverscrollActionsController:
(OverscrollActionsController*)controller {
CGFloat height = [self.headerController toolBarView].bounds.size.height;
CGFloat topInset = self.suggestionsViewController.view.safeAreaInsets.top;
return height + topInset;
}
- (FullscreenController*)fullscreenControllerForOverscrollActionsController:
(OverscrollActionsController*)controller {
// Fullscreen isn't supported here.
return nullptr;
}
#pragma mark - ThemeChangeDelegate
- (void)handleThemeChange {
if (IsDiscoverFeedEnabled()) {
ios::GetChromeBrowserProvider()->GetDiscoverFeedProvider()->UpdateTheme();
}
}
#pragma mark - URLDropDelegate
- (BOOL)canHandleURLDropInView:(UIView*)view {
return YES;
}
- (void)view:(UIView*)view didDropURL:(const GURL&)URL atPoint:(CGPoint)point {
UrlLoadingBrowserAgent::FromBrowser(self.browser)
->Load(UrlLoadParams::InCurrentTab(URL));
}
#pragma mark - DiscoverFeedMenuCommands
- (void)openDiscoverFeedMenu:(UIView*)menuButton {
[self.alertCoordinator stop];
self.alertCoordinator = nil;
self.alertCoordinator = [[ActionSheetCoordinator alloc]
initWithBaseViewController:self.suggestionsViewController
browser:self.browser
title:nil
message:nil
rect:menuButton.frame
view:menuButton.superview];
__weak ContentSuggestionsCoordinator* weakSelf = self;
if ([self.contentSuggestionsVisible value]) {
[self.alertCoordinator
addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_DISCOVER_FEED_MENU_TURN_OFF_ITEM)
action:^{
[weakSelf.contentSuggestionsVisible setValue:NO];
[weakSelf.contentSuggestionsMediator reloadAllData];
}
style:UIAlertActionStyleDestructive];
} else {
[self.alertCoordinator
addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_DISCOVER_FEED_MENU_TURN_ON_ITEM)
action:^{
[weakSelf.contentSuggestionsVisible setValue:YES];
[weakSelf.contentSuggestionsMediator reloadAllData];
}
style:UIAlertActionStyleDefault];
}
[self.alertCoordinator
addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_DISCOVER_FEED_MENU_MANAGE_INTERESTS_ITEM)
action:^{
[weakSelf.NTPMediator handleManageInterestsTapped];
}
style:UIAlertActionStyleDefault];
[self.alertCoordinator
addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_DISCOVER_FEED_MENU_LEARN_MORE_ITEM)
action:^{
[weakSelf.NTPMediator handleLearnMoreTapped];
}
style:UIAlertActionStyleDefault];
[self.alertCoordinator start];
}
#pragma mark - Public methods
- (UIView*)view {
return self.suggestionsViewController.view;
}
- (void)dismissModals {
[self.NTPMediator dismissModals];
}
- (UIEdgeInsets)contentInset {
return self.suggestionsViewController.collectionView.contentInset;
}
- (CGPoint)contentOffset {
CGPoint collectionOffset =
self.suggestionsViewController.collectionView.contentOffset;
collectionOffset.y -=
self.headerCollectionInteractionHandler.collectionShiftingOffset;
return collectionOffset;
}
- (void)willUpdateSnapshot {
[self.suggestionsViewController clearOverscroll];
}
- (void)reload {
[self.contentSuggestionsMediator.dataSink reloadAllData];
}
- (void)locationBarDidBecomeFirstResponder {
[self.NTPMediator locationBarDidBecomeFirstResponder];
}
- (void)locationBarDidResignFirstResponder {
[self.NTPMediator locationBarDidResignFirstResponder];
}
#pragma mark - ThemeChangeDelegate
- (UIContextMenuConfiguration*)contextMenuConfigurationForItem:
(ContentSuggestionsMostVisitedItem*)item API_AVAILABLE(ios(13.0)) {
return [UIContextMenuConfiguration
configurationWithIdentifier:nil
previewProvider:nil
actionProvider:^(NSArray<UIMenuElement*>* suggestedActions) {
// Record that this context menu was shown to the user.
RecordMenuShown(MenuScenario::kContentSuggestionsEntry);
ActionFactory* actionFactory = [[ActionFactory alloc]
initWithScenario:MenuScenario::
kContentSuggestionsEntry];
UIAction* copyAction =
[actionFactory actionToCopyURL:item.URL];
return [UIMenu menuWithTitle:@"" children:@[ copyAction ]];
}];
}
@end