// Copyright 2023 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/authentication/history_sync/history_sync_coordinator.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "components/signin/public/base/signin_metrics.h"
#import "components/signin/public/base/signin_switches.h"
#import "components/sync/base/user_selectable_type.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/service/sync_user_settings.h"
#import "ios/chrome/browser/first_run/model/first_run_metrics.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_factory.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/ui/authentication/enterprise/enterprise_utils.h"
#import "ios/chrome/browser/ui/authentication/history_sync/history_sync_mediator.h"
#import "ios/chrome/browser/ui/authentication/history_sync/history_sync_utils.h"
#import "ios/chrome/browser/ui/authentication/history_sync/history_sync_view_controller.h"
#import "ios/chrome/browser/ui/authentication/signin/signin_constants.h"
#import "ios/chrome/common/ui/promo_style/promo_style_view_controller.h"
@interface HistorySyncCoordinator () <HistorySyncMediatorDelegate,
PromoStyleViewControllerDelegate>
@end
@implementation HistorySyncCoordinator {
// History mediator.
HistorySyncMediator* _mediator;
// History view controller.
HistorySyncViewController* _viewController;
// Pref service.
raw_ptr<PrefService> _prefService;
// `YES` if coordinator used during the first run.
BOOL _firstRun;
// `YES` if the user's email should be shown in the footer text.
BOOL _showUserEmail;
// Whether the History Sync screen is a optional step, that can be skipped
// if declined too often.
BOOL _isOptional;
// `YES` if the opt-in aborted metric should be recorded when the
// coordinator stops.
BOOL _recordOptInEndAtStop;
// Delegate for the history sync coordinator.
__weak id<HistorySyncCoordinatorDelegate> _delegate;
// Access point associated with the history opt-in screen.
signin_metrics::AccessPoint _accessPoint;
}
@synthesize baseNavigationController = _baseNavigationController;
+ (HistorySyncSkipReason)
getHistorySyncOptInSkipReason:(syncer::SyncService*)syncService
authenticationService:(AuthenticationService*)authenticationService
prefService:(PrefService*)prefService
isHistorySyncOptional:(BOOL)isOptional {
if (syncService->HasDisableReason(
syncer::SyncService::DISABLE_REASON_ENTERPRISE_POLICY) ||
syncService->GetUserSettings()->IsTypeManagedByPolicy(
syncer::UserSelectableType::kTabs) ||
syncService->GetUserSettings()->IsTypeManagedByPolicy(
syncer::UserSelectableType::kHistory)) {
// Skip History Sync Opt-in if sync is disabled, or if history or
// tabs sync is disabled by policy.
return HistorySyncSkipReason::kSyncForbiddenByPolicies;
}
if (!authenticationService->GetPrimaryIdentity(
signin::ConsentLevel::kSignin)) {
// Don't show history sync opt-in screen if no signed-in user account.
return HistorySyncSkipReason::kNotSignedIn;
}
syncer::SyncUserSettings* userSettings = syncService->GetUserSettings();
if (userSettings->GetSelectedTypes().HasAll(
{syncer::UserSelectableType::kHistory,
syncer::UserSelectableType::kTabs})) {
// History opt-in is already set. This value is kept between signout/signin.
// In this case the UI can be skipped.
return HistorySyncSkipReason::kAlreadyOptedIn;
}
if (history_sync::IsDeclinedTooOften(prefService) && isOptional) {
return HistorySyncSkipReason::kDeclinedTooOften;
}
return HistorySyncSkipReason::kNone;
}
+ (void)recordHistorySyncSkipMetric:(HistorySyncSkipReason)reason
accessPoint:(signin_metrics::AccessPoint)accessPoint {
switch (reason) {
case HistorySyncSkipReason::kNotSignedIn:
case HistorySyncSkipReason::kSyncForbiddenByPolicies:
case HistorySyncSkipReason::kDeclinedTooOften:
base::RecordAction(base::UserMetricsAction("Signin_HistorySync_Skipped"));
base::UmaHistogramEnumeration(
"Signin.HistorySyncOptIn.Skipped", accessPoint,
signin_metrics::AccessPoint::ACCESS_POINT_MAX);
break;
case HistorySyncSkipReason::kAlreadyOptedIn:
base::RecordAction(
base::UserMetricsAction("Signin_HistorySync_AlreadyOptedIn"));
base::UmaHistogramEnumeration(
"Signin.HistorySyncOptIn.AlreadyOptedIn", accessPoint,
signin_metrics::AccessPoint::ACCESS_POINT_MAX);
break;
case HistorySyncSkipReason::kNone:
// This method should not be called if the screen should be shown.
// If a metric should be recorded in this case, it should be handled in
// HistorySyncCoordinator instance methods instead of this class method
// to avoid duplicated recording.
NOTREACHED();
}
}
- (instancetype)
initWithBaseNavigationController:
(UINavigationController*)navigationController
browser:(Browser*)browser
delegate:
(id<HistorySyncCoordinatorDelegate>)delegate
firstRun:(BOOL)firstRun
showUserEmail:(BOOL)showUserEmail
isOptional:(BOOL)isOptional
accessPoint:(signin_metrics::AccessPoint)accessPoint {
self = [super initWithBaseViewController:navigationController
browser:browser];
if (self) {
_baseNavigationController = navigationController;
_firstRun = firstRun;
_showUserEmail = showUserEmail;
_isOptional = isOptional;
_delegate = delegate;
_accessPoint = accessPoint;
}
return self;
}
- (void)start {
[super start];
ChromeBrowserState* browserState = self.browser->GetBrowserState();
CHECK_EQ(browserState, browserState->GetOriginalChromeBrowserState());
AuthenticationService* authenticationService =
AuthenticationServiceFactory::GetForBrowserState(browserState);
syncer::SyncService* syncService =
SyncServiceFactory::GetForBrowserState(browserState);
_prefService = browserState->GetPrefs();
// Check if History Sync Opt-In should be skipped.
HistorySyncSkipReason skipReason = [HistorySyncCoordinator
getHistorySyncOptInSkipReason:syncService
authenticationService:authenticationService
prefService:_prefService
isHistorySyncOptional:_isOptional];
if (skipReason != HistorySyncSkipReason::kNone) {
[HistorySyncCoordinator recordHistorySyncSkipMetric:skipReason
accessPoint:_accessPoint];
[_delegate closeHistorySyncCoordinator:self declinedByUser:NO];
return;
}
_viewController = [[HistorySyncViewController alloc] init];
_viewController.delegate = self;
ChromeAccountManagerService* chromeAccountManagerService =
ChromeAccountManagerServiceFactory::GetForBrowserState(browserState);
signin::IdentityManager* identityManager =
IdentityManagerFactory::GetForBrowserState(browserState);
_mediator = [[HistorySyncMediator alloc]
initWithAuthenticationService:authenticationService
chromeAccountManagerService:chromeAccountManagerService
identityManager:identityManager
syncService:syncService
showUserEmail:_showUserEmail];
_mediator.consumer = _viewController;
_mediator.delegate = self;
if (_firstRun) {
_viewController.modalInPresentation = YES;
base::UmaHistogramEnumeration(first_run::kFirstRunStageHistogram,
first_run::kHistorySyncScreenStart);
}
base::RecordAction(base::UserMetricsAction("Signin_HistorySync_Started"));
base::UmaHistogramEnumeration("Signin.HistorySyncOptIn.Started", _accessPoint,
signin_metrics::AccessPoint::ACCESS_POINT_MAX);
_recordOptInEndAtStop = YES;
BOOL animated = self.baseNavigationController.topViewController != nil;
[self.baseNavigationController setViewControllers:@[ _viewController ]
animated:animated];
}
- (void)stop {
[super stop];
_delegate = nil;
if (_recordOptInEndAtStop) {
// Tracks the case where the opt-in flow ends without being accepted or
// declined by the user. (eg. identity disappeared, popup swiped down by the
// user, Chrome shutdown)
//
// This can also occur during the FRE, for instance if Chrome shuts down
// when the screen is shown, or if FRE is dismissed due to policies change.
base::RecordAction(base::UserMetricsAction("Signin_HistorySync_Aborted"));
base::UmaHistogramEnumeration(
"Signin.HistorySyncOptIn.Aborted", _accessPoint,
signin_metrics::AccessPoint::ACCESS_POINT_MAX);
_recordOptInEndAtStop = NO;
}
[_mediator disconnect];
_mediator.consumer = nil;
_mediator.delegate = nil;
_mediator = nil;
_viewController.delegate = nil;
_viewController = nil;
_prefService = nullptr;
}
- (void)dealloc {
DUMP_WILL_BE_CHECK(!_viewController);
}
#pragma mark - HistorySyncMediatorDelegate
- (void)historySyncMediatorPrimaryAccountCleared:
(HistorySyncMediator*)mediator {
[_delegate closeHistorySyncCoordinator:self declinedByUser:NO];
}
#pragma mark - PromoStyleViewControllerDelegate
- (void)didTapPrimaryActionButton {
[_mediator enableHistorySyncOptin];
history_sync::ResetDeclinePrefs(_prefService);
base::RecordAction(base::UserMetricsAction("Signin_HistorySync_Completed"));
[self recordActionButtonTappedWithHistorySyncCompleted:YES];
if (_firstRun) {
base::UmaHistogramEnumeration(
first_run::kFirstRunStageHistogram,
first_run::kHistorySyncScreenCompletionWithSync);
}
base::UmaHistogramEnumeration("Signin.HistorySyncOptIn.Completed",
_accessPoint,
signin_metrics::AccessPoint::ACCESS_POINT_MAX);
_recordOptInEndAtStop = NO;
[_delegate closeHistorySyncCoordinator:self declinedByUser:NO];
}
- (void)didTapSecondaryActionButton {
history_sync::RecordDeclinePrefs(_prefService);
base::RecordAction(base::UserMetricsAction("Signin_HistorySync_Declined"));
[self recordActionButtonTappedWithHistorySyncCompleted:NO];
if (_firstRun) {
base::UmaHistogramEnumeration(
first_run::kFirstRunStageHistogram,
first_run::kHistorySyncScreenCompletionWithoutSync);
}
base::UmaHistogramEnumeration("Signin.HistorySyncOptIn.Declined",
_accessPoint,
signin_metrics::AccessPoint::ACCESS_POINT_MAX);
_recordOptInEndAtStop = NO;
[_delegate closeHistorySyncCoordinator:self declinedByUser:YES];
}
#pragma mark - Private
- (void)recordActionButtonTappedWithHistorySyncCompleted:(BOOL)completed {
if (!(base::FeatureList::GetInstance() &&
base::FeatureList::GetInstance()->IsFeatureOverridden(
switches::kMinorModeRestrictionsForHistorySyncOptIn.name))) {
return;
}
std::optional<signin_metrics::SyncButtonClicked> buttonClicked;
switch (_viewController.actionButtonsVisibility) {
case ActionButtonsVisibility::kDefault:
case ActionButtonsVisibility::kRegularButtonsShown:
buttonClicked = completed ? signin_metrics::SyncButtonClicked::
kHistorySyncOptInNotEqualWeighted
: signin_metrics::SyncButtonClicked::
kHistorySyncCancelNotEqualWeighted;
break;
case ActionButtonsVisibility::kEquallyWeightedButtonShown:
buttonClicked = completed ? signin_metrics::SyncButtonClicked::
kHistorySyncOptInEqualWeighted
: signin_metrics::SyncButtonClicked::
kHistorySyncCancelEqualWeighted;
break;
default:
NOTREACHED_IN_MIGRATION();
break;
}
base::UmaHistogramEnumeration("Signin.SyncButtons.Clicked", *buttonClicked);
}
@end