// Copyright 2023 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/sync/sync_appsync_optin_client.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "components/account_id/account_id.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/sync/base/user_selectable_type.h"
#include "components/sync/service/sync_service.h"
#include "components/sync/service/sync_user_settings.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
namespace ash {
constexpr char kOldDaemonStorePath[] = "/run/daemon-store/appsync-consent";
constexpr char kDaemonStorePath[] = "/run/daemon-store/appsync-optin";
constexpr char kDaemonStoreFileName[] = "opted-in";
namespace {
bool IsAppsSyncEnabledForSyncService(const syncer::SyncService& sync_service) {
return sync_service.GetUserSettings()->GetSelectedOsTypes().Has(
syncer::UserSelectableOsType::kOsApps);
}
void WriteOptinFile(base::FilePath filepath, bool opted_in) {
const std::string file_contents = opted_in ? "1" : "0";
if (!base::WriteFile(filepath, file_contents)) {
DLOG(ERROR) << "Failed to persist opt-in change " << file_contents
<< " to daemon-store. State on disk will be inaccurate!";
}
}
void DeleteConsentDir(const base::FilePath& app_sync_consent_dir) {
if (!base::DirectoryExists(app_sync_consent_dir)) {
// defunct daemon-store directory does not exist, no need to migrate
return;
}
if (!base::DeletePathRecursively(app_sync_consent_dir)) {
DLOG(WARNING) << "Failed to delete " << app_sync_consent_dir;
}
}
} // namespace
std::string SyncAppsyncOptinClient::GetActiveProfileHash(
const syncer::SyncService* sync_service) {
CoreAccountInfo sync_user_account = sync_service->GetAccountInfo();
if (sync_user_account.IsEmpty()) {
DLOG(WARNING) << "No user associated with current SyncService, will not be "
"able to write opt-in file!";
return "";
}
AccountId account_id = AccountId::FromNonCanonicalEmail(
sync_user_account.email, sync_user_account.gaia, AccountType::GOOGLE);
const user_manager::User* user = user_manager_->FindUser(account_id);
if (!user) {
DLOG(WARNING) << "Unable to load user for current SyncService, will not be "
"able to write opt-in file!";
return "";
}
return user->username_hash();
}
void SyncAppsyncOptinClient::UpdateOptinFile(
bool opted_in,
const syncer::SyncService* sync_service) {
std::string hash = GetActiveProfileHash(sync_service);
if (hash.empty()) {
return;
}
base::FilePath app_sync_optin_path =
daemon_store_filepath_.Append(hash).Append(kDaemonStoreFileName);
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&WriteOptinFile, app_sync_optin_path, opted_in));
}
void SyncAppsyncOptinClient::RemoveOldAppsyncDaemonDir(
const syncer::SyncService* sync_service) {
std::string hash = GetActiveProfileHash(sync_service);
if (hash.empty()) {
return;
}
base::FilePath app_sync_consent_dir = old_daemon_store_filepath_.Append(hash);
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&DeleteConsentDir, app_sync_consent_dir));
}
SyncAppsyncOptinClient::SyncAppsyncOptinClient(
syncer::SyncService* sync_service,
user_manager::UserManager* user_manager)
: SyncAppsyncOptinClient(sync_service,
user_manager,
base::FilePath(kDaemonStorePath),
base::FilePath(kOldDaemonStorePath)) {}
SyncAppsyncOptinClient::SyncAppsyncOptinClient(
syncer::SyncService* sync_service,
user_manager::UserManager* user_manager,
const base::FilePath& daemon_store_filepath)
: SyncAppsyncOptinClient(sync_service,
user_manager,
daemon_store_filepath,
base::FilePath(kDaemonStorePath)) {}
SyncAppsyncOptinClient::SyncAppsyncOptinClient(
syncer::SyncService* sync_service,
user_manager::UserManager* user_manager,
const base::FilePath& daemon_store_filepath,
const base::FilePath& old_daemon_store_filepath)
: sync_service_(sync_service),
user_manager_(user_manager),
is_apps_sync_enabled_(IsAppsSyncEnabledForSyncService(*sync_service)),
daemon_store_filepath_(daemon_store_filepath),
old_daemon_store_filepath_(old_daemon_store_filepath) {
sync_service_->AddObserver(this);
// When SyncAppsyncOptinClient is instantiated, it attempts to do 2 things:
// 1 - delete any existing directory at a legacy location
// 2 - create a file indicating a user's opt-in status to Apps Sync
// Either of these may safely fail, as they will be reattempted in the future,
// and the ordering of events does not matter as they interact with 2
// different directories.
// TODO(b/264677999): remove migration code on 2024-01-30.
RemoveOldAppsyncDaemonDir(sync_service);
UpdateOptinFile(is_apps_sync_enabled_, sync_service);
}
SyncAppsyncOptinClient::~SyncAppsyncOptinClient() {
sync_service_->RemoveObserver(this);
}
void SyncAppsyncOptinClient::OnStateChanged(syncer::SyncService* sync_service) {
bool new_is_apps_sync_enabled =
IsAppsSyncEnabledForSyncService(*sync_service_);
// Don't update file if we have a non-relevant state change reporter.
if (new_is_apps_sync_enabled != is_apps_sync_enabled_) {
UpdateOptinFile(new_is_apps_sync_enabled, sync_service);
is_apps_sync_enabled_ = new_is_apps_sync_enabled;
}
}
} // namespace ash