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

// Copyright 2018 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/drivefs_event_router.h"

#include "ash/constants/ash_features.h"
#include "base/containers/contains.h"
#include "base/files/file_path.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/ash/drive/file_system_util.h"
#include "chrome/browser/ash/extensions/file_manager/private_api_util.h"
#include "chrome/common/extensions/api/file_manager_private.h"
#include "chromeos/ash/components/drivefs/drivefs_host.h"
#include "chromeos/ash/components/drivefs/drivefs_pinning_manager.h"
#include "extensions/browser/extension_event_histogram_value.h"

namespace file_manager {
namespace file_manager_private = extensions::api::file_manager_private;

namespace {

constexpr auto& kIndividualTransferEventName =
    file_manager_private::OnIndividualFileTransfersUpdated::kEventName;

constexpr extensions::events::HistogramValue kTransferEvent =
    extensions::events::FILE_MANAGER_PRIVATE_ON_FILE_TRANSFERS_UPDATED;

file_manager_private::DriveConfirmDialogType ConvertDialogReasonType(
    drivefs::mojom::DialogReason::Type type) {
  switch (type) {
    case drivefs::mojom::DialogReason::Type::kEnableDocsOffline:
      return file_manager_private::DriveConfirmDialogType::kEnableDocsOffline;
  }
}

}  // namespace

// Time interval to check for stale sync status in
// DriveFsEventRouter::path_to_sync_state_.
constexpr auto kSyncStateStaleCheckInterval = base::Seconds(100);

// Time after which a sync status in DriveFsEventRouter::path_to_sync_state_ is
// considered to be stale.
constexpr auto kSyncStateStaleThreshold = base::Seconds(90);

DriveFsEventRouter::DriveFsEventRouter(
    Profile* profile,
    SystemNotificationManager* notification_manager)
    : profile_(profile), notification_manager_(notification_manager) {
  stale_sync_state_cleanup_timer_.Start(
      FROM_HERE, kSyncStateStaleCheckInterval,
      base::BindRepeating(&DriveFsEventRouter::ClearStaleSyncStates,
                          weak_ptr_factory_.GetWeakPtr()));
}

DriveFsEventRouter::~DriveFsEventRouter() = default;

void DriveFsEventRouter::OnUnmounted() {
  stale_sync_state_cleanup_timer_.Stop();
  path_to_sync_state_.clear();
  dialog_callback_.Reset();
}

file_manager_private::SyncStatus ConvertSyncStatus(drivefs::SyncStatus status) {
  switch (status) {
    case drivefs::SyncStatus::kNotFound:
      return file_manager_private::SyncStatus::kNotFound;
    case drivefs::SyncStatus::kQueued:
      return file_manager_private::SyncStatus::kQueued;
    case drivefs::SyncStatus::kInProgress:
      return file_manager_private::SyncStatus::kInProgress;
    case drivefs::SyncStatus::kCompleted:
      return file_manager_private::SyncStatus::kCompleted;
    case drivefs::SyncStatus::kError:
      return file_manager_private::SyncStatus::kError;
    default:
      NOTREACHED_IN_MIGRATION();
      return file_manager_private::SyncStatus::kNotFound;
  }
}

void DriveFsEventRouter::OnItemProgress(
    const drivefs::mojom::ProgressEvent& event) {
  base::FilePath file_path;
  std::string path;

  if (event.file_path.has_value()) {
    file_path = *event.file_path;
    path = file_path.value();
  } else {
    path = event.path;
    file_path = base::FilePath(path);
  }

  drivefs::SyncStatus status;
  if (event.progress == 0) {
    status = drivefs::SyncStatus::kQueued;
  } else if (event.progress == 100) {
    status = drivefs::SyncStatus::kCompleted;
  } else {
    status = drivefs::SyncStatus::kInProgress;
  }

  std::vector<drivefs::SyncState> filtered_states;

  filtered_states.emplace_back(
      drivefs::SyncState{status, static_cast<float>(event.progress) / 100.0f,
                         file_path, base::Time::Now()});

  if (status == drivefs::SyncStatus::kCompleted) {
    const auto previous_state_iter = path_to_sync_state_.find(path);
    const bool was_tracked = previous_state_iter != path_to_sync_state_.end();
    if (was_tracked) {
      // Stop tracking completed events but push it to subscribers.
      path_to_sync_state_.erase(previous_state_iter);
    }
  } else {
    path_to_sync_state_[path] = filtered_states.back();
    if (!stale_sync_state_cleanup_timer_.IsRunning()) {
      stale_sync_state_cleanup_timer_.Reset();
    }
  }

  std::vector<IndividualFileTransferStatus> statuses;
  std::vector<base::FilePath> paths;

  for (const auto& sync_state : filtered_states) {
    IndividualFileTransferStatus individual_status;
    individual_status.sync_status = ConvertSyncStatus(sync_state.status);
    individual_status.progress = sync_state.progress;
    statuses.emplace_back(std::move(individual_status));
    paths.emplace_back(sync_state.path);
  }

  for (const GURL& url : GetEventListenerURLs(kIndividualTransferEventName)) {
    const auto file_urls = ConvertPathsToFileSystemUrls(paths, url);
    for (size_t i = 0; i < file_urls.size(); i++) {
      statuses[i].file_url = file_urls[i].spec();
    }
    // Note: Inline Sync Statuses don't need to differentiate between transfer
    // and pin events because they do not display aggregate progress separately
    // for each of those two categories.
    BroadcastIndividualTransfersEvent(kTransferEvent, statuses);
  }
}

void DriveFsEventRouter::OnFilesChanged(
    const std::vector<drivefs::mojom::FileChange>& changes) {
  // Maps from parent directory to event for that directory.
  std::map<base::FilePath,
           extensions::api::file_manager_private::FileWatchEvent>
      events;
  for (const auto& listener_url : GetEventListenerURLs(
           file_manager_private::OnDirectoryChanged::kEventName)) {
    for (const auto& change : changes) {
      auto& event = events[change.path.DirName()];
      if (!event.changed_files) {
        event.event_type =
            extensions::api::file_manager_private::FileWatchEventType::kChanged;
        event.changed_files.emplace();
        event.entry.additional_properties.Set(
            "fileSystemRoot", base::StrCat({ConvertDrivePathToFileSystemUrl(
                                                base::FilePath(), listener_url)
                                                .spec(),
                                            "/"}));
        event.entry.additional_properties.Set("fileSystemName",
                                              GetDriveFileSystemName());
        event.entry.additional_properties.Set("fileFullPath",
                                              change.path.DirName().value());
        event.entry.additional_properties.Set("fileIsDirectory", true);
      }
      event.changed_files->emplace_back();
      auto& file_manager_change = event.changed_files->back();
      file_manager_change.url =
          ConvertDrivePathToFileSystemUrl(change.path, listener_url).spec();
      file_manager_change.changes.push_back(
          change.type == drivefs::mojom::FileChange::Type::kDelete
              ? extensions::api::file_manager_private::ChangeType::kDelete
              : extensions::api::file_manager_private::ChangeType::
                    kAddOrUpdate);
    }
    for (auto& event : events) {
      BroadcastOnDirectoryChangedEvent(event.first, event.second);
    }
  }
}

void DriveFsEventRouter::OnError(const drivefs::mojom::DriveError& error) {
  file_manager_private::DriveSyncErrorEvent event;
  switch (error.type) {
    case drivefs::mojom::DriveError::Type::kCantUploadStorageFull:
      event.type = file_manager_private::DriveSyncErrorType::kNoServerSpace;
      break;
    case drivefs::mojom::DriveError::Type::kCantUploadStorageFullOrganization:
      event.type =
          file_manager_private::DriveSyncErrorType::kNoServerSpaceOrganization;
      break;
    case drivefs::mojom::DriveError::Type::kPinningFailedDiskFull:
      event.type = file_manager_private::DriveSyncErrorType::kNoLocalSpace;
      break;
    case drivefs::mojom::DriveError::Type::kCantUploadSharedDriveStorageFull:
      event.type =
          file_manager_private::DriveSyncErrorType::kNoSharedDriveSpace;
      event.shared_drive = error.shared_drive;
      break;
  }
  for (const auto& listener_url : GetEventListenerURLs(
           file_manager_private::OnDriveSyncError::kEventName)) {
    event.file_url =
        ConvertDrivePathToFileSystemUrl(error.path, listener_url).spec();
    BroadcastEvent(extensions::events::FILE_MANAGER_PRIVATE_ON_DRIVE_SYNC_ERROR,
                   file_manager_private::OnDriveSyncError::kEventName,
                   file_manager_private::OnDriveSyncError::Create(event));
  }
}

void DriveFsEventRouter::Observe(
    drive::DriveIntegrationService* const service) {
  DCHECK(service);
  drive::DriveIntegrationService::Observer::Observe(service);
  drivefs::DriveFsHost* const host = service->GetDriveFsHost();
  drivefs::DriveFsHost::Observer::Observe(host);
  host->set_dialog_handler(
      base::BindRepeating(&DriveFsEventRouter::DisplayConfirmDialog,
                          weak_ptr_factory_.GetWeakPtr()));
}

void DriveFsEventRouter::Reset() {
  if (drivefs::DriveFsHost* const host = GetHost()) {
    host->set_dialog_handler({});
  }
  drivefs::DriveFsHost::Observer::Reset();
  drive::DriveIntegrationService::Observer::Reset();
}

void DriveFsEventRouter::OnDriveIntegrationServiceDestroyed() {
  Reset();
}

void DriveFsEventRouter::OnBulkPinProgress(
    const drivefs::pinning::Progress& progress) {
  BroadcastEvent(extensions::events::FILE_MANAGER_PRIVATE_ON_BULK_PIN_PROGRESS,
                 file_manager_private::OnBulkPinProgress::kEventName,
                 file_manager_private::OnBulkPinProgress::Create(
                     util::BulkPinProgressToJs(progress)));
}

void DriveFsEventRouter::ClearStaleSyncStates() {
  const base::Time now = base::Time::Now();
  for (auto it = path_to_sync_state_.cbegin();
       it != path_to_sync_state_.cend();) {
    const base::Time& last_updated = it->second.last_updated;
    if (now - last_updated > kSyncStateStaleThreshold) {
      it = path_to_sync_state_.erase(it);
    } else {
      ++it;
    }
  }
}

void DriveFsEventRouter::DisplayConfirmDialog(
    const drivefs::mojom::DialogReason& reason,
    base::OnceCallback<void(drivefs::mojom::DialogResult)> callback) {
  if (dialog_callback_) {
    std::move(callback).Run(drivefs::mojom::DialogResult::kNotDisplayed);
    return;
  }
  auto urls = GetEventListenerURLs(
      file_manager_private::OnDriveConfirmDialog::kEventName);
  if (urls.empty()) {
    std::move(callback).Run(drivefs::mojom::DialogResult::kNotDisplayed);
    return;
  }
  dialog_callback_ = std::move(callback);

  file_manager_private::DriveConfirmDialogEvent event;
  event.type = ConvertDialogReasonType(reason.type);
  for (const auto& listener_url : urls) {
    event.file_url =
        ConvertDrivePathToFileSystemUrl(reason.path, listener_url).spec();
    BroadcastEvent(
        extensions::events::FILE_MANAGER_PRIVATE_ON_DRIVE_CONFIRM_DIALOG,
        file_manager_private::OnDriveConfirmDialog::kEventName,
        file_manager_private::OnDriveConfirmDialog::Create(event));
  }
}

void DriveFsEventRouter::OnDialogResult(drivefs::mojom::DialogResult result) {
  if (dialog_callback_) {
    std::move(dialog_callback_).Run(result);
  }
}

void DriveFsEventRouter::SuppressNotificationsForFilePath(
    const base::FilePath& path) {
  ignored_file_paths_.insert(path);
}

void DriveFsEventRouter::RestoreNotificationsForFilePath(
    const base::FilePath& path) {
  if (ignored_file_paths_.erase(path) == 0) {
    LOG(ERROR) << "Provided file path was not in the set of ignored paths";
  }
}

void DriveFsEventRouter::BroadcastIndividualTransfersEvent(
    const extensions::events::HistogramValue event_type,
    const std::vector<IndividualFileTransferStatus>& status) {
  BroadcastEvent(
      event_type, kIndividualTransferEventName,
      file_manager_private::OnIndividualFileTransfersUpdated::Create(status),
      /*dispatch_to_system_notification=*/false);
}

void DriveFsEventRouter::BroadcastOnDirectoryChangedEvent(
    const base::FilePath& directory,
    const extensions::api::file_manager_private::FileWatchEvent& event) {
  if (!IsPathWatched(directory)) {
    return;
  }
  BroadcastEvent(extensions::events::FILE_MANAGER_PRIVATE_ON_DIRECTORY_CHANGED,
                 file_manager_private::OnDirectoryChanged::kEventName,
                 file_manager_private::OnDirectoryChanged::Create(event));
}

drivefs::SyncState DriveFsEventRouter::GetDriveSyncStateForPath(
    const base::FilePath& drive_path) {
  const auto it = path_to_sync_state_.find(drive_path.AsUTF8Unsafe());
  if (it == path_to_sync_state_.end()) {
    return drivefs::SyncState::CreateNotFound(drive_path);
  }
  return it->second;
}

}  // namespace file_manager