| // Copyright 2021 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/lacros/lacros_extension_apps_publisher.h" |
| |
| #include <utility> |
| |
| #include "base/check.h" |
| #include "base/containers/extend.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/scoped_observation.h" |
| #include "chrome/browser/apps/app_service/app_icon/app_icon_factory.h" |
| #include "chrome/browser/apps/app_service/extension_apps_utils.h" |
| #include "chrome/browser/apps/app_service/intent_util.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/extensions/extension_ui_util.h" |
| #include "chrome/browser/extensions/launch_util.h" |
| #include "chrome/browser/lacros/lacros_extensions_util.h" |
| #include "chrome/browser/profiles/profile_manager.h" |
| #include "chrome/browser/ui/lacros/window_utility.h" |
| #include "chromeos/crosapi/mojom/app_window_tracker.mojom.h" |
| #include "chromeos/lacros/lacros_service.h" |
| #include "components/services/app_service/public/cpp/app_types.h" |
| #include "components/services/app_service/public/cpp/icon_types.h" |
| #include "components/services/app_service/public/cpp/intent_filter.h" |
| #include "extensions/browser/app_window/app_window.h" |
| #include "extensions/browser/app_window/app_window_registry.h" |
| #include "extensions/browser/extension_prefs.h" |
| #include "extensions/browser/extension_prefs_observer.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_registry_observer.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/management_policy.h" |
| #include "extensions/browser/unloaded_extension_reason.h" |
| #include "extensions/common/constants.h" |
| #include "extensions/common/manifest_handlers/app_display_info.h" |
| |
| namespace { |
| |
| apps::InstallReason GetInstallReason(const extensions::Extension* extension) { |
| if (extensions::Manifest::IsComponentLocation(extension->location())) |
| return apps::InstallReason::kSystem; |
| |
| if (extensions::Manifest::IsPolicyLocation(extension->location())) |
| return apps::InstallReason::kPolicy; |
| |
| if (extension->was_installed_by_oem()) |
| return apps::InstallReason::kOem; |
| |
| if (extension->was_installed_by_default()) |
| return apps::InstallReason::kDefault; |
| |
| return apps::InstallReason::kUser; |
| } |
| |
| } // namespace |
| |
| // This class tracks all extension apps associated with a given Profile*. The |
| // observation of ExtensionPrefsObserver and ExtensionRegistryObserver is used |
| // to track AppService publisher events. The observation of AppsWindowRegistry |
| // is used to track window creation and destruction. |
| class LacrosExtensionAppsPublisher::ProfileTracker |
| : public extensions::ExtensionPrefsObserver, |
| public extensions::ExtensionRegistryObserver, |
| public extensions::AppWindowRegistry::Observer { |
| using Readiness = apps::Readiness; |
| |
| public: |
| ProfileTracker(Profile* profile, |
| LacrosExtensionAppsPublisher* publisher, |
| const ForWhichExtensionType& which_type) |
| : profile_(profile), publisher_(publisher), which_type_(which_type) { |
| // Start observing for relevant events. |
| prefs_observation_.Observe(extensions::ExtensionPrefs::Get(profile_)); |
| registry_observation_.Observe(extensions::ExtensionRegistry::Get(profile_)); |
| app_window_registry_observation_.Observe( |
| extensions::AppWindowRegistry::Get(profile_)); |
| |
| // Populate initial conditions [e.g. installed apps prior to starting |
| // observation]. |
| std::vector<apps::AppPtr> apps; |
| extensions::ExtensionRegistry* registry = |
| extensions::ExtensionRegistry::Get(profile_); |
| for (const scoped_refptr<const extensions::Extension> extension : |
| registry->enabled_extensions()) { |
| if (which_type_.Matches(extension.get())) { |
| apps.push_back(MakeApp(extension.get(), Readiness::kReady)); |
| } |
| } |
| for (const scoped_refptr<const extensions::Extension> extension : |
| registry->disabled_extensions()) { |
| if (which_type_.Matches(extension.get())) { |
| apps.push_back(MakeApp(extension.get(), Readiness::kDisabledByUser)); |
| } |
| } |
| for (const scoped_refptr<const extensions::Extension> extension : |
| registry->terminated_extensions()) { |
| if (which_type_.Matches(extension.get())) { |
| apps.push_back(MakeApp(extension.get(), Readiness::kTerminated)); |
| } |
| } |
| if (!apps.empty()) |
| Publish(std::move(apps)); |
| |
| if (which_type_.IsChromeApps()) { |
| // Populate initial conditions [e.g. app windows created prior to starting |
| // observation]. |
| for (extensions::AppWindow* app_window : |
| extensions::AppWindowRegistry::Get(profile_)->app_windows()) { |
| OnAppWindowAdded(app_window); |
| } |
| } |
| } |
| |
| void Publish(const extensions::Extension* extension, Readiness readiness) { |
| Publish(MakeApp(extension, readiness)); |
| } |
| |
| ~ProfileTracker() override = default; |
| |
| private: |
| // extensions::ExtensionPrefsObserver overrides. |
| void OnExtensionLastLaunchTimeChanged( |
| const std::string& app_id, |
| const base::Time& last_launch_time) override { |
| const auto* extension = |
| lacros_extensions_util::MaybeGetExtension(profile_, app_id); |
| if (!extension || !which_type_.Matches(extension)) |
| return; |
| |
| Publish(MakeApp(extension, Readiness::kReady)); |
| } |
| |
| void OnExtensionPrefsWillBeDestroyed( |
| extensions::ExtensionPrefs* prefs) override { |
| DCHECK(prefs_observation_.IsObservingSource(prefs)); |
| prefs_observation_.Reset(); |
| } |
| |
| // extensions::ExtensionRegistryObserver overrides. |
| void OnExtensionLoaded(content::BrowserContext* browser_context, |
| const extensions::Extension* extension) override { |
| if (!which_type_.Matches(extension)) |
| return; |
| Publish(MakeApp(extension, Readiness::kReady)); |
| } |
| |
| void OnExtensionUnloaded( |
| content::BrowserContext* browser_context, |
| const extensions::Extension* extension, |
| extensions::UnloadedExtensionReason reason) override { |
| if (!which_type_.Matches(extension)) |
| return; |
| |
| Readiness readiness = Readiness::kUnknown; |
| |
| switch (reason) { |
| case extensions::UnloadedExtensionReason::DISABLE: |
| readiness = Readiness::kDisabledByUser; |
| break; |
| case extensions::UnloadedExtensionReason::BLOCKLIST: |
| readiness = Readiness::kDisabledByBlocklist; |
| break; |
| case extensions::UnloadedExtensionReason::TERMINATE: |
| readiness = Readiness::kTerminated; |
| break; |
| case extensions::UnloadedExtensionReason::UNINSTALL: |
| // App readiness will be updated by OnExtensionUninstalled(). We defer |
| // to that method. |
| return; |
| case extensions::UnloadedExtensionReason::UNDEFINED: |
| case extensions::UnloadedExtensionReason::UPDATE: |
| case extensions::UnloadedExtensionReason::PROFILE_SHUTDOWN: |
| case extensions::UnloadedExtensionReason::LOCK_ALL: |
| case extensions::UnloadedExtensionReason::MIGRATED_TO_COMPONENT: |
| return; |
| } |
| Publish(MakeApp(extension, readiness)); |
| } |
| |
| void OnExtensionInstalled(content::BrowserContext* browser_context, |
| const extensions::Extension* extension, |
| bool is_update) override { |
| if (!which_type_.Matches(extension)) |
| return; |
| Publish(MakeApp(extension, Readiness::kReady)); |
| } |
| |
| void OnExtensionUninstalled(content::BrowserContext* browser_context, |
| const extensions::Extension* extension, |
| extensions::UninstallReason reason) override { |
| if (!which_type_.Matches(extension)) |
| return; |
| apps::AppPtr app = |
| MakeApp(extension, reason == extensions::UNINSTALL_REASON_MIGRATED |
| ? Readiness::kUninstalledByMigration |
| : Readiness::kUninstalledByUser); |
| Publish(std::move(app)); |
| } |
| |
| void OnShutdown(extensions::ExtensionRegistry* registry) override { |
| registry_observation_.Reset(); |
| } |
| |
| // AppWindowRegistry::Observer overrides. |
| void OnAppWindowAdded(extensions::AppWindow* app_window) override { |
| // Only chrome app windows are added to the dock. |
| if (!which_type_.IsChromeApps()) |
| return; |
| // The extension also has to match. |
| if (!which_type_.Matches(app_window->GetExtension())) |
| return; |
| std::string muxed_id = |
| lacros_extensions_util::MuxId(profile_, app_window->GetExtension()); |
| std::string window_id = lacros_window_utility::GetRootWindowUniqueId( |
| app_window->GetNativeWindow()); |
| app_window_id_cache_[app_window] = window_id; |
| |
| publisher_->OnAppWindowAdded(muxed_id, window_id); |
| } |
| |
| void OnAppWindowRemoved(extensions::AppWindow* app_window) override { |
| // Only chrome app windows are added to the dock. |
| if (!which_type_.IsChromeApps()) |
| return; |
| // The extension also has to match. As the extension may be destroyed at |
| // this point, we use presence in app_window_id_cache_ to decide whether to |
| // continue. |
| auto it = app_window_id_cache_.find(app_window); |
| if (it == app_window_id_cache_.end()) |
| return; |
| |
| std::string muxed_id = apps::MuxId(profile_, app_window->extension_id()); |
| std::string window_id = it->second; |
| publisher_->OnAppWindowRemoved(muxed_id, window_id); |
| |
| app_window_id_cache_.erase(app_window); |
| } |
| |
| // Publishes a differential update to the app service. |
| void Publish(apps::AppPtr app) { |
| std::vector<apps::AppPtr> apps; |
| apps.push_back(std::move(app)); |
| Publish(std::move(apps)); |
| } |
| |
| // Publishes a vector of differential updates to the app service. |
| void Publish(std::vector<apps::AppPtr> apps) { |
| publisher_->Publish(std::move(apps)); |
| } |
| |
| // Whether the app should be shown in the launcher, shelf, etc. |
| bool ShouldShow(const extensions::Extension* extension) { |
| if (which_type_.IsExtensions()) |
| return false; |
| extensions::ExtensionRegistry* registry = |
| extensions::ExtensionRegistry::Get(profile_); |
| const std::string& app_id = extension->id(); |
| // These three extension sets are the same three consulted by the |
| // constructor. Importantly, it will exclude previously installed but |
| // currently uninstalled extensions. |
| bool connected = registry->enabled_extensions().Contains(app_id) || |
| registry->disabled_extensions().Contains(app_id) || |
| registry->terminated_extensions().Contains(app_id); |
| if (!connected) |
| return false; |
| |
| return extensions::ui_util::ShouldDisplayInAppLauncher(extension, profile_); |
| } |
| |
| // Creates an AppPtr from an extension. |
| apps::AppPtr MakeApp(const extensions::Extension* extension, |
| Readiness readiness) { |
| DCHECK(which_type_.Matches(extension)); |
| apps::AppType app_type = which_type_.ChooseForChromeAppOrExtension( |
| apps::AppType::kStandaloneBrowserChromeApp, |
| apps::AppType::kStandaloneBrowserExtension); |
| auto app = std::make_unique<apps::App>( |
| app_type, lacros_extensions_util::MuxId(profile_, extension)); |
| app->readiness = readiness; |
| app->name = extension->name(); |
| app->short_name = extension->short_name(); |
| |
| // TODO(crbug.com/1367337): Work out how pinning interacts with Lacros |
| // multi-profile support once there is a product decision on what that looks |
| // like. |
| app->policy_ids = {extension->id()}; |
| |
| // We always use an empty icon key since we currently do not support |
| // dynamically changing icons or modifying the appearance of icons. |
| // This bug is tracked at https://crbug.com/1248499, but given that Chrome |
| // Apps is deprecated, it's unclear if we'll ever get around to implementing |
| // this functionality. |
| app->icon_key = |
| apps::IconKey(/*timeline=*/0, apps::IconKey::kInvalidResourceId, |
| apps::IconEffects::kCrOsStandardIcon); |
| |
| auto* prefs = extensions::ExtensionPrefs::Get(profile_); |
| if (prefs) { |
| app->last_launch_time = prefs->GetLastLaunchTime(extension->id()); |
| app->install_time = prefs->GetLastUpdateTime(extension->id()); |
| } else { |
| app->last_launch_time = base::Time(); |
| app->install_time = base::Time(); |
| } |
| |
| app->install_reason = GetInstallReason(extension); |
| app->recommendable = true; |
| app->searchable = true; |
| app->paused = false; |
| |
| bool show = ShouldShow(extension); |
| app->show_in_launcher = show; |
| app->show_in_shelf = show; |
| app->show_in_search = show; |
| app->show_in_management = |
| extensions::AppDisplayInfo::ShouldDisplayInAppLauncher(*extension); |
| app->handles_intents = which_type_.IsExtensions() || show; |
| |
| if (which_type_.IsChromeApps()) { |
| app->is_platform_app = extension->is_platform_app(); |
| |
| if (extension->is_hosted_app()) { |
| app->window_mode = |
| extensions::GetLaunchType(extensions::ExtensionPrefs::Get(profile_), |
| extension) == |
| extensions::LaunchType::LAUNCH_TYPE_WINDOW |
| ? apps::WindowMode::kWindow |
| : apps::WindowMode::kBrowser; |
| } |
| } |
| |
| const extensions::ManagementPolicy* policy = |
| extensions::ExtensionSystem::Get(profile_)->management_policy(); |
| app->allow_uninstall = (policy->UserMayModifySettings(extension, nullptr) && |
| !policy->MustRemainInstalled(extension, nullptr)); |
| |
| // Add file_handlers for Chrome Apps and quickoffice, or |
| // file_browser_handler for Extensions. |
| base::Extend(app->intent_filters, |
| which_type_.ChooseIntentFilter( |
| extension_misc::IsQuickOfficeExtension(extension->id()), |
| apps_util::CreateIntentFiltersForChromeApp, |
| apps_util::CreateIntentFiltersForExtension)(extension)); |
| return app; |
| } |
| |
| // This pointer is guaranteed to be valid and to outlive this object. |
| const raw_ptr<Profile> profile_; |
| |
| // This pointer is guaranteed to be valid and to outlive this object. |
| const raw_ptr<LacrosExtensionAppsPublisher> publisher_; |
| |
| // State to decide which extension type (e.g., Chrome Apps vs. Extensions) |
| // to support. |
| const ForWhichExtensionType which_type_; |
| |
| // Observes both extension prefs and registry for events that affect |
| // extensions. |
| base::ScopedObservation<extensions::ExtensionPrefs, |
| extensions::ExtensionPrefsObserver> |
| prefs_observation_{this}; |
| base::ScopedObservation<extensions::ExtensionRegistry, |
| extensions::ExtensionRegistryObserver> |
| registry_observation_{this}; |
| |
| // Observes AppWindowRegistry for app window creation and destruction. |
| base::ScopedObservation<extensions::AppWindowRegistry, |
| extensions::AppWindowRegistry::Observer> |
| app_window_registry_observation_{this}; |
| |
| // Records the window id associated with an app window. This is needed since |
| // the app window destruction callback occurs after the window is destroyed. |
| std::map<extensions::AppWindow*, std::string> app_window_id_cache_; |
| }; |
| |
| // static |
| std::unique_ptr<LacrosExtensionAppsPublisher> |
| LacrosExtensionAppsPublisher::MakeForChromeApps() { |
| return std::make_unique<LacrosExtensionAppsPublisher>(InitForChromeApps()); |
| } |
| |
| // static |
| std::unique_ptr<LacrosExtensionAppsPublisher> |
| LacrosExtensionAppsPublisher::MakeForExtensions() { |
| return std::make_unique<LacrosExtensionAppsPublisher>(InitForExtensions()); |
| } |
| |
| LacrosExtensionAppsPublisher::LacrosExtensionAppsPublisher( |
| const ForWhichExtensionType& which_type) |
| : which_type_(which_type) {} |
| |
| LacrosExtensionAppsPublisher::~LacrosExtensionAppsPublisher() = default; |
| |
| void LacrosExtensionAppsPublisher::Initialize() { |
| if (!InitializeCrosapi()) |
| return; |
| profile_manager_observation_.Observe(g_browser_process->profile_manager()); |
| auto profiles = g_browser_process->profile_manager()->GetLoadedProfiles(); |
| for (auto* profile : profiles) { |
| // TODO(https://crbug.com/1254894): The app id is not stable for secondary |
| // profiles and cannot be stored in sync. Thus, the app cannot be published |
| // at all. |
| if (!profile->IsMainProfile()) |
| continue; |
| profile_trackers_[profile] = |
| std::make_unique<ProfileTracker>(profile, this, which_type_); |
| } |
| } |
| |
| bool LacrosExtensionAppsPublisher::InitializeCrosapi() { |
| // Ash is too old to support the chrome app publisher interface. |
| int crosapiVersion = chromeos::LacrosService::Get()->GetInterfaceVersion( |
| crosapi::mojom::Crosapi::Uuid_); |
| int minRequiredVersion = |
| static_cast<int>(which_type_.ChooseForChromeAppOrExtension( |
| crosapi::mojom::Crosapi::kBindChromeAppPublisherMinVersion, |
| crosapi::mojom::Crosapi::kBindExtensionPublisherMinVersion)); |
| if (crosapiVersion < minRequiredVersion) |
| return false; |
| |
| // Ash is too old to support the chrome app window tracker interface. |
| if (!chromeos::LacrosService::Get() |
| ->IsAvailable<crosapi::mojom::AppWindowTracker>()) { |
| return false; |
| } |
| |
| if (which_type_.IsChromeApps()) { |
| chromeos::LacrosService::Get() |
| ->BindPendingReceiverOrRemote< |
| mojo::PendingReceiver<crosapi::mojom::AppPublisher>, |
| &crosapi::mojom::Crosapi::BindChromeAppPublisher>( |
| publisher_.BindNewPipeAndPassReceiver()); |
| } else if (which_type_.IsExtensions()) { |
| chromeos::LacrosService::Get() |
| ->BindPendingReceiverOrRemote< |
| mojo::PendingReceiver<crosapi::mojom::AppPublisher>, |
| &crosapi::mojom::Crosapi::BindExtensionPublisher>( |
| publisher_.BindNewPipeAndPassReceiver()); |
| } |
| return true; |
| } |
| |
| void LacrosExtensionAppsPublisher::Publish(std::vector<apps::AppPtr> apps) { |
| publisher_->OnApps(std::move(apps)); |
| } |
| |
| void LacrosExtensionAppsPublisher::OnAppWindowAdded( |
| const std::string& app_id, |
| const std::string& window_id) { |
| chromeos::LacrosService::Get() |
| ->GetRemote<crosapi::mojom::AppWindowTracker>() |
| ->OnAppWindowAdded(app_id, window_id); |
| } |
| |
| void LacrosExtensionAppsPublisher::OnAppWindowRemoved( |
| const std::string& app_id, |
| const std::string& window_id) { |
| chromeos::LacrosService::Get() |
| ->GetRemote<crosapi::mojom::AppWindowTracker>() |
| ->OnAppWindowRemoved(app_id, window_id); |
| } |
| |
| void LacrosExtensionAppsPublisher::OnProfileAdded(Profile* profile) { |
| // TODO(https://crbug.com/1254894): The app id is not stable for secondary |
| // profiles and cannot be stored in sync. Thus, the app cannot be published |
| // at all. |
| if (!profile->IsMainProfile()) |
| return; |
| profile_trackers_[profile] = |
| std::make_unique<ProfileTracker>(profile, this, which_type_); |
| } |
| |
| void LacrosExtensionAppsPublisher::OnProfileMarkedForPermanentDeletion( |
| Profile* profile) { |
| profile_trackers_.erase(profile); |
| } |
| |
| void LacrosExtensionAppsPublisher::OnProfileManagerDestroying() { |
| profile_trackers_.clear(); |
| profile_manager_observation_.Reset(); |
| } |
| |
| void LacrosExtensionAppsPublisher::UpdateAppWindowMode( |
| const std::string& app_id, |
| apps::WindowMode window_mode) { |
| Profile* profile = nullptr; |
| const extensions::Extension* extension = nullptr; |
| bool success = lacros_extensions_util::DemuxId(app_id, &profile, &extension); |
| if (!success) |
| return; |
| |
| DCHECK(extension->is_hosted_app()); |
| |
| // Persist hosted app's launch preference. |
| extensions::SetLaunchType(profile, extension->id(), |
| window_mode == apps::WindowMode::kWindow |
| ? extensions::LAUNCH_TYPE_WINDOW |
| : extensions::LAUNCH_TYPE_REGULAR); |
| |
| // Republish the app. |
| auto matched = profile_trackers_.find(profile); |
| DCHECK(matched != profile_trackers_.end()); |
| matched->second->Publish(extension, apps::Readiness::kReady); |
| } |