blob: d1c7666a236b39c9beebcb11cf8a3e897c4ce798 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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 <utility>
#include "ash/public/cpp/app_list/app_list_config.h"
#include "base/bind.h"
#include "base/files/file_util.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/post_task.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_factory.h"
#include "chrome/browser/apps/app_service/dip_px_util.h"
#include "chrome/browser/ash/borealis/borealis_features.h"
#include "chrome/browser/ash/borealis/borealis_service.h"
#include "chrome/browser/ash/crostini/crostini_features.h"
#include "chrome/browser/ash/crostini/crostini_manager.h"
#include "chrome/browser/ash/crostini/crostini_shelf_utils.h"
#include "chrome/browser/ash/guest_os/guest_os_pref_names.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_features.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_util.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/app_list/app_list_syncable_service.h"
#include "chrome/browser/ui/app_list/app_list_syncable_service_factory.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/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 "extensions/browser/api/file_handlers/mime_util.h"
#include "ui/base/l10n/l10n_util.h"
using vm_tools::apps::App;
namespace guest_os {
namespace {
// This prefix is used when generating the crostini app list id.
constexpr char kCrostiniAppIdPrefix[] = "crostini:";
constexpr char kCrostiniIconFolder[] = "crostini.icons";
constexpr char kCrostiniAppsInstalledHistogram[] =
"Crostini.AppsInstalledAtLogin";
constexpr char kPluginVmAppsInstalledHistogram[] =
"PluginVm.AppsInstalledAtLogin";
base::Value ProtoToDictionary(const App::LocaleString& locale_string) {
base::Value result(base::Value::Type::DICTIONARY);
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.SetKey(locale, base::Value(entry.value()));
}
return result;
}
std::set<std::string> ListToStringSet(const base::Value* list) {
std::set<std::string> result;
if (!list)
return result;
for (const base::Value& value : list->GetList())
result.insert(value.GetString());
return result;
}
base::Value ProtoToList(
const google::protobuf::RepeatedPtrField<std::string>& strings) {
base::Value result(base::Value::Type::LIST);
for (const std::string& string : strings)
result.Append(string);
return result;
}
base::Value LocaleStringsProtoToDictionary(
const App::LocaleStrings& repeated_locale_string) {
base::Value result(base::Value::Type::DICTIONARY);
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.SetKey(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& pref_registration,
GuestOsRegistryService::VmType vm_type,
const std::string& vm_name,
const std::string& container_name,
const vm_tools::apps::App& app,
base::Value name) {
pref_registration.SetKey(guest_os::prefs::kAppDesktopFileIdKey,
base::Value(app.desktop_file_id()));
pref_registration.SetIntKey(guest_os::prefs::kAppVmTypeKey,
static_cast<int>(vm_type));
pref_registration.SetKey(guest_os::prefs::kAppVmNameKey,
base::Value(vm_name));
pref_registration.SetKey(guest_os::prefs::kAppContainerNameKey,
base::Value(container_name));
pref_registration.SetKey(guest_os::prefs::kAppNameKey, std::move(name));
pref_registration.SetKey(guest_os::prefs::kAppCommentKey,
ProtoToDictionary(app.comment()));
pref_registration.SetKey(guest_os::prefs::kAppExecKey,
base::Value(app.exec()));
pref_registration.SetKey(guest_os::prefs::kAppExecutableFileNameKey,
base::Value(app.executable_file_name()));
pref_registration.SetKey(guest_os::prefs::kAppExtensionsKey,
ProtoToList(app.extensions()));
pref_registration.SetKey(guest_os::prefs::kAppMimeTypesKey,
ProtoToList(app.mime_types()));
pref_registration.SetKey(guest_os::prefs::kAppKeywordsKey,
LocaleStringsProtoToDictionary(app.keywords()));
pref_registration.SetKey(guest_os::prefs::kAppNoDisplayKey,
base::Value(app.no_display()));
pref_registration.SetKey(guest_os::prefs::kAppStartupWMClassKey,
base::Value(app.startup_wm_class()));
pref_registration.SetKey(guest_os::prefs::kAppStartupNotifyKey,
base::Value(app.startup_notify()));
pref_registration.SetKey(guest_os::prefs::kAppPackageIdKey,
base::Value(app.package_id()));
}
// This is the companion to GuestOsRegistryService::SetCurrentTime().
base::Time GetTime(const base::Value& pref, const char* key) {
if (!pref.is_dict())
return base::Time();
const base::Value* value = pref.FindKeyOfType(key, base::Value::Type::STRING);
int64_t time;
if (!value || !base::StringToInt64(value->GetString(), &time))
return base::Time();
return base::Time::FromDeltaSinceWindowsEpoch(
base::TimeDelta::FromMicroseconds(time));
}
bool EqualsExcludingTimestamps(const base::Value& left,
const base::Value& right) {
auto left_items = left.DictItems();
auto right_items = right.DictItems();
auto left_iter = left_items.begin();
auto right_iter = right_items.begin();
while (left_iter != left_items.end() && right_iter != right_items.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_items.end() && right_iter == right_items.end();
}
void InstallIconFromFileThread(const base::FilePath& icon_path,
const std::string& content_png) {
DCHECK(!content_png.empty());
base::CreateDirectory(icon_path.DirName());
int wrote =
base::WriteFile(icon_path, content_png.c_str(), content_png.size());
if (wrote != static_cast<int>(content_png.size())) {
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()) +
", 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;
}
void SetLocaleString(App::LocaleString* locale_string,
const std::string& locale,
const std::string& value) {
DCHECK(!locale.empty());
App::LocaleString::Entry* entry = locale_string->add_values();
// Add both specified locale, and empty default.
for (auto& l : {locale, std::string()}) {
entry->set_locale(l);
entry->set_value(value);
}
}
void SetLocaleStrings(App::LocaleStrings* locale_strings,
const std::string& locale,
std::vector<std::string> values) {
DCHECK(!locale.empty());
App::LocaleStrings::StringsWithLocale* strings = locale_strings->add_values();
// Add both specified locale, and empty default.
for (auto& l : {locale, std::string()}) {
strings->set_locale(l);
for (auto& v : values) {
strings->add_value(v);
}
}
}
// Creates a Terminal registration using partial values from prefs such as
// last_launch_time.
GuestOsRegistryService::Registration GetTerminalRegistration(
const base::Value* pref) {
std::string locale =
l10n_util::NormalizeLocale(g_browser_process->GetApplicationLocale());
vm_tools::apps::App app;
SetLocaleString(app.mutable_name(), locale,
l10n_util::GetStringUTF8(IDS_CROSTINI_TERMINAL_APP_NAME));
app.add_mime_types(
extensions::app_file_handler_util::kMimeTypeInodeDirectory);
SetLocaleStrings(
app.mutable_keywords(), locale,
{"linux", "terminal", "crostini",
l10n_util::GetStringUTF8(IDS_CROSTINI_TERMINAL_APP_SEARCH_TERMS)});
base::Value pref_registration =
pref ? pref->Clone() : base::Value(base::Value::Type::DICTIONARY);
PopulatePrefRegistrationFromApp(
pref_registration,
GuestOsRegistryService::VmType::ApplicationList_VmType_TERMINA,
crostini::kCrostiniDefaultVmName, crostini::kCrostiniDefaultContainerName,
app, ProtoToDictionary(app.name()));
return GuestOsRegistryService::Registration(
crostini::kCrostiniTerminalSystemAppId, std::move(pref_registration));
}
} // 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 pref_
.FindKeyOfType(guest_os::prefs::kAppDesktopFileIdKey,
base::Value::Type::STRING)
->GetString();
}
GuestOsRegistryService::VmType GuestOsRegistryService::Registration::VmType()
const {
absl::optional<int> vm_type =
pref_.FindIntKey(guest_os::prefs::kAppVmTypeKey);
// The VmType field is new, existing Apps that do not include it must be
// TERMINA Apps, as Plugin VM apps are not yet in production.
if (!vm_type) {
return GuestOsRegistryService::VmType::ApplicationList_VmType_TERMINA;
}
return static_cast<GuestOsRegistryService::VmType>(*vm_type);
}
std::string GuestOsRegistryService::Registration::VmName() const {
return pref_
.FindKeyOfType(guest_os::prefs::kAppVmNameKey, base::Value::Type::STRING)
->GetString();
}
std::string GuestOsRegistryService::Registration::ContainerName() const {
return pref_
.FindKeyOfType(guest_os::prefs::kAppContainerNameKey,
base::Value::Type::STRING)
->GetString();
}
std::string GuestOsRegistryService::Registration::Name() const {
if (VmType() ==
GuestOsRegistryService::VmType::ApplicationList_VmType_PLUGIN_VM) {
return l10n_util::GetStringFUTF8(
IDS_PLUGIN_VM_APP_NAME_WINDOWS_SUFFIX,
base::UTF8ToUTF16(LocalizedString(guest_os::prefs::kAppNameKey)));
}
return LocalizedString(guest_os::prefs::kAppNameKey);
}
std::string GuestOsRegistryService::Registration::Comment() const {
return LocalizedString(guest_os::prefs::kAppCommentKey);
}
std::string GuestOsRegistryService::Registration::Exec() const {
return pref_
.FindKeyOfType(guest_os::prefs::kAppExecKey, base::Value::Type::STRING)
->GetString();
}
std::string GuestOsRegistryService::Registration::ExecutableFileName() const {
if (pref_.is_none())
return std::string();
const base::Value* executable_file_name = pref_.FindKeyOfType(
guest_os::prefs::kAppExecutableFileNameKey, base::Value::Type::STRING);
if (!executable_file_name)
return std::string();
return executable_file_name->GetString();
}
std::set<std::string> GuestOsRegistryService::Registration::Extensions() const {
if (pref_.is_none())
return {};
return ListToStringSet(pref_.FindKeyOfType(guest_os::prefs::kAppExtensionsKey,
base::Value::Type::LIST));
}
std::set<std::string> GuestOsRegistryService::Registration::MimeTypes() const {
if (pref_.is_none())
return {};
return ListToStringSet(pref_.FindKeyOfType(guest_os::prefs::kAppMimeTypesKey,
base::Value::Type::LIST));
}
std::set<std::string> GuestOsRegistryService::Registration::Keywords() const {
return LocalizedList(guest_os::prefs::kAppKeywordsKey);
}
bool GuestOsRegistryService::Registration::NoDisplay() const {
if (pref_.is_none())
return false;
const base::Value* no_display = pref_.FindKeyOfType(
guest_os::prefs::kAppNoDisplayKey, base::Value::Type::BOOLEAN);
if (no_display)
return no_display->GetBool();
return false;
}
std::string GuestOsRegistryService::Registration::PackageId() const {
if (pref_.is_none())
return std::string();
const base::Value* package_id = pref_.FindKeyOfType(
guest_os::prefs::kAppPackageIdKey, base::Value::Type::STRING);
if (!package_id)
return std::string();
return package_id->GetString();
}
bool GuestOsRegistryService::Registration::CanUninstall() const {
if (pref_.is_none())
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 base::Value* package_id = pref_.FindKeyOfType(
guest_os::prefs::kAppPackageIdKey, base::Value::Type::STRING);
if (package_id)
return !package_id->GetString().empty();
return false;
}
base::Time GuestOsRegistryService::Registration::InstallTime() const {
return GetTime(pref_, guest_os::prefs::kAppInstallTimeKey);
}
base::Time GuestOsRegistryService::Registration::LastLaunchTime() const {
return GetTime(pref_, guest_os::prefs::kAppLastLaunchTimeKey);
}
bool GuestOsRegistryService::Registration::IsScaled() const {
if (pref_.is_none())
return false;
const base::Value* scaled = pref_.FindKeyOfType(
guest_os::prefs::kAppScaledKey, base::Value::Type::BOOLEAN);
if (!scaled)
return false;
return scaled->GetBool();
}
// 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::LocalizedString(
base::StringPiece key) const {
if (pref_.is_none())
return std::string();
const base::Value* dict =
pref_.FindKeyOfType(key, base::Value::Type::DICTIONARY);
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 base::Value* value =
dict->FindKeyOfType(locale, base::Value::Type::STRING);
if (value)
return value->GetString();
}
return std::string();
}
std::set<std::string> GuestOsRegistryService::Registration::LocalizedList(
base::StringPiece key) const {
if (pref_.is_none())
return {};
const base::Value* dict =
pref_.FindKeyOfType(key, base::Value::Type::DICTIONARY);
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* value =
dict->FindKeyOfType(locale, base::Value::Type::LIST);
if (value)
return ListToStringSet(value);
}
return {};
}
GuestOsRegistryService::GuestOsRegistryService(Profile* profile)
: profile_(profile),
prefs_(profile->GetPrefs()),
base_icon_path_(profile->GetPath().AppendASCII(kCrostiniIconFolder)),
clock_(base::DefaultClock::GetInstance()) {
RecordStartupMetrics();
MigrateTerminal();
}
GuestOsRegistryService::~GuestOsRegistryService() = default;
base::WeakPtr<GuestOsRegistryService> GuestOsRegistryService::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
std::map<std::string, GuestOsRegistryService::Registration>
GuestOsRegistryService::GetAllRegisteredApps() const {
const base::DictionaryValue* apps =
prefs_->GetDictionary(guest_os::prefs::kGuestOsRegistry);
std::map<std::string, GuestOsRegistryService::Registration> result;
// Register Terminal by merging optional prefs with app values.
// TODO(crbug.com/1028898): Register Terminal as a System App rather than a
// crostini app.
result.emplace(crostini::kCrostiniTerminalSystemAppId,
GetTerminalRegistration(
apps->FindKeyOfType(crostini::kCrostiniTerminalSystemAppId,
base::Value::Type::DICTIONARY)));
for (const auto item : apps->DictItems()) {
if (item.first != crostini::kCrostiniTerminalSystemAppId) {
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::ApplicationList_VmType_TERMINA:
enabled = crostini_enabled;
break;
case VmType::ApplicationList_VmType_PLUGIN_VM:
enabled = plugin_vm_enabled;
break;
case VmType::ApplicationList_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;
}
absl::optional<GuestOsRegistryService::Registration>
GuestOsRegistryService::GetRegistration(const std::string& app_id) const {
const base::DictionaryValue* apps =
prefs_->GetDictionary(guest_os::prefs::kGuestOsRegistry);
if (app_id == crostini::kCrostiniTerminalSystemAppId) {
return GetTerminalRegistration(apps->FindKeyOfType(
crostini::kCrostiniTerminalSystemAppId, base::Value::Type::DICTIONARY));
}
const base::Value* pref_registration =
apps->FindKeyOfType(app_id, base::Value::Type::DICTIONARY);
if (!pref_registration)
return absl::nullopt;
return absl::make_optional<Registration>(app_id, pref_registration->Clone());
}
void GuestOsRegistryService::RecordStartupMetrics() {
const base::DictionaryValue* apps =
prefs_->GetDictionary(guest_os::prefs::kGuestOsRegistry);
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;
int num_crostini_apps = 0;
int num_plugin_vm_apps = 0;
for (const auto item : apps->DictItems()) {
if (item.first == crostini::kCrostiniTerminalSystemAppId)
continue;
absl::optional<bool> no_display =
item.second.FindBoolKey(guest_os::prefs::kAppNoDisplayKey);
if (no_display && no_display.value())
continue;
absl::optional<int> vm_type =
item.second.FindIntKey(guest_os::prefs::kAppVmTypeKey);
if (!vm_type ||
vm_type ==
GuestOsRegistryService::VmType::ApplicationList_VmType_TERMINA) {
num_crostini_apps++;
} else if (vm_type == GuestOsRegistryService::VmType::
ApplicationList_VmType_PLUGIN_VM) {
num_plugin_vm_apps++;
} else {
NOTREACHED();
}
}
if (crostini_enabled)
UMA_HISTOGRAM_COUNTS_1000(kCrostiniAppsInstalledHistogram,
num_crostini_apps);
if (plugin_vm_enabled)
UMA_HISTOGRAM_COUNTS_1000(kPluginVmAppsInstalledHistogram,
num_plugin_vm_apps);
// TODO(b/166691285): borealis launch metrics.
}
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::SCALE_FACTOR_100P:
return app_path.AppendASCII("icon_100p.png");
case ui::SCALE_FACTOR_200P:
return app_path.AppendASCII("icon_200p.png");
case ui::SCALE_FACTOR_300P:
return app_path.AppendASCII("icon_300p.png");
default:
NOTREACHED();
return base::FilePath();
}
}
void GuestOsRegistryService::LoadIcon(
const std::string& app_id,
apps::mojom::IconKeyPtr icon_key,
apps::mojom::IconType icon_type,
int32_t size_hint_in_dip,
bool allow_placeholder_icon,
int fallback_icon_resource_id,
apps::mojom::Publisher::LoadIconCallback callback) {
if (icon_key) {
if (icon_key->resource_id != apps::mojom::IconKey::kInvalidResourceId) {
// The icon is a resource built into the Chrome OS binary.
constexpr bool is_placeholder_icon = false;
apps::LoadIconFromResource(
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;
} else {
// 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.
icon_key->icon_effects |= apps::IconEffects::kResizeAndPad;
auto scale_factor = apps_util::GetPrimaryDisplayUIScaleFactor();
// Try loading the icon from an on-disk cache. If that fails, fall back
// to LoadIconFromVM.
apps::LoadIconFromFileWithFallback(
icon_type, size_hint_in_dip, GetIconPath(app_id, scale_factor),
static_cast<apps::IconEffects>(icon_key->icon_effects),
std::move(callback),
base::BindOnce(&GuestOsRegistryService::LoadIconFromVM,
weak_ptr_factory_.GetWeakPtr(), app_id, icon_type,
size_hint_in_dip, scale_factor,
static_cast<apps::IconEffects>(icon_key->icon_effects),
fallback_icon_resource_id));
return;
}
}
// On failure, we still run the callback, with the zero IconValue.
std::move(callback).Run(apps::mojom::IconValue::New());
}
void GuestOsRegistryService::LoadIconFromVM(
const std::string& app_id,
apps::mojom::IconType icon_type,
int32_t size_hint_in_dip,
ui::ResourceScaleFactor scale_factor,
apps::IconEffects icon_effects,
int fallback_icon_resource_id,
apps::mojom::Publisher::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::mojom::IconType icon_type,
int32_t size_hint_in_dip,
apps::IconEffects icon_effects,
int fallback_icon_resource_id,
apps::mojom::Publisher::LoadIconCallback callback,
std::string compressed_icon_data) {
if (compressed_icon_data.empty()) {
if (fallback_icon_resource_id != apps::mojom::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(
icon_type, size_hint_in_dip, fallback_icon_resource_id,
/*is_placeholder_icon=*/false, icon_effects, std::move(callback));
} else {
std::move(callback).Run(apps::mojom::IconValue::New());
}
} 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 DictionaryPrefUpdate should be destructed before calling the observer.
{
DictionaryPrefUpdate update(prefs_, guest_os::prefs::kGuestOsRegistry);
base::DictionaryValue* apps = update.Get();
for (const auto item : apps->DictItems()) {
if (item.first == crostini::kCrostiniTerminalSystemAppId)
continue;
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->RemoveKey(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(1) << "Received ApplicationList : " << ToString(app_list);
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 DictionaryPrefUpdate should be destructed before calling the observer.
{
DictionaryPrefUpdate update(prefs_, guest_os::prefs::kGuestOsRegistry);
base::DictionaryValue* 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 name = ProtoToDictionary(app.name());
if (name.FindKey(base::StringPiece()) == 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 pref_registration(base::Value::Type::DICTIONARY);
PopulatePrefRegistrationFromApp(
pref_registration, app_list.vm_type(), app_list.vm_name(),
app_list.container_name(), app, std::move(name));
base::Value* old_app = apps->FindKey(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->FindKey(guest_os::prefs::kAppInstallTimeKey);
old_last_launch_time =
old_app->FindKey(guest_os::prefs::kAppLastLaunchTimeKey);
} else {
inserted_apps.push_back(app_id);
}
if (old_install_time)
pref_registration.SetKey(guest_os::prefs::kAppInstallTimeKey,
old_install_time->Clone());
else
SetCurrentTime(&pref_registration, guest_os::prefs::kAppInstallTimeKey);
if (old_last_launch_time) {
pref_registration.SetKey(guest_os::prefs::kAppLastLaunchTimeKey,
old_last_launch_time->Clone());
}
apps->SetKey(app_id, std::move(pref_registration));
}
for (const auto item : apps->DictItems()) {
if (item.first == crostini::kCrostiniTerminalSystemAppId)
continue;
if (item.second.FindKey(guest_os::prefs::kAppVmNameKey)->GetString() ==
app_list.vm_name() &&
item.second.FindKey(guest_os::prefs::kAppContainerNameKey)
->GetString() == 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->RemoveKey(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 (ui::ResourceScaleFactor 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, app_list.vm_type(), 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) {
DictionaryPrefUpdate update(prefs_, guest_os::prefs::kGuestOsRegistry);
base::DictionaryValue* apps = update.Get();
base::Value* app = apps->FindKey(app_id);
if (!app) {
DCHECK_EQ(app_id, crostini::kCrostiniTerminalSystemAppId);
base::Value pref(base::Value::Type::DICTIONARY);
SetCurrentTime(&pref, guest_os::prefs::kAppLastLaunchTimeKey);
apps->SetKey(app_id, std::move(pref));
return;
}
SetCurrentTime(app, guest_os::prefs::kAppLastLaunchTimeKey);
}
void GuestOsRegistryService::SetCurrentTime(base::Value* dictionary,
const char* key) const {
DCHECK(dictionary);
int64_t time = clock_->Now().ToDeltaSinceWindowsEpoch().InMicroseconds();
dictionary->SetKey(key, base::Value(base::NumberToString(time)));
}
void GuestOsRegistryService::SetAppScaled(const std::string& app_id,
bool scaled) {
DCHECK_NE(app_id, crostini::kCrostiniTerminalSystemAppId);
DictionaryPrefUpdate update(prefs_, guest_os::prefs::kGuestOsRegistry);
base::DictionaryValue* apps = update.Get();
base::Value* app = apps->FindKey(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->SetKey(guest_os::prefs::kAppScaledKey, base::Value(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.
absl::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::SCALE_FACTOR_200P:
icon_scale = 2;
break;
case ui::SCALE_FACTOR_300P:
icon_scale = 3;
break;
default:
break;
}
crostini::CrostiniManager::GetForProfile(profile_)->GetContainerAppIcons(
crostini::ContainerId(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::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);
} else if (icons.empty()) {
VLOG(1) << "No icon in container for app: " << app_id;
} else {
VLOG(1) << "Found icon in container for app: " << app_id;
// Now install the icon that we received.
const base::FilePath icon_path = GetIconPath(app_id, scale_factor);
base::ThreadPool::PostTask(
FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT},
base::BindOnce(&InstallIconFromFileThread, icon_path,
icons[0].content));
icon_content = std::move(icons[0].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::MigrateTerminal() const {
// Remove the old terminal from the registry.
DictionaryPrefUpdate update(profile_->GetPrefs(),
guest_os::prefs::kGuestOsRegistry);
base::DictionaryValue* apps = update.Get();
apps->RemoveKey(crostini::kCrostiniDeletedTerminalId);
// Transfer item attributes from old terminal to new, and delete old terminal
// once AppListSyncableService is initialized.
auto* app_list_syncable_service =
app_list::AppListSyncableServiceFactory::GetForProfile(profile_);
if (!app_list_syncable_service) {
return;
}
app_list_syncable_service->on_initialized().Post(
FROM_HERE,
base::BindOnce(
[](app_list::AppListSyncableService* service) {
if (service->GetSyncItem(crostini::kCrostiniDeletedTerminalId)) {
service->TransferItemAttributes(
crostini::kCrostiniDeletedTerminalId,
crostini::kCrostiniTerminalSystemAppId);
service->RemoveItem(crostini::kCrostiniDeletedTerminalId);
}
},
base::Unretained(app_list_syncable_service)));
}
} // namespace guest_os