#include "ash/metrics/feature_discovery_duration_reporter_impl.h"
#include "ash/public/cpp/feature_discovery_metric_util.h"
#include "ash/shell.h"
#include "base/containers/contains.h"
#include "base/json/values_util.h"
#include "base/metrics/histogram_functions.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "ui/display/screen.h"
namespace ash {
namespace {
constexpr base::TimeDelta kTimeMetricsMin = base::Seconds(1);
constexpr base::TimeDelta kTimeMetricsMax = base::Days(7);
constexpr int kTimeMetricsBucketCount = 100;
constexpr char kObservedFeatures[] = "FeatureDiscoveryReporterObservedFeatures";
constexpr char kCumulatedDuration[] = "cumulative_duration";
constexpr char kIsObservationFinished[] = "is_observation_finished";
constexpr char kActivatedInTablet[] = "activated_in_tablet";
void ReportFeatureDiscoveryDuration(const char* histogram,
const base::TimeDelta& duration) {
base::UmaHistogramCustomTimes(histogram, duration, kTimeMetricsMin,
kTimeMetricsMax, kTimeMetricsBucketCount);
}
const feature_discovery::TrackableFeatureInfo& FindMappedFeatureInfo(
feature_discovery::TrackableFeature feature) {
auto iter =
std::ranges::find(feature_discovery::kTrackableFeatureArray, feature,
&feature_discovery::TrackableFeatureInfo::feature);
DCHECK(feature_discovery::kTrackableFeatureArray.cend() != iter);
return *iter;
}
const char* FindMappedName(feature_discovery::TrackableFeature feature) {
return FindMappedFeatureInfo(feature).name;
}
const char* CalculateHistogram(feature_discovery::TrackableFeature feature,
std::optional<bool> in_tablet) {
const feature_discovery::TrackableFeatureInfo& info =
FindMappedFeatureInfo(feature);
if (!info.split_by_tablet_mode)
return info.histogram;
DCHECK(in_tablet);
return *in_tablet ? info.histogram_tablet : info.histogram_clamshell;
}
}
FeatureDiscoveryDurationReporterImpl::FeatureDiscoveryDurationReporterImpl(
SessionController* session_controller) {
session_controller_observation_.Observe(session_controller);
}
FeatureDiscoveryDurationReporterImpl::~FeatureDiscoveryDurationReporterImpl() {
SetActive(false);
}
void FeatureDiscoveryDurationReporterImpl::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
registry->RegisterDictionaryPref(kObservedFeatures);
}
void FeatureDiscoveryDurationReporterImpl::MaybeActivateObservation(
feature_discovery::TrackableFeature feature) {
if (!is_active())
return;
const base::Value::Dict& observed_features =
active_pref_service_->GetDict(kObservedFeatures);
const feature_discovery::TrackableFeatureInfo& info =
FindMappedFeatureInfo(feature);
const char* feature_name = info.name;
if (observed_features.Find(feature_name))
return;
base::Value::Dict observed_feature_data;
observed_feature_data.Set(kCumulatedDuration,
base::TimeDeltaToValue(base::TimeDelta()));
observed_feature_data.Set(kIsObservationFinished, false);
if (info.split_by_tablet_mode) {
observed_feature_data.Set(kActivatedInTablet,
display::Screen::Get()->InTabletMode());
}
ScopedDictPrefUpdate update(active_pref_service_, kObservedFeatures);
update->Set(feature_name, std::move(observed_feature_data));
DCHECK(!base::Contains(active_time_recordings_, feature));
active_time_recordings_.emplace(feature, base::TimeTicks::Now());
}
void FeatureDiscoveryDurationReporterImpl::MaybeFinishObservation(
feature_discovery::TrackableFeature feature) {
if (!is_active())
return;
auto iter = active_time_recordings_.find(feature);
if (iter == active_time_recordings_.end())
return;
const base::Value::Dict& observed_features =
active_pref_service_->GetDict(kObservedFeatures);
const char* const feature_name = FindMappedName(feature);
const base::Value::Dict* feature_pref_data =
observed_features.Find(feature_name)->GetIfDict();
DCHECK(feature_pref_data);
const std::optional<base::TimeDelta> accumulated_duration =
base::ValueToTimeDelta(feature_pref_data->Find(kCumulatedDuration));
DCHECK(accumulated_duration);
bool skip_report = false;
std::optional<bool> activated_in_tablet;
if (FindMappedFeatureInfo(feature).split_by_tablet_mode) {
activated_in_tablet = feature_pref_data->FindBool(kActivatedInTablet);
DCHECK(activated_in_tablet);
if (!activated_in_tablet) {
LOG(ERROR) << "Cannot find the tablet mode state under which the feature "
"observation starts for "
<< FindMappedName(feature);
skip_report = true;
}
}
if (!skip_report) {
ReportFeatureDiscoveryDuration(
CalculateHistogram(feature, activated_in_tablet),
*accumulated_duration + base::TimeTicks::Now() - iter->second);
}
ScopedDictPrefUpdate update(active_pref_service_, kObservedFeatures);
base::Value::Dict* mutable_feature_pref_data = update->FindDict(feature_name);
mutable_feature_pref_data->Remove(kCumulatedDuration);
mutable_feature_pref_data->Set(kIsObservationFinished, true);
mutable_feature_pref_data->Remove(kActivatedInTablet);
active_time_recordings_.erase(iter);
}
void FeatureDiscoveryDurationReporterImpl::AddObserver(
ReporterObserver* observer) {
observers_.AddObserver(observer);
}
void FeatureDiscoveryDurationReporterImpl::RemoveObserver(
ReporterObserver* observer) {
observers_.RemoveObserver(observer);
}
void FeatureDiscoveryDurationReporterImpl::SetActive(bool active) {
if (active == is_active() || !active_pref_service_)
return;
if (!active) {
Deactivate();
return;
}
Activate();
}
void FeatureDiscoveryDurationReporterImpl::Activate() {
if (!Shell::Get()->session_controller()->IsUserPrimary())
return;
DCHECK(active_time_recordings_.empty());
DCHECK(!is_active_);
DCHECK(active_pref_service_);
is_active_ = true;
const base::Value::Dict& observed_features =
active_pref_service_->GetDict(kObservedFeatures);
const base::Value::Dict& immutable_observed_features_dict = observed_features;
for (const auto& feature_info : feature_discovery::kTrackableFeatureArray) {
const base::Value* feature_data =
immutable_observed_features_dict.Find(feature_info.name);
if (!feature_data)
continue;
std::optional<bool> is_finished =
feature_data->GetDict().FindBool(kIsObservationFinished);
DCHECK(is_finished);
if (*is_finished)
continue;
active_time_recordings_.emplace(feature_info.feature,
base::TimeTicks::Now());
}
for (ReporterObserver& observer : observers_)
observer.OnReporterActivated();
}
void FeatureDiscoveryDurationReporterImpl::Deactivate() {
if (!active_time_recordings_.empty()) {
CHECK(active_pref_service_);
ScopedDictPrefUpdate update(active_pref_service_, kObservedFeatures);
base::Value::Dict& mutable_observed_features_dict = update.Get();
for (const auto& name_timestamp_pair : active_time_recordings_) {
const char* feature_name = FindMappedName(name_timestamp_pair.first);
base::Value* feature_data =
mutable_observed_features_dict.Find(feature_name);
DCHECK(feature_data);
base::Value::Dict& mutable_data_dict = feature_data->GetDict();
const base::Value* cumulated_duration_value =
mutable_data_dict.Find(kCumulatedDuration);
DCHECK(cumulated_duration_value);
std::optional<base::TimeDelta> cumulated_duration =
base::ValueToTimeDelta(cumulated_duration_value);
DCHECK(cumulated_duration);
mutable_data_dict.Set(
kCumulatedDuration,
base::TimeDeltaToValue(*cumulated_duration + base::TimeTicks::Now() -
name_timestamp_pair.second));
}
active_time_recordings_.clear();
}
is_active_ = false;
}
void FeatureDiscoveryDurationReporterImpl::OnSessionStateChanged(
session_manager::SessionState state) {
SetActive(state == session_manager::SessionState::ACTIVE);
}
void FeatureDiscoveryDurationReporterImpl::OnActiveUserPrefServiceChanged(
PrefService* pref_service) {
if (is_active())
SetActive(false);
active_pref_service_ = pref_service;
SetActive(Shell::Get()->session_controller()->GetSessionState() ==
session_manager::SessionState::ACTIVE);
}
}