chromium/chrome/browser/lacros/lacros_extension_apps_publisher.cc

// 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 <set>
#include <utility>

#include "base/check.h"
#include "base/containers/extend.h"
#include "base/files/file_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/not_fatal_until.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/chromeos/extensions/web_file_handlers/intent_util.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/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/policy/system_features_disable_list_policy_handler.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/lacros/window_utility.h"
#include "chrome/browser/web_applications/web_app_tab_helper.h"
#include "chromeos/crosapi/mojom/app_window_tracker.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#include "components/app_constants/constants.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/services/app_service/public/cpp/app_capability_access_cache.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 "components/services/app_service/public/cpp/package_id.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/app_window/app_window_registry.h"
#include "extensions/browser/extension_file_task_runner.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/path_util.h"
#include "extensions/browser/unloaded_extension_reason.h"
#include "extensions/common/constants.h"
#include "extensions/common/manifest_handlers/app_display_info.h"
#include "extensions/common/manifest_handlers/web_file_handlers_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_));
    if (auto* local_state = g_browser_process->local_state()) {
      local_state_pref_change_registrar_.Init(local_state);
      local_state_pref_change_registrar_.Add(
          policy::policy_prefs::kSystemFeaturesDisableList,
          base::BindRepeating(&LacrosExtensionAppsPublisher::ProfileTracker::
                                  OnSystemFeaturesPrefChanged,
                              weak_factory_.GetWeakPtr()));
      local_state_pref_change_registrar_.Add(
          policy::policy_prefs::kSystemFeaturesDisableMode,
          base::BindRepeating(&LacrosExtensionAppsPublisher::ProfileTracker::
                                  OnSystemFeaturesPrefChanged,
                              weak_factory_.GetWeakPtr()));
      OnSystemFeaturesPrefChanged();
    }

    // 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::kUninstalledByNonUser
                               : 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 window_id = lacros_window_utility::GetRootWindowUniqueId(
        app_window->GetNativeWindow());
    app_window_id_cache_[app_window] = window_id;

    publisher_->OnAppWindowAdded(app_window->GetExtension()->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 window_id = it->second;
    publisher_->OnAppWindowRemoved(app_window->extension_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, extension->id());

    const bool is_app_disabled =
        base::Contains(disabled_apps_, extension->id());
    app->readiness = is_app_disabled ? Readiness::kDisabledByPolicy : readiness;
    app->name = extension->name();
    app->short_name = extension->short_name();
    app->installer_package_id =
        apps::PackageId(apps::PackageType::kChromeApp, extension->id());

    // TODO(crbug.com/40240007): 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()};

    app->icon_key = apps::IconKey(GetIconEffects(extension));

    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;

    if (is_app_disabled && is_disabled_apps_mode_hidden_) {
      app->show_in_launcher = false;
      app->show_in_search = false;
      app->show_in_shelf = false;
      app->handles_intents = false;
      app->show_in_management = false;
    } else {
      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));

    app->allow_close = true;

    // Add file_handlers for either of the following:
    //   a) Chrome Apps and quickoffice.
    //   b) Web File Handlers or file_browser_handler for Extensions.
    base::Extend(app->intent_filters,
                 which_type_.ChooseIntentFilter(
                     extensions::IsLegacyQuickOfficeExtension(*extension),
                     apps_util::CreateIntentFiltersForChromeApp,
                     apps_util::CreateIntentFiltersForExtension)(extension));
    return app;
  }

  apps::IconEffects GetIconEffects(const extensions::Extension* extension) {
    apps::IconEffects icon_effects = apps::IconEffects::kNone;
    icon_effects = static_cast<apps::IconEffects>(
        icon_effects | apps::IconEffects::kCrOsStandardIcon);

    if (base::Contains(disabled_apps_, extension->id())) {
      icon_effects = static_cast<apps::IconEffects>(
          icon_effects | apps::IconEffects::kBlocked);
    }
    return icon_effects;
  }

  void OnSystemFeaturesPrefChanged() {
    PrefService* const local_state = g_browser_process->local_state();
    if (!local_state || !local_state->FindPreference(
                            policy::policy_prefs::kSystemFeaturesDisableList)) {
      return;
    }

    const base::Value::List& disabled_system_features =
        local_state->GetList(policy::policy_prefs::kSystemFeaturesDisableList);

    const bool is_pref_disabled_mode_hidden =
        local_state->GetString(
            policy::policy_prefs::kSystemFeaturesDisableMode) ==
        policy::kHiddenDisableMode;
    const bool is_disabled_mode_changed =
        (is_pref_disabled_mode_hidden != is_disabled_apps_mode_hidden_);
    is_disabled_apps_mode_hidden_ = is_pref_disabled_mode_hidden;

    UpdateAppDisabledState(disabled_system_features,
                           static_cast<int>(policy::SystemFeature::kWebStore),
                           extensions::kWebStoreAppId,
                           is_disabled_mode_changed);
  }

  void UpdateAppDisabledState(
      const base::Value::List& disabled_system_features_pref,
      int feature,
      const std::string& app_id,
      bool is_disabled_mode_changed) {
    const bool is_disabled =
        base::Contains(disabled_system_features_pref, base::Value(feature));
    // Sometimes the policy is updated before the app is installed, so this way
    // the disabled_apps_ is updated regardless the Publish should happen or not
    // and the app will be published with the correct readiness upon its
    // installation.
    const bool should_publish =
        (base::Contains(disabled_apps_, app_id) != is_disabled) ||
        is_disabled_mode_changed;

    if (is_disabled) {
      disabled_apps_.insert(app_id);
    } else {
      disabled_apps_.erase(app_id);
    }

    if (!should_publish) {
      return;
    }

    const auto* extension =
        lacros_extensions_util::MaybeGetExtension(profile_, app_id);
    if (!extension) {
      return;
    }

    Publish(extension,
            is_disabled ? Readiness::kDisabledByPolicy : Readiness::kReady);
  }

  // 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_;

  // Tracks apps that have been disabled from installing by enterprise policy.
  // The values come from local state and are set by updating the
  // SystemFeaturesDisableList policy.
  std::set<std::string> disabled_apps_;

  // Boolean signifying whether the preferred user experience mode of disabled
  // apps is hidden (true) or blocked (false). The value comes from local state
  // and is set by updating the SystemFeaturesDisableMode policy.
  bool is_disabled_apps_mode_hidden_ = false;

  // Registrar used to monitor the local state prefs.
  PrefChangeRegistrar local_state_pref_change_registrar_;

  // 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_;

  base::WeakPtrFactory<LacrosExtensionAppsPublisher::ProfileTracker>
      weak_factory_{this};
};

// 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(crbug.com/40199791): 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_);
  }

  // Only track the media usage for the chrome apps.
  if (which_type_.IsChromeApps()) {
    media_dispatcher_.Observe(MediaCaptureDevicesDispatcher::GetInstance()
                                  ->GetMediaStreamCaptureIndicator()
                                  .get());
  }
}

bool LacrosExtensionAppsPublisher::InitializeCrosapi() {
  // Ash is too old to support the chrome app publisher interface.
  int crosapiVersion = chromeos::LacrosService::Get()
                           ->GetInterfaceVersion<crosapi::mojom::Crosapi>();
  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::PublishCapabilityAccesses(
    std::vector<apps::CapabilityAccessPtr> accesses) {
  publisher_->OnCapabilityAccesses(std::move(accesses));
}

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(crbug.com/40199791): 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::GetProfileAndExtension(
      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);
  CHECK(matched != profile_trackers_.end(), base::NotFatalUntil::M130);
  matched->second->Publish(extension, apps::Readiness::kReady);
}

void LacrosExtensionAppsPublisher::UpdateAppSize(const std::string& app_id) {
  Profile* profile = nullptr;
  const extensions::Extension* extension = nullptr;
  bool success = lacros_extensions_util::GetProfileAndExtension(
      app_id, &profile, &extension);
  if (!success) {
    return;
  }

  extensions::path_util::CalculateExtensionDirectorySize(
      extension->path(),
      base::BindOnce(&LacrosExtensionAppsPublisher::OnSizeCalculated,
                     weak_ptr_factory_.GetWeakPtr(), extension->id()));
}

void LacrosExtensionAppsPublisher::OnIsCapturingVideoChanged(
    content::WebContents* web_contents,
    bool is_capturing_video) {
  auto app_id = MaybeGetAppId(web_contents);
  if (!app_id.has_value()) {
    return;
  }

  auto result = media_requests_.UpdateCameraState(app_id.value(), web_contents,
                                                  is_capturing_video);
  ModifyCapabilityAccess(app_id.value(), result.camera, result.microphone);
}

void LacrosExtensionAppsPublisher::OnIsCapturingAudioChanged(
    content::WebContents* web_contents,
    bool is_capturing_audio) {
  auto app_id = MaybeGetAppId(web_contents);
  if (!app_id.has_value()) {
    return;
  }

  auto result = media_requests_.UpdateMicrophoneState(
      app_id.value(), web_contents, is_capturing_audio);
  ModifyCapabilityAccess(app_id.value(), result.camera, result.microphone);
}

void LacrosExtensionAppsPublisher::OnSizeCalculated(const std::string& app_id,
                                                    int64_t size) {
  std::vector<apps::AppPtr> apps;
  apps::AppType app_type = which_type_.ChooseForChromeAppOrExtension(
      apps::AppType::kStandaloneBrowserChromeApp,
      apps::AppType::kStandaloneBrowserExtension);
  auto app = std::make_unique<apps::App>(app_type, app_id);
  app->app_size_in_bytes = size;
  apps.push_back(std::move(app));
  Publish(std::move(apps));
}

std::optional<std::string> LacrosExtensionAppsPublisher::MaybeGetAppId(
    content::WebContents* web_contents) {
  // The web app publisher is responsible to handle `web_contents` for web
  // apps.
  const webapps::AppId* web_app_id =
      web_app::WebAppTabHelper::GetAppId(web_contents);
  if (web_app_id) {
    return std::nullopt;
  }

  const auto* extension =
      lacros_extensions_util::MaybeGetExtension(web_contents);
  return (extension && which_type_.Matches(extension))
             ? std::make_optional<std::string>(extension->id())
             : std::nullopt;
}

void LacrosExtensionAppsPublisher::ModifyCapabilityAccess(
    const std::string& app_id,
    std::optional<bool> accessing_camera,
    std::optional<bool> accessing_microphone) {
  if (!accessing_camera.has_value() && !accessing_microphone.has_value()) {
    return;
  }

  std::vector<apps::CapabilityAccessPtr> capability_accesses;
  auto capability_access = std::make_unique<apps::CapabilityAccess>(app_id);
  capability_access->camera = accessing_camera;
  capability_access->microphone = accessing_microphone;
  capability_accesses.push_back(std::move(capability_access));

  PublishCapabilityAccesses(std::move(capability_accesses));
}