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