chromium/chrome/browser/apps/app_service/app_install/app_install_almanac_endpoint.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 "chrome/browser/apps/app_service/app_install/app_install_almanac_endpoint.h"

#include "base/functional/callback.h"
#include "chrome/browser/apps/almanac_api_client/almanac_api_util.h"
#include "chrome/browser/apps/almanac_api_client/device_info_manager.h"
#include "chrome/browser/apps/app_service/app_install/app_install.pb.h"
#include "chrome/browser/apps/app_service/app_install/app_install_types.h"
#include "chrome/browser/profiles/profile.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/package_id.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"

namespace apps::app_install_almanac_endpoint {

namespace {

constexpr char kAlmanacAppInstallEndpoint[] = "v1/app-install";

// TODO(b/307632613): Update annotations.xml and grouping.xml entries once
// update script issues are resolved.
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("almanac_app_install", R"(
      semantics {
        sender: "App Install Service"
        description:
          "Sends a request to a Google server to fetch app data for "
          "installation."
        trigger:
          "A request is sent when an app installation is triggered by the user "
          "for apps hosted on Almanac."
        internal: {
          contacts {
            email: "[email protected]"
          }
        }
        user_data: {
          type: HW_OS_INFO
        }
        data: "Device technical specifications (e.g. model)."
        destination: GOOGLE_OWNED_SERVICE
        last_reviewed: "2023-10-24"
      }
      policy {
        cookies_allowed: NO
        setting: "This feature cannot be disabled by settings."
        policy_exception_justification:
          "This feature is required to deliver core user experiences and "
          "cannot be disabled by policy."
      }
    )");

constexpr int kMaxResponseSizeInBytes = 1024 * 1024;

std::string BuildRequestBody(const DeviceInfo& info,
                             std::string serialized_package_id) {
  proto::AppInstallRequest request_proto;
  *request_proto.mutable_device_context() = info.ToDeviceContext();
  *request_proto.mutable_user_context() = info.ToUserContext();
  *request_proto.mutable_package_id() = std::move(serialized_package_id);

  return request_proto.SerializeAsString();
}

std::optional<AppInstallData> ParseAppInstallResponseProto(
    const proto::AppInstallResponse& app_install_response) {
  if (!app_install_response.has_app_instance()) {
    return std::nullopt;
  }
  const proto::AppInstallResponse_AppInstance& instance =
      app_install_response.app_instance();

  std::optional<PackageId> package_id =
      PackageId::FromString(instance.package_id());
  if (!package_id.has_value()) {
    return std::nullopt;
  }

  AppInstallData result(std::move(package_id).value());

  if (!instance.has_name()) {
    return std::nullopt;
  }
  result.name = instance.name();

  result.description = instance.description();

  if (instance.has_icon()) {
    AppInstallIcon icon{
        .url = GURL(instance.icon().url()),
        .width_in_pixels = instance.icon().width_in_pixels(),
        .mime_type = instance.icon().mime_type(),
        .is_masking_allowed = instance.icon().is_masking_allowed()};
    // SVG icons have 0 width.
    if (icon.url.is_valid() && icon.width_in_pixels >= 0) {
      result.icon = std::move(icon);
    }
  }

  for (const proto::AppInstallResponse_Screenshot& instance_screenshot :
       instance.screenshots()) {
    AppInstallScreenshot screenshot{
        .url = GURL(instance_screenshot.url()),
        .mime_type = instance_screenshot.mime_type(),
        .width_in_pixels = instance_screenshot.width_in_pixels(),
        .height_in_pixels = instance_screenshot.height_in_pixels(),
    };
    if (screenshot.url.is_valid()) {
      result.screenshots.push_back(std::move(screenshot));
    }
  }

  if (instance.has_install_url()) {
    result.install_url = GURL(instance.install_url());
  }

  if (result.package_id.package_type() == PackageType::kArc) {
    result.app_type_data.emplace<AndroidAppInstallData>();
  } else if (result.package_id.package_type() == PackageType::kWeb ||
             result.package_id.package_type() == PackageType::kWebsite) {
    if (!instance.has_web_extras()) {
      return std::nullopt;
    }
    WebAppInstallData& web_app_data =
        result.app_type_data.emplace<WebAppInstallData>();
    web_app_data.document_url = GURL(instance.web_extras().document_url());
    if (!web_app_data.document_url.is_valid()) {
      return std::nullopt;
    }
    web_app_data.original_manifest_url =
        GURL(instance.web_extras().original_manifest_url());
    if (!web_app_data.original_manifest_url.is_valid()) {
      return std::nullopt;
    }
    web_app_data.proxied_manifest_url = GURL(instance.web_extras().scs_url());
    if (!web_app_data.proxied_manifest_url.is_valid()) {
      return std::nullopt;
    }
    web_app_data.open_as_window = instance.web_extras().open_as_window();
  } else if (result.package_id.package_type() == PackageType::kGeForceNow) {
    result.app_type_data.emplace<GeForceNowAppInstallData>();
  } else if (result.package_id.package_type() == PackageType::kBorealis) {
    result.app_type_data.emplace<SteamAppInstallData>();
  }

  return result;
}

base::expected<AppInstallData, QueryError> ConvertAppInstallResponseProto(
    base::expected<proto::AppInstallResponse, QueryError> query_response) {
  if (!query_response.has_value()) {
    return base::unexpected(std::move(query_response).error());
  }

  std::optional<AppInstallData> data =
      ParseAppInstallResponseProto(query_response.value());
  if (!data.has_value()) {
    return base::unexpected(
        QueryError{QueryError::kBadResponse, "Failed to convert proto"});
  }

  return base::ok(std::move(data).value());
}

base::expected<GURL, QueryError> ExtractAppInstallUrlFromResponseProto(
    base::expected<proto::AppInstallResponse, QueryError> query_response) {
  if (!query_response.has_value()) {
    return base::unexpected(std::move(query_response).error());
  }

  GURL result = GURL(query_response.value().app_instance().install_url());
  if (!result.is_valid()) {
    return base::unexpected(
        QueryError{QueryError::kBadResponse, "Failed to convert install URL"});
  }

  return base::ok(std::move(result));
}

}  // namespace

GURL GetEndpointUrlForTesting() {
  return GetAlmanacEndpointUrl(kAlmanacAppInstallEndpoint);
}

void GetAppInstallInfo(PackageId package_id,
                       DeviceInfo device_info,
                       network::mojom::URLLoaderFactory& url_loader_factory,
                       GetAppInstallInfoCallback callback) {
  QueryAlmanacApi<proto::AppInstallResponse>(
      url_loader_factory, kTrafficAnnotation,
      BuildRequestBody(device_info, package_id.ToString()),
      kAlmanacAppInstallEndpoint, kMaxResponseSizeInBytes,
      /*error_histogram_name=*/std::nullopt,
      base::BindOnce(&ConvertAppInstallResponseProto)
          .Then(std::move(callback)));
}

using GetAppInstallUrlCallback =
    base::OnceCallback<void(base::expected<GURL, QueryError>)>;
void GetAppInstallUrl(std::string serialized_package_id,
                      DeviceInfo device_info,
                      network::mojom::URLLoaderFactory& url_loader_factory,
                      GetAppInstallUrlCallback callback) {
  QueryAlmanacApi<proto::AppInstallResponse>(
      url_loader_factory, kTrafficAnnotation,
      BuildRequestBody(device_info, std::move(serialized_package_id)),
      kAlmanacAppInstallEndpoint, kMaxResponseSizeInBytes,
      /*error_histogram_name=*/std::nullopt,
      base::BindOnce(&ExtractAppInstallUrlFromResponseProto)
          .Then(std::move(callback)));
}

}  // namespace apps::app_install_almanac_endpoint