chromium/chrome/browser/apps/app_preload_service/app_preload_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_preload_service/app_preload_almanac_endpoint.h"

#include <utility>

#include "ash/constants/ash_features.h"
#include "base/feature_list.h"
#include "base/functional/callback.h"
#include "base/metrics/histogram_functions.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/almanac_api_client/proto/client_context.pb.h"
#include "chrome/browser/apps/app_preload_service/app_preload_service.h"
#include "chrome/browser/apps/app_preload_service/proto/app_preload.pb.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/app_constants/constants.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::app_preload_almanac_endpoint {
namespace {

// Endpoint for requesting app preload data on the ChromeOS Almanac API.
constexpr char kAppPreloadAlmanacEndpoint[] = "v1/app-preload?alt=proto";

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

constexpr char kServerErrorHistogramName[] =
    "AppPreloadService.ServerResponseCodes";

constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("app_preload_service", R"(
      semantics {
        sender: "App Preload Service"
        description:
          "Sends a request to a Google server to determine a list of apps to "
          "be installed on the device."
        trigger:
          "A request can be sent when a device is being set up, or after a "
          "device update."
        internal: {
          contacts {
            email: "[email protected]"
          }
        }
        user_data: {
          type: HW_OS_INFO
        }
        data: "Device technical specifications (e.g. model)."
        destination: GOOGLE_OWNED_SERVICE
        last_reviewed: "2024-05-03"
      }
      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."
      }
    )");

std::string BuildGetAppsForFirstLoginRequestBody(const apps::DeviceInfo& info) {
  apps::proto::AppPreloadListRequest request_proto;
  *request_proto.mutable_device_context() = info.ToDeviceContext();
  *request_proto.mutable_user_context() = info.ToUserContext();

  return request_proto.SerializeAsString();
}

// Filters entries in LauncherConfig and ShelfConfig to ignore when the
// specified feature is disabled.
bool IsFeatureEnabled(const std::string& name) {
  if (name == kAppPreloadServiceEnableTestApps.name) {
    return base::FeatureList::IsEnabled(kAppPreloadServiceEnableTestApps);
  } else if (name == ash::features::kHelpAppWelcomeTips.name) {
    return base::FeatureList::IsEnabled(ash::features::kHelpAppWelcomeTips);
  } else if (name == chromeos::features::kCloudGamingDevice.name) {
    return base::FeatureList::IsEnabled(chromeos::features::kCloudGamingDevice);
  } else if (!name.empty()) {
    LOG(ERROR) << "Unrecognised feature flag considered disabled: " << name;
    return false;
  }

  return true;
}

// Parses LauncherConfig from `in` and adds a LauncherItemMap entry
// into `out` LauncherOrdering keyed with `folder_name`. Uses single level of
// recursion to parse folders.
void ParseLauncherOrdering(
    const google::protobuf::RepeatedPtrField<
        proto::AppPreloadListResponse_LauncherConfig>& in,
    const std::string& folder_name,
    LauncherOrdering* out,
    bool allow_nested_folders = false) {
  LauncherItemMap item_map;
  for (const auto& item : in) {
    if (!IsFeatureEnabled(item.feature_flag())) {
      continue;
    }
    // All packages are added as keys to item_map with the same data.
    for (const auto& package_id : item.package_id()) {
      if (std::optional<apps::PackageId> parsed =
              apps::PackageId::FromString(package_id)) {
        item_map[*parsed] = LauncherItemData(item.type(), item.order());
      }
    }
    // Add packages for both chrome and lacros for TYPE_CHROME.
    if (item.type() ==
        proto::AppPreloadListResponse_LauncherType_LAUNCHER_TYPE_CHROME) {
      item_map[PackageId(PackageType::kChromeApp,
                         app_constants::kChromeAppId)] =
          LauncherItemData(item.type(), item.order());
      item_map[PackageId(PackageType::kSystem, app_constants::kLacrosChrome)] =
          LauncherItemData(item.type(), item.order());
    }
    // Add nested child folder.
    if (allow_nested_folders && !item.folder_name().empty()) {
      item_map[item.folder_name()] =
          LauncherItemData(item.type(), item.order());
      ParseLauncherOrdering(item.child_config(), item.folder_name(), out);
    }
  }
  (*out)[folder_name] = std::move(item_map);
}

// Parses ShelfConfig from `in` and stores result in `out` ShelfPinOrdering.
void ParseShelfPinOrdering(const google::protobuf::RepeatedPtrField<
                               proto::AppPreloadListResponse_ShelfConfig>& in,
                           ShelfPinOrdering* out) {
  // ShelfConfig is parsed into a map of PackageId and uint32 order.
  for (const auto& item : in) {
    // Ignore any packages which specify a feature flag which is disabled on
    // this device.
    if (!IsFeatureEnabled(item.feature_flag())) {
      continue;
    }
    // All packages are added as keys to the  map with the same order value.
    for (const auto& package_id : item.package_id()) {
      if (std::optional<apps::PackageId> parsed =
              apps::PackageId::FromString(package_id)) {
        (*out)[*parsed] = item.order();
      }
    }
  }
}

void ConvertAppPreloadListResponseProto(
    GetInitialAppsCallback callback,
    base::expected<proto::AppPreloadListResponse, QueryError> query_response) {
  LauncherOrdering launcher_ordering;
  ShelfPinOrdering shelf_pin_ordering;

  if (!query_response.has_value()) {
    std::move(callback).Run(std::nullopt, std::move(launcher_ordering),
                            std::move(shelf_pin_ordering));
    return;
  }

  std::vector<PreloadAppDefinition> apps;
  for (const auto& app : query_response->apps_to_install()) {
    apps.emplace_back(app);
  }

  std::string empty_root_folder;
  ParseLauncherOrdering(query_response->launcher_config(), empty_root_folder,
                        &launcher_ordering,
                        /*allow_nested_folders=*/true);

  ParseShelfPinOrdering(query_response->shelf_config(), &shelf_pin_ordering);

  std::move(callback).Run(std::move(apps), std::move(launcher_ordering),
                          std::move(shelf_pin_ordering));
}

}  // namespace

void GetAppsForFirstLogin(const DeviceInfo& device_info,
                          network::mojom::URLLoaderFactory& url_loader_factory,
                          GetInitialAppsCallback callback) {
  QueryAlmanacApi<proto::AppPreloadListResponse>(
      url_loader_factory, kTrafficAnnotation,
      BuildGetAppsForFirstLoginRequestBody(device_info),
      kAppPreloadAlmanacEndpoint, kMaxResponseSizeInBytes,
      kServerErrorHistogramName,
      base::BindOnce(&ConvertAppPreloadListResponseProto, std::move(callback)));
}

GURL GetServerUrl() {
  return GetAlmanacEndpointUrl(kAppPreloadAlmanacEndpoint);
}

}  // namespace apps::app_preload_almanac_endpoint