chromium/chrome/browser/ash/app_list/search/search_file_scanner.cc

// Copyright 2024 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/app_list/search/search_file_scanner.h"

#include <map>
#include <string_view>
#include <utility>

#include "ash/constants/ash_pref_names.h"
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_map.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/ranges/algorithm.h"
#include "base/strings/strcat.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/profiles/profile.h"
#include "components/prefs/pref_service.h"
#include "ui/base/metadata/base_type_conversion.h"

namespace app_list {

namespace {

// The minimum time in days required to wait after the previous file scan
// logging. Assuming that the user local file won't change dramatically, log at
// most once per week to minimize the power and performance impact.
constexpr int kMinFileScanDelayDays = 7;

// The delay before we start the file scan when the file is constructed.
constexpr base::TimeDelta kFileScanDelay = base::Minutes(10);

// The key for total file numbers in `extension_file_counts`.
constexpr std::string_view kTotal = "Total";

// The list of file extensions we are interested in.
// It is in the form of {"extension format", "extension name"}, with the format
// to match the file extension and the name to get the uma metric name.
// Note:
//    When adding new extension to this map, also updates the
//    `SearchFileExtension` variants in
//    `tools/metrics/histograms/metadata/apps/histograms.xml`.
constexpr auto kExtensionMap =
    base::MakeFixedFlatMap<std::string_view, std::string_view>({
        // Image extensions.
        {".png", "Png"},
        {".jpg", "Jpg"},
        {".jpeg", "Jpeg"},
        {".webp", "Webp"},
    });

base::Time GetLastScanLogTime(Profile* profile) {
  return profile->GetPrefs()->GetTime(
      ash::prefs::kLauncherSearchLastFileScanLogTime);
}

void SetLastScanLogTime(Profile* profile, const base::Time& time) {
  profile->GetPrefs()->SetTime(ash::prefs::kLauncherSearchLastFileScanLogTime,
                               time);
}

// Looks up and counts the total file number and the file numbers of each
// extension that is of interest in the `search_path` and excludes the ones that
// overlaps with `trash_paths`.
//
// This function can be executed on any thread other than the UI thread, while
// the `SearchFileScanner` is created on UI thread.
void FullScan(const base::FilePath& search_path,
              const std::vector<base::FilePath>& trash_paths) {
  base::Time start_time = base::Time::Now();
  base::FileEnumerator file_enumerator(search_path, /*recursive=*/true,
                                       base::FileEnumerator::FILES);

  std::map<std::string_view, int> extension_file_counts;
  for (base::FilePath file_path = file_enumerator.Next(); !file_path.empty();
       file_path = file_enumerator.Next()) {
    // Exclude any paths that are parented at an enabled trash location.
    if (base::ranges::any_of(trash_paths,
                             [&file_path](const base::FilePath& trash_path) {
                               return trash_path.IsParent(file_path);
                             })) {
      continue;
    }
    // Always counts the total file number.
    extension_file_counts[kTotal]++;

    // Increments the extension count if the extension of the current file is of
    // interest.
    const auto extension_lookup =
        kExtensionMap.find(base::ToLowerASCII(file_path.Extension()));
    if (extension_lookup != kExtensionMap.end()) {
      extension_file_counts[extension_lookup->second]++;
    }
  }

  // Logs execution time.
  base::UmaHistogramTimes("Apps.AppList.SearchFileScan.ExecutionTime",
                          base::Time::Now() - start_time);

  // Logs file extension count data.
  for (const auto& it : extension_file_counts) {
    base::UmaHistogramCounts100000(
        base::StrCat({"Apps.AppList.SearchFileScan.", it.first}), it.second);
  }
}

}  // namespace

SearchFileScanner::SearchFileScanner(
    Profile* profile,
    const base::FilePath& root_path,
    const std::vector<base::FilePath>& excluded_paths,
    std::optional<base::TimeDelta> start_delay_override)
    : profile_(profile),
      root_path_(root_path),
      excluded_paths_(excluded_paths) {
  // Early returns if file scan has been done recently.
  // TODO(b/337130427) considering replacing this with a timer, which can covers
  // the users which does not logout frequently.
  if (profile_ && base::Time::Now() - GetLastScanLogTime(profile_) <
                      base::Days(kMinFileScanDelayDays)) {
    return;
  }

  // Delays the file scan so that it does not add regressions to the user login.
  // Skips the delay in test.
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&SearchFileScanner::StartFileScan,
                     weak_factory_.GetWeakPtr()),
      start_delay_override.value_or(kFileScanDelay));
}

SearchFileScanner::~SearchFileScanner() = default;

void SearchFileScanner::StartFileScan() {
  // Do the file scan on a non-UI thread.
  base::ThreadPool::PostTaskAndReply(
      FROM_HERE,
      {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
       base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
      base::BindOnce(&FullScan, std::move(root_path_),
                     std::move(excluded_paths_)),
      base::BindOnce(&SearchFileScanner::OnScanComplete,
                     weak_factory_.GetWeakPtr()));
}

void SearchFileScanner::OnScanComplete() {
  // Ensures the `profile_` is still alive to avoid any possible crashes during
  // shutdown.
  if (profile_) {
    SetLastScanLogTime(profile_, base::Time::Now());
  }
}

}  // namespace app_list