chromium/ash/webui/eche_app_ui/apps_access_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 "ash/webui/eche_app_ui/apps_access_manager_impl.h"

#include "ash/constants/ash_features.h"
#include "ash/webui/eche_app_ui/pref_names.h"
#include "ash/webui/eche_app_ui/proto/exo_messages.pb.h"
#include "base/metrics/histogram_functions.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/phonehub/multidevice_feature_access_manager.h"
#include "chromeos/ash/services/multidevice_setup/public/cpp/prefs.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"

namespace ash {
namespace eche_app {

namespace {

using multidevice_setup::mojom::Feature;
using multidevice_setup::mojom::FeatureState;

}  // namespace

using AccessStatus =
    ash::phonehub::MultideviceFeatureAccessManager::AccessStatus;

// static
void AppsAccessManagerImpl::RegisterPrefs(PrefRegistrySimple* registry) {
  registry->RegisterIntegerPref(
      prefs::kAppsAccessStatus,
      static_cast<int>(AccessStatus::kAvailableButNotGranted));
}

AppsAccessManagerImpl::AppsAccessManagerImpl(
    EcheConnector* eche_connector,
    EcheMessageReceiver* message_receiver,
    FeatureStatusProvider* feature_status_provider,
    PrefService* pref_service,
    multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client,
    secure_channel::ConnectionManager* connection_manager)
    : eche_connector_(eche_connector),
      message_receiver_(message_receiver),
      feature_status_provider_(feature_status_provider),
      pref_service_(pref_service),
      multidevice_setup_client_(multidevice_setup_client),
      connection_manager_(connection_manager) {
  DCHECK(message_receiver_);
  DCHECK(feature_status_provider_);
  DCHECK(connection_manager_);
  current_feature_status_ = feature_status_provider_->GetStatus();
  current_connection_status_ = connection_manager_->GetStatus();
  feature_status_provider_->AddObserver(this);
  message_receiver_->AddObserver(this);
  connection_manager_->AddObserver(this);
}

AppsAccessManagerImpl::~AppsAccessManagerImpl() {
  feature_status_provider_->RemoveObserver(this);
  message_receiver_->RemoveObserver(this);
  connection_manager_->RemoveObserver(this);
}

AccessStatus AppsAccessManagerImpl::GetAccessStatus() const {
  int status = pref_service_->GetInteger(prefs::kAppsAccessStatus);
  return static_cast<AccessStatus>(status);
}

void AppsAccessManagerImpl::OnSetupRequested() {
  current_feature_status_ = feature_status_provider_->GetStatus();

  if (!IsEligibleForOnboarding(current_feature_status_))
    return;

  switch (connection_manager_->GetStatus()) {
    // We're already connected, so request that the UI be shown on the phone.
    case ConnectionStatus::kConnected:
      SendShowAppsAccessSetupRequest();
      break;
    // We're already connecting, so wait until a connection succeeds before
    // trying to send a message
    case ConnectionStatus::kConnecting:
      SetAppsSetupOperationStatus(
          AppsAccessSetupOperation::Status::kConnecting);
      break;
    // We are not connected, so try to reconnect it. We'll send the message in
    // UpdateSetupOperationState().
    case ConnectionStatus::kDisconnected:
      SetAppsSetupOperationStatus(
          AppsAccessSetupOperation::Status::kConnecting);
      eche_connector_->AttemptNearbyConnection();
      break;
    default:
      break;
  }
}

void AppsAccessManagerImpl::NotifyAppsAccessCanceled() {
  if (connection_manager_->GetStatus() == ConnectionStatus::kDisconnected) {
    base::UmaHistogramEnumeration(
        kEcheOnboardingHistogramName,
        OnboardingUserActionMetric::kFailedConnection);
  } else {
    base::UmaHistogramEnumeration(
        kEcheOnboardingHistogramName,
        OnboardingUserActionMetric::kUserActionCanceled);
  }
}

void AppsAccessManagerImpl::OnGetAppsAccessStateResponseReceived(
    proto::GetAppsAccessStateResponse apps_access_state_response) {
  if (apps_access_state_response.result() == proto::Result::RESULT_NO_ERROR) {
    current_apps_access_state_ = apps_access_state_response.apps_access_state();
    AccessStatus access_status = ComputeAppsAccessState();
    UpdateFeatureEnabledState(GetAccessStatus(), access_status);
    SetAccessStatusInternal(access_status);
  }
}

void AppsAccessManagerImpl::OnSendAppsSetupResponseReceived(
    proto::SendAppsSetupResponse apps_setup_response) {
  if (apps_setup_response.result() == proto::Result::RESULT_NO_ERROR) {
    current_apps_access_state_ = apps_setup_response.apps_access_state();

    // Log the no error response after |current_apps_access_state_| is updated.
    LogAppsSetupResponse(apps_setup_response.result());
    AccessStatus access_status = ComputeAppsAccessState();
    SetAccessStatusInternal(access_status);
  } else if (IsSetupOperationInProgress()) {
    // Log the error response before we change the setup operation to not in
    // progress.
    LogAppsSetupResponse(apps_setup_response.result());
    if (apps_setup_response.result() != proto::Result::RESULT_ACK_BY_EXO) {
      SetAppsSetupOperationStatus(
          (apps_setup_response.result() ==
           proto::Result::RESULT_ERROR_USER_REJECTED)
              ? AppsAccessSetupOperation::Status::kCompletedUserRejected
              : AppsAccessSetupOperation::Status::kOperationFailedOrCancelled);
    }
  }
}

void AppsAccessManagerImpl::OnAppPolicyStateChange(
    proto::AppStreamingPolicy app_policy_state) {
  if (current_app_policy_state_ == app_policy_state)
    return;
  current_app_policy_state_ = app_policy_state;

  // We only notify policy state after we also query access status from the
  // remote phone.
  if (initialized_) {
    AccessStatus access_status = ComputeAppsAccessState();
    UpdateFeatureEnabledState(GetAccessStatus(), access_status);
    SetAccessStatusInternal(access_status);
  }
}

void AppsAccessManagerImpl::OnFeatureStatusChanged() {
  UpdateSetupOperationState();
}

void AppsAccessManagerImpl::OnConnectionStatusChanged() {
  // When this feature is disabled, we will not be able to get
  // OnFeatureStatusChanged() once the connection state changes, we need to
  // listen to OnConnectionStatusChanged() and then trigger the onboarding
  // process. For other cases (eg: feature enabled), OnFeatureStatusChanged has
  // been called, so we return directly.
  if (feature_status_provider_->GetStatus() != FeatureStatus::kDisabled)
    return;

  UpdateSetupOperationState();
}

void AppsAccessManagerImpl::AttemptAppsAccessStateRequest() {
  if (initialized_)
    return;

  const FeatureStatus new_feature_status =
      feature_status_provider_->GetStatus();
  const ConnectionStatus new_connection_status =
      connection_manager_->GetStatus();

  PA_LOG(INFO) << "AttemptAppsAccessStateRequest current_feature_status: "
               << current_feature_status_ << " changed to "
               << new_feature_status
               << " current_connection_status: " << current_connection_status_
               << " changed to " << new_connection_status;

  if (current_feature_status_ == new_feature_status &&
      current_connection_status_ == new_connection_status)
    return;

  if (!IsEligibleForOnboarding(new_feature_status))
    return;

  if (new_connection_status == ConnectionStatus::kDisconnected) {
    eche_connector_->AttemptNearbyConnection();
    return;
  }

  if (new_connection_status == ConnectionStatus::kConnected ||
      new_connection_status == ConnectionStatus::kConnecting) {
    GetAppsAccessStateRequest();
  }
}

void AppsAccessManagerImpl::GetAppsAccessStateRequest() {
  eche_connector_->GetAppsAccessStateRequest();
}

void AppsAccessManagerImpl::SendShowAppsAccessSetupRequest() {
  eche_connector_->SendAppsSetupRequest();
  SetAppsSetupOperationStatus(AppsAccessSetupOperation::Status::
                                  kSentMessageToPhoneAndWaitingForResponse);
}

void AppsAccessManagerImpl::SetAccessStatusInternal(
    AccessStatus access_status) {
  initialized_ = true;
  PA_LOG(INFO) << "Apps access: " << GetAccessStatus() << " => "
               << access_status;

  if (access_status == GetAccessStatus())
    return;

  pref_service_->SetInteger(prefs::kAppsAccessStatus,
                            static_cast<int>(access_status));
  NotifyAppsAccessChanged();

  if (!IsSetupOperationInProgress())
    return;

  switch (access_status) {
    case AccessStatus::kAccessGranted:
      SetAppsSetupOperationStatus(
          AppsAccessSetupOperation::Status::kCompletedSuccessfully);
      break;
    case AccessStatus::kProhibited:
      [[fallthrough]];
    case AccessStatus::kAvailableButNotGranted:
      // Intentionally blank; the operation status should not change.
      break;
  }
}

AccessStatus AppsAccessManagerImpl::ComputeAppsAccessState() {
  if (current_app_policy_state_ ==
      proto::AppStreamingPolicy::APP_POLICY_DISABLED)
    return AccessStatus::kProhibited;

  if (current_apps_access_state_ == proto::AppsAccessState::ACCESS_GRANTED) {
    return AccessStatus::kAccessGranted;
  }
  return AccessStatus::kAvailableButNotGranted;
}

void AppsAccessManagerImpl::UpdateFeatureEnabledState(
    AccessStatus previous_access_status,
    AccessStatus current_access_status) {
  const FeatureState feature_state =
      multidevice_setup_client_->GetFeatureState(Feature::kEche);
  switch (current_access_status) {
    case AccessStatus::kAccessGranted:
      if (IsPhoneHubEnabled() &&
          previous_access_status == AccessStatus::kAvailableButNotGranted) {
        PA_LOG(INFO) << "Enabling Apps when the access is changed from "
                        "kAvailableButNotGranted to kAccessGranted.";
        multidevice_setup_client_->SetFeatureEnabledState(
            Feature::kEche, /*enabled=*/true, /*auth_token=*/std::nullopt,
            base::DoNothing());
      } else if (IsWaitingForAccessToInitiallyEnableApps()) {
        PA_LOG(INFO) << "Enabling Apps for the first time now "
                     << "that access has been granted by the phone.";
        multidevice_setup_client_->SetFeatureEnabledState(
            Feature::kEche, /*enabled=*/true, /*auth_token=*/std::nullopt,
            base::DoNothing());
      }
      break;
    case AccessStatus::kProhibited:
      [[fallthrough]];
    case AccessStatus::kAvailableButNotGranted:
      // Disable Apps if apps access has been revoked
      // by the phone.
      if (feature_state == FeatureState::kEnabledByUser) {
        PA_LOG(INFO) << "Disabling kEche feature.";
        multidevice_setup_client_->SetFeatureEnabledState(
            Feature::kEche, /*enabled=*/false,
            /*auth_token=*/std::nullopt, base::DoNothing());
      }
      break;
  }
}

bool AppsAccessManagerImpl::IsWaitingForAccessToInitiallyEnableApps() const {
  // If the Phone Hub apps feature has never been explicitly set, we should
  // enable it after
  // 1. the top-level Phone Hub feature is enabled, and
  // 2. the phone has granted access.
  // We do *not* want to automatically enable the feature unless the opt-in flow
  // was triggered from this device
  return IsPhoneHubEnabled() && multidevice_setup::IsDefaultFeatureEnabledValue(
                                    Feature::kEche, pref_service_);
}

bool AppsAccessManagerImpl::IsPhoneHubEnabled() const {
  return multidevice_setup_client_->GetFeatureState(Feature::kPhoneHub) ==
         FeatureState::kEnabledByUser;
}

void AppsAccessManagerImpl::UpdateSetupOperationState() {
  AttemptAppsAccessStateRequest();

  if (!IsSetupOperationInProgress())
    return;

  const FeatureStatus previous_feature_status = current_feature_status_;
  current_feature_status_ = feature_status_provider_->GetStatus();

  const ConnectionStatus previous_connection_status =
      current_connection_status_;
  current_connection_status_ = connection_manager_->GetStatus();

  if (previous_feature_status == current_feature_status_ &&
      previous_connection_status == current_connection_status_)
    return;

  // If we were previously connecting and could not establish a connection,
  // send a timeout state.
  if (previous_connection_status == ConnectionStatus::kConnecting &&
      (current_connection_status_ != ConnectionStatus::kConnected ||
       !IsEligibleForOnboarding(current_feature_status_))) {
    SetAppsSetupOperationStatus(
        AppsAccessSetupOperation::Status::kTimedOutConnecting);
    return;
  }

  // If we were previously connected and are now no longer connected, send a
  // connection disconnected state.
  if (previous_connection_status == ConnectionStatus::kConnected &&
      (current_connection_status_ != ConnectionStatus::kConnected ||
       !IsEligibleForOnboarding(current_feature_status_))) {
    SetAppsSetupOperationStatus(
        AppsAccessSetupOperation::Status::kConnectionDisconnected);
    return;
  }

  if (!IsEligibleForOnboarding(current_feature_status_))
    return;

  if (current_connection_status_ == ConnectionStatus::kConnected) {
    SendShowAppsAccessSetupRequest();
  }
}

bool AppsAccessManagerImpl::IsEligibleForOnboarding(
    FeatureStatus feature_status) const {
  return feature_status == FeatureStatus::kConnected ||
         feature_status == FeatureStatus::kConnecting ||
         feature_status == FeatureStatus::kDisconnected ||
         feature_status == FeatureStatus::kDisabled;
}

void AppsAccessManagerImpl::LogAppsSetupResponse(
    proto::Result apps_setup_result) {
  if (!IsSetupOperationInProgress())
    return;

  switch (apps_setup_result) {
    case proto::Result::RESULT_NO_ERROR:
      if (current_apps_access_state_ ==
          proto::AppsAccessState::ACCESS_GRANTED) {
        base::UmaHistogramEnumeration(
            kEcheOnboardingHistogramName,
            OnboardingUserActionMetric::kUserActionPermissionGranted);
      }
      break;
    case proto::Result::RESULT_ERROR_USER_REJECTED:
      base::UmaHistogramEnumeration(
          kEcheOnboardingHistogramName,
          OnboardingUserActionMetric::kUserActionPermissionRejected);
      break;
    case proto::Result::RESULT_ERROR_ACTION_TIMEOUT:
      base::UmaHistogramEnumeration(
          kEcheOnboardingHistogramName,
          OnboardingUserActionMetric::kUserActionTimeout);
      break;
    case proto::Result::RESULT_ERROR_ACTION_CANCELED:
      base::UmaHistogramEnumeration(
          kEcheOnboardingHistogramName,
          OnboardingUserActionMetric::kUserActionRemoteInterrupt);
      break;
    case proto::Result::RESULT_ERROR_SYSTEM:
      base::UmaHistogramEnumeration(kEcheOnboardingHistogramName,
                                    OnboardingUserActionMetric::kSystemError);
      break;
    case proto::Result::RESULT_ACK_BY_EXO:
      base::UmaHistogramEnumeration(kEcheOnboardingHistogramName,
                                    OnboardingUserActionMetric::kAckByExo);
      break;
    default:
      base::UmaHistogramEnumeration(
          kEcheOnboardingHistogramName,
          OnboardingUserActionMetric::kUserActionUnknown);
      break;
  }
}

}  // namespace eche_app
}  // namespace ash