chromium/chrome/browser/ash/guest_os/guest_os_registry_service.cc

// Copyright 2018 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/ash/guest_os/guest_os_registry_service.h"

#include <string_view>
#include <utility>

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/app_list/app_list_config.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "base/time/clock.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "chrome/browser/apps/app_service/app_icon/app_icon_factory.h"
#include "chrome/browser/apps/app_service/app_icon/dip_px_util.h"
#include "chrome/browser/ash/app_list/app_list_syncable_service.h"
#include "chrome/browser/ash/app_list/app_list_syncable_service_factory.h"
#include "chrome/browser/ash/borealis/borealis_app_launcher.h"
#include "chrome/browser/ash/borealis/borealis_features.h"
#include "chrome/browser/ash/borealis/borealis_service.h"
#include "chrome/browser/ash/borealis/borealis_util.h"
#include "chrome/browser/ash/bruschetta/bruschetta_util.h"
#include "chrome/browser/ash/crostini/crostini_features.h"
#include "chrome/browser/ash/crostini/crostini_manager.h"
#include "chrome/browser/ash/guest_os/guest_os_pref_names.h"
#include "chrome/browser/ash/guest_os/guest_os_shelf_utils.h"
#include "chrome/browser/ash/guest_os/public/types.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_features.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_files.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_util.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/icon_transcoder/svg_icon_transcoder.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/grit/chromeos_app_icon_resources.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/dbus/vm_applications/apps.pb.h"
#include "components/crx_file/id_util.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/services/app_service/public/cpp/icon_types.h"
#include "extensions/browser/api/file_handlers/mime_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_scale_factor.h"
#include "ui/gfx/image/image_skia_operations.h"

using vm_tools::apps::App;

namespace guest_os {

namespace {

void Launch(vm_tools::apps::VmType vm_type,
            std::string app_id,
            Profile* profile,
            const GURL& url) {
  switch (vm_type) {
    case VmType::TERMINA:
      crostini::LaunchCrostiniApp(profile, app_id, display::kInvalidDisplayId,
                                  {url.spec()}, base::DoNothing());
      break;

    case VmType::PLUGIN_VM:
      plugin_vm::LaunchPluginVmApp(profile, app_id, {url.spec()},
                                   base::DoNothing());
      break;

    case VmType::BOREALIS:
      borealis::BorealisService::GetForProfile(profile)->AppLauncher().Launch(
          app_id, {url.spec()}, borealis::BorealisLaunchSource::kAppUrlHandler,
          base::DoNothing());
      break;

    default:
      // Usual best practice is to exhaustively handle all enum cases, in order
      // to trigger a compiler warning if a newly-added enum case isn't handled.
      // However, this enum is generated, and the source proto lives in the CrOS
      // platform2 repo. If we attempted to exhaustively handle all cases,
      // adding a new enum entry would unavoidably break Chromium's build (since
      // warnings are treated as errors). So instead we have this default case,
      // and log unexpected values.
      LOG(ERROR) << "Unsupported VmType: " << static_cast<int>(VmType());
  }
}

bool AppHandlesProtocol(const GuestOsRegistryService::Registration& app,
                        const GURL& url) {
  if (app.VmType() == VmType::BOREALIS &&
      !borealis::IsExternalURLAllowed(url)) {
    return false;
  }
  return base::Contains(app.MimeTypes(), "x-scheme-handler/" + url.scheme());
}

// This prefix is used when generating the crostini app list id.
constexpr char kCrostiniAppIdPrefix[] = "crostini:";

constexpr char kCrostiniIconFolder[] = "crostini.icons";

base::Value::Dict ProtoToDictionary(const App::LocaleString& locale_string) {
  base::Value::Dict result;
  for (const App::LocaleString::Entry& entry : locale_string.values()) {
    const std::string& locale = entry.locale();

    std::string locale_with_dashes(locale);
    std::replace(locale_with_dashes.begin(), locale_with_dashes.end(), '_',
                 '-');
    if (!locale.empty() &&
        !l10n_util::IsValidLocaleSyntax(locale_with_dashes)) {
      continue;
    }

    result.Set(locale, base::Value(entry.value()));
  }
  return result;
}

std::set<std::string> ListToStringSet(const base::Value::List* list,
                                      bool to_lower_ascii = false) {
  std::set<std::string> result;
  if (!list) {
    return result;
  }
  for (const base::Value& value : *list) {
    result.insert(to_lower_ascii ? base::ToLowerASCII(value.GetString())
                                 : value.GetString());
  }
  return result;
}

base::Value::List ProtoToList(
    const google::protobuf::RepeatedPtrField<std::string>& strings) {
  base::Value::List result;
  for (const std::string& string : strings) {
    result.Append(string);
  }
  return result;
}

base::Value::Dict LocaleStringsProtoToDictionary(
    const App::LocaleStrings& repeated_locale_string) {
  base::Value::Dict result;
  for (const auto& strings_with_locale : repeated_locale_string.values()) {
    const std::string& locale = strings_with_locale.locale();

    std::string locale_with_dashes(locale);
    std::replace(locale_with_dashes.begin(), locale_with_dashes.end(), '_',
                 '-');
    if (!locale.empty() &&
        !l10n_util::IsValidLocaleSyntax(locale_with_dashes)) {
      continue;
    }
    result.Set(locale, ProtoToList(strings_with_locale.value()));
  }
  return result;
}

// Populate |pref_registration| based on the given App proto.
// |name| should be |app.name()| in Dictionary form.
void PopulatePrefRegistrationFromApp(base::Value::Dict& pref_registration,
                                     VmType vm_type,
                                     const std::string& vm_name,
                                     const std::string& container_name,
                                     const vm_tools::apps::App& app,
                                     base::Value::Dict name) {
  pref_registration.Set(guest_os::prefs::kAppDesktopFileIdKey,
                        base::Value(app.desktop_file_id()));
  pref_registration.Set(guest_os::prefs::kVmTypeKey, static_cast<int>(vm_type));
  pref_registration.Set(guest_os::prefs::kVmNameKey, base::Value(vm_name));
  pref_registration.Set(guest_os::prefs::kContainerNameKey,
                        base::Value(container_name));
  pref_registration.Set(guest_os::prefs::kAppNameKey, std::move(name));
  pref_registration.Set(guest_os::prefs::kAppExecKey, base::Value(app.exec()));
  pref_registration.Set(guest_os::prefs::kAppExecutableFileNameKey,
                        base::Value(app.executable_file_name()));
  pref_registration.Set(guest_os::prefs::kAppExtensionsKey,
                        ProtoToList(app.extensions()));
  pref_registration.Set(guest_os::prefs::kAppMimeTypesKey,
                        ProtoToList(app.mime_types()));
  pref_registration.Set(guest_os::prefs::kAppKeywordsKey,
                        LocaleStringsProtoToDictionary(app.keywords()));
  pref_registration.Set(guest_os::prefs::kAppNoDisplayKey,
                        base::Value(app.no_display()));
  pref_registration.Set(guest_os::prefs::kAppTerminalKey,
                        base::Value(app.terminal()));
  pref_registration.Set(guest_os::prefs::kAppStartupWMClassKey,
                        base::Value(app.startup_wm_class()));
  pref_registration.Set(guest_os::prefs::kAppStartupNotifyKey,
                        base::Value(app.startup_notify()));
  pref_registration.Set(guest_os::prefs::kAppPackageIdKey,
                        base::Value(app.package_id()));
}

bool EqualsExcludingTimestamps(const base::Value::Dict& left,
                               const base::Value::Dict& right) {
  auto left_iter = left.begin();
  auto right_iter = right.begin();
  while (left_iter != left.end() && right_iter != right.end()) {
    if (left_iter->first == guest_os::prefs::kAppInstallTimeKey ||
        left_iter->first == guest_os::prefs::kAppLastLaunchTimeKey) {
      ++left_iter;
      continue;
    }
    if (right_iter->first == guest_os::prefs::kAppInstallTimeKey ||
        right_iter->first == guest_os::prefs::kAppLastLaunchTimeKey) {
      ++right_iter;
      continue;
    }
    if (*left_iter != *right_iter)
      return false;
    ++left_iter;
    ++right_iter;
  }
  return left_iter == left.end() && right_iter == right.end();
}

void InstallIconFromFileThread(const base::FilePath& icon_path,
                               const std::string& content) {
  DCHECK(!content.empty());

  base::CreateDirectory(icon_path.DirName());

  if (!base::WriteFile(icon_path, content)) {
    VLOG(2) << "Failed to write Crostini icon file: "
            << icon_path.MaybeAsASCII();
    if (!base::DeleteFile(icon_path)) {
      VLOG(2) << "Couldn't delete broken icon file" << icon_path.MaybeAsASCII();
    }
  }
}

void DeleteIconFolderFromFileThread(const base::FilePath& path) {
  DCHECK(path.DirName().BaseName().MaybeAsASCII() == kCrostiniIconFolder &&
         (!base::PathExists(path) || base::DirectoryExists(path)));
  const bool deleted = base::DeletePathRecursively(path);
  DCHECK(deleted);
}

template <typename List>
static std::string Join(const List& list);

static std::string ToString(bool b) {
  return b ? "true" : "false";
}

static std::string ToString(int i) {
  return base::NumberToString(i);
}

static std::string ToString(const std::string& string) {
  return '"' + string + '"';
}

static std::string ToString(
    const google::protobuf::RepeatedPtrField<std::string>& list) {
  return Join(list);
}

static std::string ToString(
    const vm_tools::apps::App_LocaleString_Entry& entry) {
  return "{locale: " + ToString(entry.locale()) +
         ", value: " + ToString(entry.value()) + "}";
}

static std::string ToString(
    const vm_tools::apps::App_LocaleStrings_StringsWithLocale&
        strings_with_locale) {
  return "{locale: " + ToString(strings_with_locale.locale()) +
         ", value: " + ToString(strings_with_locale.value()) + "}";
}

static std::string ToString(const vm_tools::apps::App_LocaleString& string) {
  return Join(string.values());
}

static std::string ToString(const vm_tools::apps::App_LocaleStrings& strings) {
  return Join(strings.values());
}

static std::string ToString(const vm_tools::apps::App& app) {
  return "{desktop_file_id: " + ToString(app.desktop_file_id()) +
         ", name: " + ToString(app.name()) +
         ", comment: " + ToString(app.comment()) +
         ", mime_types: " + ToString(app.mime_types()) +
         ", no_display: " + ToString(app.no_display()) +
         ", terminal: " + ToString(app.terminal()) +
         ", startup_wm_class: " + ToString(app.startup_wm_class()) +
         ", startup_notify: " + ToString(app.startup_notify()) +
         ", keywords: " + ToString(app.keywords()) +
         ", exec: " + ToString(app.exec()) +
         ", executable_file_name: " + ToString(app.executable_file_name()) +
         ", package_id: " + ToString(app.package_id()) +
         ", extensions: " + ToString(app.extensions()) + "}";
}

static std::string ToString(const vm_tools::apps::ApplicationList& list) {
  return "{apps: " + Join(list.apps()) +
         ", vm_type: " + ToString(list.vm_type()) +
         ", vm_name: " + ToString(list.vm_name()) +
         ", container_name: " + ToString(list.container_name()) +
         ", owner_id: " + ToString(list.owner_id()) + "}";
}

template <typename List>
static std::string Join(const List& list) {
  std::string joined = "[";
  const char* seperator = "";
  for (const auto& list_item : list) {
    joined += seperator + ToString(list_item);
    seperator = ", ";
  }
  joined += "]";
  return joined;
}

std::string GetStringKey(const base::Value& dict, std::string_view key) {
  if (!dict.is_dict()) {
    return std::string();
  }
  const std::string* value = dict.GetDict().FindString(key);
  if (!value) {
    return std::string();
  }
  return *value;
}

}  // namespace

GuestOsRegistryService::Registration::Registration(std::string app_id,
                                                   base::Value pref)
    : app_id_(std::move(app_id)), pref_(std::move(pref)) {}

GuestOsRegistryService::Registration::~Registration() = default;

std::string GuestOsRegistryService::Registration::DesktopFileId() const {
  return GetString(guest_os::prefs::kAppDesktopFileIdKey);
}

VmType GuestOsRegistryService::Registration::VmType() const {
  return VmTypeFromPref(pref_);
}

std::string GuestOsRegistryService::Registration::VmName() const {
  return GetString(guest_os::prefs::kVmNameKey);
}

std::string GuestOsRegistryService::Registration::ContainerName() const {
  return GetString(guest_os::prefs::kContainerNameKey);
}

std::string GuestOsRegistryService::Registration::Name() const {
  if (VmType() == VmType::PLUGIN_VM) {
    return l10n_util::GetStringFUTF8(
        IDS_PLUGIN_VM_APP_NAME_WINDOWS_SUFFIX,
        base::UTF8ToUTF16(GetLocalizedString(guest_os::prefs::kAppNameKey)));
  }
  return GetLocalizedString(guest_os::prefs::kAppNameKey);
}

std::string GuestOsRegistryService::Registration::Exec() const {
  return GetString(guest_os::prefs::kAppExecKey);
}

std::string GuestOsRegistryService::Registration::ExecutableFileName() const {
  return GetString(guest_os::prefs::kAppExecutableFileNameKey);
}

std::set<std::string> GuestOsRegistryService::Registration::Extensions() const {
  if (!pref_.is_dict()) {
    return {};
  }
  // Convert to lowercase ASCII to allow case-insensitive match.
  return ListToStringSet(
      pref_.GetDict().FindList(guest_os::prefs::kAppExtensionsKey),
      /*to_lower_ascii=*/true);
}

std::set<std::string> GuestOsRegistryService::Registration::MimeTypes() const {
  if (!pref_.is_dict()) {
    return {};
  }
  // Convert to lowercase ASCII to allow case-insensitive match.
  return ListToStringSet(
      pref_.GetDict().FindList(guest_os::prefs::kAppMimeTypesKey),
      /*to_lower_ascii=*/true);
}

std::set<std::string> GuestOsRegistryService::Registration::Keywords() const {
  return GetLocalizedList(guest_os::prefs::kAppKeywordsKey);
}

bool GuestOsRegistryService::Registration::NoDisplay() const {
  return GetBool(guest_os::prefs::kAppNoDisplayKey);
}

bool GuestOsRegistryService::Registration::Terminal() const {
  return GetBool(guest_os::prefs::kAppTerminalKey);
}
std::string GuestOsRegistryService::Registration::PackageId() const {
  return GetString(guest_os::prefs::kAppPackageIdKey);
}

bool GuestOsRegistryService::Registration::CanUninstall() const {
  if (!pref_.is_dict()) {
    return false;
  }
  // We can uninstall if and only if there is a package that owns the
  // application. If no package owns the application, we don't know how to
  // uninstall the app.
  //
  // We don't check other things that might prevent us from uninstalling the
  // app. In particular, we don't check if there are other packages which
  // depend on the owning package. This should be rare for packages that have
  // desktop files, and it's better to show an error message (which the user can
  // then Google to learn more) than to just not have an uninstall option at
  // all.
  const std::string* package_id =
      pref_.GetDict().FindString(guest_os::prefs::kAppPackageIdKey);
  if (package_id) {
    return !package_id->empty();
  }
  return false;
}

guest_os::GuestId GuestOsRegistryService::Registration::ToGuestId() const {
  return guest_os::GuestId(VmType(), VmName(), ContainerName());
}

base::Time GuestOsRegistryService::Registration::InstallTime() const {
  return GetTime(guest_os::prefs::kAppInstallTimeKey);
}

base::Time GuestOsRegistryService::Registration::LastLaunchTime() const {
  return GetTime(guest_os::prefs::kAppLastLaunchTimeKey);
}

bool GuestOsRegistryService::Registration::IsScaled() const {
  return GetBool(guest_os::prefs::kAppScaledKey);
}

std::string GuestOsRegistryService::Registration::StartupWmClass() const {
  return GetString(guest_os::prefs::kAppStartupWMClassKey);
}

bool GuestOsRegistryService::Registration::StartupNotify() const {
  return GetBool(guest_os::prefs::kAppStartupNotifyKey);
}

std::string GuestOsRegistryService::Registration::GetString(
    std::string_view key) const {
  return GetStringKey(pref_, key);
}

bool GuestOsRegistryService::Registration::GetBool(std::string_view key) const {
  if (!pref_.is_dict()) {
    return false;
  }
  const std::optional<bool> value = pref_.GetDict().FindBool(key);
  return value.value_or(false);
}

// This is the companion to GuestOsRegistryService::SetCurrentTime().
base::Time GuestOsRegistryService::Registration::GetTime(
    std::string_view key) const {
  if (!pref_.is_dict()) {
    return base::Time();
  }
  const std::string* value = pref_.GetDict().FindString(key);
  int64_t time;
  if (!value || !base::StringToInt64(*value, &time)) {
    return base::Time();
  }
  return base::Time::FromDeltaSinceWindowsEpoch(base::Microseconds(time));
}

// We store in prefs all the localized values for given fields (formatted with
// undescores, e.g. 'fr' or 'en_US'), but users of the registry don't need to
// deal with this.
std::string GuestOsRegistryService::Registration::GetLocalizedString(
    std::string_view key) const {
  if (!pref_.is_dict()) {
    return std::string();
  }
  const base::Value::Dict* dict = pref_.GetDict().FindDict(key);
  if (!dict) {
    return std::string();
  }

  std::string current_locale =
      l10n_util::NormalizeLocale(g_browser_process->GetApplicationLocale());
  std::vector<std::string> locales;
  l10n_util::GetParentLocales(current_locale, &locales);
  // We use an empty locale as fallback.
  locales.push_back(std::string());

  for (const std::string& locale : locales) {
    const std::string* value = dict->FindString(locale);
    if (value) {
      return *value;
    }
  }
  return std::string();
}

std::set<std::string> GuestOsRegistryService::Registration::GetLocalizedList(
    std::string_view key) const {
  if (!pref_.is_dict()) {
    return {};
  }
  const base::Value::Dict* dict = pref_.GetDict().FindDict(key);
  if (!dict) {
    return {};
  }

  std::string current_locale =
      l10n_util::NormalizeLocale(g_browser_process->GetApplicationLocale());
  std::vector<std::string> locales;
  l10n_util::GetParentLocales(current_locale, &locales);
  // We use an empty locale as fallback.
  locales.push_back(std::string());

  for (const std::string& locale : locales) {
    const base::Value::List* list = dict->FindList(locale);
    if (list) {
      return ListToStringSet(list);
    }
  }
  return {};
}

GuestOsRegistryService::GuestOsRegistryService(Profile* profile)
    : profile_(profile),
      prefs_(profile->GetPrefs()),
      base_icon_path_(profile->GetPath().AppendASCII(kCrostiniIconFolder)),
      clock_(base::DefaultClock::GetInstance()),
      svg_icon_transcoder_(std::make_unique<apps::SvgIconTranscoder>(profile)) {
}

GuestOsRegistryService::~GuestOsRegistryService() = default;

base::WeakPtr<GuestOsRegistryService> GuestOsRegistryService::GetWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

std::map<std::string, GuestOsRegistryService::Registration>
GuestOsRegistryService::GetAllRegisteredApps() const {
  const base::Value::Dict& apps =
      prefs_->GetDict(guest_os::prefs::kGuestOsRegistry);
  std::map<std::string, GuestOsRegistryService::Registration> result;
  for (const auto item : apps) {
    result.emplace(item.first, Registration(item.first, item.second.Clone()));
  }
  return result;
}

std::map<std::string, GuestOsRegistryService::Registration>
GuestOsRegistryService::GetEnabledApps() const {
  bool crostini_enabled =
      crostini::CrostiniFeatures::Get()->IsEnabled(profile_);
  bool plugin_vm_enabled =
      plugin_vm::PluginVmFeatures::Get()->IsEnabled(profile_);
  bool borealis_enabled = borealis::BorealisService::GetForProfile(profile_)
                              ->Features()
                              .IsEnabled();
  if (!crostini_enabled && !plugin_vm_enabled && !borealis_enabled) {
    return {};
  }

  auto apps = GetAllRegisteredApps();
  for (auto it = apps.cbegin(); it != apps.cend();) {
    bool enabled = false;
    switch (it->second.VmType()) {
      case VmType::TERMINA:
        enabled = crostini_enabled;
        break;
      case VmType::PLUGIN_VM:
        enabled = plugin_vm_enabled;
        break;
      case VmType::BOREALIS:
        enabled = borealis_enabled;
        break;
      default:
        LOG(ERROR) << "Unsupported VmType: "
                   << static_cast<int>(it->second.VmType());
    }
    if (enabled) {
      ++it;
    } else {
      it = apps.erase(it);
    }
  }
  return apps;
}

std::map<std::string, GuestOsRegistryService::Registration>
GuestOsRegistryService::GetRegisteredApps(VmType vm_type) const {
  auto apps = GetAllRegisteredApps();
  for (auto it = apps.cbegin(); it != apps.cend();) {
    if (it->second.VmType() == vm_type) {
      ++it;
    } else {
      it = apps.erase(it);
    }
  }
  return apps;
}

std::optional<GuestOsRegistryService::Registration>
GuestOsRegistryService::GetRegistration(const std::string& app_id) const {
  const base::Value::Dict& apps =
      prefs_->GetDict(guest_os::prefs::kGuestOsRegistry);

  const base::Value::Dict* pref_registration = apps.FindDict(app_id);
  if (!pref_registration) {
    return std::nullopt;
  }
  return std::make_optional<Registration>(
      app_id, base::Value(pref_registration->Clone()));
}

void GuestOsRegistryService::RegisterTransientUrlHandler(
    GuestOsUrlHandler handler,
    CanHandleUrlCallback canHandleCallback) {
  url_handlers_.emplace_back(handler, canHandleCallback);
}

std::optional<GuestOsUrlHandler> GuestOsRegistryService::GetHandler(
    const GURL& url) const {
  // Transient URL handlers are system-installed, so always take priority.
  for (const auto& handler : url_handlers_) {
    if (handler.second.Run(url)) {
      return handler.first;
    }
  }

  std::map<std::string, Registration> apps = GetEnabledApps();
  const Registration* result = nullptr;
  for (auto& [unused, registration] : apps) {
    if (AppHandlesProtocol(registration, url) &&
        (!result || registration.LastLaunchTime() > result->LastLaunchTime())) {
      result = &registration;
    }
  }
  if (!result) {
    return std::nullopt;
  }
  return std::make_optional<GuestOsUrlHandler>(
      result->Name(),
      base::BindRepeating(Launch, result->VmType(), result->app_id()));
}

base::FilePath GuestOsRegistryService::GetAppPath(
    const std::string& app_id) const {
  return base_icon_path_.AppendASCII(app_id);
}

base::FilePath GuestOsRegistryService::GetIconPath(
    const std::string& app_id,
    ui::ResourceScaleFactor scale_factor) const {
  const base::FilePath app_path = GetAppPath(app_id);
  switch (scale_factor) {
    case ui::k100Percent:
      return app_path.AppendASCII("icon_100p.png");
    case ui::k200Percent:
      return app_path.AppendASCII("icon_200p.png");
    case ui::k300Percent:
      return app_path.AppendASCII("icon_300p.png");
    case ui::kScaleFactorNone:
      return app_path.AppendASCII("icon.svg");
    default:
      NOTREACHED_IN_MIGRATION();
      return base::FilePath();
  }
}

void GuestOsRegistryService::LoadIcon(const std::string& app_id,
                                      const apps::IconKey& icon_key,
                                      apps::IconType icon_type,
                                      int32_t size_hint_in_dip,
                                      bool allow_placeholder_icon,
                                      int fallback_icon_resource_id,
                                      apps::LoadIconCallback callback) {
  // Add container-badging to all crostini apps except the terminal, which is
  // shared between containers. This is part of the multi-container UI, so is
  // guarded by a flag.
  if (crostini::CrostiniFeatures::Get()->IsMultiContainerAllowed(profile_)) {
    auto reg = GetRegistration(app_id);
    if (reg && reg->VmType() == VmType::TERMINA) {
      callback = base::BindOnce(
          &GuestOsRegistryService::ApplyContainerBadgeWithCallback,
          weak_ptr_factory_.GetWeakPtr(),
          crostini::GetContainerBadgeColor(
              profile_, guest_os::GuestId(reg->VmType(), reg->VmName(),
                                          reg->ContainerName())),
          std::move(callback));
    }
  }

  if (icon_key.resource_id != apps::IconKey::kInvalidResourceId) {
    // The icon is a resource built into the Chrome OS binary.
    constexpr bool is_placeholder_icon = false;
    apps::LoadIconFromResource(
        profile_, app_id, icon_type, size_hint_in_dip, icon_key.resource_id,
        is_placeholder_icon,
        static_cast<apps::IconEffects>(icon_key.icon_effects),
        std::move(callback));
    return;
  }

  // There are paths where nothing higher up the call stack will resize so
  // we need to ensure that returned icons are always resized to be
  // size_hint_in_dip big. crbug/1170455 is an example.
  apps::IconEffects icon_effects = static_cast<apps::IconEffects>(
      icon_key.icon_effects | apps::IconEffects::kMdIconStyle);
  auto scale_factor = apps_util::GetPrimaryDisplayUIScaleFactor();

  auto load_icon_from_vm_fallback = base::BindOnce(
      &GuestOsRegistryService::LoadIconFromVM, weak_ptr_factory_.GetWeakPtr(),
      app_id, icon_type, size_hint_in_dip, scale_factor, icon_effects,
      fallback_icon_resource_id);

  auto transcode_svg_fallback = base::BindOnce(
      &GuestOsRegistryService::TranscodeIconFromSvg,
      weak_ptr_factory_.GetWeakPtr(), GetIconPath(app_id, ui::kScaleFactorNone),
      GetIconPath(app_id, scale_factor), icon_type, size_hint_in_dip,
      icon_effects, std::move(load_icon_from_vm_fallback));

  // Try loading the icon from an on-disk cache. If that fails, try to transcode
  // the app's svg icon, and if that fails, fall back
  // to LoadIconFromVM.
  apps::LoadIconFromFileWithFallback(
      icon_type, size_hint_in_dip, GetIconPath(app_id, scale_factor),
      icon_effects, std::move(callback), std::move(transcode_svg_fallback));
}

void GuestOsRegistryService::ApplyContainerBadge(
    const std::optional<std::string>& app_id,
    gfx::ImageSkia* image_skia) {
  if (crostini::CrostiniFeatures::Get()->IsMultiContainerAllowed(profile_)) {
    auto reg = GetRegistration(*app_id);
    if (reg && reg->VmType() == guest_os::VmType::TERMINA) {
      ApplyContainerBadgeForImageSkiaIcon(
          crostini::GetContainerBadgeColor(
              profile_, guest_os::GuestId(reg->VmType(), reg->VmName(),
                                          reg->ContainerName())),
          image_skia);
    }
  }
}

void GuestOsRegistryService::ApplyContainerBadgeForImageSkiaIcon(
    SkColor badge_color,
    gfx::ImageSkia* icon_out) {
  gfx::ImageSkia badge_mask =
      *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
          IDR_ICON_BADGE_MASK);

  if (badge_mask.size() != icon_out->size()) {
    badge_mask = gfx::ImageSkiaOperations::CreateResizedImage(
        badge_mask, skia::ImageOperations::RESIZE_BEST, icon_out->size());
  }
  badge_mask =
      gfx::ImageSkiaOperations::CreateColorMask(badge_mask, badge_color);
  *icon_out =
      gfx::ImageSkiaOperations::CreateSuperimposedImage(*icon_out, badge_mask);
}

void GuestOsRegistryService::ApplyContainerBadgeWithCallback(
    SkColor badge_color,
    apps::LoadIconCallback callback,
    apps::IconValuePtr icon) {
  gfx::ImageSkia badge_mask =
      *ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
          IDR_ICON_BADGE_MASK);

  if (badge_mask.size() != icon->uncompressed.size()) {
    badge_mask = gfx::ImageSkiaOperations::CreateResizedImage(
        badge_mask, skia::ImageOperations::RESIZE_BEST,
        icon->uncompressed.size());
  }
  badge_mask =
      gfx::ImageSkiaOperations::CreateColorMask(badge_mask, badge_color);
  icon->uncompressed = gfx::ImageSkiaOperations::CreateSuperimposedImage(
      icon->uncompressed, badge_mask);

  std::move(callback).Run(std::move(icon));
}

void GuestOsRegistryService::TranscodeIconFromSvg(
    base::FilePath svg_path,
    base::FilePath png_path,
    apps::IconType icon_type,
    int32_t size_hint_in_dip,
    apps::IconEffects icon_effects,
    base::OnceCallback<void(apps::LoadIconCallback)> fallback,
    apps::LoadIconCallback callback) {
  svg_icon_transcoder_->Transcode(
      std::move(svg_path), std::move(png_path), gfx::Size(128, 128),
      base::BindOnce(
          [](apps::IconType icon_type, int32_t size_hint_in_dip,
             apps::IconEffects icon_effects, apps::LoadIconCallback callback,
             base::OnceCallback<void(apps::LoadIconCallback)> fallback,
             std::string icon_content) {
            if (!icon_content.empty()) {
              apps::LoadIconFromCompressedData(
                  icon_type, size_hint_in_dip, icon_effects,
                  std::move(icon_content), std::move(callback));
              return;
            }
            if (fallback) {
              std::move(fallback).Run(std::move(callback));
            }
          },
          icon_type, size_hint_in_dip, icon_effects, std::move(callback),
          std::move(fallback)));
}

void GuestOsRegistryService::LoadIconFromVM(
    const std::string& app_id,
    apps::IconType icon_type,
    int32_t size_hint_in_dip,
    ui::ResourceScaleFactor scale_factor,
    apps::IconEffects icon_effects,
    int fallback_icon_resource_id,
    apps::LoadIconCallback callback) {
  RequestIcon(app_id, scale_factor,
              base::BindOnce(&GuestOsRegistryService::OnLoadIconFromVM,
                             weak_ptr_factory_.GetWeakPtr(), app_id, icon_type,
                             size_hint_in_dip, icon_effects,
                             fallback_icon_resource_id, std::move(callback)));
}

void GuestOsRegistryService::OnLoadIconFromVM(
    const std::string& app_id,
    apps::IconType icon_type,
    int32_t size_hint_in_dip,
    apps::IconEffects icon_effects,
    int fallback_icon_resource_id,
    apps::LoadIconCallback callback,
    std::string compressed_icon_data) {
  if (compressed_icon_data.empty()) {
    if (fallback_icon_resource_id != apps::IconKey::kInvalidResourceId) {
      // We load the fallback icon, but we tell AppsService that this is not
      // a placeholder to avoid endless repeat calls since we don't expect to
      // find a better icon than this any time soon.
      apps::LoadIconFromResource(profile_, app_id, icon_type, size_hint_in_dip,
                                 fallback_icon_resource_id,
                                 /*is_placeholder_icon=*/false, icon_effects,
                                 std::move(callback));
    } else {
      std::move(callback).Run(std::make_unique<apps::IconValue>());
    }
  } else {
    apps::LoadIconFromCompressedData(icon_type, size_hint_in_dip, icon_effects,
                                     compressed_icon_data, std::move(callback));
  }
}

void GuestOsRegistryService::RequestIcon(
    const std::string& app_id,
    ui::ResourceScaleFactor scale_factor,
    base::OnceCallback<void(std::string)> callback) {
  if (!GetRegistration(app_id)) {
    // App isn't registered (e.g. a GUI app launched from within Crostini
    // that doesn't have a .desktop file). Can't get an icon for that case so
    // return an empty icon.
    std::move(callback).Run({});
    return;
  }

  // Coalesce calls to the container.
  auto& callbacks = active_icon_requests_[{app_id, scale_factor}];
  callbacks.emplace_back(std::move(callback));
  if (callbacks.size() > 1) {
    return;
  }
  RequestContainerAppIcon(app_id, scale_factor);
}

void GuestOsRegistryService::ClearApplicationList(
    VmType vm_type,
    const std::string& vm_name,
    const std::string& container_name) {
  std::vector<std::string> removed_apps;
  // The ScopedDictPrefUpdate should be destructed before calling the observer.
  {
    ScopedDictPrefUpdate update(prefs_, guest_os::prefs::kGuestOsRegistry);
    base::Value::Dict& apps = update.Get();

    for (const auto item : apps) {
      Registration registration(item.first, item.second.Clone());
      if (vm_type != registration.VmType()) {
        continue;
      }
      if (vm_name != registration.VmName()) {
        continue;
      }
      if (!container_name.empty() &&
          container_name != registration.ContainerName()) {
        continue;
      }
      removed_apps.push_back(item.first);
    }
    for (const std::string& removed_app : removed_apps) {
      RemoveAppData(removed_app);
      apps.Remove(removed_app);
    }
  }

  if (removed_apps.empty()) {
    return;
  }

  std::vector<std::string> updated_apps;
  std::vector<std::string> inserted_apps;
  for (Observer& obs : observers_) {
    obs.OnRegistryUpdated(this, vm_type, updated_apps, removed_apps,
                          inserted_apps);
  }
}

void GuestOsRegistryService::UpdateApplicationList(
    const vm_tools::apps::ApplicationList& app_list) {
  VLOG(3) << "Received ApplicationList : " << ToString(app_list);
  // TODO(b/294316866): Special-case Bruschetta VMs until cicerone is updated to
  // use the correct vm_type.
  vm_tools::apps::VmType vm_type = app_list.vm_type();
  if (app_list.vm_name() == bruschetta::kBruschettaVmName) {
    vm_type = vm_tools::apps::VmType::BRUSCHETTA;
  }

  if (app_list.vm_name().empty()) {
    LOG(WARNING) << "Received app list with missing VM name";
    return;
  }
  if (app_list.container_name().empty()) {
    LOG(WARNING) << "Received app list with missing container name";
    return;
  }

  // We need to compute the diff between the new list of apps and the old list
  // of apps (with matching vm/container names). We keep a set of the new app
  // ids so that we can compute these and update the Dictionary directly.
  std::set<std::string> new_app_ids;
  std::vector<std::string> updated_apps;
  std::vector<std::string> removed_apps;
  std::vector<std::string> inserted_apps;

  // The ScopedDictPrefUpdate should be destructed before calling the observer.
  {
    ScopedDictPrefUpdate update(prefs_, guest_os::prefs::kGuestOsRegistry);
    base::Value::Dict& apps = update.Get();
    for (const App& app : app_list.apps()) {
      if (app.desktop_file_id().empty()) {
        LOG(WARNING) << "Received app with missing desktop file id";
        continue;
      }

      base::Value::Dict name = ProtoToDictionary(app.name());
      if (name.Find(std::string_view()) == nullptr) {
        LOG(WARNING) << "Received app '" << app.desktop_file_id()
                     << "' with missing unlocalized name";
        continue;
      }

      std::string app_id = GenerateAppId(
          app.desktop_file_id(), app_list.vm_name(), app_list.container_name());
      new_app_ids.insert(app_id);

      base::Value::Dict pref_registration;
      PopulatePrefRegistrationFromApp(
          pref_registration, vm_type, app_list.vm_name(),
          app_list.container_name(), app, std::move(name));

      base::Value::Dict* old_app = apps.FindDict(app_id);
      if (old_app && EqualsExcludingTimestamps(pref_registration, *old_app)) {
        continue;
      }

      base::Value* old_install_time = nullptr;
      base::Value* old_last_launch_time = nullptr;
      if (old_app) {
        updated_apps.push_back(app_id);
        old_install_time = old_app->Find(guest_os::prefs::kAppInstallTimeKey);
        old_last_launch_time =
            old_app->Find(guest_os::prefs::kAppLastLaunchTimeKey);
      } else {
        inserted_apps.push_back(app_id);
      }

      if (old_install_time) {
        pref_registration.Set(guest_os::prefs::kAppInstallTimeKey,
                              old_install_time->Clone());
      } else {
        SetCurrentTime(pref_registration, guest_os::prefs::kAppInstallTimeKey);
      }

      if (old_last_launch_time) {
        pref_registration.Set(guest_os::prefs::kAppLastLaunchTimeKey,
                              old_last_launch_time->Clone());
      }

      apps.Set(app_id, std::move(pref_registration));
    }

    for (const auto item : apps) {
      std::string vm_name =
          GetStringKey(item.second, guest_os::prefs::kVmNameKey);
      std::string container_name =
          GetStringKey(item.second, guest_os::prefs::kContainerNameKey);
      if (vm_name.empty() || container_name.empty()) {
        LOG(WARNING) << "Detected app with empty vm or container name";
        removed_apps.push_back(item.first);
      } else if (vm_name == app_list.vm_name() &&
                 container_name == app_list.container_name() &&
                 new_app_ids.find(item.first) == new_app_ids.end()) {
        removed_apps.push_back(item.first);
      }
    }

    for (const std::string& removed_app : removed_apps) {
      RemoveAppData(removed_app);
      apps.Remove(removed_app);
    }
  }

  // When we receive notification of the application list then the container
  // *should* be online and we can retry all of our icon requests that failed
  // due to the container being offline.
  for (auto retry_iter = retry_icon_requests_.begin();
       retry_iter != retry_icon_requests_.end(); ++retry_iter) {
    for (const auto scale_factor : ui::GetSupportedResourceScaleFactors()) {
      if (retry_iter->second & (1 << scale_factor)) {
        RequestContainerAppIcon(retry_iter->first, scale_factor);
      }
    }
  }
  retry_icon_requests_.clear();

  if (updated_apps.empty() && removed_apps.empty() && inserted_apps.empty()) {
    return;
  }

  for (Observer& obs : observers_) {
    obs.OnRegistryUpdated(this, vm_type, updated_apps, removed_apps,
                          inserted_apps);
  }
}

void GuestOsRegistryService::ContainerBadgeColorChanged(
    const guest_os::GuestId& container_id) {
  std::vector<std::string> updated_apps;

  for (const auto& it : GetAllRegisteredApps()) {
    if (it.second.VmName() == container_id.vm_name &&
        it.second.ContainerName() == container_id.container_name) {
      updated_apps.push_back(it.first);
    }
  }

  std::vector<std::string> removed_apps;
  std::vector<std::string> inserted_apps;
  for (Observer& obs : observers_) {
    obs.OnRegistryUpdated(this, VmType::TERMINA, updated_apps, removed_apps,
                          inserted_apps);
  }
}

void GuestOsRegistryService::RemoveAppData(const std::string& app_id) {
  // Remove any pending requests we have for this icon.
  retry_icon_requests_.erase(app_id);

  // Remove local data on filesystem for the icons.
  base::ThreadPool::PostTask(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&DeleteIconFolderFromFileThread, GetAppPath(app_id)));
}

void GuestOsRegistryService::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void GuestOsRegistryService::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

void GuestOsRegistryService::AppLaunched(const std::string& app_id) {
  ScopedDictPrefUpdate update(prefs_, guest_os::prefs::kGuestOsRegistry);
  base::Value::Dict& app = update->Find(app_id)->GetDict();
  SetCurrentTime(app, guest_os::prefs::kAppLastLaunchTimeKey);

  auto vm_type = app.FindInt(guest_os::prefs::kVmTypeKey);
  if (!vm_type.has_value()) {
    LOG(ERROR) << "Failed to find " << guest_os::prefs::kVmTypeKey
               << " for app " << app_id;
    return;
  }

  for (Observer& obs : observers_) {
    obs.OnAppLastLaunchTimeUpdated(static_cast<VmType>(vm_type.value()), app_id,
                                   clock_->Now());
  }
}

void GuestOsRegistryService::SetCurrentTime(base::Value::Dict& dictionary,
                                            const char* key) const {
  int64_t time = clock_->Now().ToDeltaSinceWindowsEpoch().InMicroseconds();
  dictionary.Set(key, base::Value(base::NumberToString(time)));
}

void GuestOsRegistryService::SetAppScaled(const std::string& app_id,
                                          bool scaled) {
  ScopedDictPrefUpdate update(prefs_, guest_os::prefs::kGuestOsRegistry);
  base::Value::Dict& apps = update.Get();

  base::Value::Dict* app = apps.FindDict(app_id);
  if (!app) {
    LOG(ERROR)
        << "Tried to set display scaled property on the app with this app_id "
        << app_id << " that doesn't exist in the registry.";
    return;
  }
  app->Set(guest_os::prefs::kAppScaledKey, scaled);
}

// static
std::string GuestOsRegistryService::GenerateAppId(
    const std::string& desktop_file_id,
    const std::string& vm_name,
    const std::string& container_name) {
  // These can collide in theory because the user could choose VM and container
  // names which contain slashes, but this will only result in apps missing from
  // the launcher.
  return crx_file::id_util::GenerateId(kCrostiniAppIdPrefix + vm_name + "/" +
                                       container_name + "/" + desktop_file_id);
}

void GuestOsRegistryService::RequestContainerAppIcon(
    const std::string& app_id,
    ui::ResourceScaleFactor scale_factor) {
  // Ignore requests for app_id that isn't registered.
  std::optional<GuestOsRegistryService::Registration> registration =
      GetRegistration(app_id);
  DCHECK(registration);
  if (!registration) {
    LOG(ERROR) << "Request to load icon for non-registered app: " << app_id;
    return;
  }
  VLOG(1) << "Request to load icon for app: " << app_id;

  // Now make the call to request the actual icon.
  std::vector<std::string> desktop_file_ids{registration->DesktopFileId()};
  // We can only send integer scale factors to Crostini, so if we have a
  // non-integral scale factor we need round the scale factor. We do not expect
  // Crostini to give us back exactly what we ask for and we deal with that in
  // the CrostiniAppIcon class and may rescale the result in there to match our
  // needs.
  uint32_t icon_scale = 1;
  switch (scale_factor) {
    case ui::k200Percent:
      icon_scale = 2;
      break;
    case ui::k300Percent:
      icon_scale = 3;
      break;
    default:
      break;
  }

  crostini::CrostiniManager::GetForProfile(profile_)->GetContainerAppIcons(
      guest_os::GuestId(registration->VmType(), registration->VmName(),
                        registration->ContainerName()),
      desktop_file_ids,
      ash::SharedAppListConfig::instance().default_grid_icon_dimension(),
      icon_scale,
      base::BindOnce(&GuestOsRegistryService::OnContainerAppIcon,
                     weak_ptr_factory_.GetWeakPtr(), app_id, scale_factor));
}

void GuestOsRegistryService::InvokeActiveIconCallbacks(
    std::string app_id,
    ui::ResourceScaleFactor scale_factor,
    std::string icon_content) {
  // Invoke all active icon request callbacks with the icon.
  auto key =
      std::pair<std::string, ui::ResourceScaleFactor>(app_id, scale_factor);
  auto& callbacks = active_icon_requests_[key];
  VLOG(1) << "Invoking icon callbacks for app: " << app_id
          << ", num callbacks: " << callbacks.size();
  for (auto& callback : callbacks) {
    std::move(callback).Run(icon_content);
  }
  active_icon_requests_.erase(key);
}

void GuestOsRegistryService::OnSvgIconTranscoded(
    std::string app_id,
    ui::ResourceScaleFactor scale_factor,
    std::string svg_icon_content,
    std::string png_icon_content) {
  if (png_icon_content.empty()) {
    VLOG(1) << "Failed to transcode svg icon for " << app_id;
  }
  // Write svg to disk, then invoke active callbacks with png content.
  base::ThreadPool::PostTaskAndReply(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&InstallIconFromFileThread,
                     GetIconPath(app_id, ui::kScaleFactorNone),
                     std::move(svg_icon_content)),
      base::BindOnce(&GuestOsRegistryService::InvokeActiveIconCallbacks,
                     weak_ptr_factory_.GetWeakPtr(), app_id, scale_factor,
                     std::move(png_icon_content)));
}

void GuestOsRegistryService::OnContainerAppIcon(
    const std::string& app_id,
    ui::ResourceScaleFactor scale_factor,
    bool success,
    const std::vector<crostini::Icon>& icons) {
  std::string icon_content;
  if (!success) {
    VLOG(1) << "Failed to load icon for app: " << app_id;
    // Add this to the list of retryable icon requests so we redo this when
    // we get feedback from the container that it's available.
    retry_icon_requests_[app_id] |= (1 << scale_factor);
    InvokeActiveIconCallbacks(app_id, scale_factor, std::string());
    return;
  }

  if (icons.empty()) {
    VLOG(1) << "No icon in container for app: " << app_id;
    InvokeActiveIconCallbacks(app_id, scale_factor, std::string());
    return;
  }

  // Install the icon that we received, and invoke active callbacks.
  const base::FilePath icon_path = GetIconPath(app_id, scale_factor);
  bool is_svg = icons[0].format == vm_tools::cicerone::DesktopIcon::SVG;
  VLOG(1) << "Found icon in container for app: " << app_id
          << " path: " << icon_path << " format: " << (is_svg ? "svg" : "png")
          << " bytes: " << icons[0].content.size();
  if (is_svg) {
    svg_icon_transcoder_->Transcode(
        icons[0].content, std::move(icon_path), gfx::Size(128, 128),
        base::BindOnce(&GuestOsRegistryService::OnSvgIconTranscoded,
                       weak_ptr_factory_.GetWeakPtr(), app_id, scale_factor,
                       icons[0].content));
    return;
  }

  base::ThreadPool::PostTaskAndReply(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&InstallIconFromFileThread, std::move(icon_path),
                     icons[0].content),
      base::BindOnce(&GuestOsRegistryService::InvokeActiveIconCallbacks,
                     weak_ptr_factory_.GetWeakPtr(), app_id, scale_factor,
                     icons[0].content));
}

}  // namespace guest_os