blob: d45473d065977da5e879431102a0b25d97329d2e [file] [log] [blame]
// Copyright 2018 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 "chromeos/services/multidevice_setup/host_backend_delegate_impl.h"
#include <algorithm>
#include <sstream>
#include "base/bind.h"
#include "base/memory/ptr_util.h"
#include "base/no_destructor.h"
#include "base/stl_util.h"
#include "chromeos/components/multidevice/logging/logging.h"
#include "chromeos/components/multidevice/software_feature.h"
#include "chromeos/components/multidevice/software_feature_state.h"
#include "chromeos/services/multidevice_setup/eligible_host_devices_provider.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
namespace chromeos {
namespace multidevice_setup {
namespace {
// Name of the pref which stores the device ID of the host which is pending
// being set on the back-end.
const char kPendingRequestHostIdPrefName[] =
"multidevice_setup.pending_request_host_id";
// String to use for the pending host ID preference entry when the pending
// request is to remove the current host.
const char kPendingRemovalOfCurrentHost[] = "pendingRemovalOfCurrentHost";
// String to use for the pending host ID when there is no pending request.
const char kNoPendingRequest[] = "";
// The number of minutes to wait before retrying a failed attempt.
const int kNumMinutesBetweenRetries = 5;
const char kNoHostForLogging[] = "[no host]";
} // namespace
// static
HostBackendDelegateImpl::Factory*
HostBackendDelegateImpl::Factory::test_factory_ = nullptr;
// static
HostBackendDelegateImpl::Factory* HostBackendDelegateImpl::Factory::Get() {
if (test_factory_)
return test_factory_;
static base::NoDestructor<Factory> factory;
return factory.get();
}
// static
void HostBackendDelegateImpl::Factory::SetFactoryForTesting(
Factory* test_factory) {
test_factory_ = test_factory;
}
HostBackendDelegateImpl::Factory::~Factory() = default;
std::unique_ptr<HostBackendDelegate>
HostBackendDelegateImpl::Factory::BuildInstance(
EligibleHostDevicesProvider* eligible_host_devices_provider,
PrefService* pref_service,
device_sync::DeviceSyncClient* device_sync_client,
std::unique_ptr<base::OneShotTimer> timer) {
return base::WrapUnique(
new HostBackendDelegateImpl(eligible_host_devices_provider, pref_service,
device_sync_client, std::move(timer)));
}
// static
void HostBackendDelegateImpl::RegisterPrefs(PrefRegistrySimple* registry) {
registry->RegisterStringPref(kPendingRequestHostIdPrefName,
kNoPendingRequest);
}
HostBackendDelegateImpl::HostBackendDelegateImpl(
EligibleHostDevicesProvider* eligible_host_devices_provider,
PrefService* pref_service,
device_sync::DeviceSyncClient* device_sync_client,
std::unique_ptr<base::OneShotTimer> timer)
: HostBackendDelegate(),
eligible_host_devices_provider_(eligible_host_devices_provider),
pref_service_(pref_service),
device_sync_client_(device_sync_client),
timer_(std::move(timer)),
weak_ptr_factory_(this) {
device_sync_client_->AddObserver(this);
host_from_last_sync_ = GetHostFromDeviceSync();
if (HasPendingHostRequest())
AttemptNetworkRequest(false /* is_retry */);
}
HostBackendDelegateImpl::~HostBackendDelegateImpl() {
device_sync_client_->RemoveObserver(this);
}
void HostBackendDelegateImpl::AttemptToSetMultiDeviceHostOnBackend(
const base::Optional<multidevice::RemoteDeviceRef>& host_device) {
if (host_device && !IsHostEligible(*host_device)) {
PA_LOG(WARNING) << "HostBackendDelegateImpl::"
<< "AttemptToSetMultiDeviceHostOnBackend(): Tried to set a "
<< "device as host, but that device is not an eligible "
<< "host. Device ID: "
<< host_device->GetTruncatedDeviceIdForLogs();
return;
}
// If the device on the back-end is already |host_device|, there no longer
// needs to be a pending request.
if (host_from_last_sync_ == host_device) {
SetPendingHostRequest(kNoPendingRequest);
return;
}
// Stop the timer, since a new attempt is being started.
timer_->Stop();
if (host_device)
SetPendingHostRequest(host_device->GetDeviceId());
else
SetPendingHostRequest(kPendingRemovalOfCurrentHost);
AttemptNetworkRequest(false /* is_retry */);
}
bool HostBackendDelegateImpl::HasPendingHostRequest() {
const std::string pending_host_id_from_prefs =
pref_service_->GetString(kPendingRequestHostIdPrefName);
if (pending_host_id_from_prefs == kNoPendingRequest)
return false;
if (pending_host_id_from_prefs == kPendingRemovalOfCurrentHost) {
// If the pending request is to remove the current host but there is no
// current host, the host was removed by another device while this device
// was offline.
if (!host_from_last_sync_) {
SetPendingHostRequest(kNoPendingRequest);
return false;
}
// Otherwise, there still is a pending request to remove the current host.
return true;
}
// By this point, |pending_host_id_from_prefs| refers to a real device ID and
// not one of the two sentinel values.
for (const auto& remote_device : device_sync_client_->GetSyncedDevices()) {
if (pending_host_id_from_prefs == remote_device.GetDeviceId())
return true;
}
// If a request was pending for a specific host device, but that device is no
// longer present on the user's account, there is no longer a pending request.
SetPendingHostRequest(kNoPendingRequest);
return false;
}
base::Optional<multidevice::RemoteDeviceRef>
HostBackendDelegateImpl::GetPendingHostRequest() const {
const std::string pending_host_id_from_prefs =
pref_service_->GetString(kPendingRequestHostIdPrefName);
if (pending_host_id_from_prefs == kNoPendingRequest) {
PA_LOG(ERROR) << "HostBackendDelegateImpl::GetPendingHostRequest(): Tried "
<< "to get pending host request, but there was no pending "
<< "host request.";
NOTREACHED();
}
if (pending_host_id_from_prefs == kPendingRemovalOfCurrentHost)
return base::nullopt;
for (const auto& remote_device : device_sync_client_->GetSyncedDevices()) {
if (pending_host_id_from_prefs == remote_device.GetDeviceId())
return remote_device;
}
PA_LOG(ERROR) << "HostBackendDelegateImpl::GetPendingHostRequest(): Tried to "
<< "get pending host request, but the pending host ID was not "
<< "present.";
NOTREACHED();
return base::nullopt;
}
base::Optional<multidevice::RemoteDeviceRef>
HostBackendDelegateImpl::GetMultiDeviceHostFromBackend() const {
return host_from_last_sync_;
}
bool HostBackendDelegateImpl::IsHostEligible(
const multidevice::RemoteDeviceRef& provided_host) {
return base::ContainsValue(
eligible_host_devices_provider_->GetEligibleHostDevices(), provided_host);
}
void HostBackendDelegateImpl::SetPendingHostRequest(
const std::string& pending_host_id) {
const std::string host_id_from_prefs_before_call =
pref_service_->GetString(kPendingRequestHostIdPrefName);
if (pending_host_id == host_id_from_prefs_before_call)
return;
pref_service_->SetString(kPendingRequestHostIdPrefName, pending_host_id);
timer_->Stop();
NotifyPendingHostRequestChange();
}
void HostBackendDelegateImpl::AttemptNetworkRequest(bool is_retry) {
if (!HasPendingHostRequest()) {
PA_LOG(ERROR) << "HostBackendDelegateImpl::AttemptNetworkRequest(): Tried "
<< "to attempt a network request, but there was no pending "
<< "host request.";
NOTREACHED();
}
base::Optional<multidevice::RemoteDeviceRef> pending_host_request =
GetPendingHostRequest();
// If |pending_host_request| is non-null, the request should be to set that
// device. If it is null, the pending request is to remove the current host.
multidevice::RemoteDeviceRef device_to_set =
pending_host_request ? *pending_host_request : *host_from_last_sync_;
// Likewise, if |pending_host_request| is non-null, that device should be
// enabled, and if it is null, the old device should be disabled.
bool should_enable = pending_host_request != base::nullopt;
PA_LOG(INFO) << "HostBackendDelegateImpl::AttemptNetworkRequest(): "
<< (is_retry ? "Retrying attempt" : "Attempting") << " to "
<< (should_enable ? "enable" : "disable") << " the host with ID "
<< device_to_set.GetTruncatedDeviceIdForLogs() << ".";
device_sync_client_->SetSoftwareFeatureState(
device_to_set.public_key(),
multidevice::SoftwareFeature::kBetterTogetherHost,
should_enable /* enabled */, should_enable /* is_exclusive */,
base::BindOnce(&HostBackendDelegateImpl::OnSetSoftwareFeatureStateResult,
weak_ptr_factory_.GetWeakPtr(), device_to_set,
should_enable));
}
void HostBackendDelegateImpl::OnNewDevicesSynced() {
base::Optional<multidevice::RemoteDeviceRef> host_from_sync =
GetHostFromDeviceSync();
if (host_from_last_sync_ == host_from_sync)
return;
std::string old_host_id =
host_from_last_sync_ ? host_from_last_sync_->GetTruncatedDeviceIdForLogs()
: kNoHostForLogging;
std::string new_host_id = host_from_sync
? host_from_sync->GetTruncatedDeviceIdForLogs()
: kNoHostForLogging;
host_from_last_sync_ = host_from_sync;
PA_LOG(VERBOSE) << "HostBackendDelegateImpl::OnNewDevicesSynced(): New host "
<< "device has been set. Old host device ID: " << old_host_id
<< ", New host device ID: " << new_host_id;
// If there is a pending request and the new host fulfills that pending
// request, there is no longer a pending request.
if (HasPendingHostRequest() &&
host_from_last_sync_ == GetPendingHostRequest()) {
SetPendingHostRequest(kNoPendingRequest);
}
NotifyHostChangedOnBackend();
}
base::Optional<multidevice::RemoteDeviceRef>
HostBackendDelegateImpl::GetHostFromDeviceSync() {
multidevice::RemoteDeviceRefList synced_devices =
device_sync_client_->GetSyncedDevices();
auto it = std::find_if(
synced_devices.begin(), synced_devices.end(),
[](const auto& remote_device) {
multidevice::SoftwareFeatureState host_state =
remote_device.GetSoftwareFeatureState(
multidevice::SoftwareFeature::kBetterTogetherHost);
return host_state == multidevice::SoftwareFeatureState::kEnabled;
});
if (it == synced_devices.end())
return base::nullopt;
return *it;
}
void HostBackendDelegateImpl::OnSetSoftwareFeatureStateResult(
multidevice::RemoteDeviceRef device_for_request,
bool attempted_to_enable,
device_sync::mojom::NetworkRequestResult result_code) {
bool success =
result_code == device_sync::mojom::NetworkRequestResult::kSuccess;
std::stringstream ss;
ss << "HostBackendDelegateImpl::OnSetSoftwareFeatureStateResult(): "
<< (success ? "Completed successful" : "Failure requesting") << " "
<< "host change. Device ID: "
<< device_for_request.GetTruncatedDeviceIdForLogs()
<< ", Attempted to enable: " << (attempted_to_enable ? "true" : "false");
if (success) {
PA_LOG(VERBOSE) << ss.str();
return;
}
ss << ", Error code: " << result_code;
PA_LOG(WARNING) << ss.str();
if (!HasPendingHostRequest())
return;
base::Optional<multidevice::RemoteDeviceRef> pending_host_request =
GetPendingHostRequest();
bool failed_request_was_to_set_pending_host =
attempted_to_enable && pending_host_request &&
*pending_host_request == device_for_request;
bool failed_request_was_to_remove_pending_host =
!attempted_to_enable && !pending_host_request &&
device_for_request == host_from_last_sync_;
// If the request which failed corresponds to the most recent call to
// AttemptToSetMultiDeviceHostOnBackend(), alert observers that this request
// failed and schedule a retry.
if (failed_request_was_to_set_pending_host ||
failed_request_was_to_remove_pending_host) {
NotifyBackendRequestFailed();
timer_->Start(FROM_HERE,
base::TimeDelta::FromMinutes(kNumMinutesBetweenRetries),
base::Bind(&HostBackendDelegateImpl::AttemptNetworkRequest,
base::Unretained(this), true /* is_retry */));
}
}
} // namespace multidevice_setup
} // namespace chromeos