blob: 0438330370197aea0a3b246c62a41a536c13b8a1 [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/chromeos/crostini/crostini_registry_service.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/crostini/crostini_util.h"
#include "chrome/browser/profiles/profile.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 "ui/base/l10n/l10n_util.h"
using vm_tools::apps::App;
namespace crostini {
namespace {
// Prefixes of the ApplicationId set on exo windows.
constexpr char kArcWindowAppIdPrefix[] = "org.chromium.arc";
constexpr char kCrostiniWindowAppIdPrefix[] = "org.chromium.termina.";
// This comes after kCrostiniWindowAppIdPrefix
constexpr char kWMClassPrefix[] = "wmclass.";
// This prefix is used when generating the crostini app list id, and used as a
// prefix when generating shelf ids for windows we couldn't match to an app.
constexpr char kCrostiniAppIdPrefix[] = "crostini:";
constexpr char kCrostiniRegistryPref[] = "crostini.registry";
// Keys for the Dictionary stored in prefs for each app.
constexpr char kAppDesktopFileIdKey[] = "desktop_file_id";
constexpr char kAppVmNameKey[] = "vm_name";
constexpr char kAppContainerNameKey[] = "container_name";
constexpr char kAppCommentKey[] = "comment";
constexpr char kAppMimeTypesKey[] = "mime_types";
constexpr char kAppNameKey[] = "name";
constexpr char kAppNoDisplayKey[] = "no_display";
constexpr char kAppStartupWMClassKey[] = "startup_wm_class";
constexpr char kAppStartupNotifyKey[] = "startup_notify";
std::string 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);
}
std::map<std::string, std::string> DictionaryToStringMap(
const base::Value* value) {
std::map<std::string, std::string> result;
for (const auto& item : value->DictItems())
result[item.first] = item.second.GetString();
return result;
}
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::vector<std::string> ListToStringVector(const base::Value* list) {
std::vector<std::string> result;
for (const base::Value& value : list->GetList())
result.emplace_back(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.GetList().emplace_back(string);
return result;
}
enum class FindAppIdResult { NoMatch, UniqueMatch, NonUniqueMatch };
// Looks for an app where prefs_key is set to search_value. Returns the apps id
// if there was only one app matching, otherwise returns an empty string.
FindAppIdResult FindAppId(const base::DictionaryValue* prefs,
base::StringPiece prefs_key,
base::StringPiece search_value,
std::string* result,
bool require_startup_notify = false) {
result->clear();
for (const auto& item : prefs->DictItems()) {
if (require_startup_notify &&
!item.second
.FindKeyOfType(kAppStartupNotifyKey, base::Value::Type::BOOLEAN)
->GetBool())
continue;
const std::string& value =
item.second.FindKeyOfType(prefs_key, base::Value::Type::STRING)
->GetString();
if (!EqualsCaseInsensitiveASCII(search_value, value))
continue;
if (!result->empty())
return FindAppIdResult::NonUniqueMatch;
*result = item.first;
}
if (!result->empty())
return FindAppIdResult::UniqueMatch;
return FindAppIdResult::NoMatch;
}
} // namespace
CrostiniRegistryService::Registration::Registration(
const std::string& desktop_file_id,
const std::string& vm_name,
const std::string& container_name,
const LocaleString& name,
const LocaleString& comment,
const std::vector<std::string>& mime_types,
bool no_display,
const std::string& startup_wm_class,
bool startup_notify)
: desktop_file_id(desktop_file_id),
vm_name(vm_name),
container_name(container_name),
name(name),
comment(comment),
mime_types(mime_types),
no_display(no_display),
startup_wm_class(startup_wm_class),
startup_notify(startup_notify) {
DCHECK(name.find(std::string()) != name.end());
}
CrostiniRegistryService::Registration::~Registration() = default;
// static
const std::string& CrostiniRegistryService::Registration::Localize(
const LocaleString& locale_string) {
std::string current_locale =
l10n_util::NormalizeLocale(g_browser_process->GetApplicationLocale());
std::vector<std::string> locales;
l10n_util::GetParentLocales(current_locale, &locales);
for (const std::string& locale : locales) {
LocaleString::const_iterator it = locale_string.find(locale);
if (it != locale_string.end())
return it->second;
}
return locale_string.at(std::string());
}
CrostiniRegistryService::CrostiniRegistryService(Profile* profile)
: prefs_(profile->GetPrefs()) {}
CrostiniRegistryService::~CrostiniRegistryService() = default;
// The code follows these steps to identify apps and returns the first match:
// 1) Ignore windows if the App Id is prefixed by org.chromium.arc.
// 2) If the Startup Id is set, look for a matching desktop file id.
// 3) If the App Id is not prefixed by org.chromium.termina., it's an app with
// native Wayland support. Look for a matching desktop file id.
// 4) If the App Id is prefixed by org.chromium.wmclass.:
// 4.1) Look for an app where StartupWMClass is matches the suffix.
// 4.2) Look for an app where the desktop file id matches the suffix.
// 5) If we couldn't find a match, prefix the app id with 'crostini:' so we can
// easily identify shelf entries as Crostini apps.
std::string CrostiniRegistryService::GetCrostiniShelfAppId(
const std::string& window_app_id,
const std::string* window_startup_id) {
if (base::StartsWith(window_app_id, kArcWindowAppIdPrefix,
base::CompareCase::SENSITIVE))
return std::string();
const base::DictionaryValue* apps =
prefs_->GetDictionary(kCrostiniRegistryPref);
std::string app_id;
if (window_startup_id) {
// TODO(timloh): We should use a value that is unique so we can handle
// an app installed in multiple containers.
if (FindAppId(apps, kAppDesktopFileIdKey, *window_startup_id, &app_id,
true) == FindAppIdResult::UniqueMatch)
return app_id;
LOG(ERROR) << "Startup ID was set to '" << *window_startup_id
<< "' but not matched";
// Try a lookup with the window app id.
}
// Wayland apps won't be prefixed with org.chromium.termina.
if (!base::StartsWith(window_app_id, kCrostiniWindowAppIdPrefix,
base::CompareCase::SENSITIVE)) {
if (FindAppId(apps, kAppDesktopFileIdKey, window_app_id, &app_id) ==
FindAppIdResult::UniqueMatch)
return app_id;
return kCrostiniAppIdPrefix + window_app_id;
}
base::StringPiece suffix(
window_app_id.begin() + strlen(kCrostiniWindowAppIdPrefix),
window_app_id.end());
// If we don't have an id to match to a desktop file, use the window app id.
if (!base::StartsWith(suffix, kWMClassPrefix, base::CompareCase::SENSITIVE))
return kCrostiniAppIdPrefix + window_app_id;
// If an app had StartupWMClass set to the given WM class, use that,
// otherwise look for a desktop file id matching the WM class.
base::StringPiece key = suffix.substr(strlen(kWMClassPrefix));
FindAppIdResult result = FindAppId(apps, kAppStartupWMClassKey, key, &app_id);
if (result == FindAppIdResult::UniqueMatch)
return app_id;
if (result == FindAppIdResult::NonUniqueMatch)
return kCrostiniAppIdPrefix + window_app_id;
if (FindAppId(apps, kAppDesktopFileIdKey, key, &app_id) ==
FindAppIdResult::UniqueMatch)
return app_id;
return kCrostiniAppIdPrefix + window_app_id;
}
bool CrostiniRegistryService::IsCrostiniShelfAppId(
const std::string& shelf_app_id) {
if (base::StartsWith(shelf_app_id, kCrostiniAppIdPrefix,
base::CompareCase::SENSITIVE))
return true;
if (shelf_app_id == kCrostiniTerminalId)
return true;
// TODO(timloh): We need to handle desktop files that have been removed.
// For example, running windows with a no-longer-valid app id will try to
// use the ExtensionContextMenuModel.
return prefs_->GetDictionary(kCrostiniRegistryPref)->FindKey(shelf_app_id) !=
nullptr;
}
std::vector<std::string> CrostiniRegistryService::GetRegisteredAppIds() const {
const base::DictionaryValue* apps =
prefs_->GetDictionary(kCrostiniRegistryPref);
std::vector<std::string> result;
for (const auto& item : apps->DictItems())
result.push_back(item.first);
return result;
}
std::unique_ptr<CrostiniRegistryService::Registration>
CrostiniRegistryService::GetRegistration(const std::string& app_id) const {
const base::DictionaryValue* apps =
prefs_->GetDictionary(kCrostiniRegistryPref);
const base::Value* pref_registration =
apps->FindKeyOfType(app_id, base::Value::Type::DICTIONARY);
if (!pref_registration)
return nullptr;
const base::Value* desktop_file_id = pref_registration->FindKeyOfType(
kAppDesktopFileIdKey, base::Value::Type::STRING);
const base::Value* vm_name = pref_registration->FindKeyOfType(
kAppVmNameKey, base::Value::Type::STRING);
const base::Value* container_name = pref_registration->FindKeyOfType(
kAppContainerNameKey, base::Value::Type::STRING);
const base::Value* name = pref_registration->FindKeyOfType(
kAppNameKey, base::Value::Type::DICTIONARY);
const base::Value* comment = pref_registration->FindKeyOfType(
kAppCommentKey, base::Value::Type::DICTIONARY);
const base::Value* mime_types = pref_registration->FindKeyOfType(
kAppMimeTypesKey, base::Value::Type::LIST);
const base::Value* no_display = pref_registration->FindKeyOfType(
kAppNoDisplayKey, base::Value::Type::BOOLEAN);
const base::Value* startup_wm_class = pref_registration->FindKeyOfType(
kAppStartupWMClassKey, base::Value::Type::STRING);
const base::Value* startup_notify = pref_registration->FindKeyOfType(
kAppStartupNotifyKey, base::Value::Type::BOOLEAN);
return std::make_unique<Registration>(
desktop_file_id->GetString(), vm_name->GetString(),
container_name->GetString(), DictionaryToStringMap(name),
DictionaryToStringMap(comment), ListToStringVector(mime_types),
no_display->GetBool(),
startup_wm_class ? startup_wm_class->GetString() : "",
startup_notify && startup_notify->GetBool());
}
void CrostiniRegistryService::UpdateApplicationList(
const vm_tools::apps::ApplicationList& 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_, kCrostiniRegistryPref);
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);
pref_registration.SetKey(kAppDesktopFileIdKey,
base::Value(app.desktop_file_id()));
pref_registration.SetKey(kAppVmNameKey, base::Value(app_list.vm_name()));
pref_registration.SetKey(kAppContainerNameKey,
base::Value(app_list.container_name()));
pref_registration.SetKey(kAppNameKey, std::move(name));
pref_registration.SetKey(kAppCommentKey,
ProtoToDictionary(app.comment()));
pref_registration.SetKey(kAppMimeTypesKey, ProtoToList(app.mime_types()));
pref_registration.SetKey(kAppNoDisplayKey, base::Value(app.no_display()));
pref_registration.SetKey(kAppStartupWMClassKey,
base::Value(app.startup_wm_class()));
pref_registration.SetKey(kAppStartupNotifyKey,
base::Value(app.startup_notify()));
base::Value* old_app = apps->FindKey(app_id);
if (old_app && pref_registration == *old_app)
continue;
if (old_app)
updated_apps.push_back(app_id);
else
inserted_apps.push_back(app_id);
apps->SetKey(app_id, std::move(pref_registration));
}
for (const auto& item : apps->DictItems()) {
if (item.second.FindKey(kAppVmNameKey)->GetString() ==
app_list.vm_name() &&
item.second.FindKey(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)
apps->RemoveKey(removed_app);
}
for (Observer& obs : observers_)
obs.OnRegistryUpdated(this, updated_apps, removed_apps, inserted_apps);
}
void CrostiniRegistryService::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void CrostiniRegistryService::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
// static
void CrostiniRegistryService::RegisterProfilePrefs(
PrefRegistrySimple* registry) {
registry->RegisterDictionaryPref(kCrostiniRegistryPref);
}
} // namespace crostini