blob: a1662c125be65dd2d3aaa5f4ba77bcb4eb060d49 [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/webui/intro/intro_handler.h"
#include "base/cancelable_callback.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/scoped_observation.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/policy/chrome_browser_policy_connector.h"
#include "chrome/browser/policy/profile_policy_connector.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_avatar_icon_util.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/ui/managed_ui.h"
#include "chrome/browser/ui/webui/intro/intro_ui.h"
#include "chrome/grit/chromium_strings.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/google_chrome_strings.h"
#include "components/policy/core/common/cloud/cloud_policy_store.h"
#include "components/policy/core/common/cloud/machine_level_user_cloud_policy_manager.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "content/public/browser/web_ui.h"
#include "google_apis/gaia/core_account_id.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
namespace {
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
policy::CloudPolicyStore* GetCloudPolicyStore() {
auto* machine_level_manager = g_browser_process->browser_policy_connector()
->machine_level_user_cloud_policy_manager();
return machine_level_manager ? machine_level_manager->core()->store()
: nullptr;
}
// PolicyStoreState will make it easier to handle all the states in a single
// callback.
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class PolicyStoreState {
// Store was already loaded when we attached the observer.
kSuccessAlreadyLoaded = 0,
// Store has been loaded before the time delay ends.
kSuccess = 1,
// Store did not load in time.
kTimeout = 2,
// OnStoreError called.
kStoreError = 3,
// Store is null for a managed device.
kStoreNull = 4,
kMaxValue = kStoreNull,
};
void RecordDisclaimerMetrics(PolicyStoreState state,
base::TimeTicks start_time) {
base::UmaHistogramEnumeration("ProfilePicker.FirstRun.PolicyStoreState",
state);
if (state == PolicyStoreState::kSuccess) {
base::UmaHistogramTimes(
"ProfilePicker.FirstRun.OrganizationAvailableTiming",
/*sample*/ base::TimeTicks::Now() - start_time);
}
}
class PolicyStoreObserver : public policy::CloudPolicyStore::Observer {
public:
explicit PolicyStoreObserver(
base::OnceCallback<void(std::string)> handle_policy_store_change)
: handle_policy_store_change_(std::move(handle_policy_store_change)) {
DCHECK(handle_policy_store_change_);
start_time_ = base::TimeTicks::Now();
// Update the disclaimer directly if the policy store is already loaded.
auto* policy_store = GetCloudPolicyStore();
// GetCloudPolicyStore will return nullptr for managed devices with
// non-branded builds because the machine level cloud policy manager will be
// null while the device is still managed. In that case, we show a generic
// disclaimer.
if (!policy_store) {
// The device is not enrolled in Chrome Browser Cloud Management
HandlePolicyStoreStatusChange(PolicyStoreState::kStoreNull);
return;
}
if (policy_store->is_initialized()) {
HandlePolicyStoreStatusChange(PolicyStoreState::kSuccessAlreadyLoaded);
return;
}
policy_store_observation_.Observe(policy_store);
// 2.5 is the chrome logo animation time which is 1.5s plus the maximum
// delay of 1s that we are willing to wait for.
constexpr auto kMaximumEnterpriseDisclaimerDelay = base::Seconds(2.5);
on_organization_fetch_timeout_.Reset(
base::BindOnce(&PolicyStoreObserver::OnOrganizationFetchTimeout,
base::Unretained(this)));
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, on_organization_fetch_timeout_.callback(),
kMaximumEnterpriseDisclaimerDelay);
}
~PolicyStoreObserver() override = default;
// policy::CloudPolicyStore::Observer:
void OnStoreLoaded(policy::CloudPolicyStore* store) override {
on_organization_fetch_timeout_.Cancel();
policy_store_observation_.Reset();
HandlePolicyStoreStatusChange(PolicyStoreState::kSuccess);
}
void OnStoreError(policy::CloudPolicyStore* store) override {
on_organization_fetch_timeout_.Cancel();
policy_store_observation_.Reset();
HandlePolicyStoreStatusChange(PolicyStoreState::kStoreError);
}
// Called when the delay specified for the store to load has passed. We show a
// generic disclaimer when this happens.
void OnOrganizationFetchTimeout() {
policy_store_observation_.Reset();
HandlePolicyStoreStatusChange(PolicyStoreState::kTimeout);
}
void HandlePolicyStoreStatusChange(PolicyStoreState state) {
RecordDisclaimerMetrics(state, start_time_);
std::string managed_device_disclaimer;
if (state == PolicyStoreState::kSuccess ||
state == PolicyStoreState::kSuccessAlreadyLoaded) {
absl::optional<std::string> manager = chrome::GetDeviceManagerIdentity();
managed_device_disclaimer =
manager->empty()
? l10n_util::GetStringUTF8(IDS_FRE_MANAGED_DESCRIPTION)
: l10n_util::GetStringFUTF8(IDS_FRE_MANAGED_BY_DESCRIPTION,
base::UTF8ToUTF16(*manager));
} else {
managed_device_disclaimer =
l10n_util::GetStringUTF8(IDS_FRE_MANAGED_DESCRIPTION);
}
std::move(handle_policy_store_change_).Run(managed_device_disclaimer);
}
private:
base::ScopedObservation<policy::CloudPolicyStore,
policy::CloudPolicyStore::Observer>
policy_store_observation_{this};
base::OnceCallback<void(std::string)> handle_policy_store_change_;
base::CancelableOnceCallback<void()> on_organization_fetch_timeout_;
base::TimeTicks start_time_;
};
#endif
#if BUILDFLAG(IS_CHROMEOS_LACROS)
class IdentityManagerObserver : public signin::IdentityManager::Observer {
public:
explicit IdentityManagerObserver(
base::RepeatingCallback<void()> handle_identity_manager_change,
signin::IdentityManager& identity_manager)
: account_id_(identity_manager
.GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
.account_id) {
DCHECK(handle_identity_manager_change);
identity_manager_observation_.Observe(&identity_manager);
handle_identity_manager_change_ = std::move(handle_identity_manager_change);
}
void OnExtendedAccountInfoUpdated(const AccountInfo& account_info) override {
if (account_info.account_id != account_id_) {
return;
}
handle_identity_manager_change_.Run();
if (!account_info.account_image.IsEmpty() && account_info.IsValid()) {
identity_manager_observation_.Reset();
}
}
private:
base::ScopedObservation<signin::IdentityManager,
signin::IdentityManager::Observer>
identity_manager_observation_{this};
base::RepeatingCallback<void()> handle_identity_manager_change_;
const CoreAccountId account_id_;
};
std::string GetPictureUrl(content::WebUI& web_ui,
const AccountInfo& account_info) {
const int avatar_size = 100;
const int avatar_icon_size = avatar_size * web_ui.GetDeviceScaleFactor();
DCHECK(!account_info.IsEmpty());
gfx::Image icon = account_info.account_image.IsEmpty()
? ui::ResourceBundle::GetSharedInstance().GetImageNamed(
profiles::GetPlaceholderAvatarIconResourceID())
: account_info.account_image;
return webui::GetBitmapDataUrl(
profiles::GetSizedAvatarIcon(icon, avatar_icon_size, avatar_icon_size)
.AsBitmap());
}
std::string GetLacrosIntroWelcomeTitle(const AccountInfo& account_info) {
const bool has_given_name = !account_info.given_name.empty();
base::UmaHistogramBoolean("Profile.LacrosFre.WelcomeHasGivenName",
has_given_name);
return has_given_name ? l10n_util::GetStringFUTF8(
IDS_PRIMARY_PROFILE_FIRST_RUN_TITLE,
base::UTF8ToUTF16(account_info.given_name))
: l10n_util::GetStringUTF8(
IDS_PRIMARY_PROFILE_FIRST_RUN_NO_NAME_TITLE);
}
std::string GetLacrosIntroManagementDisclaimer(
const Profile& profile,
const std::string& account_domain_name) {
// TODO(crbug.com/1416511): Fix logic mismatch in device/account management
// between Lacros and DICE.
const bool is_managed_account =
profile.GetProfilePolicyConnector()->IsManaged();
if (!is_managed_account || account_domain_name == kNoHostedDomainFound) {
return std::string();
}
return l10n_util::GetStringFUTF8(
IDS_PRIMARY_PROFILE_FIRST_RUN_SESSION_MANAGED_BY_DESCRIPTION,
base::UTF8ToUTF16(account_domain_name));
}
base::Value::Dict GetProfileInfoValue(content::WebUI& web_ui) {
base::Value::Dict dict;
auto* profile = Profile::FromWebUI(&web_ui);
const auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
const CoreAccountInfo core_account_info =
identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin);
AccountInfo account_info =
identity_manager->FindExtendedAccountInfoByAccountId(
core_account_info.account_id);
if (account_info.email.empty()) {
return dict;
}
dict.Set("pictureUrl", GetPictureUrl(web_ui, account_info));
dict.Set("managementDisclaimer", GetLacrosIntroManagementDisclaimer(
*profile, account_info.hosted_domain));
dict.Set("title", GetLacrosIntroWelcomeTitle(account_info));
dict.Set("subtitle",
l10n_util::GetStringFUTF8(IDS_PRIMARY_PROFILE_FIRST_RUN_SUBTITLE,
base::UTF8ToUTF16(account_info.email)));
return dict;
}
#endif
} // namespace
IntroHandler::IntroHandler(base::RepeatingCallback<void(IntroChoice)> callback,
bool is_device_managed)
: callback_(std::move(callback)), is_device_managed_(is_device_managed) {
DCHECK(callback_);
}
IntroHandler::~IntroHandler() = default;
void IntroHandler::RegisterMessages() {
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
web_ui()->RegisterMessageCallback(
"continueWithoutAccount",
base::BindRepeating(&IntroHandler::HandleContinueWithoutAccount,
base::Unretained(this)));
#endif
web_ui()->RegisterMessageCallback(
"continueWithAccount",
base::BindRepeating(&IntroHandler::HandleContinueWithAccount,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"initializeMainView",
base::BindRepeating(&IntroHandler::HandleInitializeMainView,
base::Unretained(this)));
}
void IntroHandler::OnJavascriptAllowed() {
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
if (!is_device_managed_) {
return;
}
policy_store_observer_ = std::make_unique<PolicyStoreObserver>(base::BindOnce(
&IntroHandler::FireManagedDisclaimerUpdate, base::Unretained(this)));
#endif
#if BUILDFLAG(IS_CHROMEOS_LACROS)
identity_manager_observer_ = std::make_unique<IdentityManagerObserver>(
base::BindRepeating(&IntroHandler::UpdateProfileInfo,
base::Unretained(this)),
*IdentityManagerFactory::GetForProfile(Profile::FromWebUI(web_ui())));
UpdateProfileInfo();
#endif
}
void IntroHandler::HandleContinueWithAccount(const base::Value::List& args) {
CHECK(args.empty());
callback_.Run(IntroChoice::kContinueWithAccount);
}
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
void IntroHandler::HandleContinueWithoutAccount(const base::Value::List& args) {
CHECK(args.empty());
callback_.Run(IntroChoice::kContinueWithoutAccount);
}
void IntroHandler::ResetIntroButtons() {
if (IsJavascriptAllowed()) {
FireWebUIListener("reset-intro-buttons");
}
}
#endif
void IntroHandler::HandleInitializeMainView(const base::Value::List& args) {
CHECK(args.empty());
AllowJavascript();
}
#if BUILDFLAG(IS_CHROMEOS_LACROS)
void IntroHandler::UpdateProfileInfo() {
DCHECK(IsJavascriptAllowed());
FireWebUIListener("on-profile-info-changed", GetProfileInfoValue(*web_ui()));
}
#endif
void IntroHandler::FireManagedDisclaimerUpdate(std::string disclaimer) {
DCHECK(is_device_managed_);
if (IsJavascriptAllowed()) {
FireWebUIListener("managed-device-disclaimer-updated",
base::Value(std::move(disclaimer)));
}
}