chromium/chrome/browser/ui/ash/holding_space/holding_space_suggestions_delegate.cc

// Copyright 2022 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/ui/ash/holding_space/holding_space_suggestions_delegate.h"

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/holding_space/holding_space_file.h"
#include "base/containers/adapters.h"
#include "base/ranges/algorithm.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_suggest/file_suggest_keyed_service_factory.h"

namespace ash {
namespace {

// Returns the holding space item type that matches a given suggestion type.
HoldingSpaceItem::Type GetItemTypeFromSuggestionType(
    FileSuggestionType suggestion_type) {
  switch (suggestion_type) {
    case FileSuggestionType::kDriveFile:
      return HoldingSpaceItem::Type::kDriveSuggestion;
    case FileSuggestionType::kLocalFile:
      return HoldingSpaceItem::Type::kLocalSuggestion;
  }
}

// Returns whether `item` represents a pinned file that also exists as a
// suggested file in `suggestions_by_type`.
bool ItemIsPinnedSuggestion(
    const HoldingSpaceItem* item,
    std::map<HoldingSpaceItem::Type, std::vector<base::FilePath>>&
        suggestions_by_type) {
  if (item->type() != HoldingSpaceItem::Type::kPinnedFile)
    return false;

  for (const auto& [_, suggested_file_paths] : suggestions_by_type) {
    if (base::Contains(suggested_file_paths, item->file().file_path)) {
      return true;
    }
  }

  return false;
}

}  // namespace

HoldingSpaceSuggestionsDelegate::HoldingSpaceSuggestionsDelegate(
    HoldingSpaceKeyedService* service,
    HoldingSpaceModel* model)
    : HoldingSpaceKeyedServiceDelegate(service, model) {
  DCHECK(features::IsHoldingSpaceSuggestionsEnabled());
}

HoldingSpaceSuggestionsDelegate::~HoldingSpaceSuggestionsDelegate() = default;

void HoldingSpaceSuggestionsDelegate::RefreshSuggestions() {
  MaybeFetchSuggestions(FileSuggestionType::kDriveFile);
  MaybeFetchSuggestions(FileSuggestionType::kLocalFile);
}

void HoldingSpaceSuggestionsDelegate::RemoveSuggestions(
    const std::vector<base::FilePath>& absolute_file_paths) {
  FileSuggestKeyedServiceFactory::GetInstance()
      ->GetService(profile())
      ->RemoveSuggestionsAndNotify(absolute_file_paths);
}

void HoldingSpaceSuggestionsDelegate::OnHoldingSpaceItemsAdded(
    const std::vector<const HoldingSpaceItem*>& items) {
  if (base::ranges::any_of(items, [&](const HoldingSpaceItem* item) {
        return item->IsInitialized() &&
               ItemIsPinnedSuggestion(item, suggestions_by_type_);
      })) {
    // Update suggestions asynchronously to avoid updating suggestions along
    // with other model updates.
    MaybeScheduleUpdateSuggestionsInModel();
  }
}

void HoldingSpaceSuggestionsDelegate::OnHoldingSpaceItemsRemoved(
    const std::vector<const HoldingSpaceItem*>& items) {
  if (base::ranges::any_of(items, [&](const HoldingSpaceItem* item) {
        return item->IsInitialized() &&
               ItemIsPinnedSuggestion(item, suggestions_by_type_);
      })) {
    // Update suggestions asynchronously to avoid updating suggestions along
    // with other model updates.
    MaybeScheduleUpdateSuggestionsInModel();
  }
}

void HoldingSpaceSuggestionsDelegate::OnHoldingSpaceItemInitialized(
    const HoldingSpaceItem* item) {
  if (ItemIsPinnedSuggestion(item, suggestions_by_type_)) {
    // Update suggestions asynchronously to avoid updating suggestions along
    // with other model updates.
    MaybeScheduleUpdateSuggestionsInModel();
  }
}

void HoldingSpaceSuggestionsDelegate::OnPersistenceRestored() {
  // Initialize `suggestions_by_type_` with the restored suggestions. The model
  // items are iterated reversely so that the suggestions of the same category
  // in `suggestions_by_type_` follow the relevance order.
  DCHECK(suggestions_by_type_.empty());
  for (const auto& item : base::Reversed(model()->items())) {
    // Skip if `item` is not a suggestion.
    if (HoldingSpaceItem::IsSuggestionType(item->type())) {
      suggestions_by_type_[item->type()].push_back(item->file().file_path);
    }
  }

  file_suggest_service_observation_.Observe(
      FileSuggestKeyedServiceFactory::GetInstance()->GetService(profile()));

  MaybeFetchSuggestions(FileSuggestionType::kDriveFile);
  MaybeFetchSuggestions(FileSuggestionType::kLocalFile);
}

void HoldingSpaceSuggestionsDelegate::OnFileSuggestionUpdated(
    FileSuggestionType type) {
  MaybeFetchSuggestions(type);
}

void HoldingSpaceSuggestionsDelegate::MaybeFetchSuggestions(
    FileSuggestionType type) {
  // A data query on `type` has been sent so it is unnecessary to send a request
  // again. Return early.
  if (base::Contains(pending_fetches_, type))
    return;

  // Mark that the query for suggestions of `type` has been sent.
  pending_fetches_.insert(type);

  FileSuggestKeyedServiceFactory::GetInstance()
      ->GetService(profile())
      ->GetSuggestFileData(
          type,
          base::BindOnce(&HoldingSpaceSuggestionsDelegate::OnSuggestionsFetched,
                         weak_factory_.GetWeakPtr(), type));
}

void HoldingSpaceSuggestionsDelegate::MaybeScheduleUpdateSuggestionsInModel() {
  // Return early if the task of updating model suggestions has been scheduled.
  if (suggestion_update_timer_.IsRunning())
    return;

  suggestion_update_timer_.Start(
      FROM_HERE, /*delay=*/base::TimeDelta(),
      base::BindOnce(&HoldingSpaceSuggestionsDelegate::UpdateSuggestionsInModel,
                     weak_factory_.GetWeakPtr()));
}

void HoldingSpaceSuggestionsDelegate::OnSuggestionsFetched(
    FileSuggestionType type,
    const std::optional<std::vector<FileSuggestData>>& suggestions) {
  // Mark that the suggestions of `type` have been fetched.
  size_t deleted_size = pending_fetches_.erase(type);
  DCHECK_EQ(1u, deleted_size);

  if (!suggestions)
    return;

  // Extract file paths from `suggestions`.
  std::vector<base::FilePath> updated_suggestions(suggestions->size());
  base::ranges::transform(*suggestions, updated_suggestions.begin(),
                          &FileSuggestData::file_path);

  // No-op if `updated_suggestions` are unchanged.
  const HoldingSpaceItem::Type item_type = GetItemTypeFromSuggestionType(type);
  if (auto it = suggestions_by_type_.find(item_type);
      it != suggestions_by_type_.end() && it->second == updated_suggestions) {
    return;
  }

  // Update cache and model.
  suggestions_by_type_[item_type] = std::move(updated_suggestions);
  UpdateSuggestionsInModel();
}

void HoldingSpaceSuggestionsDelegate::UpdateSuggestionsInModel() {
  std::vector<std::pair<HoldingSpaceItem::Type, base::FilePath>>
      suggestion_items;
  base::FilePath downloads_folder =
      file_manager::util::GetDownloadsFolderForProfile(profile());
  for (const auto& [type, suggested_file_paths] : suggestions_by_type_) {
    for (const auto& file_path : suggested_file_paths) {
      if (file_path != downloads_folder &&
          !model()->ContainsItem(HoldingSpaceItem::Type::kPinnedFile,
                                 file_path)) {
        suggestion_items.emplace_back(type, file_path);
      }
    }
  }

  service()->SetSuggestions(suggestion_items);
}

}  // namespace ash