chromium/chrome/browser/ash/extensions/file_manager/event_router.cc

// Copyright 2014 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/extensions/file_manager/event_router.h"

#include <stddef.h>

#include <cmath>
#include <memory>
#include <set>
#include <unordered_map>
#include <utility>
#include <vector>

#include "ash/components/arc/arc_prefs.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/webui/file_manager/file_manager_ui.h"
#include "base/command_line.h"
#include "base/containers/adapters.h"
#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/task/single_thread_task_runner.h"
#include "base/values.h"
#include "chrome/browser/app_mode/app_mode_utils.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/drive/drive_integration_service.h"
#include "chrome/browser/ash/drive/file_system_util.h"
#include "chrome/browser/ash/extensions/file_manager/file_system_provider_metrics_util.h"
#include "chrome/browser/ash/extensions/file_manager/private_api_util.h"
#include "chrome/browser/ash/file_manager/file_tasks.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/file_manager/io_task.h"
#include "chrome/browser/ash/file_manager/open_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_manager/trash_common_util.h"
#include "chrome/browser/ash/file_manager/volume_manager.h"
#include "chrome/browser/ash/file_system_provider/mount_path_util.h"
#include "chrome/browser/ash/file_system_provider/operation_request_manager.h"
#include "chrome/browser/ash/file_system_provider/provided_file_system_info.h"
#include "chrome/browser/ash/file_system_provider/provided_file_system_interface.h"
#include "chrome/browser/ash/guest_os/guest_os_share_path.h"
#include "chrome/browser/ash/guest_os/public/guest_os_service.h"
#include "chrome/browser/ash/login/lock/screen_locker.h"
#include "chrome/browser/ash/login/ui/login_display_host.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_util.h"
#include "chrome/browser/ash/policy/dlp/dialogs/files_policy_dialog.h"
#include "chrome/browser/extensions/api/file_system/chrome_file_system_delegate_ash.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/extensions/api/file_manager_private.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/pref_names.h"
#include "chromeos/ash/components/disks/disk.h"
#include "chromeos/ash/components/drivefs/drivefs_host.h"
#include "chromeos/ash/components/login/login_state/login_state.h"
#include "chromeos/components/disks/disks_prefs.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/dbus/power/power_manager_client.h"
#include "components/arc/intent_helper/arc_intent_helper_bridge.h"
#include "components/drive/drive_pref_names.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/app_registry_cache.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/storage_partition.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_host.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "storage/browser/file_system/file_system_url.h"
#include "storage/common/file_system/file_system_util.h"
#include "ui/display/tablet_state.h"

using apps::AppServiceProxy;
using apps::AppServiceProxyFactory;
using arc::ArcIntentHelperBridge;
using ash::LoginState;
using ash::disks::Disk;
using ash::disks::DiskMountManager;
using chromeos::DlpClient;
using chromeos::PowerManagerClient;
using content::BrowserThread;
using drive::DriveIntegrationService;
using drive::DriveIntegrationServiceFactory;
using file_manager::io_task::IOTaskController;
using file_manager::util::EntryDefinition;
using file_manager::util::FileDefinition;
using guest_os::GuestOsService;
using guest_os::GuestOsSharePath;

namespace fmp = extensions::api::file_manager_private;

namespace file_manager {
namespace {

// Whether Files SWA has any open windows.
bool DoFilesSwaWindowsExist(Profile* profile) {
  return ash::file_manager::FileManagerUI::GetNumInstances() != 0;
}

// Checks if the Recovery Tool is running. This is a temporary solution.
// TODO(mtomasz): Replace with crbug.com/341902 solution.
bool IsRecoveryToolRunning(Profile* profile) {
  extensions::ExtensionPrefs* extension_prefs =
      extensions::ExtensionPrefs::Get(profile);
  if (!extension_prefs) {
    return false;
  }

  const std::string kRecoveryToolIds[] = {
      "kkebgepbbgbcmghedmmdfcbdcodlkngh",  // Recovery tool staging
      "jndclpdbaamdhonoechobihbbiimdgai"   // Recovery tool prod
  };

  for (const auto& extension_id : kRecoveryToolIds) {
    if (extension_prefs->IsExtensionRunning(extension_id)) {
      return true;
    }
  }

  return false;
}

// Sends an event named |event_name| with arguments |event_args| to extensions.
void BroadcastEvent(Profile* profile,
                    extensions::events::HistogramValue histogram_value,
                    const std::string& event_name,
                    base::Value::List event_args) {
  extensions::EventRouter::Get(profile)->BroadcastEvent(
      std::make_unique<extensions::Event>(histogram_value, event_name,
                                          std::move(event_args)));
}

// Sends an event named |event_name| with arguments |event_args| to an extension
// of |extention_id|.
void DispatchEventToExtension(
    Profile* profile,
    const std::string& extension_id,
    extensions::events::HistogramValue histogram_value,
    const std::string& event_name,
    base::Value::List event_args) {
  extensions::EventRouter::Get(profile)->DispatchEventToExtension(
      extension_id, std::make_unique<extensions::Event>(
                        histogram_value, event_name, std::move(event_args)));
}

// Convert the IO Task State enum to the Private API enum.
fmp::IoTaskState GetIoTaskState(io_task::State state) {
  switch (state) {
    case io_task::State::kQueued:
      return fmp::IoTaskState::kQueued;
    case io_task::State::kScanning:
      return fmp::IoTaskState::kScanning;
    case io_task::State::kInProgress:
      return fmp::IoTaskState::kInProgress;
    case io_task::State::kPaused:
      return fmp::IoTaskState::kPaused;
    case io_task::State::kSuccess:
      return fmp::IoTaskState::kSuccess;
    case io_task::State::kError:
      return fmp::IoTaskState::kError;
    case io_task::State::kNeedPassword:
      return fmp::IoTaskState::kNeedPassword;
    case io_task::State::kCancelled:
      return fmp::IoTaskState::kCancelled;
    default:
      NOTREACHED_IN_MIGRATION();
      return fmp::IoTaskState::kError;
  }
}

// Convert the IO Task Type enum to the Private API enum.
fmp::IoTaskType GetIoTaskType(io_task::OperationType type) {
  switch (type) {
    case io_task::OperationType::kCopy:
      return fmp::IoTaskType::kCopy;
    case io_task::OperationType::kDelete:
      return fmp::IoTaskType::kDelete;
    case io_task::OperationType::kEmptyTrash:
      return fmp::IoTaskType::kEmptyTrash;
    case io_task::OperationType::kExtract:
      return fmp::IoTaskType::kExtract;
    case io_task::OperationType::kMove:
      return fmp::IoTaskType::kMove;
    case io_task::OperationType::kRestore:
      return fmp::IoTaskType::kRestore;
    case io_task::OperationType::kRestoreToDestination:
      return fmp::IoTaskType::kRestoreToDestination;
    case io_task::OperationType::kTrash:
      return fmp::IoTaskType::kTrash;
    case io_task::OperationType::kZip:
      return fmp::IoTaskType::kZip;
    default:
      NOTREACHED_IN_MIGRATION();
      return fmp::IoTaskType::kCopy;
  }
}

fmp::PolicyErrorType GetPolicyErrorType(
    std::optional<io_task::PolicyErrorType> type) {
  if (!type.has_value()) {
    return fmp::PolicyErrorType::kNone;
  }
  switch (type.value()) {
    case io_task::PolicyErrorType::kDlp:
      return fmp::PolicyErrorType::kDlp;
    case io_task::PolicyErrorType::kEnterpriseConnectors:
      return fmp::PolicyErrorType::kEnterpriseConnectors;
    case io_task::PolicyErrorType::kDlpWarningTimeout:
      return fmp::PolicyErrorType::kDlpWarningTimeout;
    default:
      NOTREACHED_IN_MIGRATION();
      return fmp::PolicyErrorType::kNone;
  }
}

fmp::PolicyErrorType GetPolicyErrorType(policy::Policy policy) {
  switch (policy) {
    case policy::Policy::kDlp:
      return fmp::PolicyErrorType::kDlp;
    case policy::Policy::kEnterpriseConnectors:
      return fmp::PolicyErrorType::kEnterpriseConnectors;
  }
}

std::string FileErrorToErrorName(base::File::Error error_code) {
  switch (error_code) {
    case base::File::FILE_ERROR_NOT_FOUND:
      return "NotFoundError";
    case base::File::FILE_ERROR_INVALID_OPERATION:
    case base::File::FILE_ERROR_EXISTS:
    case base::File::FILE_ERROR_NOT_EMPTY:
      return "InvalidModificationError";
    case base::File::FILE_ERROR_NOT_A_DIRECTORY:
    case base::File::FILE_ERROR_NOT_A_FILE:
      return "TypeMismatchError";
    case base::File::FILE_ERROR_ACCESS_DENIED:
      return "NoModificationAllowedError";
    case base::File::FILE_ERROR_FAILED:
      return "InvalidStateError";
    case base::File::FILE_ERROR_ABORT:
      return "AbortError";
    case base::File::FILE_ERROR_SECURITY:
      return "SecurityError";
    case base::File::FILE_ERROR_NO_SPACE:
      return "QuotaExceededError";
    case base::File::FILE_ERROR_INVALID_URL:
      return "EncodingError";
    case base::File::FILE_ERROR_IN_USE:
      return "InUseError";
    default:
      return "InvalidModificationError";
  }
}

// Obtains whether the Files app should handle the volume or not.
bool ShouldShowNotificationForVolume(
    Profile* profile,
    const DeviceEventRouter& device_event_router,
    const Volume& volume) {
  if (volume.type() != VOLUME_TYPE_MTP &&
      volume.type() != VOLUME_TYPE_REMOVABLE_DISK_PARTITION) {
    return false;
  }

  if (device_event_router.is_resuming() ||
      device_event_router.is_starting_up()) {
    return false;
  }

  // Do not attempt to open File Manager while the login is in progress or
  // the screen is locked or running in kiosk app mode and make sure the file
  // manager is opened only for the active user.
  if (ash::LoginDisplayHost::default_host() ||
      ash::ScreenLocker::default_screen_locker() ||
      IsRunningInForcedAppMode() ||
      profile != ProfileManager::GetActiveUserProfile()) {
    return false;
  }

  // Do not pop-up the File Manager, if the recovery tool is running.
  if (IsRecoveryToolRunning(profile)) {
    return false;
  }

  // If the disable-default-apps flag is on, the Files app is not opened
  // automatically on device mount not to obstruct the manual test.
  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kDisableDefaultApps)) {
    return false;
  }

  if (volume.type() == VOLUME_TYPE_REMOVABLE_DISK_PARTITION) {
    const Disk* disk = DiskMountManager::GetInstance()->FindDiskBySourcePath(
        volume.source_path().AsUTF8Unsafe());
    if (disk) {
      // We suppress notifications about HP Elite USB-C Dock's internal storage.
      // chrome-os-partner:58309.
      // TODO(fukino): Remove this workaround when the root cause is fixed.
      if (disk->vendor_id() == "0ea0" && disk->product_id() == "2272") {
        return false;
      }
      // Suppress notifications for this disk if it has been mounted before.
      // This is to avoid duplicate notifications for operations that require a
      // remount of the disk (e.g. format or rename).
      if (!disk->is_first_mount()) {
        return false;
      }
    }
  }

  return true;
}

std::set<GURL> GetEventListenerURLs(Profile* profile,
                                    const std::string& event_name) {
  const extensions::EventListenerMap::ListenerList& listeners =
      extensions::EventRouter::Get(profile)
          ->listeners()
          .GetEventListenersByName(event_name);
  std::set<GURL> urls;
  for (const auto& listener : listeners) {
    if (!listener->extension_id().empty()) {
      urls.insert(extensions::Extension::GetBaseURLFromExtensionId(
          listener->extension_id()));
    } else {
      urls.insert(listener->listener_url());
    }
  }
  // In the Files app, there may not be a window open to listen to the event,
  // so always add the File Manager URL so events can be sent to the
  // SystemNotificationManager.
  urls.insert(util::GetFileManagerURL());
  return urls;
}

// Sub-part of the event router for handling device events.
class DeviceEventRouterImpl : public DeviceEventRouter {
 public:
  DeviceEventRouterImpl(SystemNotificationManager* notification_manager,
                        Profile* profile)
      : DeviceEventRouter(notification_manager), profile_(profile) {}

  DeviceEventRouterImpl(const DeviceEventRouterImpl&) = delete;
  DeviceEventRouterImpl& operator=(const DeviceEventRouterImpl&) = delete;

  // DeviceEventRouter overrides.
  void OnDeviceEvent(fmp::DeviceEventType type,
                     const std::string& device_path,
                     const std::string& device_label) override {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);

    fmp::DeviceEvent event;
    event.type = type;
    event.device_path = device_path;
    event.device_label = device_label;

    BroadcastEvent(
        profile_, extensions::events::FILE_MANAGER_PRIVATE_ON_DEVICE_CHANGED,
        fmp::OnDeviceChanged::kEventName, fmp::OnDeviceChanged::Create(event));

    system_notification_manager()->HandleDeviceEvent(event);
  }

  // DeviceEventRouter overrides.
  bool IsExternalStorageDisabled() override {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    return profile_->GetPrefs()->GetBoolean(
        disks::prefs::kExternalStorageDisabled);
  }

 private:
  const raw_ptr<Profile> profile_;
};

class DriveFsEventRouterImpl : public DriveFsEventRouter {
 public:
  DriveFsEventRouterImpl(const DriveFsEventRouterImpl&) = delete;
  DriveFsEventRouterImpl(
      SystemNotificationManager* notification_manager,
      Profile* profile,
      const std::map<base::FilePath, std::unique_ptr<FileWatcher>>*
          file_watchers)
      : DriveFsEventRouter(profile, notification_manager),
        profile_(profile),
        file_watchers_(file_watchers) {}

  DriveFsEventRouterImpl& operator=(const DriveFsEventRouterImpl&) = delete;

 private:
  std::set<GURL> GetEventListenerURLs(const std::string& event_name) override {
    return ::file_manager::GetEventListenerURLs(profile_, event_name);
  }

  GURL ConvertDrivePathToFileSystemUrl(const base::FilePath& file_path,
                                       const GURL& listener_url) override {
    GURL url;
    util::ConvertAbsoluteFilePathToFileSystemUrl(
        profile_,
        base::FilePath(DriveIntegrationServiceFactory::FindForProfile(profile_)
                           ->GetMountPointPath()
                           .value() +
                       file_path.value()),
        listener_url, &url);
    return url;
  }

  std::vector<GURL> ConvertPathsToFileSystemUrls(
      const std::vector<base::FilePath>& paths,
      const GURL& listener_url) override {
    std::vector<GURL> urls;
    for (const auto& path : paths) {
      GURL url;
      const bool ok = util::ConvertAbsoluteFilePathToFileSystemUrl(
          profile_, path, listener_url, &url);
      LOG_IF(ERROR, !ok) << "Cannot convert filepath to filesystem URL";
      urls.emplace_back(std::move(url));
    }
    return urls;
  }

  std::string GetDriveFileSystemName() override {
    return DriveIntegrationServiceFactory::FindForProfile(profile_)
        ->GetMountPointPath()
        .BaseName()
        .value();
  }

  bool IsPathWatched(const base::FilePath& path) override {
    base::FilePath absolute_path =
        DriveIntegrationServiceFactory::FindForProfile(profile_)
            ->GetMountPointPath();
    return base::FilePath("/").AppendRelativePath(path, &absolute_path) &&
           base::Contains(*file_watchers_, absolute_path);
  }

  void BroadcastEvent(extensions::events::HistogramValue histogram_value,
                      const std::string& event_name,
                      base::Value::List event_args,
                      bool dispatch_to_system_notification = true) override {
    std::unique_ptr<extensions::Event> event =
        std::make_unique<extensions::Event>(histogram_value, event_name,
                                            std::move(event_args));
    if (dispatch_to_system_notification) {
      system_notification_manager()->HandleEvent(*event.get());
    }
    extensions::EventRouter::Get(profile_)->BroadcastEvent(std::move(event));
  }

  const raw_ptr<Profile> profile_;
  const raw_ptr<const std::map<base::FilePath, std::unique_ptr<FileWatcher>>>
      file_watchers_;
};

// Records mounted File System Provider type if known otherwise UNKNOWN.
void RecordFileSystemProviderMountMetrics(const Volume& volume) {
  const ash::file_system_provider::ProviderId& provider_id =
      volume.provider_id();
  if (provider_id.GetType() != ash::file_system_provider::ProviderId::INVALID) {
    using FileSystemProviderMountedTypeMap =
        std::unordered_map<std::string, FileSystemProviderMountedType>;

    const std::string fsp_key = provider_id.ToString();
    FileSystemProviderMountedTypeMap fsp_sample_map =
        GetUmaForFileSystemProvider();
    FileSystemProviderMountedTypeMap::iterator sample =
        fsp_sample_map.find(fsp_key);
    if (sample != fsp_sample_map.end()) {
      UMA_HISTOGRAM_ENUMERATION(kFileSystemProviderMountedMetricName,
                                sample->second);
    } else {
      UMA_HISTOGRAM_ENUMERATION(kFileSystemProviderMountedMetricName,
                                FileSystemProviderMountedType::UNKNOWN);
    }
  }
}

// Returns a map from the given `files` to their parent directory.
std::map<base::FilePath, std::vector<base::FilePath>>
MapFilePathsToParentDirectory(const std::vector<base::FilePath> files) {
  std::map<base::FilePath, std::vector<base::FilePath>> dir_files_map;
  for (const auto& file : files) {
    dir_files_map[file.DirName()].push_back(file);
  }
  return dir_files_map;
}

// Creates a file watch event for the given `changed_files` in `directory`
// belonging to a filesystem described by `info`.
fmp::FileWatchEvent CreateFileWatchEvent(
    Profile* profile,
    const GURL& listener_url,
    const std::vector<base::FilePath>& changed_files,
    const storage::FileSystemInfo& info,
    const base::FilePath& directory,
    fmp::ChangeType change_type) {
  fmp::FileWatchEvent event;

  event.event_type = fmp::FileWatchEventType::kChanged;
  event.entry.additional_properties.Set("fileSystemRoot", info.root_url.spec());
  event.entry.additional_properties.Set("fileSystemName", info.name);
  event.entry.additional_properties.Set("fileFullPath",
                                        "/" + directory.value());
  event.entry.additional_properties.Set("fileIsDirectory", true);

  // Constructs the optional.
  event.changed_files.emplace();

  for (const base::FilePath& file : changed_files) {
    auto& change = event.changed_files->emplace_back();
    GURL url;
    util::ConvertAbsoluteFilePathToFileSystemUrl(profile, file, listener_url,
                                                 &url);
    change.url = url.spec();
    change.changes.push_back(change_type);
  }

  return event;
}

std::unique_ptr<ash::file_system_provider::ScopedUserInteraction>
MaybeStartInteractionWithODFS(const storage::FileSystemURL& url,
                              Profile* profile) {
  ash::file_system_provider::util::FileSystemURLParser parser(url);
  if (!parser.Parse()) {
    return nullptr;
  }
  if (parser.file_system()->GetFileSystemInfo().provider_id() !=
      ash::file_system_provider::ProviderId::CreateFromExtensionId(
          extension_misc::kODFSExtensionId)) {
    return nullptr;
  }
  return parser.file_system()->StartUserInteraction();
}

}  // namespace

fmp::MountError MountErrorToMountCompletedStatus(ash::MountError error) {
  switch (error) {
    case ash::MountError::kSuccess:
      return fmp::MountError::kSuccess;
    case ash::MountError::kUnknownError:
      return fmp::MountError::kUnknownError;
    case ash::MountError::kInternalError:
      return fmp::MountError::kInternalError;
    case ash::MountError::kInvalidArgument:
      return fmp::MountError::kInvalidArgument;
    case ash::MountError::kInvalidPath:
      return fmp::MountError::kInvalidPath;
    case ash::MountError::kPathAlreadyMounted:
      return fmp::MountError::kPathAlreadyMounted;
    case ash::MountError::kPathNotMounted:
      return fmp::MountError::kPathNotMounted;
    case ash::MountError::kDirectoryCreationFailed:
      return fmp::MountError::kDirectoryCreationFailed;
    case ash::MountError::kInvalidMountOptions:
      return fmp::MountError::kInvalidMountOptions;
    case ash::MountError::kInsufficientPermissions:
      return fmp::MountError::kInsufficientPermissions;
    case ash::MountError::kMountProgramNotFound:
      return fmp::MountError::kMountProgramNotFound;
    case ash::MountError::kMountProgramFailed:
      return fmp::MountError::kMountProgramFailed;
    case ash::MountError::kInvalidDevicePath:
      return fmp::MountError::kInvalidDevicePath;
    case ash::MountError::kUnknownFilesystem:
      return fmp::MountError::kUnknownFilesystem;
    case ash::MountError::kUnsupportedFilesystem:
      return fmp::MountError::kUnsupportedFilesystem;
    case ash::MountError::kNeedPassword:
      return fmp::MountError::kNeedPassword;
    case ash::MountError::kInProgress:
      return fmp::MountError::kInProgress;
    case ash::MountError::kCancelled:
      return fmp::MountError::kCancelled;
    case ash::MountError::kBusy:
      return fmp::MountError::kBusy;
    default:
      LOG(ERROR) << "Unexpected mount error: " << error;
      return fmp::MountError::kUnknownError;
  }
}

EventRouter::EventRouter(Profile* profile)
    : pref_change_registrar_(std::make_unique<PrefChangeRegistrar>()),
      profile_(profile),
      notification_manager_(
          std::make_unique<SystemNotificationManager>(profile)),
      office_tasks_(std::make_unique<OfficeTasks>()),
      device_event_router_(
          std::make_unique<DeviceEventRouterImpl>(notification_manager_.get(),
                                                  profile)),
      drivefs_event_router_(
          std::make_unique<DriveFsEventRouterImpl>(notification_manager_.get(),
                                                   profile,
                                                   &file_watchers_)),
      dispatch_directory_change_event_impl_(
          base::BindRepeating(&EventRouter::DispatchDirectoryChangeEventImpl,
                              base::Unretained(this))) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Notification manager can call into Drive FS for dialog handling.
  notification_manager_->SetDriveFSEventRouter(drivefs_event_router_.get());
  ObserveEvents();
}

EventRouter::~EventRouter() = default;

void EventRouter::OnIntentFiltersUpdated(
    const std::optional<std::string>& package_name) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  BroadcastEvent(profile_,
                 extensions::events::FILE_MANAGER_PRIVATE_ON_APPS_UPDATED,
                 fmp::OnAppsUpdated::kEventName, fmp::OnAppsUpdated::Create());
}

void EventRouter::Shutdown() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (ArcIntentHelperBridge* const bridge =
          arc::ArcIntentHelperBridge::GetForBrowserContext(profile_)) {
    bridge->RemoveObserver(this);
  }

  ash::system::TimezoneSettings::GetInstance()->RemoveObserver(this);

  DLOG_IF(WARNING, !file_watchers_.empty())
      << "Not all file watchers are "
      << "removed. This can happen when the Files app is open during shutdown.";
  file_watchers_.clear();
  DCHECK(profile_);

  pref_change_registrar_->RemoveAll();

  extensions::ExtensionRegistry::Get(profile_)->RemoveObserver(this);

  drivefs_event_router_->Reset();
  DriveIntegrationService::Observer::Reset();

  if (VolumeManager* const manager = VolumeManager::Get(profile_)) {
    manager->RemoveObserver(this);
    manager->RemoveObserver(device_event_router_.get());
    if (io_task::IOTaskController* const controller =
            manager->io_task_controller()) {
      controller->RemoveObserver(this);
    }
  }

  if (PowerManagerClient* const client = PowerManagerClient::Get()) {
    client->RemoveObserver(device_event_router_.get());
  }

  // GuestOsService doesn't exist for all profiles.
  if (GuestOsService* const service = GuestOsService::GetForProfile(profile_)) {
    service->MountProviderRegistry()->RemoveObserver(this);
  }

  if (GuestOsSharePath* const path =
          GuestOsSharePath::GetForProfile(profile_)) {
    path->RemoveObserver(this);
  }

  app_registry_cache_observer_.Reset();

  if (DlpClient* const client = DlpClient::Get()) {
    client->RemoveObserver(this);
  }

  content::GetNetworkConnectionTracker()->RemoveNetworkConnectionObserver(this);

  profile_ = nullptr;
}

void EventRouter::ObserveEvents() {
  DCHECK(profile_);

  if (!LoginState::IsInitialized() || !LoginState::Get()->IsUserLoggedIn()) {
    return;
  }

  // Ignore device events for the first few seconds.
  device_event_router_->Startup();

  // VolumeManager's construction triggers DriveIntegrationService's
  // construction, so it is necessary to call VolumeManager's Get before
  // accessing DriveIntegrationService.
  if (VolumeManager* const manager = VolumeManager::Get(profile_)) {
    manager->AddObserver(this);
    manager->AddObserver(device_event_router_.get());
    if (IOTaskController* const controller = manager->io_task_controller()) {
      controller->AddObserver(this);
      notification_manager_->SetIOTaskController(controller);
    }
  }

  if (PowerManagerClient* const client = PowerManagerClient::Get()) {
    client->AddObserver(device_event_router_.get());
  }

  if (DriveIntegrationService* const service =
          DriveIntegrationServiceFactory::FindForProfile(profile_)) {
    DriveIntegrationService::Observer::Observe(service);
    drivefs_event_router_->Observe(service);
  }

  extensions::ExtensionRegistry::Get(profile_)->AddObserver(this);

  pref_change_registrar_->Init(profile_->GetPrefs());

  {
    const base::RepeatingClosure cb = base::BindRepeating(
        &EventRouter::OnFileManagerPrefsChanged, weak_factory_.GetWeakPtr());
    pref_change_registrar_->Add(drive::prefs::kDriveFsBulkPinningVisible, cb);
    pref_change_registrar_->Add(drive::prefs::kDriveFsBulkPinningEnabled, cb);
    pref_change_registrar_->Add(drive::prefs::kDisableDriveOverCellular, cb);
    pref_change_registrar_->Add(drive::prefs::kDisableDrive, cb);
    pref_change_registrar_->Add(ash::prefs::kFilesAppTrashEnabled, cb);
    pref_change_registrar_->Add(prefs::kSearchSuggestEnabled, cb);
    pref_change_registrar_->Add(prefs::kUse24HourClock, cb);
    pref_change_registrar_->Add(arc::prefs::kArcEnabled, cb);
    pref_change_registrar_->Add(arc::prefs::kArcHasAccessToRemovableMedia, cb);
    pref_change_registrar_->Add(ash::prefs::kFilesAppFolderShortcuts, cb);
    pref_change_registrar_->Add(prefs::kOfficeFileMovedToOneDrive, cb);
    pref_change_registrar_->Add(prefs::kOfficeFileMovedToGoogleDrive, cb);
  }

  {
    const base::RepeatingClosure cb = base::BindRepeating(
        &EventRouter::BroadcastOnAppsUpdatedEvent, weak_factory_.GetWeakPtr());
    pref_change_registrar_->Add(prefs::kDefaultTasksByMimeType, cb);
    pref_change_registrar_->Add(prefs::kDefaultTasksBySuffix, cb);
  }

  ash::system::TimezoneSettings::GetInstance()->AddObserver(this);

  if (ArcIntentHelperBridge* const bridge =
          ArcIntentHelperBridge::GetForBrowserContext(profile_)) {
    bridge->AddObserver(this);
  }

  if (GuestOsSharePath* const path =
          GuestOsSharePath::GetForProfile(profile_)) {
    path->AddObserver(this);
  }

  // GuestOsService doesn't exist for all profiles.
  if (GuestOsService* const service = GuestOsService::GetForProfile(profile_)) {
    service->MountProviderRegistry()->AddObserver(this);
  }

  if (AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile_)) {
    AppServiceProxy* const proxy =
        AppServiceProxyFactory::GetForProfile(profile_);
    DCHECK(proxy);
    app_registry_cache_observer_.Observe(&proxy->AppRegistryCache());
  }

  if (DlpClient* const client = DlpClient::Get()) {
    client->AddObserver(this);
  }

  content::GetNetworkConnectionTracker()->AddNetworkConnectionObserver(this);
}

// File watch setup routines.
void EventRouter::AddFileWatch(const base::FilePath& local_path,
                               const base::FilePath& virtual_path,
                               const url::Origin& listener_origin,
                               BoolCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(!callback.is_null());

  auto iter = file_watchers_.find(local_path);
  if (iter == file_watchers_.end()) {
    std::unique_ptr<FileWatcher> watcher(new FileWatcher(virtual_path));
    watcher->AddListener(listener_origin);
    watcher->WatchLocalFile(
        profile_, local_path,
        base::BindRepeating(&EventRouter::HandleFileWatchNotification,
                            weak_factory_.GetWeakPtr()),
        std::move(callback));

    file_watchers_[local_path] = std::move(watcher);
  } else {
    iter->second->AddListener(listener_origin);
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), true));
  }
}

void EventRouter::RemoveFileWatch(const base::FilePath& local_path,
                                  const url::Origin& listener_origin) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  auto iter = file_watchers_.find(local_path);
  if (iter == file_watchers_.end()) {
    return;
  }
  // Remove the watcher if |local_path| is no longer watched by any extensions.
  iter->second->RemoveListener(listener_origin);
  if (iter->second->GetListeners().empty()) {
    file_watchers_.erase(iter);
  }
}

void EventRouter::OnWatcherManagerNotification(
    const storage::FileSystemURL& file_system_url,
    const url::Origin& listener_origin,
    storage::WatcherManager::ChangeType /* change_type */) {
  std::vector<url::Origin> listeners = {listener_origin};

  DispatchDirectoryChangeEvent(file_system_url.virtual_path(),
                               false /* error */, listeners);
}

void EventRouter::OnExtensionLoaded(content::BrowserContext* browser_context,
                                    const extensions::Extension* extension) {
  NotifyDriveConnectionStatusChanged();
}

void EventRouter::OnExtensionUnloaded(
    content::BrowserContext* browser_context,
    const extensions::Extension* extension,
    extensions::UnloadedExtensionReason reason) {
  NotifyDriveConnectionStatusChanged();
}

void EventRouter::TimezoneChanged(const icu::TimeZone& timezone) {
  OnFileManagerPrefsChanged();
}

void EventRouter::OnFileManagerPrefsChanged() {
  DCHECK(profile_);
  DCHECK(extensions::EventRouter::Get(profile_));

  BroadcastEvent(
      profile_, extensions::events::FILE_MANAGER_PRIVATE_ON_PREFERENCES_CHANGED,
      fmp::OnPreferencesChanged::kEventName,
      fmp::OnPreferencesChanged::Create());
}

void EventRouter::HandleFileWatchNotification(const base::FilePath& local_path,
                                              bool got_error) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  auto iter = file_watchers_.find(local_path);
  if (iter == file_watchers_.end()) {
    return;
  }

  DispatchDirectoryChangeEvent(iter->second->virtual_path(), got_error,
                               iter->second->GetListeners());
}

void EventRouter::DispatchDirectoryChangeEvent(
    const base::FilePath& virtual_path,
    bool got_error,
    const std::vector<url::Origin>& listeners) {
  dispatch_directory_change_event_impl_.Run(virtual_path, got_error, listeners);
}

void EventRouter::DispatchDirectoryChangeEventImpl(
    const base::FilePath& virtual_path,
    bool got_error,
    const std::vector<url::Origin>& listeners) {
  DCHECK(profile_);

  for (const url::Origin& origin : listeners) {
    FileDefinition file_definition;
    file_definition.virtual_path = virtual_path;
    // TODO(mtomasz): Add support for watching files in File System Provider
    // API.
    file_definition.is_directory = true;

    util::ConvertFileDefinitionToEntryDefinition(
        util::GetFileSystemContextForSourceURL(profile_, origin.GetURL()),
        origin, file_definition,
        base::BindOnce(
            &EventRouter::DispatchDirectoryChangeEventWithEntryDefinition,
            weak_factory_.GetWeakPtr(), got_error));
  }
}

void EventRouter::DispatchDirectoryChangeEventWithEntryDefinition(
    bool watcher_error,
    const EntryDefinition& entry_definition) {
  // TODO(mtomasz): Add support for watching files in File System Provider API.
  if (entry_definition.error != base::File::FILE_OK ||
      !entry_definition.is_directory) {
    DVLOG(1) << "Unable to dispatch event because resolving the directory "
             << "entry definition failed.";
    return;
  }

  fmp::FileWatchEvent event;
  event.event_type = watcher_error ? fmp::FileWatchEventType::kError
                                   : fmp::FileWatchEventType::kChanged;

  event.entry.additional_properties.Set("fileSystemName",
                                        entry_definition.file_system_name);
  event.entry.additional_properties.Set("fileSystemRoot",
                                        entry_definition.file_system_root_url);
  event.entry.additional_properties.Set(
      "fileFullPath", "/" + entry_definition.full_path.value());
  event.entry.additional_properties.Set("fileIsDirectory",
                                        entry_definition.is_directory);

  BroadcastEvent(profile_,
                 extensions::events::FILE_MANAGER_PRIVATE_ON_DIRECTORY_CHANGED,
                 fmp::OnDirectoryChanged::kEventName,
                 fmp::OnDirectoryChanged::Create(event));
}

void EventRouter::OnDiskAdded(const Disk& disk, bool mounting) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Do nothing.
}

void EventRouter::OnDiskRemoved(const Disk& disk) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Do nothing.
}

void EventRouter::OnDeviceAdded(const std::string& device_path) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Do nothing.
}

void EventRouter::OnDeviceRemoved(const std::string& device_path) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Do nothing.
}

void EventRouter::OnVolumeMounted(ash::MountError error_code,
                                  const Volume& volume) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // profile_ is NULL if ShutdownOnUIThread() is called earlier. This can
  // happen at shutdown. This should be removed after removing Drive mounting
  // code in addMount. (addMount -> OnFileSystemMounted -> OnVolumeMounted is
  // the only path to come here after Shutdown is called).
  if (!profile_) {
    return;
  }

  DispatchMountCompletedEvent(fmp::MountCompletedEventType::kMount, error_code,
                              volume);

  // Record the UMA metrics for mounted FSPs.
  RecordFileSystemProviderMountMetrics(volume);

  // TODO(mtomasz): Move VolumeManager and part of the event router outside of
  // file_manager, so there is no dependency between File System API and the
  // file_manager code.
  extensions::file_system_api::DispatchVolumeListChangeEventAsh(profile_);
}

void EventRouter::OnVolumeUnmounted(ash::MountError error_code,
                                    const Volume& volume) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DispatchMountCompletedEvent(fmp::MountCompletedEventType::kUnmount,
                              error_code, volume);

  // TODO(mtomasz): Move VolumeManager and part of the event router outside of
  // file_manager, so there is no dependency between File System API and the
  // file_manager code.
  extensions::file_system_api::DispatchVolumeListChangeEventAsh(profile_);
}

void EventRouter::DispatchMountCompletedEvent(
    fmp::MountCompletedEventType event_type,
    ash::MountError error,
    const Volume& volume) {
  // Build an event object.
  fmp::MountCompletedEvent event;
  event.event_type = event_type;
  event.status = MountErrorToMountCompletedStatus(error);
  util::VolumeToVolumeMetadata(profile_, volume, &event.volume_metadata);
  event.should_notify =
      ShouldShowNotificationForVolume(profile_, *device_event_router_, volume);
  notification_manager_->HandleMountCompletedEvent(event, volume);
  BroadcastEvent(
      profile_, extensions::events::FILE_MANAGER_PRIVATE_ON_MOUNT_COMPLETED,
      fmp::OnMountCompleted::kEventName, fmp::OnMountCompleted::Create(event));
}

void EventRouter::OnFormatStarted(const std::string& device_path,
                                  const std::string& device_label,
                                  bool success) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Do nothing.
}

void EventRouter::OnFormatCompleted(const std::string& device_path,
                                    const std::string& device_label,
                                    bool success) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Do nothing.
}

void EventRouter::OnPartitionStarted(const std::string& device_path,
                                     const std::string& device_label,
                                     bool success) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Do nothing.
}

void EventRouter::OnPartitionCompleted(const std::string& device_path,
                                       const std::string& device_label,
                                       bool success) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Do nothing.
}

void EventRouter::OnRenameStarted(const std::string& device_path,
                                  const std::string& device_label,
                                  bool success) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Do nothing.
}

void EventRouter::OnRenameCompleted(const std::string& device_path,
                                    const std::string& device_label,
                                    bool success) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  // Do nothing.
}

void EventRouter::SetDispatchDirectoryChangeEventImplForTesting(
    const DispatchDirectoryChangeEventImplCallback& callback) {
  dispatch_directory_change_event_impl_ = callback;
}

void EventRouter::OnFileSystemMountFailed() {
  OnFileManagerPrefsChanged();
}

void EventRouter::OnDriveConnectionStatusChanged(
    drive::util::ConnectionStatus status) {
  NotifyDriveConnectionStatusChanged();
}

// Send crostini share, unshare event.
void EventRouter::SendCrostiniEvent(fmp::CrostiniEventType event_type,
                                    const std::string& vm_name,
                                    const base::FilePath& path) {
  std::string mount_name;
  std::string file_system_name;
  std::string full_path;
  if (!util::ExtractMountNameFileSystemNameFullPath(
          path, &mount_name, &file_system_name, &full_path)) {
    return;
  }

  const std::string event_name(fmp::OnCrostiniChanged::kEventName);
  const extensions::EventListenerMap::ListenerList& listeners =
      extensions::EventRouter::Get(profile_)
          ->listeners()
          .GetEventListenersByName(event_name);

  // We handle two types of listeners, those with extension IDs and those with
  // listener URL. For listeners with extension IDs we use direct dispatch. For
  // listeners with listener URL we use a broadcast.
  std::set<std::string> extension_ids;
  std::set<url::Origin> origins;
  for (auto const& listener : listeners) {
    if (!listener->extension_id().empty()) {
      extension_ids.insert(listener->extension_id());
    } else if (listener->listener_url().is_valid()) {
      origins.insert(url::Origin::Create(listener->listener_url()));
    }
  }

  for (const std::string& extension_id : extension_ids) {
    url::Origin origin = url::Origin::Create(
        extensions::Extension::GetBaseURLFromExtensionId(extension_id));
    fmp::CrostiniEvent event;
    PopulateCrostiniEvent(event, event_type, vm_name, origin, mount_name,
                          file_system_name, full_path);
    DispatchEventToExtension(
        profile_, extension_id,
        extensions::events::FILE_MANAGER_PRIVATE_ON_CROSTINI_CHANGED,
        event_name, fmp::OnCrostiniChanged::Create(event));
  }
  for (const url::Origin& origin : origins) {
    fmp::CrostiniEvent event;
    PopulateCrostiniEvent(event, event_type, vm_name, origin, mount_name,
                          file_system_name, full_path);
    BroadcastEvent(profile_,
                   extensions::events::FILE_MANAGER_PRIVATE_ON_CROSTINI_CHANGED,
                   event_name, fmp::OnCrostiniChanged::Create(event));
  }
}

// static
void EventRouter::PopulateCrostiniEvent(fmp::CrostiniEvent& event,
                                        fmp::CrostiniEventType event_type,
                                        const std::string& vm_name,
                                        const url::Origin& origin,
                                        const std::string& mount_name,
                                        const std::string& file_system_name,
                                        const std::string& full_path) {
  event.event_type = event_type;
  event.vm_name = vm_name;
  event.container_name = "";  // Unused for the event types handled by this.
  fmp::CrostiniEvent::EntriesType entry;
  entry.additional_properties.Set(
      "fileSystemRoot",
      storage::GetExternalFileSystemRootURIString(origin.GetURL(), mount_name));
  entry.additional_properties.Set("fileSystemName", file_system_name);
  entry.additional_properties.Set("fileFullPath", full_path);
  entry.additional_properties.Set("fileIsDirectory", true);
  event.entries.emplace_back(std::move(entry));
}

void EventRouter::OnPersistedPathRegistered(const std::string& vm_name,
                                            const base::FilePath& path) {
  SendCrostiniEvent(fmp::CrostiniEventType::kShare, vm_name, path);
}

void EventRouter::OnUnshare(const std::string& vm_name,
                            const base::FilePath& path) {
  SendCrostiniEvent(fmp::CrostiniEventType::kUnshare, vm_name, path);
}

void EventRouter::OnGuestRegistered(const guest_os::GuestId& guest) {
  fmp::CrostiniEvent event;
  event.vm_name = guest.vm_name;
  event.container_name = guest.container_name;
  event.event_type = fmp::CrostiniEventType::kEnable;
  BroadcastEvent(profile_,
                 extensions::events::FILE_MANAGER_PRIVATE_ON_CROSTINI_CHANGED,
                 fmp::OnCrostiniChanged::kEventName,
                 fmp::OnCrostiniChanged::Create(event));
}

void EventRouter::OnGuestUnregistered(const guest_os::GuestId& guest) {
  fmp::CrostiniEvent event;
  event.vm_name = guest.vm_name;
  event.container_name = guest.container_name;
  event.event_type = fmp::CrostiniEventType::kDisable;
  BroadcastEvent(profile_,
                 extensions::events::FILE_MANAGER_PRIVATE_ON_CROSTINI_CHANGED,
                 fmp::OnCrostiniChanged::kEventName,
                 fmp::OnCrostiniChanged::Create(event));
}

void EventRouter::OnDisplayTabletStateChanged(display::TabletState state) {
  if (display::IsTabletStateChanging(state)) {
    return;
  }

  BroadcastEvent(
      profile_, extensions::events::FILE_MANAGER_PRIVATE_ON_TABLET_MODE_CHANGED,
      fmp::OnTabletModeChanged::kEventName,
      fmp::OnTabletModeChanged::Create(state ==
                                       display::TabletState::kInTabletMode));
}

void EventRouter::NotifyDriveConnectionStatusChanged() {
  DCHECK(profile_);
  DCHECK(extensions::EventRouter::Get(profile_));

  BroadcastEvent(profile_,
                 extensions::events::
                     FILE_MANAGER_PRIVATE_ON_DRIVE_CONNECTION_STATUS_CHANGED,
                 fmp::OnDriveConnectionStatusChanged::kEventName,
                 fmp::OnDriveConnectionStatusChanged::Create());
}

void EventRouter::DropFailedPluginVmDirectoryNotShared() {
  fmp::CrostiniEvent event;
  event.vm_name = plugin_vm::kPluginVmName;
  event.event_type =
      fmp::CrostiniEventType::kDropFailedPluginVmDirectoryNotShared;
  BroadcastEvent(profile_,
                 extensions::events::FILE_MANAGER_PRIVATE_ON_CROSTINI_CHANGED,
                 fmp::OnCrostiniChanged::kEventName,
                 fmp::OnCrostiniChanged::Create(event));
}

void EventRouter::OnDriveDialogResult(drivefs::mojom::DialogResult result) {
  drivefs_event_router_->OnDialogResult(result);
}

void EventRouter::SuppressDriveNotificationsForFilePath(
    const base::FilePath& relative_drive_path) {
  drivefs_event_router_->SuppressNotificationsForFilePath(relative_drive_path);
}

void EventRouter::RestoreDriveNotificationsForFilePath(
    const base::FilePath& relative_drive_path) {
  drivefs_event_router_->RestoreNotificationsForFilePath(relative_drive_path);
}

base::WeakPtr<EventRouter> EventRouter::GetWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

void EventRouter::OnIOTaskStatus(const io_task::ProgressStatus& status) {
  // Send the progress report to the system notification regardless of whether
  // Files app window exists as we may need to remove an existing
  // notification.
  notification_manager_->HandleIOTaskProgress(status);
  if (!DoFilesSwaWindowsExist(profile_) && !force_broadcasting_for_testing_) {
    return;
  }

  // If copying to/from ODFS, mark the provider's request manager
  // as "interacting with user" to prevent long operation warnings when
  // progress UI is already displayed.
  if (chromeos::features::IsUploadOfficeToCloudEnabled()) {
    if (status.IsCompleted()) {
      office_tasks_->odfs_interactions.erase(status.task_id);
    } else {
      auto it = office_tasks_->odfs_interactions.find(status.task_id);
      if (it == office_tasks_->odfs_interactions.end()) {
        auto interaction = MaybeStartInteractionWithODFS(
            status.GetDestinationFolder(), profile_);
        if (!interaction) {
          for (const io_task::EntryStatus& entry : status.sources) {
            interaction = MaybeStartInteractionWithODFS(entry.url, profile_);
            if (interaction) {
              break;
            }
          }
        }
        if (interaction) {
          office_tasks_->odfs_interactions[status.task_id] =
              std::move(interaction);
        }
      }
    }
  }

  // Send directory change events on I/O task completion. inotify is flaky on
  // some filesystems, so send these notifications so that at least operations
  // made from Files App are always reflected in the UI. Additionally, this
  // ensures the directory tree will be updated too, as the tree needs
  // notifications for folders outside of those being watched by a file watcher.
  if (status.IsCompleted()) {
    std::set<std::pair<base::FilePath, url::Origin>> updated_paths;
    if (status.GetDestinationFolder().is_valid()) {
      updated_paths.emplace(status.GetDestinationFolder().virtual_path(),
                            status.GetDestinationFolder().origin());
    }
    for (const auto& source : status.sources) {
      updated_paths.emplace(source.url.virtual_path().DirName(),
                            source.url.origin());
    }
    for (const auto& output : status.outputs) {
      updated_paths.emplace(output.url.virtual_path().DirName(),
                            output.url.origin());
    }

    for (const auto& [path, origin] : updated_paths) {
      DispatchDirectoryChangeEvent(path, false, {origin});
    }
  }

  // If any Files app window exists we send the progress to all of them.
  fmp::ProgressStatus event_status;
  event_status.task_id = status.task_id;
  event_status.type = GetIoTaskType(status.type);
  event_status.state = GetIoTaskState(status.state);
  if (status.policy_error.has_value()) {
    event_status.policy_error.emplace();
    event_status.policy_error->type =
        GetPolicyErrorType(status.policy_error->type);
    event_status.policy_error->policy_file_count =
        status.policy_error->blocked_files;
    event_status.policy_error->file_name = status.policy_error->file_name;
    event_status.policy_error->always_show_review =
        status.policy_error->always_show_review;
  }
  event_status.sources_scanned = status.sources_scanned;
  event_status.destination_volume_id = status.GetDestinationVolumeId();
  event_status.show_notification = status.show_notification;

  // Speedometer can produce infinite result which can't be serialized to JSON
  // when sending the status via private API.
  if (std::isfinite(status.remaining_seconds)) {
    event_status.remaining_seconds = status.remaining_seconds;
  }

  if (status.GetDestinationFolder().is_valid()) {
    event_status.destination_name =
        util::GetDisplayablePath(profile_, status.GetDestinationFolder())
            .value_or(base::FilePath())
            .BaseName()
            .value();
  }

  size_t processed = 0;
  std::vector<storage::FileSystemURL> outputs;
  for (const auto& file_status : status.outputs) {
    if (file_status.error) {
      if (status.type == io_task::OperationType::kTrash &&
          file_status.error.value() == base::File::FILE_OK) {
        // These entries are currently used to undo a TrashIOTask so only
        // consider the successfully trashed files.
        outputs.push_back(file_status.url);
      }
      processed++;
    }
  }

  event_status.num_remaining_items = status.sources.size() - processed;
  event_status.item_count = status.sources.size();

  // Get the last error occurrence in the `sources`.
  for (const io_task::EntryStatus& source : base::Reversed(status.sources)) {
    if (source.error && source.error.value() != base::File::FILE_OK) {
      event_status.error_name = FileErrorToErrorName(source.error.value());
    }
  }
  // If we have no error on 'sources', check if an error came from 'outputs'.
  if (status.state == io_task::State::kError &&
      event_status.error_name.empty()) {
    for (const io_task::EntryStatus& dest : base::Reversed(status.outputs)) {
      if (dest.error && dest.error.value() != base::File::FILE_OK) {
        event_status.error_name = FileErrorToErrorName(dest.error.value());
      }
    }
  }

  event_status.source_name = status.GetSourceName(profile_);
  event_status.bytes_transferred = status.bytes_transferred;
  event_status.total_bytes = status.total_bytes;

  // CopyOrMoveIOTask can enter PAUSED state when it needs the user to resolve
  // a file name conflict, or because it needs user to review a policy warning.
  if (GetIoTaskState(status.state) == fmp::IoTaskState::kPaused) {
    fmp::PauseParams pause_params;
    if (status.pause_params.conflict_params) {
      pause_params.conflict_params.emplace();
      pause_params.conflict_params->conflict_name =
          status.pause_params.conflict_params->conflict_name;
      pause_params.conflict_params->conflict_multiple =
          status.pause_params.conflict_params->conflict_multiple;
      pause_params.conflict_params->conflict_is_directory =
          status.pause_params.conflict_params->conflict_is_directory;
      pause_params.conflict_params->conflict_target_url =
          status.pause_params.conflict_params->conflict_target_url;
    }
    if (status.pause_params.policy_params) {
      pause_params.policy_params.emplace();
      pause_params.policy_params->type =
          GetPolicyErrorType(status.pause_params.policy_params->type);
      pause_params.policy_params->policy_file_count =
          status.pause_params.policy_params->warning_files_count;
      pause_params.policy_params->file_name =
          status.pause_params.policy_params->file_name;
      pause_params.policy_params->always_show_review =
          status.pause_params.policy_params->always_show_review;
    }
    event_status.pause_params = std::move(pause_params);
  }

  for (const FileSystemURL& skipped_encrypted_file :
       status.skipped_encrypted_files) {
    event_status.skipped_encrypted_files.push_back(
        skipped_encrypted_file.path().BaseName().value());
  }

  // The TrashIOTask is the only IOTask that uses the output Entry's, so don't
  // try to resolve the outputs for all other IOTasks.
  if (GetIoTaskType(status.type) != fmp::IoTaskType::kTrash ||
      outputs.size() == 0) {
    BroadcastIOTask(std::move(event_status));
    return;
  }

  // All FileSystemURLs in the output come from the same FileSystemContext, so
  // use the first URL to obtain the context.
  auto* file_system_context = util::GetFileSystemContextForSourceURL(
      profile_, outputs[0].origin().GetURL());
  if (file_system_context == nullptr) {
    LOG(ERROR) << "Could not find file system context";
    BroadcastIOTask(std::move(event_status));
    return;
  }

  util::FileDefinitionList file_definition_list;
  for (const auto& url : outputs) {
    util::FileDefinition file_definition;
    if (util::ConvertAbsoluteFilePathToRelativeFileSystemPath(
            profile_, url.origin().GetURL(), url.path(),
            &file_definition.virtual_path)) {
      file_definition_list.push_back(std::move(file_definition));
    }
  }

  util::ConvertFileDefinitionListToEntryDefinitionList(
      file_system_context, outputs[0].origin(), std::move(file_definition_list),
      base::BindOnce(
          &EventRouter::OnConvertFileDefinitionListToEntryDefinitionList,
          weak_factory_.GetWeakPtr(), std::move(event_status)));
}

void EventRouter::OnConvertFileDefinitionListToEntryDefinitionList(
    fmp::ProgressStatus event_status,
    std::unique_ptr<util::EntryDefinitionList> entry_definition_list) {
  if (entry_definition_list == nullptr) {
    BroadcastIOTask(std::move(event_status));
    return;
  }
  std::vector<OutputsType> outputs;
  for (const auto& def : *entry_definition_list) {
    if (def.error != base::File::FILE_OK) {
      LOG(WARNING) << "File entry ignored: " << static_cast<int>(def.error);
      continue;
    }
    OutputsType output_entry;
    output_entry.additional_properties.Set("fileSystemName",
                                           def.file_system_name);
    output_entry.additional_properties.Set("fileSystemRoot",
                                           def.file_system_root_url);
    // The `full_path` comes back as relative to the file system root, but the
    // UI requires it as an absolute path.
    output_entry.additional_properties.Set(
        "fileFullPath", base::FilePath("/").Append(def.full_path).value());
    output_entry.additional_properties.Set("fileIsDirectory", def.is_directory);
    outputs.push_back(std::move(output_entry));
  }
  event_status.outputs = std::move(outputs);
  BroadcastIOTask(std::move(event_status));
}

void EventRouter::OnFilesChanged(
    const std::vector<base::FilePath>& changed_files,
    fmp::ChangeType change_type) {
  std::map<base::FilePath, std::vector<base::FilePath>> files_to_directory_map =
      MapFilePathsToParentDirectory(changed_files);
  for (const auto& listener_url :
       GetEventListenerURLs(profile_, fmp::OnDirectoryChanged::kEventName)) {
    BroadcastDirectoryChangeEvent(files_to_directory_map, listener_url,
                                  change_type);
  }
}

void EventRouter::BroadcastDirectoryChangeEvent(
    const std::map<base::FilePath, std::vector<base::FilePath>>&
        files_to_directory_map,
    const GURL& listener_url,
    fmp::ChangeType change_type) {
  auto* file_system_context =
      util::GetFileSystemContextForSourceURL(profile_, listener_url);
  if (file_system_context == nullptr) {
    LOG(ERROR) << "Could not find file system context";
    return;
  }
  for (const auto& [dir, files] : files_to_directory_map) {
    GURL dir_url;
    util::ConvertAbsoluteFilePathToFileSystemUrl(profile_, dir, listener_url,
                                                 &dir_url);

    const storage::FileSystemURL dir_filesystem_url =
        file_system_context->CrackURLInFirstPartyContext(dir_url);
    if (dir_filesystem_url.path().empty()) {
      LOG(ERROR) << "Invalid URL";
      continue;
    }

    file_system_context->ResolveURL(
        dir_filesystem_url,
        base::BindOnce(
            &EventRouter::BroadcastDirectoryChangeEventOnFilesystemInfoResolved,
            weak_factory_.GetWeakPtr(), listener_url, std::move(files),
            change_type));
  }
}

void EventRouter::BroadcastDirectoryChangeEventOnFilesystemInfoResolved(
    GURL listener_url,
    std::vector<base::FilePath> changed_files,
    fmp::ChangeType change_type,
    base::File::Error result,
    const storage::FileSystemInfo& info,
    const base::FilePath& dir_path,
    storage::FileSystemContext::ResolvedEntryType) {
  fmp::FileWatchEvent event = CreateFileWatchEvent(
      profile_, listener_url, changed_files, info, dir_path, change_type);
  BroadcastEvent(profile_,
                 extensions::events::FILE_MANAGER_PRIVATE_ON_DIRECTORY_CHANGED,
                 fmp::OnDirectoryChanged::kEventName,
                 fmp::OnDirectoryChanged::Create(event));
}

void EventRouter::BroadcastIOTask(const fmp::ProgressStatus& event_status) {
  BroadcastEvent(
      profile_,
      extensions::events::FILE_MANAGER_PRIVATE_ON_IO_TASK_PROGRESS_STATUS,
      fmp::OnIOTaskProgressStatus::kEventName,
      fmp::OnIOTaskProgressStatus::Create(event_status));
}

void EventRouter::OnRegistered(guest_os::GuestOsMountProviderRegistry::Id id,
                               guest_os::GuestOsMountProvider* provider) {
  OnMountableGuestsChanged();
}

void EventRouter::OnUnregistered(
    guest_os::GuestOsMountProviderRegistry::Id id) {
  OnMountableGuestsChanged();
}

void EventRouter::BroadcastOnAppsUpdatedEvent() {
  DCHECK(profile_);
  DCHECK(extensions::EventRouter::Get(profile_));

  BroadcastEvent(profile_,
                 extensions::events::FILE_MANAGER_PRIVATE_ON_APPS_UPDATED,
                 fmp::OnAppsUpdated::kEventName, fmp::OnAppsUpdated::Create());
}

void EventRouter::OnMountableGuestsChanged() {
  auto guests = util::CreateMountableGuestList(profile_);
  BroadcastEvent(
      profile_,
      extensions::events::FILE_MANAGER_PRIVATE_ON_IO_TASK_PROGRESS_STATUS,
      fmp::OnMountableGuestsChanged::kEventName,
      fmp::OnMountableGuestsChanged::Create(guests));
}

drivefs::SyncState EventRouter::GetDriveSyncStateForPath(
    const base::FilePath& drive_path) {
  return drivefs_event_router_->GetDriveSyncStateForPath(drive_path);
}

void EventRouter::OnFilesAddedToDlpDaemon(
    const std::vector<base::FilePath>& files) {
  OnFilesChanged(files, fmp::ChangeType::kAddOrUpdate);
}

// Observes App Service and notifies Files app when there are any changes in the
// apps which might affect which file tasks are currently available, e.g. when
// an app is installed or uninstalled.
void EventRouter::OnAppUpdate(const apps::AppUpdate& update) {
  BroadcastOnAppsUpdatedEvent();
}

void EventRouter::OnAppRegistryCacheWillBeDestroyed(
    apps::AppRegistryCache* cache) {
  app_registry_cache_observer_.Reset();
}

void EventRouter::OnConnectionChanged(
    const network::mojom::ConnectionType type) {
  fmp::DeviceConnectionState result =
      content::GetNetworkConnectionTracker()->IsOffline()
          ? fmp::DeviceConnectionState::kOffline
          : fmp::DeviceConnectionState::kOnline;
  BroadcastEvent(profile_,
                 extensions::events::
                     FILE_MANAGER_PRIVATE_ON_DEVICE_CONNECTION_STATUS_CHANGED,
                 fmp::OnDeviceConnectionStatusChanged::kEventName,
                 fmp::OnDeviceConnectionStatusChanged::Create(result));
}

void EventRouter::OnLocalUserFilesPolicyChanged() {
  if (!base::FeatureList::IsEnabled(features::kSkyVault)) {
    return;
  }
  OnFileManagerPrefsChanged();
}

bool EventRouter::AddCloudOpenTask(const storage::FileSystemURL& file_url) {
  return office_tasks_->cloud_open_tasks.emplace(file_url.path()).second;
}

void EventRouter::RemoveCloudOpenTask(const storage::FileSystemURL& file_url) {
  office_tasks_->cloud_open_tasks.erase(file_url.path());
}

}  // namespace file_manager