| // Copyright 2020 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_shelf_utils.h" |
| |
| #include <string_view> |
| |
| #include "base/logging.h" |
| #include "base/no_destructor.h" |
| #include "base/strings/string_util.h" |
| #include "chrome/browser/ash/borealis/borealis_window_manager.h" |
| #include "chrome/browser/ash/guest_os/guest_id.h" |
| #include "chrome/browser/ash/guest_os/guest_os_pref_names.h" |
| #include "chrome/browser/ash/guest_os/guest_os_session_tracker.h" |
| #include "chrome/browser/ash/guest_os/public/types.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/services/app_service/public/cpp/app_types.h" |
| |
| namespace guest_os { |
| |
| namespace { |
| |
| // This prefix is used as a prefix when generating shelf ids for windows we |
| // couldn't match to an app. It is also used for crostini web dialogs (e.g. |
| // crostini installer/upgrader) which need to appear in the shelf. |
| // |
| // Note: if the value is changed, you will also need to manually update |
| // kCrostiniInstallerShelfId and kCrostiniUpgraderShelfId. |
| constexpr char kCrostiniShelfIdPrefix[] = "crostini:"; |
| |
| // Prefix of the WindowAppId set on exo windows for GuestOS X apps. |
| constexpr char kGuestOsWindowAppIdPrefix[] = "org.chromium.guest_os."; |
| // This comes after kGuestOsWindowAppIdPrefix+token for GuestOS Wayland apps. |
| constexpr char kWaylandPrefix[] = "wayland."; |
| // This comes after kGuestOsWindowAppIdPrefix+token. |
| constexpr char kWmClassPrefix[] = "wmclass."; |
| |
| // TODO(b/267377562): Borealis windows have a hardcoded "borealis" token. |
| constexpr char kBorealisToken[] = "borealis"; |
| |
| const std::string* GetAppNameForWMClass(std::string_view wmclass) { |
| // A hard-coded mapping from WMClass to app names. |
| // This is used to deal with the Linux apps that don't specify the correct |
| // WMClass in their desktop files so that their aura windows can be identified |
| // with their respective app IDs. |
| static const base::NoDestructor<std::map<std::string, std::string>> |
| kWMClassToNname({{"Octave-gui", "GNU Octave"}, |
| {"MuseScore2", "MuseScore 2"}, |
| {"XnViewMP", "XnView Multi Platform"}}); |
| const auto it = kWMClassToNname->find(std::string(wmclass)); |
| if (it == kWMClassToNname->end()) |
| return nullptr; |
| return &it->second; |
| } |
| |
| bool MatchingString(const std::string& search_string, |
| const std::string& value_string, |
| bool ignore_space) { |
| std::string search = search_string; |
| std::string value = value_string; |
| if (ignore_space) { |
| base::RemoveChars(search, " ", &search); |
| base::RemoveChars(value, " ", &value); |
| } |
| return base::EqualsCaseInsensitiveASCII(search, value); |
| } |
| |
| 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::Value::Dict& prefs, |
| std::string_view prefs_key, |
| std::string_view search_value, |
| const std::optional<GuestId>& guest_id, |
| std::string* result, |
| bool require_startup_notify = false, |
| bool need_display = false, |
| bool ignore_space = false) { |
| result->clear(); |
| for (const auto item : prefs) { |
| if (require_startup_notify && !*item.second.GetDict().FindBool( |
| guest_os::prefs::kAppStartupNotifyKey)) { |
| continue; |
| } |
| |
| if (need_display) { |
| const std::optional<bool> no_display = |
| item.second.GetDict().FindBool(guest_os::prefs::kAppNoDisplayKey); |
| if (no_display && *no_display) { |
| continue; |
| } |
| } |
| |
| // If guest_id is provided, also check that it matches. The guest_id is |
| // considered matched if its vm_name and container_name matches |
| // corresponding entries in the dictionary. |
| if (guest_id && !MatchContainerDict(item.second, *guest_id)) { |
| continue; |
| } |
| |
| const base::Value* value = item.second.GetDict().Find(prefs_key); |
| if (!value) |
| continue; |
| if (value->is_string()) { |
| if (!MatchingString(std::string(search_value), value->GetString(), |
| ignore_space)) { |
| continue; |
| } |
| } else if (value->is_dict()) { |
| // Look at the unlocalized name to see if that matches. |
| const std::string* str_value = value->GetDict().FindString(""); |
| if (!str_value || !MatchingString(std::string(search_value), *str_value, |
| ignore_space)) { |
| continue; |
| } |
| } else { |
| continue; |
| } |
| |
| if (!result->empty()) |
| return FindAppIdResult::NonUniqueMatch; |
| *result = item.first; |
| } |
| |
| if (!result->empty()) |
| return FindAppIdResult::UniqueMatch; |
| return FindAppIdResult::NoMatch; |
| } |
| |
| // For GuestOS |window_app_id|s which match the prefix of |
| // org.chromium.guest_os.<token>.*, return the guest token. |
| // The token should be one of the following: |
| // - For Crostini app windows: it is the container_token |
| // - For Bruschetta app windows: it is the container_token |
| // - For Borealis app windows: "borealis" |
| // - For all other guest app windows: "termina" |
| // Note that PluginVM does not match this prefix since it has a |
| // hard-coded window_app_id. |
| std::string GetGuestTokenForWindowId(const std::string* window_app_id) { |
| if (!window_app_id || |
| !base::StartsWith(*window_app_id, kGuestOsWindowAppIdPrefix, |
| base::CompareCase::SENSITIVE)) { |
| return std::string(); |
| } |
| const auto token_start = strlen(kGuestOsWindowAppIdPrefix); |
| // Find the first "." after the kGuestOsWindowAppIdPrefix |
| const auto token_end = window_app_id->find(".", token_start); |
| |
| auto token = window_app_id->substr(token_start, token_end - token_start); |
| |
| return token; |
| } |
| |
| std::string GetUnregisteredAppIdPrefix(const std::optional<std::string> token) { |
| if (token == kBorealisToken) { |
| return borealis::kBorealisAnonymousPrefix; |
| } |
| |
| // TODO(b/244651040): We should support other VMs, e.g. bruschetta. |
| // For all other unregistered apps, default to "crostini:". |
| return kCrostiniShelfIdPrefix; |
| } |
| |
| } // namespace |
| |
| // The code follows these steps to identify apps and returns the first match: |
| // 1) If the |window_startup_id| is set, look for a matching desktop file id. |
| // 2) Ignore windows if the |window_app_id| is not set. |
| // 3) The |window_app_id| is prefixed by org.chromium.guest_os.<token>., so we |
| // should be able to obtain a guest token from it. This will be used to find |
| // a guest_id to which the app window belongs to. In the following steps, the |
| // container_name and vm_name from the guest_id will be used to find a unique |
| // match if available. |
| // 4) Remove the org.chromium.guest_os.<token>. prefix and use the remaining |
| // string (the suffix) for the next steps. |
| // 5) If the suffix is prefixed by wayland., it's a native Wayland app. Look for |
| // a matching desktop file id. |
| // 6) If the suffix from step 4 is prefixed by wmclass.: |
| // 6.1) Look for an app where StartupWMClass matches the remaining string. |
| // 6.2) Look for an app where the desktop file id matches the remaining string. |
| // 6.3) Look for an app where the unlocalized name matches the remaining |
| // string. This handles the xterm & uxterm examples. |
| // 7) If we couldn't find a match, prefix the |window_app_id| with a generic |
| // prefix of 'crostini:' or 'borealis:"', so we can easily identify |
| // shelf entries as GuestOs apps. If we could not identify the VM, default |
| // to using "crostini:". |
| std::string GetGuestOsShelfAppId(Profile* profile, |
| const std::string* window_app_id, |
| const std::string* window_startup_id) { |
| if (!profile || !profile->GetPrefs()) |
| return std::string(); |
| |
| const base::Value::Dict& apps = |
| profile->GetPrefs()->GetDict(guest_os::prefs::kGuestOsRegistry); |
| |
| // TODO(b/244651040): Consider moving the borealis GetBorealisAppId logic |
| // here. |
| std::string app_id; |
| |
| std::string token = GetGuestTokenForWindowId(window_app_id); |
| std::optional<GuestId> guest_id = |
| GuestOsSessionTracker::GetForProfile(profile)->GetGuestIdForToken(token); |
| |
| if (window_startup_id) { |
| if (FindAppId(apps, guest_os::prefs::kAppDesktopFileIdKey, |
| *window_startup_id, guest_id, &app_id, |
| true) == FindAppIdResult::UniqueMatch) { |
| return app_id; |
| } |
| LOG(WARNING) << "Startup ID was set to '" << *window_startup_id |
| << "' but not matched. Will attempt to match with window ID."; |
| } |
| |
| if (!window_app_id) { |
| return std::string(); |
| } |
| |
| // If the window_id does not follow the expected format, return a generic id. |
| if (!base::StartsWith(*window_app_id, kGuestOsWindowAppIdPrefix, |
| base::CompareCase::SENSITIVE)) { |
| LOG(ERROR) << "window_app_id:" << *window_app_id |
| << " provided is not prefixed with " |
| << kGuestOsWindowAppIdPrefix; |
| return GetUnregisteredAppIdPrefix(token) + *window_app_id; |
| } |
| |
| // Get the suffix by stripping "org.chromium.guest_os.<token>.". |
| // token.length() + 1 is used since the '.' separator was not included in the |
| // token. |
| std::string_view suffix = base::MakeStringPiece( |
| window_app_id->begin() + strlen(kGuestOsWindowAppIdPrefix) + |
| token.length() + 1, |
| window_app_id->end()); |
| |
| // Wayland apps will have a "wayland." identifier. |
| if (base::StartsWith(suffix, kWaylandPrefix, base::CompareCase::SENSITIVE)) { |
| const std::string_view wayland_app = suffix.substr(strlen(kWaylandPrefix)); |
| if (FindAppId(apps, guest_os::prefs::kAppDesktopFileIdKey, wayland_app, |
| guest_id, &app_id) == FindAppIdResult::UniqueMatch) { |
| return app_id; |
| } |
| return GetUnregisteredAppIdPrefix(token) + *window_app_id; |
| } |
| |
| // 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 GetUnregisteredAppIdPrefix(token) + *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. |
| std::string_view key = suffix.substr(strlen(kWmClassPrefix)); |
| FindAppIdResult result = FindAppId( |
| apps, guest_os::prefs::kAppStartupWMClassKey, key, guest_id, &app_id, |
| false /* require_startup_notification */, true /* need_display */); |
| if (result == FindAppIdResult::UniqueMatch) |
| return app_id; |
| if (result == FindAppIdResult::NonUniqueMatch) |
| return GetUnregisteredAppIdPrefix(token) + *window_app_id; |
| |
| if (FindAppId(apps, guest_os::prefs::kAppDesktopFileIdKey, key, guest_id, |
| &app_id) == FindAppIdResult::UniqueMatch) { |
| return app_id; |
| } |
| |
| if (FindAppId(apps, guest_os::prefs::kAppNameKey, key, guest_id, &app_id, |
| false /* require_startup_notification */, |
| true /* need_display */, |
| true /* ignore_space */) == FindAppIdResult::UniqueMatch) { |
| return app_id; |
| } |
| |
| const std::string* app_name = GetAppNameForWMClass(key); |
| if (app_name && |
| FindAppId(apps, guest_os::prefs::kAppNameKey, *app_name, guest_id, |
| &app_id, false /* require_startup_notification */, |
| true /* need_display */) == FindAppIdResult::UniqueMatch) { |
| return app_id; |
| } |
| |
| return GetUnregisteredAppIdPrefix(token) + *window_app_id; |
| } |
| |
| bool IsUnregisteredCrostiniShelfAppId(std::string_view shelf_app_id) { |
| return base::StartsWith(shelf_app_id, kCrostiniShelfIdPrefix, |
| base::CompareCase::SENSITIVE); |
| } |
| |
| bool IsUnregisteredGuestOsShelfAppId(std::string_view shelf_app_id) { |
| return IsUnregisteredCrostiniShelfAppId(shelf_app_id) || |
| base::StartsWith(shelf_app_id, borealis::kBorealisAnonymousPrefix, |
| base::CompareCase::SENSITIVE); |
| } |
| |
| bool IsCrostiniShelfAppId(const Profile* profile, |
| std::string_view shelf_app_id) { |
| if (IsUnregisteredCrostiniShelfAppId(shelf_app_id)) { |
| return true; |
| } |
| |
| if (!profile || !profile->GetPrefs()) { |
| return false; |
| } |
| // 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. |
| const auto& apps = |
| profile->GetPrefs()->GetDict(guest_os::prefs::kGuestOsRegistry); |
| return apps.contains(shelf_app_id); |
| } |
| |
| apps::AppType GetAppType(Profile* profile, std::string_view shelf_app_id) { |
| if (shelf_app_id.starts_with(kCrostiniShelfIdPrefix)) { |
| shelf_app_id.remove_prefix(strlen(kCrostiniShelfIdPrefix)); |
| } |
| const std::string id(shelf_app_id); |
| const std::string token = GetGuestTokenForWindowId(&id); |
| std::optional<GuestId> guest_id = |
| GuestOsSessionTracker::GetForProfile(profile)->GetGuestIdForToken(token); |
| if (guest_id.has_value()) { |
| return ToAppType(guest_id->vm_type); |
| } |
| return ToAppType(vm_tools::apps::UNKNOWN); |
| } |
| |
| } // namespace guest_os |