chromium/chrome/browser/apps/app_service/promise_apps/promise_app_service.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_service.h"

#include <memory>
#include <optional>

#include "ash/components/arc/arc_features.h"
#include "base/check.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "chrome/browser/apps/app_service/app_icon/app_icon_factory.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/package_id_util.h"
#include "chrome/browser/apps/app_service/promise_apps/promise_app.h"
#include "chrome/browser/apps/app_service/promise_apps/promise_app_almanac_connector.h"
#include "chrome/browser/apps/app_service/promise_apps/promise_app_icon_cache.h"
#include "chrome/browser/apps/app_service/promise_apps/promise_app_metrics.h"
#include "chrome/browser/apps/app_service/promise_apps/promise_app_registry_cache.h"
#include "chrome/browser/apps/app_service/promise_apps/promise_app_utils.h"
#include "chrome/browser/apps/app_service/promise_apps/promise_app_wrapper.h"
#include "chrome/browser/ash/app_list/arc/arc_app_list_prefs.h"
#include "chrome/browser/ash/app_list/arc/arc_package_install_priority_handler.h"
#include "chrome/browser/image_fetcher/image_decoder_impl.h"
#include "chrome/browser/profiles/profile.h"
#include "components/image_fetcher/core/image_fetcher_impl.h"
#include "components/services/app_service/public/cpp/app_registry_cache.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/icon_types.h"
#include "components/services/app_service/public/cpp/package_id.h"
#include "components/services/app_service/public/cpp/types_util.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "url/gurl.h"

namespace {

const net::NetworkTrafficAnnotationTag kTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("promise_app_service_download_icon",
                                        R"(
    semantics {
      sender: "Promise App Service"
      description:
        "Queries a Google server to fetch the icon of an app that is being "
        "installed or is pending installation on the device."
      trigger:
        "A request can be sent when an app starts installing or is pending "
        "installation."
      destination: GOOGLE_OWNED_SERVICE
      internal {
        contacts {
          email: "[email protected]"
        }
      }
      user_data {
        type: SENSITIVE_URL
      }
      data: "URL of the image to be fetched."
      last_reviewed: "2023-05-16"
    }
    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."
    }
  )");

apps::PromiseAppType GetPromiseAppType(apps::PackageType promise_app_type,
                                       apps::AppType installed_app_type) {
  if (promise_app_type == apps::PackageType::kArc &&
      installed_app_type == apps::AppType::kArc) {
    return apps::PromiseAppType::kArc;
  }
  if (promise_app_type == apps::PackageType::kArc &&
      installed_app_type == apps::AppType::kWeb) {
    return apps::PromiseAppType::kTwa;
  }
  return apps::PromiseAppType::kUnknown;
}

}  // namespace

namespace apps {
PromiseAppService::PromiseAppService(Profile* profile,
                                     AppRegistryCache& app_registry_cache)
    : profile_(profile),
      promise_app_registry_cache_(
          std::make_unique<apps::PromiseAppRegistryCache>()),
      promise_app_almanac_connector_(
          std::make_unique<PromiseAppAlmanacConnector>(profile)),
      promise_app_icon_cache_(std::make_unique<apps::PromiseAppIconCache>()),
      image_fetcher_(std::make_unique<image_fetcher::ImageFetcherImpl>(
          std::make_unique<ImageDecoderImpl>(),
          profile->GetURLLoaderFactory())),
      app_registry_cache_(&app_registry_cache) {
  app_registry_cache_observation_.Observe(&app_registry_cache);
}

PromiseAppService::~PromiseAppService() = default;

PromiseAppRegistryCache* PromiseAppService::PromiseAppRegistryCache() {
  return promise_app_registry_cache_.get();
}

PromiseAppIconCache* PromiseAppService::PromiseAppIconCache() {
  return promise_app_icon_cache_.get();
}

void PromiseAppService::OnPromiseApp(PromiseAppPtr delta) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  const PackageId package_id = delta->package_id;
  bool is_new_promise_app =
      !promise_app_registry_cache_->HasPromiseApp(package_id);

  // If the app is in the AppRegistryCache, then it already has an item in the
  // Launcher/ Shelf and we don't need to create a new promise app item to
  // represent it. This scenario happens when we start installing a default ARC
  // app (which is a stubbed app in App Registry Cache to show an icon in the
  // Launcher/ Shelf but uses legacy ARC default apps implementation).
  // TODO(b/286981938): Remove this check after refactoring to allow the Promise
  // App Service to manage ARC default app icons.
  if (is_new_promise_app && IsRegisteredInAppRegistryCache(package_id)) {
    return;
  }

  promise_app_registry_cache_->OnPromiseApp(std::move(delta));

  // If the promise app is newly removed, clear out the icons.
  if (!promise_app_registry_cache_->HasPromiseApp(package_id)) {
    promise_app_icon_cache_->RemoveIconsForPackageId(package_id);
  }

  // If this is a new promise app, send an Almanac request to fetch more
  // details.
  if (is_new_promise_app && !skip_almanac_for_testing_) {
    promise_app_almanac_connector_->GetPromiseAppInfo(
        package_id,
        base::BindOnce(&PromiseAppService::OnGetPromiseAppInfoCompleted,
                       weak_ptr_factory_.GetWeakPtr(), package_id));
  }
}

void PromiseAppService::LoadIcon(const PackageId& package_id,
                                 int32_t size_hint_in_dip,
                                 apps::IconEffects icon_effects,
                                 apps::LoadIconCallback callback) {
  promise_app_icon_cache_->GetIconAndApplyEffects(
      package_id, size_hint_in_dip, icon_effects, std::move(callback));
}

void PromiseAppService::OnAppUpdate(const apps::AppUpdate& update) {
  // Check that the update is for a new installation.
  if (!update.ReadinessChanged() ||
      update.Readiness() != apps::Readiness::kReady ||
      apps_util::IsInstalled(update.PriorReadiness())) {
    return;
  }

  std::optional<PackageId> package_id =
      apps_util::GetPackageIdForApp(profile_.get(), update);
  if (!package_id.has_value()) {
    return;
  }

  // Check that the update corresponds to a registered promise app.
  if (!promise_app_registry_cache_->HasPromiseApp(package_id.value())) {
    return;
  }

  // Record metrics for app type, noting that the app type may differ between
  // the promise app and the installed app.
  RecordPromiseAppType(
      GetPromiseAppType(package_id->package_type(), update.AppType()));

  // Delete the promise app.
  PromiseAppPtr promise_app = std::make_unique<PromiseApp>(package_id.value());
  promise_app->status = PromiseStatus::kSuccess;
  promise_app->installed_app_id = update.AppId();
  OnPromiseApp(std::move(promise_app));
}

void PromiseAppService::OnAppRegistryCacheWillBeDestroyed(
    apps::AppRegistryCache* cache) {
  app_registry_cache_observation_.Reset();
  app_registry_cache_ = nullptr;
}

void PromiseAppService::SetSkipAlmanacForTesting(bool skip_almanac) {
  skip_almanac_for_testing_ = skip_almanac;
}

void PromiseAppService::SetSkipApiKeyCheckForTesting(bool skip_api_key_check) {
  promise_app_almanac_connector_->SetSkipApiKeyCheckForTesting(  // IN-TEST
      skip_api_key_check);
}

void PromiseAppService::UpdateInstallPriority(const std::string& id) {
  const auto* promise_app =
      promise_app_registry_cache_->GetPromiseAppForStringPackageId(id);
  CHECK(promise_app);

  // Currently, updating install priority is only supported for ARC promise
  // apps.
  if (promise_app->package_id.package_type() != PackageType::kArc) {
    return;
  }

  // Feature flag that enables interacing with promise icon.
  if (!base::FeatureList::IsEnabled(arc::kSyncInstallPriority)) {
    return;
  }

  // We can only increase install priority for packages that are
  // queued/ pending. Promise apps that are already actively installing are
  // already treated as the highest priority installation and their installation
  // progress cannot be accelerated any further.
  if (promise_app->status != PromiseStatus::kPending) {
    return;
  }

  ArcAppListPrefs* arc_app_list_prefs = ArcAppListPrefs::Get(profile_);
  CHECK(arc_app_list_prefs);

  arc_app_list_prefs->GetInstallPriorityHandler()->PromotePackageInstall(
      promise_app->package_id.identifier());
}

void PromiseAppService::OnGetPromiseAppInfoCompleted(
    const PackageId& package_id,
    std::optional<PromiseAppWrapper> promise_app_info) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // If the promise app doesn't exist in the registry, drop the update. The app
  // installation may have completed before the Almanac returned a response.
  if (!promise_app_registry_cache_->HasPromiseApp(package_id)) {
    LOG(ERROR) << "Cannot update promise app " << package_id.ToString()
               << " as it does not exist in PromiseAppRegistry";
    return;
  }

  // If Almanac doesn't provide any meaningful response, continue to show the
  // promise app item. When an icon is requested, the PromiseAppIconCache will
  // fallback to returning a placeholder icon.
  if (!promise_app_info.has_value() ||
      !promise_app_info->GetName().has_value() ||
      promise_app_info->GetIcons().size() == 0) {
    RecordPromiseAppIconType(PromiseAppIconType::kPlaceholderIcon);
    SetPromiseAppReadyToShow(package_id);
    return;
  }

  PromiseAppPtr promise_app = std::make_unique<PromiseApp>(package_id);
  promise_app->name = promise_app_info->GetName().value();
  promise_app_registry_cache_->OnPromiseApp(std::move(promise_app));

  pending_download_count_[package_id] = promise_app_info->GetIcons().size();

  for (auto icon : promise_app_info->GetIcons()) {
    image_fetcher_->FetchImage(
        icon.GetUrl(),
        base::BindOnce(&PromiseAppService::OnIconDownloaded,
                       weak_ptr_factory_.GetWeakPtr(), package_id),
        image_fetcher::ImageFetcherParams(kTrafficAnnotation,
                                          "Promise App Service Icon Fetcher"));
  }
}

void PromiseAppService::OnIconDownloaded(
    const PackageId& package_id,
    const gfx::Image& image,
    const image_fetcher::RequestMetadata& metadata) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // If we weren't expecting an icon to be downloaded for this package ID, don't
  // process the result.
  if (!pending_download_count_.contains(package_id)) {
    LOG(ERROR) << "Will not save icon for unexpected package ID: "
               << package_id.ToString();
    return;
  }
  if (pending_download_count_[package_id] == 0) {
    LOG(ERROR) << "Will not save icon for unexpected package ID: "
               << package_id.ToString();
    pending_download_count_.erase(package_id);
    return;
  }

  // Save valid icons to the icon cache.
  if (!image.IsEmpty()) {
    PromiseAppIconPtr promise_app_icon = std::make_unique<PromiseAppIcon>();
    promise_app_icon->icon = image.AsBitmap();
    promise_app_icon->width_in_pixels = promise_app_icon->icon.width();
    promise_app_icon_cache_->SaveIcon(package_id, std::move(promise_app_icon));
  }

  // If there are still icons to be downloaded, we should wait for those
  // downloads to finish before updating the promise app. Otherwise, stop
  // tracking pending downloads for this package ID.
  pending_download_count_[package_id] -= 1;
  if (pending_download_count_[package_id] > 0) {
    return;
  }
  pending_download_count_.erase(package_id);
  RecordPromiseAppIconType(
      promise_app_icon_cache_->DoesPackageIdHaveIcons(package_id)
          ? PromiseAppIconType::kRealIcon
          : PromiseAppIconType::kPlaceholderIcon);

  // Update the promise app so it can show to the user.
  SetPromiseAppReadyToShow(package_id);
}

bool PromiseAppService::IsRegisteredInAppRegistryCache(
    const PackageId& package_id) {
  if (!app_registry_cache_) {
    return false;
  }
  bool is_registered = false;
  app_registry_cache_->ForEachApp(
      [&package_id, &is_registered](const AppUpdate& update) {
        // TODO(b/297296711): Update check for TWAs, which can have differing
        // package IDs.
        if (ConvertPackageTypeToAppType(package_id.package_type()) !=
            update.AppType()) {
          return;
        }
        if (update.PublisherId() != package_id.identifier()) {
          return;
        }
        if (!apps_util::IsInstalled(update.Readiness())) {
          // It's possible for an app to be in the AppRegistryCache despite
          // being uninstalled. Do not consider this as a registered
          // installed app.
          return;
        }
        is_registered = true;
        return;
      });
  return is_registered;
}

void PromiseAppService::SetPromiseAppReadyToShow(const PackageId& package_id) {
  PromiseAppPtr promise_app = std::make_unique<PromiseApp>(package_id);
  promise_app->should_show = true;
  promise_app_registry_cache_->OnPromiseApp(std::move(promise_app));
}

void PromiseAppService::OnApkWebAppInstallationFinished(
    const std::string& package_name) {
  PackageId package_id(PackageType::kArc, package_name);

  // Successful APK web app installations are already handled during a call to
  // observers via AppRegistryCache::OnAppUpdate which happens before this
  // method is called.
  if (!promise_app_registry_cache_->HasPromiseApp(package_id)) {
    return;
  }

  // We get to this point if the APK web installation failed. In this case, we
  // should remove the promise app and consider it a cancellation.
  PromiseAppPtr promise_app = std::make_unique<PromiseApp>(package_id);
  promise_app->status = PromiseStatus::kCancelled;
  OnPromiseApp(std::move(promise_app));
}

}  // namespace apps