Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(265)

Side by Side Diff: components/reading_list/ios/reading_list_entry.cc

Issue 2764533002: Reading List iOS: Use external clock in ReadingListEntry. (Closed)
Patch Set: fix microseconds Created 3 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2016 The Chromium Authors. All rights reserved. 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 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "components/reading_list/ios/reading_list_entry.h" 5 #include "components/reading_list/ios/reading_list_entry.h"
6 6
7 #include "base/json/json_string_value_serializer.h" 7 #include "base/json/json_string_value_serializer.h"
8 #include "base/memory/ptr_util.h" 8 #include "base/memory/ptr_util.h"
9 #include "components/reading_list/ios/offline_url_utils.h" 9 #include "components/reading_list/ios/offline_url_utils.h"
10 #include "components/reading_list/ios/proto/reading_list.pb.h" 10 #include "components/reading_list/ios/proto/reading_list.pb.h"
11 #include "components/reading_list/ios/reading_list_store.h" 11 #include "components/reading_list/ios/reading_list_store.h"
12 #include "components/sync/protocol/reading_list_specifics.pb.h" 12 #include "components/sync/protocol/reading_list_specifics.pb.h"
13 #include "net/base/backoff_entry_serializer.h" 13 #include "net/base/backoff_entry_serializer.h"
14 14
15 namespace {
16 // Converts |time| to the number of microseconds since Jan 1st 1970.
17 int64_t TimeToUS(const base::Time& time) {
18 return (time - base::Time::UnixEpoch()).InMicroseconds();
19 }
20 }
21
15 // The backoff time is the following: 10min, 10min, 1h, 2h, 2h..., starting 22 // The backoff time is the following: 10min, 10min, 1h, 2h, 2h..., starting
16 // after the first failure. 23 // after the first failure.
17 const net::BackoffEntry::Policy ReadingListEntry::kBackoffPolicy = { 24 const net::BackoffEntry::Policy ReadingListEntry::kBackoffPolicy = {
18 // Number of initial errors (in sequence) to ignore before applying 25 // Number of initial errors (in sequence) to ignore before applying
19 // exponential back-off rules. 26 // exponential back-off rules.
20 2, 27 2,
21 28
22 // Initial delay for exponential back-off in ms. 29 // Initial delay for exponential back-off in ms.
23 10 * 60 * 1000, // 10 minutes. 30 10 * 60 * 1000, // 10 minutes.
24 31
25 // Factor by which the waiting time will be multiplied. 32 // Factor by which the waiting time will be multiplied.
26 6, 33 6,
27 34
28 // Fuzzing percentage. ex: 10% will spread requests randomly 35 // Fuzzing percentage. ex: 10% will spread requests randomly
29 // between 90%-100% of the calculated time. 36 // between 90%-100% of the calculated time.
30 0.1, // 10%. 37 0.1, // 10%.
31 38
32 // Maximum amount of time we are willing to delay our request in ms. 39 // Maximum amount of time we are willing to delay our request in ms.
33 2 * 3600 * 1000, // 2 hours. 40 2 * 3600 * 1000, // 2 hours.
34 41
35 // Time to keep an entry from being discarded even when it 42 // Time to keep an entry from being discarded even when it
36 // has no significant state, -1 to never discard. 43 // has no significant state, -1 to never discard.
37 -1, 44 -1,
38 45
39 true, // Don't use initial delay unless the last request was an error. 46 true, // Don't use initial delay unless the last request was an error.
40 }; 47 };
41 48
42 ReadingListEntry::ReadingListEntry(const GURL& url, const std::string& title) 49 ReadingListEntry::ReadingListEntry(const GURL& url,
43 : ReadingListEntry(url, title, nullptr){}; 50 const std::string& title,
51 const base::Time& now)
52 : ReadingListEntry(url, title, now, nullptr){};
44 53
45 ReadingListEntry::ReadingListEntry(const GURL& url, 54 ReadingListEntry::ReadingListEntry(const GURL& url,
46 const std::string& title, 55 const std::string& title,
56 const base::Time& now,
47 std::unique_ptr<net::BackoffEntry> backoff) 57 std::unique_ptr<net::BackoffEntry> backoff)
48 : ReadingListEntry(url, 58 : ReadingListEntry(url,
49 title, 59 title,
50 UNSEEN, 60 UNSEEN,
61 TimeToUS(now),
51 0, 62 0,
52 0, 63 TimeToUS(now),
53 0, 64 TimeToUS(now),
54 0,
55 WAITING, 65 WAITING,
56 base::FilePath(), 66 base::FilePath(),
57 GURL(), 67 GURL(),
58 0, 68 0,
59 0, 69 0,
60 0, 70 0,
61 std::move(backoff)) {} 71 std::move(backoff)) {}
62 72
63 ReadingListEntry::ReadingListEntry( 73 ReadingListEntry::ReadingListEntry(
64 const GURL& url, 74 const GURL& url,
(...skipping 21 matching lines...) Expand all
86 first_read_time_us_(first_read_time), 96 first_read_time_us_(first_read_time),
87 update_time_us_(update_time), 97 update_time_us_(update_time),
88 update_title_time_us_(update_title_time), 98 update_title_time_us_(update_title_time),
89 distillation_time_us_(distillation_time), 99 distillation_time_us_(distillation_time),
90 distillation_size_(distillation_size) { 100 distillation_size_(distillation_size) {
91 if (backoff) { 101 if (backoff) {
92 backoff_ = std::move(backoff); 102 backoff_ = std::move(backoff);
93 } else { 103 } else {
94 backoff_ = base::MakeUnique<net::BackoffEntry>(&kBackoffPolicy); 104 backoff_ = base::MakeUnique<net::BackoffEntry>(&kBackoffPolicy);
95 } 105 }
96 if (creation_time_us_ == 0) { 106 DCHECK(creation_time_us_);
97 DCHECK(update_time_us_ == 0); 107 DCHECK(update_time_us_);
98 DCHECK(update_title_time_us_ == 0); 108 DCHECK(update_title_time_us_);
99 creation_time_us_ =
100 (base::Time::Now() - base::Time::UnixEpoch()).InMicroseconds();
101 update_time_us_ = creation_time_us_;
102 update_title_time_us_ = creation_time_us_;
103 }
104 DCHECK(!url.is_empty()); 109 DCHECK(!url.is_empty());
105 DCHECK(url.is_valid()); 110 DCHECK(url.is_valid());
106 } 111 }
107 112
108 ReadingListEntry::ReadingListEntry(ReadingListEntry&& entry) 113 ReadingListEntry::ReadingListEntry(ReadingListEntry&& entry)
109 : url_(std::move(entry.url_)), 114 : url_(std::move(entry.url_)),
110 title_(std::move(entry.title_)), 115 title_(std::move(entry.title_)),
111 state_(std::move(entry.state_)), 116 state_(std::move(entry.state_)),
112 distilled_path_(std::move(entry.distilled_path_)), 117 distilled_path_(std::move(entry.distilled_path_)),
113 distilled_url_(std::move(entry.distilled_url_)), 118 distilled_url_(std::move(entry.distilled_url_)),
(...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after
174 update_title_time_us_ = std::move(other.update_title_time_us_); 179 update_title_time_us_ = std::move(other.update_title_time_us_);
175 distillation_time_us_ = std::move(other.distillation_time_us_); 180 distillation_time_us_ = std::move(other.distillation_time_us_);
176 distillation_size_ = std::move(other.distillation_size_); 181 distillation_size_ = std::move(other.distillation_size_);
177 return *this; 182 return *this;
178 } 183 }
179 184
180 bool ReadingListEntry::operator==(const ReadingListEntry& other) const { 185 bool ReadingListEntry::operator==(const ReadingListEntry& other) const {
181 return url_ == other.url_; 186 return url_ == other.url_;
182 } 187 }
183 188
184 void ReadingListEntry::SetTitle(const std::string& title) { 189 void ReadingListEntry::SetTitle(const std::string& title,
190 const base::Time& now) {
185 title_ = title; 191 title_ = title;
186 update_title_time_us_ = 192 update_title_time_us_ = TimeToUS(now);
187 (base::Time::Now() - base::Time::UnixEpoch()).InMicroseconds();
188 } 193 }
189 194
190 void ReadingListEntry::SetRead(bool read) { 195 void ReadingListEntry::SetRead(bool read, const base::Time& now) {
191 State previous_state = state_; 196 State previous_state = state_;
192 state_ = read ? READ : UNREAD; 197 state_ = read ? READ : UNREAD;
193 if (state_ == previous_state) { 198 if (state_ == previous_state) {
194 return; 199 return;
195 } 200 }
196 if (FirstReadTime() == 0 && read) { 201 if (FirstReadTime() == 0 && read) {
197 first_read_time_us_ = 202 first_read_time_us_ = TimeToUS(now);
198 (base::Time::Now() - base::Time::UnixEpoch()).InMicroseconds();
199 } 203 }
200 if (!(previous_state == UNSEEN && state_ == UNREAD)) { 204 if (!(previous_state == UNSEEN && state_ == UNREAD)) {
201 // If changing UNSEEN -> UNREAD, entry is not marked updated to preserve 205 // If changing UNSEEN -> UNREAD, entry is not marked updated to preserve
202 // order in Reading List View. 206 // order in Reading List View.
203 MarkEntryUpdated(); 207 MarkEntryUpdated(now);
204 } 208 }
205 } 209 }
206 210
207 bool ReadingListEntry::IsRead() const { 211 bool ReadingListEntry::IsRead() const {
208 return state_ == READ; 212 return state_ == READ;
209 } 213 }
210 214
211 bool ReadingListEntry::HasBeenSeen() const { 215 bool ReadingListEntry::HasBeenSeen() const {
212 return state_ != UNSEEN; 216 return state_ != UNSEEN;
213 } 217 }
214 218
215 void ReadingListEntry::SetDistilledInfo(const base::FilePath& path, 219 void ReadingListEntry::SetDistilledInfo(const base::FilePath& path,
216 const GURL& distilled_url, 220 const GURL& distilled_url,
217 int64_t distilation_size, 221 int64_t distilation_size,
218 int64_t distilation_time) { 222 const base::Time& distilation_time) {
219 DCHECK(!path.empty()); 223 DCHECK(!path.empty());
220 DCHECK(distilled_url.is_valid()); 224 DCHECK(distilled_url.is_valid());
221 distilled_path_ = path; 225 distilled_path_ = path;
222 distilled_state_ = PROCESSED; 226 distilled_state_ = PROCESSED;
223 distilled_url_ = distilled_url; 227 distilled_url_ = distilled_url;
224 distillation_time_us_ = distilation_time; 228 distillation_time_us_ = TimeToUS(distilation_time);
225 ;
226 distillation_size_ = distilation_size; 229 distillation_size_ = distilation_size;
227 backoff_->Reset(); 230 backoff_->Reset();
228 failed_download_counter_ = 0; 231 failed_download_counter_ = 0;
229 } 232 }
230 233
231 void ReadingListEntry::SetDistilledState(DistillationState distilled_state) { 234 void ReadingListEntry::SetDistilledState(DistillationState distilled_state) {
232 DCHECK(distilled_state != PROCESSED); // use SetDistilledPath instead. 235 DCHECK(distilled_state != PROCESSED); // use SetDistilledPath instead.
233 DCHECK(distilled_state != WAITING); 236 DCHECK(distilled_state != WAITING);
234 // Increase time until next retry exponentially if the state change from a 237 // Increase time until next retry exponentially if the state change from a
235 // non-error state to an error state. 238 // non-error state to an error state.
(...skipping 17 matching lines...) Expand all
253 } 256 }
254 257
255 int64_t ReadingListEntry::CreationTime() const { 258 int64_t ReadingListEntry::CreationTime() const {
256 return creation_time_us_; 259 return creation_time_us_;
257 } 260 }
258 261
259 int64_t ReadingListEntry::FirstReadTime() const { 262 int64_t ReadingListEntry::FirstReadTime() const {
260 return first_read_time_us_; 263 return first_read_time_us_;
261 } 264 }
262 265
263 void ReadingListEntry::MarkEntryUpdated() { 266 void ReadingListEntry::MarkEntryUpdated(const base::Time& now) {
264 update_time_us_ = 267 update_time_us_ = TimeToUS(now);
265 (base::Time::Now() - base::Time::UnixEpoch()).InMicroseconds();
266 } 268 }
267 269
268 // static 270 // static
269 std::unique_ptr<ReadingListEntry> ReadingListEntry::FromReadingListLocal( 271 std::unique_ptr<ReadingListEntry> ReadingListEntry::FromReadingListLocal(
270 const reading_list::ReadingListLocal& pb_entry) { 272 const reading_list::ReadingListLocal& pb_entry,
273 const base::Time& now) {
271 if (!pb_entry.has_url()) { 274 if (!pb_entry.has_url()) {
272 return nullptr; 275 return nullptr;
273 } 276 }
274 GURL url(pb_entry.url()); 277 GURL url(pb_entry.url());
275 if (url.is_empty() || !url.is_valid()) { 278 if (url.is_empty() || !url.is_valid()) {
276 return nullptr; 279 return nullptr;
277 } 280 }
278 std::string title; 281 std::string title;
279 if (pb_entry.has_title()) { 282 if (pb_entry.has_title()) {
280 title = pb_entry.title(); 283 title = pb_entry.title();
281 } 284 }
282 285
283 int64_t creation_time_us = 0; 286 int64_t creation_time_us = 0;
284 if (pb_entry.has_creation_time_us()) { 287 if (pb_entry.has_creation_time_us()) {
285 creation_time_us = pb_entry.creation_time_us(); 288 creation_time_us = pb_entry.creation_time_us();
289 } else {
290 creation_time_us = (now - base::Time::UnixEpoch()).InMicroseconds();
gambard 2017/03/21 13:08:06 I am not sure I understand this. What is the use?
Olivier 2017/03/21 14:01:09 We have to be conservative when deserializing data
gambard 2017/03/21 15:36:53 Acknowledged.
286 } 291 }
287 292
288 int64_t first_read_time_us = 0; 293 int64_t first_read_time_us = 0;
289 if (pb_entry.has_first_read_time_us()) { 294 if (pb_entry.has_first_read_time_us()) {
290 first_read_time_us = pb_entry.first_read_time_us(); 295 first_read_time_us = pb_entry.first_read_time_us();
291 } 296 }
292 297
293 int64_t update_time_us = 0; 298 int64_t update_time_us = creation_time_us;
294 if (pb_entry.has_update_time_us()) { 299 if (pb_entry.has_update_time_us()) {
295 update_time_us = pb_entry.update_time_us(); 300 update_time_us = pb_entry.update_time_us();
296 } 301 }
297 302
298 int64_t update_title_time_us = 0; 303 int64_t update_title_time_us = creation_time_us;
299 if (pb_entry.has_update_title_time_us()) { 304 if (pb_entry.has_update_title_time_us()) {
300 update_title_time_us = pb_entry.update_title_time_us(); 305 update_title_time_us = pb_entry.update_title_time_us();
301 } 306 }
302 307
303 State state = UNSEEN; 308 State state = UNSEEN;
304 if (pb_entry.has_status()) { 309 if (pb_entry.has_status()) {
305 switch (pb_entry.status()) { 310 switch (pb_entry.status()) {
306 case reading_list::ReadingListLocal::READ: 311 case reading_list::ReadingListLocal::READ:
307 state = READ; 312 state = READ;
308 break; 313 break;
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after
362 failed_download_counter = pb_entry.failed_download_counter(); 367 failed_download_counter = pb_entry.failed_download_counter();
363 } 368 }
364 369
365 std::unique_ptr<net::BackoffEntry> backoff; 370 std::unique_ptr<net::BackoffEntry> backoff;
366 if (pb_entry.has_backoff()) { 371 if (pb_entry.has_backoff()) {
367 JSONStringValueDeserializer deserializer(pb_entry.backoff()); 372 JSONStringValueDeserializer deserializer(pb_entry.backoff());
368 std::unique_ptr<base::Value> value( 373 std::unique_ptr<base::Value> value(
369 deserializer.Deserialize(nullptr, nullptr)); 374 deserializer.Deserialize(nullptr, nullptr));
370 if (value) { 375 if (value) {
371 backoff = net::BackoffEntrySerializer::DeserializeFromValue( 376 backoff = net::BackoffEntrySerializer::DeserializeFromValue(
372 *value, &kBackoffPolicy, nullptr, base::Time::Now()); 377 *value, &kBackoffPolicy, nullptr, now);
373 } 378 }
374 } 379 }
375 380
376 return base::WrapUnique<ReadingListEntry>(new ReadingListEntry( 381 return base::WrapUnique<ReadingListEntry>(new ReadingListEntry(
377 url, title, state, creation_time_us, first_read_time_us, update_time_us, 382 url, title, state, creation_time_us, first_read_time_us, update_time_us,
378 update_title_time_us, distillation_state, distilled_path, distilled_url, 383 update_title_time_us, distillation_state, distilled_path, distilled_url,
379 distillation_time_us, distillation_size, failed_download_counter, 384 distillation_time_us, distillation_size, failed_download_counter,
380 std::move(backoff))); 385 std::move(backoff)));
381 } 386 }
382 387
383 // static 388 // static
384 std::unique_ptr<ReadingListEntry> ReadingListEntry::FromReadingListSpecifics( 389 std::unique_ptr<ReadingListEntry> ReadingListEntry::FromReadingListSpecifics(
385 const sync_pb::ReadingListSpecifics& pb_entry) { 390 const sync_pb::ReadingListSpecifics& pb_entry,
391 const base::Time& now) {
386 if (!pb_entry.has_url()) { 392 if (!pb_entry.has_url()) {
387 return nullptr; 393 return nullptr;
388 } 394 }
389 GURL url(pb_entry.url()); 395 GURL url(pb_entry.url());
390 if (url.is_empty() || !url.is_valid()) { 396 if (url.is_empty() || !url.is_valid()) {
391 return nullptr; 397 return nullptr;
392 } 398 }
393 std::string title; 399 std::string title;
394 if (pb_entry.has_title()) { 400 if (pb_entry.has_title()) {
395 title = pb_entry.title(); 401 title = pb_entry.title();
396 } 402 }
397 403
398 int64_t creation_time_us = 0; 404 int64_t creation_time_us = TimeToUS(now);
399 if (pb_entry.has_creation_time_us()) { 405 if (pb_entry.has_creation_time_us()) {
400 creation_time_us = pb_entry.creation_time_us(); 406 creation_time_us = pb_entry.creation_time_us();
401 } 407 }
402 408
403 int64_t first_read_time_us = 0; 409 int64_t first_read_time_us = 0;
404 if (pb_entry.has_first_read_time_us()) { 410 if (pb_entry.has_first_read_time_us()) {
405 first_read_time_us = pb_entry.first_read_time_us(); 411 first_read_time_us = pb_entry.first_read_time_us();
406 } 412 }
407 413
408 int64_t update_time_us = 0; 414 int64_t update_time_us = creation_time_us;
409 if (pb_entry.has_update_time_us()) { 415 if (pb_entry.has_update_time_us()) {
410 update_time_us = pb_entry.update_time_us(); 416 update_time_us = pb_entry.update_time_us();
411 } 417 }
412 418
413 int64_t update_title_time_us = 0; 419 int64_t update_title_time_us = creation_time_us;
414 if (pb_entry.has_update_title_time_us()) { 420 if (pb_entry.has_update_title_time_us()) {
415 update_title_time_us = pb_entry.update_title_time_us(); 421 update_title_time_us = pb_entry.update_title_time_us();
416 } 422 }
417 423
418 State state = UNSEEN; 424 State state = UNSEEN;
419 if (pb_entry.has_status()) { 425 if (pb_entry.has_status()) {
420 switch (pb_entry.status()) { 426 switch (pb_entry.status()) {
421 case sync_pb::ReadingListSpecifics::READ: 427 case sync_pb::ReadingListSpecifics::READ:
422 state = READ; 428 state = READ;
423 break; 429 break;
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after
482 } 488 }
483 #if !defined(NDEBUG) 489 #if !defined(NDEBUG)
484 std::unique_ptr<sync_pb::ReadingListSpecifics> new_this_pb( 490 std::unique_ptr<sync_pb::ReadingListSpecifics> new_this_pb(
485 AsReadingListSpecifics()); 491 AsReadingListSpecifics());
486 DCHECK(ReadingListStore::CompareEntriesForSync(*old_this_pb, *new_this_pb)); 492 DCHECK(ReadingListStore::CompareEntriesForSync(*old_this_pb, *new_this_pb));
487 DCHECK(ReadingListStore::CompareEntriesForSync(*other_pb, *new_this_pb)); 493 DCHECK(ReadingListStore::CompareEntriesForSync(*other_pb, *new_this_pb));
488 #endif 494 #endif
489 } 495 }
490 496
491 std::unique_ptr<reading_list::ReadingListLocal> 497 std::unique_ptr<reading_list::ReadingListLocal>
492 ReadingListEntry::AsReadingListLocal() const { 498 ReadingListEntry::AsReadingListLocal(const base::Time& now) const {
493 std::unique_ptr<reading_list::ReadingListLocal> pb_entry = 499 std::unique_ptr<reading_list::ReadingListLocal> pb_entry =
494 base::MakeUnique<reading_list::ReadingListLocal>(); 500 base::MakeUnique<reading_list::ReadingListLocal>();
495 501
496 // URL is used as the key for the database and sync as there is only one entry 502 // URL is used as the key for the database and sync as there is only one entry
497 // per URL. 503 // per URL.
498 pb_entry->set_entry_id(URL().spec()); 504 pb_entry->set_entry_id(URL().spec());
499 pb_entry->set_title(Title()); 505 pb_entry->set_title(Title());
500 pb_entry->set_url(URL().spec()); 506 pb_entry->set_url(URL().spec());
501 pb_entry->set_creation_time_us(CreationTime()); 507 pb_entry->set_creation_time_us(CreationTime());
502 pb_entry->set_first_read_time_us(FirstReadTime()); 508 pb_entry->set_first_read_time_us(FirstReadTime());
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
544 pb_entry->set_distillation_time_us(DistillationTime()); 550 pb_entry->set_distillation_time_us(DistillationTime());
545 } 551 }
546 if (DistillationSize()) { 552 if (DistillationSize()) {
547 pb_entry->set_distillation_size(DistillationSize()); 553 pb_entry->set_distillation_size(DistillationSize());
548 } 554 }
549 555
550 pb_entry->set_failed_download_counter(failed_download_counter_); 556 pb_entry->set_failed_download_counter(failed_download_counter_);
551 557
552 if (backoff_) { 558 if (backoff_) {
553 std::unique_ptr<base::Value> backoff = 559 std::unique_ptr<base::Value> backoff =
554 net::BackoffEntrySerializer::SerializeToValue(*backoff_, 560 net::BackoffEntrySerializer::SerializeToValue(*backoff_, now);
555 base::Time::Now());
556 561
557 std::string output; 562 std::string output;
558 JSONStringValueSerializer serializer(&output); 563 JSONStringValueSerializer serializer(&output);
559 serializer.Serialize(*backoff); 564 serializer.Serialize(*backoff);
560 pb_entry->set_backoff(output); 565 pb_entry->set_backoff(output);
561 } 566 }
562 return pb_entry; 567 return pb_entry;
563 } 568 }
564 569
565 std::unique_ptr<sync_pb::ReadingListSpecifics> 570 std::unique_ptr<sync_pb::ReadingListSpecifics>
(...skipping 18 matching lines...) Expand all
584 case UNREAD: 589 case UNREAD:
585 pb_entry->set_status(sync_pb::ReadingListSpecifics::UNREAD); 590 pb_entry->set_status(sync_pb::ReadingListSpecifics::UNREAD);
586 break; 591 break;
587 case UNSEEN: 592 case UNSEEN:
588 pb_entry->set_status(sync_pb::ReadingListSpecifics::UNSEEN); 593 pb_entry->set_status(sync_pb::ReadingListSpecifics::UNSEEN);
589 break; 594 break;
590 } 595 }
591 596
592 return pb_entry; 597 return pb_entry;
593 } 598 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698