chromium/chromeos/ash/components/phonehub/multidevice_feature_access_manager_impl.cc

// 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/multidevice_feature_access_manager_impl.h"

#include "ash/constants/ash_features.h"
#include "ash/webui/eche_app_ui/pref_names.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/phonehub/connection_scheduler.h"
#include "chromeos/ash/components/phonehub/message_sender.h"
#include "chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h"
#include "chromeos/ash/components/phonehub/pref_names.h"
#include "chromeos/ash/components/phonehub/util/histogram_util.h"
#include "chromeos/ash/services/multidevice_setup/public/cpp/multidevice_setup_client.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "pref_names.h"

namespace ash {
namespace phonehub {

namespace {

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

}  // namespace

// static
void MultideviceFeatureAccessManagerImpl::RegisterPrefs(
    PrefRegistrySimple* registry) {
  registry->RegisterIntegerPref(
      prefs::kCameraRollAccessStatus,
      static_cast<int>(AccessStatus::kAvailableButNotGranted));
  registry->RegisterIntegerPref(
      prefs::kNotificationAccessStatus,
      static_cast<int>(AccessStatus::kAvailableButNotGranted));
  registry->RegisterIntegerPref(
      prefs::kNotificationAccessProhibitedReason,
      static_cast<int>(AccessProhibitedReason::kUnknown));
  registry->RegisterBooleanPref(prefs::kHasDismissedSetupRequiredUi, false);
  registry->RegisterBooleanPref(prefs::kNeedsOneTimeNotificationAccessUpdate,
                                true);
  registry->RegisterBooleanPref(prefs::kFeatureSetupRequestSupported, false);
}

MultideviceFeatureAccessManagerImpl::MultideviceFeatureAccessManagerImpl(
    PrefService* pref_service,
    multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client,
    FeatureStatusProvider* feature_status_provider,
    MessageSender* message_sender,
    ConnectionScheduler* connection_scheduler)
    : pref_service_(pref_service),
      multidevice_setup_client_(multidevice_setup_client),
      feature_status_provider_(feature_status_provider),
      message_sender_(message_sender),
      connection_scheduler_(connection_scheduler) {
  DCHECK(feature_status_provider_);
  DCHECK(message_sender_);
  DCHECK(multidevice_setup_client_);

  current_feature_status_ = feature_status_provider_->GetStatus();
  PA_LOG(VERBOSE) << __func__
                  << ": current feature status = " << current_feature_status_;

  feature_status_provider_->AddObserver(this);

  pref_change_registrar_.Init(pref_service_);
  pref_change_registrar_.Add(
      eche_app::prefs::kAppsAccessStatus,
      base::BindRepeating(
          &MultideviceFeatureAccessManagerImpl::NotifyAppsAccessChanged,
          base::Unretained(this)));
}

MultideviceFeatureAccessManagerImpl::~MultideviceFeatureAccessManagerImpl() {
  feature_status_provider_->RemoveObserver(this);
  pref_change_registrar_.RemoveAll();
}

bool MultideviceFeatureAccessManagerImpl::
    HasMultideviceFeatureSetupUiBeenDismissed() const {
  return pref_service_->GetBoolean(prefs::kHasDismissedSetupRequiredUi);
}

void MultideviceFeatureAccessManagerImpl::DismissSetupRequiredUi() {
  pref_service_->SetBoolean(prefs::kHasDismissedSetupRequiredUi, true);
}

bool MultideviceFeatureAccessManagerImpl::IsAccessRequestAllowed(
    Feature feature) {
  const FeatureState feature_state =
      multidevice_setup_client_->GetFeatureState(feature);
  bool result = feature_state == FeatureState::kDisabledByUser ||
                feature_state == FeatureState::kEnabledByUser;
  return result;
}

MultideviceFeatureAccessManagerImpl::AccessStatus
MultideviceFeatureAccessManagerImpl::GetNotificationAccessStatus() const {
  int status = pref_service_->GetInteger(prefs::kNotificationAccessStatus);
  return static_cast<AccessStatus>(status);
}

MultideviceFeatureAccessManagerImpl::AccessStatus
MultideviceFeatureAccessManagerImpl::GetCameraRollAccessStatus() const {
  int status = pref_service_->GetInteger(prefs::kCameraRollAccessStatus);
  return static_cast<AccessStatus>(status);
}

MultideviceFeatureAccessManager::AccessStatus
MultideviceFeatureAccessManagerImpl::GetAppsAccessStatus() const {
  // TODO(samchiu): The AppsAccessStatus will be updated by eche_app_ui
  // component only. We should listen to pref change and update it to
  // MultiDeviceFeatureOptInView.
  int status = pref_service_->GetInteger(eche_app::prefs::kAppsAccessStatus);
  return static_cast<AccessStatus>(status);
}

bool MultideviceFeatureAccessManagerImpl::GetFeatureSetupRequestSupported()
    const {
  return pref_service_->GetBoolean(prefs::kFeatureSetupRequestSupported);
}

MultideviceFeatureAccessManagerImpl::AccessProhibitedReason
MultideviceFeatureAccessManagerImpl::GetNotificationAccessProhibitedReason()
    const {
  int reason =
      pref_service_->GetInteger(prefs::kNotificationAccessProhibitedReason);
  return static_cast<AccessProhibitedReason>(reason);
}

void MultideviceFeatureAccessManagerImpl::SetNotificationAccessStatusInternal(
    AccessStatus access_status,
    AccessProhibitedReason reason) {
  // TODO(http://crbug.com/1215559): Deprecate when there are no more active
  // Phone Hub notification users on M89. Some users had notifications
  // automatically disabled when updating from M89 to M90+ because the
  // notification feature state went from enabled-by-default to
  // disabled-by-default. To re-enable those users, we once and only once notify
  // observers if access has been granted by the phone. Notably, the
  // MultideviceSetupStateUpdate will decide whether or not the notification
  // feature should be enabled. See MultideviceSetupStateUpdater's method
  // IsWaitingForAccessToInitiallyEnableNotifications() for more details.
  bool needs_one_time_notifications_access_update =
      pref_service_->GetBoolean(prefs::kNeedsOneTimeNotificationAccessUpdate) &&
      access_status == AccessStatus::kAccessGranted;

  if (!needs_one_time_notifications_access_update &&
      !HasAccessStatusChanged(access_status, reason)) {
    return;
  }

  pref_service_->SetBoolean(prefs::kNeedsOneTimeNotificationAccessUpdate,
                            false);

  PA_LOG(INFO) << "Notification access: "
               << std::make_pair(GetNotificationAccessStatus(),
                                 GetNotificationAccessProhibitedReason())
               << " => " << std::make_pair(access_status, reason);

  pref_service_->SetInteger(prefs::kNotificationAccessStatus,
                            static_cast<int>(access_status));
  pref_service_->SetInteger(prefs::kNotificationAccessProhibitedReason,
                            static_cast<int>(reason));
  NotifyNotificationAccessChanged();

  if (IsNotificationSetupOperationInProgress()) {
    switch (access_status) {
      case AccessStatus::kProhibited:
        SetNotificationSetupOperationStatus(
            NotificationAccessSetupOperation::Status::
                kProhibitedFromProvidingAccess);
        break;
      case AccessStatus::kAccessGranted:
        SetNotificationSetupOperationStatus(
            NotificationAccessSetupOperation::Status::kCompletedSuccessfully);
        break;
      case AccessStatus::kAvailableButNotGranted:
        // Intentionally blank; the operation status should not change.
        break;
    }
  } else if (IsCombinedSetupOperationInProgress()) {
    switch (access_status) {
      case AccessStatus::kProhibited:
        SetCombinedSetupOperationStatus(CombinedAccessSetupOperation::Status::
                                            kProhibitedFromProvidingAccess);
        break;
      case AccessStatus::kAccessGranted:
        combined_setup_notifications_pending_ = false;
        break;
      case AccessStatus::kAvailableButNotGranted:
        // Intentionally blank; the operation status should not change.
        break;
    }
    if (!combined_setup_notifications_pending_ &&
        !combined_setup_camera_roll_pending_) {
      SetCombinedSetupOperationStatus(
          CombinedAccessSetupOperation::Status::kCompletedSuccessfully);
    }
  }
}

void MultideviceFeatureAccessManagerImpl::SetCameraRollAccessStatusInternal(
    AccessStatus access_status) {
  PA_LOG(INFO) << "Camera Roll access: " << GetCameraRollAccessStatus()
               << " => " << access_status;
  pref_service_->SetInteger(prefs::kCameraRollAccessStatus,
                            static_cast<int>(access_status));
  NotifyCameraRollAccessChanged();

  if (!IsCombinedSetupOperationInProgress()) {
    return;
  }

  switch (access_status) {
    case AccessStatus::kProhibited:
      SetCombinedSetupOperationStatus(
          CombinedAccessSetupOperation::Status::kProhibitedFromProvidingAccess);
      break;
    case AccessStatus::kAccessGranted:
      combined_setup_camera_roll_pending_ = false;
      break;
    case AccessStatus::kAvailableButNotGranted:
      // Intentionally blank; the operation status should not change.
      break;
  }
  if (!combined_setup_notifications_pending_ &&
      !combined_setup_camera_roll_pending_) {
    SetCombinedSetupOperationStatus(
        CombinedAccessSetupOperation::Status::kCompletedSuccessfully);
  }
}

void MultideviceFeatureAccessManagerImpl::
    SetFeatureSetupRequestSupportedInternal(bool supported) {
  pref_service_->SetBoolean(prefs::kFeatureSetupRequestSupported, supported);
  NotifyFeatureSetupRequestSupportedChanged();
}

void MultideviceFeatureAccessManagerImpl::OnNotificationSetupRequested() {
  PA_LOG(INFO) << "Notification access setup flow started.";

  switch (feature_status_provider_->GetStatus()) {
    // We're already connected, so request that the UI be shown on the phone.
    case FeatureStatus::kEnabledAndConnected:
      SendShowNotificationAccessSetupRequest();
      break;
    // We're already connecting, so wait until a connection succeeds before
    // trying to send a message
    case FeatureStatus::kEnabledAndConnecting:
      SetNotificationSetupOperationStatus(
          NotificationAccessSetupOperation::Status::kConnecting);
      break;
    // We are not connected, so schedule a connection; once the
    // connection succeeds, we'll send the message in OnFeatureStatusChanged().
    case FeatureStatus::kEnabledButDisconnected:
      SetNotificationSetupOperationStatus(
          NotificationAccessSetupOperation::Status::kConnecting);
      connection_scheduler_->ScheduleConnectionNow(
          phonehub::DiscoveryEntryPoint::kMultiDeviceFeatureSetup);
      break;
    default:
      NOTREACHED_IN_MIGRATION();
      break;
  }
}

void MultideviceFeatureAccessManagerImpl::OnCombinedSetupRequested(
    bool camera_roll,
    bool notifications) {
  combined_setup_camera_roll_pending_ = camera_roll;
  combined_setup_notifications_pending_ = notifications;
  PA_LOG(INFO) << "Combined access setup flow started.";

  switch (feature_status_provider_->GetStatus()) {
    // We're already connected, so request that the UI be shown on the phone.
    case FeatureStatus::kEnabledAndConnected:
      SendShowCombinedAccessSetupRequest();
      break;
    // We're already connecting, so wait until a connection succeeds before
    // trying to send a message
    case FeatureStatus::kEnabledAndConnecting:
      SetCombinedSetupOperationStatus(
          CombinedAccessSetupOperation::Status::kConnecting);
      break;
    // We are not connected, so schedule a connection; once the
    // connection succeeds, we'll send the message in OnFeatureStatusChanged().
    case FeatureStatus::kEnabledButDisconnected:
      SetCombinedSetupOperationStatus(
          CombinedAccessSetupOperation::Status::kConnecting);
      connection_scheduler_->ScheduleConnectionNow(
          DiscoveryEntryPoint::kMultiDeviceFeatureSetup);
      break;
    default:
      NOTREACHED_IN_MIGRATION();
      break;
  }
}

void MultideviceFeatureAccessManagerImpl::OnFeatureSetupConnectionRequested() {
  PA_LOG(INFO) << "Connection for feature setup started";

  switch (feature_status_provider_->GetStatus()) {
    case FeatureStatus::kEnabledAndConnected:
      SetFeatureSetupConnectionOperationStatus(
          FeatureSetupConnectionOperation::Status::kConnected);
      break;
    case FeatureStatus::kEnabledAndConnecting:
      SetFeatureSetupConnectionOperationStatus(
          FeatureSetupConnectionOperation::Status::kConnecting);
      break;
    case FeatureStatus::kEnabledButDisconnected:
    case FeatureStatus::kUnavailableBluetoothOff:
      SetFeatureSetupConnectionOperationStatus(
          FeatureSetupConnectionOperation::Status::kConnecting);
      connection_scheduler_->ScheduleConnectionNow(
          DiscoveryEntryPoint::kMultiDeviceFeatureSetup);
      break;
    default:
      DUMP_WILL_BE_NOTREACHED();
      break;
  }
}

void MultideviceFeatureAccessManagerImpl::OnFeatureStatusChanged() {
  if (IsFeatureSetupConnectionOperationInProgress()) {
    FeatureStatusChangedFeatureSetupConnection();
  } else if (IsNotificationSetupOperationInProgress()) {
    FeatureStatusChangedNotificationAccessSetup();
  } else if (IsCombinedSetupOperationInProgress()) {
    FeatureStatusChangedCombinedAccessSetup();
  }
}

void MultideviceFeatureAccessManagerImpl::
    FeatureStatusChangedNotificationAccessSetup() {
  const FeatureStatus previous_feature_status = current_feature_status_;
  current_feature_status_ = feature_status_provider_->GetStatus();

  PA_LOG(VERBOSE) << __func__
                  << ": previous feature status = " << previous_feature_status
                  << ", current feature status = " << current_feature_status_;

  if (previous_feature_status == current_feature_status_)
    return;

  // If we were previously connecting and could not establish a connection,
  // send a timeout state.
  if (previous_feature_status == FeatureStatus::kEnabledAndConnecting &&
      current_feature_status_ != FeatureStatus::kEnabledAndConnected) {
    SetNotificationSetupOperationStatus(
        NotificationAccessSetupOperation::Status::kTimedOutConnecting);
    return;
  }

  // If we were previously connected and are now no longer connected, send a
  // connection disconnected state.
  if (previous_feature_status == FeatureStatus::kEnabledAndConnected &&
      current_feature_status_ != FeatureStatus::kEnabledAndConnected) {
    SetNotificationSetupOperationStatus(
        NotificationAccessSetupOperation::Status::kConnectionDisconnected);
    return;
  }

  if (current_feature_status_ == FeatureStatus::kEnabledAndConnected) {
    SendShowNotificationAccessSetupRequest();
    return;
  }
}

void MultideviceFeatureAccessManagerImpl::
    FeatureStatusChangedCombinedAccessSetup() {
  const FeatureStatus previous_feature_status = current_feature_status_;
  current_feature_status_ = feature_status_provider_->GetStatus();

  PA_LOG(VERBOSE) << __func__
                  << ": previous feature status = " << previous_feature_status
                  << ", current feature status = " << current_feature_status_;

  if (previous_feature_status == current_feature_status_)
    return;

  // If we were previously connecting and could not establish a connection,
  // send a timeout state.
  if (previous_feature_status == FeatureStatus::kEnabledAndConnecting &&
      current_feature_status_ != FeatureStatus::kEnabledAndConnected) {
    SetCombinedSetupOperationStatus(
        CombinedAccessSetupOperation::Status::kTimedOutConnecting);
    return;
  }

  // If we were previously connected and are now no longer connected, send a
  // connection disconnected state.
  if (previous_feature_status == FeatureStatus::kEnabledAndConnected &&
      current_feature_status_ != FeatureStatus::kEnabledAndConnected) {
    SetCombinedSetupOperationStatus(
        CombinedAccessSetupOperation::Status::kConnectionDisconnected);
    return;
  }

  if (current_feature_status_ == FeatureStatus::kEnabledAndConnected) {
    SendShowCombinedAccessSetupRequest();
    return;
  }
}

void MultideviceFeatureAccessManagerImpl::
    FeatureStatusChangedFeatureSetupConnection() {
  const FeatureStatus previous_feature_status = current_feature_status_;
  current_feature_status_ = feature_status_provider_->GetStatus();

  PA_LOG(VERBOSE) << __func__
                  << ": previous feature status = " << previous_feature_status
                  << ", current feature status = " << current_feature_status_;

  if (previous_feature_status == current_feature_status_)
    return;

  // If we were previously connecting and could not establish a connection,
  // send a timeout state.
  if (previous_feature_status == FeatureStatus::kEnabledAndConnecting &&
      current_feature_status_ != FeatureStatus::kEnabledAndConnected) {
    SetFeatureSetupConnectionOperationStatus(
        FeatureSetupConnectionOperation::Status::kTimedOutConnecting);
    return;
  }

  if (previous_feature_status == FeatureStatus::kEnabledAndConnected &&
      current_feature_status_ != FeatureStatus::kEnabledAndConnected) {
    SetFeatureSetupConnectionOperationStatus(
        FeatureSetupConnectionOperation::Status::kConnectionLost);
    return;
  }

  if (current_feature_status_ == FeatureStatus::kEnabledAndConnected) {
    feature_setup_connection_update_pending_ = true;
    return;
  }
}

void MultideviceFeatureAccessManagerImpl::
    UpdatedFeatureSetupConnectionStatusIfNeeded() {
  if (feature_setup_connection_update_pending_) {
    SetFeatureSetupConnectionOperationStatus(
        FeatureSetupConnectionOperation::Status::kConnected);
  }
  feature_setup_connection_update_pending_ = false;
}

void MultideviceFeatureAccessManagerImpl::
    SendShowNotificationAccessSetupRequest() {
  message_sender_->SendShowNotificationAccessSetupRequest();
  SetNotificationSetupOperationStatus(
      NotificationAccessSetupOperation::Status::
          kSentMessageToPhoneAndWaitingForResponse);
}

void MultideviceFeatureAccessManagerImpl::SendShowCombinedAccessSetupRequest() {
  message_sender_->SendFeatureSetupRequest(
      combined_setup_camera_roll_pending_,
      combined_setup_notifications_pending_);
  SetCombinedSetupOperationStatus(CombinedAccessSetupOperation::Status::
                                      kSentMessageToPhoneAndWaitingForResponse);
}

bool MultideviceFeatureAccessManagerImpl::HasAccessStatusChanged(
    AccessStatus access_status,
    AccessProhibitedReason reason) {
  if (access_status != GetNotificationAccessStatus())
    return true;
  if (access_status == AccessStatus::kProhibited &&
      reason != GetNotificationAccessProhibitedReason()) {
    return true;
  }
  return false;
}

}  // namespace phonehub
}  // namespace ash