1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "components/reading_list/core/reading_list_entry.h"
6
7 #include <memory>
8
9 #include "base/json/json_string_value_serializer.h"
10 #include "base/memory/ptr_util.h"
11 #include "components/reading_list/core/offline_url_utils.h"
12 #include "components/reading_list/core/proto/reading_list.pb.h"
13 #include "components/reading_list/core/reading_list_store.h"
14 #include "components/sync/protocol/reading_list_specifics.pb.h"
15 #include "net/base/backoff_entry_serializer.h"
16
17 namespace {
18 // Converts |time| to the number of microseconds since Jan 1st 1970.
TimeToUS(const base::Time & time)19 int64_t TimeToUS(const base::Time& time) {
20 return (time - base::Time::UnixEpoch()).InMicroseconds();
21 }
22 }
23
24 // The backoff time is the following: 10min, 10min, 1h, 2h, 2h..., starting
25 // after the first failure.
26 const net::BackoffEntry::Policy ReadingListEntry::kBackoffPolicy = {
27 // Number of initial errors (in sequence) to ignore before applying
28 // exponential back-off rules.
29 2,
30
31 // Initial delay for exponential back-off in ms.
32 10 * 60 * 1000, // 10 minutes.
33
34 // Factor by which the waiting time will be multiplied.
35 6,
36
37 // Fuzzing percentage. ex: 10% will spread requests randomly
38 // between 90%-100% of the calculated time.
39 0.1, // 10%.
40
41 // Maximum amount of time we are willing to delay our request in ms.
42 2 * 3600 * 1000, // 2 hours.
43
44 // Time to keep an entry from being discarded even when it
45 // has no significant state, -1 to never discard.
46 -1,
47
48 true, // Don't use initial delay unless the last request was an error.
49 };
50
ReadingListEntry(const GURL & url,const std::string & title,const base::Time & now)51 ReadingListEntry::ReadingListEntry(const GURL& url,
52 const std::string& title,
53 const base::Time& now)
54 : ReadingListEntry(url, title, now, nullptr) {}
55
ReadingListEntry(const GURL & url,const std::string & title,const base::Time & now,std::unique_ptr<net::BackoffEntry> backoff)56 ReadingListEntry::ReadingListEntry(const GURL& url,
57 const std::string& title,
58 const base::Time& now,
59 std::unique_ptr<net::BackoffEntry> backoff)
60 : ReadingListEntry(url,
61 title,
62 UNSEEN,
63 TimeToUS(now),
64 0,
65 TimeToUS(now),
66 TimeToUS(now),
67 WAITING,
68 base::FilePath(),
69 GURL(),
70 0,
71 0,
72 0,
73 std::move(backoff),
74 reading_list::ContentSuggestionsExtra()) {}
75
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)76 ReadingListEntry::ReadingListEntry(
77 const GURL& url,
78 const std::string& title,
79 State state,
80 int64_t creation_time,
81 int64_t first_read_time,
82 int64_t update_time,
83 int64_t update_title_time,
84 ReadingListEntry::DistillationState distilled_state,
85 const base::FilePath& distilled_path,
86 const GURL& distilled_url,
87 int64_t distillation_time,
88 int64_t distillation_size,
89 int failed_download_counter,
90 std::unique_ptr<net::BackoffEntry> backoff,
91 const reading_list::ContentSuggestionsExtra& content_suggestions_extra)
92 : url_(url),
93 title_(title),
94 state_(state),
95 distilled_path_(distilled_path),
96 distilled_url_(distilled_url),
97 distilled_state_(distilled_state),
98 failed_download_counter_(failed_download_counter),
99 creation_time_us_(creation_time),
100 first_read_time_us_(first_read_time),
101 update_time_us_(update_time),
102 update_title_time_us_(update_title_time),
103 distillation_time_us_(distillation_time),
104 distillation_size_(distillation_size),
105 content_suggestions_extra_(content_suggestions_extra) {
106 if (backoff) {
107 backoff_ = std::move(backoff);
108 } else {
109 backoff_ = std::make_unique<net::BackoffEntry>(&kBackoffPolicy);
110 }
111 DCHECK(creation_time_us_);
112 DCHECK(update_time_us_);
113 DCHECK(update_title_time_us_);
114 DCHECK(!url.is_empty());
115 DCHECK(url.is_valid());
116 }
117
ReadingListEntry(ReadingListEntry && entry)118 ReadingListEntry::ReadingListEntry(ReadingListEntry&& entry)
119 : url_(std::move(entry.url_)),
120 title_(std::move(entry.title_)),
121 state_(std::move(entry.state_)),
122 distilled_path_(std::move(entry.distilled_path_)),
123 distilled_url_(std::move(entry.distilled_url_)),
124 distilled_state_(std::move(entry.distilled_state_)),
125 backoff_(std::move(entry.backoff_)),
126 failed_download_counter_(std::move(entry.failed_download_counter_)),
127 creation_time_us_(std::move(entry.creation_time_us_)),
128 first_read_time_us_(std::move(entry.first_read_time_us_)),
129 update_time_us_(std::move(entry.update_time_us_)),
130 update_title_time_us_(std::move(entry.update_title_time_us_)),
131 distillation_time_us_(std::move(entry.distillation_time_us_)),
132 distillation_size_(std::move(entry.distillation_size_)),
133 content_suggestions_extra_(std::move(entry.content_suggestions_extra_)) {}
134
~ReadingListEntry()135 ReadingListEntry::~ReadingListEntry() {}
136
URL() const137 const GURL& ReadingListEntry::URL() const {
138 return url_;
139 }
140
Title() const141 const std::string& ReadingListEntry::Title() const {
142 return title_;
143 }
144
DistilledState() const145 ReadingListEntry::DistillationState ReadingListEntry::DistilledState() const {
146 return distilled_state_;
147 }
148
DistilledPath() const149 const base::FilePath& ReadingListEntry::DistilledPath() const {
150 return distilled_path_;
151 }
152
DistilledURL() const153 const GURL& ReadingListEntry::DistilledURL() const {
154 return distilled_url_;
155 }
156
DistillationTime() const157 int64_t ReadingListEntry::DistillationTime() const {
158 return distillation_time_us_;
159 }
160
DistillationSize() const161 int64_t ReadingListEntry::DistillationSize() const {
162 return distillation_size_;
163 }
164
TimeUntilNextTry() const165 base::TimeDelta ReadingListEntry::TimeUntilNextTry() const {
166 return backoff_->GetTimeUntilRelease();
167 }
168
FailedDownloadCounter() const169 int ReadingListEntry::FailedDownloadCounter() const {
170 return failed_download_counter_;
171 }
172
operator =(ReadingListEntry && other)173 ReadingListEntry& ReadingListEntry::operator=(ReadingListEntry&& other) {
174 url_ = std::move(other.url_);
175 title_ = std::move(other.title_);
176 distilled_path_ = std::move(other.distilled_path_);
177 distilled_url_ = std::move(other.distilled_url_);
178 distilled_state_ = std::move(other.distilled_state_);
179 backoff_ = std::move(other.backoff_);
180 state_ = std::move(other.state_);
181 failed_download_counter_ = std::move(other.failed_download_counter_);
182 creation_time_us_ = std::move(other.creation_time_us_);
183 first_read_time_us_ = std::move(other.first_read_time_us_);
184 update_time_us_ = std::move(other.update_time_us_);
185 update_title_time_us_ = std::move(other.update_title_time_us_);
186 distillation_time_us_ = std::move(other.distillation_time_us_);
187 distillation_size_ = std::move(other.distillation_size_);
188 content_suggestions_extra_ = std::move(other.content_suggestions_extra_);
189 return *this;
190 }
191
operator ==(const ReadingListEntry & other) const192 bool ReadingListEntry::operator==(const ReadingListEntry& other) const {
193 return url_ == other.url_;
194 }
195
SetTitle(const std::string & title,const base::Time & now)196 void ReadingListEntry::SetTitle(const std::string& title,
197 const base::Time& now) {
198 title_ = title;
199 update_title_time_us_ = TimeToUS(now);
200 }
201
SetRead(bool read,const base::Time & now)202 void ReadingListEntry::SetRead(bool read, const base::Time& now) {
203 State previous_state = state_;
204 state_ = read ? READ : UNREAD;
205 if (state_ == previous_state) {
206 return;
207 }
208 if (FirstReadTime() == 0 && read) {
209 first_read_time_us_ = TimeToUS(now);
210 }
211 if (!(previous_state == UNSEEN && state_ == UNREAD)) {
212 // If changing UNSEEN -> UNREAD, entry is not marked updated to preserve
213 // order in Reading List View.
214 MarkEntryUpdated(now);
215 }
216 }
217
IsRead() const218 bool ReadingListEntry::IsRead() const {
219 return state_ == READ;
220 }
221
HasBeenSeen() const222 bool ReadingListEntry::HasBeenSeen() const {
223 return state_ != UNSEEN;
224 }
225
226 const reading_list::ContentSuggestionsExtra*
ContentSuggestionsExtra() const227 ReadingListEntry::ContentSuggestionsExtra() const {
228 return &content_suggestions_extra_;
229 }
230
SetContentSuggestionsExtra(const reading_list::ContentSuggestionsExtra & extra)231 void ReadingListEntry::SetContentSuggestionsExtra(
232 const reading_list::ContentSuggestionsExtra& extra) {
233 content_suggestions_extra_ = extra;
234 }
235
SetDistilledInfo(const base::FilePath & path,const GURL & distilled_url,int64_t distilation_size,const base::Time & distilation_time)236 void ReadingListEntry::SetDistilledInfo(const base::FilePath& path,
237 const GURL& distilled_url,
238 int64_t distilation_size,
239 const base::Time& distilation_time) {
240 DCHECK(!path.empty());
241 DCHECK(distilled_url.is_valid());
242 distilled_path_ = path;
243 distilled_state_ = PROCESSED;
244 distilled_url_ = distilled_url;
245 distillation_time_us_ = TimeToUS(distilation_time);
246 distillation_size_ = distilation_size;
247 backoff_->Reset();
248 failed_download_counter_ = 0;
249 }
250
SetDistilledState(DistillationState distilled_state)251 void ReadingListEntry::SetDistilledState(DistillationState distilled_state) {
252 DCHECK(distilled_state != PROCESSED); // use SetDistilledPath instead.
253 DCHECK(distilled_state != WAITING);
254 // Increase time until next retry exponentially if the state change from a
255 // non-error state to an error state.
256 if ((distilled_state == WILL_RETRY ||
257 distilled_state == DISTILLATION_ERROR) &&
258 distilled_state_ != WILL_RETRY &&
259 distilled_state_ != DISTILLATION_ERROR) {
260 backoff_->InformOfRequest(false);
261 failed_download_counter_++;
262 }
263
264 distilled_state_ = distilled_state;
265 distilled_path_ = base::FilePath();
266 distilled_url_ = GURL::EmptyGURL();
267 distillation_size_ = 0;
268 distillation_time_us_ = 0;
269 }
270
UpdateTime() const271 int64_t ReadingListEntry::UpdateTime() const {
272 return update_time_us_;
273 }
274
UpdateTitleTime() const275 int64_t ReadingListEntry::UpdateTitleTime() const {
276 return update_title_time_us_;
277 }
278
CreationTime() const279 int64_t ReadingListEntry::CreationTime() const {
280 return creation_time_us_;
281 }
282
FirstReadTime() const283 int64_t ReadingListEntry::FirstReadTime() const {
284 return first_read_time_us_;
285 }
286
MarkEntryUpdated(const base::Time & now)287 void ReadingListEntry::MarkEntryUpdated(const base::Time& now) {
288 update_time_us_ = TimeToUS(now);
289 }
290
291 // static
FromReadingListLocal(const reading_list::ReadingListLocal & pb_entry,const base::Time & now)292 std::unique_ptr<ReadingListEntry> ReadingListEntry::FromReadingListLocal(
293 const reading_list::ReadingListLocal& pb_entry,
294 const base::Time& now) {
295 if (!pb_entry.has_url()) {
296 return nullptr;
297 }
298 GURL url(pb_entry.url());
299 if (url.is_empty() || !url.is_valid()) {
300 return nullptr;
301 }
302 std::string title;
303 if (pb_entry.has_title()) {
304 title = pb_entry.title();
305 }
306
307 int64_t creation_time_us = 0;
308 if (pb_entry.has_creation_time_us()) {
309 creation_time_us = pb_entry.creation_time_us();
310 } else {
311 creation_time_us = (now - base::Time::UnixEpoch()).InMicroseconds();
312 }
313
314 int64_t first_read_time_us = 0;
315 if (pb_entry.has_first_read_time_us()) {
316 first_read_time_us = pb_entry.first_read_time_us();
317 }
318
319 int64_t update_time_us = creation_time_us;
320 if (pb_entry.has_update_time_us()) {
321 update_time_us = pb_entry.update_time_us();
322 }
323
324 int64_t update_title_time_us = 0;
325 if (pb_entry.has_update_title_time_us()) {
326 update_title_time_us = pb_entry.update_title_time_us();
327 }
328 if (update_title_time_us == 0) {
329 // Entries created before title could be modified don't have
330 // update_title_time_us. Set it to creation_time_us for consistency.
331 update_title_time_us = creation_time_us;
332 }
333
334 State state = UNSEEN;
335 if (pb_entry.has_status()) {
336 switch (pb_entry.status()) {
337 case reading_list::ReadingListLocal::READ:
338 state = READ;
339 break;
340 case reading_list::ReadingListLocal::UNREAD:
341 state = UNREAD;
342 break;
343 case reading_list::ReadingListLocal::UNSEEN:
344 state = UNSEEN;
345 break;
346 }
347 }
348
349 ReadingListEntry::DistillationState distillation_state =
350 ReadingListEntry::WAITING;
351 if (pb_entry.has_distillation_state()) {
352 switch (pb_entry.distillation_state()) {
353 case reading_list::ReadingListLocal::WAITING:
354 distillation_state = ReadingListEntry::WAITING;
355 break;
356 case reading_list::ReadingListLocal::PROCESSING:
357 distillation_state = ReadingListEntry::PROCESSING;
358 break;
359 case reading_list::ReadingListLocal::PROCESSED:
360 distillation_state = ReadingListEntry::PROCESSED;
361 break;
362 case reading_list::ReadingListLocal::WILL_RETRY:
363 distillation_state = ReadingListEntry::WILL_RETRY;
364 break;
365 case reading_list::ReadingListLocal::DISTILLATION_ERROR:
366 distillation_state = ReadingListEntry::DISTILLATION_ERROR;
367 break;
368 }
369 }
370
371 base::FilePath distilled_path;
372 if (pb_entry.has_distilled_path()) {
373 distilled_path = base::FilePath::FromUTF8Unsafe(pb_entry.distilled_path());
374 }
375
376 GURL distilled_url;
377 if (pb_entry.has_distilled_url()) {
378 distilled_url = GURL(pb_entry.distilled_url());
379 }
380
381 int64_t distillation_time_us = 0;
382 if (pb_entry.has_distillation_time_us()) {
383 distillation_time_us = pb_entry.distillation_time_us();
384 }
385
386 int64_t distillation_size = 0;
387 if (pb_entry.has_distillation_size()) {
388 distillation_size = pb_entry.distillation_size();
389 }
390
391 int64_t failed_download_counter = 0;
392 if (pb_entry.has_failed_download_counter()) {
393 failed_download_counter = pb_entry.failed_download_counter();
394 }
395
396 std::unique_ptr<net::BackoffEntry> backoff;
397 if (pb_entry.has_backoff()) {
398 JSONStringValueDeserializer deserializer(pb_entry.backoff());
399 std::unique_ptr<base::Value> value(
400 deserializer.Deserialize(nullptr, nullptr));
401 if (value) {
402 backoff = net::BackoffEntrySerializer::DeserializeFromValue(
403 *value, &kBackoffPolicy, nullptr, now);
404 }
405 }
406
407 reading_list::ContentSuggestionsExtra content_suggestions_extra;
408 if (pb_entry.has_content_suggestions_extra()) {
409 const reading_list::ReadingListContentSuggestionsExtra& pb_extra =
410 pb_entry.content_suggestions_extra();
411 if (pb_extra.has_dismissed()) {
412 content_suggestions_extra.dismissed = pb_extra.dismissed();
413 }
414 }
415
416 return base::WrapUnique<ReadingListEntry>(new ReadingListEntry(
417 url, title, state, creation_time_us, first_read_time_us, update_time_us,
418 update_title_time_us, distillation_state, distilled_path, distilled_url,
419 distillation_time_us, distillation_size, failed_download_counter,
420 std::move(backoff), content_suggestions_extra));
421 }
422
423 // static
FromReadingListSpecifics(const sync_pb::ReadingListSpecifics & pb_entry,const base::Time & now)424 std::unique_ptr<ReadingListEntry> ReadingListEntry::FromReadingListSpecifics(
425 const sync_pb::ReadingListSpecifics& pb_entry,
426 const base::Time& now) {
427 if (!pb_entry.has_url()) {
428 return nullptr;
429 }
430 GURL url(pb_entry.url());
431 if (url.is_empty() || !url.is_valid()) {
432 return nullptr;
433 }
434 std::string title;
435 if (pb_entry.has_title()) {
436 title = pb_entry.title();
437 }
438
439 int64_t creation_time_us = TimeToUS(now);
440 if (pb_entry.has_creation_time_us()) {
441 creation_time_us = pb_entry.creation_time_us();
442 }
443
444 int64_t first_read_time_us = 0;
445 if (pb_entry.has_first_read_time_us()) {
446 first_read_time_us = pb_entry.first_read_time_us();
447 }
448
449 int64_t update_time_us = creation_time_us;
450 if (pb_entry.has_update_time_us()) {
451 update_time_us = pb_entry.update_time_us();
452 }
453
454 int64_t update_title_time_us = 0;
455 if (pb_entry.has_update_title_time_us()) {
456 update_title_time_us = pb_entry.update_title_time_us();
457 }
458 if (update_title_time_us == 0) {
459 // Entries created before title could be modified don't have
460 // update_title_time_us. Set it to creation_time_us for consistency.
461 update_title_time_us = creation_time_us;
462 }
463
464 State state = UNSEEN;
465 if (pb_entry.has_status()) {
466 switch (pb_entry.status()) {
467 case sync_pb::ReadingListSpecifics::READ:
468 state = READ;
469 break;
470 case sync_pb::ReadingListSpecifics::UNREAD:
471 state = UNREAD;
472 break;
473 case sync_pb::ReadingListSpecifics::UNSEEN:
474 state = UNSEEN;
475 break;
476 }
477 }
478
479 return base::WrapUnique<ReadingListEntry>(new ReadingListEntry(
480 url, title, state, creation_time_us, first_read_time_us, update_time_us,
481 update_title_time_us, WAITING, base::FilePath(), GURL(), 0, 0, 0, nullptr,
482 reading_list::ContentSuggestionsExtra()));
483 }
484
MergeWithEntry(const ReadingListEntry & other)485 void ReadingListEntry::MergeWithEntry(const ReadingListEntry& other) {
486 #if !defined(NDEBUG)
487 // Checks that the result entry respects the sync order.
488 std::unique_ptr<sync_pb::ReadingListSpecifics> old_this_pb(
489 AsReadingListSpecifics());
490 std::unique_ptr<sync_pb::ReadingListSpecifics> other_pb(
491 other.AsReadingListSpecifics());
492 #endif
493 DCHECK(url_ == other.url_);
494 if (update_title_time_us_ < other.update_title_time_us_) {
495 // Take the most recent title updated.
496 title_ = std::move(other.title_);
497 update_title_time_us_ = std::move(other.update_title_time_us_);
498 } else if (update_title_time_us_ == other.update_title_time_us_) {
499 if (title_.compare(other.title_) < 0) {
500 // Take the last in alphabetical order or the longer one.
501 // This ensure empty string is replaced.
502 title_ = std::move(other.title_);
503 }
504 }
505 if (creation_time_us_ < other.creation_time_us_) {
506 creation_time_us_ = std::move(other.creation_time_us_);
507 first_read_time_us_ = std::move(other.first_read_time_us_);
508 } else if (creation_time_us_ == other.creation_time_us_) {
509 // The first_time_read_us from |other| is used if
510 // - this.first_time_read_us == 0: the entry was never read in this device.
511 // - this.first_time_read_us > other.first_time_read_us: the entry was
512 // first read on another device.
513 if (first_read_time_us_ == 0 ||
514 (other.first_read_time_us_ != 0 &&
515 other.first_read_time_us_ < first_read_time_us_)) {
516 first_read_time_us_ = std::move(other.first_read_time_us_);
517 }
518 }
519 if (update_time_us_ < other.update_time_us_) {
520 update_time_us_ = std::move(other.update_time_us_);
521 state_ = std::move(other.state_);
522 } else if (update_time_us_ == other.update_time_us_) {
523 if (state_ == UNSEEN) {
524 state_ = std::move(other.state_);
525 } else if (other.state_ == READ) {
526 state_ = std::move(other.state_);
527 }
528 }
529 #if !defined(NDEBUG)
530 std::unique_ptr<sync_pb::ReadingListSpecifics> new_this_pb(
531 AsReadingListSpecifics());
532 DCHECK(ReadingListStore::CompareEntriesForSync(*old_this_pb, *new_this_pb));
533 DCHECK(ReadingListStore::CompareEntriesForSync(*other_pb, *new_this_pb));
534 #endif
535 }
536
537 std::unique_ptr<reading_list::ReadingListLocal>
AsReadingListLocal(const base::Time & now) const538 ReadingListEntry::AsReadingListLocal(const base::Time& now) const {
539 std::unique_ptr<reading_list::ReadingListLocal> pb_entry =
540 std::make_unique<reading_list::ReadingListLocal>();
541
542 // URL is used as the key for the database and sync as there is only one entry
543 // per URL.
544 pb_entry->set_entry_id(URL().spec());
545 pb_entry->set_title(Title());
546 pb_entry->set_url(URL().spec());
547 pb_entry->set_creation_time_us(CreationTime());
548 pb_entry->set_first_read_time_us(FirstReadTime());
549 pb_entry->set_update_time_us(UpdateTime());
550 pb_entry->set_update_title_time_us(UpdateTitleTime());
551
552 switch (state_) {
553 case READ:
554 pb_entry->set_status(reading_list::ReadingListLocal::READ);
555 break;
556 case UNREAD:
557 pb_entry->set_status(reading_list::ReadingListLocal::UNREAD);
558 break;
559 case UNSEEN:
560 pb_entry->set_status(reading_list::ReadingListLocal::UNSEEN);
561 break;
562 }
563
564 reading_list::ReadingListLocal::DistillationState distilation_state =
565 reading_list::ReadingListLocal::WAITING;
566 switch (DistilledState()) {
567 case ReadingListEntry::WAITING:
568 distilation_state = reading_list::ReadingListLocal::WAITING;
569 break;
570 case ReadingListEntry::PROCESSING:
571 distilation_state = reading_list::ReadingListLocal::PROCESSING;
572 break;
573 case ReadingListEntry::PROCESSED:
574 distilation_state = reading_list::ReadingListLocal::PROCESSED;
575 break;
576 case ReadingListEntry::WILL_RETRY:
577 distilation_state = reading_list::ReadingListLocal::WILL_RETRY;
578 break;
579 case ReadingListEntry::DISTILLATION_ERROR:
580 distilation_state = reading_list::ReadingListLocal::DISTILLATION_ERROR;
581 break;
582 }
583 pb_entry->set_distillation_state(distilation_state);
584 if (!DistilledPath().empty()) {
585 pb_entry->set_distilled_path(DistilledPath().AsUTF8Unsafe());
586 }
587 if (DistilledURL().is_valid()) {
588 pb_entry->set_distilled_url(DistilledURL().spec());
589 }
590 if (DistillationTime()) {
591 pb_entry->set_distillation_time_us(DistillationTime());
592 }
593 if (DistillationSize()) {
594 pb_entry->set_distillation_size(DistillationSize());
595 }
596
597 pb_entry->set_failed_download_counter(failed_download_counter_);
598
599 if (backoff_) {
600 std::unique_ptr<base::Value> backoff =
601 net::BackoffEntrySerializer::SerializeToValue(*backoff_, now);
602
603 std::string output;
604 JSONStringValueSerializer serializer(&output);
605 serializer.Serialize(*backoff);
606 pb_entry->set_backoff(output);
607 }
608
609 reading_list::ReadingListContentSuggestionsExtra* pb_extra =
610 pb_entry->mutable_content_suggestions_extra();
611 pb_extra->set_dismissed(content_suggestions_extra_.dismissed);
612
613 return pb_entry;
614 }
615
616 std::unique_ptr<sync_pb::ReadingListSpecifics>
AsReadingListSpecifics() const617 ReadingListEntry::AsReadingListSpecifics() const {
618 std::unique_ptr<sync_pb::ReadingListSpecifics> pb_entry =
619 std::make_unique<sync_pb::ReadingListSpecifics>();
620
621 // URL is used as the key for the database and sync as there is only one entry
622 // per URL.
623 pb_entry->set_entry_id(URL().spec());
624 pb_entry->set_title(Title());
625 pb_entry->set_url(URL().spec());
626 pb_entry->set_creation_time_us(CreationTime());
627 pb_entry->set_first_read_time_us(FirstReadTime());
628 pb_entry->set_update_time_us(UpdateTime());
629 pb_entry->set_update_title_time_us(UpdateTitleTime());
630
631 switch (state_) {
632 case READ:
633 pb_entry->set_status(sync_pb::ReadingListSpecifics::READ);
634 break;
635 case UNREAD:
636 pb_entry->set_status(sync_pb::ReadingListSpecifics::UNREAD);
637 break;
638 case UNSEEN:
639 pb_entry->set_status(sync_pb::ReadingListSpecifics::UNSEEN);
640 break;
641 }
642
643 return pb_entry;
644 }
645