// 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/contacts/nearby_share_contact_manager_impl.h"
#include <algorithm>
#include "base/containers/contains.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.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/contacts/nearby_share_contact_downloader.h"
#include "chrome/browser/nearby_sharing/contacts/nearby_share_contact_downloader_impl.h"
#include "chrome/browser/nearby_sharing/contacts/nearby_share_contacts_sorter.h"
#include "chrome/browser/nearby_sharing/local_device_data/nearby_share_local_device_data_manager.h"
#include "chromeos/ash/components/nearby/common/scheduling/nearby_scheduler.h"
#include "chromeos/ash/components/nearby/common/scheduling/nearby_scheduler_factory.h"
#include "chromeos/ash/services/nearby/public/mojom/nearby_share_settings.mojom-shared.h"
#include "chromeos/ash/services/nearby/public/mojom/nearby_share_settings.mojom.h"
#include "components/cross_device/logging/logging.h"
#include "components/prefs/pref_service.h"
#include "crypto/secure_hash.h"
#include "third_party/nearby/sharing/proto/device_rpc.pb.h"
#include "third_party/nearby/sharing/proto/rpc_resources.pb.h"
namespace {
constexpr base::TimeDelta kContactUploadPeriod = base::Hours(24);
constexpr base::TimeDelta kContactDownloadPeriod = base::Hours(12);
constexpr base::TimeDelta kContactDownloadRpcTimeout = base::Seconds(60);
// Removes contact IDs from the allowlist if they are not in |contacts|.
std::set<std::string> RemoveNonexistentContactsFromAllowlist(
const std::set<std::string>& allowed_contact_ids,
const std::vector<nearby::sharing::proto::ContactRecord>& contacts) {
std::set<std::string> new_allowed_contact_ids;
for (const nearby::sharing::proto::ContactRecord& contact : contacts) {
if (base::Contains(allowed_contact_ids, contact.id()))
new_allowed_contact_ids.insert(contact.id());
}
return new_allowed_contact_ids;
}
// Converts a list of ContactRecord protos, along with the allowlist, into a
// list of Contact protos.
std::vector<nearby::sharing::proto::Contact> ContactRecordsToContacts(
const std::set<std::string>& allowed_contact_ids,
const std::vector<nearby::sharing::proto::ContactRecord>& contact_records) {
std::vector<nearby::sharing::proto::Contact> contacts;
for (const auto& contact_record : contact_records) {
bool is_selected = base::Contains(allowed_contact_ids, contact_record.id());
for (const auto& identifier : contact_record.identifiers()) {
nearby::sharing::proto::Contact contact;
contact.mutable_identifier()->CopyFrom(identifier);
contact.set_is_selected(is_selected);
contacts.push_back(contact);
}
}
return contacts;
}
nearby::sharing::proto::Contact CreateLocalContact(
const std::string& profile_user_name) {
nearby::sharing::proto::Contact contact;
contact.mutable_identifier()->set_account_name(profile_user_name);
// Always consider your own account a selected contact.
contact.set_is_selected(true);
return contact;
}
// Creates a hex-encoded hash of the contact data, implicitly including the
// allowlist, to be sent to the Nearby Share server. This hash is persisted and
// used to detect any changes to the user's contact list or allowlist since the
// last successful upload to the server. The hash is invariant under the
// ordering of |contacts|.
std::string ComputeHash(
const std::vector<nearby::sharing::proto::Contact>& contacts) {
// To ensure that the hash is invariant under ordering of input |contacts|,
// add all serialized protos to an ordered set. Then, incrementally calculate
// the hash as we itereate through the set.
std::set<std::string> serialized_contacts_set;
for (const nearby::sharing::proto::Contact& contact : contacts) {
serialized_contacts_set.insert(contact.SerializeAsString());
}
std::unique_ptr<crypto::SecureHash> hasher =
crypto::SecureHash::Create(crypto::SecureHash::Algorithm::SHA256);
for (const std::string& serialized_contact : serialized_contacts_set) {
hasher->Update(serialized_contact.data(), serialized_contact.size());
}
std::vector<uint8_t> hash(hasher->GetHashLength());
hasher->Finish(hash.data(), hash.size());
return base::HexEncode(hash);
}
nearby_share::mojom::ContactIdentifierPtr ProtoToMojo(
const nearby::sharing::proto::Contact_Identifier& identifier) {
switch (identifier.identifier_case()) {
case nearby::sharing::proto::Contact_Identifier::IdentifierCase::
kAccountName:
return nearby_share::mojom::ContactIdentifier::NewAccountName(
identifier.account_name());
case nearby::sharing::proto::Contact_Identifier::IdentifierCase::
kObfuscatedGaia:
return nearby_share::mojom::ContactIdentifier::NewObfuscatedGaia(
identifier.obfuscated_gaia());
case nearby::sharing::proto::Contact_Identifier::IdentifierCase::
kPhoneNumber:
return nearby_share::mojom::ContactIdentifier::NewPhoneNumber(
identifier.phone_number());
case nearby::sharing::proto::Contact_Identifier::IdentifierCase::
IDENTIFIER_NOT_SET:
break;
}
NOTREACHED_IN_MIGRATION();
return nullptr;
}
nearby_share::mojom::ContactRecordPtr ProtoToMojo(
const nearby::sharing::proto::ContactRecord& contact_record) {
nearby_share::mojom::ContactRecordPtr contact_record_ptr =
nearby_share::mojom::ContactRecord::New();
contact_record_ptr->id = contact_record.id();
contact_record_ptr->person_name = contact_record.person_name();
contact_record_ptr->image_url = GURL(contact_record.image_url());
for (const auto& identifier : contact_record.identifiers()) {
contact_record_ptr->identifiers.push_back(ProtoToMojo(identifier));
}
return contact_record_ptr;
}
// Note: This conversion preserves the ordering of |contacts|.
std::vector<nearby_share::mojom::ContactRecordPtr> ProtoToMojo(
const std::vector<nearby::sharing::proto::ContactRecord>& contacts) {
std::vector<nearby_share::mojom::ContactRecordPtr> mojo_contacts;
mojo_contacts.reserve(contacts.size());
for (const auto& contact_record : contacts) {
mojo_contacts.push_back(ProtoToMojo(contact_record));
}
return mojo_contacts;
}
void RecordAllowlistMetrics(size_t num_contacts,
size_t num_allowed_contacts,
PrefService* pref_service) {
// Only record metrics if the user is in selected-contacts visibility mode.
// Note: We should really use NearbyShareSettings to get the visibility.
// Because this is just for metrics, we read the pref directly for simplicity.
nearby_share::mojom::Visibility visibility =
static_cast<nearby_share::mojom::Visibility>(pref_service->GetInteger(
prefs::kNearbySharingBackgroundVisibilityName));
if (visibility != nearby_share::mojom::Visibility::kSelectedContacts)
return;
base::UmaHistogramCounts10000("Nearby.Share.Contacts.NumContacts.Selected",
num_allowed_contacts);
if (num_contacts != 0) {
base::UmaHistogramPercentage(
"Nearby.Share.Contacts.PercentSelected",
std::lround(100.0f * num_allowed_contacts / num_contacts));
}
}
} // namespace
// static
NearbyShareContactManagerImpl::Factory*
NearbyShareContactManagerImpl::Factory::test_factory_ = nullptr;
// static
std::unique_ptr<NearbyShareContactManager>
NearbyShareContactManagerImpl::Factory::Create(
PrefService* pref_service,
NearbyShareClientFactory* http_client_factory,
NearbyShareLocalDeviceDataManager* local_device_data_manager,
NearbyShareProfileInfoProvider* profile_info_provider) {
if (test_factory_) {
return test_factory_->CreateInstance(pref_service, http_client_factory,
local_device_data_manager,
profile_info_provider);
}
return base::WrapUnique(new NearbyShareContactManagerImpl(
pref_service, http_client_factory, local_device_data_manager,
profile_info_provider));
}
// static
void NearbyShareContactManagerImpl::Factory::SetFactoryForTesting(
Factory* test_factory) {
test_factory_ = test_factory;
}
NearbyShareContactManagerImpl::Factory::~Factory() = default;
NearbyShareContactManagerImpl::NearbyShareContactManagerImpl(
PrefService* pref_service,
NearbyShareClientFactory* http_client_factory,
NearbyShareLocalDeviceDataManager* local_device_data_manager,
NearbyShareProfileInfoProvider* profile_info_provider)
: pref_service_(pref_service),
http_client_factory_(http_client_factory),
local_device_data_manager_(local_device_data_manager),
profile_info_provider_(profile_info_provider),
periodic_contact_upload_scheduler_(
ash::nearby::NearbySchedulerFactory::CreatePeriodicScheduler(
kContactUploadPeriod,
/*retry_failures=*/false,
/*require_connectivity=*/true,
prefs::kNearbySharingSchedulerPeriodicContactUploadPrefName,
pref_service_,
base::BindRepeating(&NearbyShareContactManagerImpl::
OnPeriodicContactsUploadRequested,
base::Unretained(this)),
Feature::NS)),
contact_download_and_upload_scheduler_(
ash::nearby::NearbySchedulerFactory::CreatePeriodicScheduler(
kContactDownloadPeriod,
/*retry_failures=*/true,
/*require_connectivity=*/true,
prefs::kNearbySharingSchedulerContactDownloadAndUploadPrefName,
pref_service_,
base::BindRepeating(
&NearbyShareContactManagerImpl::OnContactsDownloadRequested,
base::Unretained(this)),
Feature::NS)) {}
NearbyShareContactManagerImpl::~NearbyShareContactManagerImpl() = default;
void NearbyShareContactManagerImpl::DownloadContacts() {
// Make sure the scheduler is running so we can retrieve contacts while
// onboarding.
Start();
contact_download_and_upload_scheduler_->MakeImmediateRequest();
}
void NearbyShareContactManagerImpl::SetAllowedContacts(
const std::set<std::string>& allowed_contact_ids) {
// If the allowlist changed, re-upload contacts to Nearby server.
if (SetAllowlist(allowed_contact_ids))
contact_download_and_upload_scheduler_->MakeImmediateRequest();
}
void NearbyShareContactManagerImpl::OnStart() {
periodic_contact_upload_scheduler_->Start();
contact_download_and_upload_scheduler_->Start();
}
void NearbyShareContactManagerImpl::OnStop() {
periodic_contact_upload_scheduler_->Stop();
contact_download_and_upload_scheduler_->Stop();
}
void NearbyShareContactManagerImpl::Bind(
mojo::PendingReceiver<nearby_share::mojom::ContactManager> receiver) {
receiver_set_.Add(this, std::move(receiver));
}
void NearbyShareContactManagerImpl::AddDownloadContactsObserver(
::mojo::PendingRemote<nearby_share::mojom::DownloadContactsObserver>
observer) {
observers_set_.Add(std::move(observer));
}
std::set<std::string> NearbyShareContactManagerImpl::GetAllowedContacts()
const {
std::set<std::string> allowlist;
for (const base::Value& id :
pref_service_->GetList(prefs::kNearbySharingAllowedContactsPrefName)) {
allowlist.insert(id.GetString());
}
return allowlist;
}
void NearbyShareContactManagerImpl::OnPeriodicContactsUploadRequested() {
CD_LOG(VERBOSE, Feature::NS)
<< __func__ << ": Periodic Nearby Share contacts upload requested. "
<< "Upload will occur after next contacts download.";
}
void NearbyShareContactManagerImpl::OnContactsDownloadRequested() {
CD_LOG(VERBOSE, Feature::NS)
<< __func__ << ": Nearby Share contacts download requested.";
DCHECK(!contact_downloader_);
contact_downloader_ = NearbyShareContactDownloaderImpl::Factory::Create(
local_device_data_manager_->GetId(), kContactDownloadRpcTimeout,
http_client_factory_,
base::BindOnce(&NearbyShareContactManagerImpl::OnContactsDownloadSuccess,
base::Unretained(this)),
base::BindOnce(&NearbyShareContactManagerImpl::OnContactsDownloadFailure,
base::Unretained(this)));
contact_downloader_->Run();
}
void NearbyShareContactManagerImpl::OnContactsDownloadSuccess(
std::vector<nearby::sharing::proto::ContactRecord> contacts,
uint32_t num_unreachable_contacts_filtered_out) {
contact_downloader_.reset();
CD_LOG(INFO, Feature::NS) << __func__ << ": Nearby Share download of "
<< contacts.size() << " contacts succeeded.";
// Remove contacts from the allowlist that are not in the contact list.
SetAllowlist(
RemoveNonexistentContactsFromAllowlist(GetAllowedContacts(), contacts));
// Notify observers that the contact list was downloaded.
std::set<std::string> allowed_contact_ids = GetAllowedContacts();
RecordAllowlistMetrics(contacts.size(), allowed_contact_ids.size(),
pref_service_);
NotifyAllObserversContactsDownloaded(allowed_contact_ids, contacts,
num_unreachable_contacts_filtered_out);
std::vector<nearby::sharing::proto::Contact> contacts_to_upload =
ContactRecordsToContacts(GetAllowedContacts(), contacts);
// Enable cross-device self-share by adding your account to the list of
// contacts. It is also marked as a selected contact.
std::optional<std::string> user_name =
profile_info_provider_->GetProfileUserName();
base::UmaHistogramBoolean("Nearby.Share.Contacts.CanGetProfileUserName",
user_name.has_value());
if (!user_name) {
CD_LOG(WARNING, Feature::NS)
<< __func__ << ": Profile user name is not valid; could not "
<< "add self to list of contacts to upload.";
} else {
contacts_to_upload.push_back(CreateLocalContact(*user_name));
}
std::string contact_upload_hash = ComputeHash(contacts_to_upload);
bool did_contacts_change_since_last_upload =
contact_upload_hash !=
pref_service_->GetString(prefs::kNearbySharingContactUploadHashPrefName);
if (did_contacts_change_since_last_upload) {
CD_LOG(VERBOSE, Feature::NS)
<< __func__ << ": Contact list or allowlist changed since last "
<< "successful upload to the Nearby Share server.";
}
// Request a contacts upload if the contact list or allowlist has changed
// since the last successful upload. Also request an upload periodically.
if (did_contacts_change_since_last_upload ||
periodic_contact_upload_scheduler_->IsWaitingForResult()) {
local_device_data_manager_->UploadContacts(
std::move(contacts_to_upload),
base::BindOnce(&NearbyShareContactManagerImpl::OnContactsUploadFinished,
weak_ptr_factory_.GetWeakPtr(),
did_contacts_change_since_last_upload,
contact_upload_hash));
return;
}
// No upload is needed.
contact_download_and_upload_scheduler_->HandleResult(/*success=*/true);
}
void NearbyShareContactManagerImpl::OnContactsDownloadFailure() {
contact_downloader_.reset();
CD_LOG(WARNING, Feature::NS)
<< __func__ << ": Nearby Share contacts download failed.";
// Notify mojo remotes.
for (auto& remote : observers_set_) {
remote->OnContactsDownloadFailed();
}
contact_download_and_upload_scheduler_->HandleResult(/*success=*/false);
}
void NearbyShareContactManagerImpl::OnContactsUploadFinished(
bool did_contacts_change_since_last_upload,
const std::string& contact_upload_hash,
bool success) {
CD_LOG(INFO, Feature::NS)
<< __func__ << ": Upload of contacts to Nearby Share server "
<< (success ? "succeeded." : "failed.")
<< " Contact upload hash: " << contact_upload_hash;
if (success) {
// Only resolve the periodic upload request on success; let the
// download-and-upload scheduler handle any failure retries. The periodic
// upload scheduler will remember that it has an outstanding request even
// after reboot.
if (periodic_contact_upload_scheduler_->IsWaitingForResult()) {
periodic_contact_upload_scheduler_->HandleResult(success);
}
pref_service_->SetString(prefs::kNearbySharingContactUploadHashPrefName,
contact_upload_hash);
NotifyContactsUploaded(did_contacts_change_since_last_upload);
}
contact_download_and_upload_scheduler_->HandleResult(success);
}
bool NearbyShareContactManagerImpl::SetAllowlist(
const std::set<std::string>& new_allowlist) {
if (new_allowlist == GetAllowedContacts())
return false;
base::Value::List allowlist_value;
for (const std::string& id : new_allowlist) {
allowlist_value.Append(id);
}
pref_service_->SetList(prefs::kNearbySharingAllowedContactsPrefName,
std::move(allowlist_value));
return true;
}
void NearbyShareContactManagerImpl::NotifyAllObserversContactsDownloaded(
const std::set<std::string>& allowed_contact_ids,
const std::vector<nearby::sharing::proto::ContactRecord>& contacts,
uint32_t num_unreachable_contacts_filtered_out) {
// Sort the contacts before sending the list to observers.
std::vector<nearby::sharing::proto::ContactRecord> sorted_contacts = contacts;
SortNearbyShareContactRecords(&sorted_contacts);
// First, notify NearbyShareContactManager::Observers.
// Note: These are direct observers of the NearbyShareContactManager base
// class, distinct from the mojo remote observers that we notify below.
NotifyContactsDownloaded(allowed_contact_ids, sorted_contacts,
num_unreachable_contacts_filtered_out);
// Next, notify mojo remote observers.
if (observers_set_.empty()) {
return;
}
// Mojo doesn't have sets, so we have to copy to an array.
std::vector<std::string> allowed_contact_ids_vector(
allowed_contact_ids.begin(), allowed_contact_ids.end());
for (auto& remote : observers_set_) {
remote->OnContactsDownloaded(allowed_contact_ids_vector,
ProtoToMojo(sorted_contacts),
num_unreachable_contacts_filtered_out);
}
}