blob: 9e2a393a3f8f235f0b5bce6647d14258bba7d649 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef COMPONENTS_NTP_SNIPPETS_REMOTE_REMOTE_SUGGESTIONS_PROVIDER_IMPL_H_
#define COMPONENTS_NTP_SNIPPETS_REMOTE_REMOTE_SUGGESTIONS_PROVIDER_IMPL_H_
#include <cstddef>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "base/callback_forward.h"
#include "base/containers/circular_deque.h"
#include "base/gtest_prod_util.h"
#include "base/macros.h"
#include "base/optional.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "components/ntp_snippets/category.h"
#include "components/ntp_snippets/category_status.h"
#include "components/ntp_snippets/content_suggestion.h"
#include "components/ntp_snippets/content_suggestions_provider.h"
#include "components/ntp_snippets/logger.h"
#include "components/ntp_snippets/remote/cached_image_fetcher.h"
#include "components/ntp_snippets/remote/json_to_categories.h"
#include "components/ntp_snippets/remote/prefetched_pages_tracker.h"
#include "components/ntp_snippets/remote/remote_suggestion.h"
#include "components/ntp_snippets/remote/remote_suggestions_fetcher.h"
#include "components/ntp_snippets/remote/remote_suggestions_provider.h"
#include "components/ntp_snippets/remote/remote_suggestions_status_service.h"
#include "components/ntp_snippets/remote/request_params.h"
#include "components/ntp_snippets/remote/request_throttler.h"
class PrefRegistrySimple;
class PrefService;
namespace image_fetcher {
class ImageFetcher;
} // namespace image_fetcher
namespace ntp_snippets {
class CategoryRanker;
class RemoteSuggestionsDatabase;
class RemoteSuggestionsScheduler;
// Retrieves fresh content data (articles) from the server, stores them and
// provides them as content suggestions.
// This class is final because it does things in its constructor which make it
// unsafe to derive from it.
// TODO(treib): Introduce two-phase initialization and make the class not final?
class RemoteSuggestionsProviderImpl final : public RemoteSuggestionsProvider {
public:
// |application_language_code| should be a ISO 639-1 compliant string, e.g.
// 'en' or 'en-US'. Note that this code should only specify the language, not
// the locale, so 'en_US' (English language with US locale) and 'en-GB_US'
// (British English person in the US) are not language codes.
RemoteSuggestionsProviderImpl(
Observer* observer,
PrefService* pref_service,
const std::string& application_language_code,
CategoryRanker* category_ranker,
RemoteSuggestionsScheduler* scheduler,
std::unique_ptr<RemoteSuggestionsFetcher> suggestions_fetcher,
std::unique_ptr<image_fetcher::ImageFetcher> image_fetcher,
std::unique_ptr<RemoteSuggestionsDatabase> database,
std::unique_ptr<RemoteSuggestionsStatusService> status_service,
std::unique_ptr<PrefetchedPagesTracker> prefetched_pages_tracker,
Logger* debug_logger,
std::unique_ptr<base::OneShotTimer> fetch_timeout_timer);
~RemoteSuggestionsProviderImpl() override;
static void RegisterProfilePrefs(PrefRegistrySimple* registry);
// Returns whether the service is successfully initialized. While this is
// false, some calls may trigger DCHECKs.
bool initialized() const { return ready() || state_ == State::DISABLED; }
// RemoteSuggestionsProvider implementation.
void RefetchInTheBackground(FetchStatusCallback callback) override;
void RefetchWhileDisplaying(FetchStatusCallback callback) override;
// TODO(fhorschig): Remove this getter when there is an interface for the
// fetcher that allows better mocks.
const RemoteSuggestionsFetcher* suggestions_fetcher_for_debugging()
const override;
GURL GetUrlWithFavicon(
const ContentSuggestion::ID& suggestion_id) const override;
bool IsDisabled() const override;
bool ready() const override;
// ContentSuggestionsProvider implementation.
CategoryStatus GetCategoryStatus(Category category) override;
CategoryInfo GetCategoryInfo(Category category) override;
void DismissSuggestion(const ContentSuggestion::ID& suggestion_id) override;
void FetchSuggestionImage(const ContentSuggestion::ID& suggestion_id,
ImageFetchedCallback callback) override;
void FetchSuggestionImageData(const ContentSuggestion::ID& suggestion_id,
ImageDataFetchedCallback callback) override;
void Fetch(const Category& category,
const std::set<std::string>& known_suggestion_ids,
FetchDoneCallback callback) override;
void ReloadSuggestions() override;
void ClearHistory(
base::Time begin,
base::Time end,
const base::Callback<bool(const GURL& url)>& filter) override;
void ClearCachedSuggestions() override;
void OnSignInStateChanged(bool has_signed_in) override;
void GetDismissedSuggestionsForDebugging(
Category category,
DismissedSuggestionsCallback callback) override;
void ClearDismissedSuggestionsForDebugging(Category category) override;
// Returns the maximum number of suggestions we expect to receive from the
// server during a normal (not fetch-more) fetch..
static int GetMaxNormalFetchSuggestionCountForTesting();
// Available suggestions, only for unit tests.
// TODO(treib): Get rid of this. Tests should use a fake observer instead.
const RemoteSuggestion::PtrVector& GetSuggestionsForTesting(
Category category) const {
return category_contents_.find(category)->second.suggestions;
}
// Dismissed suggestions, only for unit tests.
const RemoteSuggestion::PtrVector& GetDismissedSuggestionsForTesting(
Category category) const {
return category_contents_.find(category)->second.dismissed;
}
// Overrides internal clock for testing purposes.
void SetClockForTesting(base::Clock* clock) { clock_ = clock; }
// TODO(tschumann): remove this method as soon as we inject the fetcher into
// the constructor.
CachedImageFetcher& GetImageFetcherForTesting() { return image_fetcher_; }
private:
friend class RemoteSuggestionsProviderImplTest;
// TODO(jkrcal): Mock the database to trigger the error naturally (or remove
// the error state and get rid of the test).
FRIEND_TEST_ALL_PREFIXES(RemoteSuggestionsProviderImplTest,
CallsSchedulerOnError);
// TODO(jkrcal): Mock the status service and remove these friend declarations.
FRIEND_TEST_ALL_PREFIXES(RemoteSuggestionsProviderImplTest,
CallsSchedulerWhenDisabled);
FRIEND_TEST_ALL_PREFIXES(RemoteSuggestionsProviderImplTest,
DontNotifyIfNotAvailable);
FRIEND_TEST_ALL_PREFIXES(RemoteSuggestionsProviderImplTest,
CallsSchedulerWhenSignedIn);
FRIEND_TEST_ALL_PREFIXES(RemoteSuggestionsProviderImplTest,
CallsSchedulerWhenSignedOut);
FRIEND_TEST_ALL_PREFIXES(RemoteSuggestionsProviderImplTest,
RestartsFetchWhenSignedInWhileFetching);
FRIEND_TEST_ALL_PREFIXES(RemoteSuggestionsProviderImplTest,
ShouldHandleCategoryDisabledBeforeTimeout);
FRIEND_TEST_ALL_PREFIXES(
RemoteSuggestionsProviderImplTest,
ShouldNotSetExclusiveCategoryWhenFetchingSuggestions);
// Possible state transitions:
// NOT_INITED --------+
// / \ |
// v v |
// READY <--> DISABLED |
// \ / |
// v v |
// ERROR_OCCURRED <-----+
// TODO(jkrcal): Do we need to keep the distinction between states DISABLED
// and ERROR_OCCURED?
enum class State {
// The service has just been created. Can change to states:
// - DISABLED: After the database is done loading,
// GetStateForDependenciesStatus can identify the next state to
// be DISABLED.
// - READY: if GetStateForDependenciesStatus returns it, after the database
// is done loading.
// - ERROR_OCCURRED: when an unrecoverable error occurred.
NOT_INITED,
// The service registered observers, timers, etc. and is ready to answer to
// queries, fetch suggestions... Can change to states:
// - DISABLED: when the global Chrome state changes, for example after
// |OnStateChanged| is called and sync is disabled.
// - ERROR_OCCURRED: when an unrecoverable error occurred.
READY,
// The service is disabled and unregistered the related resources.
// Can change to states:
// - READY: when the global Chrome state changes, for example after
// |OnStateChanged| is called and sync is enabled.
// - ERROR_OCCURRED: when an unrecoverable error occurred.
DISABLED,
// The service or one of its dependencies encountered an unrecoverable error
// and the service can't be used anymore.
ERROR_OCCURRED,
COUNT
};
// Documents the status of the ongoing request and what action should be taken
// on completion.
enum class FetchRequestStatus {
// There is no request in progress for remote suggestions.
NONE,
// There is a valid request in progress that should be treated normally on
// completion.
IN_PROGRESS,
// There is a canceled request in progress. The response should be ignored
// when it arrives.
IN_PROGRESS_CANCELED,
// There is an invalidated request in progress. On completion, we should
// ignore the response and initiate a new fetch (with updated parameters).
IN_PROGRESS_NEEDS_REFETCH
};
struct CategoryContent {
// The current status of the category.
CategoryStatus status = CategoryStatus::INITIALIZING;
// The additional information about a category.
CategoryInfo info;
// TODO(vitaliii): Remove this field. It is always true, because we now
// remove categories not included in the last fetch.
// True iff the server returned results in this category in the last fetch.
// We never remove categories that the server still provides.
bool included_in_last_server_response = true;
// All currently active suggestions (excl. the dismissed ones).
RemoteSuggestion::PtrVector suggestions;
// All previous suggestions that we keep around in memory because they can
// be on some open NTP. We do not persist this list so that on a new start
// of Chrome, this is empty.
// |archived| is a FIFO buffer with a maximum length.
base::circular_deque<std::unique_ptr<RemoteSuggestion>> archived;
// Suggestions that the user dismissed. We keep these around until they
// expire so we won't re-add them to |suggestions| on the next fetch.
RemoteSuggestion::PtrVector dismissed;
// Returns a non-dismissed suggestion with the given |id_within_category|,
// or null if none exist.
const RemoteSuggestion* FindSuggestion(
const std::string& id_within_category) const;
explicit CategoryContent(const CategoryInfo& info);
CategoryContent(CategoryContent&&);
~CategoryContent();
CategoryContent& operator=(CategoryContent&&);
};
// Fetches suggestions from the server and replaces old suggestions by the new
// ones. Requests can be marked more important by setting
// |interactive_request| to true (such request might circumvent the daily
// quota for requests, etc.), useful for requests triggered by the user. After
// the fetch finished, the provided |callback| will be triggered with the
// status of the fetch.
void FetchSuggestions(bool interactive_request, FetchStatusCallback callback);
// Similar To FetchSuggestions, only adds a loading indicator on top of that.
// If |enable_loading_indication_timeout| is true, the indicator is hidden if
// the fetch does not finish within a certain amount of time (the fetch itself
// is not canceled, though).
void FetchSuggestionsWithLoadingIndicator(
bool interactive_request,
FetchStatusCallback callback,
bool enable_loading_indication_timeout);
void OnFetchSuggestionsWithLoadingIndicatorFinished(
FetchStatusCallback callback,
Status status);
// Returns the URL of the image of a suggestion if it is among the current or
// among the archived suggestions in the matching category. Returns an empty
// URL otherwise.
GURL FindSuggestionImageUrl(const ContentSuggestion::ID& suggestion_id) const;
// Callbacks for the RemoteSuggestionsDatabase.
void OnDatabaseLoaded(RemoteSuggestion::PtrVector suggestions);
void OnDatabaseError();
// Callback for fetch-more requests with the RemoteSuggestionsFetcher.
void OnFetchMoreFinished(
FetchDoneCallback fetching_callback,
Status status,
RemoteSuggestionsFetcher::OptionalFetchedCategories fetched_categories);
// Callback for regular fetch requests with the RemoteSuggestionsFetcher.
void OnFetchFinished(
FetchStatusCallback callback,
bool interactive_request,
Status status,
RemoteSuggestionsFetcher::OptionalFetchedCategories fetched_categories);
// Moves all suggestions from |to_archive| into the archive of the |content|.
// Clears |to_archive|. As the archive is a FIFO buffer of limited size, this
// function will also delete images from the database in case the associated
// suggestion gets evicted from the archive.
void ArchiveSuggestions(CategoryContent* content,
RemoteSuggestion::PtrVector* to_archive);
// Sanitizes newly fetched suggestions -- e.g. adding missing dates and
// filtering out incomplete results or dismissed suggestions (indicated by
// |dismissed|).
void SanitizeReceivedSuggestions(const RemoteSuggestion::PtrVector& dismissed,
RemoteSuggestion::PtrVector* suggestions);
// Adds newly available suggestions to |content| corresponding to |category|.
void IntegrateSuggestions(Category category,
CategoryContent* content,
RemoteSuggestion::PtrVector new_suggestions);
// Adds newly available suggestion at the top of Articles category.
void PrependArticleSuggestion(
std::unique_ptr<RemoteSuggestion> remote_suggestion);
// Refreshes the content suggestions upon receiving a push-to-refresh request.
void RefreshSuggestionsUponPushToRefreshRequest();
// Dismisses a suggestion within a given category content.
// Note that this modifies the suggestion datastructures of |content|
// invalidating iterators.
void DismissSuggestionFromCategoryContent(
CategoryContent* content,
const std::string& id_within_category);
// Sets categories status to NOT_PROVIDED and deletes them (including their
// suggestions from the database).
void DeleteCategories(const std::vector<Category>& categories);
// Removes expired dismissed suggestions from the service and the database.
void ClearExpiredDismissedSuggestions();
// Removes images from the DB that are not referenced from any known
// suggestion. Needs to iterate the whole suggestion database -- so do it
// often enough to keep it small but not too often as it still iterates over
// the file system.
void ClearOrphanedImages();
// Clears suggestions because any history item has been removed.
void ClearHistoryDependentState();
// Clears the cached suggestions
void ClearCachedSuggestionsImpl();
// Clears all stored suggestions and updates the observer.
void NukeAllSuggestions();
// Completes the initialization phase of the service, registering the last
// observers. This is done after construction, once the database is loaded.
void FinishInitialization();
// Triggers a state transition depending on the provided status. This method
// is called when a change is detected by |status_service_|.
void OnStatusChanged(RemoteSuggestionsStatus old_status,
RemoteSuggestionsStatus new_status);
// Verifies state transitions (see |State|'s documentation) and applies them.
// Also updates the provider status. Does nothing except updating the provider
// status if called with the current state.
void EnterState(State state);
// Notifies the state change to ProviderStatusCallback specified by
// SetProviderStatusCallback().
void NotifyStateChanged();
// Converts the given |suggestions| to content suggestions and notifies the
// observer with them for category |category|.
void NotifyNewSuggestions(Category category,
const RemoteSuggestion::PtrVector& suggestions);
// Updates the internal status for |category| to |category_status_| and
// notifies the content suggestions observer if it changed.
void UpdateCategoryStatus(Category category, CategoryStatus status);
// Calls UpdateCategoryStatus() for all provided categories.
void UpdateAllCategoryStatus(CategoryStatus status);
// Updates the category info for |category|. If a corresponding
// CategoryContent object does not exist, it will be created.
// Returns the existing or newly created object.
CategoryContent* UpdateCategoryInfo(Category category,
const CategoryInfo& info);
void RestoreCategoriesFromPrefs();
void StoreCategoriesToPrefs();
// If |fetched_category| is nullopt, fetches all categories. Otherwise,
// fetches at most |count_to_fetch| suggestions only from |fetched_category|.
// TODO(vitaliii): Also support |count_to_fetch| when |fetched_category| is
// nullopt.
RequestParams BuildFetchParams(base::Optional<Category> fetched_category,
int count_to_fetch) const;
bool AreArticlesEmpty() const;
bool AreArticlesAvailable() const;
void NotifyFetchWithLoadingIndicatorStarted();
void NotifyFetchWithLoadingIndicatorFailedOrTimeouted();
GURL GetImageURLToFetch(const ContentSuggestion::ID& suggestion_id) const;
State state_;
PrefService* pref_service_;
const Category articles_category_;
std::map<Category, CategoryContent, Category::CompareByID> category_contents_;
// The ISO 639-1 code of the language used by the application.
const std::string application_language_code_;
// Ranker that orders the categories. Not owned.
CategoryRanker* category_ranker_;
// Scheduler to inform about scheduling-related events. Not owned.
RemoteSuggestionsScheduler* remote_suggestions_scheduler_;
// The suggestions fetcher.
std::unique_ptr<RemoteSuggestionsFetcher> suggestions_fetcher_;
// The database for persisting suggestions.
std::unique_ptr<RemoteSuggestionsDatabase> database_;
base::TimeTicks database_load_start_;
// The image fetcher.
CachedImageFetcher image_fetcher_;
// The service that provides events and data about the signin and sync state.
std::unique_ptr<RemoteSuggestionsStatusService> status_service_;
// Set to true if ClearHistoryDependentState is called while the service isn't
// ready. The nuke will be executed once the service finishes initialization
// or enters the READY state.
bool clear_history_dependent_state_when_initialized_;
// Set to true if ClearCachedSuggestions has been called while the service
// isn't ready. The clearing will be executed once the service finishes
// initialization or enters the READY state.
bool clear_cached_suggestions_when_initialized_;
// A clock for getting the time. This allows to inject a clock in tests.
base::Clock* clock_;
// Prefetched pages tracker to query which urls have been prefetched.
// |nullptr| is handled gracefully and just disables the functionality.
std::unique_ptr<PrefetchedPagesTracker> prefetched_pages_tracker_;
// Additional logging, accesible through snippets-internals.
Logger* debug_logger_;
// A Timer for canceling too long fetches.
std::unique_ptr<base::OneShotTimer> fetch_timeout_timer_;
// Keeps track of the status of the ongoing request(s) and what action should
// be taken on completion. Requests via Fetch() (fetching more) are _not_
// tracked by this variable (as they do not need any special actions on
// completion).
FetchRequestStatus request_status_;
DISALLOW_COPY_AND_ASSIGN(RemoteSuggestionsProviderImpl);
};
} // namespace ntp_snippets
#endif // COMPONENTS_NTP_SNIPPETS_REMOTE_REMOTE_SUGGESTIONS_PROVIDER_IMPL_H_