chromium/chrome/browser/ash/account_manager/account_apps_availability.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 "chrome/browser/ash/account_manager/account_apps_availability.h"

#include <optional>

#include "ash/constants/ash_features.h"
#include "base/containers/flat_set.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "components/account_manager_core/account.h"
#include "components/account_manager_core/account_manager_facade.h"
#include "components/account_manager_core/pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/user_manager/user_manager.h"

// Structure of `account_manager::prefs::kAccountAppsAvailability`.
// `kAccountAppsAvailability` is a dictionary of dictionaries of the following
// format:
// {
//   "gaia_id_1": { "is_available_in_arc": <bool> },
//   "gaia_id_2": { "is_available_in_arc": <bool> },
// }
// Regular users will always have an entry for the primary account in the
// `kAccountAppsAvailability` pref (so it will never be empty). Active Directory
// users may have no Gaia accounts in-session and therefore may have an empty
// `kAccountAppsAvailability` pref.

namespace ash {

namespace {

constexpr int kMaxNumAccountsInArcMetric =
    10;  // To match AccountManager.NumAccounts metrics.

bool IsPrimaryGaiaAccount(const std::string& gaia_id) {
  const user_manager::User* user =
      user_manager::UserManager::Get()->GetPrimaryUser();
  // GetPrimaryUser may return nullptr in tests.
  if (!user)
    return false;

  return user->GetAccountId().GetAccountType() == AccountType::GOOGLE &&
         user->GetAccountId().GetGaiaId() == gaia_id;
}

bool IsPrefInitialized(PrefService* prefs) {
  const base::Value::Dict& accounts =
      prefs->GetDict(account_manager::prefs::kAccountAppsAvailability);
  return accounts.size() > 0;
}

void CompleteFindAccountByGaiaId(
    const std::string& gaia_id,
    base::OnceCallback<void(const std::optional<account_manager::Account>&)>
        callback,
    const std::vector<account_manager::Account>& accounts) {
  for (const auto& account : accounts) {
    if (account.key.account_type() == account_manager::AccountType::kGaia &&
        account.key.id() == gaia_id) {
      std::move(callback).Run(account);
      return;
    }
  }
  LOG(ERROR) << "Couldn't find account by gaia id in AccountManager";
  std::move(callback).Run(std::nullopt);
}

void CompleteGetAccountsAvailableInArc(
    const base::flat_set<std::string>& gaia_ids_in_arc,
    base::OnceCallback<void(const base::flat_set<account_manager::Account>&)>
        callback,
    const std::vector<account_manager::Account>& all_accounts) {
  base::flat_set<account_manager::Account> result;
  for (const auto& account : all_accounts) {
    if (account.key.account_type() != account_manager::AccountType::kGaia)
      continue;

    if (gaia_ids_in_arc.contains(account.key.id()))
      result.insert(account);
  }

  DCHECK_EQ(result.size(), gaia_ids_in_arc.size());
  if (result.size() != gaia_ids_in_arc.size()) {
    LOG(ERROR) << "Expected " << gaia_ids_in_arc.size() << " accounts, but "
               << result.size() << " accounts were found in Account Manager.";
    // TODO(crbug.com/1277453): Repair prefs if this happens.
  }
  std::move(callback).Run(result);
}

base::flat_set<std::string> GetGaiaIdsAvailableInArc(PrefService* prefs) {
  base::flat_set<std::string> result;
  const base::Value::Dict& accounts =
      prefs->GetDict(account_manager::prefs::kAccountAppsAvailability);

  // See structure of `accounts` at the top of the file.
  for (const auto dict : accounts) {
    std::optional<bool> is_available = dict.second.GetDict().FindBool(
        account_manager::prefs::kIsAvailableInArcKey);
    if (!is_available.has_value() || !is_available.value())
      continue;

    result.insert(dict.first);
  }

  return result;
}

// Return `true` if account with `gaia_id` should be available in ARC.
// Return `false` if account with `gaia_id` should not be available in ARC.
// Return `nullopt` if account with `gaia_id` is not in prefs (it can happen if
// `SetIsAccountAvailableInArc` wasn't called for this account yet).
std::optional<bool> IsAccountAvailableInArc(PrefService* prefs,
                                            const std::string& gaia_id) {
  const base::Value::Dict& accounts =
      prefs->GetDict(account_manager::prefs::kAccountAppsAvailability);

  // See structure of `accounts` at the top of the file.
  const base::Value::Dict* account_entry = accounts.FindDict(gaia_id);
  if (!account_entry)
    return std::nullopt;

  std::optional<bool> is_available_in_arc =
      account_entry->FindBool(account_manager::prefs::kIsAvailableInArcKey);
  DCHECK(is_available_in_arc);
  // If there is no `is_available_in_arc` key, assume that account is available
  // in ARC.
  // TODO(crbug.com/1277453): Repair prefs if it happens.
  return is_available_in_arc.value_or(true);
}

void RemoveAccountFromPrefs(PrefService* prefs, const std::string& gaia_id) {
  DCHECK(!IsPrimaryGaiaAccount(gaia_id));

  ScopedDictPrefUpdate update(prefs,
                              account_manager::prefs::kAccountAppsAvailability);
  const bool success = update->Remove(gaia_id);
  if (!success)
    LOG(ERROR) << "Account apps availability pref not found";
}

void AddAccountToPrefs(PrefService* prefs,
                       const std::string& gaia_id,
                       bool is_available_in_arc) {
  // Account shouldn't already exist.
  DCHECK(!IsAccountAvailableInArc(prefs, gaia_id).has_value());

  base::Value::Dict account_entry;
  account_entry.Set(account_manager::prefs::kIsAvailableInArcKey,
                    base::Value(is_available_in_arc));

  ScopedDictPrefUpdate update(prefs,
                              account_manager::prefs::kAccountAppsAvailability);
  update->Set(gaia_id, std::move(account_entry));
}

void UpdateAccountInPrefs(PrefService* prefs,
                          const std::string& gaia_id,
                          bool is_available_in_arc) {
  ScopedDictPrefUpdate update(prefs,
                              account_manager::prefs::kAccountAppsAvailability);
  base::Value::Dict* account_entry = update->FindDict(gaia_id);
  DCHECK(account_entry);

  account_entry->Set(account_manager::prefs::kIsAvailableInArcKey,
                     is_available_in_arc);
}

}  // namespace

// static
const char AccountAppsAvailability::kNumAccountsInArcMetricName[] =
    "Arc.Auth.NumAccounts";

// static
const char AccountAppsAvailability::kPercentAccountsInArcMetricName[] =
    "Arc.Auth.PercentAccounts";

AccountAppsAvailability::AccountAppsAvailability(
    account_manager::AccountManagerFacade* account_manager_facade,
    signin::IdentityManager* identity_manager,
    PrefService* prefs)
    : account_manager_facade_(account_manager_facade),
      identity_manager_(identity_manager),
      prefs_(prefs) {
  DCHECK(account_manager_facade_);
  DCHECK(identity_manager_);
  DCHECK(prefs_);

  account_manager_facade_observation_.Observe(account_manager_facade_.get());
  identity_manager_observation_.Observe(identity_manager_.get());

  if (IsPrefInitialized(prefs_)) {
    is_initialized_ = true;
    // The metric is recorded once per session.
    account_manager_facade_->GetAccounts(base::BindOnce(
        &AccountAppsAvailability::ReportMetrics, weak_factory_.GetWeakPtr()));
    return;
  }

  account_manager_facade_->GetAccounts(
      base::BindOnce(&AccountAppsAvailability::InitAccountsAvailableInArcPref,
                     weak_factory_.GetWeakPtr()));
}

AccountAppsAvailability::~AccountAppsAvailability() = default;

// static
bool AccountAppsAvailability::IsArcAccountRestrictionsEnabled() {
  return crosapi::browser_util::IsLacrosEnabled();
}

bool AccountAppsAvailability::IsArcManagedAccountRestrictionEnabled() {
  return base::FeatureList::IsEnabled(
      ash::features::kSecondaryAccountAllowedInArcPolicy);
}

// static
void AccountAppsAvailability::RegisterPrefs(PrefRegistrySimple* registry) {
  registry->RegisterDictionaryPref(
      account_manager::prefs::kAccountAppsAvailability);
}

void AccountAppsAvailability::AddObserver(Observer* observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  observer_list_.AddObserver(observer);
}

void AccountAppsAvailability::RemoveObserver(Observer* observer) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  observer_list_.RemoveObserver(observer);
}

void AccountAppsAvailability::SetIsAccountAvailableInArc(
    const account_manager::Account& account,
    bool is_available) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK_EQ(account.key.account_type(), account_manager::AccountType::kGaia);

  if (!IsInitialized()) {
    // Using base::Unretained(this) is fine because `initialization_callbacks_`
    // is owned by this.
    initialization_callbacks_.push_back(
        base::BindOnce(&AccountAppsAvailability::SetIsAccountAvailableInArc,
                       base::Unretained(this), account, is_available));
    return;
  }

  std::optional<bool> current_status =
      IsAccountAvailableInArc(prefs_, account.key.id());
  if (!current_status.has_value()) {
    // Account is not in prefs yet - add a new entry.
    AddAccountToPrefs(prefs_, account.key.id(), is_available);

    // Notify observers only if account should be available.
    if (is_available)
      NotifyObservers(account, is_available);

    return;
  }

  if (current_status.value() == is_available)
    return;

  UpdateAccountInPrefs(prefs_, account.key.id(), is_available);
  NotifyObservers(account, is_available);
}

void AccountAppsAvailability::GetAccountsAvailableInArc(
    base::OnceCallback<void(const base::flat_set<account_manager::Account>&)>
        callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!IsInitialized()) {
    // Using base::Unretained(this) is fine because `initialization_callbacks_`
    // is owned by this.
    initialization_callbacks_.push_back(
        base::BindOnce(&AccountAppsAvailability::GetAccountsAvailableInArc,
                       base::Unretained(this), std::move(callback)));
    return;
  }

  account_manager_facade_->GetAccounts(
      base::BindOnce(&CompleteGetAccountsAvailableInArc,
                     GetGaiaIdsAvailableInArc(prefs_), std::move(callback)));
}

void AccountAppsAvailability::Shutdown() {
  identity_manager_observation_.Reset();
  account_manager_facade_observation_.Reset();
}

void AccountAppsAvailability::OnRefreshTokenUpdatedForAccount(
    const CoreAccountInfo& account_info) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!IsInitialized()) {
    // Using base::Unretained(this) is fine because `initialization_callbacks_`
    // is owned by this.
    initialization_callbacks_.push_back(base::BindOnce(
        &AccountAppsAvailability::OnRefreshTokenUpdatedForAccount,
        base::Unretained(this), account_info));
    return;
  }

  std::optional<bool> current_status =
      IsAccountAvailableInArc(prefs_, account_info.gaia);
  // - If `current_status.has_value()` is `false` - this account is not in prefs
  // yet. This happens when account is just added and
  // `SetIsAccountAvailableInArc()` wasn't called yet.
  // - If `current_status.value()` is `false` - this account is not available in
  // ARC. In this case we don't want to notify the observers.
  if (!current_status.has_value() || !current_status.value())
    return;

  FindAccountByGaiaId(
      account_info.gaia,
      base::BindOnce(&AccountAppsAvailability::MaybeNotifyObservers,
                     weak_factory_.GetWeakPtr(),
                     /*is_available_in_arc=*/true));
}

void AccountAppsAvailability::OnAccountUpserted(
    const account_manager::Account& account) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (IsInitialized())
    return;

  // Initialize the prefs list:
  account_manager_facade_->GetAccounts(
      base::BindOnce(&AccountAppsAvailability::InitAccountsAvailableInArcPref,
                     weak_factory_.GetWeakPtr()));
}

void AccountAppsAvailability::OnAccountRemoved(
    const account_manager::Account& account) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (account.key.account_type() != account_manager::AccountType::kGaia)
    return;

  if (!IsInitialized()) {
    // Using base::Unretained(this) is fine because `initialization_callbacks_`
    // is owned by this.
    initialization_callbacks_.push_back(
        base::BindOnce(&AccountAppsAvailability::OnAccountRemoved,
                       base::Unretained(this), account));
    return;
  }

  std::optional<bool> current_status =
      IsAccountAvailableInArc(prefs_, account.key.id());
  RemoveAccountFromPrefs(prefs_, account.key.id());
  if (!current_status.has_value() || !current_status.value())
    return;

  NotifyObservers(account, /*is_available_in_arc=*/false);
}

void AccountAppsAvailability::OnAuthErrorChanged(
    const account_manager::AccountKey& account,
    const GoogleServiceAuthError& error) {
  // Nothing to do.
}

bool AccountAppsAvailability::IsInitialized() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return is_initialized_;
}

void AccountAppsAvailability::InitAccountsAvailableInArcPref(
    const std::vector<account_manager::Account>& accounts) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (IsInitialized())
    return;

  // If there are no accounts in Account Manager at the moment,
  // `OnAccountUpserted` will be called when the primary account is added.
  if (accounts.size() == 0)
    return;

  prefs_->Set(account_manager::prefs::kAccountAppsAvailability,
              base::Value(base::Value::Type::DICT));

  ScopedDictPrefUpdate update(prefs_,
                              account_manager::prefs::kAccountAppsAvailability);
  DCHECK(update->empty());

  // See structure of `update` dictionary at the top of the file.
  for (const auto& account : accounts) {
    if (account.key.account_type() != account_manager::AccountType::kGaia)
      continue;

    base::Value::Dict account_entry;
    account_entry.Set(account_manager::prefs::kIsAvailableInArcKey, true);

    // Key: `account.key.id()` = Gaia ID
    // Value: { "is_available_in_arc": true }
    update->Set(account.key.id(), std::move(account_entry));
  }

  // User type cannot be active directory, so we expect to have at least
  // primary account in the list.
  DCHECK(!update->empty());

  is_initialized_ = true;

  for (auto& callback : initialization_callbacks_)
    std::move(callback).Run();
  initialization_callbacks_.clear();
}

void AccountAppsAvailability::ReportMetrics(
    const std::vector<account_manager::Account>& accounts) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  const int num_total_accounts = accounts.size();
  const int num_arc_accounts = GetGaiaIdsAvailableInArc(prefs_).size();
  base::UmaHistogramExactLinear(kNumAccountsInArcMetricName, num_arc_accounts,
                                kMaxNumAccountsInArcMetric + 1);

  DCHECK_GE(num_total_accounts, num_arc_accounts);
  const int percent_arc_accounts =
      (num_arc_accounts * 100.0) / num_total_accounts;
  base::UmaHistogramPercentage(kPercentAccountsInArcMetricName,
                               percent_arc_accounts);
}

void AccountAppsAvailability::FindAccountByGaiaId(
    const std::string& gaia_id,
    base::OnceCallback<void(const std::optional<account_manager::Account>&)>
        callback) {
  account_manager_facade_->GetAccounts(base::BindOnce(
      &CompleteFindAccountByGaiaId, gaia_id, std::move(callback)));
}

void AccountAppsAvailability::MaybeNotifyObservers(
    bool is_available_in_arc,
    const std::optional<account_manager::Account>& account) {
  if (!account)
    return;

  NotifyObservers(account.value(), is_available_in_arc);
}

void AccountAppsAvailability::NotifyObservers(
    const account_manager::Account& account,
    bool is_available_in_arc) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (is_available_in_arc) {
    for (auto& observer : observer_list_) {
      observer.OnAccountAvailableInArc(account);
    }
    return;
  }

  for (auto& observer : observer_list_) {
    observer.OnAccountUnavailableInArc(account);
  }
}

}  // namespace ash