// 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 "chromeos/ash/components/phonehub/feature_status_provider_impl.h"
#include <utility>
#include "ash/constants/ash_features.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/trace_event/trace_event.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/multidevice/remote_device_ref.h"
#include "chromeos/ash/components/multidevice/software_feature.h"
#include "chromeos/ash/components/multidevice/software_feature_state.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
namespace ash::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 std::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 (feature_state == FeatureState::kNotSupportedByChromebook) {
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,
secure_channel::ConnectionManager* connection_manager,
session_manager::SessionManager* session_manager,
chromeos::PowerManagerClient* power_manager_client,
PhoneHubStructuredMetricsLogger* phone_hub_structured_metrics_logger)
: 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),
phone_hub_structured_metrics_logger_(
phone_hub_structured_metrics_logger) {
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 {
PA_LOG(VERBOSE) << __func__ << ": status = " << *status_;
return *status_;
}
void FeatureStatusProviderImpl::OnReady() {
UpdateStatus();
}
void FeatureStatusProviderImpl::OnNewDevicesSynced() {
if (features::IsPhoneHubOnboardingNotifierRevampEnabled() &&
ComputeStatus() == FeatureStatus::kEligiblePhoneButNotSetUp) {
CheckEligibleDevicesForNudge();
}
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() {
TRACE_EVENT0("login", "FeatureStatusProviderImpl::OnSessionStateChanged");
UpdateStatus();
}
void FeatureStatusProviderImpl::UpdateStatus() {
TRACE_EVENT0("ui", "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;
switch (status_.value()) {
case FeatureStatus::kDisabled:
case FeatureStatus::kLockOrSuspended:
phone_hub_structured_metrics_logger_->ResetSessionId();
break;
case FeatureStatus::kEligiblePhoneButNotSetUp:
case FeatureStatus::kNotEligibleForFeature:
case FeatureStatus::kPhoneSelectedAndPendingSetup:
phone_hub_structured_metrics_logger_->ResetCachedInformation();
break;
case FeatureStatus::kEnabledAndConnecting:
case FeatureStatus::kEnabledAndConnected:
case FeatureStatus::kUnavailableBluetoothOff:
case FeatureStatus::kEnabledButDisconnected:
break;
}
NotifyStatusChanged();
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 secure_channel::ConnectionManager::Status::kDisconnected:
return FeatureStatus::kEnabledButDisconnected;
case secure_channel::ConnectionManager::Status::kConnecting:
return FeatureStatus::kEnabledAndConnecting;
case secure_channel::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::SuspendImminent(
power_manager::SuspendImminent::Reason reason) {
PA_LOG(INFO) << "Device is suspending";
is_suspended_ = true;
UpdateStatus();
}
void FeatureStatusProviderImpl::SuspendDone(base::TimeDelta sleep_duration) {
PA_LOG(INFO) << "Device has stopped suspending";
is_suspended_ = false;
UpdateStatus();
}
void FeatureStatusProviderImpl::CheckEligibleDevicesForNudge() {
RemoteDeviceRefList eligible_devices;
for (const RemoteDeviceRef& device :
device_sync_client_->GetSyncedDevices()) {
if (IsEligiblePhoneHubHost(device)) {
eligible_devices.push_back(device);
}
}
NotifyEligibleDevicesFound(eligible_devices);
}
} // namespace ash::phonehub