chromium/extensions/browser/api/media_perception_private/media_perception_api_manager.cc

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

#include "extensions/browser/api/media_perception_private/media_perception_api_manager.h"

#include <memory>
#include <utility>
#include <vector>

#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/lazy_instance.h"
#include "base/memory/raw_ptr.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "chromeos/ash/components/dbus/dbus_thread_manager.h"
#include "chromeos/ash/components/dbus/upstart/upstart_client.h"
#include "extensions/browser/api/extensions_api_client.h"
#include "extensions/browser/api/media_perception_private/conversion_utils.h"
#include "extensions/browser/api/media_perception_private/media_perception_api_delegate.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_function.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/platform/platform_channel.h"
#include "mojo/public/cpp/system/invitation.h"

namespace extensions {

namespace {

const int kStartupDelayMs = 1000;

extensions::api::media_perception_private::State GetStateForServiceError(
    const extensions::api::media_perception_private::ServiceError
        service_error) {
  extensions::api::media_perception_private::State state;
  state.status =
      extensions::api::media_perception_private::Status::kServiceError;
  state.service_error = service_error;
  return state;
}

extensions::api::media_perception_private::ProcessState
GetProcessStateForServiceError(
    const extensions::api::media_perception_private::ServiceError
        service_error) {
  extensions::api::media_perception_private::ProcessState process_state;
  process_state.status =
      extensions::api::media_perception_private::ProcessStatus::kServiceError;
  process_state.service_error = service_error;
  return process_state;
}

extensions::api::media_perception_private::Diagnostics
GetDiagnosticsForServiceError(
    const extensions::api::media_perception_private::ServiceError&
        service_error) {
  extensions::api::media_perception_private::Diagnostics diagnostics;
  diagnostics.service_error = service_error;
  return diagnostics;
}

extensions::api::media_perception_private::ComponentState
GetFailedToInstallComponentState() {
  extensions::api::media_perception_private::ComponentState component_state;
  component_state.status = extensions::api::media_perception_private::
      ComponentStatus::kFailedToInstall;
  return component_state;
}

// Pulls out the version number from a mount_point location for the media
// perception component. Mount points look like
// /run/imageloader/rtanalytics-light/1.0, where 1.0 is the version string.
std::string ExtractVersionFromMountPoint(const std::string& mount_point) {
  return base::FilePath(mount_point).BaseName().value();
}

}  // namespace

class MediaPerceptionAPIManager::MediaPerceptionControllerClient
    : public chromeos::media_perception::mojom::
          MediaPerceptionControllerClient {
 public:
  // delegate is owned by the ExtensionsAPIClient.
  MediaPerceptionControllerClient(
      MediaPerceptionAPIDelegate* delegate,
      mojo::PendingReceiver<
          chromeos::media_perception::mojom::MediaPerceptionControllerClient>
          receiver)
      : delegate_(delegate), receiver_(this, std::move(receiver)) {
    DCHECK(delegate_) << "Delegate not set.";
  }

  MediaPerceptionControllerClient(const MediaPerceptionControllerClient&) =
      delete;
  MediaPerceptionControllerClient& operator=(
      const MediaPerceptionControllerClient&) = delete;

  ~MediaPerceptionControllerClient() override = default;

  // media_perception::mojom::MediaPerceptionControllerClient:
  void ConnectToVideoCaptureService(
      mojo::PendingReceiver<video_capture::mojom::VideoSourceProvider> receiver)
      override {
    DCHECK(delegate_) << "Delegate not set.";
    delegate_->BindVideoSourceProvider(std::move(receiver));
  }

 private:
  // Provides access to methods for talking to core Chrome code.
  raw_ptr<MediaPerceptionAPIDelegate, DanglingUntriaged> delegate_;

  // Receiver of the MediaPerceptionControllerClient to the message pipe.
  mojo::Receiver<
      chromeos::media_perception::mojom::MediaPerceptionControllerClient>
      receiver_;
};

// static
MediaPerceptionAPIManager* MediaPerceptionAPIManager::Get(
    content::BrowserContext* context) {
  return GetFactoryInstance()->Get(context);
}

static base::LazyInstance<
    BrowserContextKeyedAPIFactory<MediaPerceptionAPIManager>>::Leaky g_factory =
    LAZY_INSTANCE_INITIALIZER;

// static
BrowserContextKeyedAPIFactory<MediaPerceptionAPIManager>*
MediaPerceptionAPIManager::GetFactoryInstance() {
  return g_factory.Pointer();
}

MediaPerceptionAPIManager::MediaPerceptionAPIManager(
    content::BrowserContext* context)
    : browser_context_(context),
      analytics_process_state_(AnalyticsProcessState::IDLE) {
  // `MediaAnalyticsClient` can be null in tests (browser_tests or
  // extensions_browsertests).
  if (auto* client = ash::MediaAnalyticsClient::Get()) {
    scoped_observation_.Observe(client);
  }
}

MediaPerceptionAPIManager::~MediaPerceptionAPIManager() {
  // Stop the separate media analytics process.
  // `UpstartClient` can be null in tests (browser_tests or
  // extensions_browsertests).
  if (auto* client = ash::UpstartClient::Get()) {
    client->StopMediaAnalytics();
  }
}

void MediaPerceptionAPIManager::ActivateMediaPerception(
    mojo::PendingReceiver<chromeos::media_perception::mojom::MediaPerception>
        receiver) {
  if (media_perception_controller_.is_bound())
    media_perception_controller_->ActivateMediaPerception(std::move(receiver));
}

void MediaPerceptionAPIManager::SetMountPointNonEmptyForTesting() {
  mount_point_ = "non-empty-string";
}

void MediaPerceptionAPIManager::GetState(APIStateCallback callback) {
  if (analytics_process_state_ == AnalyticsProcessState::RUNNING) {
    ash::MediaAnalyticsClient::Get()->GetState(
        base::BindOnce(&MediaPerceptionAPIManager::StateCallback,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
    return;
  }

  if (analytics_process_state_ ==
      AnalyticsProcessState::CHANGING_PROCESS_STATE) {
    std::move(callback).Run(
        GetStateForServiceError(extensions::api::media_perception_private::
                                    ServiceError::kServiceBusyLaunching));
    return;
  }

  // Calling getState with process not running returns State UNINITIALIZED.
  extensions::api::media_perception_private::State state_uninitialized;
  state_uninitialized.status =
      extensions::api::media_perception_private::Status::kUninitialized;
  std::move(callback).Run(std::move(state_uninitialized));
}

void MediaPerceptionAPIManager::SetAnalyticsComponent(
    const extensions::api::media_perception_private::Component& component,
    APISetAnalyticsComponentCallback callback) {
  MediaPerceptionAPIDelegate* delegate =
      ExtensionsAPIClient::Get()->GetMediaPerceptionAPIDelegate();
  if (!delegate) {
    LOG(WARNING) << "Could not get MediaPerceptionAPIDelegate.";
    std::move(callback).Run(GetFailedToInstallComponentState());
    return;
  }

  delegate->LoadCrOSComponent(
      component.type,
      base::BindOnce(&MediaPerceptionAPIManager::LoadComponentCallback,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void MediaPerceptionAPIManager::LoadComponentCallback(
    APISetAnalyticsComponentCallback callback,
    const extensions::api::media_perception_private::ComponentInstallationError
        installation_error,
    const base::FilePath& mount_point) {
  if (installation_error != extensions::api::media_perception_private::
                                ComponentInstallationError::kNone) {
    extensions::api::media_perception_private::ComponentState component_state =
        GetFailedToInstallComponentState();
    component_state.installation_error_code = installation_error;
    std::move(callback).Run(std::move(component_state));
    return;
  }

  // If the new component is loaded, override the mount point.
  mount_point_ = mount_point.value();

  extensions::api::media_perception_private::ComponentState component_state;
  component_state.status =
      extensions::api::media_perception_private::ComponentStatus::kInstalled;
  component_state.version = ExtractVersionFromMountPoint(mount_point_);
  std::move(callback).Run(std::move(component_state));
  return;
}

void MediaPerceptionAPIManager::SetComponentProcessState(
    const extensions::api::media_perception_private::ProcessState&
        process_state,
    APIComponentProcessStateCallback callback) {
  DCHECK(
      process_state.status ==
          extensions::api::media_perception_private::ProcessStatus::kStarted ||
      process_state.status ==
          extensions::api::media_perception_private::ProcessStatus::kStopped);
  if (analytics_process_state_ ==
      AnalyticsProcessState::CHANGING_PROCESS_STATE) {
    std::move(callback).Run(GetProcessStateForServiceError(
        extensions::api::media_perception_private::ServiceError::
            kServiceBusyLaunching));
    return;
  }

  analytics_process_state_ = AnalyticsProcessState::CHANGING_PROCESS_STATE;
  if (process_state.status ==
      extensions::api::media_perception_private::ProcessStatus::kStopped) {
    base::OnceCallback<void(bool)> stop_callback =
        base::BindOnce(&MediaPerceptionAPIManager::UpstartStopProcessCallback,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback));
    ash::UpstartClient::Get()->StopMediaAnalytics(std::move(stop_callback));
    return;
  }

  if (process_state.status ==
      extensions::api::media_perception_private::ProcessStatus::kStarted) {
    // Check if a component is loaded and add the necessary mount_point
    // information to the Upstart start command.
    if (mount_point_.empty()) {
      analytics_process_state_ = AnalyticsProcessState::IDLE;
      std::move(callback).Run(GetProcessStateForServiceError(
          extensions::api::media_perception_private::ServiceError::
              kServiceNotInstalled));
      return;
    }

    std::vector<std::string> upstart_env;
    upstart_env.push_back(std::string("mount_point=") + mount_point_);

    ash::UpstartClient::Get()->StartMediaAnalytics(
        upstart_env,
        base::BindOnce(&MediaPerceptionAPIManager::UpstartStartProcessCallback,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
    return;
  }

  analytics_process_state_ = AnalyticsProcessState::IDLE;
  std::move(callback).Run(
      GetProcessStateForServiceError(extensions::api::media_perception_private::
                                         ServiceError::kServiceNotRunning));
}

void MediaPerceptionAPIManager::SetState(
    const extensions::api::media_perception_private::State& state,
    APIStateCallback callback) {
  mri::State state_proto = StateIdlToProto(state);
  DCHECK(state_proto.status() == mri::State::RUNNING ||
         state_proto.status() == mri::State::SUSPENDED ||
         state_proto.status() == mri::State::RESTARTING ||
         state_proto.status() == mri::State::STOPPED)
      << "Cannot set state to something other than RUNNING, SUSPENDED "
         "RESTARTING, or STOPPED.";

  if (analytics_process_state_ ==
      AnalyticsProcessState::CHANGING_PROCESS_STATE) {
    std::move(callback).Run(
        GetStateForServiceError(extensions::api::media_perception_private::
                                    ServiceError::kServiceBusyLaunching));
    return;
  }

  // Regardless of the state of the media analytics process, always send an
  // upstart stop command if requested.
  if (state_proto.status() == mri::State::STOPPED) {
    analytics_process_state_ = AnalyticsProcessState::CHANGING_PROCESS_STATE;
    ash::UpstartClient::Get()->StopMediaAnalytics(
        base::BindOnce(&MediaPerceptionAPIManager::UpstartStopCallback,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
    return;
  }

  // If the media analytics process is running or not and restart is requested,
  // then send restart upstart command.
  if (state_proto.status() == mri::State::RESTARTING) {
    analytics_process_state_ = AnalyticsProcessState::CHANGING_PROCESS_STATE;
    ash::UpstartClient::Get()->RestartMediaAnalytics(
        base::BindOnce(&MediaPerceptionAPIManager::UpstartRestartCallback,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
    return;
  }

  if (analytics_process_state_ == AnalyticsProcessState::RUNNING) {
    SetStateInternal(std::move(callback), state_proto);
    return;
  }

  // Analytics process is in state IDLE.
  if (state_proto.status() == mri::State::RUNNING) {
    analytics_process_state_ = AnalyticsProcessState::CHANGING_PROCESS_STATE;
    std::vector<std::string> upstart_env;
    // Check if a component is loaded and add the necessary mount_point
    // information to the Upstart start command. If no component is loaded,
    // StartMediaAnalytics will likely fail and the client will get an error
    // callback. StartMediaAnalytics is still called, however, in the case that
    // the old CrOS deployment path for the media analytics process is still in
    // use.
    // TODO(crbug.com/40552021): When the old deployment path is no longer in
    // use, only start media analytics if the mount point is set.
    if (!mount_point_.empty())
      upstart_env.push_back(std::string("mount_point=") + mount_point_);

    ash::UpstartClient::Get()->StartMediaAnalytics(
        upstart_env,
        base::BindOnce(&MediaPerceptionAPIManager::UpstartStartCallback,
                       weak_ptr_factory_.GetWeakPtr(), std::move(callback),
                       state_proto));
    return;
  }

  std::move(callback).Run(
      GetStateForServiceError(extensions::api::media_perception_private::
                                  ServiceError::kServiceNotRunning));
}

void MediaPerceptionAPIManager::SetStateInternal(APIStateCallback callback,
                                                 const mri::State& state) {
  ash::MediaAnalyticsClient::Get()->SetState(
      state,
      base::BindOnce(&MediaPerceptionAPIManager::StateCallback,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void MediaPerceptionAPIManager::GetDiagnostics(
    APIGetDiagnosticsCallback callback) {
  ash::MediaAnalyticsClient::Get()->GetDiagnostics(
      base::BindOnce(&MediaPerceptionAPIManager::GetDiagnosticsCallback,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void MediaPerceptionAPIManager::UpstartStartProcessCallback(
    APIComponentProcessStateCallback callback,
    bool succeeded) {
  if (!succeeded) {
    analytics_process_state_ = AnalyticsProcessState::IDLE;
    std::move(callback).Run(GetProcessStateForServiceError(
        extensions::api::media_perception_private::ServiceError::
            kServiceNotRunning));
    return;
  }

  // Check if the extensions api client is available in this context. Code path
  // used for testing.
  if (!ExtensionsAPIClient::Get()) {
    LOG(ERROR) << "Could not get ExtensionsAPIClient.";
    OnBootstrapMojoConnection(std::move(callback), true);
    return;
  }

  // TODO(crbug.com/40098825): Look into using
  // ObjectProxy::WaitForServiceToBeAvailable instead, since a timeout is
  // inherently not deterministic, even if it works in practice.
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&MediaPerceptionAPIManager::SendMojoInvitation,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)),
      base::Milliseconds(kStartupDelayMs));
}

void MediaPerceptionAPIManager::SendMojoInvitation(
    APIComponentProcessStateCallback callback) {
  MediaPerceptionAPIDelegate* delegate =
      ExtensionsAPIClient::Get()->GetMediaPerceptionAPIDelegate();
  if (!delegate) {
    DLOG(WARNING) << "Could not get MediaPerceptionAPIDelegate.";
    std::move(callback).Run(GetProcessStateForServiceError(
        extensions::api::media_perception_private::ServiceError::
            kMojoConnectionFailure));
    return;
  }

  mojo::OutgoingInvitation invitation;
  mojo::PlatformChannel channel;
  mojo::ScopedMessagePipeHandle server_pipe =
      invitation.AttachMessagePipe("mpp-connector-pipe");
  mojo::OutgoingInvitation::Send(std::move(invitation),
                                 base::kNullProcessHandle,
                                 channel.TakeLocalEndpoint());

  media_perception_service_.reset();
  media_perception_service_.Bind(
      mojo::PendingRemote<
          chromeos::media_perception::mojom::MediaPerceptionService>(
          std::move(server_pipe), 0));

  base::ScopedFD fd =
      channel.TakeRemoteEndpoint().TakePlatformHandle().TakeFD();
  ash::MediaAnalyticsClient::Get()->BootstrapMojoConnection(
      std::move(fd),
      base::BindOnce(&MediaPerceptionAPIManager::OnBootstrapMojoConnection,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void MediaPerceptionAPIManager::OnBootstrapMojoConnection(
    APIComponentProcessStateCallback callback,
    bool succeeded) {
  if (!succeeded) {
    analytics_process_state_ = AnalyticsProcessState::UNKNOWN;
    std::move(callback).Run(GetProcessStateForServiceError(
        extensions::api::media_perception_private::ServiceError::
            kMojoConnectionFailure));
    return;
  }

  analytics_process_state_ = AnalyticsProcessState::RUNNING;
  extensions::api::media_perception_private::ProcessState state_started;
  state_started.status =
      extensions::api::media_perception_private::ProcessStatus::kStarted;

  // Check if the extensions api client is available in this context. Code path
  // used for testing.
  if (!ExtensionsAPIClient::Get()) {
    DLOG(ERROR) << "Could not get ExtensionsAPIClient.";
    std::move(callback).Run(std::move(state_started));
    return;
  }

  MediaPerceptionAPIDelegate* delegate =
      ExtensionsAPIClient::Get()->GetMediaPerceptionAPIDelegate();
  if (!delegate) {
    DLOG(WARNING) << "Could not get MediaPerceptionAPIDelegate.";
    std::move(callback).Run(GetProcessStateForServiceError(
        extensions::api::media_perception_private::ServiceError::
            kMojoConnectionFailure));
    return;
  }

  if (!media_perception_service_.is_bound()) {
    DLOG(WARNING) << "MediaPerceptionService interface not bound.";
    std::move(callback).Run(GetProcessStateForServiceError(
        extensions::api::media_perception_private::ServiceError::
            kMojoConnectionFailure));
    return;
  }

  media_perception_controller_.reset();
  auto controller_receiver =
      media_perception_controller_.BindNewPipeAndPassReceiver();

  mojo::PendingRemote<
      chromeos::media_perception::mojom::MediaPerceptionControllerClient>
      client_remote;
  media_perception_controller_client_ =
      std::make_unique<MediaPerceptionControllerClient>(
          delegate, client_remote.InitWithNewPipeAndPassReceiver());
  delegate->SetMediaPerceptionRequestHandler(
      base::BindRepeating(&MediaPerceptionAPIManager::ActivateMediaPerception,
                          weak_ptr_factory_.GetWeakPtr()));

  media_perception_service_->GetController(std::move(controller_receiver),
                                           std::move(client_remote));
  std::move(callback).Run(std::move(state_started));
}

void MediaPerceptionAPIManager::UpstartStopProcessCallback(
    APIComponentProcessStateCallback callback,
    bool succeeded) {
  if (!succeeded) {
    analytics_process_state_ = AnalyticsProcessState::UNKNOWN;
    std::move(callback).Run(GetProcessStateForServiceError(
        extensions::api::media_perception_private::ServiceError::
            kServiceUnreachable));
    return;
  }
  analytics_process_state_ = AnalyticsProcessState::IDLE;
  // Stopping the process succeeded so fire a callback with status STOPPED.
  extensions::api::media_perception_private::ProcessState state_stopped;
  state_stopped.status =
      extensions::api::media_perception_private::ProcessStatus::kStopped;
  std::move(callback).Run(std::move(state_stopped));
}

void MediaPerceptionAPIManager::UpstartStartCallback(APIStateCallback callback,
                                                     const mri::State& state,
                                                     bool succeeded) {
  if (!succeeded) {
    analytics_process_state_ = AnalyticsProcessState::IDLE;
    std::move(callback).Run(
        GetStateForServiceError(extensions::api::media_perception_private::
                                    ServiceError::kServiceNotRunning));
    return;
  }
  analytics_process_state_ = AnalyticsProcessState::RUNNING;
  SetStateInternal(std::move(callback), state);
}

void MediaPerceptionAPIManager::UpstartStopCallback(APIStateCallback callback,
                                                    bool succeeded) {
  if (!succeeded) {
    analytics_process_state_ = AnalyticsProcessState::UNKNOWN;
    std::move(callback).Run(
        GetStateForServiceError(extensions::api::media_perception_private::
                                    ServiceError::kServiceUnreachable));
    return;
  }
  analytics_process_state_ = AnalyticsProcessState::IDLE;
  // Stopping the process succeeded so fire a callback with status STOPPED.
  extensions::api::media_perception_private::State state_stopped;
  state_stopped.status =
      extensions::api::media_perception_private::Status::kStopped;
  std::move(callback).Run(std::move(state_stopped));
}

void MediaPerceptionAPIManager::UpstartRestartCallback(
    APIStateCallback callback,
    bool succeeded) {
  if (!succeeded) {
    analytics_process_state_ = AnalyticsProcessState::IDLE;
    std::move(callback).Run(
        GetStateForServiceError(extensions::api::media_perception_private::
                                    ServiceError::kServiceNotRunning));
    return;
  }
  analytics_process_state_ = AnalyticsProcessState::RUNNING;
  GetState(std::move(callback));
}

void MediaPerceptionAPIManager::StateCallback(
    APIStateCallback callback,
    std::optional<mri::State> result) {
  if (!result.has_value()) {
    std::move(callback).Run(
        GetStateForServiceError(extensions::api::media_perception_private::
                                    ServiceError::kServiceUnreachable));
    return;
  }
  std::move(callback).Run(
      extensions::api::media_perception_private::StateProtoToIdl(
          result.value()));
}

void MediaPerceptionAPIManager::GetDiagnosticsCallback(
    APIGetDiagnosticsCallback callback,
    std::optional<mri::Diagnostics> result) {
  if (!result.has_value()) {
    std::move(callback).Run(GetDiagnosticsForServiceError(
        extensions::api::media_perception_private::ServiceError::
            kServiceUnreachable));
    return;
  }
  std::move(callback).Run(
      extensions::api::media_perception_private::DiagnosticsProtoToIdl(
          result.value()));
}

void MediaPerceptionAPIManager::OnDetectionSignal(
    const mri::MediaPerception& media_perception_proto) {
  EventRouter* router = EventRouter::Get(browser_context_);
  DCHECK(router) << "EventRouter is null.";

  extensions::api::media_perception_private::MediaPerception media_perception =
      extensions::api::media_perception_private::MediaPerceptionProtoToIdl(
          media_perception_proto);
  std::unique_ptr<Event> event(new Event(
      events::MEDIA_PERCEPTION_PRIVATE_ON_MEDIA_PERCEPTION,
      extensions::api::media_perception_private::OnMediaPerception::kEventName,
      extensions::api::media_perception_private::OnMediaPerception::Create(
          media_perception)));
  router->BroadcastEvent(std::move(event));
}

}  // namespace extensions