// Copyright 2020 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/policy/status_collector/app_info_generator.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/pref_names.h"
#include "components/policy/proto/device_management_backend.pb.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/app_registry_cache.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/app_update.h"
#include "components/session_manager/core/session_manager.h"
#include "url/gurl.h"
namespace em = enterprise_management;
namespace {
bool IsPrimaryAndAffiliated(Profile* profile) {
user_manager::User* user =
ash::ProfileHelper::Get()->GetUserByProfile(profile);
bool is_primary = ash::ProfileHelper::Get()->IsPrimaryProfile(profile);
bool is_affiliated = user && user->IsAffiliated();
if (!is_primary || !is_affiliated) {
VLOG(1) << "The profile for the primary user is not associated with an "
"affiliated user.";
}
return is_primary && is_affiliated;
}
em::AppInfo::Status ExtractStatus(const apps::Readiness readiness) {
switch (readiness) {
case apps::Readiness::kReady:
return em::AppInfo::Status::AppInfo_Status_STATUS_INSTALLED;
case apps::Readiness::kRemoved:
case apps::Readiness::kUninstalledByUser:
case apps::Readiness::kUninstalledByNonUser:
return em::AppInfo::Status::AppInfo_Status_STATUS_UNINSTALLED;
case apps::Readiness::kDisabledByBlocklist:
case apps::Readiness::kDisabledByPolicy:
case apps::Readiness::kDisabledByUser:
case apps::Readiness::kTerminated:
case apps::Readiness::kDisabledByLocalSettings:
return em::AppInfo::Status::AppInfo_Status_STATUS_DISABLED;
case apps::Readiness::kUnknown:
return em::AppInfo::Status::AppInfo_Status_STATUS_UNKNOWN;
}
}
em::AppInfo::AppType ExtractAppType(const apps::AppType app_type) {
switch (app_type) {
case apps::AppType::kArc:
return em::AppInfo::AppType::AppInfo_AppType_TYPE_ARC;
case apps::AppType::kBuiltIn:
return em::AppInfo::AppType::AppInfo_AppType_TYPE_BUILTIN;
case apps::AppType::kCrostini:
return em::AppInfo::AppType::AppInfo_AppType_TYPE_CROSTINI;
case apps::AppType::kPluginVm:
return em::AppInfo::AppType::AppInfo_AppType_TYPE_PLUGINVM;
case apps::AppType::kChromeApp:
case apps::AppType::kStandaloneBrowserChromeApp:
return em::AppInfo::AppType::AppInfo_AppType_TYPE_EXTENSION;
case apps::AppType::kWeb:
case apps::AppType::kSystemWeb:
return em::AppInfo::AppType::AppInfo_AppType_TYPE_WEB;
case apps::AppType::kBorealis:
return em::AppInfo::AppType::AppInfo_AppType_TYPE_BOREALIS;
case apps::AppType::kBruschetta:
return em::AppInfo::AppType::AppInfo_AppType_TYPE_BRUSCHETTA;
case apps::AppType::kStandaloneBrowser:
case apps::AppType::kExtension:
case apps::AppType::kStandaloneBrowserExtension:
case apps::AppType::kRemote:
case apps::AppType::kUnknown:
return em::AppInfo::AppType::AppInfo_AppType_TYPE_UNKNOWN;
}
}
} // namespace
namespace policy {
AppInfoGenerator::AppInfoProvider::AppInfoProvider(Profile* profile)
: activity_storage(profile->GetPrefs(),
prefs::kAppActivityTimes,
/*day_start_offset=*/base::Seconds(0)),
app_service_proxy(*apps::AppServiceProxyFactory::GetForProfile(profile)) {
}
AppInfoGenerator::AppInfoProvider::~AppInfoProvider() = default;
AppInfoGenerator::AppInfoGenerator(
ManagedSessionService* managed_session_service,
base::TimeDelta max_stored_past_activity_interval,
base::Clock* clock)
: max_stored_past_activity_interval_(max_stored_past_activity_interval),
clock_(*clock) {
if (managed_session_service) {
managed_session_observation_.Observe(managed_session_service);
}
}
AppInfoGenerator::AppInstances::AppInstances(const base::Time start_time_)
: start_time(start_time_) {}
AppInfoGenerator::AppInstances::~AppInstances() = default;
AppInfoGenerator::~AppInfoGenerator() {
SetOpenDurationsToClosed(clock_->Now());
}
// static
void AppInfoGenerator::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterDictionaryPref(prefs::kAppActivityTimes);
}
const AppInfoGenerator::Result AppInfoGenerator::Generate() const {
if (!should_report_) {
VLOG(1) << "App usage reporting is not enabled for this user.";
return std::nullopt;
}
if (!provider_) {
VLOG(1) << "No affiliated user session. Returning empty app list.";
return std::nullopt;
}
auto activity_periods = provider_->activity_storage.GetActivityPeriods();
auto activity_compare = [](const em::TimePeriod& time_period1,
const em::TimePeriod& time_period2) {
return time_period1.start_timestamp() < time_period2.start_timestamp();
};
std::vector<em::AppInfo> app_infos;
provider_->app_service_proxy->AppRegistryCache().ForEachApp(
[&app_infos, &activity_periods, &activity_compare,
this](const apps::AppUpdate& update) {
ActivityStorage::Activities& app_activity =
activity_periods[update.AppId()];
std::sort(app_activity.begin(), app_activity.end(), activity_compare);
app_infos.push_back(ConvertToAppInfo(update, app_activity));
});
return app_infos;
}
void AppInfoGenerator::OnReportingChanged(bool should_report) {
if (should_report_ == should_report) {
return;
}
should_report_ = should_report;
if (provider_) {
if (should_report) {
provider_->app_service_proxy->InstanceRegistry().AddObserver(this);
} else {
provider_->app_service_proxy->InstanceRegistry().RemoveObserver(this);
}
}
}
void AppInfoGenerator::OnReportedSuccessfully(const base::Time report_time) {
if (!provider_) {
return;
}
provider_->activity_storage.TrimActivityPeriods(
report_time.InMillisecondsSinceUnixEpoch(),
base::Time::Max().InMillisecondsSinceUnixEpoch());
}
void AppInfoGenerator::OnWillReport() {
if (!provider_ || device_locked_) {
return;
}
SetOpenDurationsToClosed(clock_->Now());
SetIdleDurationsToOpen();
}
void AppInfoGenerator::OnLogin(Profile* profile) {
if (!IsPrimaryAndAffiliated(profile)) {
return;
}
if (!apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile)) {
VLOG(1) << "No apps available. Will not track usage.";
return;
}
provider_ = std::make_unique<AppInfoGenerator::AppInfoProvider>(profile);
provider_->activity_storage.PruneActivityPeriods(
clock_->Now(), max_stored_past_activity_interval_);
if (should_report_) {
provider_->app_service_proxy->InstanceRegistry().AddObserver(this);
}
}
void AppInfoGenerator::OnLogout(Profile* profile) {
if (!IsPrimaryAndAffiliated(profile)) {
return;
}
if (provider_) {
if (should_report_) {
provider_->app_service_proxy->InstanceRegistry().RemoveObserver(this);
}
provider_.reset();
}
}
void AppInfoGenerator::OnLocked() {
device_locked_ = true;
SetOpenDurationsToClosed(clock_->Now());
}
void AppInfoGenerator::OnUnlocked() {
device_locked_ = false;
SetIdleDurationsToOpen();
}
void AppInfoGenerator::OnResumeActive(base::Time suspend_time) {
if (device_locked_) {
return;
}
SetOpenDurationsToClosed(suspend_time);
SetIdleDurationsToOpen();
}
const em::AppInfo AppInfoGenerator::ConvertToAppInfo(
const apps::AppUpdate& update,
const std::vector<em::TimePeriod>& app_activity) const {
em::AppInfo info;
bool is_web_app = (update.AppType() == apps::AppType::kWeb) ||
(update.AppType() == apps::AppType::kSystemWeb);
if (!is_web_app) {
info.set_app_id(update.AppId());
info.set_app_name(update.Name());
} else {
// For web apps, publisher id is the start url.
GURL start_url(update.PublisherId());
DCHECK(start_url.is_valid());
const std::string launch_origin =
start_url.DeprecatedGetOriginAsURL().spec();
info.set_app_id(launch_origin);
info.set_app_name(launch_origin);
}
info.set_status(ExtractStatus(update.Readiness()));
info.set_version(update.Version());
info.set_app_type(ExtractAppType(update.AppType()));
*info.mutable_active_time_periods() = {app_activity.begin(),
app_activity.end()};
return info;
}
void AppInfoGenerator::SetOpenDurationsToClosed(base::Time end_time) {
if (!provider_) {
return;
}
provider_->app_service_proxy->InstanceRegistry().RemoveObserver(this);
for (auto const& app : app_instances_by_id_) {
const std::string& app_id = app.first;
base::Time start_time = app.second.get()->start_time;
provider_->activity_storage.AddActivityPeriod(start_time, end_time, app_id);
}
app_instances_by_id_.clear();
}
void AppInfoGenerator::SetIdleDurationsToOpen() {
if (!provider_) {
return;
}
base::Time start_time = clock_->Now();
provider_->app_service_proxy->InstanceRegistry().ForEachInstance(
[this, start_time](const apps::InstanceUpdate& update) {
if (update.State() & apps::InstanceState::kStarted) {
OpenUsageInterval(update.AppId(), update.InstanceId(), start_time);
}
});
provider_->app_service_proxy->InstanceRegistry().AddObserver(this);
}
void AppInfoGenerator::OpenUsageInterval(
const std::string& app_id,
const base::UnguessableToken& instance_id,
const base::Time start_time) {
if (app_instances_by_id_.count(app_id) == 0) {
app_instances_by_id_[app_id] = std::make_unique<AppInstances>(start_time);
}
app_instances_by_id_[app_id]->running_instances.insert(instance_id);
}
void AppInfoGenerator::CloseUsageInterval(
const std::string& app_id,
const base::UnguessableToken& instance_id,
const base::Time end_time) {
if (app_instances_by_id_.count(app_id)) {
auto& app_instances = app_instances_by_id_[app_id];
app_instances->running_instances.erase(instance_id);
if (app_instances->running_instances.empty()) {
base::Time start_time = app_instances->start_time;
provider_->activity_storage.AddActivityPeriod(start_time, end_time,
app_id);
app_instances_by_id_.erase(app_id);
}
}
}
void AppInfoGenerator::OnInstanceUpdate(const apps::InstanceUpdate& update) {
if (!update.StateChanged()) {
return;
}
apps::InstanceState state = update.State();
const std::string& app_id = update.AppId();
auto instance_id = update.InstanceId();
if (state & apps::InstanceState::kStarted) {
OpenUsageInterval(app_id, instance_id, update.LastUpdatedTime());
} else if (state & apps::InstanceState::kDestroyed) {
CloseUsageInterval(app_id, instance_id, update.LastUpdatedTime());
}
}
void AppInfoGenerator::OnInstanceRegistryWillBeDestroyed(
apps::InstanceRegistry* registry) {
registry->RemoveObserver(this);
}
} // namespace policy