chromium/chrome/browser/ui/webui/help/version_updater_chromeos.cc

// Copyright 2012 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/ui/webui/help/version_updater_chromeos.h"

#include <cmath>
#include <memory>
#include <optional>
#include <string>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/ash/login/startup_utils.h"
#include "chrome/browser/ash/login/wizard_controller.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/browser_process.h"
#include "chrome/browser/enterprise/browser_management/management_service_factory.h"
#include "chrome/browser/ui/webui/help/help_utils_chromeos.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/dbus/update_engine/update_engine_client.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_state.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/network/network_type_pattern.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "chromeos/ash/components/settings/cros_settings_names.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "components/policy/core/common/management/management_service.h"
#include "content/public/browser/web_contents.h"
#include "third_party/cros_system_api/dbus/service_constants.h"
#include "ui/base/l10n/l10n_util.h"

namespace {

using ::ash::OwnerSettingsServiceAsh;
using ::ash::OwnerSettingsServiceAshFactory;
using ::ash::UpdateEngineClient;

// Network status in the context of device update.
enum NetworkStatus {
  // It's allowed to use current network for update.
  NETWORK_STATUS_ALLOWED = 0,
  // It's disallowed to use current network for update.
  NETWORK_STATUS_DISALLOWED,
  // Device is in offline state.
  NETWORK_STATUS_OFFLINE
};

const bool kDefaultAutoUpdateDisabled = false;

NetworkStatus GetNetworkStatus(bool interactive,
                               const ash::NetworkState* network,
                               bool metered) {
  if (!network || !network->IsConnectedState())  // Offline state.
    return NETWORK_STATUS_OFFLINE;

  if (metered &&
      !help_utils_chromeos::IsUpdateOverCellularAllowed(interactive)) {
    return NETWORK_STATUS_DISALLOWED;
  }
  return NETWORK_STATUS_ALLOWED;
}

// Returns true if auto-update is disabled by the system administrator.
bool IsAutoUpdateDisabled() {
  bool update_disabled = kDefaultAutoUpdateDisabled;
  ash::CrosSettings* settings = ash::CrosSettings::Get();
  if (!settings)
    return update_disabled;
  const base::Value* update_disabled_value =
      settings->GetPref(ash::kUpdateDisabled);
  if (update_disabled_value) {
    CHECK(update_disabled_value->is_bool());
    update_disabled = update_disabled_value->GetBool();
  }
  return update_disabled;
}

std::u16string GetConnectionTypeAsUTF16(const ash::NetworkState* network,
                                        bool metered) {
  const std::string type = network->type();
  if (ash::NetworkTypePattern::WiFi().MatchesType(type)) {
    if (metered)
      return l10n_util::GetStringUTF16(IDS_NETWORK_TYPE_METERED_WIFI);
    return l10n_util::GetStringUTF16(IDS_NETWORK_TYPE_WIFI);
  }
  if (ash::NetworkTypePattern::Ethernet().MatchesType(type))
    return l10n_util::GetStringUTF16(IDS_NETWORK_TYPE_ETHERNET);
  if (ash::NetworkTypePattern::Mobile().MatchesType(type))
    return l10n_util::GetStringUTF16(IDS_NETWORK_TYPE_MOBILE_DATA);
  if (ash::NetworkTypePattern::VPN().MatchesType(type))
    return l10n_util::GetStringUTF16(IDS_NETWORK_TYPE_VPN);
  NOTREACHED_IN_MIGRATION();
  return std::u16string();
}

// Returns whether an update is allowed. If not, it calls the callback with
// the appropriate status. |interactive| indicates whether the user is actively
// checking for updates.
bool EnsureCanUpdate(bool interactive,
                     const VersionUpdater::StatusCallback& callback) {
  if (IsAutoUpdateDisabled()) {
    callback.Run(VersionUpdater::DISABLED_BY_ADMIN, 0, false, false,
                 std::string(), 0,
                 l10n_util::GetStringUTF16(IDS_UPGRADE_DISABLED_BY_POLICY));
    return false;
  }

  ash::NetworkStateHandler* network_state_handler =
      ash::NetworkHandler::Get()->network_state_handler();
  const ash::NetworkState* network = network_state_handler->DefaultNetwork();
  const bool metered = network_state_handler->default_network_is_metered();
  // Don't allow an update if we're currently offline or connected
  // to a network for which updates are disallowed.
  NetworkStatus status = GetNetworkStatus(interactive, network, metered);
  if (status == NETWORK_STATUS_OFFLINE) {
    callback.Run(VersionUpdater::FAILED_OFFLINE, 0, false, false, std::string(),
                 0, l10n_util::GetStringUTF16(IDS_UPGRADE_OFFLINE));
    return false;
  } else if (status == NETWORK_STATUS_DISALLOWED) {
    std::u16string message = l10n_util::GetStringFUTF16(
        IDS_UPGRADE_DISALLOWED, GetConnectionTypeAsUTF16(network, metered));
    callback.Run(VersionUpdater::FAILED_CONNECTION_TYPE_DISALLOWED, 0, false,
                 false, std::string(), 0, message);
    return false;
  }

  return true;
}

}  // namespace

std::unique_ptr<VersionUpdater> VersionUpdater::Create(
    content::WebContents* web_contents) {
  return base::WrapUnique(new VersionUpdaterCros(web_contents));
}

void VersionUpdaterCros::GetUpdateStatus(StatusCallback callback) {
  callback_ = std::move(callback);

  // User is not actively checking for updates.
  if (!EnsureCanUpdate(false /* interactive */, callback_))
    return;

  UpdateEngineClient* update_engine_client = UpdateEngineClient::Get();
  if (!update_engine_client->HasObserver(this))
    update_engine_client->AddObserver(this);

  this->UpdateStatusChanged(update_engine_client->GetLastStatus());
}

void VersionUpdaterCros::ApplyDeferredUpdate() {
  UpdateEngineClient* update_engine_client = UpdateEngineClient::Get();

  DCHECK(update_engine_client->GetLastStatus().current_operation() ==
         update_engine::Operation::UPDATED_BUT_DEFERRED);

  update_engine_client->ApplyDeferredUpdate(/*shutdown_after_update=*/false,
                                            base::DoNothing());
}

void VersionUpdaterCros::CheckForUpdate(StatusCallback callback,
                                        PromoteCallback) {
  callback_ = std::move(callback);

  // User is actively checking for updates.
  if (!EnsureCanUpdate(true /* interactive */, callback_))
    return;

  UpdateEngineClient* update_engine_client = UpdateEngineClient::Get();
  if (!update_engine_client->HasObserver(this))
    update_engine_client->AddObserver(this);

  if (update_engine_client->GetLastStatus().current_operation() !=
      update_engine::Operation::IDLE) {
    check_for_update_when_idle_ = true;
    return;
  }
  check_for_update_when_idle_ = false;

  // Make sure that libcros is loaded and OOBE is complete.
  if (!ash::WizardController::default_controller() ||
      ash::StartupUtils::IsDeviceRegistered()) {
    update_engine_client->RequestUpdateCheck(base::BindOnce(
        &VersionUpdaterCros::OnUpdateCheck, weak_ptr_factory_.GetWeakPtr()));
  }
}

void VersionUpdaterCros::SetChannel(const std::string& channel,
                                    bool is_powerwash_allowed) {
  OwnerSettingsServiceAsh* service =
      context_
          ? OwnerSettingsServiceAshFactory::GetInstance()->GetForBrowserContext(
                context_)
          : nullptr;
  // For local owner set the field in the policy blob.
  if (service)
    service->SetString(ash::kReleaseChannel, channel);
  UpdateEngineClient::Get()->SetChannel(channel, is_powerwash_allowed);
}

void VersionUpdaterCros::SetUpdateOverCellularOneTimePermission(
    StatusCallback callback,
    const std::string& update_version,
    int64_t update_size) {
  callback_ = std::move(callback);
  UpdateEngineClient::Get()->SetUpdateOverCellularOneTimePermission(
      update_version, update_size,
      base::BindOnce(
          &VersionUpdaterCros::OnSetUpdateOverCellularOneTimePermission,
          weak_ptr_factory_.GetWeakPtr()));
}

void VersionUpdaterCros::OnSetUpdateOverCellularOneTimePermission(
    bool success) {
  if (success) {
    // One time permission is set successfully, so we can proceed to update.
    CheckForUpdate(callback_, VersionUpdater::PromoteCallback());
  } else {
    // TODO(crbug.com/40612027): invoke callback to signal about page to
    // show appropriate error message.
    LOG(ERROR) << "Error setting update over cellular one time permission.";
    callback_.Run(VersionUpdater::FAILED, 0, false, false, std::string(), 0,
                  std::u16string());
  }
}

void VersionUpdaterCros::GetChannel(bool get_current_channel,
                                    ChannelCallback cb) {
  // Request the channel information. Bind to a weak_ptr bound method rather
  // than passing |cb| directly so that |cb| does not outlive |this|.
  UpdateEngineClient::Get()->GetChannel(
      get_current_channel,
      base::BindOnce(&VersionUpdaterCros::OnGetChannel,
                     weak_ptr_factory_.GetWeakPtr(), std::move(cb)));
}

void VersionUpdaterCros::OnGetChannel(ChannelCallback cb,
                                      const std::string& current_channel) {
  std::move(cb).Run(current_channel);
}

void VersionUpdaterCros::GetEolInfo(EolInfoCallback cb) {
  // Request the EolInfo. Bind to a weak_ptr bound method rather than passing
  // |cb| directly so that |cb| does not outlive |this|.
  UpdateEngineClient::Get()->GetEolInfo(
      base::BindOnce(&VersionUpdaterCros::OnGetEolInfo,
                     weak_ptr_factory_.GetWeakPtr(), std::move(cb)));
}

void VersionUpdaterCros::OnGetEolInfo(EolInfoCallback cb,
                                      UpdateEngineClient::EolInfo eol_info) {
  std::move(cb).Run(std::move(eol_info));
}

void VersionUpdaterCros::ToggleFeature(const std::string& feature,
                                       bool enable) {
  UpdateEngineClient::Get()->ToggleFeature(feature, enable);
}

void VersionUpdaterCros::IsFeatureEnabled(const std::string& feature,
                                          IsFeatureEnabledCallback callback) {
  UpdateEngineClient::Get()->IsFeatureEnabled(
      feature,
      base::BindOnce(&VersionUpdaterCros::OnIsFeatureEnabled,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void VersionUpdaterCros::OnIsFeatureEnabled(IsFeatureEnabledCallback callback,
                                            std::optional<bool> enabled) {
  std::move(callback).Run(std::move(enabled));
}

bool VersionUpdaterCros::IsManagedAutoUpdateEnabled() {
  return !IsAutoUpdateDisabled();
}

VersionUpdaterCros::VersionUpdaterCros(content::WebContents* web_contents)
    : context_(web_contents ? web_contents->GetBrowserContext() : nullptr),
      last_operation_(update_engine::Operation::IDLE),
      check_for_update_when_idle_(false) {}

VersionUpdaterCros::~VersionUpdaterCros() {
  UpdateEngineClient::Get()->RemoveObserver(this);
}

void VersionUpdaterCros::UpdateStatusChanged(
    const update_engine::StatusResult& status) {
  Status my_status = UPDATED;
  int progress = 0;
  std::string version = status.new_version();
  int64_t size = status.new_size();
  std::u16string message;

  // If the status change is for an installation, this means that DLCs are being
  // installed and has nothing to with the OS. Ignore this status change.
  if (status.is_install())
    return;

  // If the updater is currently idle, just show the last operation (unless it
  // was previously checking for an update -- in that case, the system is
  // up to date now).  See http://crbug.com/120063 for details.
  update_engine::Operation operation_to_show = status.current_operation();
  if (status.current_operation() == update_engine::Operation::IDLE &&
      last_operation_ != update_engine::Operation::CHECKING_FOR_UPDATE) {
    operation_to_show = last_operation_;
  }

  switch (operation_to_show) {
    case update_engine::Operation::IDLE:
    case update_engine::Operation::DISABLED:
    case update_engine::Operation::ERROR:
    case update_engine::Operation::REPORTING_ERROR_EVENT:
    case update_engine::Operation::ATTEMPTING_ROLLBACK:
    case update_engine::Operation::CLEANUP_PREVIOUS_UPDATE:
      // Update engine reports errors for some conditions that shouldn't
      // actually be displayed as errors to users so leave the status as
      // UPDATED. However for some specific errors use the specific FAILED
      // statuses. Last attempt error remains when update engine state is
      // idle.

      if (status.last_attempt_error() ==
          static_cast<int32_t>(
              update_engine::ErrorCode::kOmahaUpdateIgnoredPerPolicy)) {
        if (policy::ManagementServiceFactory::GetForPlatform()->IsManaged()) {
          my_status = DISABLED_BY_ADMIN;
        } else {
          // Handle the special case where after a consumer rollback,
          // updating to the previously installed version just rolledback from
          // is disallowed.
          // TODO(b/277962165) Update the platform side to expose a more
          // specific error code for this case.
          my_status = UPDATE_TO_ROLLBACK_VERSION_DISALLOWED;
        }
      } else if (status.last_attempt_error() ==
                 static_cast<int32_t>(
                     update_engine::ErrorCode::kOmahaErrorInHTTPResponse)) {
        my_status = FAILED_HTTP;
      } else if (status.last_attempt_error() ==
                 static_cast<int32_t>(
                     update_engine::ErrorCode::kDownloadTransferError)) {
        my_status = FAILED_DOWNLOAD;
      }
      break;
    case update_engine::Operation::CHECKING_FOR_UPDATE:
      my_status = CHECKING;
      break;
    case update_engine::Operation::DOWNLOADING:
      progress = static_cast<int>(round(status.progress() * 100));
      [[fallthrough]];
    case update_engine::Operation::UPDATE_AVAILABLE:
      my_status = UPDATING;
      break;
    case update_engine::Operation::NEED_PERMISSION_TO_UPDATE:
      my_status = NEED_PERMISSION_TO_UPDATE;
      break;
    case update_engine::Operation::VERIFYING:
    case update_engine::Operation::FINALIZING:
      // Once the download is finished, keep the progress at 100; it shouldn't
      // go down while the status is the same.
      progress = 100;
      my_status = UPDATING;
      break;
    case update_engine::Operation::UPDATED_NEED_REBOOT:
      my_status = NEARLY_UPDATED;
      break;
    case update_engine::Operation::UPDATED_BUT_DEFERRED:
      my_status = DEFERRED;
      break;
    default:
      NOTREACHED_IN_MIGRATION();
  }

  // If the current auto update is non-interactive and will be deferred, ignore
  // update status change and show UPDATED instead. The NEARLY_UPDATED or
  // DEFERRED status will still be shown, because user may need to interact with
  // UI to apply the update and reboot the device.
  if (my_status != NEARLY_UPDATED && my_status != DEFERRED &&
      !status.is_interactive() && status.will_defer_update()) {
    my_status = UPDATED;
    progress = 0;
  }

  callback_.Run(my_status, progress, status.is_enterprise_rollback(),
                status.will_powerwash_after_reboot(), version, size, message);
  last_operation_ = status.current_operation();

  if (check_for_update_when_idle_ &&
      status.current_operation() == update_engine::Operation::IDLE) {
    CheckForUpdate(callback_, VersionUpdater::PromoteCallback());
  }
}

void VersionUpdaterCros::OnUpdateCheck(
    UpdateEngineClient::UpdateCheckResult result) {
  // If version updating is not implemented, this binary is the most up-to-date
  // possible with respect to automatic updating.
  if (result == UpdateEngineClient::UPDATE_RESULT_NOTIMPLEMENTED)
    callback_.Run(UPDATED, 0, false, false, std::string(), 0, std::u16string());
}