blob: 26e636f9f76c1cba88e5bb739013e75bc91ef2b1 [file] [log] [blame]
// 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 "chromeos/components/phonehub/feature_status_provider_impl.h"
#include <utility>
#include "base/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chromeos/components/multidevice/logging/logging.h"
#include "chromeos/components/multidevice/remote_device_ref.h"
#include "chromeos/components/multidevice/software_feature.h"
#include "chromeos/components/multidevice/software_feature_state.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
namespace chromeos {
namespace phonehub {
namespace {
using multidevice::RemoteDeviceRef;
using multidevice::RemoteDeviceRefList;
using multidevice::SoftwareFeature;
using multidevice::SoftwareFeatureState;
using multidevice_setup::mojom::Feature;
using multidevice_setup::mojom::FeatureState;
using multidevice_setup::mojom::HostStatus;
bool IsEligiblePhoneHubHost(const RemoteDeviceRef& device) {
// Device must be capable of being a multi-device host.
if (device.GetSoftwareFeatureState(SoftwareFeature::kBetterTogetherHost) ==
SoftwareFeatureState::kNotSupported) {
return false;
}
if (device.GetSoftwareFeatureState(SoftwareFeature::kPhoneHubHost) ==
SoftwareFeatureState::kNotSupported) {
return false;
}
// Device must have a synced Bluetooth public address, which is used to
// bootstrap Phone Hub connections.
return !device.bluetooth_public_address().empty();
}
bool IsEligibleForFeature(
const base::Optional<multidevice::RemoteDeviceRef>& local_device,
multidevice_setup::MultiDeviceSetupClient::HostStatusWithDevice host_status,
const RemoteDeviceRefList& remote_devices,
FeatureState feature_state) {
// If the feature is prohibited by policy, we don't initialize Phone Hub
// classes at all. But, there is an edge case where a user session starts up
// normally, then an administrator prohibits the policy during the user
// session. If this occurs, we consider the session ineligible for using Phone
// Hub.
if (feature_state == FeatureState::kProhibitedByPolicy)
return false;
// If the local device has not yet been enrolled, no phone can serve as its
// Phone Hub host.
if (!local_device)
return false;
// If the local device does not support being a Phone Hub client, no phone can
// serve as its host.
if (local_device->GetSoftwareFeatureState(SoftwareFeature::kPhoneHubClient) ==
SoftwareFeatureState::kNotSupported) {
return false;
}
// If the local device does not have an enrolled Bluetooth address, no phone
// can serve as its host.
if (local_device->bluetooth_public_address().empty())
return false;
// If the host device is not an eligible host, do not initialize Phone Hub.
if (host_status.first == HostStatus::kNoEligibleHosts)
return false;
// If there is a host device available, check if the device is eligible for
// Phonehub.
if (host_status.second.has_value())
return IsEligiblePhoneHubHost(*(host_status.second));
// Otherwise, check if there is any available remote device that is
// eligible for Phonehub.
for (const RemoteDeviceRef& device : remote_devices) {
if (IsEligiblePhoneHubHost(device))
return true;
}
// If none of the devices return true above, there are no phones capable of
// Phone Hub connections on the account.
return false;
}
bool IsPhonePendingSetup(HostStatus host_status, FeatureState feature_state) {
// The user has completed the opt-in flow, but we have not yet notified the
// back-end of this selection. One common cause of this state is when the user
// completes setup while offline.
if (host_status ==
HostStatus::kHostSetLocallyButWaitingForBackendConfirmation) {
return true;
}
// The device has been set up with the back-end, but the phone has not yet
// enabled itself.
if (host_status == HostStatus::kHostSetButNotYetVerified)
return true;
// The phone has enabled itself for the multi-device suite but has not yet
// enabled itself for Phone Hub. Note that kNotSupportedByPhone is a bit of a
// misnomer here; this value means that the phone has advertised support for
// the feature but has not yet enabled it.
return host_status == HostStatus::kHostVerified &&
feature_state == FeatureState::kNotSupportedByPhone;
}
bool IsFeatureDisabledByUser(FeatureState feature_state) {
return feature_state == FeatureState::kDisabledByUser ||
feature_state == FeatureState::kUnavailableSuiteDisabled ||
feature_state == FeatureState::kUnavailableTopLevelFeatureDisabled;
}
} // namespace
FeatureStatusProviderImpl::FeatureStatusProviderImpl(
device_sync::DeviceSyncClient* device_sync_client,
multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client,
ConnectionManager* connection_manager,
session_manager::SessionManager* session_manager,
PowerManagerClient* power_manager_client)
: device_sync_client_(device_sync_client),
multidevice_setup_client_(multidevice_setup_client),
connection_manager_(connection_manager),
session_manager_(session_manager),
power_manager_client_(power_manager_client) {
DCHECK(session_manager_);
DCHECK(power_manager_client_);
device_sync_client_->AddObserver(this);
multidevice_setup_client_->AddObserver(this);
connection_manager_->AddObserver(this);
session_manager_->AddObserver(this);
power_manager_client_->AddObserver(this);
device::BluetoothAdapterFactory::Get()->GetAdapter(
base::BindOnce(&FeatureStatusProviderImpl::OnBluetoothAdapterReceived,
weak_ptr_factory_.GetWeakPtr()));
status_ = ComputeStatus();
}
FeatureStatusProviderImpl::~FeatureStatusProviderImpl() {
device_sync_client_->RemoveObserver(this);
multidevice_setup_client_->RemoveObserver(this);
connection_manager_->RemoveObserver(this);
if (bluetooth_adapter_)
bluetooth_adapter_->RemoveObserver(this);
session_manager_->RemoveObserver(this);
power_manager_client_->RemoveObserver(this);
}
FeatureStatus FeatureStatusProviderImpl::GetStatus() const {
return *status_;
}
void FeatureStatusProviderImpl::OnReady() {
UpdateStatus();
// The status may change a few times before initialization is
// complete. Before the login status is recorded, all asynchronous
// action should be complete. Note that scheduling
// RecordFeatureStatusOnLogin() with BEST_EFFORT sooner (e.g in the
// constructor) may yield an incorrect metric, because there may be many
// cycles between the constructor being called and |device_sync_client_| being
// ready, allowing tasks posted even with BEST_EFFORT to succeed before
// initialization.
base::ThreadPool::PostTask(
FROM_HERE, {base::TaskPriority::BEST_EFFORT},
base::BindOnce(&FeatureStatusProviderImpl::RecordFeatureStatusOnLogin,
weak_ptr_factory_.GetWeakPtr()));
}
void FeatureStatusProviderImpl::OnNewDevicesSynced() {
UpdateStatus();
}
void FeatureStatusProviderImpl::OnHostStatusChanged(
const multidevice_setup::MultiDeviceSetupClient::HostStatusWithDevice&
host_device_with_status) {
UpdateStatus();
}
void FeatureStatusProviderImpl::OnFeatureStatesChanged(
const multidevice_setup::MultiDeviceSetupClient::FeatureStatesMap&
feature_states_map) {
UpdateStatus();
}
void FeatureStatusProviderImpl::AdapterPresentChanged(
device::BluetoothAdapter* adapter,
bool present) {
UpdateStatus();
}
void FeatureStatusProviderImpl::AdapterPoweredChanged(
device::BluetoothAdapter* adapter,
bool powered) {
UpdateStatus();
}
void FeatureStatusProviderImpl::OnBluetoothAdapterReceived(
scoped_refptr<device::BluetoothAdapter> bluetooth_adapter) {
bluetooth_adapter_ = std::move(bluetooth_adapter);
bluetooth_adapter_->AddObserver(this);
// If |status_| has not yet been set, this call occurred synchronously in the
// constructor, so status_ has not yet been initialized.
if (status_.has_value())
UpdateStatus();
}
void FeatureStatusProviderImpl::OnConnectionStatusChanged() {
UpdateStatus();
}
void FeatureStatusProviderImpl::OnSessionStateChanged() {
UpdateStatus();
}
void FeatureStatusProviderImpl::UpdateStatus() {
DCHECK(status_.has_value());
FeatureStatus computed_status = ComputeStatus();
if (computed_status == *status_)
return;
PA_LOG(INFO) << "Phone Hub feature status: " << *status_ << " => "
<< computed_status;
*status_ = computed_status;
NotifyStatusChanged();
if (!is_login_status_metric_recorded_)
return;
UMA_HISTOGRAM_ENUMERATION("PhoneHub.Adoption.FeatureStatusChangesSinceLogin",
GetStatus());
}
FeatureStatus FeatureStatusProviderImpl::ComputeStatus() {
FeatureState feature_state =
multidevice_setup_client_->GetFeatureState(Feature::kPhoneHub);
HostStatus host_status = multidevice_setup_client_->GetHostStatus().first;
// Note: If |device_sync_client_| is not yet ready, it has not initialized
// itself with device metadata, so we assume that we are ineligible for the
// feature until proven otherwise.
if (!device_sync_client_->is_ready() ||
!IsEligibleForFeature(device_sync_client_->GetLocalDeviceMetadata(),
multidevice_setup_client_->GetHostStatus(),
device_sync_client_->GetSyncedDevices(),
feature_state)) {
return FeatureStatus::kNotEligibleForFeature;
}
if (session_manager_->IsScreenLocked() || is_suspended_)
return FeatureStatus::kLockOrSuspended;
if (host_status == HostStatus::kEligibleHostExistsButNoHostSet)
return FeatureStatus::kEligiblePhoneButNotSetUp;
if (IsPhonePendingSetup(host_status, feature_state))
return FeatureStatus::kPhoneSelectedAndPendingSetup;
if (IsFeatureDisabledByUser(feature_state))
return FeatureStatus::kDisabled;
if (!IsBluetoothOn())
return FeatureStatus::kUnavailableBluetoothOff;
switch (connection_manager_->GetStatus()) {
case ConnectionManager::Status::kDisconnected:
return FeatureStatus::kEnabledButDisconnected;
case ConnectionManager::Status::kConnecting:
return FeatureStatus::kEnabledAndConnecting;
case ConnectionManager::Status::kConnected:
return FeatureStatus::kEnabledAndConnected;
}
return FeatureStatus::kEnabledButDisconnected;
}
bool FeatureStatusProviderImpl::IsBluetoothOn() const {
if (!bluetooth_adapter_)
return false;
return bluetooth_adapter_->IsPresent() && bluetooth_adapter_->IsPowered();
}
void FeatureStatusProviderImpl::RecordFeatureStatusOnLogin() {
UMA_HISTOGRAM_ENUMERATION("PhoneHub.Adoption.LoginFeatureStatus",
GetStatus());
is_login_status_metric_recorded_ = true;
}
void FeatureStatusProviderImpl::SuspendImminent(
power_manager::SuspendImminent::Reason reason) {
PA_LOG(INFO) << "Device is suspending";
is_suspended_ = true;
UpdateStatus();
}
void FeatureStatusProviderImpl::SuspendDone(
const base::TimeDelta& sleep_duration) {
PA_LOG(INFO) << "Device has stopped suspending";
is_suspended_ = false;
UpdateStatus();
}
} // namespace phonehub
} // namespace chromeos