chromium/chrome/browser/apps/app_service/promise_apps/promise_app_almanac_connector.cc

// Copyright 2023 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/promise_apps/promise_app_almanac_connector.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/promise_apps/promise_app_wrapper.h"
#include "chrome/browser/apps/app_service/promise_apps/proto/promise_app.pb.h"
#include "chrome/browser/profiles/profile.h"
#include "components/services/app_service/public/cpp/package_id.h"
#include "google_apis/google_api_keys.h"
#include "net/base/net_errors.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"

namespace apps {

namespace {

// Endpoint for requesting promise app data on the ChromeOS Almanac API.
constexpr char kPromiseAppAlmanacEndpoint[] = "v1/promise-app/";

// Maximum accepted size of an APS Response. 1MB.
constexpr int kMaxResponseSizeInBytes = 1024 * 1024;

constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("promise_app_service", R"(
      semantics {
        sender: "Promise App Service"
        description:
          "Sends a request to a Google server to get the name and "
          "icon data of an app being installed on the device."
        trigger:
          "A request can be sent when an app starts installing."
        destination: GOOGLE_OWNED_SERVICE
        internal {
          contacts {
            email: "[email protected]"
          }
        }
        user_data {
          type: PROFILE_DATA
        }
        data: "Name of the app platform and the platform-specific ID of the "
          "app package being installed, e.g. android:com.example.myapp"
        last_reviewed: "2023-04-21"
      }
      policy {
        cookies_allowed: NO
        setting:
          "This request is enabled by app sync without passphrase. You can"
          "disable this request in the 'Sync and Google services' section"
          "in Settings by either: 1. Going into the 'Manage What You Sync'"
          "settings page and turning off Apps sync; OR 2. In the 'Encryption"
          "Options' settings page, select the option to use a sync passphrase."
        policy_exception_justification:
          "This feature is required to deliver core user experiences and "
          "cannot be disabled by policy."
      }
    )");

std::optional<PromiseAppWrapper> ConvertPromiseAppResponseProto(
    base::expected<proto::PromiseAppResponse, QueryError> query_response) {
  if (query_response.has_value()) {
    return PromiseAppWrapper(std::move(query_response).value());
  }
  return std::nullopt;
}

}  // namespace

PromiseAppAlmanacConnector::PromiseAppAlmanacConnector(Profile* profile)
    : url_loader_factory_(profile->GetURLLoaderFactory()),
      device_info_manager_(std::make_unique<DeviceInfoManager>(profile)) {}

PromiseAppAlmanacConnector::~PromiseAppAlmanacConnector() = default;

void PromiseAppAlmanacConnector::GetPromiseAppInfo(
    const PackageId& package_id,
    GetPromiseAppCallback callback) {
  // Ensure that the build uses the Google-internal file containing the
  // official API keys, which are required to make queries to the Almanac.
  if (!google_apis::IsGoogleChromeAPIKeyUsed() &&
      !skip_api_key_check_for_testing_) {
    return;
  }
  if (locale_.empty()) {
    device_info_manager_->GetDeviceInfo(base::BindOnce(
        &PromiseAppAlmanacConnector::SetLocale, weak_ptr_factory_.GetWeakPtr(),
        package_id, std::move(callback)));
  } else {
    GetPromiseAppInfoImpl(package_id, std::move(callback));
  }
}

// static
GURL PromiseAppAlmanacConnector::GetServerUrl() {
  return GetAlmanacEndpointUrl(kPromiseAppAlmanacEndpoint);
}

void PromiseAppAlmanacConnector::SetSkipApiKeyCheckForTesting(
    bool skip_api_key_check) {
  skip_api_key_check_for_testing_ = skip_api_key_check;
}

void PromiseAppAlmanacConnector::GetPromiseAppInfoImpl(
    const PackageId& package_id,
    GetPromiseAppCallback callback) {
  QueryAlmanacApi<proto::PromiseAppResponse>(
      *url_loader_factory_, kTrafficAnnotation,
      BuildGetPromiseAppRequestBody(package_id), kPromiseAppAlmanacEndpoint,
      kMaxResponseSizeInBytes,
      /*error_histogram_name=*/std::nullopt,
      base::BindOnce(&ConvertPromiseAppResponseProto)
          .Then(std::move(callback)));
}

void PromiseAppAlmanacConnector::SetLocale(const PackageId& package_id,
                                           GetPromiseAppCallback callback,
                                           DeviceInfo device_info) {
  locale_ = device_info.locale;
  GetPromiseAppInfoImpl(package_id, std::move(callback));
}

std::string PromiseAppAlmanacConnector::BuildGetPromiseAppRequestBody(
    const apps::PackageId& package_id) {
  apps::proto::PromiseAppRequest request_proto;
  request_proto.set_language(locale_);
  request_proto.set_package_id(package_id.ToString());
  return request_proto.SerializeAsString();
}

}  // namespace apps