chromium/chromeos/components/quick_answers/public/cpp/quick_answers_state.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/components/quick_answers/public/cpp/quick_answers_state.h"

#include <cstdint>

#include "base/check_is_test.h"
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_set.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/stringprintf.h"
#include "base/system/sys_info.h"
#include "base/types/expected.h"
#include "chromeos/components/kiosk/kiosk_utils.h"
#include "chromeos/components/magic_boost/public/cpp/magic_boost_state.h"
#include "chromeos/components/quick_answers/public/cpp/quick_answers_prefs.h"
#include "chromeos/constants/chromeos_features.h"
#include "third_party/icu/source/common/unicode/locid.h"
#include "ui/base/l10n/l10n_util.h"

namespace {

QuickAnswersState* g_quick_answers_state = nullptr;

// Supported languages of the Quick Answers feature.
constexpr auto kSupportedLanguages =
    base::MakeFixedFlatSet<std::string>({"en", "es", "it", "fr", "pt", "de"});

// TODO(b/340628526): extract Error and ConsentStatus enums to a shared place.
QuickAnswersState::Error ToQuickAnswersStateError(
    chromeos::MagicBoostState::Error error) {
  switch (error) {
    case chromeos::MagicBoostState::Error::kUninitialized:
      return QuickAnswersState::Error::kUninitialized;
  }

  CHECK(false) << "Unknown MagicBoostState::Error enum class value provided.";
}

quick_answers::prefs::ConsentStatus ToQuickAnswersPrefsConsentStatus(
    chromeos::HMRConsentStatus consent_status) {
  switch (consent_status) {
    case chromeos::HMRConsentStatus::kUnset:
      return quick_answers::prefs::ConsentStatus::kUnknown;
    case chromeos::HMRConsentStatus::kPendingDisclaimer:
      // Quick Answers capability is available from `kPendingDisclaimer` state.
      // See comments in `chromeos::HMRConsentStatus` for details of those
      // states.
      return quick_answers::prefs::ConsentStatus::kAccepted;
    case chromeos::HMRConsentStatus::kApproved:
      return quick_answers::prefs::ConsentStatus::kAccepted;
    case chromeos::HMRConsentStatus::kDeclined:
      return quick_answers::prefs::ConsentStatus::kRejected;
  }

  CHECK(false) << "Unknown HMRConsentStatus enum class value provided.";
}

base::expected<bool, QuickAnswersState::Error> ToQuickAnswersStateIsEnabled(
    base::expected<bool, chromeos::MagicBoostState::Error> is_enabled) {
  return is_enabled.transform_error(&ToQuickAnswersStateError);
}

base::expected<quick_answers::prefs::ConsentStatus, QuickAnswersState::Error>
ToQuickAnswersStateConsentStatus(
    base::expected<chromeos::HMRConsentStatus, chromeos::MagicBoostState::Error>
        consent_status) {
  return consent_status.transform_error(&ToQuickAnswersStateError)
      .transform(&ToQuickAnswersPrefsConsentStatus);
}

}  // namespace

// static
QuickAnswersState* QuickAnswersState::Get() {
  return g_quick_answers_state;
}

// static
QuickAnswersState::FeatureType QuickAnswersState::GetFeatureType() {
  return chromeos::features::IsMagicBoostEnabled()
             ? QuickAnswersState::FeatureType::kHmr
             : QuickAnswersState::FeatureType::kQuickAnswers;
}

// static
bool QuickAnswersState::IsEligible() {
  return IsEligibleAs(GetFeatureType());
}

// static
bool QuickAnswersState::IsEligibleAs(
    QuickAnswersState::FeatureType feature_type) {
  QuickAnswersState* quick_answers_state = Get();
  if (!quick_answers_state) {
    return false;
  }

  return quick_answers_state->IsEligibleExpectedAs(feature_type)
      .value_or(false);
}

// static
bool QuickAnswersState::IsEnabled() {
  return IsEnabledAs(GetFeatureType());
}

// static
bool QuickAnswersState::IsEnabledAs(FeatureType feature_type) {
  QuickAnswersState* quick_answers_state = Get();
  if (!quick_answers_state) {
    return false;
  }

  return quick_answers_state->IsEnabledExpectedAs(feature_type).value_or(false);
}

// static
base::expected<quick_answers::prefs::ConsentStatus, QuickAnswersState::Error>
QuickAnswersState::GetConsentStatus() {
  return GetConsentStatusAs(GetFeatureType());
}

// static
base::expected<quick_answers::prefs::ConsentStatus, QuickAnswersState::Error>
QuickAnswersState::GetConsentStatusAs(FeatureType feature_type) {
  QuickAnswersState* quick_answers_state = Get();
  if (!quick_answers_state) {
    return base::unexpected(QuickAnswersState::Error::kUninitialized);
  }

  return quick_answers_state->GetConsentStatusExpectedAs(feature_type);
}

// static
bool QuickAnswersState::IsIntentEligible(quick_answers::Intent intent) {
  return IsIntentEligibleAs(intent, GetFeatureType());
}

// static
bool QuickAnswersState::IsIntentEligibleAs(quick_answers::Intent intent,
                                           FeatureType feature_type) {
  QuickAnswersState* quick_answers_state = Get();
  if (!quick_answers_state) {
    return false;
  }

  return quick_answers_state->IsIntentEligibleExpectedAs(intent, feature_type)
      .value_or(false);
}

QuickAnswersState::QuickAnswersState() {
  CHECK(!g_quick_answers_state);
  g_quick_answers_state = this;

  if (GetFeatureType() == FeatureType::kHmr) {
    chromeos::MagicBoostState* magic_boost_state =
        chromeos::MagicBoostState::Get();
    CHECK(magic_boost_state) << "QuickAnswersState depends on MagicBoostState.";
    magic_boost_state_observation_.Observe(magic_boost_state);
  }
}

QuickAnswersState::~QuickAnswersState() {
  DCHECK_EQ(g_quick_answers_state, this);
  g_quick_answers_state = nullptr;
}

void QuickAnswersState::AddObserver(QuickAnswersStateObserver* observer) {
  observers_.AddObserver(observer);
  InitializeObserver(observer);
}

void QuickAnswersState::RemoveObserver(QuickAnswersStateObserver* observer) {
  observers_.RemoveObserver(observer);
}

void QuickAnswersState::OnHMREnabledUpdated(bool enabled) {
  MaybeNotifyIsEnabledChanged();
}

void QuickAnswersState::OnHMRConsentStatusUpdated(
    chromeos::HMRConsentStatus consent_status) {
  MaybeNotifyConsentStatusChanged();
}

void QuickAnswersState::OnIsDeleting() {
  magic_boost_state_observation_.Reset();
}

void QuickAnswersState::AsyncSetConsentStatus(
    quick_answers::prefs::ConsentStatus consent_status) {
  switch (consent_status) {
    case quick_answers::prefs::ConsentStatus::kAccepted:
      AsyncWriteConsentStatus(quick_answers::prefs::ConsentStatus::kAccepted);
      AsyncWriteEnabled(true);
      break;
    case quick_answers::prefs::ConsentStatus::kRejected:
      AsyncWriteConsentStatus(quick_answers::prefs::ConsentStatus::kRejected);
      AsyncWriteEnabled(false);
      break;
    case quick_answers::prefs::ConsentStatus::kUnknown:
      // This is test only path for now. `kUnknown` is set only from default
      // values in prod.
      CHECK_IS_TEST();

      AsyncWriteConsentStatus(quick_answers::prefs::ConsentStatus::kUnknown);
      AsyncWriteEnabled(false);
      break;
  }
}

int32_t QuickAnswersState::AsyncIncrementImpressionCount() {
  int32_t incremented_count = consent_ui_impression_count_ + 1;
  AsyncWriteConsentUiImpressionCount(incremented_count);
  return incremented_count;
}

bool QuickAnswersState::ShouldUseQuickAnswersTextAnnotator() {
  return base::SysInfo::IsRunningOnChromeOS() ||
         use_text_annotator_for_testing_;
}

bool QuickAnswersState::IsSupportedLanguage(const std::string& language) const {
  return kSupportedLanguages.contains(language);
}

base::expected<bool, QuickAnswersState::Error>
QuickAnswersState::IsEligibleExpected() const {
  return IsEligibleExpectedAs(GetFeatureType());
}

base::expected<bool, QuickAnswersState::Error>
QuickAnswersState::IsEligibleExpectedAs(
    QuickAnswersState::FeatureType feature_type) const {
  if (is_eligible_for_testing_.has_value()) {
    CHECK_IS_TEST();
    return is_eligible_for_testing_.value();
  }

  if (GetFeatureType() != feature_type) {
    return false;
  }

  if (resolved_application_locale_.empty()) {
    return base::unexpected(QuickAnswersState::Error::kUninitialized);
  }

  return IsSupportedLanguage(
      l10n_util::GetLanguage(resolved_application_locale_));
}

base::expected<bool, QuickAnswersState::Error>
QuickAnswersState::IsEnabledExpected() const {
  return IsEnabledExpectedAs(GetFeatureType());
}

base::expected<bool, QuickAnswersState::Error>
QuickAnswersState::IsEnabledExpectedAs(
    QuickAnswersState::FeatureType feature_type) const {
  // TODO(b/340628526): Use `IsEligibleExpectedAs` to propagate error values.
  if (!IsEligibleAs(feature_type)) {
    return false;
  }

  // Quick Answers capability should not be enabled without kAccepted consent
  // status. Note that there is a combination of IsEnabled=false and
  // ConsentStatus=kAccepted if a user has turned off a feature after they have
  // enabled/consented.
  // TODO(b/340628526): Use `GetConsentStatusExpectedAs` to propagate error
  // values.
  if (GetConsentStatusAs(feature_type) !=
      quick_answers::prefs::ConsentStatus::kAccepted) {
    return false;
  }

  switch (feature_type) {
    case QuickAnswersState::FeatureType::kHmr: {
      chromeos::MagicBoostState* magic_boost_state =
          chromeos::MagicBoostState::Get();
      if (!magic_boost_state) {
        return base::unexpected(QuickAnswersState::Error::kUninitialized);
      }

      return ToQuickAnswersStateIsEnabled(magic_boost_state->hmr_enabled());
    }
    case QuickAnswersState::FeatureType::kQuickAnswers: {
      if (chromeos::IsKioskSession()) {
        return false;
      }

      return quick_answers_enabled_;
    }
  }
}

base::expected<quick_answers::prefs::ConsentStatus, QuickAnswersState::Error>
QuickAnswersState::GetConsentStatusExpected() const {
  return GetConsentStatusExpectedAs(GetFeatureType());
}

base::expected<quick_answers::prefs::ConsentStatus, QuickAnswersState::Error>
QuickAnswersState::GetConsentStatusExpectedAs(
    QuickAnswersState::FeatureType feature_type) const {
  switch (feature_type) {
    case QuickAnswersState::FeatureType::kHmr: {
      chromeos::MagicBoostState* magic_boost_state =
          chromeos::MagicBoostState::Get();
      if (!magic_boost_state) {
        return base::unexpected(QuickAnswersState::Error::kUninitialized);
      }

      return ToQuickAnswersStateConsentStatus(
          magic_boost_state->hmr_consent_status());
    }
    case QuickAnswersState::FeatureType::kQuickAnswers: {
      return quick_answers_consent_status_;
    }
  }
}

base::expected<bool, QuickAnswersState::Error>
QuickAnswersState::IsIntentEligibleExpected(
    quick_answers::Intent intent) const {
  return IsIntentEligibleExpectedAs(intent, GetFeatureType());
}

base::expected<bool, QuickAnswersState::Error>
QuickAnswersState::IsIntentEligibleExpectedAs(
    quick_answers::Intent intent,
    QuickAnswersState::FeatureType feature_type) const {
  // Use `IsEligibleExpectedAs` instead of `IsEligibleAs` since we would like to
  // return an error value if eligible is an error value.
  base::expected<bool, QuickAnswersState::Error> maybe_eligible =
      IsEligibleExpectedAs(feature_type);
  if (maybe_eligible != true) {
    return maybe_eligible;
  }

  switch (feature_type) {
    case QuickAnswersState::FeatureType::kHmr:
      // All intents are always eligible for kHmr.
      return true;
    case QuickAnswersState::FeatureType::kQuickAnswers:
      switch (intent) {
        case quick_answers::Intent::kDefinition:
          return quick_answers_definition_eligible_;
        case quick_answers::Intent::kTranslation:
          return quick_answers_translation_eligible_;
        case quick_answers::Intent::kUnitConversion:
          return quick_answers_unit_conversion_eligible_;
      }

      CHECK(false) << "Invalid IntentType enum class value provided.";
  }
}

void QuickAnswersState::SetEligibilityForTesting(bool is_eligible) {
  CHECK_IS_TEST();
  is_eligible_for_testing_ = is_eligible;
  MaybeNotifyEligibilityChanged();
}

void QuickAnswersState::SetQuickAnswersFeatureConsentStatus(
    quick_answers::prefs::ConsentStatus consent_status) {
  quick_answers_consent_status_ = consent_status;

  MaybeNotifyConsentStatusChanged();
}

void QuickAnswersState::SetIntentEligibilityAsQuickAnswers(
    quick_answers::Intent intent,
    bool eligible) {
  switch (intent) {
    case quick_answers::Intent::kDefinition:
      quick_answers_definition_eligible_ = eligible;
      return;
    case quick_answers::Intent::kTranslation:
      quick_answers_translation_eligible_ = eligible;
      return;
    case quick_answers::Intent::kUnitConversion:
      quick_answers_unit_conversion_eligible_ = eligible;
      return;
  }

  CHECK(false) << "Invalid Intent enum class value provided.";
}

void QuickAnswersState::InitializeObserver(
    QuickAnswersStateObserver* observer) {
  if (prefs_initialized_) {
    observer->OnPrefsInitialized();
    observer->OnApplicationLocaleReady(resolved_application_locale_);
    observer->OnPreferredLanguagesChanged(preferred_languages_);
  }

  base::expected<bool, QuickAnswersState::Error> maybe_is_enabled =
      IsEnabledExpected();
  if (maybe_is_enabled.has_value()) {
    observer->OnSettingsEnabled(maybe_is_enabled.value());
  }

  base::expected<bool, QuickAnswersState::Error> maybe_is_eligible =
      IsEligibleExpected();
  if (maybe_is_eligible.has_value()) {
    observer->OnEligibilityChanged(maybe_is_eligible.value());
  }

  base::expected<quick_answers::prefs::ConsentStatus, QuickAnswersState::Error>
      maybe_consent_status = GetConsentStatusExpected();
  if (maybe_consent_status.has_value()) {
    observer->OnConsentStatusUpdated(maybe_consent_status.value());
  }
}

void QuickAnswersState::MaybeNotifyEligibilityChanged() {
  base::expected<bool, QuickAnswersState::Error> is_eligible =
      IsEligibleExpected();

  if (last_notified_is_eligible_ == is_eligible) {
    return;
  }

  last_notified_is_eligible_ = is_eligible;

  if (!last_notified_is_eligible_.has_value()) {
    return;
  }

  for (auto& observer : observers_) {
    observer.OnEligibilityChanged(last_notified_is_eligible_.value());
  }

  // `IsEnabled` depends on `IsEligible`. Maybe notify if eligibility has
  // changed.
  MaybeNotifyIsEnabledChanged();
}

void QuickAnswersState::MaybeNotifyIsEnabledChanged() {
  base::expected<bool, QuickAnswersState::Error> is_enabled =
      IsEnabledExpected();

  if (last_notified_is_enabled_ == is_enabled) {
    return;
  }

  last_notified_is_enabled_ = is_enabled;

  if (!last_notified_is_enabled_.has_value()) {
    return;
  }

  for (auto& observer : observers_) {
    observer.OnSettingsEnabled(last_notified_is_enabled_.value());
  }
}

void QuickAnswersState::MaybeNotifyConsentStatusChanged() {
  base::expected<quick_answers::prefs::ConsentStatus, Error> consent_status =
      GetConsentStatusExpected();

  if (last_notified_consent_status_ == consent_status) {
    return;
  }

  last_notified_consent_status_ = consent_status;

  // TODO(b/340628526): Change other MaybeNotify methods to notify a value
  // change to dependent values for error value case.
  if (last_notified_consent_status_.has_value()) {
    for (auto& observer : observers_) {
      observer.OnConsentStatusUpdated(last_notified_consent_status_.value());
    }
  }

  // `IsEnabled` depends on `GetConsentStatus`. Maybe notify if consent status
  // has changed.
  MaybeNotifyIsEnabledChanged();
}