// Copyright 2024 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/extended_updates/extended_updates_controller.h"
#include <memory>
#include "ash/constants/ash_features.h"
#include "ash/shell.h"
#include "ash/system/extended_updates/extended_updates_metrics.h"
#include "ash/system/model/system_tray_model.h"
#include "ash/system/model/update_model.h"
#include "base/callback_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/logging.h"
#include "base/time/default_clock.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/arc/arc_util.h"
#include "chrome/browser/ash/extended_updates/extended_updates_notification.h"
#include "chrome/browser/ash/ownership/owner_settings_service_ash.h"
#include "chrome/browser/ash/ownership/owner_settings_service_ash_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/ash/components/dbus/update_engine/update_engine_client.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "chromeos/ash/components/settings/cros_settings_names.h"
#include "components/ownership/owner_settings_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"
namespace ash {
namespace {
ExtendedUpdatesController* instance_ = nullptr;
// Returns true if the EOL params satisfy opt-in eligibility.
bool CheckEolParams(const ExtendedUpdatesController::Params& params) {
// Valid date range is between extended date and eol date.
// Extended date is expected to be before eol date.
// Also, not eligible if opt-in is not required.
return !params.eol_passed && params.extended_date_passed &&
params.opt_in_required;
}
// Returns true if the user could have apps but doesn't have any Android apps.
bool HasNoAndroidApps(content::BrowserContext* context) {
Profile* profile = Profile::FromBrowserContext(context);
if (!apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile)) {
// Likely incognito profile, which is not applicable here.
return false;
}
if (!arc::IsArcPlayStoreEnabledForProfile(profile)) {
// Play store turned off.
return true;
}
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
auto& registry = proxy->AppRegistryCache();
if (!registry.IsAppTypeInitialized(apps::AppType::kArc)) {
// If ARC app type hasn't been initialized by now, there are no ARC apps.
return true;
}
bool has_arc_app = false;
registry.ForEachApp([&has_arc_app](const apps::AppUpdate& update) {
if (!has_arc_app && update.AppType() == apps::AppType::kArc &&
update.Readiness() == apps::Readiness::kReady) {
has_arc_app = true;
}
});
return !has_arc_app;
}
} // namespace
ExtendedUpdatesController* ExtendedUpdatesController::Get() {
if (!instance_) {
instance_ = new ExtendedUpdatesController();
}
return instance_;
}
void ExtendedUpdatesController::ResetInstanceForTesting() {
if (instance_) {
delete instance_;
instance_ = nullptr;
}
}
void ExtendedUpdatesController::
RecordEntryPointEventForSettingsSetUpButtonShown() {
RecordExtendedUpdatesEntryPointEvent(
ExtendedUpdatesEntryPointEvent::kSettingsSetUpButtonShown);
}
void ExtendedUpdatesController::
RecordEntryPointEventForSettingsSetUpButtonClicked() {
RecordExtendedUpdatesEntryPointEvent(
ExtendedUpdatesEntryPointEvent::kSettingsSetUpButtonClicked);
}
base::CallbackListSubscription
ExtendedUpdatesController::SubscribeToDeviceSettingsChanges(
base::RepeatingClosure callback) {
if (CrosSettings::IsInitialized()) {
return CrosSettings::Get()->AddSettingsObserver(
kDeviceExtendedAutoUpdateEnabled, std::move(callback));
}
return base::CallbackListSubscription();
}
ExtendedUpdatesController::ExtendedUpdatesController()
: clock_(base::DefaultClock::GetInstance()) {
SubscribeToDeviceSettingsChanges();
}
ExtendedUpdatesController::~ExtendedUpdatesController() = default;
ExtendedUpdatesController* ExtendedUpdatesController::SetInstanceForTesting(
ExtendedUpdatesController* controller) {
auto* old_instance = instance_;
instance_ = controller;
return old_instance;
}
bool ExtendedUpdatesController::IsOptInEligible(
content::BrowserContext* context,
const Params& params) {
if (!CheckEolParams(params)) {
return false;
}
return IsOptInEligible(context);
}
bool ExtendedUpdatesController::IsOptInEligible(
content::BrowserContext* context) {
auto* owner_settings =
OwnerSettingsServiceAshFactory::GetForBrowserContext(context);
return HasOptInAbility(owner_settings);
}
bool ExtendedUpdatesController::IsOptedIn() {
bool value;
if (CrosSettings::Get()->GetBoolean(kDeviceExtendedAutoUpdateEnabled,
&value)) {
return value;
}
return false;
}
bool ExtendedUpdatesController::OptIn(content::BrowserContext* context) {
auto* owner_settings =
OwnerSettingsServiceAshFactory::GetForBrowserContext(context);
if (!HasOptInAbility(owner_settings)) {
return false;
}
return owner_settings->SetBoolean(kDeviceExtendedAutoUpdateEnabled, true);
}
void ExtendedUpdatesController::OnEolInfo(
content::BrowserContext* context,
const UpdateEngineClient::EolInfo& eol_info) {
if (!context || eol_info.eol_date.is_null() ||
eol_info.extended_date.is_null()) {
return;
}
const base::Time now = clock_->Now();
Params params{
.eol_passed = eol_info.eol_date <= now,
.extended_date_passed = eol_info.extended_date <= now,
.opt_in_required = eol_info.extended_opt_in_required,
};
if (!CheckEolParams(params)) {
return;
}
auto* owner_settings =
OwnerSettingsServiceAshFactory::GetForBrowserContext(context);
if (!owner_settings) {
// In some sessions OwnerSettingsService may be completely uninitialized,
// for example Guest Mode.
LOG(WARNING) << "OwnerSettingsService is uninitialized for the profile."
" Will not notify about extended updates";
return;
}
// This function is called upon login, so owner settings may not have finished
// loading yet. Defer decision to show notification until then.
owner_settings->IsOwnerAsync(base::IgnoreArgs<bool>(
base::BindOnce(&ExtendedUpdatesController::OnOwnershipDetermined,
weak_factory_.GetWeakPtr(), context->GetWeakPtr())));
}
void ExtendedUpdatesController::SetClockForTesting(base::Clock* clock) {
clock_ = clock;
}
void ExtendedUpdatesController::OnOwnershipDetermined(
base::WeakPtr<content::BrowserContext> context) {
if (!context || !IsOptInEligible(context.get())) {
return;
}
if (auto* system_tray_model = Shell::Get()->system_tray_model()) {
system_tray_model->SetShowExtendedUpdatesNotice(true);
SubscribeToDeviceSettingsChanges();
}
if (ShouldShowNotification(context.get())) {
ShowNotification(context.get());
}
}
// TODO(b/333767804): Show notification again if extended updates date changed.
bool ExtendedUpdatesController::ShouldShowNotification(
content::BrowserContext* context) {
if (!IsOptInEligible(context) || !HasNoAndroidApps(context)) {
return false;
}
Profile* profile = Profile::FromBrowserContext(context);
if (ExtendedUpdatesNotification::IsNotificationDismissed(profile)) {
return false;
}
return true;
}
void ExtendedUpdatesController::ShowNotification(
content::BrowserContext* context) {
ExtendedUpdatesNotification::Show(Profile::FromBrowserContext(context));
}
void ExtendedUpdatesController::SubscribeToDeviceSettingsChanges() {
if (!settings_change_subscription_) {
settings_change_subscription_ = SubscribeToDeviceSettingsChanges(
base::BindRepeating(&ExtendedUpdatesController::OnDeviceSettingsChanged,
weak_factory_.GetWeakPtr()));
}
}
void ExtendedUpdatesController::OnDeviceSettingsChanged() {
if (IsOptedIn()) {
if (auto* system_tray_model = Shell::Get()->system_tray_model()) {
system_tray_model->SetShowExtendedUpdatesNotice(false);
}
}
}
bool ExtendedUpdatesController::HasOptInAbility(
ownership::OwnerSettingsService* owner_settings) {
// Only owner user can opt in.
// By extension, only unmanaged devices can opt in.
if (!owner_settings || !owner_settings->IsOwner()) {
return false;
}
// Check feature enablement after other checks to reduce noise due to how
// finch experiment is recorded.
if (!ash::features::IsExtendedUpdatesOptInFeatureEnabled()) {
return false;
}
// Only eligible if not already opted in.
return !IsOptedIn();
}
} // namespace ash