chromium/chromeos/ash/services/multidevice_setup/global_state_feature_manager_impl.cc

// Copyright 2021 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/global_state_feature_manager_impl.h"

#include <memory>
#include <sstream>
#include <string>
#include <utility>

#include "ash/constants/ash_features.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "base/timer/timer.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 "chromeos/ash/services/device_sync/feature_status_change.h"
#include "chromeos/ash/services/device_sync/public/cpp/device_sync_client.h"
#include "chromeos/ash/services/device_sync/public/mojom/device_sync.mojom.h"
#include "chromeos/ash/services/multidevice_setup/host_status_provider.h"
#include "chromeos/ash/services/multidevice_setup/public/cpp/prefs.h"
#include "chromeos/ash/services/multidevice_setup/public/mojom/multidevice_setup.mojom.h"
#include "chromeos/ash/services/multidevice_setup/wifi_sync_notification_controller.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"

namespace ash {

namespace multidevice_setup {

namespace {

// This pref name is left in a legacy format to maintain compatibility.
const char kWifiSyncPendingStatePrefName[] =
    "multidevice_setup.pending_set_wifi_sync_enabled_request";

// The number of minutes to wait before retrying a failed attempt.
const int kNumMinutesBetweenRetries = 5;

}  // namespace

// static
GlobalStateFeatureManagerImpl::Factory*
    GlobalStateFeatureManagerImpl::Factory::test_factory_ = nullptr;

// static
std::unique_ptr<GlobalStateFeatureManager>
GlobalStateFeatureManagerImpl::Factory::Create(
    Option option,
    HostStatusProvider* host_status_provider,
    PrefService* pref_service,
    device_sync::DeviceSyncClient* device_sync_client,
    std::unique_ptr<base::OneShotTimer> timer) {
  if (test_factory_) {
    return test_factory_->CreateInstance(option, host_status_provider,
                                         pref_service, device_sync_client,
                                         std::move(timer));
  }

  mojom::Feature managed_feature;
  multidevice::SoftwareFeature managed_host_feature;
  std::string pending_state_pref_name;
  switch (option) {
    case Option::kWifiSync:
      managed_feature = mojom::Feature::kWifiSync;
      managed_host_feature = multidevice::SoftwareFeature::kWifiSyncHost;
      pending_state_pref_name = kWifiSyncPendingStatePrefName;
      break;
  }
  return base::WrapUnique(new GlobalStateFeatureManagerImpl(
      managed_feature, managed_host_feature, pending_state_pref_name,
      host_status_provider, pref_service, device_sync_client,
      std::move(timer)));
}

// static
void GlobalStateFeatureManagerImpl::Factory::SetFactoryForTesting(
    Factory* test_factory) {
  test_factory_ = test_factory;
}

GlobalStateFeatureManagerImpl::Factory::~Factory() = default;

void GlobalStateFeatureManagerImpl::RegisterPrefs(
    PrefRegistrySimple* registry) {
  registry->RegisterIntegerPref(kWifiSyncPendingStatePrefName,
                                static_cast<int>(PendingState::kPendingNone));
}

GlobalStateFeatureManagerImpl::GlobalStateFeatureManagerImpl(
    mojom::Feature managed_feature,
    multidevice::SoftwareFeature managed_host_feature,
    const std::string& pending_state_pref_name,
    HostStatusProvider* host_status_provider,
    PrefService* pref_service,
    device_sync::DeviceSyncClient* device_sync_client,
    std::unique_ptr<base::OneShotTimer> timer)
    : GlobalStateFeatureManager(),
      managed_feature_(managed_feature),
      managed_host_feature_(managed_host_feature),
      pending_state_pref_name_(pending_state_pref_name),
      host_status_provider_(host_status_provider),
      pref_service_(pref_service),
      device_sync_client_(device_sync_client),
      timer_(std::move(timer)) {
  host_status_provider_->AddObserver(this);
  device_sync_client_->AddObserver(this);

  if (GetCurrentState() == CurrentState::kValidPendingRequest) {
    AttemptSetHostStateNetworkRequest(false /* is_retry */);
  }

  if (ShouldEnableOnVerify()) {
    ProcessEnableOnVerifyAttempt();
  }
}

GlobalStateFeatureManagerImpl::~GlobalStateFeatureManagerImpl() {
  host_status_provider_->RemoveObserver(this);
  device_sync_client_->RemoveObserver(this);
}

void GlobalStateFeatureManagerImpl::OnHostStatusChange(
    const HostStatusProvider::HostStatusWithDevice& host_status_with_device) {
  if (GetCurrentState() == CurrentState::kNoVerifiedHost &&
      !ShouldEnableOnVerify()) {
    ResetPendingNetworkRequest();
  }

  if (ShouldAttemptToEnableAfterHostVerified()) {
    SetPendingState(PendingState::kSetPendingEnableOnVerify);
    return;
  }

  if (ShouldEnableOnVerify()) {
    ProcessEnableOnVerifyAttempt();
  }
}

void GlobalStateFeatureManagerImpl::OnNewDevicesSynced() {
  if (GetCurrentState() != CurrentState::kValidPendingRequest &&
      !ShouldEnableOnVerify()) {
    ResetPendingNetworkRequest();
  }
}

void GlobalStateFeatureManagerImpl::SetIsFeatureEnabled(bool enabled) {
  if (GetCurrentState() == CurrentState::kNoVerifiedHost) {
    PA_LOG(ERROR) << "GlobalStateFeatureManagerImpl::SetIsFeatureEnabled:  "
                     "Network request "
                     "not attempted because there is No Verified Host";
    ResetPendingNetworkRequest();
    return;
  }

  SetPendingState(enabled ? PendingState::kPendingEnable
                          : PendingState::kPendingDisable);

  if (managed_feature_ == mojom::Feature::kWifiSync)
    pref_service_->SetBoolean(kCanShowWifiSyncAnnouncementPrefName, false);

  // Stop timer since new attempt is started.
  timer_->Stop();
  AttemptSetHostStateNetworkRequest(false /* is_retry */);
}

bool GlobalStateFeatureManagerImpl::IsFeatureEnabled() {
  CurrentState current_state = GetCurrentState();
  if (current_state == CurrentState::kNoVerifiedHost) {
    return false;
  }

  if (current_state == CurrentState::kValidPendingRequest) {
    return GetPendingState() == PendingState::kPendingEnable;
  }

  return host_status_provider_->GetHostWithStatus()
             .host_device()
             ->GetSoftwareFeatureState(managed_host_feature_) ==
         multidevice::SoftwareFeatureState::kEnabled;
}

void GlobalStateFeatureManagerImpl::ResetPendingNetworkRequest() {
  SetPendingState(PendingState::kPendingNone);
  timer_->Stop();
}

void GlobalStateFeatureManagerImpl::SetPendingState(
    PendingState pending_state) {
  pref_service_->SetInteger(pending_state_pref_name_,
                            static_cast<int>(pending_state));
}

GlobalStateFeatureManagerImpl::PendingState
GlobalStateFeatureManagerImpl::GetPendingState() {
  return static_cast<PendingState>(
      pref_service_->GetInteger(pending_state_pref_name_));
}

GlobalStateFeatureManagerImpl::CurrentState
GlobalStateFeatureManagerImpl::GetCurrentState() {
  if (host_status_provider_->GetHostWithStatus().host_status() !=
      mojom::HostStatus::kHostVerified) {
    return CurrentState::kNoVerifiedHost;
  }

  PendingState pending_state = GetPendingState();

  // If the pending request is kSetPendingEnableOnVerify then there is no
  // actionable pending equest. The pending request will be changed from
  // kSetPendingEnableOnVerify when the host has been verified.
  if (pending_state == PendingState::kPendingNone ||
      pending_state == PendingState::kSetPendingEnableOnVerify) {
    return CurrentState::kNoPendingRequest;
  }

  bool enabled_on_host =
      (host_status_provider_->GetHostWithStatus()
           .host_device()
           ->GetSoftwareFeatureState(managed_host_feature_) ==
       multidevice::SoftwareFeatureState::kEnabled);
  bool pending_enabled = (pending_state == PendingState::kPendingEnable);

  if (pending_enabled == enabled_on_host) {
    return CurrentState::kPendingMatchesBackend;
  }

  return CurrentState::kValidPendingRequest;
}

void GlobalStateFeatureManagerImpl::AttemptSetHostStateNetworkRequest(
    bool is_retry) {
  if (network_request_in_flight_) {
    return;
  }

  bool pending_enabled = (GetPendingState() == PendingState::kPendingEnable);

  PA_LOG(INFO) << "GlobalStateFeatureManagerImpl::" << __func__ << ": "
               << (is_retry ? "Retrying attempt" : "Attempting") << " to "
               << (pending_enabled ? "enable" : "disable");

  network_request_in_flight_ = true;
  multidevice::RemoteDeviceRef host_device =
      *host_status_provider_->GetHostWithStatus().host_device();

  if (features::ShouldUseV1DeviceSync()) {
    // Even if the |device_to_set| has a non-trivial Instance ID, we still
    // invoke the v1 DeviceSync RPC to set the feature state. This ensures that
    // GmsCore will be notified of the change regardless of what version of
    // DeviceSync it is running. The v1 and v2 RPCs to change feature states
    // ultimately update the same backend database entry. Note: The
    // RemoteDeviceProvider guarantees that every device will have a public key
    // while v1 DeviceSync is enabled.
    device_sync_client_->SetSoftwareFeatureState(
        host_device.public_key(), managed_host_feature_,
        pending_enabled /* enabled */, pending_enabled /* is_exclusive */,
        base::BindOnce(&GlobalStateFeatureManagerImpl::
                           OnSetHostStateNetworkRequestFinished,
                       weak_ptr_factory_.GetWeakPtr(), pending_enabled));
  } else {
    device_sync_client_->SetFeatureStatus(
        host_device.instance_id(), managed_host_feature_,
        pending_enabled ? device_sync::FeatureStatusChange::kEnableExclusively
                        : device_sync::FeatureStatusChange::kDisable,
        base::BindOnce(&GlobalStateFeatureManagerImpl::
                           OnSetHostStateNetworkRequestFinished,
                       weak_ptr_factory_.GetWeakPtr(), pending_enabled));
  }
}

void GlobalStateFeatureManagerImpl::OnSetHostStateNetworkRequestFinished(
    bool attempted_to_enable,
    device_sync::mojom::NetworkRequestResult result_code) {
  network_request_in_flight_ = false;

  bool success =
      (result_code == device_sync::mojom::NetworkRequestResult::kSuccess);

  std::stringstream ss;
  ss << "GlobalStateFeatureManagerImpl::" << __func__ << ": Completed with "
     << (success ? "success" : "failure")
     << ". Attempted to enable: " << (attempted_to_enable ? "true" : "false");

  if (success) {
    PA_LOG(VERBOSE) << ss.str();
    PendingState pending_state = GetPendingState();
    if (pending_state == PendingState::kPendingNone) {
      return;
    }

    bool pending_enabled = (pending_state == PendingState::kPendingEnable);
    // If the network request was successful but there is still a pending
    // network request then trigger a network request immediately. This could
    // happen if there was a second attempt to set the backend while the first
    // one was still in progress.
    if (attempted_to_enable != pending_enabled) {
      AttemptSetHostStateNetworkRequest(false /* is_retry */);
    }
    return;
  }

  ss << ", Error code: " << result_code;
  PA_LOG(WARNING) << ss.str();

  // If the network request failed and there is still a pending network request,
  // schedule a retry.
  if (GetCurrentState() == CurrentState::kValidPendingRequest) {
    timer_->Start(
        FROM_HERE, base::Minutes(kNumMinutesBetweenRetries),
        base::BindOnce(
            &GlobalStateFeatureManagerImpl::AttemptSetHostStateNetworkRequest,
            base::Unretained(this), true /* is_retry */));
  }
}

bool GlobalStateFeatureManagerImpl::ShouldEnableOnVerify() {
  return (GetPendingState() == PendingState::kSetPendingEnableOnVerify);
}

void GlobalStateFeatureManagerImpl::ProcessEnableOnVerifyAttempt() {
  mojom::HostStatus host_status =
      host_status_provider_->GetHostWithStatus().host_status();

  // If host is not set.
  if (host_status == mojom::HostStatus::kNoEligibleHosts ||
      host_status == mojom::HostStatus::kEligibleHostExistsButNoHostSet) {
    ResetPendingNetworkRequest();
    return;
  }

  if (host_status != mojom::HostStatus::kHostVerified) {
    return;
  }

  if (IsFeatureEnabled()) {
    ResetPendingNetworkRequest();
    return;
  }

  SetIsFeatureEnabled(true);

  if (managed_feature_ == mojom::Feature::kPhoneHubCameraRoll) {
    base::UmaHistogramEnumeration("PhoneHub.CameraRoll.OptInEntryPoint",
                                  mojom::CameraRollOptInEntryPoint::kSetupFlow);
  }
}

bool GlobalStateFeatureManagerImpl::ShouldAttemptToEnableAfterHostVerified() {
  HostStatusProvider::HostStatusWithDevice host_status_with_device =
      host_status_provider_->GetHostWithStatus();

  // kHostSetLocallyButWaitingForBackendConfirmation is only possible if the
  // setup flow has been completed on the local device.
  if (host_status_with_device.host_status() !=
      mojom::HostStatus::kHostSetLocallyButWaitingForBackendConfirmation) {
    return false;
  }

  // Check if the feature is prohibited by enterprise policy or if feature flag
  // is disabled.
  if (!IsFeatureAllowed(managed_feature_, pref_service_)) {
    return false;
  }

  // Check if the feature is supported by host device.
  if (host_status_with_device.host_device()->GetSoftwareFeatureState(
          managed_host_feature_) ==
      multidevice::SoftwareFeatureState::kNotSupported) {
    return false;
  }

  return true;
}

}  // namespace multidevice_setup

}  // namespace ash