chromium/chrome/browser/ash/extended_updates/extended_updates_controller.cc

// 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