blob: 281c2f460820238e6def5d64028b76e6d402b8b9 [file] [log] [blame]
// Copyright 2016 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/reading_list/core/reading_list_entry.h"
#include <memory>
#include "base/json/json_string_value_serializer.h"
#include "base/memory/ptr_util.h"
#include "components/reading_list/core/offline_url_utils.h"
#include "components/reading_list/core/proto/reading_list.pb.h"
#include "components/reading_list/core/reading_list_store.h"
#include "components/sync/protocol/reading_list_specifics.pb.h"
#include "net/base/backoff_entry_serializer.h"
namespace {
// Converts |time| to the number of microseconds since Jan 1st 1970.
int64_t TimeToUS(const base::Time& time) {
return (time - base::Time::UnixEpoch()).InMicroseconds();
}
}
// The backoff time is the following: 10min, 10min, 1h, 2h, 2h..., starting
// after the first failure.
const net::BackoffEntry::Policy ReadingListEntry::kBackoffPolicy = {
// Number of initial errors (in sequence) to ignore before applying
// exponential back-off rules.
2,
// Initial delay for exponential back-off in ms.
10 * 60 * 1000, // 10 minutes.
// Factor by which the waiting time will be multiplied.
6,
// Fuzzing percentage. ex: 10% will spread requests randomly
// between 90%-100% of the calculated time.
0.1, // 10%.
// Maximum amount of time we are willing to delay our request in ms.
2 * 3600 * 1000, // 2 hours.
// Time to keep an entry from being discarded even when it
// has no significant state, -1 to never discard.
-1,
true, // Don't use initial delay unless the last request was an error.
};
ReadingListEntry::ReadingListEntry(const GURL& url,
const std::string& title,
const base::Time& now)
: ReadingListEntry(url, title, now, nullptr){};
ReadingListEntry::ReadingListEntry(const GURL& url,
const std::string& title,
const base::Time& now,
std::unique_ptr<net::BackoffEntry> backoff)
: ReadingListEntry(url,
title,
UNSEEN,
TimeToUS(now),
0,
TimeToUS(now),
TimeToUS(now),
WAITING,
base::FilePath(),
GURL(),
0,
0,
0,
std::move(backoff),
reading_list::ContentSuggestionsExtra()) {}
ReadingListEntry::ReadingListEntry(
const GURL& url,
const std::string& title,
State state,
int64_t creation_time,
int64_t first_read_time,
int64_t update_time,
int64_t update_title_time,
ReadingListEntry::DistillationState distilled_state,
const base::FilePath& distilled_path,
const GURL& distilled_url,
int64_t distillation_time,
int64_t distillation_size,
int failed_download_counter,
std::unique_ptr<net::BackoffEntry> backoff,
const reading_list::ContentSuggestionsExtra& content_suggestions_extra)
: url_(url),
title_(title),
state_(state),
distilled_path_(distilled_path),
distilled_url_(distilled_url),
distilled_state_(distilled_state),
failed_download_counter_(failed_download_counter),
creation_time_us_(creation_time),
first_read_time_us_(first_read_time),
update_time_us_(update_time),
update_title_time_us_(update_title_time),
distillation_time_us_(distillation_time),
distillation_size_(distillation_size),
content_suggestions_extra_(content_suggestions_extra) {
if (backoff) {
backoff_ = std::move(backoff);
} else {
backoff_ = std::make_unique<net::BackoffEntry>(&kBackoffPolicy);
}
DCHECK(creation_time_us_);
DCHECK(update_time_us_);
DCHECK(update_title_time_us_);
DCHECK(!url.is_empty());
DCHECK(url.is_valid());
}
ReadingListEntry::ReadingListEntry(ReadingListEntry&& entry)
: url_(std::move(entry.url_)),
title_(std::move(entry.title_)),
state_(std::move(entry.state_)),
distilled_path_(std::move(entry.distilled_path_)),
distilled_url_(std::move(entry.distilled_url_)),
distilled_state_(std::move(entry.distilled_state_)),
backoff_(std::move(entry.backoff_)),
failed_download_counter_(std::move(entry.failed_download_counter_)),
creation_time_us_(std::move(entry.creation_time_us_)),
first_read_time_us_(std::move(entry.first_read_time_us_)),
update_time_us_(std::move(entry.update_time_us_)),
update_title_time_us_(std::move(entry.update_title_time_us_)),
distillation_time_us_(std::move(entry.distillation_time_us_)),
distillation_size_(std::move(entry.distillation_size_)),
content_suggestions_extra_(std::move(entry.content_suggestions_extra_)) {}
ReadingListEntry::~ReadingListEntry() {}
const GURL& ReadingListEntry::URL() const {
return url_;
}
const std::string& ReadingListEntry::Title() const {
return title_;
}
ReadingListEntry::DistillationState ReadingListEntry::DistilledState() const {
return distilled_state_;
}
const base::FilePath& ReadingListEntry::DistilledPath() const {
return distilled_path_;
}
const GURL& ReadingListEntry::DistilledURL() const {
return distilled_url_;
}
int64_t ReadingListEntry::DistillationTime() const {
return distillation_time_us_;
}
int64_t ReadingListEntry::DistillationSize() const {
return distillation_size_;
}
base::TimeDelta ReadingListEntry::TimeUntilNextTry() const {
return backoff_->GetTimeUntilRelease();
}
int ReadingListEntry::FailedDownloadCounter() const {
return failed_download_counter_;
}
ReadingListEntry& ReadingListEntry::operator=(ReadingListEntry&& other) {
url_ = std::move(other.url_);
title_ = std::move(other.title_);
distilled_path_ = std::move(other.distilled_path_);
distilled_url_ = std::move(other.distilled_url_);
distilled_state_ = std::move(other.distilled_state_);
backoff_ = std::move(other.backoff_);
state_ = std::move(other.state_);
failed_download_counter_ = std::move(other.failed_download_counter_);
creation_time_us_ = std::move(other.creation_time_us_);
first_read_time_us_ = std::move(other.first_read_time_us_);
update_time_us_ = std::move(other.update_time_us_);
update_title_time_us_ = std::move(other.update_title_time_us_);
distillation_time_us_ = std::move(other.distillation_time_us_);
distillation_size_ = std::move(other.distillation_size_);
content_suggestions_extra_ = std::move(other.content_suggestions_extra_);
return *this;
}
bool ReadingListEntry::operator==(const ReadingListEntry& other) const {
return url_ == other.url_;
}
void ReadingListEntry::SetTitle(const std::string& title,
const base::Time& now) {
title_ = title;
update_title_time_us_ = TimeToUS(now);
}
void ReadingListEntry::SetRead(bool read, const base::Time& now) {
State previous_state = state_;
state_ = read ? READ : UNREAD;
if (state_ == previous_state) {
return;
}
if (FirstReadTime() == 0 && read) {
first_read_time_us_ = TimeToUS(now);
}
if (!(previous_state == UNSEEN && state_ == UNREAD)) {
// If changing UNSEEN -> UNREAD, entry is not marked updated to preserve
// order in Reading List View.
MarkEntryUpdated(now);
}
}
bool ReadingListEntry::IsRead() const {
return state_ == READ;
}
bool ReadingListEntry::HasBeenSeen() const {
return state_ != UNSEEN;
}
const reading_list::ContentSuggestionsExtra*
ReadingListEntry::ContentSuggestionsExtra() const {
return &content_suggestions_extra_;
}
void ReadingListEntry::SetContentSuggestionsExtra(
const reading_list::ContentSuggestionsExtra& extra) {
content_suggestions_extra_ = extra;
}
void ReadingListEntry::SetDistilledInfo(const base::FilePath& path,
const GURL& distilled_url,
int64_t distilation_size,
const base::Time& distilation_time) {
DCHECK(!path.empty());
DCHECK(distilled_url.is_valid());
distilled_path_ = path;
distilled_state_ = PROCESSED;
distilled_url_ = distilled_url;
distillation_time_us_ = TimeToUS(distilation_time);
distillation_size_ = distilation_size;
backoff_->Reset();
failed_download_counter_ = 0;
}
void ReadingListEntry::SetDistilledState(DistillationState distilled_state) {
DCHECK(distilled_state != PROCESSED); // use SetDistilledPath instead.
DCHECK(distilled_state != WAITING);
// Increase time until next retry exponentially if the state change from a
// non-error state to an error state.
if ((distilled_state == WILL_RETRY ||
distilled_state == DISTILLATION_ERROR) &&
distilled_state_ != WILL_RETRY &&
distilled_state_ != DISTILLATION_ERROR) {
backoff_->InformOfRequest(false);
failed_download_counter_++;
}
distilled_state_ = distilled_state;
distilled_path_ = base::FilePath();
distilled_url_ = GURL::EmptyGURL();
distillation_size_ = 0;
distillation_time_us_ = 0;
}
int64_t ReadingListEntry::UpdateTime() const {
return update_time_us_;
}
int64_t ReadingListEntry::UpdateTitleTime() const {
return update_title_time_us_;
}
int64_t ReadingListEntry::CreationTime() const {
return creation_time_us_;
}
int64_t ReadingListEntry::FirstReadTime() const {
return first_read_time_us_;
}
void ReadingListEntry::MarkEntryUpdated(const base::Time& now) {
update_time_us_ = TimeToUS(now);
}
// static
std::unique_ptr<ReadingListEntry> ReadingListEntry::FromReadingListLocal(
const reading_list::ReadingListLocal& pb_entry,
const base::Time& now) {
if (!pb_entry.has_url()) {
return nullptr;
}
GURL url(pb_entry.url());
if (url.is_empty() || !url.is_valid()) {
return nullptr;
}
std::string title;
if (pb_entry.has_title()) {
title = pb_entry.title();
}
int64_t creation_time_us = 0;
if (pb_entry.has_creation_time_us()) {
creation_time_us = pb_entry.creation_time_us();
} else {
creation_time_us = (now - base::Time::UnixEpoch()).InMicroseconds();
}
int64_t first_read_time_us = 0;
if (pb_entry.has_first_read_time_us()) {
first_read_time_us = pb_entry.first_read_time_us();
}
int64_t update_time_us = creation_time_us;
if (pb_entry.has_update_time_us()) {
update_time_us = pb_entry.update_time_us();
}
int64_t update_title_time_us = 0;
if (pb_entry.has_update_title_time_us()) {
update_title_time_us = pb_entry.update_title_time_us();
}
if (update_title_time_us == 0) {
// Entries created before title could be modified don't have
// update_title_time_us. Set it to creation_time_us for consistency.
update_title_time_us = creation_time_us;
}
State state = UNSEEN;
if (pb_entry.has_status()) {
switch (pb_entry.status()) {
case reading_list::ReadingListLocal::READ:
state = READ;
break;
case reading_list::ReadingListLocal::UNREAD:
state = UNREAD;
break;
case reading_list::ReadingListLocal::UNSEEN:
state = UNSEEN;
break;
}
}
ReadingListEntry::DistillationState distillation_state =
ReadingListEntry::WAITING;
if (pb_entry.has_distillation_state()) {
switch (pb_entry.distillation_state()) {
case reading_list::ReadingListLocal::WAITING:
distillation_state = ReadingListEntry::WAITING;
break;
case reading_list::ReadingListLocal::PROCESSING:
distillation_state = ReadingListEntry::PROCESSING;
break;
case reading_list::ReadingListLocal::PROCESSED:
distillation_state = ReadingListEntry::PROCESSED;
break;
case reading_list::ReadingListLocal::WILL_RETRY:
distillation_state = ReadingListEntry::WILL_RETRY;
break;
case reading_list::ReadingListLocal::DISTILLATION_ERROR:
distillation_state = ReadingListEntry::DISTILLATION_ERROR;
break;
}
}
base::FilePath distilled_path;
if (pb_entry.has_distilled_path()) {
distilled_path = base::FilePath::FromUTF8Unsafe(pb_entry.distilled_path());
}
GURL distilled_url;
if (pb_entry.has_distilled_url()) {
distilled_url = GURL(pb_entry.distilled_url());
}
int64_t distillation_time_us = 0;
if (pb_entry.has_distillation_time_us()) {
distillation_time_us = pb_entry.distillation_time_us();
}
int64_t distillation_size = 0;
if (pb_entry.has_distillation_size()) {
distillation_size = pb_entry.distillation_size();
}
int64_t failed_download_counter = 0;
if (pb_entry.has_failed_download_counter()) {
failed_download_counter = pb_entry.failed_download_counter();
}
std::unique_ptr<net::BackoffEntry> backoff;
if (pb_entry.has_backoff()) {
JSONStringValueDeserializer deserializer(pb_entry.backoff());
std::unique_ptr<base::Value> value(
deserializer.Deserialize(nullptr, nullptr));
if (value) {
backoff = net::BackoffEntrySerializer::DeserializeFromValue(
*value, &kBackoffPolicy, nullptr, now);
}
}
reading_list::ContentSuggestionsExtra content_suggestions_extra;
if (pb_entry.has_content_suggestions_extra()) {
const reading_list::ReadingListContentSuggestionsExtra& pb_extra =
pb_entry.content_suggestions_extra();
if (pb_extra.has_dismissed()) {
content_suggestions_extra.dismissed = pb_extra.dismissed();
}
}
return base::WrapUnique<ReadingListEntry>(new ReadingListEntry(
url, title, state, creation_time_us, first_read_time_us, update_time_us,
update_title_time_us, distillation_state, distilled_path, distilled_url,
distillation_time_us, distillation_size, failed_download_counter,
std::move(backoff), content_suggestions_extra));
}
// static
std::unique_ptr<ReadingListEntry> ReadingListEntry::FromReadingListSpecifics(
const sync_pb::ReadingListSpecifics& pb_entry,
const base::Time& now) {
if (!pb_entry.has_url()) {
return nullptr;
}
GURL url(pb_entry.url());
if (url.is_empty() || !url.is_valid()) {
return nullptr;
}
std::string title;
if (pb_entry.has_title()) {
title = pb_entry.title();
}
int64_t creation_time_us = TimeToUS(now);
if (pb_entry.has_creation_time_us()) {
creation_time_us = pb_entry.creation_time_us();
}
int64_t first_read_time_us = 0;
if (pb_entry.has_first_read_time_us()) {
first_read_time_us = pb_entry.first_read_time_us();
}
int64_t update_time_us = creation_time_us;
if (pb_entry.has_update_time_us()) {
update_time_us = pb_entry.update_time_us();
}
int64_t update_title_time_us = 0;
if (pb_entry.has_update_title_time_us()) {
update_title_time_us = pb_entry.update_title_time_us();
}
if (update_title_time_us == 0) {
// Entries created before title could be modified don't have
// update_title_time_us. Set it to creation_time_us for consistency.
update_title_time_us = creation_time_us;
}
State state = UNSEEN;
if (pb_entry.has_status()) {
switch (pb_entry.status()) {
case sync_pb::ReadingListSpecifics::READ:
state = READ;
break;
case sync_pb::ReadingListSpecifics::UNREAD:
state = UNREAD;
break;
case sync_pb::ReadingListSpecifics::UNSEEN:
state = UNSEEN;
break;
}
}
return base::WrapUnique<ReadingListEntry>(new ReadingListEntry(
url, title, state, creation_time_us, first_read_time_us, update_time_us,
update_title_time_us, WAITING, base::FilePath(), GURL(), 0, 0, 0, nullptr,
reading_list::ContentSuggestionsExtra()));
}
void ReadingListEntry::MergeWithEntry(const ReadingListEntry& other) {
#if !defined(NDEBUG)
// Checks that the result entry respects the sync order.
std::unique_ptr<sync_pb::ReadingListSpecifics> old_this_pb(
AsReadingListSpecifics());
std::unique_ptr<sync_pb::ReadingListSpecifics> other_pb(
other.AsReadingListSpecifics());
#endif
DCHECK(url_ == other.url_);
if (update_title_time_us_ < other.update_title_time_us_) {
// Take the most recent title updated.
title_ = std::move(other.title_);
update_title_time_us_ = std::move(other.update_title_time_us_);
} else if (update_title_time_us_ == other.update_title_time_us_) {
if (title_.compare(other.title_) < 0) {
// Take the last in alphabetical order or the longer one.
// This ensure empty string is replaced.
title_ = std::move(other.title_);
}
}
if (creation_time_us_ < other.creation_time_us_) {
creation_time_us_ = std::move(other.creation_time_us_);
first_read_time_us_ = std::move(other.first_read_time_us_);
} else if (creation_time_us_ == other.creation_time_us_) {
// The first_time_read_us from |other| is used if
// - this.first_time_read_us == 0: the entry was never read in this device.
// - this.first_time_read_us > other.first_time_read_us: the entry was
// first read on another device.
if (first_read_time_us_ == 0 ||
(other.first_read_time_us_ != 0 &&
other.first_read_time_us_ < first_read_time_us_)) {
first_read_time_us_ = std::move(other.first_read_time_us_);
}
}
if (update_time_us_ < other.update_time_us_) {
update_time_us_ = std::move(other.update_time_us_);
state_ = std::move(other.state_);
} else if (update_time_us_ == other.update_time_us_) {
if (state_ == UNSEEN) {
state_ = std::move(other.state_);
} else if (other.state_ == READ) {
state_ = std::move(other.state_);
}
}
#if !defined(NDEBUG)
std::unique_ptr<sync_pb::ReadingListSpecifics> new_this_pb(
AsReadingListSpecifics());
DCHECK(ReadingListStore::CompareEntriesForSync(*old_this_pb, *new_this_pb));
DCHECK(ReadingListStore::CompareEntriesForSync(*other_pb, *new_this_pb));
#endif
}
std::unique_ptr<reading_list::ReadingListLocal>
ReadingListEntry::AsReadingListLocal(const base::Time& now) const {
std::unique_ptr<reading_list::ReadingListLocal> pb_entry =
std::make_unique<reading_list::ReadingListLocal>();
// URL is used as the key for the database and sync as there is only one entry
// per URL.
pb_entry->set_entry_id(URL().spec());
pb_entry->set_title(Title());
pb_entry->set_url(URL().spec());
pb_entry->set_creation_time_us(CreationTime());
pb_entry->set_first_read_time_us(FirstReadTime());
pb_entry->set_update_time_us(UpdateTime());
pb_entry->set_update_title_time_us(UpdateTitleTime());
switch (state_) {
case READ:
pb_entry->set_status(reading_list::ReadingListLocal::READ);
break;
case UNREAD:
pb_entry->set_status(reading_list::ReadingListLocal::UNREAD);
break;
case UNSEEN:
pb_entry->set_status(reading_list::ReadingListLocal::UNSEEN);
break;
}
reading_list::ReadingListLocal::DistillationState distilation_state =
reading_list::ReadingListLocal::WAITING;
switch (DistilledState()) {
case ReadingListEntry::WAITING:
distilation_state = reading_list::ReadingListLocal::WAITING;
break;
case ReadingListEntry::PROCESSING:
distilation_state = reading_list::ReadingListLocal::PROCESSING;
break;
case ReadingListEntry::PROCESSED:
distilation_state = reading_list::ReadingListLocal::PROCESSED;
break;
case ReadingListEntry::WILL_RETRY:
distilation_state = reading_list::ReadingListLocal::WILL_RETRY;
break;
case ReadingListEntry::DISTILLATION_ERROR:
distilation_state = reading_list::ReadingListLocal::DISTILLATION_ERROR;
break;
}
pb_entry->set_distillation_state(distilation_state);
if (!DistilledPath().empty()) {
pb_entry->set_distilled_path(DistilledPath().AsUTF8Unsafe());
}
if (DistilledURL().is_valid()) {
pb_entry->set_distilled_url(DistilledURL().spec());
}
if (DistillationTime()) {
pb_entry->set_distillation_time_us(DistillationTime());
}
if (DistillationSize()) {
pb_entry->set_distillation_size(DistillationSize());
}
pb_entry->set_failed_download_counter(failed_download_counter_);
if (backoff_) {
std::unique_ptr<base::Value> backoff =
net::BackoffEntrySerializer::SerializeToValue(*backoff_, now);
std::string output;
JSONStringValueSerializer serializer(&output);
serializer.Serialize(*backoff);
pb_entry->set_backoff(output);
}
reading_list::ReadingListContentSuggestionsExtra* pb_extra =
pb_entry->mutable_content_suggestions_extra();
pb_extra->set_dismissed(content_suggestions_extra_.dismissed);
return pb_entry;
}
std::unique_ptr<sync_pb::ReadingListSpecifics>
ReadingListEntry::AsReadingListSpecifics() const {
std::unique_ptr<sync_pb::ReadingListSpecifics> pb_entry =
std::make_unique<sync_pb::ReadingListSpecifics>();
// URL is used as the key for the database and sync as there is only one entry
// per URL.
pb_entry->set_entry_id(URL().spec());
pb_entry->set_title(Title());
pb_entry->set_url(URL().spec());
pb_entry->set_creation_time_us(CreationTime());
pb_entry->set_first_read_time_us(FirstReadTime());
pb_entry->set_update_time_us(UpdateTime());
pb_entry->set_update_title_time_us(UpdateTitleTime());
switch (state_) {
case READ:
pb_entry->set_status(sync_pb::ReadingListSpecifics::READ);
break;
case UNREAD:
pb_entry->set_status(sync_pb::ReadingListSpecifics::UNREAD);
break;
case UNSEEN:
pb_entry->set_status(sync_pb::ReadingListSpecifics::UNSEEN);
break;
}
return pb_entry;
}