blob: 4ad688525395cd8fba7123eb9e3c2be88e3010d6 [file] [log] [blame]
// 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.
#include "chrome/browser/ui/startup/first_run_service.h"
#include <memory>
#include <utility>
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/first_run/first_run.h"
#include "chrome/browser/policy/chrome_browser_policy_connector.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profile_metrics.h"
#include "chrome/browser/profiles/profile_selections.h"
#include "chrome/browser/profiles/profiles_state.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/signin/signin_features.h"
#include "chrome/browser/signin/signin_util.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/ui/profile_picker.h"
#include "chrome/browser/ui/signin/profile_customization_util.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/base/signin_pref_names.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "content/public/browser/browser_context.h"
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chrome/browser/enterprise/util/managed_browser_utils.h"
#include "chrome/browser/ui/startup/silent_sync_enabler.h"
#include "chromeos/crosapi/mojom/device_settings_service.mojom.h"
#endif
namespace {
bool IsFirstRunEligibleProfile(Profile* profile) {
if (profile->IsOffTheRecord()) {
return false;
}
// The parent guest and the profiles in a ChromeOS Guest session get through
// the OTR check above.
if (profile->IsGuestSession()) {
return false;
}
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// Skip for users without Gaia account (e.g. Active Directory, Kiosk, Guest…)
if (!profiles::SessionHasGaiaAccount()) {
return false;
}
// Having secondary profiles implies that the user already used Chrome and so
// should not have to see the FRE. So we never want to run it for these.
if (!profile->IsMainProfile()) {
return false;
}
#endif
return true;
}
bool IsFirstRunEligibleProcess() {
#if !BUILDFLAG(IS_CHROMEOS_LACROS)
// On Lacros we want to run the FRE beyond the strict first run as defined by
// `IsChromeFirstRun()` for a few reasons:
// - Migrated profiles will have their first run sentinel imported from the
// ash data dir, but we need to run the FRE in silent mode to re-enable sync
// on the Lacros primary profile.
// - If the user exits the FRE without advancing beyond the first step, we
// need to show the FRE again next time they open Chrome, this is definitely
// not the "first run" anymore.
if (!first_run::IsChromeFirstRun()) {
return false;
}
#endif
// TODO(crbug.com/1347504): `IsChromeFirstRun()` should be a sufficient check
// for Dice platforms. We currently keep this because some tests add
// `--force-first-run` while keeping `--no-first-run`. We should updated the
// affected tests to handle correctly the FRE opening instead of a tab.
return !base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNoFirstRun);
}
enum class PolicyEffect {
// The First Run experience can proceed unaffected.
kNone,
// The First Run experience should not run.
kDisabled,
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// The profile should be set up and opted-in to sync silently if possible.
kSilenced,
#endif
};
PolicyEffect ComputeDevicePolicyEffect(Profile& profile) {
const PrefService* const local_state = g_browser_process->local_state();
if (!local_state->GetBoolean(prefs::kPromotionalTabsEnabled)) {
// Corresponding policy: PromotionalTabsEnabled=false
return PolicyEffect::kDisabled;
}
if (!SyncServiceFactory::IsSyncAllowed(&profile)) {
// Corresponding policy: SyncDisabled=true
return PolicyEffect::kDisabled;
}
#if !BUILDFLAG(IS_CHROMEOS_LACROS)
// The BrowserSignin policy is not supported on Lacros
if (signin_util::IsForceSigninEnabled()) {
// Corresponding policy: BrowserSignin=2
// Debugging note: On Linux this policy is not supported and does not get
// translated to the prefs (see crbug.com/956998), but we still respond to
// `prefs::kForceBrowserSignin` being set (e.g. if manually edited).
return PolicyEffect::kDisabled;
}
if (!profile.GetPrefs()->GetBoolean(prefs::kSigninAllowed) ||
!profile.GetPrefs()->GetBoolean(prefs::kSigninAllowedOnNextStartup)) {
// Corresponding policy: BrowserSignin=0
return PolicyEffect::kDisabled;
}
#endif
#if BUILDFLAG(IS_CHROMEOS_LACROS)
if (!profile.GetPrefs()->GetBoolean(prefs::kEnableSyncConsent)) {
// Corresponding policy: EnableSyncConsent=false
return PolicyEffect::kSilenced;
}
crosapi::mojom::DeviceSettings* device_settings =
g_browser_process->browser_policy_connector()->GetDeviceSettings();
if (device_settings->device_ephemeral_users_enabled ==
crosapi::mojom::DeviceSettings::OptionalBool::kTrue) {
// Corresponding policy: DeviceEphemeralUsersEnabled=true
return PolicyEffect::kSilenced;
}
#endif
return PolicyEffect::kNone;
}
void SetFirstRunFinished(FirstRunService::FinishedReason reason) {
PrefService* local_state = g_browser_process->local_state();
local_state->SetBoolean(prefs::kFirstRunFinished, true);
base::UmaHistogramEnumeration("ProfilePicker.FirstRun.FinishReason", reason);
}
// Returns whether `prefs::kFirstRunFinished` is true. This implies that the FRE
// should not be opened again and would set if the user saw the FRE and is done
// with it, or if for some other reason (e.g. policy or some other browser
// state) we determine that we should not show it.
bool IsFirstRunMarkedFinishedInPrefs() {
// Can be null in unit tests.
const PrefService* const local_state = g_browser_process->local_state();
return local_state && local_state->GetBoolean(prefs::kFirstRunFinished);
}
} // namespace
// FirstRunService -------------------------------------------------------------
// static
void FirstRunService::RegisterLocalStatePrefs(PrefRegistrySimple* registry) {
registry->RegisterBooleanPref(prefs::kFirstRunFinished, false);
registry->RegisterStringPref(prefs::kFirstRunStudyGroup, "");
}
FirstRunService::FirstRunService(Profile* profile) : profile_(profile) {}
FirstRunService::~FirstRunService() = default;
bool FirstRunService::ShouldOpenFirstRun() const {
return ::ShouldOpenFirstRun(profile_);
}
void FirstRunService::TryMarkFirstRunAlreadyFinished(
base::OnceClosure callback) {
DCHECK(ShouldOpenFirstRun()); // Caller should check.
// The method has multiple exit points, this ensures `callback` gets called.
base::ScopedClosureRunner scoped_closure_runner(std::move(callback));
// If the FRE is already open, it is obviously not finished and we also don't
// want to preemptively mark it completed. Skip all the below, the profile
// picker can handle being called while already shown.
if (ProfilePicker::IsFirstRunOpen()) {
return;
}
auto* identity_manager = IdentityManagerFactory::GetForProfile(profile_);
bool has_set_up_profile =
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// Indicates that the profile was likely migrated from pre-Lacros Ash.
identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSync);
#else
// The Dice FRE focuses on identity and offering the user to sign in. If
// the profile already has an account (e.g. the sentinel file was deleted
// or `--force-first-run` was passed), this ensures we still skip it and
// avoid having to handle too strange states later.
identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSignin);
#endif
if (has_set_up_profile) {
FinishFirstRun(FinishedReason::kProfileAlreadySetUp);
return;
}
auto policy_effect = ComputeDevicePolicyEffect(*profile_);
#if BUILDFLAG(IS_CHROMEOS_LACROS)
switch (policy_effect) {
case PolicyEffect::kDisabled:
if (!chrome::enterprise_util::UserAcceptedAccountManagement(profile_)) {
// Management had to be accepted to create the session. Normally this
// gets set during the FRE (TurnSyncOn flow), but since it is skipped,
// set the flag here.
chrome::enterprise_util::SetUserAcceptedAccountManagement(profile_,
true);
}
break;
case PolicyEffect::kSilenced:
StartSilentSync(scoped_closure_runner.Release());
break;
case PolicyEffect::kNone:
// Do nothing.
break;
}
#endif
if (policy_effect != PolicyEffect::kNone) {
FinishFirstRun(FinishedReason::kSkippedByPolicies);
return;
}
// Fallthrough: let the FRE be shown when the user opens a browser UI for the
// first time.
}
#if BUILDFLAG(IS_CHROMEOS_LACROS)
void FirstRunService::StartSilentSync(base::OnceClosure callback) {
// We should not be able to re-enter here as the FRE should be marked
// already finished.
DCHECK(!silent_sync_enabler_);
auto reset_enabler_callback = base::BindOnce(
&FirstRunService::ClearSilentSyncEnabler, weak_ptr_factory_.GetWeakPtr());
silent_sync_enabler_ = std::make_unique<SilentSyncEnabler>(profile_);
silent_sync_enabler_->StartAttempt(
callback ? std::move(reset_enabler_callback).Then(std::move(callback))
: std::move(reset_enabler_callback));
}
void FirstRunService::ClearSilentSyncEnabler() {
silent_sync_enabler_.reset();
}
#endif
// `resume_task_callback_` should be run to allow the caller to resume what
// they were trying to do before they stopped to show the FRE.
// If the FRE's `status` is not `ProfilePicker::FirstRunExitStatus::kCompleted`,
// that `resume_task_callback_` will be called with `proceed` set to false,
// otherwise it will be called with true.
void FirstRunService::OnFirstRunHasExited(
ProfilePicker::FirstRunExitStatus status) {
if (!resume_task_callback_) {
return;
}
bool proceed = false;
bool should_mark_fre_finished = false;
switch (status) {
case ProfilePicker::FirstRunExitStatus::kCompleted:
proceed = true;
should_mark_fre_finished = true;
break;
#if BUILDFLAG(IS_CHROMEOS_LACROS)
case ProfilePicker::FirstRunExitStatus::kQuitEarly:
#endif
case ProfilePicker::FirstRunExitStatus::kAbortTask:
proceed = false;
should_mark_fre_finished = false;
break;
case ProfilePicker::FirstRunExitStatus::kQuitAtEnd:
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
proceed = kForYouFreCloseShouldProceed.Get();
#endif
should_mark_fre_finished = true;
break;
case ProfilePicker::FirstRunExitStatus::kAbandonedFlow:
proceed = false;
should_mark_fre_finished = true;
break;
}
if (should_mark_fre_finished) {
// The user got to the last step, we can mark the FRE as finished, whether
// we eventually proceed with the original intent or not.
FinishFirstRun(FinishedReason::kFinishedFlow);
}
base::UmaHistogramEnumeration("ProfilePicker.FirstRun.ExitStatus", status);
std::move(resume_task_callback_).Run(proceed);
}
void FirstRunService::FinishFirstRun(FinishedReason reason) {
SetFirstRunFinished(reason);
#if BUILDFLAG(IS_CHROMEOS_LACROS)
absl::optional<ProfileMetrics::ProfileSignedInFlowOutcome> outcome;
switch (reason) {
case FinishedReason::kFinishedFlow:
// No outcome to log, the flow logs it by itself.
break;
case FinishedReason::kProfileAlreadySetUp:
outcome =
ProfileMetrics::ProfileSignedInFlowOutcome::kSkippedAlreadySyncing;
break;
case FinishedReason::kSkippedByPolicies:
outcome = ProfileMetrics::ProfileSignedInFlowOutcome::kSkippedByPolicies;
break;
}
if (outcome.has_value()) {
ProfileMetrics::LogLacrosPrimaryProfileFirstRunOutcome(*outcome);
}
#endif
auto* identity_manager = IdentityManagerFactory::GetForProfile(profile_);
if (identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
// Noting that we expect that the name should already be available, as
// after sign-in, the extended info is fetched and used for the sync
// opt-in screen.
profile_name_resolver_ =
std::make_unique<ProfileNameResolver>(identity_manager);
profile_name_resolver_->RunWithProfileName(base::BindOnce(
&FirstRunService::FinishProfileSetUp, weak_ptr_factory_.GetWeakPtr()));
} else if (reason == FinishedReason::kSkippedByPolicies) {
// TODO(crbug.com/1416511): Try to get a domain name if available.
FinishProfileSetUp(
profiles::GetDefaultNameForNewEnterpriseProfile(kNoHostedDomainFound));
}
}
void FirstRunService::FinishProfileSetUp(std::u16string profile_name) {
DCHECK(IsFirstRunMarkedFinishedInPrefs());
profile_name_resolver_.reset();
DCHECK(!profile_name.empty());
FinalizeNewProfileSetup(profile_, profile_name, /*is_default_name=*/false);
}
void FirstRunService::OpenFirstRunIfNeeded(EntryPoint entry_point,
ResumeTaskCallback callback) {
OnFirstRunHasExited(ProfilePicker::FirstRunExitStatus::kAbortTask);
resume_task_callback_ = std::move(callback);
TryMarkFirstRunAlreadyFinished(
base::BindOnce(&FirstRunService::OpenFirstRunInternal,
weak_ptr_factory_.GetWeakPtr(), entry_point));
}
void FirstRunService::OpenFirstRunInternal(EntryPoint entry_point) {
if (IsFirstRunMarkedFinishedInPrefs()) {
// Opening the First Run is not needed. For example it might have been
// marked finished silently, or is suppressed by policy.
//
// Note that this assumes that the prefs state is the the only part of
// `ShouldOpenFirstRun()` that can change during the service's lifetime.
std::move(resume_task_callback_).Run(/*proceed=*/true);
return;
}
#if BUILDFLAG(IS_CHROMEOS_LACROS)
base::UmaHistogramEnumeration(
"Profile.LacrosPrimaryProfileFirstRunEntryPoint", entry_point);
#endif
base::UmaHistogramEnumeration("ProfilePicker.FirstRun.EntryPoint",
entry_point);
// Note: we call `Show()` even if the FRE might be already open and rely on
// the ProfilePicker to decide what it wants to do with `callback`.
ProfilePicker::Show(ProfilePicker::Params::ForFirstRun(
profile_->GetPath(), base::BindOnce(&FirstRunService::OnFirstRunHasExited,
weak_ptr_factory_.GetWeakPtr())));
}
void FirstRunService::FinishFirstRunWithoutResumeTask() {
if (!resume_task_callback_) {
return;
}
DCHECK(ProfilePicker::IsFirstRunOpen());
OnFirstRunHasExited(ProfilePicker::FirstRunExitStatus::kAbandonedFlow);
ProfilePicker::Hide();
}
// FirstRunServiceFactory ------------------------------------------------------
FirstRunServiceFactory::FirstRunServiceFactory()
: ProfileKeyedServiceFactory(
"FirstRunServiceFactory",
ProfileSelections::Builder()
.WithRegular(ProfileSelection::kOriginalOnly)
.WithGuest(ProfileSelection::kNone)
.WithSystem(ProfileSelection::kNone)
.Build()) {
// Used for checking Sync consent level.
DependsOn(IdentityManagerFactory::GetInstance());
}
FirstRunServiceFactory::~FirstRunServiceFactory() = default;
// static
FirstRunServiceFactory* FirstRunServiceFactory::GetInstance() {
static base::NoDestructor<FirstRunServiceFactory> factory;
return factory.get();
}
// static
FirstRunService* FirstRunServiceFactory::GetForBrowserContext(
content::BrowserContext* context) {
return static_cast<FirstRunService*>(
GetInstance()->GetServiceForBrowserContext(context, /*create=*/true));
}
// static
FirstRunService* FirstRunServiceFactory::GetForBrowserContextIfExists(
content::BrowserContext* context) {
return static_cast<FirstRunService*>(
GetInstance()->GetServiceForBrowserContext(context, /*create=*/false));
}
KeyedService* FirstRunServiceFactory::BuildServiceInstanceFor(
content::BrowserContext* context) const {
Profile* profile = Profile::FromBrowserContext(context);
if (!ShouldOpenFirstRun(profile)) {
return nullptr;
}
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
if (base::FeatureList::IsEnabled(kForYouFreSyntheticTrialRegistration)) {
// We use this point to register for the study as it can give us a good
// counterfactual, before checking the state of the feature itself. The
// service is created on demand so we are in a code path that will require
// the FRE to be shown.
// Besides being suppressed by enterprise policy, if the FRE doesn't run, it
// would be related to handling some corner cases, and should not impact our
// metrics too much.
FirstRunService::JoinFirstRunCohort();
}
if (!base::FeatureList::IsEnabled(kForYouFre)) {
base::UmaHistogramBoolean("ProfilePicker.FirstRun.ServiceCreated", false);
SetFirstRunFinished(
FirstRunService::FinishedReason::kExperimentCounterfactual);
return nullptr;
}
#endif
auto* instance = new FirstRunService(profile);
base::UmaHistogramBoolean("ProfilePicker.FirstRun.ServiceCreated", true);
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// Check if we should turn Sync on from the background and skip the FRE.
// If we don't manage to set it, we will just have to defer silent or visual
// handling of the FRE to when the user attempts to open a browser UI. So
// we don't need to do anything when the attempt finishes.
instance->TryMarkFirstRunAlreadyFinished(base::OnceClosure());
#endif
return instance;
}
bool FirstRunServiceFactory::ServiceIsCreatedWithBrowserContext() const {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
// We want the service to be created early, even if the browser is created in
// the background, so we can check whether we need to enable Sync silently.
return true;
#else
return false;
#endif
}
// Helpers ---------------------------------------------------------------------
bool ShouldOpenFirstRun(Profile* profile) {
return IsFirstRunEligibleProcess() && IsFirstRunEligibleProfile(profile) &&
!IsFirstRunMarkedFinishedInPrefs();
}