chromium/chromeos/ash/services/libassistant/service_controller.cc

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

#include "chromeos/ash/services/libassistant/service_controller.h"

#include <memory>

#include "base/check.h"
#include "base/functional/bind.h"
#include "chromeos/ash/services/assistant/public/cpp/features.h"
#include "chromeos/ash/services/libassistant/chromium_api_delegate.h"
#include "chromeos/ash/services/libassistant/grpc/assistant_client.h"
#include "chromeos/ash/services/libassistant/libassistant_factory.h"
#include "chromeos/ash/services/libassistant/settings_controller.h"
#include "chromeos/ash/services/libassistant/util.h"
#include "chromeos/assistant/internal/internal_util.h"
#include "chromeos/assistant/internal/libassistant/shared_headers.h"
#include "services/network/public/cpp/cross_thread_pending_shared_url_loader_factory.h"
#include "services/network/public/cpp/wrapper_shared_url_loader_factory.h"

namespace ash::libassistant {

namespace {

using mojom::ServiceState;

// A macro which ensures we are running on the mojom thread.
#define ENSURE_MOJOM_THREAD(method, ...)                                    \
  if (!mojom_task_runner_->RunsTasksInCurrentSequence()) {                  \
    mojom_task_runner_->PostTask(                                           \
        FROM_HERE,                                                          \
        base::BindOnce(method, weak_factory_.GetWeakPtr(), ##__VA_ARGS__)); \
    return;                                                                 \
  }

BASE_FEATURE(kChromeOSAssistantDogfood,
             "ChromeOSAssistantDogfood",
             base::FEATURE_DISABLED_BY_DEFAULT);

constexpr char kServersideDogfoodExperimentId[] = "20347368";
constexpr char kServersideOpenAppExperimentId[] = "39651593";
constexpr char kServersideResponseProcessingV2ExperimentId[] = "1793869";

std::string ToLibassistantConfig(const mojom::BootupConfig& bootup_config) {
  return CreateLibAssistantConfig(bootup_config.s3_server_uri_override,
                                  bootup_config.device_id_override);
}

std::unique_ptr<network::PendingSharedURLLoaderFactory>
CreatePendingURLLoaderFactory(
    mojo::PendingRemote<network::mojom::URLLoaderFactory>
        url_loader_factory_remote) {
  // First create a wrapped factory that can accept the pending remote.
  auto pending_url_loader_factory =
      std::make_unique<network::WrapperPendingSharedURLLoaderFactory>(
          std::move(url_loader_factory_remote));
  auto wrapped_factory = network::SharedURLLoaderFactory::Create(
      std::move(pending_url_loader_factory));

  // Then move it into a cross thread factory, as the url loader factory will be
  // used from internal Libassistant threads.
  return std::make_unique<network::CrossThreadPendingSharedURLLoaderFactory>(
      std::move(wrapped_factory));
}

void FillServerExperimentIds(std::vector<std::string>* server_experiment_ids) {
  if (base::FeatureList::IsEnabled(kChromeOSAssistantDogfood)) {
    server_experiment_ids->emplace_back(kServersideDogfoodExperimentId);
  }

  if (base::FeatureList::IsEnabled(assistant::features::kAssistantAppSupport))
    server_experiment_ids->emplace_back(kServersideOpenAppExperimentId);

  server_experiment_ids->emplace_back(
      kServersideResponseProcessingV2ExperimentId);
}

void SetServerExperiments(AssistantClient* assistant_client) {
  std::vector<std::string> server_experiment_ids;
  FillServerExperimentIds(&server_experiment_ids);

  if (server_experiment_ids.size() > 0) {
    assistant_client->AddExperimentIds(server_experiment_ids);
  }
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////
//  ServiceController
////////////////////////////////////////////////////////////////////////////////

ServiceController::ServiceController(LibassistantFactory* factory)
    : libassistant_factory_(*factory) {
  DCHECK(factory);
}

ServiceController::~ServiceController() {
  // Ensure all our observers know this service is no longer running.
  // This will be a noop if we're already stopped.
  Stop();
}

void ServiceController::Bind(
    mojo::PendingReceiver<mojom::ServiceController> receiver,
    mojom::SettingsController* settings_controller) {
  DCHECK(!receiver_.is_bound());
  receiver_.Bind(std::move(receiver));
  settings_controller_ = settings_controller;
}

void ServiceController::Initialize(
    mojom::BootupConfigPtr config,
    mojo::PendingRemote<network::mojom::URLLoaderFactory> url_loader_factory) {
  if (assistant_client_) {
    LOG(ERROR) << "Initialize() should only be called once.";
    return;
  }

  auto assistant_manager = libassistant_factory_->CreateAssistantManager(
      ToLibassistantConfig(*config));
  assistant_client_ = AssistantClient::Create(std::move(assistant_manager));

  DCHECK(settings_controller_);
  settings_controller_->SetAuthenticationTokens(
      std::move(config->authentication_tokens));
  settings_controller_->SetLocale(config->locale);
  settings_controller_->SetHotwordEnabled(config->hotword_enabled);
  settings_controller_->SetSpokenFeedbackEnabled(
      config->spoken_feedback_enabled);
  settings_controller_->SetDarkModeEnabled(config->dark_mode_enabled);

  CreateAndRegisterChromiumApiDelegate(std::move(url_loader_factory));
  for (auto& observer : assistant_client_observers_) {
    observer.OnAssistantClientCreated(assistant_client_.get());
  }
}

void ServiceController::Start() {
  if (state_ != ServiceState::kStopped)
    return;

  DCHECK(IsInitialized()) << "Initialize() must be called before Start()";
  DVLOG(1) << "Starting Libassistant service";

  // |this| will outlive |assistant_client_|.
  assistant_client_->StartServices(/*services_status_observer=*/this);
}

void ServiceController::Stop() {
  if (state_ == ServiceState::kStopped)
    return;

  DVLOG(1) << "Stopping Libassistant service";

  for (auto& observer : assistant_client_observers_) {
    observer.OnDestroyingAssistantClient(assistant_client_.get());
  }

  assistant_client_ = nullptr;
  chromium_api_delegate_ = nullptr;

  DVLOG(1) << "Stopped Libassistant service";
  SetStateAndInformObservers(ServiceState::kStopped);

  for (auto& observer : assistant_client_observers_)
    observer.OnAssistantClientDestroyed();
}

void ServiceController::ResetAllDataAndStop() {
  if (assistant_client_) {
    DVLOG(1) << "Resetting all Libassistant data";
    assistant_client_->ResetAllDataAndShutdown();
  }
  Stop();
}

void ServiceController::AddAndFireStateObserver(
    mojo::PendingRemote<mojom::StateObserver> pending_observer) {
  mojo::Remote<mojom::StateObserver> observer(std::move(pending_observer));

  observer->OnStateChanged(state_);

  state_observers_.Add(std::move(observer));
}

void ServiceController::OnServicesStatusChanged(ServicesStatus status) {
  switch (status) {
    case ServicesStatus::ONLINE_ALL_SERVICES_AVAILABLE:
      OnAllServicesReady();
      break;
    case ServicesStatus::ONLINE_BOOTING_UP:
      // Configing internal options or other essential services that are
      // supported during bootup stage should happen here.
      OnServicesBootingUp();
      break;
    case ServicesStatus::OFFLINE:
      // No action needed.
      break;
  }
}

void ServiceController::AddAndFireAssistantClientObserver(
    AssistantClientObserver* observer) {
  DCHECK(observer);

  assistant_client_observers_.AddObserver(observer);

  if (IsInitialized()) {
    observer->OnAssistantClientCreated(assistant_client_.get());
  }
  // Note we do send the |OnAssistantClientStarted| event even if the service
  // is currently running, to ensure that an observer that only observes
  // |OnAssistantClientStarted| will not miss a currently running instance
  // when it is being added.
  if (IsStarted()) {
    observer->OnAssistantClientStarted(assistant_client_.get());
  }
  if (IsRunning()) {
    observer->OnAssistantClientRunning(assistant_client_.get());
  }
}

void ServiceController::RemoveAssistantClientObserver(
    AssistantClientObserver* observer) {
  assistant_client_observers_.RemoveObserver(observer);
}

void ServiceController::RemoveAllAssistantClientObservers() {
  assistant_client_observers_.Clear();
}

bool ServiceController::IsStarted() const {
  switch (state_) {
    case ServiceState::kStopped:
    case ServiceState::kDisconnected:
      return false;
    case ServiceState::kStarted:
    case ServiceState::kRunning:
      return true;
  }
}

bool ServiceController::IsInitialized() const {
  return assistant_client_ != nullptr;
}

bool ServiceController::IsRunning() const {
  switch (state_) {
    case ServiceState::kStopped:
    case ServiceState::kStarted:
    case ServiceState::kDisconnected:
      return false;
    case ServiceState::kRunning:
      return true;
  }
}

AssistantClient* ServiceController::assistant_client() {
  return assistant_client_.get();
}

void ServiceController::OnAllServicesReady() {
  DVLOG(1) << "Libassistant services are ready.";

  SetServerExperiments(assistant_client_.get());

  // Notify observers on Libassistant services ready.
  SetStateAndInformObservers(mojom::ServiceState::kRunning);

  for (auto& observer : assistant_client_observers_)
    observer.OnAssistantClientRunning(assistant_client_.get());
}

void ServiceController::OnServicesBootingUp() {
  DVLOG(1) << "Started Libassistant service";

  // We set one precondition of BootupState to reach `INITIALIZING_INTERNAL`
  // is to wait for the gRPC HttpConnection be ready. Only after the BootupState
  // meets the state, can AssistantManager start.
  assistant_client_->StartGrpcHttpConnectionClient(
      chromium_api_delegate_->GetHttpConnectionFactory());

  // The Libassistant BootupState goes to `RUNNING` right after
  // `SETTING_UP_ESSENTIAL_SERVICES` if AssistantManager::Start() is called
  // right after the AssistantManager is created. And Libassistant emits signals
  // of `ESSENTIAL_SERVICES_AVAILABLE` and `ALL_SERVICES_AVAILABLE` almost the
  // same time. However, unary gRPC does not guarantee order. ChromeOS could
  // receive these signals out of order.
  // We call AssistantManager::Start() here, ServicesBootingUp(), which is
  // triggered by `ESSENTIAL_SERVICES_AVAILABLE`. After the AssistantManager is
  // started, it will trigger `ALL_SERVICES_AVAILABLE`. Therefore these two
  // signals are generated in order.
  assistant_client_->assistant_manager()->Start();

  // Notify observer on Libassistant services started.
  SetStateAndInformObservers(ServiceState::kStarted);

  for (auto& observer : assistant_client_observers_)
    observer.OnAssistantClientStarted(assistant_client_.get());
}

void ServiceController::SetStateAndInformObservers(
    mojom::ServiceState new_state) {
  DCHECK_NE(state_, new_state);

  state_ = new_state;

  for (auto& observer : state_observers_)
    observer->OnStateChanged(state_);
}

void ServiceController::CreateAndRegisterChromiumApiDelegate(
    mojo::PendingRemote<network::mojom::URLLoaderFactory>
        url_loader_factory_remote) {
  CreateChromiumApiDelegate(std::move(url_loader_factory_remote));
}

void ServiceController::CreateChromiumApiDelegate(
    mojo::PendingRemote<network::mojom::URLLoaderFactory>
        url_loader_factory_remote) {
  DCHECK(!chromium_api_delegate_);

  chromium_api_delegate_ = std::make_unique<ChromiumApiDelegate>(
      CreatePendingURLLoaderFactory(std::move(url_loader_factory_remote)));
}

}  // namespace ash::libassistant