chromium/chrome/browser/ash/crostini/crostini_shared_devices.cc

// Copyright 2022 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/crostini/crostini_shared_devices.h"

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/no_destructor.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/guest_os/guest_os_pref_names.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_keyed_service_factory.h"
#include "chromeos/ash/components/dbus/cicerone/cicerone_client.h"

namespace crostini {

namespace {

class CrostiniSharedDevicesFactory : public ProfileKeyedServiceFactory {
 public:
  static CrostiniSharedDevices* GetForProfile(Profile* profile) {
    return static_cast<CrostiniSharedDevices*>(
        GetInstance()->GetServiceForBrowserContext(profile, true));
  }

  static CrostiniSharedDevicesFactory* GetInstance() {
    static base::NoDestructor<CrostiniSharedDevicesFactory> factory;
    return factory.get();
  }

 private:
  friend class base::NoDestructor<CrostiniSharedDevicesFactory>;

  CrostiniSharedDevicesFactory()
      : ProfileKeyedServiceFactory(
            "CrostiniSharedDevicesService",
            ProfileSelections::Builder()
                .WithRegular(ProfileSelection::kOriginalOnly)
                // TODO(crbug.com/40257657): Check if this service is needed in
                // Guest mode.
                .WithGuest(ProfileSelection::kOriginalOnly)
                // TODO(crbug.com/41488885): Check if this service is needed for
                // Ash Internals.
                .WithAshInternals(ProfileSelection::kOriginalOnly)
                .Build()) {}

  ~CrostiniSharedDevicesFactory() override = default;

  // BrowserContextKeyedServiceFactory:
  KeyedService* BuildServiceInstanceFor(
      content::BrowserContext* context) const override {
    Profile* profile = Profile::FromBrowserContext(context);
    return new CrostiniSharedDevices(profile);
  }
};

}  // namespace

CrostiniSharedDevices* CrostiniSharedDevices::GetForProfile(Profile* profile) {
  return CrostiniSharedDevicesFactory::GetForProfile(profile);
}

CrostiniSharedDevices::CrostiniSharedDevices(Profile* profile)
    : profile_(profile) {
  // There is a risk that after a Chrome crash and restart, we won't recognize
  // an already running container for which we have not yet applied user prefs.
  // In this case, there is little risk as CrostiniRecoveryView should have been
  // shown to tell the user they need to restart their container.
  guest_os::GuestOsSessionTracker::GetForProfile(profile_)
      ->AddContainerStartedObserver(this);
}

CrostiniSharedDevices::~CrostiniSharedDevices() = default;

bool CrostiniSharedDevices::IsVmDeviceShared(guest_os::GuestId container_id,
                                             const std::string& vm_device) {
  const base::Value* shared_devices = GetContainerPrefValue(
      profile_, container_id, guest_os::prefs::kContainerSharedVmDevicesKey);
  if (!shared_devices || !shared_devices->is_dict()) {
    VLOG(2) << "No shared_devices dict for " << container_id;
    return false;
  }
  auto opt_shared = shared_devices->GetDict().FindBool(vm_device);
  return opt_shared.has_value() && opt_shared.value();
}

void CrostiniSharedDevices::SetVmDeviceShared(guest_os::GuestId container_id,
                                              const std::string& vm_device,
                                              bool shared,
                                              ResultCallback callback) {
  const base::Value* prev_shared_devices = guest_os::GetContainerPrefValue(
      profile_, container_id, guest_os::prefs::kContainerSharedVmDevicesKey);
  base::Value::Dict shared_devices;
  if (prev_shared_devices && prev_shared_devices->is_dict()) {
    shared_devices = prev_shared_devices->GetDict().Clone();
  }
  shared_devices.Set(vm_device, shared);

  if (guest_os::GuestOsSessionTracker::GetForProfile(profile_)->IsRunning(
          container_id)) {
    ApplySharingState(std::move(container_id), std::move(shared_devices),
                      std::move(callback));
  } else {
    VLOG(2) << "Not applying yet, container not running " << container_id;
    guest_os::MergeContainerPref(profile_, container_id,
                                 guest_os::prefs::kContainerSharedVmDevicesKey,
                                 std::move(shared_devices));
    std::move(callback).Run(false);
  }
}

void CrostiniSharedDevices::EnsureFactoryBuilt() {
  CrostiniSharedDevicesFactory::GetInstance();
}

void CrostiniSharedDevices::ApplySharingState(
    const guest_os::GuestId container_id,
    base::Value::Dict next_shared_devices,
    ResultCallback callback) {
  if (next_shared_devices.empty()) {
    VLOG(2) << "Nothing to apply";
    return;
  }
  vm_tools::cicerone::UpdateContainerDevicesRequest request;
  request.set_owner_id(CryptohomeIdForProfile(profile_));
  request.set_vm_name(container_id.vm_name);
  request.set_container_name(container_id.container_name);

  auto* updates = request.mutable_updates();
  for (auto it : next_shared_devices) {
    auto opt_shared = it.second.GetIfBool();
    vm_tools::cicerone::VmDeviceAction action =
        opt_shared.has_value() && opt_shared.value()
            ? vm_tools::cicerone::VmDeviceAction::ENABLE
            : vm_tools::cicerone::VmDeviceAction::DISABLE;
    (*updates)[it.first] = action;
  }

  ash::CiceroneClient::Get()->UpdateContainerDevices(
      request,
      base::BindOnce(&CrostiniSharedDevices::OnUpdateContainerDevices,
                     weak_ptr_factory_.GetWeakPtr(), std::move(container_id),
                     std::move(next_shared_devices), std::move(callback)));
}

void CrostiniSharedDevices::OnUpdateContainerDevices(
    const guest_os::GuestId container_id,
    base::Value::Dict next_shared_devices,
    ResultCallback callback,
    std::optional<vm_tools::cicerone::UpdateContainerDevicesResponse>
        response) {
  bool success = true;
  if (!response.has_value()) {
    LOG(ERROR) << "No UpdateContainerDevicesResponse received.";
    success = false;
  } else if (response->status() !=
             vm_tools::cicerone::UpdateContainerDevicesResponse::OK) {
    LOG(ERROR)
        << "UpdateContainerDevices failed, error "
        << vm_tools::cicerone::UpdateContainerDevicesResponse_Status_Name(
               response->status());
    success = false;
  }

  if (success) {
    for (const auto& it : response->results()) {
      const base::Value* prev_shared_devices = guest_os::GetContainerPrefValue(
          profile_, container_id,
          guest_os::prefs::kContainerSharedVmDevicesKey);
      const auto& vm_device = it.first;
      auto result = it.second;
      switch (result) {
        case vm_tools::cicerone::UpdateContainerDevicesResponse::ACTION_FAILED:
          LOG(WARNING) << "trying to restore old pref value for " << it.first;
          if (prev_shared_devices && prev_shared_devices->is_dict()) {
            // Preserve/restore the old pref value.
            auto opt_shared =
                prev_shared_devices->GetDict().FindBool(vm_device);
            if (opt_shared.has_value()) {
              next_shared_devices.Set(vm_device, opt_shared.value());
            }
          } else {
            next_shared_devices.Remove(vm_device);
          }
          break;
        case vm_tools::cicerone::UpdateContainerDevicesResponse::
            NO_SUCH_VM_DEVICE:
          LOG(WARNING) << "VM has no such device " << it.first;
          next_shared_devices.Set(vm_device, false);
          break;
        case vm_tools::cicerone::UpdateContainerDevicesResponse::SUCCESS:
        default:
          VLOG(2) << "applied pref for " << it.first;
          break;
      }
    }
    guest_os::MergeContainerPref(profile_, container_id,
                                 guest_os::prefs::kContainerSharedVmDevicesKey,
                                 std::move(next_shared_devices));
  }
  std::move(callback).Run(success);
}

void CrostiniSharedDevices::OnContainerStarted(
    const guest_os::GuestId& container_id) {
  const base::Value* prev_shared_devices = guest_os::GetContainerPrefValue(
      profile_, container_id, guest_os::prefs::kContainerSharedVmDevicesKey);
  base::Value::Dict shared_devices;
  if (prev_shared_devices && prev_shared_devices->is_dict()) {
    shared_devices = prev_shared_devices->GetDict().Clone();
  }
  VLOG(2) << "Applying device sharing prefs for " << container_id;
  ApplySharingState(std::move(container_id), std::move(shared_devices),
                    base::DoNothing());
}

}  // namespace crostini