chromium/chromeos/ash/services/bluetooth_config/device_name_manager_impl.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.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chromeos/ash/services/bluetooth_config/device_name_manager_impl.h"

#include "base/containers/span.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "chromeos/ash/services/bluetooth_config/public/mojom/cros_bluetooth_config.mojom.h"
#include "components/device_event_log/device_event_log.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "device/bluetooth/chromeos/bluetooth_utils.h"
#include "device/bluetooth/floss/floss_features.h"
#include "third_party/re2/src/re2/re2.h"

namespace ash::bluetooth_config {

namespace {

bool IsNicknameValid(const std::string& nickname) {
  if (nickname.empty())
    return false;

  return nickname.size() <= mojom::kDeviceNicknameCharacterLimit;
}

}  // namespace

// static
void DeviceNameManagerImpl::RegisterLocalStatePrefs(
    PrefRegistrySimple* registry) {
  registry->RegisterDictionaryPref(kDeviceIdToNicknameMapPrefNameLegacy);
  registry->RegisterDictionaryPref(kDeviceIdToNicknameMapPrefName);
}

DeviceNameManagerImpl::DeviceNameManagerImpl(
    scoped_refptr<device::BluetoothAdapter> bluetooth_adapter)
    : bluetooth_adapter_(std::move(bluetooth_adapter)) {}

DeviceNameManagerImpl::~DeviceNameManagerImpl() = default;

std::optional<std::string> DeviceNameManagerImpl::GetDeviceNickname(
    const std::string& device_id) {
  if (!local_state_) {
    BLUETOOTH_LOG(ERROR) << "Cannot get device nickname for " << device_id
                         << " because local_state_ is null";
    return std::nullopt;
  }

  const std::string* nickname =
      local_state_
          ->GetDict(floss::features::IsFlossEnabled()
                        ? kDeviceIdToNicknameMapPrefName
                        : kDeviceIdToNicknameMapPrefNameLegacy)
          .FindString(device_id);
  if (!nickname)
    return std::nullopt;

  return *nickname;
}

void DeviceNameManagerImpl::SetDeviceNickname(const std::string& device_id,
                                              const std::string& nickname) {
  if (!IsNicknameValid(nickname)) {
    BLUETOOTH_LOG(ERROR) << "SetDeviceNickname for device with id " << device_id
                         << " failed because nickname is invalid, nickname: "
                         << nickname;
    device::RecordSetDeviceNickName(
        device::SetNicknameResult::kInvalidNicknameFormat);
    return;
  }

  if (!DoesDeviceExist(device_id)) {
    BLUETOOTH_LOG(ERROR) << "SetDeviceNickname for device failed because "
                            "device_id was not found, device_id: "
                         << device_id;
    device::RecordSetDeviceNickName(device::SetNicknameResult::kDeviceNotFound);
    return;
  }

  if (!local_state_) {
    BLUETOOTH_LOG(ERROR) << "SetDeviceNickname for device failed because "
                            "no local_state_ was set, device_id: "
                         << device_id;
    device::RecordSetDeviceNickName(
        device::SetNicknameResult::kPrefsUnavailable);
    return;
  }

  ScopedDictPrefUpdate update(local_state_,
                              floss::features::IsFlossEnabled()
                                  ? kDeviceIdToNicknameMapPrefName
                                  : kDeviceIdToNicknameMapPrefNameLegacy);

  BLUETOOTH_LOG(USER) << "Setting device nickname for " << device_id << " to "
                      << nickname;
  update->Set(device_id, nickname);

  NotifyDeviceNicknameChanged(device_id, nickname);
  device::RecordSetDeviceNickName(device::SetNicknameResult::kSuccess);
}

void DeviceNameManagerImpl::RemoveDeviceNickname(const std::string& device_id) {
  if (!local_state_) {
    BLUETOOTH_LOG(ERROR) << "RemoveDeviceNickname for device failed because "
                         << "no local_state_ was set, device_id: " << device_id;
    return;
  }

  ScopedDictPrefUpdate update(local_state_,
                              floss::features::IsFlossEnabled()
                                  ? kDeviceIdToNicknameMapPrefName
                                  : kDeviceIdToNicknameMapPrefNameLegacy);

  // Do nothing if no nickname exists for |device_id|.
  if (!update->Find(device_id)) {
    BLUETOOTH_LOG(ERROR) << "RemoveDeviceNickname for device failed because no "
                         << "nickname exists for " << device_id;
    return;
  }

  BLUETOOTH_LOG(EVENT) << "Removing device nickname for " << device_id;
  update->Remove(device_id);
  NotifyDeviceNicknameChanged(device_id, /*nickname=*/std::nullopt);
}

void DeviceNameManagerImpl::SetPrefs(PrefService* local_state) {
  local_state_ = local_state;

  if (!local_state_) {
    return;
  }
  const bool hasPref =
      local_state_->HasPrefPath(kDeviceIdToNicknameMapPrefName);
  if (!floss::features::IsFlossEnabled()) {
    if (hasPref) {
      local_state_->ClearPref(kDeviceIdToNicknameMapPrefName);
    }
    return;
  }
  if (!hasPref) {
    MigrateExistingNicknames();
  }
}

void DeviceNameManagerImpl::MigrateExistingNicknames() {
  DCHECK(floss::features::IsFlossEnabled());
  DCHECK(local_state_);

  BLUETOOTH_LOG(EVENT) << "Starting migration of Bluetooth nickname prefs";

  const re2::RE2 kFlossIdRegex("^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$");

  const base::Value::Dict& existing_prefs =
      local_state_->GetDict(kDeviceIdToNicknameMapPrefNameLegacy);

  for (const auto [id, nickname] : existing_prefs) {
    // Check whether the ID already matches the Floss format. If so, simply copy
    // the entry into the migrated prefs.
    if (re2::RE2::FullMatch(id, kFlossIdRegex)) {
      BLUETOOTH_LOG(EVENT)
          << "Device ID format already matches Floss format; overwriting entry";
      ScopedDictPrefUpdate update(local_state_, kDeviceIdToNicknameMapPrefName);
      update->Set(id, nickname.Clone());
      continue;
    }

    // Since BlueZ uses the DBus object path of the device as the ID, which
    // includes the parts of the address of the device, we are able to extract
    // these parts and use them to generate an ID for the device that follows
    // the format expected of a Floss device ID. Using the following ID as an
    // example:
    //
    //   /org/bluez/hci0/dev_7C_96_D2_8B_FB_17  ==>  7C:96:D2:8B:FB:17
    //
    // We start by splitting the ID using underscores as the delimiter and then
    // concatenating the last six parts, which correspond to the parts of the
    // Bluetooth address of the device, using colons to delineate.
    const std::vector<std::string> parts =
        base::SplitString(id, "_", base::WhitespaceHandling::TRIM_WHITESPACE,
                          base::SplitResult::SPLIT_WANT_NONEMPTY);

    // We expect at least seven different parts; six for the device address, and
    // at least one additional part for the non-address prefix.
    if (parts.size() < 7) {
      BLUETOOTH_LOG(ERROR) << "Failed to migrate Bluetooth nickname, device ID "
                           << "has an unexpected format: " << id;
      continue;
    }

    auto floss_id =
        base::JoinString(base::make_span(parts.end() - 6, parts.end()), ":");

    // Avoid overwriting an existing entry with a BlueZ nickname. This allows us
    // to guarantee that Floss nicknames are migrated and outdate BlueZ
    // nicknames are effectively forgotten.
    if (local_state_->GetDict(kDeviceIdToNicknameMapPrefName)
            .contains(floss_id)) {
      BLUETOOTH_LOG(EVENT) << "Device ID already exists; skipping entry";
      continue;
    }

    ScopedDictPrefUpdate update(local_state_, kDeviceIdToNicknameMapPrefName);
    update->Set(floss_id, nickname.Clone());

    BLUETOOTH_LOG(EVENT) << "Successfully migrated Bluetooth nickname pref";
  }
  BLUETOOTH_LOG(EVENT) << "Finished migration of Bluetooth nickname prefs";
}

bool DeviceNameManagerImpl::DoesDeviceExist(
    const std::string& device_id) const {
  for (auto* device : bluetooth_adapter_->GetDevices()) {
    if (device->GetIdentifier() == device_id)
      return true;
  }
  return false;
}

}  // namespace ash::bluetooth_config