blob: 779005ffe03981bf25497a5c6349d904bda8aae9 [file] [log] [blame]
// 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.
#include "chrome/browser/ash/growth/campaigns_manager_client_impl.h"
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/root_window_controller.h"
#include "ash/shelf/hotseat_widget.h"
#include "ash/shelf/shelf_app_button.h"
#include "ash/shelf/shelf_view.h"
#include "ash/shell.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/version.h"
#include "chrome/browser/ash/growth/campaigns_manager_session.h"
#include "chrome/browser/ash/growth/install_web_app_action_performer.h"
#include "chrome/browser/ash/growth/metrics.h"
#include "chrome/browser/ash/growth/open_url_action_performer.h"
#include "chrome/browser/ash/growth/show_notification_action_performer.h"
#include "chrome/browser/ash/growth/show_nudge_action_performer.h"
#include "chrome/browser/ash/growth/update_user_pref_action_performer.h"
#include "chrome/browser/ash/login/demo_mode/demo_components.h"
#include "chrome/browser/ash/login/demo_mode/demo_mode_dimensions.h"
#include "chrome/browser/ash/login/demo_mode/demo_session.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part.h"
#include "chrome/browser/feature_engagement/tracker_factory.h"
#include "chrome/browser/metrics/chrome_metrics_service_accessor.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chromeos/ash/components/demo_mode/utils/demo_session_utils.h"
#include "chromeos/ash/components/growth/campaigns_logger.h"
#include "chromeos/ash/components/growth/campaigns_manager.h"
#include "chromeos/ash/components/growth/campaigns_utils.h"
#include "chromeos/ash/components/growth/growth_metrics.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/component_updater/ash/component_manager_ash.h"
#include "components/feature_engagement/public/feature_constants.h"
#include "components/feature_engagement/public/tracker.h"
#include "components/language/core/browser/pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/variations/service/variations_service.h"
#include "components/variations/synthetic_trials.h"
#include "ui/display/screen.h"
namespace {
inline constexpr char kCampaignComponentName[] = "growth-campaigns";
// A util function to add the `kGrowthCampaignsEventNamePrefix`.
std::string AddEventPrefix(std::string_view event) {
return base::StrCat({growth::GetGrowthCampaignsEventNamePrefix(), event});
}
Profile* GetProfile() {
return ProfileManager::GetActiveUserProfile();
}
} // namespace
CampaignsManagerClientImpl::CampaignsManagerClientImpl() {
// `show_nudge_performer_observation_` is used in `campaigns_manager_` ctor,
// so it needs to be initialized first.
campaigns_manager_ = std::make_unique<growth::CampaignsManager>(
/*client=*/this, g_browser_process->local_state());
}
CampaignsManagerClientImpl::~CampaignsManagerClientImpl() = default;
void CampaignsManagerClientImpl::LoadCampaignsComponent(
growth::CampaignComponentLoadedCallback callback) {
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(ash::switches::kGrowthCampaignsPath)) {
const auto path =
command_line->GetSwitchValuePath(ash::switches::kGrowthCampaignsPath);
CAMPAIGNS_LOG(DEBUG) << "Switch `kGrowthCampaignsPath` is set. Load "
"campaigns component from file "
<< path;
std::move(callback).Run(base::FilePath(path));
return;
}
// Loads campaigns component.
auto component_manager_ash =
g_browser_process->platform_part()->component_manager_ash();
CHECK(component_manager_ash);
component_manager_ash->Load(
kCampaignComponentName,
component_updater::ComponentManagerAsh::MountPolicy::kMount,
component_updater::ComponentManagerAsh::UpdatePolicy::kDontForce,
base::BindOnce(&CampaignsManagerClientImpl::OnComponentDownloaded,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void CampaignsManagerClientImpl::AddOnTrackerInitializedCallback(
growth::OnTrackerInitializedCallback callback) {
auto* tracker =
feature_engagement::TrackerFactory::GetForBrowserContext(GetProfile());
if (!tracker) {
CAMPAIGNS_LOG(ERROR) << "Feature Engagement tracer is not available";
std::move(callback).Run(false);
}
tracker->AddOnInitializedCallback(
base::BindOnce(&CampaignsManagerClientImpl::OnTrackerInitialized,
weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
bool CampaignsManagerClientImpl::IsDeviceInDemoMode() const {
return ash::demo_mode::IsDeviceInDemoMode();
}
bool CampaignsManagerClientImpl::IsCloudGamingDevice() const {
return ash::demo_mode::IsCloudGamingDevice();
}
bool CampaignsManagerClientImpl::IsFeatureAwareDevice() const {
return ash::demo_mode::IsFeatureAwareDevice();
}
bool CampaignsManagerClientImpl::IsAppIconOnShelf(
const std::string& app_id) const {
auto* shelf = ash::Shell::GetPrimaryRootWindowController()->shelf();
const bool is_shelf_visible =
shelf && (shelf->GetVisibilityState() ==
ash::ShelfVisibilityState::SHELF_VISIBLE ||
(shelf->GetVisibilityState() ==
ash::ShelfVisibilityState::SHELF_AUTO_HIDE &&
shelf->GetAutoHideState() ==
ash::ShelfAutoHideState::SHELF_AUTO_HIDE_SHOWN));
// Shelf is always considered hidden when in tablet mode, but the Hotseat can
// still be expanded.
const bool is_tablet_mode = display::Screen::Get()->InTabletMode();
if (!is_shelf_visible && !is_tablet_mode) {
growth::RecordCampaignsManagerError(
growth::CampaignsManagerError::kShelfInvisibleAtMatching);
CAMPAIGNS_LOG(ERROR) << "Matching hotseat state when shelf is not visible.";
return false;
}
auto* hotseat_widget = shelf->hotseat_widget();
const bool is_hotseat_visible =
hotseat_widget && hotseat_widget->state() != ash::HotseatState::kNone &&
hotseat_widget->state() != ash::HotseatState::kHidden;
if (!is_hotseat_visible) {
growth::RecordCampaignsManagerError(
growth::CampaignsManagerError::kHotseatInvisibleAtMatching);
CAMPAIGNS_LOG(ERROR)
<< "Matching hotseat state when hotseat is not visible.";
return false;
}
auto* shelf_view = hotseat_widget->GetShelfView();
if (!shelf_view) {
growth::RecordCampaignsManagerError(
growth::CampaignsManagerError::kShelfViewNotAvailableAtMatching);
CAMPAIGNS_LOG(ERROR) << "Matching hotseat state when hotseat is available "
"but shelf_view is not available.";
return false;
}
if (!shelf_view->GetShelfAppButton(ash::ShelfID(app_id))) {
growth::RecordCampaignsManagerError(
growth::CampaignsManagerError::kHotseatAppIconNotPresent);
CAMPAIGNS_LOG(ERROR) << "App icon is not on shelf.";
return false;
}
return true;
}
const std::string& CampaignsManagerClientImpl::GetApplicationLocale() const {
// User selected locale, then resolved using
// `l10n_util::CheckAndResolveLocale` to a platform locale.
// For example: `en-IN` will be resolved to `en-GB`.
return g_browser_process->GetApplicationLocale();
}
const std::string& CampaignsManagerClientImpl::GetUserLocale() const {
// The locale as selected by the user, such as "en-IN". This is different
// from `GetApplication` locale which is actually platform locale that
// resolved using `l10n_util::CheckAndResolveLocale`.
return GetProfile()->GetPrefs()->GetString(
language::prefs::kApplicationLocale);
}
const std::string CampaignsManagerClientImpl::GetCountryCode() const {
return g_browser_process->variations_service()->GetStoredPermanentCountry();
}
const base::Version& CampaignsManagerClientImpl::GetDemoModeAppVersion() const {
auto* demo_session = ash::DemoSession::Get();
if (!demo_session) {
// When campaigns are loaded and fetched in `DemoLoginController`,
// `DemoSession` is not available yet. In this case, we will return empty
// version so campaigns that are targeting a specific app version won't be
// matched.
static const base::NoDestructor<base::Version> empty_version;
return *empty_version;
}
const auto& version = demo_session->components()->app_component_version();
if (!version.has_value()) {
growth::RecordCampaignsManagerError(
growth::CampaignsManagerError::kDemoModeAppVersionUnavailable);
static const base::NoDestructor<base::Version> empty_version;
return *empty_version;
}
return version.value();
}
growth::ActionMap CampaignsManagerClientImpl::GetCampaignsActions() {
growth::ActionMap action_map;
action_map.emplace(
make_pair(growth::ActionType::kInstallWebApp,
std::make_unique<InstallWebAppActionPerformer>()));
action_map.emplace(make_pair(growth::ActionType::kOpenUrl,
std::make_unique<OpenUrlActionPerformer>()));
std::unique_ptr<ShowNudgeActionPerformer> show_nudge_performer =
std::make_unique<ShowNudgeActionPerformer>();
show_nudge_performer_observation_.Observe(show_nudge_performer.get());
action_map.emplace(make_pair(growth::ActionType::kShowNudge,
std::move(show_nudge_performer)));
std::unique_ptr<ShowNotificationActionPerformer> show_notification_performer =
std::make_unique<ShowNotificationActionPerformer>();
show_notification_performer_observation_.Observe(
show_notification_performer.get());
action_map.emplace(make_pair(growth::ActionType::kShowNotification,
std::move(show_notification_performer)));
action_map.emplace(
make_pair(growth::ActionType::kUpdateUserPref,
std::make_unique<UpdateUserPrefActionPerformer>()));
return action_map;
}
void CampaignsManagerClientImpl::RegisterSyntheticFieldTrial(
const std::string& trial_name,
const std::string& group_name) const {
CAMPAIGNS_LOG(DEBUG) << "Register synthetic field trial: trial_name: "
<< trial_name << " group_name: " << group_name;
ChromeMetricsServiceAccessor::RegisterSyntheticFieldTrial(
trial_name, group_name,
variations::SyntheticTrialAnnotationMode::kCurrentLog);
}
void CampaignsManagerClientImpl::RecordEvent(const std::string& event_name,
bool trigger_campaigns) {
auto* tracker =
feature_engagement::TrackerFactory::GetForBrowserContext(GetProfile());
if (!tracker || !tracker->IsInitialized()) {
CAMPAIGNS_LOG(ERROR) << "Feature Engagement tracer is not available";
growth::RecordCampaignsManagerError(
growth::CampaignsManagerError::kTrackerNotAvailableInSession);
return;
}
CAMPAIGNS_LOG(DEBUG) << "Record event: " << event_name
<< " Trigger Campaigns: "
<< growth::ToString(trigger_campaigns);
tracker->NotifyEvent(AddEventPrefix(event_name));
if (!trigger_campaigns) {
return;
}
if (auto* session = CampaignsManagerSession::Get()) {
session->MaybeTriggerCampaignsOnEvent(event_name);
}
}
void CampaignsManagerClientImpl::ClearConfig(
const std::map<std::string, std::string>& params) {
auto* tracker =
feature_engagement::TrackerFactory::GetForBrowserContext(GetProfile());
if (!tracker || !tracker->IsInitialized()) {
CAMPAIGNS_LOG(ERROR) << "Feature Engagement tracer is not available";
growth::RecordCampaignsManagerError(
growth::CampaignsManagerError::kTrackerNotAvailableInSession);
return;
}
for (const auto& param : params) {
CAMPAIGNS_LOG(DEBUG) << "Clear config: " << param.second;
}
UpdateConfig(params);
tracker->ClearEventData(feature_engagement::kIPHGrowthFramework);
}
bool CampaignsManagerClientImpl::WouldTriggerHelpUI(
const std::map<std::string, std::string>& params) {
auto* tracker =
feature_engagement::TrackerFactory::GetForBrowserContext(GetProfile());
if (!tracker || !tracker->IsInitialized()) {
CAMPAIGNS_LOG(ERROR) << "Feature Engagement tracer is not available";
growth::RecordCampaignsManagerError(
growth::CampaignsManagerError::kTrackerNotAvailableInSession);
return false;
}
UpdateConfig(params);
return tracker->WouldTriggerHelpUI(feature_engagement::kIPHGrowthFramework);
}
signin::IdentityManager* CampaignsManagerClientImpl::GetIdentityManager()
const {
return IdentityManagerFactory::GetForProfile(GetProfile());
}
void CampaignsManagerClientImpl::OnReadyToLogImpression(
int campaign_id,
std::optional<int> group_id,
bool should_log_cros_events) {
// Records impression UMA metrics.
// TODO: b/348495965 - Verify group metrics when ready.
RecordImpression(campaign_id, should_log_cros_events);
RecordImpressionEvents(campaign_id, group_id);
}
void CampaignsManagerClientImpl::RecordImpressionEvents(
int campaign_id,
std::optional<int> group_id) {
campaigns_manager_->RecordEvent(GetEventName(
growth::CampaignEvent::kImpression, base::NumberToString(campaign_id)));
if (group_id) {
campaigns_manager_->RecordEvent(
GetEventName(growth::CampaignEvent::kGroupImpression,
base::NumberToString(group_id.value())));
}
}
void CampaignsManagerClientImpl::OnDismissed(int campaign_id,
std::optional<int> group_id,
bool should_mark_dismissed,
bool should_log_cros_events) {
// Records dismissal UMA metrics.
// TODO: b/348495965 - Verify group metrics when ready.
RecordDismissed(campaign_id, should_log_cros_events);
if (!should_mark_dismissed) {
return;
}
RecordDismissalEvents(campaign_id, group_id);
}
void CampaignsManagerClientImpl::OnButtonPressed(int campaign_id,
std::optional<int> group_id,
CampaignButtonId button_id,
bool should_mark_dismissed,
bool should_log_cros_events) {
// TODO: b/348495965 - Verify group metrics when ready.
RecordButtonPressed(campaign_id, button_id, should_log_cros_events);
if (!should_mark_dismissed) {
return;
}
// Notify `kDismissed` event to the Feature Engagement framework. This event
// will be stored and could be used later.
switch (button_id) {
case CampaignButtonId::kPrimary:
case CampaignButtonId::kSecondary:
case CampaignButtonId::kClose:
// Primary, Secondary and close button press will treated as user
// dismissal.
RecordDismissalEvents(campaign_id, group_id);
break;
case CampaignButtonId::kOthers:
break;
}
}
void CampaignsManagerClientImpl::OnComponentDownloaded(
growth::CampaignComponentLoadedCallback loaded_callback,
component_updater::ComponentManagerAsh::Error error,
const base::FilePath& path) {
if (error != component_updater::ComponentManagerAsh::Error::NONE) {
// TODO - b/365582608: Add error metrics.
CAMPAIGNS_LOG(ERROR) << "Failed to download campaigns component. Error: "
<< static_cast<int>(error);
std::move(loaded_callback).Run(std::nullopt);
return;
}
std::move(loaded_callback).Run(path);
}
void CampaignsManagerClientImpl::OnTrackerInitialized(
growth::OnTrackerInitializedCallback callback,
bool init_success) {
std::move(callback).Run(init_success);
}
void CampaignsManagerClientImpl::UpdateConfig(
const std::map<std::string, std::string>& params) {
auto* tracker =
feature_engagement::TrackerFactory::GetForBrowserContext(GetProfile());
if (!tracker || !tracker->IsInitialized()) {
CAMPAIGNS_LOG(ERROR) << "Feature Engagement tracer is not available";
growth::RecordCampaignsManagerError(
growth::CampaignsManagerError::kTrackerNotAvailableInSession);
return;
}
config_provider_.SetConfig(params);
tracker->UpdateConfig(feature_engagement::kIPHGrowthFramework,
&config_provider_);
}
void CampaignsManagerClientImpl::RecordDismissalEvents(
int campaign_id,
std::optional<int> group_id) {
campaigns_manager_->RecordEvent(GetEventName(
growth::CampaignEvent::kDismissed, base::NumberToString(campaign_id)));
if (group_id) {
campaigns_manager_->RecordEvent(
GetEventName(growth::CampaignEvent::kGroupDismissed,
base::NumberToString(group_id.value())));
}
}