chromium/ios/chrome/browser/ntp/ui_bundled/new_tab_page_mediator.mm

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_mediator.h"

#import <memory>

#import "base/apple/foundation_util.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "components/prefs/ios/pref_observer_bridge.h"
#import "components/prefs/pref_change_registrar.h"
#import "components/search/search.h"
#import "components/signin/public/identity_manager/objc/identity_manager_observer_bridge.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/discover_feed/model/discover_feed_service.h"
#import "ios/chrome/browser/discover_feed/model/discover_feed_service_factory.h"
#import "ios/chrome/browser/metrics/model/new_tab_page_uma.h"
#import "ios/chrome/browser/ntp/model/new_tab_page_state.h"
#import "ios/chrome/browser/ntp/model/new_tab_page_tab_helper.h"
#import "ios/chrome/browser/ntp/model/new_tab_page_util.h"
#import "ios/chrome/browser/policy/model/policy_util.h"
#import "ios/chrome/browser/search_engines/model/search_engine_observer_bridge.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_observer_bridge.h"
#import "ios/chrome/browser/sync/model/sync_observer_bridge.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_mediator.h"
#import "ios/chrome/browser/ui/content_suggestions/user_account_image_update_delegate.h"
#import "ios/chrome/browser/ntp/ui_bundled/feed_control_delegate.h"
#import "ios/chrome/browser/ntp/ui_bundled/feed_wrapper_view_controller.h"
#import "ios/chrome/browser/ntp/shared/metrics/feed_metrics_constants.h"
#import "ios/chrome/browser/ntp/shared/metrics/feed_metrics_recorder.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_consumer.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_content_delegate.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_feature.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_header_constants.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_header_consumer.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_view_controller.h"
#import "ios/chrome/browser/url_loading/model/url_loading_browser_agent.h"
#import "ios/chrome/browser/url_loading/model/url_loading_params.h"
#import "ios/chrome/common/ui/favicon/favicon_attributes.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/public/provider/chrome/browser/voice_search/voice_search_api.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/navigation/referrer.h"
#import "ios/web/public/web_state.h"
#import "ui/base/l10n/l10n_util.h"
#import "url/gurl.h"

namespace {
// URL for 'Manage Activity' item in the Discover feed menu.
const char kFeedManageActivityURL[] =
    "https://myactivity.google.com/myactivity?product=50";
// URL for 'Manage Interests' item in the Discover feed menu.
const char kFeedManageInterestsURL[] =
    "https://google.com/preferences/interests/yourinterests";
// URL for 'Manage Hidden' item in the Discover feed menu.
const char kFeedManageHiddenURL[] =
    "https://google.com/preferences/interests/hidden";
// URL for 'Learn More' item in the Discover feed menu;
const char kFeedLearnMoreURL[] = "https://support.google.com/chrome/"
                                 "?p=new_tab&co=GENIE.Platform%3DiOS&oco=1";
}  // namespace

@interface NewTabPageMediator () <ChromeAccountManagerServiceObserver,
                                  IdentityManagerObserverBridgeDelegate,
                                  PrefObserverDelegate,
                                  SearchEngineObserving,
                                  SyncObserverModelBridge>

@property(nonatomic, assign) ChromeAccountManagerService* accountManagerService;
// TemplateURL used to get the search engine.
@property(nonatomic, assign) TemplateURLService* templateURLService;
// Authentication Service to get the current user's avatar.
@property(nonatomic, assign) AuthenticationService* authService;
// This is the object that knows how to update the Identity Disc UI.
@property(nonatomic, weak) id<UserAccountImageUpdateDelegate> imageUpdater;
// Yes if the browser is currently in incognito mode.
@property(nonatomic, assign) BOOL isIncognito;
// DiscoverFeed Service to display the Feed.
@property(nonatomic, assign) DiscoverFeedService* discoverFeedService;

@end

@implementation NewTabPageMediator {
  std::unique_ptr<ChromeAccountManagerServiceObserverBridge>
      _accountManagerServiceObserver;
  // Listen for default search engine changes.
  std::unique_ptr<SearchEngineObserverBridge> _searchEngineObserver;
  // Observes changes in identity and updates the Identity Disc.
  std::unique_ptr<signin::IdentityManagerObserverBridge>
      _identityObserverBridge;
  // Used to load URLs.
  raw_ptr<UrlLoadingBrowserAgent> _URLLoader;
  raw_ptr<PrefService> _prefService;
  BOOL _isSafeMode;
  // Pref observer to track changes to prefs.
  std::unique_ptr<PrefObserverBridge> _prefObserverBridge;
  // Registrar for pref changes notifications.
  std::unique_ptr<PrefChangeRegistrar> _prefChangeRegistrar;
  // The current default search engine.
  raw_ptr<const TemplateURL> _defaultSearchEngine;
  // Sync Service.
  raw_ptr<syncer::SyncService> _syncService;
  // Observer to keep track of the syncing status.
  std::unique_ptr<SyncObserverBridge> _syncObserver;
}

// Synthesized from NewTabPageMutator.
@synthesize scrollPositionToSave = _scrollPositionToSave;

- (instancetype)
    initWithTemplateURLService:(TemplateURLService*)templateURLService
                     URLLoader:(UrlLoadingBrowserAgent*)URLLoader
                   authService:(AuthenticationService*)authService
               identityManager:(signin::IdentityManager*)identityManager
         accountManagerService:
             (ChromeAccountManagerService*)accountManagerService
      identityDiscImageUpdater:(id<UserAccountImageUpdateDelegate>)imageUpdater
                   isIncognito:(BOOL)isIncognito
           discoverFeedService:(DiscoverFeedService*)discoverFeedService
                   prefService:(PrefService*)prefService
                   syncService:(syncer::SyncService*)syncService
                    isSafeMode:(BOOL)isSafeMode {
  self = [super init];
  if (self) {
    CHECK(accountManagerService);
    _templateURLService = templateURLService;
    _defaultSearchEngine = templateURLService->GetDefaultSearchProvider();
    _URLLoader = URLLoader;
    _authService = authService;
    _accountManagerService = accountManagerService;
    _accountManagerServiceObserver =
        std::make_unique<ChromeAccountManagerServiceObserverBridge>(
            self, _accountManagerService);
    _identityObserverBridge.reset(
        new signin::IdentityManagerObserverBridge(identityManager, self));
    // Listen for default search engine changes.
    _searchEngineObserver = std::make_unique<SearchEngineObserverBridge>(
        self, self.templateURLService);
    _syncService = syncService;
    _syncObserver = std::make_unique<SyncObserverBridge>(self, syncService);
    _imageUpdater = imageUpdater;
    _isIncognito = isIncognito;
    _discoverFeedService = discoverFeedService;
    _prefService = prefService;
    _isSafeMode = isSafeMode;
  }
  return self;
}

- (void)setUp {
  _feedHeaderVisible = [self updatedFeedHeaderVisible];
  self.templateURLService->Load();
  [self updateModuleVisibilityForConsumer];
  [self.headerConsumer setLogoIsShowing:search::DefaultSearchProviderIsGoogle(
                                            self.templateURLService)];
  [self.headerConsumer
      setVoiceSearchIsEnabled:ios::provider::IsVoiceSearchEnabled()];
  [self updateAccountImage];
  [self updateAccountErrorBadge];
  [self startObservingPrefs];
}

- (void)shutdown {
  _searchEngineObserver.reset();
  _identityObserverBridge.reset();
  _accountManagerServiceObserver.reset();
  self.accountManagerService = nil;
  self.discoverFeedService = nullptr;
  _prefChangeRegistrar.reset();
  _prefObserverBridge.reset();
  _prefService = nullptr;
  _syncObserver.reset();
  _syncService = nullptr;
  self.feedControlDelegate = nil;
}

- (void)handleFeedLearnMoreTapped {
  [self.feedMetricsRecorder recordHeaderMenuLearnMoreTapped];
  [self openMenuItemWebPage:GURL(kFeedLearnMoreURL)];
}

- (void)saveNTPStateForWebState:(web::WebState*)webState {
  NewTabPageState* NTPState = [[NewTabPageState alloc]
      initWithScrollPosition:self.scrollPositionToSave
                selectedFeed:[self.feedControlDelegate selectedFeed]
       followingFeedSortType:[self.feedControlDelegate followingFeedSortType]];
  self.feedMetricsRecorder.NTPState = NTPState;
  NewTabPageTabHelper::FromWebState(webState)->SetNTPState(NTPState);
}

- (void)restoreNTPStateForWebState:(web::WebState*)webState {
  NewTabPageState* NTPState =
      NewTabPageTabHelper::FromWebState(webState)->GetNTPState();
  self.feedMetricsRecorder.NTPState = NTPState;
  if ([self.feedControlDelegate isFollowingFeedAvailable]) {
    [self.NTPContentDelegate updateForSelectedFeed:NTPState.selectedFeed];
  }

  if (NTPState.shouldScrollToTopOfFeed) {
    [self.consumer restoreScrollPositionToTopOfFeed];
    // Prevent next NTP from being scrolled to the top of feed.
    NTPState.shouldScrollToTopOfFeed = NO;
    NewTabPageTabHelper::FromWebState(webState)->SetNTPState(NTPState);
  } else {
    [self.consumer restoreScrollPosition:NTPState.scrollPosition];
  }
}

#pragma mark - FeedManagementNavigationDelegate

- (void)handleNavigateToActivity {
  [self.feedMetricsRecorder recordHeaderMenuManageActivityTapped];
  [self openMenuItemWebPage:GURL(kFeedManageActivityURL)];
}

- (void)handleNavigateToFollowing {
  [self.feedMetricsRecorder recordHeaderMenuManageFollowingTapped];
  [self openMenuItemWebPage:GURL(kFeedManageInterestsURL)];
}

- (void)handleNavigateToHidden {
  [self.feedMetricsRecorder recordHeaderMenuManageHiddenTapped];
  [self openMenuItemWebPage:GURL(kFeedManageHiddenURL)];
}

- (void)handleNavigateToFollowedURL:(const GURL&)url {
  // TODO(crbug.com/40227407): Add metrics.
  [self openMenuItemWebPage:url];
}

#pragma mark - ChromeAccountManagerServiceObserver

- (void)identityUpdated:(id<SystemIdentity>)identity {
  [self updateAccountImage];
  [self updateAccountErrorBadge];
}

#pragma mark - SearchEngineObserving

- (void)searchEngineChanged {
  const TemplateURL* updatedDefaultSearchEngine =
      self.templateURLService->GetDefaultSearchProvider();
  if (_defaultSearchEngine == updatedDefaultSearchEngine) {
    return;
  }
  _defaultSearchEngine = updatedDefaultSearchEngine;
  [self.headerConsumer setLogoIsShowing:search::DefaultSearchProviderIsGoogle(
                                            self.templateURLService)];
  [self setFeedHeaderVisible:[self updatedFeedHeaderVisible]];
  [self.feedControlDelegate updateFeedForDefaultSearchEngineChanged];
}

#pragma mark - IdentityManagerObserverBridgeDelegate

- (void)onPrimaryAccountChanged:
    (const signin::PrimaryAccountChangeEvent&)event {
  switch (event.GetEventTypeFor(signin::ConsentLevel::kSignin)) {
    case signin::PrimaryAccountChangeEvent::Type::kSet:
    case signin::PrimaryAccountChangeEvent::Type::kCleared:
      [self updateAccountImage];
      [self updateAccountErrorBadge];
      break;
    case signin::PrimaryAccountChangeEvent::Type::kNone:
      break;
  }
}
#pragma mark - PrefObserverDelegate

- (void)onPreferenceChanged:(const std::string&)preferenceName {
  [self setFeedHeaderVisible:[self updatedFeedHeaderVisible]];

  // Handle customization prefs
  if (preferenceName == prefs::kHomeCustomizationMostVisitedEnabled ||
      preferenceName == prefs::kHomeCustomizationMagicStackEnabled ||
      preferenceName == prefs::kArticlesForYouEnabled) {
    [self updateModuleVisibilityForConsumer];
    [self.NTPContentDelegate updateModuleVisibility];
  }
}

#pragma mark - SyncObserverModelBridge

- (void)onSyncStateChanged {
  [self updateAccountErrorBadge];
}

#pragma mark - Private

// Fetches and update user's avatar on NTP, or use default avatar if user is
// not signed in.
- (void)updateAccountImage {
  // Fetches user's identity from Authentication Service.
  id<SystemIdentity> identity =
      self.authService->GetPrimaryIdentity(signin::ConsentLevel::kSignin);
  if (identity) {
    // Only show an avatar if the user is signed in.
    UIImage* image = self.accountManagerService->GetIdentityAvatarWithIdentity(
        identity, IdentityAvatarSize::SmallSize);
    [self.imageUpdater updateAccountImage:image
                                     name:identity.userFullName
                                    email:identity.userEmail];
  } else {
    [self.imageUpdater setSignedOutAccountImage];
  }
}

// Opens web page for a menu item in the NTP.
- (void)openMenuItemWebPage:(GURL)URL {
  _URLLoader->Load(UrlLoadParams::InCurrentTab(URL));
  // TODO(crbug.com/40693626): Add metrics.
}

// Returns an updated value for feedHeaderVisible.
- (BOOL)updatedFeedHeaderVisible {
  return _prefService->GetBoolean(prefs::kArticlesForYouEnabled) &&
         _prefService->GetBoolean(prefs::kNTPContentSuggestionsEnabled) &&
         !IsFeedAblationEnabled() &&
         IsContentSuggestionsForSupervisedUserEnabled(_prefService) &&
         !_isSafeMode &&
         !ShouldHideFeedWithSearchChoice(self.templateURLService);
}

// Sets whether the feed header should be visible.
- (void)setFeedHeaderVisible:(BOOL)feedHeaderVisible {
  if (feedHeaderVisible == _feedHeaderVisible) {
    return;
  }

  _feedHeaderVisible = feedHeaderVisible;
  [self.feedControlDelegate setFeedAndHeaderVisibility:_feedHeaderVisible];
}

// Updates the consumer with the current visibility of the NTP modules.
- (void)updateModuleVisibilityForConsumer {
  self.consumer.mostVisitedVisible =
      _prefService->GetBoolean(prefs::kHomeCustomizationMostVisitedEnabled);
  self.consumer.magicStackVisible =
      _prefService->GetBoolean(prefs::kHomeCustomizationMagicStackEnabled);
}

// Starts observing some prefs.
- (void)startObservingPrefs {
  _prefChangeRegistrar = std::make_unique<PrefChangeRegistrar>();
  _prefChangeRegistrar->Init(_prefService);
  _prefObserverBridge = std::make_unique<PrefObserverBridge>(self);

  // Observe feed visibility prefs.
  _prefObserverBridge->ObserveChangesForPreference(
      prefs::kArticlesForYouEnabled, _prefChangeRegistrar.get());
  _prefObserverBridge->ObserveChangesForPreference(
      prefs::kNTPContentSuggestionsEnabled, _prefChangeRegistrar.get());
  _prefObserverBridge->ObserveChangesForPreference(
      prefs::kNTPContentSuggestionsForSupervisedUserEnabled,
      _prefChangeRegistrar.get());

  // Observe customization prefs.
  _prefObserverBridge->ObserveChangesForPreference(
      prefs::kHomeCustomizationMostVisitedEnabled, _prefChangeRegistrar.get());
  _prefObserverBridge->ObserveChangesForPreference(
      prefs::kHomeCustomizationMagicStackEnabled, _prefChangeRegistrar.get());
}

- (void)updateAccountErrorBadge {
  if (!base::FeatureList::IsEnabled(kIdentityDiscAccountMenu)) {
    return;
  }
  id<SystemIdentity> identity =
      self.authService->GetPrimaryIdentity(signin::ConsentLevel::kSignin);
  BOOL primaryIdentityHasError =
      identity && _syncService->GetUserActionableError() !=
                      syncer::SyncService::UserActionableError::kNone;
  [self.headerConsumer updateADPBadgeWithErrorFound:primaryIdentityHasError];
}

@end