| // 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); |
| DCHECK(success); |
| } |
| |
| 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(); |
| } |
| |
| // 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 |