chromium/chromeos/ash/components/dbus/fwupd/fwupd_client.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/dbus/fwupd/fwupd_client.h"

#include <cstdint>
#include <memory>
#include <optional>
#include <utility>

#include "ash/constants/ash_features.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/no_destructor.h"
#include "base/values.h"
#include "chromeos/ash/components/dbus/fwupd/dbus_constants.h"
#include "chromeos/ash/components/dbus/fwupd/fake_fwupd_client.h"
#include "chromeos/ash/components/dbus/fwupd/fwupd_properties_dbus.h"
#include "chromeos/ash/components/dbus/fwupd/fwupd_request.h"
#include "components/device_event_log/device_event_log.h"
#include "dbus/bus.h"
#include "dbus/message.h"
#include "dbus/object_proxy.h"
#include "url/gurl.h"

namespace ash {

namespace {

// This enum should match the UpdatePriority enum here:
// ash/webui/firmware_update_ui/mojom/firmware_update.mojom
enum UpdatePriority { kLow, kMedium, kHigh, kCritical };

// Global singleton instance. Always set.
FwupdClient* g_instance = nullptr;

// Global singleton for fake instance. Only set when a InitializeFake is used.
// If not null, matches g_instance.
FakeFwupdClient* g_fake_instance = nullptr;

const char kCabFileExtension[] = ".cab";
const int kSha256Length = 64;

// "1" is the bitflag for an internal device. Defined here:
// https://github.com/fwupd/fwupd/blob/main/libfwupd/fwupd-enums.h
const uint64_t kInternalDeviceFlag = 1;
// "100000000"(9th bit) is the bit release flag for a trusted report.
// Defined here: https://github.com/fwupd/fwupd/blob/main/libfwupd/fwupd-enums.h
const uint64_t kTrustedReportsReleaseFlag = 1llu << 8;
// "10000"(5th bit) is the fwupd feature flag to allow interactive requests.
// Defined here: https://github.com/fwupd/fwupd/blob/main/libfwupd/fwupd-enums.h
const uint64_t kRequestsFeatureFlag = 1llu << 4;

// Dict key for the IsInternal device flag.
const char kIsInternalKey[] = "IsInternal";
// Dict key for the HasTrustedReport release flag.
const char kHasTrustedReportKey[] = "HasTrustedReport";

// String to FwupdDbusResult conversion
// Consistent with
// https://github.com/fwupd/fwupd/blob/988f27fd96c5334089ec5daf9c4b2a34f5c6943a/libfwupd/fwupd-error.c#L26
FwupdDbusResult GetFwupdDbusResult(const std::string& error_name) {
  if (error_name == std::string(kFwupdErrorName_Internal)) {
    return FwupdDbusResult::kInternalError;
  } else if (error_name == std::string(kFwupdErrorName_VersionNewer)) {
    return FwupdDbusResult::kVersionNewerError;
  } else if (error_name == std::string(kFwupdErrorName_VersionSame)) {
    return FwupdDbusResult::kVersionSameError;
  } else if (error_name == std::string(kFwupdErrorName_AlreadyPending)) {
    return FwupdDbusResult::kAlreadyPendingError;
  } else if (error_name == std::string(kFwupdErrorName_AuthFailed)) {
    return FwupdDbusResult::kAuthFailedError;
  } else if (error_name == std::string(kFwupdErrorName_Read)) {
    return FwupdDbusResult::kReadError;
  } else if (error_name == std::string(kFwupdErrorName_Write)) {
    return FwupdDbusResult::kWriteError;
  } else if (error_name == std::string(kFwupdErrorName_InvalidFile)) {
    return FwupdDbusResult::kInvalidFileError;
  } else if (error_name == std::string(kFwupdErrorName_NotFound)) {
    return FwupdDbusResult::kNotFoundError;
  } else if (error_name == std::string(kFwupdErrorName_NothingToDo)) {
    return FwupdDbusResult::kNothingToDoError;
  } else if (error_name == std::string(kFwupdErrorName_NotSupported)) {
    return FwupdDbusResult::kNotSupportedError;
  } else if (error_name == std::string(kFwupdErrorName_SignatureInvalid)) {
    return FwupdDbusResult::kSignatureInvalidError;
  } else if (error_name == std::string(kFwupdErrorName_AcPowerRequired)) {
    return FwupdDbusResult::kAcPowerRequiredError;
  } else if (error_name == std::string(kFwupdErrorName_PermissionDenied)) {
    return FwupdDbusResult::kPermissionDeniedError;
  } else if (error_name == std::string(kFwupdErrorName_BrokenSystem)) {
    return FwupdDbusResult::kBrokenSystemError;
  } else if (error_name == std::string(kFwupdErrorName_BatteryLevelTooLow)) {
    return FwupdDbusResult::kBatteryLevelTooLowError;
  } else if (error_name == std::string(kFwupdErrorName_NeedsUserAction)) {
    return FwupdDbusResult::kNeedsUserActionError;
  } else if (error_name == std::string(kFwupdErrorName_AuthExpired)) {
    return FwupdDbusResult::kAuthExpiredError;
  }
  FIRMWARE_LOG(ERROR) << "No matching error found for: " << error_name;
  return FwupdDbusResult::kUnknownError;
}

base::FilePath GetFilePathFromUri(const GURL uri) {
  const std::string filepath = uri.spec();

  if (!filepath.empty()) {
    // Verify that the extension is .cab.
    std::size_t extension_delim = filepath.find_last_of(".");
    if (extension_delim == std::string::npos ||
        filepath.substr(extension_delim) != kCabFileExtension) {
      // Bad file, return with empty file path;
      FIRMWARE_LOG(ERROR) << "Bad file found: " << filepath;
      return base::FilePath();
    }

    return base::FilePath(FILE_PATH_LITERAL(filepath));
  }

  // Return empty file path if filename can't be found.
  return base::FilePath();
}

std::string ParseCheckSum(const std::string& raw_sum) {
  // The raw checksum string from fwupd can be formatted as:
  // "SHA{Option},SHA{Option}" or "SHA{Option}". Grab the SHA256 when possible.
  const std::size_t delim_pos = raw_sum.find_first_of(",");
  if (delim_pos != std::string::npos) {
    DCHECK(raw_sum.size() > 0);
    if (delim_pos >= raw_sum.size() - 1) {
      return "";
    }

    const std::string first = raw_sum.substr(0, delim_pos);
    const std::string second = raw_sum.substr(delim_pos + 1);
    if (first.length() == kSha256Length) {
      return first;
    }
    if (second.length() == kSha256Length) {
      return second;
    }
    return "";
  }

  // Only one checksum available, use it if it's a sha256 checksum.
  if (raw_sum.length() != kSha256Length) {
    return "";
  }

  return raw_sum;
}

std::optional<DeviceRequestId> GetDeviceRequestIdFromFwupdString(
    std::string fwupd_device_id_string) {
  static base::NoDestructor<FwupdStringToRequestIdMap>
      fwupdStringToRequestIdMap(
          {{kFwupdDeviceRequestId_DoNotPowerOff,
            DeviceRequestId::kDoNotPowerOff},
           {kFwupdDeviceRequestId_ReplugInstall,
            DeviceRequestId::kReplugInstall},
           {kFwupdDeviceRequestId_InsertUSBCable,
            DeviceRequestId::kInsertUSBCable},
           {kFwupdDeviceRequestId_RemoveUSBCable,
            DeviceRequestId::kRemoveUSBCable},
           {kFwupdDeviceRequestId_PressUnlock, DeviceRequestId::kPressUnlock},
           {kFwupdDeviceRequestId_RemoveReplug, DeviceRequestId::kRemoveReplug},
           {kFwupdDeviceRequestId_ReplugPower, DeviceRequestId::kReplugPower}});

  if (fwupdStringToRequestIdMap->contains(fwupd_device_id_string)) {
    return fwupdStringToRequestIdMap->at(fwupd_device_id_string);
  } else {
    return std::nullopt;
  }
}

class FwupdClientImpl : public FwupdClient {
 public:
  FwupdClientImpl() = default;
  FwupdClientImpl(const FwupdClientImpl&) = delete;
  FwupdClientImpl& operator=(const FwupdClientImpl&) = delete;
  ~FwupdClientImpl() override = default;

  void Init(dbus::Bus* bus) override {
    DCHECK(bus);

    proxy_ = bus->GetObjectProxy(kFwupdServiceName,
                                 dbus::ObjectPath(kFwupdServicePath));
    DCHECK(proxy_);
    proxy_->ConnectToSignal(
        kFwupdServiceInterface, kFwupdDeviceAddedSignalName,
        base::BindRepeating(&FwupdClientImpl::OnDeviceAddedReceived,
                            weak_ptr_factory_.GetWeakPtr()),
        base::BindOnce(&FwupdClientImpl::OnSignalConnected,
                       weak_ptr_factory_.GetWeakPtr()));
    proxy_->ConnectToSignal(
        kFwupdServiceInterface, kFwupdDeviceRequestReceivedSignalName,
        base::BindRepeating(&FwupdClientImpl::OnDeviceRequestReceived,
                            weak_ptr_factory_.GetWeakPtr()),
        base::BindOnce(&FwupdClientImpl::OnSignalConnected,
                       weak_ptr_factory_.GetWeakPtr()));

    properties_ = std::make_unique<FwupdDbusProperties>(
        proxy_, base::BindRepeating(&FwupdClientImpl::OnPropertyChanged,
                                    weak_ptr_factory_.GetWeakPtr()));
    properties_->ConnectSignals();
    properties_->GetAll();

    SetFwupdFeatureFlags();
  }

  void SetFwupdFeatureFlags() override {
    // Enable interactive updates in fwupd by setting the "requests"
    // FwupdFeatureFlag when the Firmware Updates v2 feature flag is enabled.
    if (base::FeatureList::IsEnabled(features::kFirmwareUpdateUIV2)) {
      dbus::MethodCall method_call(kFwupdServiceInterface,
                                   kFwupdSetFeatureFlagsMethodName);
      dbus::MessageWriter writer(&method_call);
      writer.AppendUint64(kRequestsFeatureFlag);

      proxy_->CallMethodWithErrorResponse(
          &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
          base::BindOnce(&FwupdClientImpl::SetFeatureFlagsCallback,
                         weak_ptr_factory_.GetWeakPtr()));
    }
  }

  void RequestUpdates(const std::string& device_id) override {
    FIRMWARE_LOG(USER) << "fwupd: RequestUpdates called for: " << device_id;
    dbus::MethodCall method_call(kFwupdServiceInterface,
                                 kFwupdGetUpgradesMethodName);
    dbus::MessageWriter writer(&method_call);

    writer.AppendString(device_id);

    proxy_->CallMethodWithErrorResponse(
        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
        base::BindOnce(&FwupdClientImpl::RequestUpdatesCallback,
                       weak_ptr_factory_.GetWeakPtr(), device_id));
  }

  void RequestDevices() override {
    FIRMWARE_LOG(USER) << "fwupd: RequestDevices called";
    dbus::MethodCall method_call(kFwupdServiceInterface,
                                 kFwupdGetDevicesMethodName);
    proxy_->CallMethodWithErrorResponse(
        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
        base::BindOnce(&FwupdClientImpl::RequestDevicesCallback,
                       weak_ptr_factory_.GetWeakPtr()));
  }

  void InstallUpdate(
      const std::string& device_id,
      base::ScopedFD file_descriptor,
      FirmwareInstallOptions options,
      base::OnceCallback<void(FwupdDbusResult)> callback) override {
    FIRMWARE_LOG(USER) << "fwupd: InstallUpdate called for id: " << device_id;
    dbus::MethodCall method_call(kFwupdServiceInterface,
                                 kFwupdInstallMethodName);
    dbus::MessageWriter writer(&method_call);

    writer.AppendString(device_id);
    writer.AppendFileDescriptor(file_descriptor.get());

    // Write the options in form of "a{sv}".
    dbus::MessageWriter array_writer(nullptr);
    writer.OpenArray("{sv}", &array_writer);
    for (const auto& option : options) {
      dbus::MessageWriter dict_entry_writer(nullptr);
      array_writer.OpenDictEntry(&dict_entry_writer);
      dict_entry_writer.AppendString(option.first);
      dict_entry_writer.AppendVariantOfBool(option.second);
      array_writer.CloseContainer(&dict_entry_writer);
    }
    writer.CloseContainer(&array_writer);

    // TODO(michaelcheco): Investigate whether or not the estimated install time
    // multiplied by some factor can be used in place of |TIMEOUT_INFINITE|.
    proxy_->CallMethodWithErrorResponse(
        &method_call, dbus::ObjectProxy::TIMEOUT_INFINITE,
        base::BindOnce(&FwupdClientImpl::InstallUpdateCallback,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
  }

  void UpdateMetadata(
      const std::string& remote_id,
      base::ScopedFD data_file_descriptor,
      base::ScopedFD sig_file_descriptor,
      base::OnceCallback<void(FwupdDbusResult)> callback) override {
    FIRMWARE_LOG(USER) << "fwupd: UpdateMetadata called for remote id: "
                       << remote_id;
    dbus::MethodCall method_call(kFwupdServiceInterface,
                                 kFwupdUpdateMetadataMethodName);
    dbus::MessageWriter writer(&method_call);

    writer.AppendString(remote_id);
    writer.AppendFileDescriptor(data_file_descriptor.get());
    writer.AppendFileDescriptor(sig_file_descriptor.get());

    proxy_->CallMethodWithErrorResponse(
        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
        base::BindOnce(&FwupdClientImpl::UpdateMetadataCallback,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
  }

 private:
  // Pops a string-to-variant-string dictionary from the reader.
  base::Value::Dict PopStringToStringDictionary(dbus::MessageReader* reader) {
    dbus::MessageReader array_reader(nullptr);

    if (!reader->PopArray(&array_reader)) {
      FIRMWARE_LOG(ERROR) << "Failed to pop array into the array reader.";
      return base::Value::Dict();
    }
    base::Value::Dict result;

    while (array_reader.HasMoreData()) {
      dbus::MessageReader entry_reader(nullptr);
      dbus::MessageReader variant_reader(nullptr);
      std::string key;
      std::string value_string;
      uint32_t value_uint = 0;

      const bool success = array_reader.PopDictEntry(&entry_reader) &&
                           entry_reader.PopString(&key) &&
                           entry_reader.PopVariant(&variant_reader);

      if (!success) {
        FIRMWARE_LOG(ERROR) << "Failed to get a dictionary entry. ";
        return base::Value::Dict();
      }

      // Values in the response can have different types. The fields we are
      // interested in, are all either strings, uint64, or uint32.
      // Some fields in the response have other types, but we don't use them, so
      // we just skip them.

      const dbus::Message::DataType data_type = variant_reader.GetDataType();
      if (data_type == dbus::Message::UINT32) {
        variant_reader.PopUint32(&value_uint);
        // Value doesn't support unsigned numbers, so this has to be converted
        // to int.
        result.Set(key, (int)value_uint);
      } else if (data_type == dbus::Message::STRING) {
        variant_reader.PopString(&value_string);
        result.Set(key, value_string);
      } else if (data_type == dbus::Message::UINT64) {
        // Value doesn't support lossless storage of uint64_t, so
        // convert flags to boolean keys.
        if (key == "Flags") {
          uint64_t value_uint64 = 0;
          variant_reader.PopUint64(&value_uint64);
          const bool is_internal =
              (value_uint64 & kInternalDeviceFlag) == kInternalDeviceFlag;
          result.Set(kIsInternalKey, is_internal);
        }
        if (key == "TrustFlags") {
          uint64_t value_uint64 = 0;
          variant_reader.PopUint64(&value_uint64);
          const bool has_trusted_report =
              (value_uint64 & kTrustedReportsReleaseFlag) ==
              kTrustedReportsReleaseFlag;
          result.Set(kHasTrustedReportKey, has_trusted_report);
        }
      }
    }
    return result;
  }

  void RequestUpdatesCallback(const std::string& device_id,
                              dbus::Response* response,
                              dbus::ErrorResponse* error_response) {
    bool can_parse = true;
    if (!response) {
      can_parse = false;
      FIRMWARE_LOG(DEBUG) << "No updates found, reason: "
                          << (error_response ? error_response->ToString()
                                             : std::string("No response"));
    } else if (error_response) {
      // Returning updates and still getting an error response means this is a
      // real error.
      FIRMWARE_LOG(ERROR) << "Request Updates had error message: "
                          << error_response->ToString();
    }

    dbus::MessageReader reader(response);
    dbus::MessageReader array_reader(nullptr);

    if (can_parse && !reader.PopArray(&array_reader)) {
      FIRMWARE_LOG(ERROR) << "Failed to parse string from DBus Signal";
      can_parse = false;
    }

    FwupdUpdateList updates;
    while (can_parse && array_reader.HasMoreData()) {
      // Parse update description.
      base::Value::Dict dict = PopStringToStringDictionary(&array_reader);
      if (dict.empty()) {
        FIRMWARE_LOG(ERROR) << "Failed to parse the update description.";
        // Ran into an error, exit early.
        break;
      }

      const std::string* version = dict.FindString("Version");
      const std::string* description = dict.FindString("Description");
      std::optional<int> priority = dict.FindInt("Urgency");
      const std::string* uri = dict.FindString("Uri");
      const std::string* checksum = dict.FindString("Checksum");
      const std::string* remote_id = dict.FindString("RemoteId");
      std::optional<bool> trusted_report = dict.FindBool(kHasTrustedReportKey);
      bool has_trusted_report =
          !base::FeatureList::IsEnabled(
              features::kUpstreamTrustedReportsFirmware) ||
          (trusted_report.has_value() && trusted_report.value());
      FIRMWARE_LOG(DEBUG) << "Trusted Reports: " << has_trusted_report;

      // Skip release if its coming from LVFS and feature flag not enabled
      if (remote_id && *remote_id == "lvfs" &&
          !base::FeatureList::IsEnabled(
              features::kUpstreamTrustedReportsFirmware)) {
        continue;
      }

      base::FilePath filepath;
      if (uri) {
        filepath = GetFilePathFromUri(GURL(*uri));
      }

      std::string sha_checksum;
      if (checksum) {
        sha_checksum = ParseCheckSum(*checksum);
      }

      std::string description_value = "";

      if (description) {
        description_value = *description;
      } else {
        FIRMWARE_LOG(ERROR)
            << "Device: " << device_id << " is missing its description text.";
      }

      // If priority isn't specified we use default of low priority.
      if (!priority) {
        FIRMWARE_LOG(ERROR)
            << "Device: " << device_id
            << " is missing its priority field, using default of low priority.";
      }
      int priority_value = priority.value_or(UpdatePriority::kLow);

      const bool success = version && !filepath.empty() &&
                           !sha_checksum.empty() && has_trusted_report;
      // TODO(michaelcheco): Confirm that this is the expected behavior.
      if (success) {
        FIRMWARE_LOG(USER) << "fwupd: Found update version for device: "
                           << device_id << " with version: " << *version;
        updates.emplace_back(*version, description_value, priority_value,
                             filepath, sha_checksum);
      } else {
        if (!version) {
          FIRMWARE_LOG(ERROR)
              << "Device: " << device_id << " is missing its version field.";
        }
        if (!uri) {
          FIRMWARE_LOG(ERROR)
              << "Device: " << device_id << " is missing its URI field.";
        }
        if (!checksum) {
          FIRMWARE_LOG(ERROR)
              << "Device: " << device_id << " is missing its checksum field.";
        }
      }
    }

    FIRMWARE_LOG(USER) << "fwupd: Updates for: " << device_id << ": "
                       << updates.size();

    for (auto& observer : observers_) {
      observer.OnUpdateListResponse(device_id, &updates);
    }
  }

  void RequestDevicesCallback(dbus::Response* response,
                              dbus::ErrorResponse* error_response) {
    if (!response) {
      FIRMWARE_LOG(ERROR) << "No Dbus response received from fwupd.";
      return;
    }

    dbus::MessageReader reader(response);
    dbus::MessageReader array_reader(nullptr);

    if (!reader.PopArray(&array_reader)) {
      FIRMWARE_LOG(ERROR) << "Failed to parse string from DBus Signal";
      return;
    }

    FwupdDeviceList devices;
    while (array_reader.HasMoreData()) {
      // Parse device description.
      base::Value::Dict dict = PopStringToStringDictionary(&array_reader);
      if (dict.empty()) {
        FIRMWARE_LOG(ERROR) << "Failed to parse the device description.";
        return;
      }

      std::optional<bool> is_internal = dict.FindBool(kIsInternalKey);
      const std::string* name = dict.FindString("Name");
      if (is_internal.has_value() && is_internal.value()) {
        if (name) {
          FIRMWARE_LOG(DEBUG) << "Ignoring internal device: " << *name;
        } else {
          FIRMWARE_LOG(DEBUG) << "Ignoring unnamed internal device.";
        }
        continue;
      }

      const std::string* id = dict.FindString("DeviceId");

      // The keys "DeviceId" and "Name" must exist in the dictionary.
      const bool success = id && name;
      if (!success) {
        FIRMWARE_LOG(ERROR) << "No device id or name found.";
        return;
      }

      FIRMWARE_LOG(DEBUG) << "fwupd: Device found: " << *id << " " << *name;
      devices.emplace_back(*id, *name);
    }

    FIRMWARE_LOG(USER) << "fwupd: Devices found: " << devices.size();

    for (auto& observer : observers_) {
      observer.OnDeviceListResponse(&devices);
    }
  }

  void InstallUpdateCallback(base::OnceCallback<void(FwupdDbusResult)> callback,
                             dbus::Response* response,
                             dbus::ErrorResponse* error_response) {
    FwupdDbusResult result = FwupdDbusResult::kSuccess;
    if (error_response) {
      FIRMWARE_LOG(ERROR) << "Firmware install failed with error message: "
                          << error_response->ToString();
      result = GetFwupdDbusResult(error_response->GetErrorName());
    }

    FIRMWARE_LOG(USER) << "fwupd: InstallUpdate returned with: "
                       << static_cast<int>(result);
    std::move(callback).Run(result);
  }

  void OnSignalConnected(const std::string& interface_name,
                         const std::string& signal_name,
                         bool is_connected) {
    if (!is_connected) {
      FIRMWARE_LOG(ERROR) << "Failed to connect to signal " << signal_name;
    }
  }

  // TODO(swifton): This is a stub implementation.
  void OnDeviceAddedReceived(dbus::Signal* signal) {
    if (client_is_in_testing_mode_) {
      ++device_signal_call_count_for_testing_;
    }
  }

  void OnDeviceRequestReceived(dbus::Signal* signal) {
    FIRMWARE_LOG(EVENT) << "fwupd: Received device request";
    dbus::MessageReader signal_reader(signal);
    dbus::MessageReader array_reader(nullptr);

    if (!signal_reader.PopArray(&array_reader)) {
      FIRMWARE_LOG(ERROR) << "Failed to pop array into the array reader.";
      return;
    }

    std::string request_id_string;
    uint32_t request_kind;

    while (array_reader.HasMoreData()) {
      dbus::MessageReader dict_entry_reader(nullptr);
      dbus::MessageReader value_reader(nullptr);
      std::string key;
      if (!array_reader.PopDictEntry(&dict_entry_reader) ||
          !dict_entry_reader.PopString(&key) ||
          !dict_entry_reader.PopVariant(&value_reader)) {
        FIRMWARE_LOG(ERROR)
            << "Failed to pop dict entry into the entry reader.";
        return;
      }
      if (key == kFwupdDeviceRequestKey_AppstreamId) {
        if (!value_reader.PopString(&request_id_string)) {
          FIRMWARE_LOG(ERROR)
              << "Failed to pop string for AppstreamId (DeviceRequestId).";
          return;
        }
      } else if (key == kFwupdDeviceRequestKey_RequestKind) {
        if (!value_reader.PopUint32(&request_kind)) {
          FIRMWARE_LOG(ERROR) << "Failed to pop uint32 for RequestKind";
          return;
        }
      }
    }

    if (request_id_string.empty()) {
      FIRMWARE_LOG(ERROR)
          << "Could not parse request_id from DeviceRequest signal.";
      return;
    }

    std::optional<DeviceRequestId> request_id =
        GetDeviceRequestIdFromFwupdString(request_id_string);

    if (!request_id.has_value()) {
      FIRMWARE_LOG(ERROR) << "Could not get DeviceRequestId for string "
                          << request_id_string;
      return;
    }

    for (auto& observer : observers_) {
      observer.OnDeviceRequestResponse(FwupdRequest(
          static_cast<uint32_t>(request_id.value()), request_kind));
    }
  }

  void OnPropertyChanged(const std::string& name) {
    for (auto& observer : observers_) {
      observer.OnPropertiesChangedResponse(properties_.get());
    }
  }

  void SetFeatureFlagsCallback(dbus::Response* response,
                               dbus::ErrorResponse* error_response) {
    // No need to take any specific action here.
    if (!response) {
      FIRMWARE_LOG(ERROR) << "No D-Bus response received from fwupd.";
      return;
    }
  }

  void UpdateMetadataCallback(
      base::OnceCallback<void(FwupdDbusResult)> callback,
      dbus::Response* response,
      dbus::ErrorResponse* error_response) {
    FwupdDbusResult result = FwupdDbusResult::kSuccess;
    if (error_response) {
      FIRMWARE_LOG(ERROR) << "UpdateMetadata failed with error message: "
                          << error_response->ToString();
      result = GetFwupdDbusResult(error_response->GetErrorName());
    }

    FIRMWARE_LOG(USER) << "UpdateMetadata returned with: "
                       << static_cast<int>(result);
    std::move(callback).Run(result);
  }

  raw_ptr<dbus::ObjectProxy> proxy_ = nullptr;

  // Note: This should remain the last member so it'll be destroyed and
  // invalidate its weak pointers before any other members are destroyed.
  base::WeakPtrFactory<FwupdClientImpl> weak_ptr_factory_{this};
};

}  // namespace

void FwupdClient::AddObserver(FwupdClient::Observer* observer) {
  observers_.AddObserver(observer);
}

void FwupdClient::RemoveObserver(FwupdClient::Observer* observer) {
  observers_.RemoveObserver(observer);
}

FwupdClient::FwupdClient() {
  DCHECK(!g_instance);
  g_instance = this;
}

FwupdClient::~FwupdClient() {
  DCHECK_EQ(g_instance, this);
  g_instance = nullptr;
}

// static
FwupdClient* FwupdClient::Get() {
  return g_instance;
}

// static
FakeFwupdClient* FwupdClient::GetFake() {
  return g_fake_instance;
}

// static
void FwupdClient::Initialize(dbus::Bus* bus) {
  CHECK(bus);
  (new FwupdClientImpl())->Init(bus);
}

// static
void FwupdClient::InitializeFake() {
  g_fake_instance = new FakeFwupdClient();
  g_fake_instance->Init(nullptr);
}

// static
void FwupdClient::Shutdown() {
  CHECK(g_instance);
  delete g_instance;
}

}  // namespace ash