blob: 38c98f199319b6398624718deb225e539524eac1 [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.
#include "components/ntp_snippets/ntp_snippets_service.h"
#include <algorithm>
#include <utility>
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/location.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "base/task_runner_util.h"
#include "base/time/time.h"
#include "base/values.h"
#include "components/ntp_snippets/pref_names.h"
#include "components/ntp_snippets/switches.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/suggestions/proto/suggestions.pb.h"
using suggestions::ChromeSuggestion;
using suggestions::SuggestionsProfile;
using suggestions::SuggestionsService;
namespace ntp_snippets {
namespace {
const int kFetchingIntervalWifiChargingSeconds = 30 * 60;
const int kFetchingIntervalWifiSeconds = 2 * 60 * 60;
const int kFetchingIntervalFallbackSeconds = 24 * 60 * 60;
const int kDefaultExpiryTimeMins = 24 * 60;
base::TimeDelta GetFetchingInterval(const char* switch_name,
int default_value_seconds) {
int value_seconds = default_value_seconds;
const base::CommandLine& cmdline = *base::CommandLine::ForCurrentProcess();
if (cmdline.HasSwitch(switch_name)) {
std::string str = cmdline.GetSwitchValueASCII(switch_name);
int switch_value_seconds = 0;
if (base::StringToInt(str, &switch_value_seconds))
value_seconds = switch_value_seconds;
else
LOG(WARNING) << "Invalid value for switch " << switch_name;
}
return base::TimeDelta::FromSeconds(value_seconds);
}
base::TimeDelta GetFetchingIntervalWifiCharging() {
return GetFetchingInterval(switches::kFetchingIntervalWifiChargingSeconds,
kFetchingIntervalWifiChargingSeconds);
}
base::TimeDelta GetFetchingIntervalWifi() {
return GetFetchingInterval(switches::kFetchingIntervalWifiSeconds,
kFetchingIntervalWifiSeconds);
}
base::TimeDelta GetFetchingIntervalFallback() {
return GetFetchingInterval(switches::kFetchingIntervalFallbackSeconds,
kFetchingIntervalFallbackSeconds);
}
// Extracts the hosts from |suggestions| and returns them in a set.
std::set<std::string> GetSuggestionsHosts(
const SuggestionsProfile& suggestions) {
std::set<std::string> hosts;
for (int i = 0; i < suggestions.suggestions_size(); ++i) {
const ChromeSuggestion& suggestion = suggestions.suggestions(i);
GURL url(suggestion.url());
if (url.is_valid())
hosts.insert(url.host());
}
return hosts;
}
const char kContentInfo[] = "contentInfo";
// Parses snippets from |list| and adds them to |snippets|. Returns true on
// success, false if anything went wrong.
bool AddSnippetsFromListValue(const base::ListValue& list,
NTPSnippetsService::NTPSnippetStorage* snippets) {
for (const base::Value* const value : list) {
const base::DictionaryValue* dict = nullptr;
if (!value->GetAsDictionary(&dict))
return false;
const base::DictionaryValue* content = nullptr;
if (!dict->GetDictionary(kContentInfo, &content))
return false;
scoped_ptr<NTPSnippet> snippet = NTPSnippet::CreateFromDictionary(*content);
if (!snippet)
return false;
snippets->push_back(std::move(snippet));
}
return true;
}
scoped_ptr<base::ListValue> SnippetsToListValue(
const NTPSnippetsService::NTPSnippetStorage& snippets) {
scoped_ptr<base::ListValue> list(new base::ListValue);
for (const auto& snippet : snippets) {
scoped_ptr<base::DictionaryValue> dict(new base::DictionaryValue);
dict->Set(kContentInfo, snippet->ToDictionary());
list->Append(std::move(dict));
}
return list;
}
} // namespace
NTPSnippetsService::NTPSnippetsService(
PrefService* pref_service,
SuggestionsService* suggestions_service,
scoped_refptr<base::SequencedTaskRunner> file_task_runner,
const std::string& application_language_code,
NTPSnippetsScheduler* scheduler,
scoped_ptr<NTPSnippetsFetcher> snippets_fetcher,
const ParseJSONCallback& parse_json_callback)
: pref_service_(pref_service),
suggestions_service_(suggestions_service),
file_task_runner_(file_task_runner),
application_language_code_(application_language_code),
scheduler_(scheduler),
snippets_fetcher_(std::move(snippets_fetcher)),
parse_json_callback_(parse_json_callback),
weak_ptr_factory_(this) {
snippets_fetcher_subscription_ = snippets_fetcher_->AddCallback(base::Bind(
&NTPSnippetsService::OnSnippetsDownloaded, base::Unretained(this)));
}
NTPSnippetsService::~NTPSnippetsService() {}
// static
void NTPSnippetsService::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterListPref(prefs::kSnippets);
registry->RegisterListPref(prefs::kDiscardedSnippets);
registry->RegisterListPref(prefs::kSnippetHosts);
}
void NTPSnippetsService::Init(bool enabled) {
if (enabled) {
// |suggestions_service_| can be null in tests.
if (suggestions_service_) {
suggestions_service_subscription_ = suggestions_service_->AddCallback(
base::Bind(&NTPSnippetsService::OnSuggestionsChanged,
base::Unretained(this)));
}
// Get any existing snippets immediately from prefs.
LoadDiscardedSnippetsFromPrefs();
LoadSnippetsFromPrefs();
// If we don't have any snippets yet, start a fetch.
if (snippets_.empty())
FetchSnippets();
}
// The scheduler only exists on Android so far, it's null on other platforms.
if (!scheduler_)
return;
if (enabled) {
scheduler_->Schedule(GetFetchingIntervalWifiCharging(),
GetFetchingIntervalWifi(),
GetFetchingIntervalFallback());
} else {
scheduler_->Unschedule();
}
}
void NTPSnippetsService::Shutdown() {
FOR_EACH_OBSERVER(NTPSnippetsServiceObserver, observers_,
NTPSnippetsServiceShutdown());
}
void NTPSnippetsService::FetchSnippets() {
// |suggestions_service_| can be null in tests.
if (!suggestions_service_)
return;
FetchSnippetsImpl(GetSuggestionsHosts(
suggestions_service_->GetSuggestionsDataFromCache()));
}
bool NTPSnippetsService::DiscardSnippet(const GURL& url) {
auto it = std::find_if(snippets_.begin(), snippets_.end(),
[&url](const scoped_ptr<NTPSnippet>& snippet) {
return snippet->url() == url;
});
if (it == snippets_.end())
return false;
discarded_snippets_.push_back(std::move(*it));
snippets_.erase(it);
StoreDiscardedSnippetsToPrefs();
StoreSnippetsToPrefs();
return true;
}
void NTPSnippetsService::AddObserver(NTPSnippetsServiceObserver* observer) {
observers_.AddObserver(observer);
observer->NTPSnippetsServiceLoaded();
}
void NTPSnippetsService::RemoveObserver(NTPSnippetsServiceObserver* observer) {
observers_.RemoveObserver(observer);
}
void NTPSnippetsService::OnSuggestionsChanged(
const SuggestionsProfile& suggestions) {
std::set<std::string> hosts = GetSuggestionsHosts(suggestions);
if (hosts == GetSnippetHostsFromPrefs())
return;
// Remove existing snippets that aren't in the suggestions anymore.
snippets_.erase(
std::remove_if(snippets_.begin(), snippets_.end(),
[&hosts](const scoped_ptr<NTPSnippet>& snippet) {
return !hosts.count(snippet->url().host());
}),
snippets_.end());
StoreSnippetsToPrefs();
StoreSnippetHostsToPrefs(hosts);
FOR_EACH_OBSERVER(NTPSnippetsServiceObserver, observers_,
NTPSnippetsServiceLoaded());
FetchSnippetsImpl(hosts);
}
void NTPSnippetsService::OnSnippetsDownloaded(
const std::string& snippets_json) {
parse_json_callback_.Run(
snippets_json, base::Bind(&NTPSnippetsService::OnJsonParsed,
weak_ptr_factory_.GetWeakPtr(), snippets_json),
base::Bind(&NTPSnippetsService::OnJsonError,
weak_ptr_factory_.GetWeakPtr(), snippets_json));
}
void NTPSnippetsService::OnJsonParsed(const std::string& snippets_json,
scoped_ptr<base::Value> parsed) {
LOG_IF(WARNING, !LoadFromValue(*parsed)) << "Received invalid snippets: "
<< snippets_json;
}
void NTPSnippetsService::OnJsonError(const std::string& snippets_json,
const std::string& error) {
LOG(WARNING) << "Received invalid JSON (" << error << "): " << snippets_json;
}
void NTPSnippetsService::FetchSnippetsImpl(
const std::set<std::string>& hosts) {
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kDontRestrict)) {
snippets_fetcher_->FetchSnippets(std::set<std::string>());
return;
}
if (!hosts.empty())
snippets_fetcher_->FetchSnippets(hosts);
}
bool NTPSnippetsService::LoadFromValue(const base::Value& value) {
const base::DictionaryValue* top_dict = nullptr;
if (!value.GetAsDictionary(&top_dict))
return false;
const base::ListValue* list = nullptr;
if (!top_dict->GetList("recos", &list))
return false;
return LoadFromListValue(*list);
}
bool NTPSnippetsService::LoadFromListValue(const base::ListValue& list) {
NTPSnippetStorage new_snippets;
if (!AddSnippetsFromListValue(list, &new_snippets))
return false;
for (scoped_ptr<NTPSnippet>& snippet : new_snippets) {
// If this snippet has previously been discarded, don't add it again.
if (HasDiscardedSnippet(snippet->url()))
continue;
// If the snippet has no publish/expiry dates, fill in defaults.
if (snippet->publish_date().is_null())
snippet->set_publish_date(base::Time::Now());
if (snippet->expiry_date().is_null()) {
snippet->set_expiry_date(
snippet->publish_date() +
base::TimeDelta::FromMinutes(kDefaultExpiryTimeMins));
}
// Check if we already have a snippet with the same URL. If so, replace it
// rather than adding a duplicate.
const GURL& url = snippet->url();
auto it = std::find_if(snippets_.begin(), snippets_.end(),
[&url](const scoped_ptr<NTPSnippet>& old_snippet) {
return old_snippet->url() == url;
});
if (it != snippets_.end())
*it = std::move(snippet);
else
snippets_.push_back(std::move(snippet));
}
// Immediately remove any already-expired snippets. This will also notify our
// observers and schedule the expiry timer.
RemoveExpiredSnippets();
return true;
}
void NTPSnippetsService::LoadSnippetsFromPrefs() {
bool success = LoadFromListValue(*pref_service_->GetList(prefs::kSnippets));
DCHECK(success) << "Failed to parse snippets from prefs";
}
void NTPSnippetsService::StoreSnippetsToPrefs() {
pref_service_->Set(prefs::kSnippets, *SnippetsToListValue(snippets_));
}
void NTPSnippetsService::LoadDiscardedSnippetsFromPrefs() {
discarded_snippets_.clear();
bool success = AddSnippetsFromListValue(
*pref_service_->GetList(prefs::kDiscardedSnippets),
&discarded_snippets_);
DCHECK(success) << "Failed to parse discarded snippets from prefs";
}
void NTPSnippetsService::StoreDiscardedSnippetsToPrefs() {
pref_service_->Set(prefs::kDiscardedSnippets,
*SnippetsToListValue(discarded_snippets_));
}
std::set<std::string> NTPSnippetsService::GetSnippetHostsFromPrefs() const {
std::set<std::string> hosts;
const base::ListValue* list = pref_service_->GetList(prefs::kSnippetHosts);
for (const base::Value* value : *list) {
std::string str;
bool success = value->GetAsString(&str);
DCHECK(success) << "Failed to parse snippet host from prefs";
hosts.insert(std::move(str));
}
return hosts;
}
void NTPSnippetsService::StoreSnippetHostsToPrefs(
const std::set<std::string>& hosts) {
base::ListValue list;
for (const std::string& host : hosts)
list.AppendString(host);
pref_service_->Set(prefs::kSnippetHosts, list);
}
bool NTPSnippetsService::HasDiscardedSnippet(const GURL& url) const {
auto it = std::find_if(discarded_snippets_.begin(), discarded_snippets_.end(),
[&url](const scoped_ptr<NTPSnippet>& snippet) {
return snippet->url() == url;
});
return it != discarded_snippets_.end();
}
void NTPSnippetsService::RemoveExpiredSnippets() {
base::Time expiry = base::Time::Now();
snippets_.erase(
std::remove_if(snippets_.begin(), snippets_.end(),
[&expiry](const scoped_ptr<NTPSnippet>& snippet) {
return snippet->expiry_date() <= expiry;
}),
snippets_.end());
StoreSnippetsToPrefs();
discarded_snippets_.erase(
std::remove_if(discarded_snippets_.begin(), discarded_snippets_.end(),
[&expiry](const scoped_ptr<NTPSnippet>& snippet) {
return snippet->expiry_date() <= expiry;
}),
discarded_snippets_.end());
StoreDiscardedSnippetsToPrefs();
FOR_EACH_OBSERVER(NTPSnippetsServiceObserver, observers_,
NTPSnippetsServiceLoaded());
// If there are any snippets left, schedule a timer for the next expiry.
if (snippets_.empty() && discarded_snippets_.empty())
return;
base::Time next_expiry = base::Time::Max();
for (const auto& snippet : snippets_) {
if (snippet->expiry_date() < next_expiry)
next_expiry = snippet->expiry_date();
}
for (const auto& snippet : discarded_snippets_) {
if (snippet->expiry_date() < next_expiry)
next_expiry = snippet->expiry_date();
}
DCHECK_GT(next_expiry, expiry);
expiry_timer_.Start(FROM_HERE, next_expiry - expiry,
base::Bind(&NTPSnippetsService::RemoveExpiredSnippets,
base::Unretained(this)));
}
} // namespace ntp_snippets