| // 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 = ®istration; |
| } |
| } |
| 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 |