chromium/chrome/browser/ash/sync/sync_appsync_optin_client.cc

// 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