chromium/components/manta/sparky/sparky_provider.cc

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/manta/sparky/sparky_provider.h"

#include <memory>
#include <optional>
#include <set>
#include <string>
#include <vector>

#include "base/check.h"
#include "base/containers/fixed_flat_map.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "base/values.h"
#include "components/endpoint_fetcher/endpoint_fetcher.h"
#include "components/manta/base_provider.h"
#include "components/manta/manta_service.h"
#include "components/manta/manta_service_callbacks.h"
#include "components/manta/manta_status.h"
#include "components/manta/proto/manta.pb.h"
#include "components/manta/proto/sparky.pb.h"
#include "components/manta/sparky/sparky_delegate.h"
#include "components/manta/sparky/sparky_util.h"
#include "components/manta/sparky/system_info_delegate.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "net/traffic_annotation/network_traffic_annotation.h"

namespace manta {

namespace {

// TODO (b/336703051) Update with new Oauth.
constexpr char kOauthConsumerName[] = "manta_sparky";

// Handles the QA response from the server.
void OnQAServerResponseOrErrorReceived(
    SparkyProvider::SparkyProtoResponseCallback callback,
    std::unique_ptr<proto::Response> manta_response,
    MantaStatus manta_status) {
  if (manta_status.status_code != MantaStatusCode::kOk) {
    CHECK(manta_response == nullptr);
    std::move(callback).Run(nullptr, std::move(manta_status));
    return;
  }

  CHECK(manta_response != nullptr);
  if (manta_response->output_data_size() < 1 ||
      !manta_response->output_data(0).has_sparky_response()) {
    std::string message = std::string();

    // Tries to find more information from filtered_data
    if (manta_response->filtered_data_size() > 0 &&
        manta_response->filtered_data(0).is_output_data()) {
      message = base::StringPrintf(
          "filtered output for: %s",
          proto::FilteredReason_Name(manta_response->filtered_data(0).reason())
              .c_str());
    }
    std::move(callback).Run(std::make_unique<proto::SparkyResponse>(),
                            {MantaStatusCode::kBlockedOutputs, message});
    return;
  }

  std::move(callback).Run(std::make_unique<proto::SparkyResponse>(
                              manta_response->output_data(0).sparky_response()),
                          std::move(manta_status));
}

}  // namespace

SparkyProvider::SparkyProvider(
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
    signin::IdentityManager* identity_manager,
    const ProviderParams& provider_params,
    std::unique_ptr<SparkyDelegate> sparky_delegate,
    std::unique_ptr<SystemInfoDelegate> system_info_delegate)
    : BaseProvider(url_loader_factory, identity_manager, provider_params),
      sparky_delegate_(std::move(sparky_delegate)),
      system_info_delegate_(std::move(system_info_delegate)) {}

SparkyProvider::SparkyProvider(
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
    signin::IdentityManager* identity_manager,
    std::unique_ptr<SparkyDelegate> sparky_delegate,
    std::unique_ptr<SystemInfoDelegate> system_info_delegate)
    : BaseProvider(url_loader_factory, identity_manager),
      sparky_delegate_(std::move(sparky_delegate)),
      system_info_delegate_(std::move(system_info_delegate)) {}

SparkyProvider::~SparkyProvider() = default;

std::vector<manta::FileData> SparkyProvider::GetFilesSummary() {
  return sparky_delegate_->GetFileSummaries();
}

void SparkyProvider::QuestionAndAnswer(
    std::unique_ptr<SparkyContext> sparky_context,
    SparkyShowAnswerCallback done_callback) {
  sparky_delegate_->GetScreenshot(base::BindOnce(
      &SparkyProvider::OnScreenshotObtained, weak_ptr_factory_.GetWeakPtr(),
      std::move(sparky_context), std::move(done_callback)));
}

void SparkyProvider::OnScreenshotObtained(
    std::unique_ptr<SparkyContext> sparky_context,
    SparkyShowAnswerCallback done_callback,
    scoped_refptr<base::RefCountedMemory> jpeg_screenshot) {
  proto::Request request;
  request.set_feature_name(proto::FeatureName::CHROMEOS_SPARKY);

  auto* input_data = request.add_input_data();
  input_data->set_tag("sparky_context");

  auto* sparky_context_data = input_data->mutable_sparky_context_data();

  AddDialogToSparkyContext(sparky_context->dialog, sparky_context_data);

  sparky_context_data->set_task(sparky_context->task);
  if (sparky_context->page_url.has_value()) {
    auto* web_contents = sparky_context_data->mutable_web_contents();
    web_contents->set_page_url(sparky_context->page_url.value());
    if (sparky_context->page_content.has_value()) {
      web_contents->set_page_contents(sparky_context->page_content.value());
    }
  }

  if (jpeg_screenshot) {
    proto::Image* image_proto = sparky_context_data->mutable_screenshot();
    image_proto->set_serialized_bytes(
        std::string(base::as_string_view(*jpeg_screenshot)));
  }
  auto* apps_data = sparky_context_data->mutable_apps_data();
  AddAppsData(sparky_delegate_->GetAppsList(), apps_data);

  if (sparky_context->collect_settings) {
    auto* settings_list = sparky_delegate_->GetSettingsList();
    if (settings_list) {
      auto* settings_data = sparky_context_data->mutable_settings_data();
      AddSettingsProto(*settings_list, settings_data);
    }
  }
  if (sparky_context->diagnostics_data) {
    auto* diagnostics_proto = sparky_context_data->mutable_diagnostics_data();
    AddDiagnosticsProto(sparky_context->diagnostics_data, diagnostics_proto);
  }
  if (!sparky_context->files.empty()) {
    auto* files_proto = sparky_context_data->mutable_files_data();
    AddFilesData(sparky_context->files, files_proto);
  }

  // This parameter contains the address of one of the backends which the
  // request is passed through to once it is pushed up in a manta request.
  if (sparky_context->server_url) {
    proto::ServerConfig* server_config =
        sparky_context_data->mutable_server_config();
    server_config->set_server_url(sparky_context->server_url.value());
  }

  MantaProtoResponseCallback internal_callback = base::BindOnce(
      &OnQAServerResponseOrErrorReceived,
      base::BindOnce(&SparkyProvider::OnResponseReceived,
                     weak_ptr_factory_.GetWeakPtr(), std::move(done_callback),
                     std::move(sparky_context)));

  // TODO(b:338501686): MISSING_TRAFFIC_ANNOTATION should be resolved before
  // launch.
  RequestInternal(GURL{GetProviderEndpoint(false)}, kOauthConsumerName,
                  MISSING_TRAFFIC_ANNOTATION, request, MantaMetricType::kSparky,
                  std::move(internal_callback));
}

void SparkyProvider::OnResponseReceived(
    SparkyShowAnswerCallback done_callback,
    std::unique_ptr<SparkyContext> sparky_context,
    std::unique_ptr<proto::SparkyResponse> sparky_response,
    manta::MantaStatus status) {
  if (status.status_code != manta::MantaStatusCode::kOk) {
    std::move(done_callback).Run(status, nullptr);
    return;
  }

  if (sparky_response->has_update()) {
    if (sparky_response->update().has_files_with_summary()) {
      auto files_proto = sparky_response->update().files_with_summary();
      sparky_delegate_->UpdateFileSummaries(GetFileDataFromProto(files_proto));
    }
  }

  if (sparky_response->has_context_request()) {
    RequestAdditionalInformation(sparky_response->context_request(),
                                 std::move(sparky_context),
                                 std::move(done_callback), status);
    return;
  }
  if (sparky_response->has_latest_reply()) {
    OnDialogResponse(std::move(sparky_context), sparky_response->latest_reply(),
                     std::move(done_callback), status);
    return;
  }

  // Occurs if the response cannot be parsed correctly.
  std::move(done_callback).Run(status, nullptr);
  return;
}

void SparkyProvider::RequestAdditionalInformation(
    proto::ContextRequest context_request,
    std::unique_ptr<SparkyContext> sparky_context,
    SparkyShowAnswerCallback done_callback,
    manta::MantaStatus status) {
  if (context_request.has_settings()) {
    if (!sparky_delegate_->GetSettingsList()->empty()) {
      sparky_context->collect_settings = true;
      sparky_context->task = proto::TASK_SETTINGS;
      QuestionAndAnswer(std::move(sparky_context), std::move(done_callback));
      return;
    }
    std::move(done_callback).Run(status, nullptr);
    return;
  }
  if (context_request.has_diagnostics()) {
    auto diagnostics_vector =
        ObtainDiagnosticsVectorFromProto(context_request.diagnostics());
    if (!diagnostics_vector.empty()) {
      if (std::find(diagnostics_vector.begin(), diagnostics_vector.end(),
                    Diagnostics::kStorage) != diagnostics_vector.end()) {
        sparky_delegate_->ObtainStorageInfo(base::BindOnce(
            &SparkyProvider::OnStorageReceived, weak_ptr_factory_.GetWeakPtr(),
            std::move(sparky_context), std::move(done_callback), status,
            diagnostics_vector));
        return;
      }
      system_info_delegate_->ObtainDiagnostics(
          diagnostics_vector,
          base::BindOnce(&SparkyProvider::OnDiagnosticsReceived,
                         weak_ptr_factory_.GetWeakPtr(),
                         std::move(sparky_context), std::move(done_callback),
                         status, nullptr));
      return;
    }
    std::move(done_callback).Run(status, nullptr);
    return;
  }
  if (context_request.has_files()) {
    std::set<std::string> files = GetSelectedFilePaths(context_request.files());
    sparky_delegate_->GetMyFiles(
        base::BindOnce(
            &SparkyProvider::OnFilesObtained, weak_ptr_factory_.GetWeakPtr(),
            std::move(sparky_context), std::move(done_callback), status),
        /*obtain_bytes=*/true, /*allowed_file_paths=*/files);
    return;
  }

  // Occurs if no valid request can be found.
  std::move(done_callback).Run(status, nullptr);
}

void SparkyProvider::OnStorageReceived(
    std::unique_ptr<SparkyContext> sparky_context,
    SparkyShowAnswerCallback done_callback,
    manta::MantaStatus status,
    std::vector<Diagnostics> diagnostics_vector,
    std::unique_ptr<StorageData> storage_data) {
  bool get_system_diagnostics = false;
  for (auto diagnostic : diagnostics_vector) {
    if (diagnostic == Diagnostics::kBattery ||
        diagnostic == Diagnostics::kCpu || diagnostic == Diagnostics::kMemory) {
      get_system_diagnostics = true;
      break;
    }
  }
  if (get_system_diagnostics) {
    system_info_delegate_->ObtainDiagnostics(
        diagnostics_vector,
        base::BindOnce(&SparkyProvider::OnDiagnosticsReceived,
                       weak_ptr_factory_.GetWeakPtr(),
                       std::move(sparky_context), std::move(done_callback),
                       status, std::move(storage_data)));
    return;
  }
  sparky_context->diagnostics_data = std::make_optional<DiagnosticsData>(
      std::nullopt, std::nullopt, std::nullopt,
      std::make_optional(*storage_data));
  QuestionAndAnswer(std::move(sparky_context), std::move(done_callback));
}

void SparkyProvider::OnDiagnosticsReceived(
    std::unique_ptr<SparkyContext> sparky_context,
    SparkyShowAnswerCallback done_callback,
    manta::MantaStatus status,
    std::unique_ptr<StorageData> storage_data,
    std::unique_ptr<DiagnosticsData> diagnostics_data) {
  if (diagnostics_data) {
    diagnostics_data->storage_data = std::make_optional(*storage_data);
    sparky_context->diagnostics_data = std::make_optional(*diagnostics_data);
    sparky_context->task = proto::TASK_DIAGNOSTICS;
    QuestionAndAnswer(std::move(sparky_context), std::move(done_callback));
    return;
  }
  std::move(done_callback).Run(status, nullptr);
}

void SparkyProvider::OnDialogResponse(std::unique_ptr<SparkyContext>,
                                      proto::Turn latest_reply,
                                      SparkyShowAnswerCallback done_callback,
                                      manta::MantaStatus status) {
  // If the response does not contain any dialog then return an error.
  if (!latest_reply.has_message()) {
    std::move(done_callback).Run(status, nullptr);
    return;
  }
  if (latest_reply.action_size() > 0) {
    auto actions_repeated_field = latest_reply.action();
    for (const proto::Action& action : actions_repeated_field) {
      if (action.has_update_setting()) {
        std::unique_ptr<SettingsData> setting_data =
            ObtainSettingFromProto(action.update_setting());
        if (!setting_data) {
          // Return an error if the setting cannot be converted correctly from
          // the proto.
          DVLOG(1) << "Invalid setting action requested.";
          std::move(done_callback).Run(status, nullptr);
          return;
        }
        sparky_delegate_->SetSettings(std::move(setting_data));
      }
      if (action.has_launch_app_id()) {
        sparky_delegate_->LaunchApp(action.launch_app_id());
      }
      if (action.has_click()) {
        sparky_delegate_->Click(action.click().x_pos(), action.click().y_pos());
      }
      if (action.has_text_entry()) {
        sparky_delegate_->KeyboardEntry(action.text_entry().text());
      }
      if (action.has_file_action() &&
          action.file_action().has_launch_file_path()) {
        sparky_delegate_->LaunchFile(action.file_action().launch_file_path());
      }
    }
  }

  DialogTurn latest_dialog_struct = ConvertDialogToStruct(&latest_reply);
  std::move(done_callback).Run(status, &latest_dialog_struct);
}

void SparkyProvider::OnFilesObtained(
    std::unique_ptr<SparkyContext> sparky_context,
    SparkyShowAnswerCallback done_callback,
    manta::MantaStatus status,
    std::vector<FileData> files_data) {
  if (!files_data.empty()) {
    sparky_context->files = std::move(files_data);
    QuestionAndAnswer(std::move(sparky_context), std::move(done_callback));
  } else {
    std::move(done_callback).Run(status, nullptr);
  }
}

}  // namespace manta