| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // 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/memory/ptr_util.h" |
| #include "base/stl_util.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/contacts/nearby_share_contact_downloader.h" |
| #include "chrome/browser/nearby_sharing/contacts/nearby_share_contact_downloader_impl.h" |
| #include "chrome/browser/nearby_sharing/local_device_data/nearby_share_local_device_data_manager.h" |
| #include "chrome/browser/nearby_sharing/logging/logging.h" |
| #include "chrome/browser/nearby_sharing/proto/device_rpc.pb.h" |
| #include "chrome/browser/nearby_sharing/proto/rpc_resources.pb.h" |
| #include "chrome/browser/nearby_sharing/scheduling/nearby_share_scheduler.h" |
| #include "chrome/browser/nearby_sharing/scheduling/nearby_share_scheduler_factory.h" |
| #include "chrome/browser/ui/webui/nearby_share/public/mojom/nearby_share_settings.mojom-shared.h" |
| #include "chrome/browser/ui/webui/nearby_share/public/mojom/nearby_share_settings.mojom.h" |
| #include "components/prefs/pref_service.h" |
| #include "crypto/secure_hash.h" |
| |
| namespace { |
| |
| constexpr base::TimeDelta kContactDownloadPeriod = |
| base::TimeDelta::FromHours(12); |
| constexpr base::TimeDelta kContactDownloadRpcTimeout = |
| base::TimeDelta::FromSeconds(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<nearbyshare::proto::ContactRecord>& contacts) { |
| std::set<std::string> new_allowed_contact_ids; |
| for (const nearbyshare::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<nearbyshare::proto::Contact> ContactRecordsToContacts( |
| const std::set<std::string>& allowed_contact_ids, |
| const std::vector<nearbyshare::proto::ContactRecord>& contact_records) { |
| std::vector<nearbyshare::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()) { |
| nearbyshare::proto::Contact contact; |
| contact.mutable_identifier()->CopyFrom(identifier); |
| contact.set_is_selected(is_selected); |
| contacts.push_back(contact); |
| } |
| } |
| return contacts; |
| } |
| |
| nearbyshare::proto::Contact CreateLocalContact( |
| const std::string& profile_user_name) { |
| nearbyshare::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. |
| std::string ComputeHash( |
| const std::vector<nearbyshare::proto::Contact>& contacts) { |
| std::unique_ptr<crypto::SecureHash> hasher = |
| crypto::SecureHash::Create(crypto::SecureHash::Algorithm::SHA256); |
| |
| for (const nearbyshare::proto::Contact& contact : contacts) { |
| std::string serialized = contact.SerializeAsString(); |
| hasher->Update(serialized.data(), serialized.size()); |
| } |
| |
| std::vector<uint8_t> hash(hasher->GetHashLength()); |
| hasher->Finish(hash.data(), hash.size()); |
| |
| return base::HexEncode(hash); |
| } |
| |
| nearby_share::mojom::ContactIdentifierPtr ProtoToMojo( |
| const nearbyshare::proto::Contact_Identifier& identifier) { |
| nearby_share::mojom::ContactIdentifierPtr identifier_ptr = |
| nearby_share::mojom::ContactIdentifier::New(); |
| switch (identifier.identifier_case()) { |
| case nearbyshare::proto::Contact_Identifier::IdentifierCase::kAccountName: |
| identifier_ptr->set_account_name(identifier.account_name()); |
| break; |
| case nearbyshare::proto::Contact_Identifier::IdentifierCase:: |
| kObfuscatedGaia: |
| identifier_ptr->set_obfuscated_gaia(identifier.obfuscated_gaia()); |
| break; |
| case nearbyshare::proto::Contact_Identifier::IdentifierCase::kPhoneNumber: |
| identifier_ptr->set_phone_number(identifier.phone_number()); |
| break; |
| case nearbyshare::proto::Contact_Identifier::IdentifierCase:: |
| IDENTIFIER_NOT_SET: |
| NOTREACHED(); |
| break; |
| } |
| return identifier_ptr; |
| } |
| |
| nearby_share::mojom::ContactRecordPtr ProtoToMojo( |
| const nearbyshare::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; |
| } |
| |
| std::vector<nearby_share::mojom::ContactRecordPtr> ProtoToMojo( |
| const std::vector<nearbyshare::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; |
| } |
| |
| } // 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, |
| const std::string& profile_user_name) { |
| if (test_factory_) { |
| return test_factory_->CreateInstance(pref_service, http_client_factory, |
| local_device_data_manager, |
| profile_user_name); |
| } |
| return base::WrapUnique(new NearbyShareContactManagerImpl( |
| pref_service, http_client_factory, local_device_data_manager, |
| profile_user_name)); |
| } |
| |
| // 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, |
| const std::string& profile_user_name) |
| : pref_service_(pref_service), |
| http_client_factory_(http_client_factory), |
| local_device_data_manager_(local_device_data_manager), |
| profile_user_name_(profile_user_name), |
| contact_download_and_upload_scheduler_( |
| NearbyShareSchedulerFactory::CreatePeriodicScheduler( |
| kContactDownloadPeriod, |
| /*retry_failures=*/true, |
| /*require_connectivity=*/true, |
| prefs::kNearbySharingSchedulerContactDownloadAndUploadPrefName, |
| pref_service_, |
| base::BindRepeating( |
| &NearbyShareContactManagerImpl::OnContactsDownloadRequested, |
| base::Unretained(this)))) {} |
| |
| 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() { |
| contact_download_and_upload_scheduler_->Start(); |
| } |
| |
| void NearbyShareContactManagerImpl::OnStop() { |
| 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_->Get(prefs::kNearbySharingAllowedContactsPrefName) |
| ->GetList()) { |
| allowlist.insert(id.GetString()); |
| } |
| return allowlist; |
| } |
| |
| void NearbyShareContactManagerImpl::OnContactsDownloadRequested() { |
| NS_LOG(VERBOSE) << __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<nearbyshare::proto::ContactRecord> contacts, |
| uint32_t num_unreachable_contacts_filtered_out) { |
| contact_downloader_.reset(); |
| |
| NS_LOG(VERBOSE) << __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(); |
| NotifyContactsDownloaded(allowed_contact_ids, contacts, |
| num_unreachable_contacts_filtered_out); |
| NotifyMojoObserverContactsDownloaded(allowed_contact_ids, contacts, |
| num_unreachable_contacts_filtered_out); |
| |
| std::vector<nearbyshare::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. |
| if (profile_user_name_.empty()) { |
| NS_LOG(WARNING) << __func__ << ": Profile user name is empty; could not " |
| << "add self to list of contacts to upload."; |
| } else { |
| contacts_to_upload.push_back(CreateLocalContact(profile_user_name_)); |
| } |
| |
| // Only request a contacts upload if the contact list or allowlist has changed |
| // since the last successful upload. |
| std::string contact_upload_hash = ComputeHash(contacts_to_upload); |
| if (contact_upload_hash == |
| pref_service_->GetString( |
| prefs::kNearbySharingContactUploadHashPrefName)) { |
| contact_download_and_upload_scheduler_->HandleResult(/*success=*/true); |
| return; |
| } |
| |
| NS_LOG(VERBOSE) << __func__ |
| << ": Contact list or allowlist changed since last " |
| << "successful upload to the Nearby Share server. " |
| << "Starting contacts upload."; |
| local_device_data_manager_->UploadContacts( |
| std::move(contacts_to_upload), |
| base::BindOnce(&NearbyShareContactManagerImpl::OnContactsUploadFinished, |
| weak_ptr_factory_.GetWeakPtr(), contact_upload_hash)); |
| } |
| |
| void NearbyShareContactManagerImpl::OnContactsDownloadFailure() { |
| contact_downloader_.reset(); |
| |
| NS_LOG(WARNING) << __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( |
| const std::string& contact_upload_hash, |
| bool success) { |
| NS_LOG(VERBOSE) << __func__ << ": Upload of contacts to Nearby Share server " |
| << (success ? "succeeded." : "failed.") |
| << " Contact upload hash: " << contact_upload_hash; |
| if (success) { |
| pref_service_->SetString(prefs::kNearbySharingContactUploadHashPrefName, |
| contact_upload_hash); |
| NotifyContactsUploaded(/*did_contacts_change_since_last_upload=*/true); |
| } |
| |
| 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 allowlist_value(base::Value::Type::LIST); |
| for (const std::string& id : new_allowlist) { |
| allowlist_value.Append(id); |
| } |
| pref_service_->Set(prefs::kNearbySharingAllowedContactsPrefName, |
| std::move(allowlist_value)); |
| |
| return true; |
| } |
| |
| void NearbyShareContactManagerImpl::NotifyMojoObserverContactsDownloaded( |
| const std::set<std::string>& allowed_contact_ids, |
| const std::vector<nearbyshare::proto::ContactRecord>& contacts, |
| uint32_t num_unreachable_contacts_filtered_out) { |
| 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()); |
| |
| // Notify mojo remotes. |
| for (auto& remote : observers_set_) { |
| remote->OnContactsDownloaded(allowed_contact_ids_vector, |
| ProtoToMojo(contacts), |
| num_unreachable_contacts_filtered_out); |
| } |
| } |