chromium/chrome/browser/apps/app_service/webapk/webapk_utils.cc

// Copyright 2022 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/webapk/webapk_utils.h"

#include <optional>
#include <utility>
#include <vector>

#include "base/location.h"
#include "base/logging.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/web_applications/web_app_icon_manager.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "components/services/app_service/public/cpp/share_target.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
#include "third_party/smhasher/src/MurmurHash2.h"
#include "url/gurl.h"

// TODO(crbug.com/40199484): Consolidate logic with apps::WebApkInstallTask.

#if BUILDFLAG(IS_CHROMEOS_LACROS)

namespace {

const web_app::SquareSizePx kMinimumIconSize = 64;

// The seed to use when taking the murmur2 hash of the icon.
const uint64_t kMurmur2HashSeed = 0;

crosapi::mojom::WebApkCreationParamsPtr AddIconDataAndSerializeProto(
    const GURL& manifest_url,
    std::unique_ptr<webapk::WebAppManifest> webapk_manifest,
    std::vector<uint8_t> icon_data) {
  base::AssertLongCPUWorkAllowed();
  DCHECK_EQ(webapk_manifest->icons_size(), 1);

  webapk::Image* icon = webapk_manifest->mutable_icons(0);
  icon->set_image_data(icon_data.data(), icon_data.size());

  uint64_t icon_hash =
      MurmurHash64A(icon_data.data(), icon_data.size(), kMurmur2HashSeed);
  icon->set_hash(base::NumberToString(icon_hash));

  std::vector<uint8_t> serialized_proto(webapk_manifest->ByteSize());
  webapk_manifest->SerializeToArray(serialized_proto.data(),
                                    webapk_manifest->ByteSize());

  return crosapi::mojom::WebApkCreationParams::New(manifest_url.spec(),
                                                   std::move(serialized_proto));
}

void OnLoadedIcon(apps::GetWebApkCreationParamsCallback callback,
                  const GURL& manifest_url,
                  std::unique_ptr<webapk::WebAppManifest> webapk_manifest,
                  web_app::IconPurpose purpose,
                  std::vector<uint8_t> data) {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::TaskPriority::BEST_EFFORT},
      base::BindOnce(AddIconDataAndSerializeProto, manifest_url,
                     std::move(webapk_manifest), std::move(data)),
      std::move(callback));
}

}  // namespace

#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)

namespace apps {

void PopulateWebApkManifest(Profile* profile,
                            const std::string& app_id,
                            webapk::WebAppManifest* web_app_manifest) {
  auto* provider = web_app::WebAppProvider::GetForWebApps(profile);
  auto& registrar = provider->registrar_unsafe();

  // TODO(crbug.com/40199484): Call WebAppRegistrar::GetAppById(const AppId&
  // app_id) instead of performing repeated app_id lookups.

  web_app_manifest->set_short_name(registrar.GetAppShortName(app_id));
  web_app_manifest->set_start_url(registrar.GetAppStartUrl(app_id).spec());
  web_app_manifest->add_scopes(registrar.GetAppScope(app_id).spec());

  // We currently don't set name, orientation, display_mode, theme_color,
  // background_color, shortcuts.

  auto* share_target = registrar.GetAppShareTarget(app_id);
  webapk::ShareTarget* proto_share_target =
      web_app_manifest->add_share_targets();
  proto_share_target->set_action(share_target->action.spec());
  proto_share_target->set_method(
      apps::ShareTarget::MethodToString(share_target->method));
  proto_share_target->set_enctype(
      apps::ShareTarget::EnctypeToString(share_target->enctype));

  webapk::ShareTargetParams* proto_params =
      proto_share_target->mutable_params();
  if (!share_target->params.title.empty()) {
    proto_params->set_title(share_target->params.title);
  }
  if (!share_target->params.text.empty()) {
    proto_params->set_text(share_target->params.text);
  }
  if (!share_target->params.url.empty()) {
    proto_params->set_url(share_target->params.url);
  }

  for (const auto& file : share_target->params.files) {
    webapk::ShareTargetParamsFile* proto_file = proto_params->add_files();
    proto_file->set_name(file.name);
    for (const auto& accept_type : file.accept) {
      proto_file->add_accept(accept_type);
    }
  }
}

#if BUILDFLAG(IS_CHROMEOS_LACROS)

void GetWebApkCreationParams(Profile* profile,
                             const std::string& app_id,
                             GetWebApkCreationParamsCallback callback) {
  auto* provider = web_app::WebAppProvider::GetForWebApps(profile);
  if (!provider) {
    DVLOG(1) << "WebAppProvider is not available.";
    std::move(callback).Run({});
    return;
  }

  auto& registrar = provider->registrar_unsafe();

  // TODO(crbug.com/40199484): Call WebAppRegistrar::GetAppById(const AppId&
  // app_id) instead of performing repeated app_id lookups.

  // Installation & share target are already checked in WebApkManager, check
  // again in case anything changed while the install request was queued.
  // Manifest URL is always set for apps installed or updated in recent
  // versions, but might be missing for older apps.
  if (!registrar.IsInstalled(app_id) || !registrar.GetAppShareTarget(app_id) ||
      registrar.GetAppManifestUrl(app_id).is_empty()) {
    DVLOG(1) << "App is not installed, has no share target or is invalid.";
    std::move(callback).Run({});
    return;
  }

  auto webapk_manifest = std::make_unique<webapk::WebAppManifest>();

  auto& icon_manager = provider->icon_manager();
  std::optional<web_app::WebAppIconManager::IconSizeAndPurpose>
      icon_size_and_purpose = icon_manager.FindIconMatchBigger(
          app_id, {web_app::IconPurpose::MASKABLE, web_app::IconPurpose::ANY},
          kMinimumIconSize);

  if (!icon_size_and_purpose) {
    LOG(ERROR) << "Could not find suitable icon";
    std::move(callback).Run({});
    return;
  }

  // We need to send a URL for the icon, but it's possible the local image we're
  // sending has been resized and so doesn't exactly match any of the images in
  // the manifest. Since we can't be perfect, it's okay to be roughly correct
  // and just send any URL of the correct purpose.
  const auto& manifest_icons = registrar.GetAppIconInfos(app_id);
  auto it = base::ranges::find_if(
      manifest_icons, [&icon_size_and_purpose](const apps::IconInfo& info) {
        return info.purpose == web_app::ManifestPurposeToIconInfoPurpose(
                                   icon_size_and_purpose->purpose);
      });

  if (it == manifest_icons.end()) {
    LOG(ERROR) << "Could not find suitable icon";
    std::move(callback).Run({});
    return;
  }
  std::string icon_url = it->url.spec();

  PopulateWebApkManifest(profile, app_id, webapk_manifest.get());

  webapk::Image* image = webapk_manifest->add_icons();
  image->set_src(std::move(icon_url));
  image->add_purposes(icon_size_and_purpose->purpose ==
                              web_app::IconPurpose::MASKABLE
                          ? webapk::Image::MASKABLE
                          : webapk::Image::ANY);
  image->add_usages(webapk::Image::PRIMARY_ICON);

  icon_manager.ReadSmallestCompressedIcon(
      app_id, {icon_size_and_purpose->purpose}, icon_size_and_purpose->size_px,
      base::BindOnce(&OnLoadedIcon, std::move(callback),
                     registrar.GetAppManifestUrl(app_id),
                     std::move(webapk_manifest)));
}

#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)

}  // namespace apps