// Copyright 2020 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/nearby_sharing/local_device_data/nearby_share_local_device_data_manager_impl.h"
#include <string>
#include <utility>
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/notreached.h"
#include "base/rand_util.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/nearby_sharing/common/nearby_share_prefs.h"
#include "chrome/browser/nearby_sharing/common/nearby_share_profile_info_provider.h"
#include "chrome/browser/nearby_sharing/common/nearby_share_switches.h"
#include "chrome/browser/nearby_sharing/local_device_data/nearby_share_device_data_updater.h"
#include "chrome/browser/nearby_sharing/local_device_data/nearby_share_device_data_updater_impl.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/nearby/common/scheduling/nearby_scheduler.h"
#include "chromeos/ash/components/nearby/common/scheduling/nearby_scheduler_factory.h"
#include "components/prefs/pref_service.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/chromeos/devicetype_utils.h"
namespace {
// Using the alphanumeric characters below, this provides 36^10 unique device
// IDs. Note that the uniqueness requirement is not global; the IDs are only
// used to differentiate between devices associated with a single GAIA account.
// This ID length agrees with the GmsCore implementation.
const size_t kDeviceIdLength = 10;
// Possible characters used in a randomly generated device ID. This agrees with
// the GmsCore implementation.
constexpr std::array<char, 36> kAlphaNumericChars = {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
constexpr base::TimeDelta kUpdateDeviceDataTimeout = base::Seconds(30);
constexpr base::TimeDelta kDeviceDataDownloadPeriod = base::Hours(12);
// Returns a truncated version of |name| that is |overflow_length| characters
// too long. For example, name="Reallylongname" with overflow_length=5 will
// return "Really...".
std::string GetTruncatedName(std::string name, size_t overflow_length) {
std::string ellipsis("...");
size_t max_name_length = name.length() - overflow_length - ellipsis.length();
DCHECK_GT(max_name_length, 0u);
std::string truncated;
base::TruncateUTF8ToByteSize(name, max_name_length, &truncated);
truncated.append(ellipsis);
return truncated;
}
} // namespace
// static
NearbyShareLocalDeviceDataManagerImpl::Factory*
NearbyShareLocalDeviceDataManagerImpl::Factory::test_factory_ = nullptr;
// static
std::unique_ptr<NearbyShareLocalDeviceDataManager>
NearbyShareLocalDeviceDataManagerImpl::Factory::Create(
PrefService* pref_service,
NearbyShareClientFactory* http_client_factory,
NearbyShareProfileInfoProvider* profile_info_provider) {
if (test_factory_) {
return test_factory_->CreateInstance(pref_service, http_client_factory,
profile_info_provider);
}
return base::WrapUnique(new NearbyShareLocalDeviceDataManagerImpl(
pref_service, http_client_factory, profile_info_provider));
}
// static
void NearbyShareLocalDeviceDataManagerImpl::Factory::SetFactoryForTesting(
Factory* test_factory) {
test_factory_ = test_factory;
}
NearbyShareLocalDeviceDataManagerImpl::Factory::~Factory() = default;
NearbyShareLocalDeviceDataManagerImpl::NearbyShareLocalDeviceDataManagerImpl(
PrefService* pref_service,
NearbyShareClientFactory* http_client_factory,
NearbyShareProfileInfoProvider* profile_info_provider)
: pref_service_(pref_service),
profile_info_provider_(profile_info_provider),
device_data_updater_(NearbyShareDeviceDataUpdaterImpl::Factory::Create(
GetId(),
kUpdateDeviceDataTimeout,
http_client_factory)),
download_device_data_scheduler_(
ash::nearby::NearbySchedulerFactory::CreatePeriodicScheduler(
kDeviceDataDownloadPeriod,
/*retry_failures=*/true,
/*require_connectivity=*/true,
prefs::kNearbySharingSchedulerDownloadDeviceDataPrefName,
pref_service_,
base::BindRepeating(&NearbyShareLocalDeviceDataManagerImpl::
OnDownloadDeviceDataRequested,
base::Unretained(this)),
Feature::NS)) {}
NearbyShareLocalDeviceDataManagerImpl::
~NearbyShareLocalDeviceDataManagerImpl() = default;
std::string NearbyShareLocalDeviceDataManagerImpl::GetId() {
std::string id =
pref_service_->GetString(prefs::kNearbySharingDeviceIdPrefName);
if (!id.empty())
return id;
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(switches::kNearbyShareDeviceID)) {
id = command_line->GetSwitchValueASCII(switches::kNearbyShareDeviceID);
} else {
for (size_t i = 0; i < kDeviceIdLength; ++i)
id += kAlphaNumericChars[base::RandGenerator(kAlphaNumericChars.size())];
}
pref_service_->SetString(prefs::kNearbySharingDeviceIdPrefName, id);
return id;
}
std::string NearbyShareLocalDeviceDataManagerImpl::GetDeviceName() const {
std::string device_name =
pref_service_->GetString(prefs::kNearbySharingDeviceNamePrefName);
return device_name.empty() ? GetDefaultDeviceName() : device_name;
}
std::optional<std::string> NearbyShareLocalDeviceDataManagerImpl::GetFullName()
const {
if (pref_service_->FindPreference(prefs::kNearbySharingFullNamePrefName)
->IsDefaultValue()) {
return std::nullopt;
}
return pref_service_->GetString(prefs::kNearbySharingFullNamePrefName);
}
std::optional<std::string> NearbyShareLocalDeviceDataManagerImpl::GetIconUrl()
const {
if (pref_service_->FindPreference(prefs::kNearbySharingIconUrlPrefName)
->IsDefaultValue()) {
return std::nullopt;
}
return pref_service_->GetString(prefs::kNearbySharingIconUrlPrefName);
}
std::optional<std::string> NearbyShareLocalDeviceDataManagerImpl::GetIconToken()
const {
if (pref_service_->FindPreference(prefs::kNearbySharingIconTokenPrefName)
->IsDefaultValue()) {
return std::nullopt;
}
return pref_service_->GetString(prefs::kNearbySharingIconTokenPrefName);
}
nearby_share::mojom::DeviceNameValidationResult
NearbyShareLocalDeviceDataManagerImpl::ValidateDeviceName(
const std::string& name) {
if (name.empty())
return nearby_share::mojom::DeviceNameValidationResult::kErrorEmpty;
if (!base::IsStringUTF8(name))
return nearby_share::mojom::DeviceNameValidationResult::kErrorNotValidUtf8;
if (name.length() > kNearbyShareDeviceNameMaxLength)
return nearby_share::mojom::DeviceNameValidationResult::kErrorTooLong;
return nearby_share::mojom::DeviceNameValidationResult::kValid;
}
nearby_share::mojom::DeviceNameValidationResult
NearbyShareLocalDeviceDataManagerImpl::SetDeviceName(const std::string& name) {
if (name == GetDeviceName())
return nearby_share::mojom::DeviceNameValidationResult::kValid;
auto error = ValidateDeviceName(name);
if (error != nearby_share::mojom::DeviceNameValidationResult::kValid)
return error;
pref_service_->SetString(prefs::kNearbySharingDeviceNamePrefName, name);
NotifyLocalDeviceDataChanged(/*did_device_name_change=*/true,
/*did_full_name_change=*/false,
/*did_icon_change=*/false);
return nearby_share::mojom::DeviceNameValidationResult::kValid;
}
void NearbyShareLocalDeviceDataManagerImpl::DownloadDeviceData() {
download_device_data_scheduler_->MakeImmediateRequest();
}
void NearbyShareLocalDeviceDataManagerImpl::UploadContacts(
std::vector<nearby::sharing::proto::Contact> contacts,
UploadCompleteCallback callback) {
device_data_updater_->UpdateDeviceData(
std::move(contacts),
/*certificates=*/std::nullopt,
base::BindOnce(
&NearbyShareLocalDeviceDataManagerImpl::OnUploadContactsFinished,
base::Unretained(this), std::move(callback)));
}
void NearbyShareLocalDeviceDataManagerImpl::UploadCertificates(
std::vector<nearby::sharing::proto::PublicCertificate> certificates,
UploadCompleteCallback callback) {
device_data_updater_->UpdateDeviceData(
/*contacts=*/std::nullopt, std::move(certificates),
base::BindOnce(
&NearbyShareLocalDeviceDataManagerImpl::OnUploadCertificatesFinished,
base::Unretained(this), std::move(callback)));
}
void NearbyShareLocalDeviceDataManagerImpl::OnStart() {
// This schedules an immediate download of the full name and icon URL from the
// server if that has never happened before.
download_device_data_scheduler_->Start();
}
void NearbyShareLocalDeviceDataManagerImpl::OnStop() {
download_device_data_scheduler_->Stop();
}
std::string NearbyShareLocalDeviceDataManagerImpl::GetDefaultDeviceName()
const {
std::u16string device_type = ui::GetChromeOSDeviceName();
std::optional<std::u16string> given_name =
profile_info_provider_->GetGivenName();
if (!given_name)
return base::UTF16ToUTF8(device_type);
std::string device_name = l10n_util::GetStringFUTF8(
IDS_NEARBY_DEFAULT_DEVICE_NAME, *given_name, device_type);
if (device_name.length() <= kNearbyShareDeviceNameMaxLength)
return device_name;
std::string truncated_name =
GetTruncatedName(base::UTF16ToUTF8(*given_name),
device_name.length() - kNearbyShareDeviceNameMaxLength);
return l10n_util::GetStringFUTF8(IDS_NEARBY_DEFAULT_DEVICE_NAME,
base::UTF8ToUTF16(truncated_name),
device_type);
}
void NearbyShareLocalDeviceDataManagerImpl::OnDownloadDeviceDataRequested() {
device_data_updater_->UpdateDeviceData(
/*contacts=*/std::nullopt,
/*certificates=*/std::nullopt,
base::BindOnce(
&NearbyShareLocalDeviceDataManagerImpl::OnDownloadDeviceDataFinished,
base::Unretained(this)));
}
void NearbyShareLocalDeviceDataManagerImpl::OnDownloadDeviceDataFinished(
const std::optional<nearby::sharing::proto::UpdateDeviceResponse>&
response) {
if (response)
HandleUpdateDeviceResponse(response);
download_device_data_scheduler_->HandleResult(
/*success=*/response.has_value());
}
void NearbyShareLocalDeviceDataManagerImpl::OnUploadContactsFinished(
UploadCompleteCallback callback,
const std::optional<nearby::sharing::proto::UpdateDeviceResponse>&
response) {
// NOTE(http://crbug.com/1211189): Only process the UpdateDevice response for
// DownloadDeviceData() calls. We want avoid infinite loops if the full name
// or icon URL unexpectedly change.
std::move(callback).Run(/*success=*/response.has_value());
}
void NearbyShareLocalDeviceDataManagerImpl::OnUploadCertificatesFinished(
UploadCompleteCallback callback,
const std::optional<nearby::sharing::proto::UpdateDeviceResponse>&
response) {
// NOTE(http://crbug.com/1211189): Only process the UpdateDevice response for
// DownloadDeviceData() calls. We want avoid infinite loops if the full name
// or icon URL unexpectedly change.
std::move(callback).Run(/*success=*/response.has_value());
}
void NearbyShareLocalDeviceDataManagerImpl::HandleUpdateDeviceResponse(
const std::optional<nearby::sharing::proto::UpdateDeviceResponse>&
response) {
if (!response)
return;
bool did_full_name_change = response->person_name() != GetFullName();
if (did_full_name_change) {
pref_service_->SetString(prefs::kNearbySharingFullNamePrefName,
response->person_name());
}
// NOTE(http://crbug.com/1211189): An icon URL can change without the
// underlying image changing. For example, icon URLs for some child accounts
// can rotate on every UpdateDevice RPC call; a timestamp is included in the
// URL. The icon token is used to detect changes in the underlying image. If a
// new URL is sent and the token doesn't change, the old URL may still be
// valid for a couple weeks, for example. So, private certificates do not
// necessarily need to update the icon URL whenever it changes. Also, we don't
// expect the token to change without the URL changing; regardless, we don't
// consider the icon changed unless the URL changes. That way, private
// certificates will not be unnecessarily regenerated.
bool did_icon_url_change = response->image_url() != GetIconUrl();
bool did_icon_token_change = response->image_token() != GetIconToken();
bool did_icon_change = did_icon_url_change && did_icon_token_change;
if (did_icon_url_change) {
pref_service_->SetString(prefs::kNearbySharingIconUrlPrefName,
response->image_url());
}
if (did_icon_token_change) {
pref_service_->SetString(prefs::kNearbySharingIconTokenPrefName,
response->image_token());
}
if (!did_full_name_change && !did_icon_change)
return;
NotifyLocalDeviceDataChanged(/*did_device_name_change=*/false,
did_full_name_change, did_icon_change);
}