// Copyright 2024 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/ui/content_suggestions/set_up_list/set_up_list_mediator.h"
#import <AuthenticationServices/AuthenticationServices.h>
#import "base/ios/crb_protocol_observers.h"
#import "base/memory/raw_ptr.h"
#import "base/strings/sys_string_conversions.h"
#import "components/prefs/ios/pref_observer_bridge.h"
#import "components/prefs/pref_service.h"
#import "components/segmentation_platform/embedder/default_model/device_switcher_model.h"
#import "components/segmentation_platform/embedder/default_model/device_switcher_result_dispatcher.h"
#import "components/segmentation_platform/public/constants.h"
#import "components/segmentation_platform/public/result.h"
#import "components/segmentation_platform/public/segmentation_platform_service.h"
#import "components/signin/public/identity_manager/identity_manager.h"
#import "components/signin/public/identity_manager/objc/identity_manager_observer_bridge.h"
#import "ios/chrome/browser/content_notification/model/content_notification_util.h"
#import "ios/chrome/browser/credential_provider_promo/ui_bundled/credential_provider_promo_metrics.h"
#import "ios/chrome/browser/default_browser/model/utils.h"
#import "ios/chrome/browser/ntp/model/set_up_list.h"
#import "ios/chrome/browser/ntp/model/set_up_list_delegate.h"
#import "ios/chrome/browser/ntp/model/set_up_list_item.h"
#import "ios/chrome/browser/ntp/model/set_up_list_item_type.h"
#import "ios/chrome/browser/ntp/model/set_up_list_prefs.h"
#import "ios/chrome/browser/push_notification/model/push_notification_settings_util.h"
#import "ios/chrome/browser/segmentation_platform/model/segmented_default_browser_utils.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/authentication_service_observer_bridge.h"
#import "ios/chrome/browser/sync/model/enterprise_utils.h"
#import "ios/chrome/browser/sync/model/sync_observer_bridge.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_constants.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_delegate.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_metrics_recorder.h"
#import "ios/chrome/browser/ui/content_suggestions/set_up_list/set_up_list_config.h"
#import "ios/chrome/browser/ui/content_suggestions/set_up_list/set_up_list_consumer_source.h"
#import "ios/chrome/browser/ui/content_suggestions/set_up_list/set_up_list_item_view_data.h"
#import "ios/chrome/browser/ui/content_suggestions/set_up_list/utils.h"
using credential_provider_promo::IOSCredentialProviderPromoAction;
namespace {
// Checks the last action the user took on the Credential Provider Promo to
// determine if it was dismissed.
bool CredentialProviderPromoDismissed(PrefService* local_state) {
IOSCredentialProviderPromoAction last_action =
static_cast<IOSCredentialProviderPromoAction>(local_state->GetInteger(
prefs::kIosCredentialProviderPromoLastActionTaken));
return last_action == IOSCredentialProviderPromoAction::kNo;
}
// Returns true if a Default Browser Promo was completed, outside of SetUpList.
// This includes the FRE.
bool DefaultBrowserPromoCompleted() {
std::optional<IOSDefaultBrowserPromoAction> action =
DefaultBrowserPromoLastAction();
if (!action.has_value()) {
return false;
}
switch (action.value()) {
case IOSDefaultBrowserPromoAction::kActionButton:
case IOSDefaultBrowserPromoAction::kCancel:
return true;
case IOSDefaultBrowserPromoAction::kRemindMeLater:
case IOSDefaultBrowserPromoAction::kDismiss:
return false;
}
}
} // namespace
#pragma mark - SetUpListConsumerList
@interface SetUpListConsumerList : CRBProtocolObservers <SetUpListConsumer>
@end
@implementation SetUpListConsumerList
@end
@interface SetUpListMediator () <AuthenticationServiceObserving,
IdentityManagerObserverBridgeDelegate,
PrefObserverDelegate,
SceneStateObserver,
SetUpListDelegate,
SetUpListConsumerSource,
SyncObserverModelBridge>
@end
@implementation SetUpListMediator {
SetUpList* _setUpList;
raw_ptr<PrefService> _localState;
raw_ptr<PrefService> _prefService;
// Used by SetUpList to get the sync status.
raw_ptr<syncer::SyncService> _syncService;
// Observer for sync service status changes.
std::unique_ptr<SyncObserverBridge> _syncObserverBridge;
// Observes changes to signed-in status.
std::unique_ptr<signin::IdentityManagerObserverBridge>
_identityObserverBridge;
// Used by SetUpList to get signed-in status.
raw_ptr<AuthenticationService> _authenticationService;
// Observer for auth service status changes.
std::unique_ptr<AuthenticationServiceObserverBridge>
_authServiceObserverBridge;
// Bridge to listen to pref changes.
std::unique_ptr<PrefObserverBridge> _prefObserverBridge;
// Registrars for pref changes notifications.
PrefChangeRegistrar _prefChangeRegistrar;
PrefChangeRegistrar _localStatePrefChangeRegistrar;
SceneState* _sceneState;
SetUpListConsumerList* _consumers;
NSArray<SetUpListConfig*>* _setUpListConfigs;
// Components for retrieving user segmentation information from the
// Segmentation Platform.
raw_ptr<segmentation_platform::SegmentationPlatformService>
_segmentationService;
raw_ptr<segmentation_platform::DeviceSwitcherResultDispatcher>
_deviceSwitcherResultDispatcher;
// User segment retrieved by the Segmentation Platform.
segmentation_platform::DefaultBrowserUserSegment _userSegment;
}
#pragma mark - Public
- (instancetype)initWithPrefService:(PrefService*)prefService
syncService:(syncer::SyncService*)syncService
identityManager:(signin::IdentityManager*)identityManager
authenticationService:(AuthenticationService*)authService
sceneState:(SceneState*)sceneState
isDefaultSearchEngine:(BOOL)isDefaultSearchEngine
segmentationService:
(segmentation_platform::SegmentationPlatformService*)
segmentationService
deviceSwitcherResultDispatcher:
(segmentation_platform::DeviceSwitcherResultDispatcher*)dispatcher {
self = [super init];
if (self) {
_prefService = prefService;
_localState = GetApplicationContext()->GetLocalState();
_syncService = syncService;
_syncObserverBridge =
std::make_unique<SyncObserverBridge>(self, syncService);
_identityObserverBridge =
std::make_unique<signin::IdentityManagerObserverBridge>(identityManager,
self);
_authenticationService = authService;
_authServiceObserverBridge =
std::make_unique<AuthenticationServiceObserverBridge>(authService,
self);
_prefObserverBridge = std::make_unique<PrefObserverBridge>(self);
_localStatePrefChangeRegistrar.Init(_localState);
_prefChangeRegistrar.Init(prefService);
_prefObserverBridge->ObserveChangesForPreference(
prefs::kIosCredentialProviderPromoLastActionTaken,
&_localStatePrefChangeRegistrar);
_prefObserverBridge->ObserveChangesForPreference(
prefs::kIosDefaultBrowserPromoLastAction,
&_localStatePrefChangeRegistrar);
_prefObserverBridge->ObserveChangesForPreference(
set_up_list_prefs::kDisabled, &_localStatePrefChangeRegistrar);
if (IsHomeCustomizationEnabled()) {
_prefObserverBridge->ObserveChangesForPreference(
prefs::kHomeCustomizationMagicStackSetUpListEnabled,
&_prefChangeRegistrar);
}
if (IsIOSTipsNotificationsEnabled()) {
_prefObserverBridge->ObserveChangesForPreference(
prefs::kAppLevelPushNotificationPermissions,
&_localStatePrefChangeRegistrar);
_prefObserverBridge->ObserveChangesForPreference(
prefs::kFeaturePushNotificationPermissions, &_prefChangeRegistrar);
}
if (CredentialProviderPromoDismissed(_localState)) {
set_up_list_prefs::MarkItemComplete(_localState,
SetUpListItemType::kAutofill);
} else {
[self checkIfCPEEnabled];
}
if (DefaultBrowserPromoCompleted()) {
set_up_list_prefs::MarkItemComplete(_localState,
SetUpListItemType::kDefaultBrowser);
}
_sceneState = sceneState;
[_sceneState addObserver:self];
if (IsSegmentedDefaultBrowserPromoEnabled()) {
_segmentationService = segmentationService;
_deviceSwitcherResultDispatcher = dispatcher;
}
BOOL isContentNotificationEnabled =
IsContentNotificationExperimentEnabled() &&
IsContentNotificationSetUpListEnabled(
identityManager->HasPrimaryAccount(signin::ConsentLevel::kSignin),
isDefaultSearchEngine, prefService);
_setUpList = [SetUpList buildFromPrefs:prefService
localState:_localState
syncService:syncService
authenticationService:authService
contentNotificationEnabled:isContentNotificationEnabled];
_setUpList.delegate = self;
_consumers = [SetUpListConsumerList
observersWithProtocol:@protocol(SetUpListConsumer)];
}
return self;
}
- (void)disconnect {
_segmentationService = nullptr;
_deviceSwitcherResultDispatcher = nullptr;
_authenticationService = nullptr;
_authServiceObserverBridge.reset();
_syncObserverBridge.reset();
_identityObserverBridge.reset();
if (_prefObserverBridge) {
_localStatePrefChangeRegistrar.RemoveAll();
_prefChangeRegistrar.RemoveAll();
_prefObserverBridge.reset();
}
[_setUpList disconnect];
_setUpList = nil;
[_sceneState removeObserver:self];
_localState = nullptr;
_prefService = nullptr;
}
- (void)addConsumer:(id<SetUpListConsumer>)consumer {
[_consumers addObserver:consumer];
}
- (void)removeConsumer:(id<SetUpListConsumer>)consumer {
[_consumers removeObserver:consumer];
}
- (NSArray<SetUpListItemViewData*>*)allItems {
NSMutableArray<SetUpListItemViewData*>* allItems =
[[NSMutableArray alloc] init];
for (SetUpListItem* model in _setUpList.allItems) {
SetUpListItemViewData* item =
[[SetUpListItemViewData alloc] initWithType:model.type
complete:model.complete];
if (IsSegmentedDefaultBrowserPromoEnabled()) {
[item setUserSegment:_userSegment];
}
[allItems addObject:item];
}
return allItems;
}
- (BOOL)allItemsComplete {
return [_setUpList allItemsComplete];
}
- (void)disableModule {
set_up_list_prefs::DisableSetUpList(
IsHomeCustomizationEnabled() ? _prefService : _localState);
}
- (BOOL)shouldShowSetUpList {
if (!set_up_list_utils::IsSetUpListActive(_localState, _prefService)) {
return NO;
}
if ([self setUpListItems].count == 0) {
return NO;
}
return YES;
}
- (NSArray<SetUpListConfig*>*)setUpListConfigs {
if (!_setUpListConfigs) {
if ([self allItemsComplete]) {
SetUpListConfig* config = [[SetUpListConfig alloc] init];
config.setUpListConsumerSource = self;
config.commandHandler = self.commandHandler;
config.setUpListItems = @[ [self allSetItem] ];
_setUpListConfigs = @[ config ];
} else {
NSArray<SetUpListItemViewData*>* items = [self setUpListItems];
if (set_up_list_utils::ShouldShowCompactedSetUpListModule()) {
SetUpListConfig* config = [[SetUpListConfig alloc] init];
config.shouldShowCompactModule = YES;
config.shouldShowSeeMore = YES;
config.setUpListConsumerSource = self;
config.commandHandler = self.commandHandler;
if ([items count] > 2) {
items = [items subarrayWithRange:NSMakeRange(0, 2)];
}
for (SetUpListItemViewData* data in items) {
data.compactLayout = YES;
data.heroCellMagicStackLayout = NO;
}
config.setUpListItems = items;
_setUpListConfigs = @[ config ];
} else {
// Iterate through all items and create config for each hero module.
NSMutableArray<SetUpListConfig*>* configs = [NSMutableArray array];
for (SetUpListItemViewData* data in items) {
data.compactLayout = NO;
data.heroCellMagicStackLayout = YES;
SetUpListConfig* config = [[SetUpListConfig alloc] init];
config.setUpListConsumerSource = self;
config.commandHandler = self.commandHandler;
config.setUpListItems = @[ data ];
[configs addObject:config];
}
_setUpListConfigs = configs;
}
}
// Record "ItemDisplayed" histogram for each item.
for (SetUpListConfig* config in _setUpListConfigs) {
for (SetUpListItemViewData* item in config.setUpListItems) {
[self.contentSuggestionsMetricsRecorder
recordSetUpListItemShown:item.type];
}
}
[self.contentSuggestionsMetricsRecorder recordSetUpListShown];
}
return _setUpListConfigs;
}
- (void)retrieveUserSegment {
CHECK(_segmentationService);
CHECK(_deviceSwitcherResultDispatcher);
segmentation_platform::PredictionOptions options =
segmentation_platform::PredictionOptions::ForCached();
segmentation_platform::ClassificationResult deviceSwitcherResult =
_deviceSwitcherResultDispatcher->GetCachedClassificationResult();
__weak __typeof(self) weakSelf = self;
auto classificationResultCallback = base::BindOnce(
[](__typeof(self) strongSelf,
segmentation_platform::ClassificationResult deviceSwitcherResult,
const segmentation_platform::ClassificationResult& shopperResult) {
[strongSelf didReceiveShopperSegmentationResult:shopperResult
deviceSwitcherResult:deviceSwitcherResult];
},
weakSelf, deviceSwitcherResult);
_segmentationService->GetClassificationResult(
segmentation_platform::kShoppingUserSegmentationKey, options, nullptr,
std::move(classificationResultCallback));
}
#pragma mark - SetUpListDelegate
- (void)setUpListItemDidComplete:(SetUpListItem*)item
allItemsCompleted:(BOOL)completed {
// Can resend signal to mediator from Set Up List after SetUpListItemView
// completes animation
ProceduralBlock completion = ^{
if (completed) {
SetUpListConfig* config = [[SetUpListConfig alloc] init];
config.setUpListItems = @[ [self allSetItem] ];
[self.audience replaceSetUpListWithAllSet:config];
}
};
[_consumers setUpListItemDidComplete:item
allItemsCompleted:completed
completion:completion];
}
#pragma mark - IdentityManagerObserverBridgeDelegate
// Called when a user changes the syncing state.
- (void)onPrimaryAccountChanged:
(const signin::PrimaryAccountChangeEvent&)event {
switch (event.GetEventTypeFor(signin::ConsentLevel::kSignin)) {
case signin::PrimaryAccountChangeEvent::Type::kSet: {
// User has signed in, mark SetUpList item complete. Delayed to allow
// Signin UI flow to be fully dismissed before starting SetUpList
// completion animation.
__weak __typeof(self) weakSelf = self;
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, base::BindOnce(^{
[weakSelf
markSetUpListItemPrefComplete:SetUpListItemType::kSignInSync];
}),
base::Seconds(0.5));
} break;
case signin::PrimaryAccountChangeEvent::Type::kCleared:
case signin::PrimaryAccountChangeEvent::Type::kNone:
break;
}
}
#pragma mark - PrefObserverDelegate
- (void)onPreferenceChanged:(const std::string&)preferenceName {
if (preferenceName == prefs::kIosCredentialProviderPromoLastActionTaken &&
CredentialProviderPromoDismissed(_localState)) {
[self markSetUpListItemPrefComplete:SetUpListItemType::kAutofill];
} else if (preferenceName == prefs::kIosDefaultBrowserPromoLastAction &&
DefaultBrowserPromoCompleted()) {
[self markSetUpListItemPrefComplete:SetUpListItemType::kDefaultBrowser];
} else if (preferenceName == set_up_list_prefs::kDisabled &&
set_up_list_prefs::IsSetUpListDisabled(_localState)) {
[self hideSetUpList];
} else if (preferenceName == prefs::kAppLevelPushNotificationPermissions ||
preferenceName == prefs::kFeaturePushNotificationPermissions) {
CHECK(IsIOSTipsNotificationsEnabled());
if ([self hasOptedInToNotifications]) {
[self markSetUpListItemPrefComplete:SetUpListItemType::kNotifications];
}
} else if (preferenceName ==
prefs::kHomeCustomizationMagicStackSetUpListEnabled &&
!_prefService->GetBoolean(
prefs::kHomeCustomizationMagicStackSetUpListEnabled)) {
CHECK(IsHomeCustomizationEnabled());
[self hideSetUpList];
}
}
#pragma mark - SyncObserverModelBridge
- (void)onSyncStateChanged {
if (_setUpList) {
if (_syncService->HasDisableReason(
syncer::SyncService::DISABLE_REASON_ENTERPRISE_POLICY) ||
HasManagedSyncDataType(_syncService)) {
// Sync is now disabled, so mark the SetUpList item complete so that it
// cannot be used again.
[self markSetUpListItemPrefComplete:SetUpListItemType::kSignInSync];
}
}
}
#pragma mark - AuthenticationServiceObserving
- (void)onServiceStatusChanged {
if (_setUpList) {
switch (_authenticationService->GetServiceStatus()) {
case AuthenticationService::ServiceStatus::SigninForcedByPolicy:
case AuthenticationService::ServiceStatus::SigninAllowed:
break;
case AuthenticationService::ServiceStatus::SigninDisabledByUser:
case AuthenticationService::ServiceStatus::SigninDisabledByPolicy:
case AuthenticationService::ServiceStatus::SigninDisabledByInternal:
// Signin is now disabled, so mark the SetUpList item complete so that
// it cannot be used again.
[self markSetUpListItemPrefComplete:SetUpListItemType::kSignInSync];
}
}
}
#pragma mark - SceneStateObserver
- (void)sceneState:(SceneState*)sceneState
transitionedToActivationLevel:(SceneActivationLevel)level {
if (level == SceneActivationLevelForegroundActive) {
if (_setUpList) {
[self checkIfCPEEnabled];
}
}
}
#pragma mark - Private
- (NSArray<SetUpListItemViewData*>*)setUpListItems {
NSMutableArray<SetUpListItemViewData*>* items = [[NSMutableArray alloc] init];
// Add items that are not complete yet.
for (SetUpListItem* model in _setUpList.items) {
if (model.complete) {
continue;
}
SetUpListItemViewData* item =
[[SetUpListItemViewData alloc] initWithType:model.type
complete:model.complete];
if (IsSegmentedDefaultBrowserPromoEnabled()) {
[item setUserSegment:_userSegment];
}
[items addObject:item];
}
// Add items that are complete to the end.
for (SetUpListItem* model in _setUpList.items) {
if (!model.complete) {
continue;
}
SetUpListItemViewData* item =
[[SetUpListItemViewData alloc] initWithType:model.type
complete:model.complete];
if (IsSegmentedDefaultBrowserPromoEnabled()) {
[item setUserSegment:_userSegment];
}
[items addObject:item];
}
return items;
}
// Sets the pref for a SetUpList item to indicate it is complete.
- (void)markSetUpListItemPrefComplete:(SetUpListItemType)type {
// Exit early if this is called after `disconnect` which clears _localState.
// Item states will be reevaluated the next time this mediator is loaded.
if (!_localState) {
return;
}
set_up_list_prefs::MarkItemComplete(_localState, type);
}
// Returns an item for the "All Set" Set Up List state.
- (SetUpListItemViewData*)allSetItem {
SetUpListItemViewData* allSetItem =
[[SetUpListItemViewData alloc] initWithType:SetUpListItemType::kAllSet
complete:NO];
allSetItem.compactLayout = NO;
allSetItem.heroCellMagicStackLayout = YES;
return allSetItem;
}
// Hides the Set Up List with an animation.
- (void)hideSetUpList {
[self.audience removeSetUpList];
}
// Checks if the CPE is enabled and marks the SetUpList Autofill item complete
// if it is.
- (void)checkIfCPEEnabled {
__weak __typeof(self) weakSelf = self;
scoped_refptr<base::SequencedTaskRunner> runner =
base::SequencedTaskRunner::GetCurrentDefault();
[ASCredentialIdentityStore.sharedStore
getCredentialIdentityStoreStateWithCompletion:^(
ASCredentialIdentityStoreState* state) {
if (state.isEnabled) {
// The completion handler sent to ASCredentialIdentityStore is
// executed on a background thread. Putting it back onto the main
// thread to update local state prefs.
runner->PostTask(
FROM_HERE, base::BindOnce(^{
[weakSelf
markSetUpListItemPrefComplete:SetUpListItemType::kAutofill];
}));
}
}];
}
- (BOOL)hasOptedInToNotifications {
id<SystemIdentity> identity =
_authenticationService->GetPrimaryIdentity(signin::ConsentLevel::kSignin);
return push_notification_settings::IsMobileNotificationsEnabledForAnyClient(
base::SysNSStringToUTF8(identity.gaiaID), _prefService);
}
// Sets user's highest priority segment retrieved from the Segmentation
// Platform.
- (void)didReceiveShopperSegmentationResult:
(const segmentation_platform::ClassificationResult&)shopperResult
deviceSwitcherResult:
(const segmentation_platform::ClassificationResult&)
deviceSwitcherResult {
_userSegment =
GetDefaultBrowserUserSegment(&deviceSwitcherResult, &shopperResult);
}
@end