OLD | NEW |
---|---|
1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 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 "content/browser/background_sync/background_sync_manager.h" | 5 #include "content/browser/background_sync/background_sync_manager.h" |
6 | 6 |
7 #include "base/barrier_closure.h" | 7 #include "base/barrier_closure.h" |
8 #include "base/bind.h" | 8 #include "base/bind.h" |
9 #include "content/browser/background_sync/background_sync_network_observer.h" | 9 #include "content/browser/background_sync/background_sync_network_observer.h" |
10 #include "content/browser/service_worker/service_worker_context_wrapper.h" | 10 #include "content/browser/service_worker/service_worker_context_wrapper.h" |
(...skipping 211 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
222 registration_proto.periodicity()); | 222 registration_proto.periodicity()); |
223 BackgroundSyncRegistration* registration = | 223 BackgroundSyncRegistration* registration = |
224 ®istrations->registration_map[registration_key]; | 224 ®istrations->registration_map[registration_key]; |
225 | 225 |
226 registration->id = registration_proto.id(); | 226 registration->id = registration_proto.id(); |
227 registration->tag = registration_proto.tag(); | 227 registration->tag = registration_proto.tag(); |
228 registration->periodicity = registration_proto.periodicity(); | 228 registration->periodicity = registration_proto.periodicity(); |
229 registration->min_period = registration_proto.min_period(); | 229 registration->min_period = registration_proto.min_period(); |
230 registration->network_state = registration_proto.network_state(); | 230 registration->network_state = registration_proto.network_state(); |
231 registration->power_state = registration_proto.power_state(); | 231 registration->power_state = registration_proto.power_state(); |
232 registration->sync_state = registration_proto.sync_state(); | |
233 if (registration->sync_state == SYNC_STATE_FIRING) { | |
234 // If the browser (or worker) closed while firing the event, consider | |
235 // it pending again> | |
236 registration->sync_state = SYNC_STATE_PENDING; | |
237 } | |
232 } | 238 } |
233 } | 239 } |
234 | 240 |
235 if (corruption_detected) | 241 if (corruption_detected) |
236 break; | 242 break; |
237 } | 243 } |
238 | 244 |
239 if (corruption_detected) { | 245 if (corruption_detected) { |
240 LOG(ERROR) << "Corruption detected in background sync backend"; | 246 LOG(ERROR) << "Corruption detected in background sync backend"; |
241 DisableAndClearManager(base::Bind(callback)); | 247 DisableAndClearManager(base::Bind(callback)); |
242 return; | 248 return; |
243 } | 249 } |
244 | 250 |
245 // TODO(jkarlin): Call the scheduling algorithm here. | 251 FireReadyEvents(); |
246 | 252 |
247 base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind(callback)); | 253 base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind(callback)); |
248 } | 254 } |
249 | 255 |
250 void BackgroundSyncManager::RegisterImpl( | 256 void BackgroundSyncManager::RegisterImpl( |
251 const GURL& origin, | 257 const GURL& origin, |
252 int64 sw_registration_id, | 258 int64 sw_registration_id, |
253 const BackgroundSyncRegistration& sync_registration, | 259 const BackgroundSyncRegistration& sync_registration, |
254 const StatusAndRegistrationCallback& callback) { | 260 const StatusAndRegistrationCallback& callback) { |
255 if (disabled_) { | 261 if (disabled_) { |
(...skipping 108 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
364 const BackgroundSyncRegistration& registration = | 370 const BackgroundSyncRegistration& registration = |
365 key_and_registration.second; | 371 key_and_registration.second; |
366 BackgroundSyncRegistrationProto* registration_proto = | 372 BackgroundSyncRegistrationProto* registration_proto = |
367 registrations_proto.add_registration(); | 373 registrations_proto.add_registration(); |
368 registration_proto->set_id(registration.id); | 374 registration_proto->set_id(registration.id); |
369 registration_proto->set_tag(registration.tag); | 375 registration_proto->set_tag(registration.tag); |
370 registration_proto->set_periodicity(registration.periodicity); | 376 registration_proto->set_periodicity(registration.periodicity); |
371 registration_proto->set_min_period(registration.min_period); | 377 registration_proto->set_min_period(registration.min_period); |
372 registration_proto->set_network_state(registration.network_state); | 378 registration_proto->set_network_state(registration.network_state); |
373 registration_proto->set_power_state(registration.power_state); | 379 registration_proto->set_power_state(registration.power_state); |
380 registration_proto->set_sync_state(registration.sync_state); | |
374 } | 381 } |
375 std::string serialized; | 382 std::string serialized; |
376 bool success = registrations_proto.SerializeToString(&serialized); | 383 bool success = registrations_proto.SerializeToString(&serialized); |
377 DCHECK(success); | 384 DCHECK(success); |
378 | 385 |
379 StoreDataInBackend(sw_registration_id, registrations.origin, | 386 StoreDataInBackend(sw_registration_id, registrations.origin, |
380 kBackgroundSyncUserDataKey, serialized, callback); | 387 kBackgroundSyncUserDataKey, serialized, callback); |
381 } | 388 } |
382 | 389 |
383 void BackgroundSyncManager::RegisterDidStore( | 390 void BackgroundSyncManager::RegisterDidStore( |
(...skipping 11 matching lines...) Expand all Loading... | |
395 } | 402 } |
396 | 403 |
397 if (status != SERVICE_WORKER_OK) { | 404 if (status != SERVICE_WORKER_OK) { |
398 LOG(ERROR) << "BackgroundSync failed to store registration due to backend " | 405 LOG(ERROR) << "BackgroundSync failed to store registration due to backend " |
399 "failure."; | 406 "failure."; |
400 DisableAndClearManager( | 407 DisableAndClearManager( |
401 base::Bind(callback, ERROR_TYPE_STORAGE, BackgroundSyncRegistration())); | 408 base::Bind(callback, ERROR_TYPE_STORAGE, BackgroundSyncRegistration())); |
402 return; | 409 return; |
403 } | 410 } |
404 | 411 |
405 // TODO(jkarlin): Run the registration algorithm. | 412 FireReadyEvents(); |
406 base::MessageLoop::current()->PostTask( | 413 base::MessageLoop::current()->PostTask( |
407 FROM_HERE, base::Bind(callback, ERROR_TYPE_OK, new_registration)); | 414 FROM_HERE, base::Bind(callback, ERROR_TYPE_OK, new_registration)); |
408 } | 415 } |
409 | 416 |
410 void BackgroundSyncManager::RemoveRegistrationFromMap( | 417 void BackgroundSyncManager::RemoveRegistrationFromMap( |
411 int64 sw_registration_id, | 418 int64 sw_registration_id, |
412 const RegistrationKey& registration_key) { | 419 const RegistrationKey& registration_key) { |
413 DCHECK(LookupRegistration(sw_registration_id, registration_key)); | 420 DCHECK(LookupRegistration(sw_registration_id, registration_key)); |
414 | 421 |
415 BackgroundSyncRegistrations* registrations = | 422 BackgroundSyncRegistrations* registrations = |
(...skipping 30 matching lines...) Expand all Loading... | |
446 void BackgroundSyncManager::GetDataFromBackend( | 453 void BackgroundSyncManager::GetDataFromBackend( |
447 const std::string& backend_key, | 454 const std::string& backend_key, |
448 const ServiceWorkerStorage::GetUserDataForAllRegistrationsCallback& | 455 const ServiceWorkerStorage::GetUserDataForAllRegistrationsCallback& |
449 callback) { | 456 callback) { |
450 DCHECK_CURRENTLY_ON(BrowserThread::IO); | 457 DCHECK_CURRENTLY_ON(BrowserThread::IO); |
451 | 458 |
452 service_worker_context_->GetUserDataForAllRegistrations(backend_key, | 459 service_worker_context_->GetUserDataForAllRegistrations(backend_key, |
453 callback); | 460 callback); |
454 } | 461 } |
455 | 462 |
463 void BackgroundSyncManager::FireOneShotSync( | |
464 const scoped_refptr<ServiceWorkerVersion>& active_version, | |
465 const ServiceWorkerVersion::StatusCallback& callback) { | |
466 DCHECK_CURRENTLY_ON(BrowserThread::IO); | |
467 | |
468 active_version->DispatchSyncEvent(callback); | |
469 } | |
470 | |
456 void BackgroundSyncManager::UnregisterImpl( | 471 void BackgroundSyncManager::UnregisterImpl( |
457 const GURL& origin, | 472 const GURL& origin, |
458 int64 sw_registration_id, | 473 int64 sw_registration_id, |
459 const RegistrationKey& registration_key, | 474 const RegistrationKey& registration_key, |
460 BackgroundSyncRegistration::RegistrationId sync_registration_id, | 475 BackgroundSyncRegistration::RegistrationId sync_registration_id, |
461 const StatusCallback& callback) { | 476 const StatusCallback& callback) { |
462 if (disabled_) { | 477 if (disabled_) { |
463 base::MessageLoop::current()->PostTask( | 478 base::MessageLoop::current()->PostTask( |
464 FROM_HERE, base::Bind(callback, ERROR_TYPE_STORAGE)); | 479 FROM_HERE, base::Bind(callback, ERROR_TYPE_STORAGE)); |
465 return; | 480 return; |
(...skipping 27 matching lines...) Expand all Loading... | |
493 FROM_HERE, base::Bind(callback, ERROR_TYPE_STORAGE)); | 508 FROM_HERE, base::Bind(callback, ERROR_TYPE_STORAGE)); |
494 return; | 509 return; |
495 } | 510 } |
496 | 511 |
497 if (status != SERVICE_WORKER_OK) { | 512 if (status != SERVICE_WORKER_OK) { |
498 LOG(ERROR) << "BackgroundSync failed to unregister due to backend failure."; | 513 LOG(ERROR) << "BackgroundSync failed to unregister due to backend failure."; |
499 DisableAndClearManager(base::Bind(callback, ERROR_TYPE_STORAGE)); | 514 DisableAndClearManager(base::Bind(callback, ERROR_TYPE_STORAGE)); |
500 return; | 515 return; |
501 } | 516 } |
502 | 517 |
503 // TODO(jkarlin): Run the registration algorithm. | |
504 base::MessageLoop::current()->PostTask(FROM_HERE, | 518 base::MessageLoop::current()->PostTask(FROM_HERE, |
505 base::Bind(callback, ERROR_TYPE_OK)); | 519 base::Bind(callback, ERROR_TYPE_OK)); |
506 } | 520 } |
507 | 521 |
508 void BackgroundSyncManager::GetRegistrationImpl( | 522 void BackgroundSyncManager::GetRegistrationImpl( |
509 const GURL& origin, | 523 const GURL& origin, |
510 int64 sw_registration_id, | 524 int64 sw_registration_id, |
511 const RegistrationKey& registration_key, | 525 const RegistrationKey& registration_key, |
512 const StatusAndRegistrationCallback& callback) { | 526 const StatusAndRegistrationCallback& callback) { |
513 if (disabled_) { | 527 if (disabled_) { |
(...skipping 25 matching lines...) Expand all Loading... | |
539 base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind(callback)); | 553 base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind(callback)); |
540 } | 554 } |
541 | 555 |
542 void BackgroundSyncManager::OnStorageWipedImpl(const base::Closure& callback) { | 556 void BackgroundSyncManager::OnStorageWipedImpl(const base::Closure& callback) { |
543 sw_to_registrations_map_.clear(); | 557 sw_to_registrations_map_.clear(); |
544 disabled_ = false; | 558 disabled_ = false; |
545 InitImpl(callback); | 559 InitImpl(callback); |
546 } | 560 } |
547 | 561 |
548 void BackgroundSyncManager::OnNetworkChanged() { | 562 void BackgroundSyncManager::OnNetworkChanged() { |
549 // TODO(jkarlin): Run the scheduling algorithm here if initialized and not | 563 FireReadyEvents(); |
550 // disabled. | |
551 } | 564 } |
552 | 565 |
553 void BackgroundSyncManager::PendingStatusAndRegistrationCallback( | 566 void BackgroundSyncManager::PendingStatusAndRegistrationCallback( |
554 const StatusAndRegistrationCallback& callback, | 567 const StatusAndRegistrationCallback& callback, |
555 ErrorType error, | 568 ErrorType error, |
556 const BackgroundSyncRegistration& sync_registration) { | 569 const BackgroundSyncRegistration& sync_registration) { |
557 // The callback might delete this object, so hang onto a weak ptr to find out. | 570 // The callback might delete this object, so hang onto a weak ptr to find out. |
558 base::WeakPtr<BackgroundSyncManager> manager = weak_ptr_factory_.GetWeakPtr(); | 571 base::WeakPtr<BackgroundSyncManager> manager = weak_ptr_factory_.GetWeakPtr(); |
559 callback.Run(error, sync_registration); | 572 callback.Run(error, sync_registration); |
560 if (manager) | 573 if (manager) |
(...skipping 11 matching lines...) Expand all Loading... | |
572 } | 585 } |
573 | 586 |
574 void BackgroundSyncManager::PendingClosure(const base::Closure& callback) { | 587 void BackgroundSyncManager::PendingClosure(const base::Closure& callback) { |
575 // The callback might delete this object, so hang onto a weak ptr to find out. | 588 // The callback might delete this object, so hang onto a weak ptr to find out. |
576 base::WeakPtr<BackgroundSyncManager> manager = weak_ptr_factory_.GetWeakPtr(); | 589 base::WeakPtr<BackgroundSyncManager> manager = weak_ptr_factory_.GetWeakPtr(); |
577 callback.Run(); | 590 callback.Run(); |
578 if (manager) | 591 if (manager) |
579 op_scheduler_.CompleteOperationAndRunNext(); | 592 op_scheduler_.CompleteOperationAndRunNext(); |
580 } | 593 } |
581 | 594 |
595 bool BackgroundSyncManager::IsRegistrationReadyToFire( | |
596 const BackgroundSyncRegistration& registration) { | |
597 // TODO(jkarlin): Add support for firing periodic registrations. | |
598 if (registration.periodicity == SYNC_PERIODIC) | |
599 return false; | |
600 | |
601 if (registration.sync_state != SYNC_STATE_PENDING) | |
602 return false; | |
603 | |
604 DCHECK_EQ(SYNC_ONE_SHOT, registration.periodicity); | |
605 | |
606 return network_observer_->NetworkSufficient(registration.network_state); | |
607 } | |
608 | |
609 void BackgroundSyncManager::FireReadyEvents() { | |
610 DCHECK_CURRENTLY_ON(BrowserThread::IO); | |
611 if (disabled_) | |
612 return; | |
613 | |
614 op_scheduler_.ScheduleOperation( | |
615 base::Bind(&BackgroundSyncManager::FireReadyEventsImpl, | |
616 weak_ptr_factory_.GetWeakPtr(), MakeEmptyCompletion())); | |
617 } | |
618 | |
619 void BackgroundSyncManager::FireReadyEventsImpl(const base::Closure& callback) { | |
620 if (disabled_) { | |
621 base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind(callback)); | |
622 return; | |
623 } | |
624 | |
625 // Find the registrations that are ready to run. | |
626 std::vector<std::pair<int64, RegistrationKey>> sw_id_and_keys_to_fire; | |
627 | |
628 for (auto& sw_id_and_registrations : sw_to_registrations_map_) { | |
629 const int64 service_worker_id = sw_id_and_registrations.first; | |
630 for (auto& key_and_registration : | |
631 sw_id_and_registrations.second.registration_map) { | |
632 BackgroundSyncRegistration* registration = &key_and_registration.second; | |
633 if (IsRegistrationReadyToFire(*registration)) { | |
634 sw_id_and_keys_to_fire.push_back( | |
635 std::make_pair(service_worker_id, key_and_registration.first)); | |
636 // The state change is not saved to persistent storage because | |
637 // if the sync event is killed mid-sync then it should return to | |
638 // SYNC_STATE_PENDING. | |
639 registration->sync_state = SYNC_STATE_FIRING; | |
640 } | |
641 } | |
642 } | |
643 | |
644 // Fire the sync event of the ready registrations and run |callback| once | |
645 // they're all done. | |
646 base::Closure barrier_closure = | |
647 base::BarrierClosure(sw_id_and_keys_to_fire.size(), base::Bind(callback)); | |
648 | |
649 for (const auto& sw_id_and_key : sw_id_and_keys_to_fire) { | |
650 int64 service_worker_id = sw_id_and_key.first; | |
651 const BackgroundSyncRegistration* registration = | |
652 LookupRegistration(service_worker_id, sw_id_and_key.second); | |
653 | |
654 service_worker_context_->FindRegistrationForId( | |
655 service_worker_id, sw_to_registrations_map_[service_worker_id].origin, | |
656 base::Bind(&BackgroundSyncManager::FireReadyEventsDidFindRegistration, | |
657 weak_ptr_factory_.GetWeakPtr(), sw_id_and_key.second, | |
658 registration->id, barrier_closure)); | |
659 } | |
660 } | |
661 | |
662 void BackgroundSyncManager::FireReadyEventsDidFindRegistration( | |
663 const RegistrationKey& registration_key, | |
664 BackgroundSyncRegistration::RegistrationId registration_id, | |
665 const base::Closure& callback, | |
666 ServiceWorkerStatusCode service_worker_status, | |
667 const scoped_refptr<ServiceWorkerRegistration>& | |
668 service_worker_registration) { | |
669 if (service_worker_status != SERVICE_WORKER_OK) { | |
670 base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind(callback)); | |
671 return; | |
672 } | |
673 | |
674 ServiceWorkerVersion* active_version = | |
675 service_worker_registration->active_version(); | |
676 if (!active_version) { | |
677 base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind(callback)); | |
iclelland
2015/04/28 02:48:53
Will this happen if the service worker is still ac
jkarlin
2015/04/28 14:41:00
Ah, good point. I'll check for active registration
jkarlin
2015/04/28 16:39:22
Followup CL is here: https://crrev.com/1110993003
| |
678 return; | |
679 } | |
680 | |
681 FireOneShotSync(active_version, | |
682 base::Bind(&BackgroundSyncManager::EventComplete, | |
683 weak_ptr_factory_.GetWeakPtr(), | |
684 service_worker_registration->id(), | |
685 registration_key, registration_id)); | |
686 | |
687 base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind(callback)); | |
688 } | |
689 | |
690 void BackgroundSyncManager::EventComplete( | |
691 int64 service_worker_id, | |
692 const RegistrationKey& key, | |
693 BackgroundSyncRegistration::RegistrationId sync_registration_id, | |
694 ServiceWorkerStatusCode status_code) { | |
695 DCHECK_CURRENTLY_ON(BrowserThread::IO); | |
696 | |
697 if (disabled_) | |
698 return; | |
699 | |
700 op_scheduler_.ScheduleOperation( | |
701 base::Bind(&BackgroundSyncManager::EventCompleteImpl, | |
702 weak_ptr_factory_.GetWeakPtr(), service_worker_id, key, | |
703 sync_registration_id, status_code, MakeEmptyCompletion())); | |
704 } | |
705 | |
706 void BackgroundSyncManager::EventCompleteImpl( | |
iclelland
2015/04/28 02:48:53
It looks like there are situations here where the
jkarlin
2015/04/28 14:41:00
Are you referring to the early returns below? In a
iclelland
2015/04/28 18:50:39
Yes, those ones. So if I'm following the code corr
jkarlin
2015/04/28 21:00:08
We should be good. If it's disabled_ then all memo
| |
707 int64 service_worker_id, | |
708 const RegistrationKey& key, | |
709 BackgroundSyncRegistration::RegistrationId sync_registration_id, | |
710 ServiceWorkerStatusCode status_code, | |
711 const base::Closure& callback) { | |
712 DCHECK_CURRENTLY_ON(BrowserThread::IO); | |
713 | |
714 if (disabled_) { | |
715 base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind(callback)); | |
716 return; | |
717 } | |
718 | |
719 BackgroundSyncRegistration* registration = | |
720 LookupRegistration(service_worker_id, key); | |
721 if (!registration || registration->id != sync_registration_id) { | |
722 base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind(callback)); | |
723 return; | |
724 } | |
725 | |
726 if (registration->periodicity == SYNC_ONE_SHOT) { | |
727 if (status_code != SERVICE_WORKER_OK) { | |
728 // TODO(jkarlin) Fire the sync event on the next page load controlled by | |
729 // this registration. (crbug.com/479665) | |
730 registration->sync_state = SYNC_STATE_FAILED; | |
731 } else { | |
732 registration = nullptr; | |
733 RemoveRegistrationFromMap(service_worker_id, key); | |
734 } | |
735 } else { | |
736 // TODO(jkarlin): Add support for running periodic syncs. (crbug.com/479674) | |
737 NOTREACHED(); | |
738 } | |
739 | |
740 StoreRegistrations( | |
741 service_worker_id, | |
742 base::Bind(&BackgroundSyncManager::EventCompleteDidStore, | |
743 weak_ptr_factory_.GetWeakPtr(), service_worker_id, callback)); | |
744 } | |
745 | |
746 void BackgroundSyncManager::EventCompleteDidStore( | |
747 int64 service_worker_id, | |
748 const base::Closure& callback, | |
749 ServiceWorkerStatusCode status_code) { | |
750 if (status_code == SERVICE_WORKER_ERROR_NOT_FOUND) { | |
751 // The registration is gone. | |
752 sw_to_registrations_map_.erase(service_worker_id); | |
753 base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind(callback)); | |
754 return; | |
755 } | |
756 | |
757 if (status_code != SERVICE_WORKER_OK) { | |
758 LOG(ERROR) << "BackgroundSync failed to store registration due to backend " | |
759 "failure."; | |
760 DisableAndClearManager(base::Bind(callback)); | |
761 return; | |
762 } | |
763 | |
764 base::MessageLoop::current()->PostTask(FROM_HERE, base::Bind(callback)); | |
765 } | |
766 | |
582 base::Closure BackgroundSyncManager::MakeEmptyCompletion() { | 767 base::Closure BackgroundSyncManager::MakeEmptyCompletion() { |
583 return base::Bind(&BackgroundSyncManager::PendingClosure, | 768 return base::Bind(&BackgroundSyncManager::PendingClosure, |
584 weak_ptr_factory_.GetWeakPtr(), | 769 weak_ptr_factory_.GetWeakPtr(), |
585 base::Bind(base::DoNothing)); | 770 base::Bind(base::DoNothing)); |
586 } | 771 } |
587 | 772 |
588 BackgroundSyncManager::StatusAndRegistrationCallback | 773 BackgroundSyncManager::StatusAndRegistrationCallback |
589 BackgroundSyncManager::MakeStatusAndRegistrationCompletion( | 774 BackgroundSyncManager::MakeStatusAndRegistrationCompletion( |
590 const StatusAndRegistrationCallback& callback) { | 775 const StatusAndRegistrationCallback& callback) { |
591 return base::Bind( | 776 return base::Bind( |
592 &BackgroundSyncManager::PendingStatusAndRegistrationCallback, | 777 &BackgroundSyncManager::PendingStatusAndRegistrationCallback, |
593 weak_ptr_factory_.GetWeakPtr(), callback); | 778 weak_ptr_factory_.GetWeakPtr(), callback); |
594 } | 779 } |
595 | 780 |
596 BackgroundSyncManager::StatusCallback | 781 BackgroundSyncManager::StatusCallback |
597 BackgroundSyncManager::MakeStatusCompletion(const StatusCallback& callback) { | 782 BackgroundSyncManager::MakeStatusCompletion(const StatusCallback& callback) { |
598 return base::Bind(&BackgroundSyncManager::PendingStatusCallback, | 783 return base::Bind(&BackgroundSyncManager::PendingStatusCallback, |
599 weak_ptr_factory_.GetWeakPtr(), callback); | 784 weak_ptr_factory_.GetWeakPtr(), callback); |
600 } | 785 } |
601 | 786 |
602 } // namespace content | 787 } // namespace content |
OLD | NEW |