// Copyright 2018 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/services/multidevice_setup/feature_state_manager_impl.h"
#include <array>
#include <optional>
#include "ash/constants/ash_features.h"
#include "base/containers/contains.h"
#include "base/containers/flat_map.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.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/services/multidevice_setup/global_state_feature_manager.h"
#include "chromeos/ash/services/multidevice_setup/public/cpp/prefs.h"
#include "components/prefs/pref_service.h"
namespace ash {
namespace multidevice_setup {
namespace {
constexpr base::TimeDelta kFeatureStateLoggingPeriod = base::Minutes(30);
constexpr std::array<mojom::Feature, 4> kPhoneHubSubFeatures{
mojom::Feature::kPhoneHubNotifications, mojom::Feature::kPhoneHubCameraRoll,
mojom::Feature::kPhoneHubTaskContinuation, mojom::Feature::kEche};
base::flat_map<mojom::Feature, std::string>
GenerateFeatureToEnabledPrefNameMap() {
return base::flat_map<mojom::Feature, std::string>{
{mojom::Feature::kBetterTogetherSuite,
kBetterTogetherSuiteEnabledPrefName},
{mojom::Feature::kInstantTethering, kInstantTetheringEnabledPrefName},
{mojom::Feature::kSmartLock, kSmartLockEnabledPrefName},
{mojom::Feature::kPhoneHub, kPhoneHubEnabledPrefName},
{mojom::Feature::kPhoneHubCameraRoll, kPhoneHubCameraRollEnabledPrefName},
{mojom::Feature::kPhoneHubNotifications,
kPhoneHubNotificationsEnabledPrefName},
{mojom::Feature::kPhoneHubTaskContinuation,
kPhoneHubTaskContinuationEnabledPrefName},
{mojom::Feature::kEche, kEcheEnabledPrefName}};
}
base::flat_map<mojom::Feature, std::string>
GenerateFeatureToAllowedPrefNameMap() {
return base::flat_map<mojom::Feature, std::string>{
{mojom::Feature::kInstantTethering, kInstantTetheringAllowedPrefName},
{mojom::Feature::kSmartLock, kSmartLockAllowedPrefName},
{mojom::Feature::kPhoneHub, kPhoneHubAllowedPrefName},
{mojom::Feature::kPhoneHubCameraRoll, kPhoneHubCameraRollAllowedPrefName},
{mojom::Feature::kPhoneHubNotifications,
kPhoneHubNotificationsAllowedPrefName},
{mojom::Feature::kPhoneHubTaskContinuation,
kPhoneHubTaskContinuationAllowedPrefName},
{mojom::Feature::kWifiSync, kWifiSyncAllowedPrefName},
{mojom::Feature::kEche, kEcheAllowedPrefName}};
}
// Each feature's default value is kUnavailableNoVerifiedHost_NoEligibleHosts
// until proven otherwise.
base::flat_map<mojom::Feature, mojom::FeatureState>
GenerateInitialDefaultCachedStateMap() {
return base::flat_map<mojom::Feature, mojom::FeatureState>{
{mojom::Feature::kBetterTogetherSuite,
mojom::FeatureState::kUnavailableNoVerifiedHost_NoEligibleHosts},
{mojom::Feature::kInstantTethering,
mojom::FeatureState::kUnavailableNoVerifiedHost_NoEligibleHosts},
{mojom::Feature::kSmartLock,
mojom::FeatureState::kUnavailableNoVerifiedHost_NoEligibleHosts},
{mojom::Feature::kPhoneHub,
mojom::FeatureState::kUnavailableNoVerifiedHost_NoEligibleHosts},
{mojom::Feature::kPhoneHubCameraRoll,
mojom::FeatureState::kUnavailableNoVerifiedHost_NoEligibleHosts},
{mojom::Feature::kPhoneHubNotifications,
mojom::FeatureState::kUnavailableNoVerifiedHost_NoEligibleHosts},
{mojom::Feature::kPhoneHubTaskContinuation,
mojom::FeatureState::kUnavailableNoVerifiedHost_NoEligibleHosts},
{mojom::Feature::kWifiSync,
mojom::FeatureState::kUnavailableNoVerifiedHost_NoEligibleHosts},
{mojom::Feature::kEche,
mojom::FeatureState::kUnavailableNoVerifiedHost_NoEligibleHosts},
};
}
void ProcessSuiteEdgeCases(
FeatureStateManager::FeatureStatesMap* feature_states_map_ptr) {
FeatureStateManager::FeatureStatesMap& feature_states_map =
*feature_states_map_ptr;
// If the top-level Phone Hub feature is prohibited by policy, all of the
// sub-features are implicitly prohibited as well.
if (feature_states_map[mojom::Feature::kPhoneHub] ==
mojom::FeatureState::kProhibitedByPolicy) {
for (const auto& phone_hub_sub_feature : kPhoneHubSubFeatures) {
feature_states_map[phone_hub_sub_feature] =
mojom::FeatureState::kProhibitedByPolicy;
}
}
bool are_all_sub_features_prohibited_or_unsupported = true;
bool is_at_least_one_feature_supported = false;
for (const auto& map_entry : feature_states_map) {
// Skip the suite feature, since it doesn't have its own policy.
if (map_entry.first == mojom::Feature::kBetterTogetherSuite) {
continue;
}
const mojom::FeatureState feature_state = map_entry.second;
if (feature_state != mojom::FeatureState::kNotSupportedByChromebook) {
is_at_least_one_feature_supported = true;
}
// Also check for features that are not supported by the Chromebook, since
// we should still consider the suite prohibited if all sub-features are
// prohibited except for those that aren't even shown in the UI at all.
if (feature_state != mojom::FeatureState::kProhibitedByPolicy &&
feature_state != mojom::FeatureState::kNotSupportedByChromebook) {
are_all_sub_features_prohibited_or_unsupported = false;
}
}
// If no features are supported, the suite as a whole is considered
// unsupported.
if (!is_at_least_one_feature_supported) {
feature_states_map[mojom::Feature::kBetterTogetherSuite] =
mojom::FeatureState::kNotSupportedByChromebook;
return;
}
// The Better Together suite does not have its own explicit device policy;
// instead, if all supported sub-features are prohibited by policy, the entire
// suite should be considered prohibited.
if (are_all_sub_features_prohibited_or_unsupported) {
feature_states_map[mojom::Feature::kBetterTogetherSuite] =
mojom::FeatureState::kProhibitedByPolicy;
return;
}
// If the Better Together suite is disabled by the user, any sub-features
// which have been enabled by the user should be unavailable. The suite serves
// as a gatekeeper to all sub-features.
if (feature_states_map[mojom::Feature::kBetterTogetherSuite] ==
mojom::FeatureState::kDisabledByUser) {
for (auto& map_entry : feature_states_map) {
mojom::FeatureState& feature_state = map_entry.second;
if (feature_state == mojom::FeatureState::kEnabledByUser) {
feature_state = mojom::FeatureState::kUnavailableSuiteDisabled;
}
}
}
// If the top-level Phone Hub feature is disabled, its sub-features are
// unavailable.
if (feature_states_map[mojom::Feature::kPhoneHub] ==
mojom::FeatureState::kDisabledByUser) {
for (const auto& phone_hub_sub_feature : kPhoneHubSubFeatures) {
mojom::FeatureState& feature_state =
feature_states_map[phone_hub_sub_feature];
if (feature_state == mojom::FeatureState::kEnabledByUser ||
feature_state == mojom::FeatureState::kUnavailableSuiteDisabled) {
feature_state =
mojom::FeatureState::kUnavailableTopLevelFeatureDisabled;
}
}
}
// If the top level Phone Hub feature is not supported by the phone, the
// sub-features should also be not supported by the phone.
if (feature_states_map[mojom::Feature::kPhoneHub] ==
mojom::FeatureState::kNotSupportedByPhone) {
for (const auto& phone_hub_sub_feature : kPhoneHubSubFeatures) {
feature_states_map[phone_hub_sub_feature] =
mojom::FeatureState::kNotSupportedByPhone;
}
}
}
} // namespace
// static
FeatureStateManagerImpl::Factory*
FeatureStateManagerImpl::Factory::test_factory_ = nullptr;
// static
std::unique_ptr<FeatureStateManager> FeatureStateManagerImpl::Factory::Create(
PrefService* pref_service,
HostStatusProvider* host_status_provider,
device_sync::DeviceSyncClient* device_sync_client,
AndroidSmsPairingStateTracker* android_sms_pairing_state_tracker,
const base::flat_map<mojom::Feature, GlobalStateFeatureManager*>&
global_state_feature_managers,
bool is_secondary_user) {
if (test_factory_) {
return test_factory_->CreateInstance(
pref_service, host_status_provider, device_sync_client,
android_sms_pairing_state_tracker, global_state_feature_managers,
is_secondary_user);
}
return base::WrapUnique(new FeatureStateManagerImpl(
pref_service, host_status_provider, device_sync_client,
android_sms_pairing_state_tracker, global_state_feature_managers,
is_secondary_user));
}
// static
void FeatureStateManagerImpl::Factory::SetFactoryForTesting(
Factory* test_factory) {
test_factory_ = test_factory;
}
FeatureStateManagerImpl::Factory::~Factory() = default;
FeatureStateManagerImpl::FeatureStateManagerImpl(
PrefService* pref_service,
HostStatusProvider* host_status_provider,
device_sync::DeviceSyncClient* device_sync_client,
AndroidSmsPairingStateTracker* android_sms_pairing_state_tracker,
const base::flat_map<mojom::Feature, GlobalStateFeatureManager*>&
global_state_feature_managers,
bool is_secondary_user)
: pref_service_(pref_service),
host_status_provider_(host_status_provider),
device_sync_client_(device_sync_client),
android_sms_pairing_state_tracker_(android_sms_pairing_state_tracker),
global_state_feature_managers_(global_state_feature_managers),
is_secondary_user_(is_secondary_user),
feature_to_enabled_pref_name_map_(GenerateFeatureToEnabledPrefNameMap()),
feature_to_allowed_pref_name_map_(GenerateFeatureToAllowedPrefNameMap()),
cached_feature_state_map_(GenerateInitialDefaultCachedStateMap()) {
host_status_provider_->AddObserver(this);
device_sync_client_->AddObserver(this);
if (android_sms_pairing_state_tracker_) {
android_sms_pairing_state_tracker_->AddObserver(this);
}
registrar_.Init(pref_service_);
// Listen for changes to each of the "enabled" feature names.
for (const auto& map_entry : feature_to_enabled_pref_name_map_) {
registrar_.Add(
map_entry.second,
base::BindRepeating(&FeatureStateManagerImpl::OnPrefValueChanged,
base::Unretained(this)));
}
// Also listen for changes to each of the "allowed" feature names.
for (const auto& map_entry : feature_to_allowed_pref_name_map_) {
registrar_.Add(
map_entry.second,
base::BindRepeating(&FeatureStateManagerImpl::OnPrefValueChanged,
base::Unretained(this)));
}
registrar_.Add(
kEcheOverriddenSupportReceivedFromPhoneHubPrefName,
base::BindRepeating(&FeatureStateManagerImpl::OnPrefValueChanged,
base::Unretained(this)));
// Prime the cache. Since this is the initial computation, it does not
// represent a true change of feature state values, so observers should not be
// notified.
UpdateFeatureStateCache(false /* notify_observers_of_changes */);
LogFeatureStates();
feature_state_metric_timer_.Start(
FROM_HERE, kFeatureStateLoggingPeriod,
base::BindRepeating(&FeatureStateManagerImpl::LogFeatureStates,
base::Unretained(this)));
}
FeatureStateManagerImpl::~FeatureStateManagerImpl() {
host_status_provider_->RemoveObserver(this);
device_sync_client_->RemoveObserver(this);
if (android_sms_pairing_state_tracker_) {
android_sms_pairing_state_tracker_->RemoveObserver(this);
}
}
FeatureStateManager::FeatureStatesMap
FeatureStateManagerImpl::GetFeatureStates() {
return cached_feature_state_map_;
}
void FeatureStateManagerImpl::PerformSetFeatureEnabledState(
mojom::Feature feature,
bool enabled) {
if (global_state_feature_managers_.contains(feature)) {
global_state_feature_managers_.at(feature)->SetIsFeatureEnabled(enabled);
// Need to manually trigger UpdateFeatureStateCache since changes to
// this global feature state is not observed by |registrar_| and will not
// invoke OnPrefValueChanged
UpdateFeatureStateCache(true /* notify_observers_of_changes */);
return;
}
// Note: Since |registrar_| observes changes to all relevant preferences,
// this call will result in OnPrefValueChanged() being invoked, resulting in
// observers being notified of the change.
pref_service_->SetBoolean(feature_to_enabled_pref_name_map_[feature],
enabled);
}
void FeatureStateManagerImpl::OnHostStatusChange(
const HostStatusProvider::HostStatusWithDevice& host_status_with_device) {
UpdateFeatureStateCache(true /* notify_observers_of_changes */);
}
void FeatureStateManagerImpl::OnNewDevicesSynced() {
UpdateFeatureStateCache(true /* notify_observers_of_changes */);
}
void FeatureStateManagerImpl::OnPrefValueChanged() {
UpdateFeatureStateCache(true /* notify_observers_of_changes */);
}
void FeatureStateManagerImpl::OnPairingStateChanged() {
UpdateFeatureStateCache(true /* notify_observers_of_changes */);
}
void FeatureStateManagerImpl::UpdateFeatureStateCache(
bool notify_observers_of_changes) {
// Make a copy of |cached_feature_state_map_| before making edits to it.
FeatureStatesMap previous_cached_feature_state_map =
cached_feature_state_map_;
// Update |cached_feature_state_map_| with computed values.
auto it = cached_feature_state_map_.begin();
while (it != cached_feature_state_map_.end()) {
it->second = ComputeFeatureState(it->first);
++it;
}
// Some computed values must be updated to support various edge cases.
ProcessSuiteEdgeCases(&cached_feature_state_map_);
if (previous_cached_feature_state_map == cached_feature_state_map_) {
return;
}
PA_LOG(INFO) << "Feature states map changed. Old map: "
<< previous_cached_feature_state_map
<< ", new map: " << cached_feature_state_map_;
LogFeatureStates();
NotifyFeatureStatesChange(cached_feature_state_map_);
}
mojom::FeatureState FeatureStateManagerImpl::ComputeFeatureState(
mojom::Feature feature) {
if (!IsAllowedByPolicy(feature)) {
return mojom::FeatureState::kProhibitedByPolicy;
}
HostStatusProvider::HostStatusWithDevice status_with_device =
host_status_provider_->GetHostWithStatus();
if (status_with_device.host_status() == mojom::HostStatus::kNoEligibleHosts) {
return mojom::FeatureState::kUnavailableNoVerifiedHost_NoEligibleHosts;
}
if (status_with_device.host_status() != mojom::HostStatus::kHostVerified) {
return mojom::FeatureState::
kUnavailableNoVerifiedHost_HostExistsButNotSetAndVerified;
}
if (!IsSupportedByChromebook(feature)) {
return mojom::FeatureState::kNotSupportedByChromebook;
}
if (!HasSufficientSecurity(feature, *status_with_device.host_device())) {
return mojom::FeatureState::kUnavailableInsufficientSecurity;
}
if (!HasBeenActivatedByPhone(feature, *status_with_device.host_device())) {
return mojom::FeatureState::kNotSupportedByPhone;
}
return GetEnabledOrDisabledState(feature);
}
bool FeatureStateManagerImpl::IsAllowedByPolicy(mojom::Feature feature) {
// If no policy preference exists for this feature, the feature is implicitly
// allowed.
if (!base::Contains(feature_to_allowed_pref_name_map_, feature)) {
return true;
}
return pref_service_->GetBoolean(feature_to_allowed_pref_name_map_[feature]);
}
bool FeatureStateManagerImpl::IsSupportedByChromebook(mojom::Feature feature) {
if (!features::IsCrossDeviceFeatureSuiteAllowed()) {
return false;
}
static const std::pair<mojom::Feature, multidevice::SoftwareFeature>
kFeatureAndClientSoftwareFeaturePairs[] = {
{mojom::Feature::kBetterTogetherSuite,
multidevice::SoftwareFeature::kBetterTogetherClient},
{mojom::Feature::kInstantTethering,
multidevice::SoftwareFeature::kInstantTetheringClient},
{mojom::Feature::kSmartLock,
multidevice::SoftwareFeature::kSmartLockClient},
// Note: Most Phone Hub-related features use the same SoftwareFeature.
{mojom::Feature::kPhoneHub,
multidevice::SoftwareFeature::kPhoneHubClient},
{mojom::Feature::kPhoneHubNotifications,
multidevice::SoftwareFeature::kPhoneHubClient},
{mojom::Feature::kPhoneHubTaskContinuation,
multidevice::SoftwareFeature::kPhoneHubClient},
// Note: Camera Roll is launched separately from the rest of PhoneHub.
{mojom::Feature::kPhoneHubCameraRoll,
multidevice::SoftwareFeature::kPhoneHubCameraRollClient},
{mojom::Feature::kWifiSync,
multidevice::SoftwareFeature::kWifiSyncClient},
{mojom::Feature::kEche, multidevice::SoftwareFeature::kEcheClient}};
std::optional<multidevice::RemoteDeviceRef> local_device =
device_sync_client_->GetLocalDeviceMetadata();
if (!local_device) {
PA_LOG(ERROR) << "FeatureStateManagerImpl::" << __func__
<< ": Local device unexpectedly null.";
return false;
}
for (const auto& pair : kFeatureAndClientSoftwareFeaturePairs) {
if (pair.first != feature) {
continue;
}
if ((pair.second == multidevice::SoftwareFeature::kPhoneHubClient ||
pair.second == multidevice::SoftwareFeature::kEcheClient) &&
is_secondary_user_) {
return false;
}
return local_device->GetSoftwareFeatureState(pair.second) !=
multidevice::SoftwareFeatureState::kNotSupported;
}
NOTREACHED_IN_MIGRATION();
return false;
}
bool FeatureStateManagerImpl::HasSufficientSecurity(
mojom::Feature feature,
const multidevice::RemoteDeviceRef& host_device) {
if (feature != mojom::Feature::kSmartLock) {
return true;
}
// Special case for Smart Lock: if the host device does not have a lock screen
// set, its SoftwareFeatureState for kSmartLockHost is supported but not
// enabled.
return host_device.GetSoftwareFeatureState(
multidevice::SoftwareFeature::kSmartLockHost) !=
multidevice::SoftwareFeatureState::kSupported;
}
bool FeatureStateManagerImpl::HasBeenActivatedByPhone(
mojom::Feature feature,
const multidevice::RemoteDeviceRef& host_device) {
static const std::pair<mojom::Feature, multidevice::SoftwareFeature>
kFeatureAndHostSoftwareFeaturePairs[] = {
{mojom::Feature::kBetterTogetherSuite,
multidevice::SoftwareFeature::kBetterTogetherHost},
{mojom::Feature::kInstantTethering,
multidevice::SoftwareFeature::kInstantTetheringHost},
{mojom::Feature::kSmartLock,
multidevice::SoftwareFeature::kSmartLockHost},
// Note: Most Phone Hub-related features use the same SoftwareFeature.
{mojom::Feature::kPhoneHub,
multidevice::SoftwareFeature::kPhoneHubHost},
{mojom::Feature::kPhoneHubNotifications,
multidevice::SoftwareFeature::kPhoneHubHost},
{mojom::Feature::kPhoneHubTaskContinuation,
multidevice::SoftwareFeature::kPhoneHubHost},
// Note: Camera Roll is launched separately from the rest of PhoneHub.
{mojom::Feature::kPhoneHubCameraRoll,
multidevice::SoftwareFeature::kPhoneHubCameraRollHost},
{mojom::Feature::kWifiSync,
multidevice::SoftwareFeature::kWifiSyncHost},
{mojom::Feature::kEche, multidevice::SoftwareFeature::kEcheHost}};
for (const auto& pair : kFeatureAndHostSoftwareFeaturePairs) {
if (pair.first != feature) {
continue;
}
// The bluetooth public address is required in order to use PhoneHub/Eche
// and its sub-features.
if ((pair.second == multidevice::SoftwareFeature::kPhoneHubHost ||
pair.second == multidevice::SoftwareFeature::kEcheHost) &&
host_device.bluetooth_public_address().empty()) {
return false;
}
multidevice::SoftwareFeatureState feature_state =
host_device.GetSoftwareFeatureState(pair.second);
// Edge Case: Eche is considered activated on the host when Phone Hub is
// enabled and either:
// * if phone did not specify Eche state via PhoneHub status message:
// * Eche's state is kSupported or kEnabled.
// * if phone did specify Eche state via PhoneHub status message:
// * use the specified Eche state.
if (feature == mojom::Feature::kEche) {
if (host_device.GetSoftwareFeatureState(
multidevice::SoftwareFeature::kPhoneHubHost) !=
multidevice::SoftwareFeatureState::kEnabled) {
return false;
}
EcheSupportReceivedFromPhoneHub eche_support_received_from_phone_hub =
static_cast<EcheSupportReceivedFromPhoneHub>(
pref_service_->GetInteger(
kEcheOverriddenSupportReceivedFromPhoneHubPrefName));
switch (eche_support_received_from_phone_hub) {
case EcheSupportReceivedFromPhoneHub::kNotSpecified:
return feature_state ==
multidevice::SoftwareFeatureState::kSupported ||
feature_state == multidevice::SoftwareFeatureState::kEnabled;
case EcheSupportReceivedFromPhoneHub::kNotSupported:
return false;
case EcheSupportReceivedFromPhoneHub::kSupported:
return true;
};
}
if (feature_state == multidevice::SoftwareFeatureState::kEnabled) {
return true;
}
// Edge Case: features with global states are considered activated on host
// when the state is kSupported or kEnabled. kEnabled/kSupported correspond
// to on/off for the global host state.
return (global_state_feature_managers_.contains(feature) &&
feature_state == multidevice::SoftwareFeatureState::kSupported);
}
NOTREACHED_IN_MIGRATION();
return false;
}
mojom::FeatureState FeatureStateManagerImpl::GetEnabledOrDisabledState(
mojom::Feature feature) {
if (global_state_feature_managers_.contains(feature)) {
return (global_state_feature_managers_.at(feature)->IsFeatureEnabled()
? mojom::FeatureState::kEnabledByUser
: mojom::FeatureState::kDisabledByUser);
}
if (!base::Contains(feature_to_enabled_pref_name_map_, feature)) {
PA_LOG(ERROR) << "FeatureStateManagerImpl::GetEnabledOrDisabledState(): "
<< "Feature not present in \"enabled pref\" map: " << feature;
NOTREACHED_IN_MIGRATION();
}
return pref_service_->GetBoolean(feature_to_enabled_pref_name_map_[feature])
? mojom::FeatureState::kEnabledByUser
: mojom::FeatureState::kDisabledByUser;
}
void FeatureStateManagerImpl::LogFeatureStates() const {
base::UmaHistogramEnumeration(
"MultiDevice.BetterTogetherSuite.MultiDeviceFeatureState",
cached_feature_state_map_.find(mojom::Feature::kBetterTogetherSuite)
->second);
base::UmaHistogramEnumeration(
"InstantTethering.MultiDeviceFeatureState",
cached_feature_state_map_.find(mojom::Feature::kInstantTethering)
->second);
base::UmaHistogramEnumeration(
"SmartLock.MultiDeviceFeatureState",
cached_feature_state_map_.find(mojom::Feature::kSmartLock)->second);
base::UmaHistogramEnumeration(
"PhoneHub.MultiDeviceFeatureState.TopLevelFeature",
cached_feature_state_map_.find(mojom::Feature::kPhoneHub)->second);
base::UmaHistogramEnumeration(
"PhoneHub.MultiDeviceFeatureState.CameraRoll",
cached_feature_state_map_.find(mojom::Feature::kPhoneHubCameraRoll)
->second);
base::UmaHistogramEnumeration(
"PhoneHub.MultiDeviceFeatureState.Notifications",
cached_feature_state_map_.find(mojom::Feature::kPhoneHubNotifications)
->second);
base::UmaHistogramEnumeration(
"PhoneHub.MultiDeviceFeatureState.TaskContinuation",
cached_feature_state_map_.find(mojom::Feature::kPhoneHubTaskContinuation)
->second);
base::UmaHistogramEnumeration(
"WifiSync.MultiDeviceFeatureState",
cached_feature_state_map_.find(mojom::Feature::kWifiSync)->second);
base::UmaHistogramEnumeration(
"Eche.MultiDeviceFeatureState",
cached_feature_state_map_.find(mojom::Feature::kEche)->second);
}
} // namespace multidevice_setup
} // namespace ash