// 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 "chromeos/ash/components/report/report_controller.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "chromeos/ash/components/dbus/private_computing/private_computing_client.h"
#include "chromeos/ash/components/dbus/private_computing/private_computing_service.pb.h"
#include "chromeos/ash/components/dbus/session_manager/session_manager_client.h"
#include "chromeos/ash/components/dbus/system_clock/system_clock_sync_observation.h"
#include "chromeos/ash/components/network/network_state.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/report/device_metrics/actives/one_day_impl.h"
#include "chromeos/ash/components/report/device_metrics/actives/twenty_eight_day_impl.h"
#include "chromeos/ash/components/report/device_metrics/churn/cohort_impl.h"
#include "chromeos/ash/components/report/device_metrics/churn/observation_impl.h"
#include "chromeos/ash/components/report/device_metrics/use_case/use_case.h"
#include "chromeos/ash/components/report/prefs/fresnel_pref_names.h"
#include "chromeos/ash/components/report/utils/pref_utils.h"
#include "chromeos/ash/components/report/utils/time_utils.h"
#include "chromeos/ash/components/system/statistics_provider.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/version_info/channel.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
namespace psm_rlwe = private_membership::rlwe;
namespace ash::report {
namespace {
ReportController* g_ash_report_controller = nullptr;
// Amount of time to wait before triggering repeating timer.
constexpr base::TimeDelta kTimeToRepeat = base::Hours(1);
// Maximum time to wait for time sync before not reporting device as active
// in current attempt.
// This corresponds to at least seven TCP retransmissions attempts to
// the remote server used to update the system clock.
constexpr base::TimeDelta kSystemClockSyncWaitTimeout = base::Seconds(45);
// Create helper methods for lambda functions.
base::OnceClosure CreateSavePreservedFileCallback(
PrefService* local_state,
base::WeakPtr<ReportController> controller_weak_ptr) {
return base::BindOnce(
[](PrefService* local_state,
base::WeakPtr<ReportController> controller_weak_ptr) {
PrivateComputingClient::Get()->SaveLastPingDatesStatus(
utils::CreatePreservedFileContents(local_state),
base::BindOnce(
&ReportController::OnSaveLocalStateToPreservedFileComplete,
controller_weak_ptr));
},
local_state, controller_weak_ptr);
}
base::OnceClosure CreateReportObservationCallback(
base::WeakPtr<device_metrics::ObservationImpl> observation_weak_ptr,
base::OnceCallback<void()> save_preserved_file_cb) {
return base::BindOnce(&device_metrics::ObservationImpl::Run,
observation_weak_ptr,
std::move(save_preserved_file_cb));
}
base::OnceClosure CreateReportCohortCallback(
base::WeakPtr<device_metrics::CohortImpl> cohort_weak_ptr,
base::OnceCallback<void()> report_observation_cb) {
return base::BindOnce(&device_metrics::CohortImpl::Run, cohort_weak_ptr,
std::move(report_observation_cb));
}
base::OnceClosure CreateReport28DaCallback(
base::WeakPtr<device_metrics::TwentyEightDayImpl> twenty_eight_day_weak_ptr,
base::OnceCallback<void()> report_cohort_cb) {
return base::BindOnce(&device_metrics::TwentyEightDayImpl::Run,
twenty_eight_day_weak_ptr, std::move(report_cohort_cb));
}
// Record boolean indicating the preserved file has been written successfully.
void RecordPreservedFileWritten(bool is_written) {
base::UmaHistogramBoolean("Ash.Report.PreservedFileWritten", is_written);
}
// Record boolean for whether the device is classified as for testing.
void RecordIsTestDevice(bool is_test) {
base::UmaHistogramBoolean("Ash.Report.IsTestDevice", is_test);
}
// Record boolean for whether the psm stable secret key is read and parsed
// successfully from the VPD file that is loaded to machine statistics.
void RecordIsPsmSecretSet(bool is_set) {
base::UmaHistogramBoolean("Ash.Report.IsPsmSecretSet", is_set);
}
// Record boolean for if the system clock has been synced successfully or not.
void RecordIsSystemClockSynced(bool is_synced) {
base::UmaHistogramBoolean("Ash.Report.IsSystemClockSynced", is_synced);
}
} // namespace
ReportController* ReportController::Get() {
return g_ash_report_controller;
}
// static
void ReportController::RegisterPrefs(PrefRegistrySimple* registry) {
const base::Time unix_epoch = base::Time::UnixEpoch();
registry->RegisterTimePref(
prefs::kDeviceActiveLastKnown1DayActivePingTimestamp, unix_epoch);
registry->RegisterTimePref(
prefs::kDeviceActiveLastKnown28DayActivePingTimestamp, unix_epoch);
registry->RegisterTimePref(
prefs::kDeviceActiveChurnCohortMonthlyPingTimestamp, unix_epoch);
registry->RegisterTimePref(
prefs::kDeviceActiveChurnObservationMonthlyPingTimestamp, unix_epoch);
registry->RegisterIntegerPref(prefs::kDeviceActiveLastKnownChurnActiveStatus,
0);
registry->RegisterBooleanPref(
prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus0, false);
registry->RegisterBooleanPref(
prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus1, false);
registry->RegisterBooleanPref(
prefs::kDeviceActiveLastKnownIsActiveCurrentPeriodMinus2, false);
registry->RegisterDictionaryPref(prefs::kDeviceActive28DayActivePingCache);
}
ReportController::ReportController(
const device_metrics::ChromeDeviceMetadataParameters& chrome_device_params,
PrefService* local_state,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
std::unique_ptr<device_metrics::PsmClientManager> psm_client_manager)
: chrome_device_params_(chrome_device_params),
local_state_(local_state),
url_loader_factory_(url_loader_factory),
report_timer_(std::make_unique<base::RepeatingTimer>()),
network_state_handler_(NetworkHandler::Get()->network_state_handler()),
statistics_provider_(system::StatisticsProvider::GetInstance()),
clock_(base::DefaultClock::GetInstance()),
psm_client_manager_(std::move(psm_client_manager)) {
DCHECK(local_state);
DCHECK(psm_client_manager_.get());
DCHECK(!g_ash_report_controller);
g_ash_report_controller = this;
// Halt if device is a testimage/unknown channel.
if (chrome_device_params.chrome_channel == version_info::Channel::UNKNOWN) {
LOG(ERROR) << "Halt - Client should enter device active reporting logic. "
<< "Unknown and test image channels should not be counted as "
<< "legitimate device counts.";
return;
}
// Wrap with callback from |psm_device_active_secret_| retrieval using
// |SessionManagerClient| DBus.
SessionManagerClient::Get()->GetPsmDeviceActiveSecret(
base::BindOnce(&report::ReportController::OnPsmDeviceActiveSecretFetched,
weak_factory_.GetWeakPtr()));
}
ReportController::~ReportController() {
DCHECK_EQ(this, g_ash_report_controller);
g_ash_report_controller = nullptr;
// Reset all dependency unique_ptr objects of this class that are not needed.
report_timer_.reset();
system_clock_sync_observation_.reset();
// Reset all reporting use cases.
one_day_impl_.reset();
twenty_eight_day_impl_.reset();
cohort_impl_.reset();
observation_impl_.reset();
// Reset parameters that is passed to reporting use cases.
use_case_params_.reset();
}
void ReportController::DefaultNetworkChanged(const NetworkState* network) {
bool was_connected = network_connected_;
network_connected_ = network && network->IsOnline();
if (network_connected_ == was_connected) {
return;
}
if (network_connected_) {
OnNetworkOnline();
} else {
OnNetworkOffline();
}
}
void ReportController::OnShuttingDown() {
network_state_handler_observer_.Reset();
}
void ReportController::OnPsmDeviceActiveSecretFetched(
const std::string& psm_device_active_secret) {
// In order for the device actives to be reported, the psm device active
// secret must be retrieved successfully.
if (psm_device_active_secret.empty()) {
LOG(ERROR) << "PSM device secret is empty and could not be fetched.";
RecordIsPsmSecretSet(false);
return;
}
RecordIsPsmSecretSet(true);
high_entropy_seed_ = psm_device_active_secret;
// Continue when machine statistics are loaded, to avoid blocking.
statistics_provider_->ScheduleOnMachineStatisticsLoaded(
base::BindOnce(&report::ReportController::OnMachineStatisticsLoaded,
weak_factory_.GetWeakPtr()));
}
void ReportController::OnMachineStatisticsLoaded() {
// Block virtual machines and debug builds (dev mode enabled).
if (statistics_provider_->IsRunningOnVm() ||
statistics_provider_->IsCrosDebugMode()) {
LOG(ERROR) << "Terminate - device is running in VM or with cros_debug mode "
"enabled.";
LOG(ERROR) << "VM = " << statistics_provider_->IsRunningOnVm()
<< " and DEBUG = " << statistics_provider_->IsCrosDebugMode();
RecordIsTestDevice(true);
return;
}
RecordIsTestDevice(false);
// Send DBus method to that reads preserved files for missing local state
// prefs.
PrivateComputingClient::Get()->GetLastPingDatesStatus(
base::BindOnce(&ReportController::OnPreservedFileReadComplete,
weak_factory_.GetWeakPtr()));
}
void ReportController::OnPreservedFileReadComplete(
private_computing::GetStatusResponse response) {
if (response.has_error_message()) {
LOG(ERROR) << "Failed to read preserved file. "
<< "Error from DBus: " << response.error_message();
OnReadyToReport();
return;
}
utils::RestoreLocalStateWithPreservedFile(local_state_, response);
OnReadyToReport();
}
void ReportController::OnSaveLocalStateToPreservedFileComplete(
private_computing::SaveStatusResponse response) {
bool write_success = true;
if (response.has_error_message()) {
write_success = false;
LOG(ERROR) << "Failed to write to preserved file. "
<< "Error from DBus: " << response.error_message();
}
RecordPreservedFileWritten(write_success);
// Device is done reporting after writing to preserved file.
is_device_reporting_ = false;
// Reset all reporting use cases.
one_day_impl_.reset();
twenty_eight_day_impl_.reset();
cohort_impl_.reset();
observation_impl_.reset();
// Reset parameters that is passed to reporting use cases.
use_case_params_.reset();
}
bool ReportController::IsDeviceReportingForTesting() const {
return is_device_reporting_;
}
void ReportController::OnReadyToReport() {
// Retry reporting metrics every 1 hour.
report_timer_->Start(FROM_HERE, kTimeToRepeat, this,
&ReportController::ReportingTriggeredByTimer);
// Retry reporting metrics if network comes online.
// DefaultNetworkChanged method is called every time the
// device network properties change.
network_state_handler_observer_.Observe(network_state_handler_.get());
DefaultNetworkChanged(network_state_handler_->DefaultNetwork());
}
void ReportController::ReportingTriggeredByTimer() {
// Return early if the device is not connected to the network
// or if it is already in the middle of reporting.
if (!network_connected_ || is_device_reporting_) {
LOG(ERROR) << "Timer triggered reporting failed. "
<< "Network_connected = " << network_connected_
<< ". Is device reporting = " << is_device_reporting_ << ".";
return;
}
OnNetworkOnline();
}
void ReportController::OnNetworkOnline() {
// Async wait for the system clock to synchronize on network connection.
system_clock_sync_observation_ =
SystemClockSyncObservation::WaitForSystemClockSync(
SystemClockClient::Get(), kSystemClockSyncWaitTimeout,
base::BindOnce(&ReportController::OnSystemClockSyncResult,
weak_factory_.GetWeakPtr()));
}
void ReportController::OnNetworkOffline() {
// TODO: Evaluate if we should cancel callbacks here.
}
void ReportController::OnSystemClockSyncResult(bool system_clock_synchronized) {
RecordIsSystemClockSynced(system_clock_synchronized);
if (!system_clock_synchronized) {
LOG(ERROR) << "System clock failed to be synchronized.";
return;
}
StartReport();
}
void ReportController::StartReport() {
DCHECK(local_state_);
DCHECK(psm_client_manager_.get());
// Get new adjusted timestamp from GMT to Pacific Time.
active_ts_ = utils::ConvertGmtToPt(clock_);
// Create instances of use cases and parameters.
use_case_params_ = std::make_unique<device_metrics::UseCaseParameters>(
active_ts_, chrome_device_params_, url_loader_factory_,
high_entropy_seed_, local_state_, psm_client_manager_.get());
one_day_impl_ =
std::make_unique<device_metrics::OneDayImpl>(use_case_params_.get());
twenty_eight_day_impl_ = std::make_unique<device_metrics::TwentyEightDayImpl>(
use_case_params_.get());
cohort_impl_ =
std::make_unique<device_metrics::CohortImpl>(use_case_params_.get());
observation_impl_ =
std::make_unique<device_metrics::ObservationImpl>(use_case_params_.get());
// Create callbacks to report use cases in a specific order, and also a
// callback that updates the preserved file using the latest local state.
// Note that the order of the use case callbacks is important.
// Contact hirthanan@ or qianwan@ before making changes here.
base::OnceClosure save_preserved_file_cb =
CreateSavePreservedFileCallback(local_state_, weak_factory_.GetWeakPtr());
base::OnceClosure report_observation_cb = CreateReportObservationCallback(
observation_impl_->GetWeakPtr(), std::move(save_preserved_file_cb));
base::OnceClosure report_cohort_cb = CreateReportCohortCallback(
cohort_impl_->GetWeakPtr(), std::move(report_observation_cb));
base::OnceClosure report_28da_cb = CreateReport28DaCallback(
twenty_eight_day_impl_->GetWeakPtr(), std::move(report_cohort_cb));
// Trigger sequential execution of callbacks by running first use case.
is_device_reporting_ = true;
one_day_impl_->Run(std::move(report_28da_cb));
}
} // namespace ash::report