chromium/chrome/browser/ash/file_suggest/local_file_suggestion_provider.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/ash/file_suggest/local_file_suggestion_provider.h"

#include <optional>
#include <vector>

#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/ranges/algorithm.h"
#include "base/time/time.h"
#include "chrome/browser/ash/app_list/search/files/justifications.h"
#include "chrome/browser/ash/app_list/search/ranking/util.h"
#include "chrome/browser/ash/app_list/search/util/mrfu_cache.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_suggest/file_suggest_util.h"
#include "chrome/browser/ash/file_suggest/file_suggestion_provider.h"
#include "chrome/browser/profiles/profile.h"

namespace ash {

namespace {
constexpr base::TimeDelta kSaveDelay = base::Seconds(3);
constexpr base::TimeDelta kSuggestionNotificationDebounce =
    base::Milliseconds(100);

// Given the output of MrfuCache::GetAll, partition files into valid and invalid
// files. Valid files are files that:
// - Exist on-disk
// - Have been modified in the last |max_last_modified_time| days
std::pair<std::vector<LocalFileSuggestionProvider::LocalFileData>,
          std::vector<base::FilePath>>
ValidateFiles(const std::vector<std::pair<std::string, float>>& ranker_results,
              const base::TimeDelta& max_last_modified_time,
              std::vector<base::FilePath> trash_paths) {
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  std::vector<LocalFileSuggestionProvider::LocalFileData> valid_results;
  std::vector<base::FilePath> invalid_results;
  const base::Time now = base::Time::Now();
  for (const auto& path_score : ranker_results) {
    // We use FilePath::FromUTF8Unsafe to decode the filepath string. As per its
    // documentation, this is a safe use of the function because
    // LocalFileSuggestionProvider is only used on ChromeOS, for which filepaths
    // are UTF8.
    const auto& path = base::FilePath::FromUTF8Unsafe(path_score.first);

    // Exclude any paths that are parented at an enabled trash location.
    if (base::ranges::any_of(trash_paths,
                             [&path](const base::FilePath& trash_path) {
                               return trash_path.IsParent(path);
                             })) {
      invalid_results.emplace_back(path);
      continue;
    }

    base::File::Info info;
    if (base::PathExists(path) && base::GetFileInfo(path, &info) &&
        (now - info.last_modified <= max_last_modified_time)) {
      valid_results.emplace_back(LocalFileSuggestionProvider::LocalFileData{
          path_score.second, path, info});
    } else {
      invalid_results.emplace_back(path);
    }
  }
  return {valid_results, invalid_results};
}

}  // anonymous namespace

LocalFileSuggestionProvider::LocalFileSuggestionProvider(
    Profile* profile,
    base::RepeatingCallback<void(FileSuggestionType)> notify_update_callback)
    : FileSuggestionProvider(notify_update_callback),
      profile_(profile),
      max_last_modified_time_(GetMaxFileSuggestionRecency()) {
  DCHECK(profile_);

  task_runner_ = base::ThreadPool::CreateSequencedTaskRunner(
      {base::TaskPriority::USER_BLOCKING, base::MayBlock(),
       base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN});

  auto* notifier =
      file_manager::file_tasks::FileTasksNotifier::GetForProfile(profile);

  if (notifier) {
    file_tasks_observer_.Observe(notifier);

    app_list::MrfuCache::Params params;
    // 5 consecutive clicks to get a new file to a score of 0.8, and 10 clicks
    // on other files to reduce its score by half.
    params.half_life = 10.0f;
    params.boost_factor = 5.0f;
    app_list::MrfuCache::Proto proto(
        app_list::RankerStateDirectory(profile).AppendASCII(
            "zero_state_local_files.pb"),
        kSaveDelay);

    // `proto` is owned by `files_ranker_` which is a class member so it is safe
    // to call `RegisterOnInitUnsafe()`.
    proto.RegisterOnInitUnsafe(
        base::BindOnce(&LocalFileSuggestionProvider::OnProtoInitialized,
                       base::Unretained(this)));

    files_ranker_ =
        std::make_unique<app_list::MrfuCache>(std::move(proto), params);
  }
}

LocalFileSuggestionProvider::~LocalFileSuggestionProvider() = default;

bool LocalFileSuggestionProvider::IsInitialized() const {
  return files_ranker_ && files_ranker_->initialized();
}

void LocalFileSuggestionProvider::GetSuggestFileData(
    GetSuggestFileDataCallback callback) {
  if (!files_ranker_ || !files_ranker_->initialized()) {
    std::move(callback).Run(std::nullopt);
    return;
  }

  if (!on_validation_complete_callback_list_.empty()) {
    on_validation_complete_callback_list_.AddUnsafe(std::move(callback));
    return;
  }

  on_validation_complete_callback_list_.AddUnsafe(std::move(callback));

  // Generate the trash paths on the first get suggestion of file data. This is
  // to enable unit tests to mock out the trash paths appropriately.
  if (trash_paths_.empty()) {
    auto enabled_trash_locations =
        file_manager::trash::GenerateEnabledTrashLocationsForProfile(
            profile_, /*base_path=*/base::FilePath());
    for (const auto& it : enabled_trash_locations) {
      trash_paths_.emplace_back(
          it.first.Append(it.second.relative_folder_path));
    }
  }

  task_runner_->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&ValidateFiles, files_ranker_->GetAll(),
                     max_last_modified_time_,
                     (file_manager::trash::IsTrashEnabledForProfile(profile_)
                          ? trash_paths_
                          : std::vector<base::FilePath>())),
      base::BindOnce(&LocalFileSuggestionProvider::OnValidationComplete,
                     weak_factory_.GetWeakPtr()));
}

void LocalFileSuggestionProvider::MaybeUpdateItemSuggestCache(
    base::PassKey<FileSuggestKeyedService>) {
  NOTREACHED_IN_MIGRATION();
}

void LocalFileSuggestionProvider::OnFilesOpened(
    const std::vector<FileOpenEvent>& file_opens) {
  if (!files_ranker_) {
    return;
  }

  const auto& profile_path = profile_->GetPath();
  for (const auto& file_open : file_opens) {
    // Filter out file opens if:
    // 1. The open event is not a kLaunch or a kOpen.
    if (file_open.open_type != FileTasksObserver::OpenType::kLaunch &&
        file_open.open_type != FileTasksObserver::OpenType::kOpen) {
      continue;
    }

    // 2. The open relates to a Drive file, which is handled by another
    // provider. Filter this out by checking if the file resides in the user's
    // cryptohome.
    if (!profile_path.IsParent(file_open.path) &&
        !file_manager::util::GetMyFilesFolderForProfile(profile_).IsParent(
            file_open.path) &&
        !file_manager::util::GetDownloadsFolderForProfile(profile_).IsParent(
            file_open.path)) {
      continue;
    }

    files_ranker_->Use(file_open.path.value());
  }

  if (!queued_notification_.IsRunning()) {
    queued_notification_.Start(
        FROM_HERE, kSuggestionNotificationDebounce,
        base::BindOnce(&LocalFileSuggestionProvider::NotifySuggestionUpdate,
                       weak_factory_.GetWeakPtr(),
                       FileSuggestionType::kLocalFile));
  }
}

void LocalFileSuggestionProvider::OnProtoInitialized() {
  NotifySuggestionUpdate(FileSuggestionType::kLocalFile);
}

void LocalFileSuggestionProvider::OnValidationComplete(
    std::pair<std::vector<LocalFileData>, std::vector<base::FilePath>>
        results) {
  // Delete invalid results from the ranker.
  for (const base::FilePath& path : results.second) {
    files_ranker_->Delete(path.value());
  }

  std::vector<FileSuggestData> final_results;
  for (auto& result : results.first) {
    std::optional<std::u16string> justification_string;
    if (result.info.last_accessed > result.info.last_modified) {
      justification_string = app_list::GetJustificationString(
          FileSuggestionJustificationType::kViewed, result.info.last_accessed,
          /*user_name=*/"");
    } else {
      justification_string = app_list::GetJustificationString(
          FileSuggestionJustificationType::kModifiedByCurrentUser,
          result.info.last_modified,
          /*user_name=*/"");
    }

    final_results.emplace_back(FileSuggestionType::kLocalFile, result.path,
                               /*title=*/std::nullopt, justification_string,
                               /*modified_time=*/result.info.last_modified,
                               /*viewed_time=*/result.info.last_accessed,
                               /*shared_time=*/std::nullopt, result.score,
                               /*drive_file_id=*/std::nullopt,
                               /*icon_url=*/std::nullopt);
  }

  // Sort valid results high-to-low by score.
  std::sort(final_results.begin(), final_results.end(),
            [](const auto& a, const auto& b) {
              return a.score.value() > b.score.value();
            });

  on_validation_complete_callback_list_.Notify(final_results);
  DCHECK(on_validation_complete_callback_list_.empty());
}

}  // namespace ash