chromium/ash/system/federated/federated_service_controller_impl.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/system/federated/federated_service_controller_impl.h"

#include <string>

#include "ash/constants/ash_features.h"
#include "ash/login_status.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "base/containers/fixed_flat_map.h"
#include "base/containers/flat_map.h"
#include "base/feature_list.h"
#include "base/i18n/timezone.h"
#include "base/memory/raw_ptr.h"
#include "base/no_destructor.h"
#include "chromeos/ash/services/federated/public/cpp/federated_example_util.h"
#include "chromeos/ash/services/federated/public/cpp/service_connection.h"
#include "chromeos/ash/services/federated/public/mojom/federated_service.mojom.h"
#include "chromeos/ash/services/federated/public/mojom/tables.mojom.h"
#include "components/user_manager/user_type.h"

namespace ash::federated {
namespace {

using chromeos::federated::mojom::ClientScheduleConfig;
using chromeos::federated::mojom::ClientScheduleConfigPtr;
using chromeos::federated::mojom::FederatedExampleTableId;

// Local client config struct that can be converted to mojom
// ClientScheduleConfig.
struct LocalClientConfig {
  // A client uses its client_name and launch stage to make up the task group
  // identifier when checking in with the server.
  std::string_view client_name;
  // The table_id is defined in
  // chromeos/ash/services/federated/public/mojom/tables.mojom
  FederatedExampleTableId table_id;
  // The associated_feature is usually defined in
  // ash/constants/ash_features.h/cc
  raw_ptr<const base::Feature> associated_feature;
  // The hardcoded_stage is set when a client is fully launched and no longer
  // needs to be changed, in sucn cases the associated_featur should be nullptr,
  // e.g.
  // {"foo_client_name", FederatedExampleTableId::BAR_TABLE, nullptr, "prod"}
  std::optional<std::string_view> hardcoded_stage;
};

// Each federated client should have an entry in `kLocalClientConfigs` that
// contains its client name, example table id and a feature with launch stage as
// the associated parameter, or a hardcoded launch stage.
const std::array<LocalClientConfig, 4> kLocalClientConfigs = {{
    {"input_autocorrect_phh", FederatedExampleTableId::INPUT_AUTOCORRECT,
     &features::kAutocorrectFederatedPhh},
    {"launcher_query_analytics_v1", FederatedExampleTableId::LAUNCHER_QUERY,
     &features::kFederatedLauncherQueryAnalyticsTask},
    {"launcher_query_analytics_v2", FederatedExampleTableId::LAUNCHER_QUERY_V2,
     &features::kFederatedLauncherQueryAnalyticsVersion2Task},
    {"timezone_code_phh", FederatedExampleTableId::TIMEZONE_CODE,
     &features::kFederatedTimezoneCodePhh},
}};

// Converts a LocalClientConfig to mojom ClientScheduleConfigPtr that can be
// used for scheduling tasks via mojom interface.
// The major logic here is to obtain a valid launch stage. If a client's
// associated_feature is not null, tries to get the launch_stage from the
// feature's parameter. Otherwise tries to use the hardcoded stage. If
// eventually no valid launch_stage, the client is ignored.
std::optional<ClientScheduleConfigPtr> ConvertLocalConfigToSchedulingConfig(
    const LocalClientConfig& local_config) {
  std::string launch_stage;
  if (local_config.associated_feature != nullptr) {
    base::FeatureParam<std::string> launch_stage_param{
        local_config.associated_feature, "launch_stage", ""};
    launch_stage = launch_stage_param.Get();
  } else if (local_config.hardcoded_stage.has_value()) {
    launch_stage = local_config.hardcoded_stage.value();
  }

  if (launch_stage.empty()) {
    DVLOG(1) << "client " << local_config.client_name
             << " has no valid launch stage, ignored.";
    return std::nullopt;
  }
  auto schedule_config = ClientScheduleConfig::New();
  schedule_config->client_name = local_config.client_name;
  schedule_config->example_storage_table_id = local_config.table_id;
  schedule_config->launch_stage = launch_stage;

  return schedule_config;
}

// Prepare client configs for scheduling tasks.
std::vector<ClientScheduleConfigPtr> PrepareClientScheduleConfigs() {
  std::vector<ClientScheduleConfigPtr> client_schedule_configs;
  for (const auto& local_config : kLocalClientConfigs) {
    auto maybe_schedule_config =
        ConvertLocalConfigToSchedulingConfig(local_config);
    if (maybe_schedule_config.has_value()) {
      client_schedule_configs.push_back(
          std::move(maybe_schedule_config.value()));
    }
  }

  return client_schedule_configs;
}

chromeos::federated::mojom::ExamplePtr CreateBrellaAnalyticsExamplePtr() {
  auto example = chromeos::federated::mojom::Example::New();
  example->features = chromeos::federated::mojom::Features::New();
  auto& feature_map = example->features->feature;
  feature_map["timezone_code"] =
      federated::CreateStringList({base::CountryCodeForCurrentTimezone()});
  return example;
}

// Returns whether federated can run for this type of logged-in user.
bool IsValidPrimaryUserType(const user_manager::UserType user_type) {
  // Primary user session must have user_type = regular or child (v.s. guest,
  // public account, kiosk app).
  return user_type == user_manager::UserType::kRegular ||
         user_type == user_manager::UserType::kChild;
}

}  // namespace

FederatedServiceControllerImpl::FederatedServiceControllerImpl() {
  SessionControllerImpl* session_controller =
      Shell::Get()->session_controller();
  DCHECK(session_controller);
  session_observation_.Observe(session_controller);
}

FederatedServiceControllerImpl::~FederatedServiceControllerImpl() = default;

void FederatedServiceControllerImpl::OnLoginStatusChanged(
    LoginStatus login_status) {
  // Federated service daemon uses cryptohome as example store and we only
  // treat it available when a proper primary user type has signed in.

  // Actually once `federated_service_` gets bound, even if availability is
  // set false because of subsequent LoginStatus changes, it keeps bound and
  // it's safe to call `federated_service_->ReportExampleToTable()`. But on the
  // ChromeOS daemon side it loses a valid cryptohome hence no valid example
  // storage, all reported examples are abandoned.

  auto* primary_user_session =
      Shell::Get()->session_controller()->GetPrimaryUserSession();

  service_available_ =
      primary_user_session != nullptr &&
      IsValidPrimaryUserType(primary_user_session->user_info.type);

  if (service_available_ && !federated_service_.is_bound()) {
    federated::ServiceConnection::GetInstance()->BindReceiver(
        federated_service_.BindNewPipeAndPassReceiver());

    if (features::IsFederatedServiceScheduleTasksEnabled()) {
      federated_service_->StartSchedulingWithConfig(
          PrepareClientScheduleConfigs());
    }

    // On session first login, reports one example for "timezone_code_phh", a
    // trivial F.A. task for prove-out purpose.
    if (!reported_) {
      federated_service_->ReportExampleToTable(
          FederatedExampleTableId::TIMEZONE_CODE,
          CreateBrellaAnalyticsExamplePtr());
      reported_ = true;
    }
  }
}

bool FederatedServiceControllerImpl::IsServiceAvailable() const {
  return service_available_;
}

}  // namespace ash::federated