chromium/chrome/browser/ash/app_list/search/files/zero_state_drive_provider.cc

// Copyright 2020 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/files/zero_state_drive_provider.h"

#include <memory>
#include <optional>

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "base/trace_event/common/trace_event_common.h"
#include "base/trace_event/trace_event.h"
#include "chrome/browser/ash/app_list/search/search_controller.h"
#include "chrome/browser/ash/app_list/search/search_provider.h"
#include "chrome/browser/ash/drive/drive_integration_service.h"
#include "chrome/browser/ash/file_suggest/file_suggest_keyed_service.h"
#include "chrome/browser/ash/file_suggest/file_suggest_keyed_service_factory.h"
#include "chrome/browser/ash/file_suggest/file_suggest_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chromeos/dbus/power_manager/idle.pb.h"
#include "content/public/browser/browser_context.h"

namespace app_list {
namespace {

using SuggestResults = std::vector<ash::FileSuggestData>;

// How long to wait before making the first request for results from the
// ItemSuggestCache.
constexpr base::TimeDelta kFirstUpdateDelay = base::Seconds(10);

void LogLatency(base::TimeDelta latency) {
  base::UmaHistogramTimes("Apps.AppList.DriveZeroStateProvider.Latency",
                          latency);
}

}  // namespace

ZeroStateDriveProvider::ZeroStateDriveProvider(
    Profile* profile,
    SearchController* search_controller,
    drive::DriveIntegrationService* drive_service,
    session_manager::SessionManager* session_manager)
    : SearchProvider(SearchCategory::kFiles),
      profile_(profile),
      drive_service_(drive_service),
      session_manager_(session_manager),
      file_suggest_service_(
          ash::FileSuggestKeyedServiceFactory::GetInstance()->GetService(
              profile)),
      construction_time_(base::Time::Now()) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(profile_);

  // `FileSuggestKeyedServiceFactory` ensures to build the keyed
  // service when the app list syncable service is built. Meanwhile,
  // `ZeroStateDriveProvider` is built only when the app list syncable service
  // exists. Therefore, `file_suggest_service_` should always be true.
  DCHECK(file_suggest_service_);

  if (drive_service_) {
    if (drive_service_->IsMounted()) {
      // DriveFS is mounted, so we can fetch results immediately.
      OnFileSystemMounted();
    } else {
      // Wait for DriveFS to be mounted, then fetch results. This happens in
      // OnFileSystemMounted.
      Observe(drive_service_.get());
    }
  }

  if (session_manager_)
    session_observation_.Observe(session_manager_.get());

  auto* power_manager = chromeos::PowerManagerClient::Get();
  if (power_manager)
    power_observation_.Observe(power_manager);

  file_suggest_service_observation_.Observe(file_suggest_service_);
}

ZeroStateDriveProvider::~ZeroStateDriveProvider() = default;

void ZeroStateDriveProvider::OnFileSystemMounted() {
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&ZeroStateDriveProvider::MaybeUpdateCache,
                     update_cache_weak_factory_.GetWeakPtr()),
      kFirstUpdateDelay);
}

void ZeroStateDriveProvider::OnSessionStateChanged() {
  TRACE_EVENT0("ui", "ZeroStateDriveProvider::OnSessionStateChanged");
  // Update cache if the user has logged in.
  if (session_manager_->session_state() ==
      session_manager::SessionState::ACTIVE) {
    MaybeUpdateCache();
  }
}

void ZeroStateDriveProvider::ScreenIdleStateChanged(
    const power_manager::ScreenIdleState& proto) {
  // Update cache if the screen changed from off to on.
  if (screen_off_ && !proto.dimmed() && !proto.off()) {
    MaybeUpdateCache();
  }
  screen_off_ = proto.off();
}

void ZeroStateDriveProvider::StopZeroState() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // Cancel any in-flight queries for this provider.
  suggestion_query_weak_factory_.InvalidateWeakPtrs();

  // Update the cache for when the zero state is next requested.
  MaybeUpdateCache();
}

ash::AppListSearchResultType ZeroStateDriveProvider::ResultType() const {
  return ash::AppListSearchResultType::kZeroStateDrive;
}

void ZeroStateDriveProvider::StartZeroState() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  query_start_time_ = base::TimeTicks::Now();

  // Cancel any in-flight queries for this provider.
  suggestion_query_weak_factory_.InvalidateWeakPtrs();

  file_suggest_service_->GetSuggestFileData(
      ash::FileSuggestionType::kDriveFile,
      base::BindOnce(&ZeroStateDriveProvider::OnSuggestFileDataFetched,
                     suggestion_query_weak_factory_.GetWeakPtr()));
}

void ZeroStateDriveProvider::OnSuggestFileDataFetched(
    const std::optional<SuggestResults>& suggest_results) {
  // Fail to fetch the suggest data, so return early.
  if (!suggest_results) {
    // Send empty result list to search controller to unblock zero state.
    SearchProvider::Results empty_results;
    SwapResults(&empty_results);
    return;
  }

  SetSearchResults(*suggest_results);
}

void ZeroStateDriveProvider::SetSearchResults(
    const SuggestResults& suggest_results) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Assign scores to results by simply using their position in the results
  // list. The order of results from the ItemSuggest API is significant:
  // the first is better than the second, etc. Resulting scores are in [0, 1].
  //
  // If drive files and local files need to be mixed in continue section, create
  // ranking using time stamps, so local and drive files are consistently
  // ranked.
  const bool timestamp_based_score =
      ash::features::UseMixedFileLauncherContinueSection();

  const double total_items = static_cast<double>(suggest_results.size());
  int item_index = 0;

  const base::TimeDelta max_recency = ash::GetMaxFileSuggestionRecency();
  SearchProvider::Results provider_results;
  for (const auto& result : suggest_results) {
    const double score = timestamp_based_score
                             ? ash::ToTimestampBasedScore(result, max_recency)
                             : (1.0 - item_index / total_items);
    ++item_index;
    auto provider_result = std::make_unique<FileResult>(
        result.id, result.file_path, result.prediction_reason,
        ash::AppListSearchResultType::kZeroStateDrive,
        ash::SearchResultDisplayType::kContinue, score, std::u16string(),
        FileResult::Type::kFile, profile_, /*thumbnail_loader=*/nullptr);
    if (result.modified_time) {
      provider_result->SetContinueFileSuggestionType(
          ash::ContinueFileSuggestionType::kModifiedByCurrentUserDrive);
    } else if (result.viewed_time) {
      provider_result->SetContinueFileSuggestionType(
          ash::ContinueFileSuggestionType::kViewedDrive);
    } else if (result.shared_time) {
      provider_result->SetContinueFileSuggestionType(
          ash::ContinueFileSuggestionType::kSharedWithUserDrive);
    }

    provider_results.emplace_back(std::move(provider_result));
  }

  SwapResults(&provider_results);
  LogLatency(base::TimeTicks::Now() - query_start_time_);
}

void ZeroStateDriveProvider::MaybeUpdateCache() {
  if (base::Time::Now() - kFirstUpdateDelay > construction_time_) {
    file_suggest_service_->MaybeUpdateItemSuggestCache(
        base::PassKey<ZeroStateDriveProvider>());
  }
}

void ZeroStateDriveProvider::OnFileSuggestionUpdated(
    ash::FileSuggestionType type) {
  if (type == ash::FileSuggestionType::kDriveFile) {
    StartZeroState();
  }
}

}  // namespace app_list