| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/sync/glue/typed_url_change_processor.h" |
| |
| #include "base/location.h" |
| #include "base/metrics/histogram.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/chrome_notification_types.h" |
| #include "chrome/browser/history/history_backend.h" |
| #include "chrome/browser/history/history_notifications.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/sync/glue/typed_url_model_associator.h" |
| #include "chrome/browser/sync/profile_sync_service.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/notification_service.h" |
| #include "sync/internal_api/public/change_record.h" |
| #include "sync/internal_api/public/read_node.h" |
| #include "sync/internal_api/public/write_node.h" |
| #include "sync/internal_api/public/write_transaction.h" |
| #include "sync/protocol/typed_url_specifics.pb.h" |
| #include "sync/syncable/entry.h" // TODO(tim): Investigating bug 121587. |
| |
| using content::BrowserThread; |
| |
| namespace browser_sync { |
| |
| // This is the threshold at which we start throttling sync updates for typed |
| // URLs - any URLs with a typed_count >= this threshold will be throttled. |
| static const int kTypedUrlVisitThrottleThreshold = 10; |
| |
| // This is the multiple we use when throttling sync updates. If the multiple is |
| // N, we sync up every Nth update (i.e. when typed_count % N == 0). |
| static const int kTypedUrlVisitThrottleMultiple = 10; |
| |
| TypedUrlChangeProcessor::TypedUrlChangeProcessor( |
| Profile* profile, |
| TypedUrlModelAssociator* model_associator, |
| history::HistoryBackend* history_backend, |
| sync_driver::DataTypeErrorHandler* error_handler) |
| : sync_driver::ChangeProcessor(error_handler), |
| profile_(profile), |
| model_associator_(model_associator), |
| history_backend_(history_backend), |
| backend_loop_(base::MessageLoop::current()), |
| disconnected_(false) { |
| DCHECK(model_associator); |
| DCHECK(history_backend); |
| DCHECK(error_handler); |
| DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| // When running in unit tests, there is already a NotificationService object. |
| // Since only one can exist at a time per thread, check first. |
| if (!content::NotificationService::current()) |
| notification_service_.reset(content::NotificationService::Create()); |
| } |
| |
| TypedUrlChangeProcessor::~TypedUrlChangeProcessor() { |
| DCHECK(backend_loop_ == base::MessageLoop::current()); |
| DCHECK(history_backend_); |
| history_backend_->RemoveObserver(this); |
| } |
| |
| void TypedUrlChangeProcessor::Observe( |
| int type, |
| const content::NotificationSource& source, |
| const content::NotificationDetails& details) { |
| DCHECK(backend_loop_ == base::MessageLoop::current()); |
| |
| base::AutoLock al(disconnect_lock_); |
| if (disconnected_) |
| return; |
| |
| DVLOG(1) << "Observed typed_url change."; |
| if (type == chrome::NOTIFICATION_HISTORY_URLS_MODIFIED) { |
| HandleURLsModified( |
| content::Details<history::URLsModifiedDetails>(details).ptr()); |
| } else if (type == chrome::NOTIFICATION_HISTORY_URLS_DELETED) { |
| HandleURLsDeleted( |
| content::Details<history::URLsDeletedDetails>(details).ptr()); |
| } |
| UMA_HISTOGRAM_PERCENTAGE("Sync.TypedUrlChangeProcessorErrors", |
| model_associator_->GetErrorPercentage()); |
| } |
| |
| void TypedUrlChangeProcessor::OnURLVisited( |
| history::HistoryBackend* history_backend, |
| ui::PageTransition transition, |
| const history::URLRow& row, |
| const history::RedirectList& redirects, |
| base::Time visit_time) { |
| DCHECK(backend_loop_ == base::MessageLoop::current()); |
| |
| base::AutoLock al(disconnect_lock_); |
| if (disconnected_) |
| return; |
| |
| DVLOG(1) << "Observed typed_url change."; |
| if (ShouldSyncVisit(row.typed_count(), transition)) { |
| syncer::WriteTransaction trans(FROM_HERE, share_handle()); |
| CreateOrUpdateSyncNode(row, &trans); |
| } |
| UMA_HISTOGRAM_PERCENTAGE("Sync.TypedUrlChangeProcessorErrors", |
| model_associator_->GetErrorPercentage()); |
| } |
| |
| void TypedUrlChangeProcessor::HandleURLsModified( |
| history::URLsModifiedDetails* details) { |
| |
| syncer::WriteTransaction trans(FROM_HERE, share_handle()); |
| for (history::URLRows::iterator url = details->changed_urls.begin(); |
| url != details->changed_urls.end(); ++url) { |
| if (url->typed_count() > 0) { |
| // If there were any errors updating the sync node, just ignore them and |
| // continue on to process the next URL. |
| CreateOrUpdateSyncNode(*url, &trans); |
| } |
| } |
| } |
| |
| bool TypedUrlChangeProcessor::CreateOrUpdateSyncNode( |
| history::URLRow url, syncer::WriteTransaction* trans) { |
| DCHECK_GT(url.typed_count(), 0); |
| // Get the visits for this node. |
| history::VisitVector visit_vector; |
| if (!model_associator_->FixupURLAndGetVisits(&url, &visit_vector)) { |
| DLOG(ERROR) << "Could not load visits for url: " << url.url(); |
| return false; |
| } |
| |
| syncer::ReadNode typed_url_root(trans); |
| if (typed_url_root.InitTypeRoot(syncer::TYPED_URLS) != |
| syncer::BaseNode::INIT_OK) { |
| syncer::SyncError error(FROM_HERE, |
| syncer::SyncError::DATATYPE_ERROR, |
| "No top level folder", |
| syncer::TYPED_URLS); |
| error_handler()->OnSingleDataTypeUnrecoverableError(error); |
| return false; |
| } |
| |
| if (model_associator_->ShouldIgnoreUrl(url.url())) |
| return true; |
| |
| DCHECK(!visit_vector.empty()); |
| std::string tag = url.url().spec(); |
| syncer::WriteNode update_node(trans); |
| syncer::BaseNode::InitByLookupResult result = |
| update_node.InitByClientTagLookup(syncer::TYPED_URLS, tag); |
| if (result == syncer::BaseNode::INIT_OK) { |
| model_associator_->WriteToSyncNode(url, visit_vector, &update_node); |
| } else if (result == syncer::BaseNode::INIT_FAILED_DECRYPT_IF_NECESSARY) { |
| syncer::SyncError error(FROM_HERE, |
| syncer::SyncError::DATATYPE_ERROR, |
| "Failed to decrypt.", |
| syncer::TYPED_URLS); |
| error_handler()->OnSingleDataTypeUnrecoverableError(error); |
| return false; |
| } else { |
| syncer::WriteNode create_node(trans); |
| syncer::WriteNode::InitUniqueByCreationResult result = |
| create_node.InitUniqueByCreation(syncer::TYPED_URLS, |
| typed_url_root, tag); |
| if (result != syncer::WriteNode::INIT_SUCCESS) { |
| |
| syncer::SyncError error(FROM_HERE, |
| syncer::SyncError::DATATYPE_ERROR, |
| "Failed to create sync node", |
| syncer::TYPED_URLS); |
| error_handler()->OnSingleDataTypeUnrecoverableError(error); |
| return false; |
| } |
| |
| create_node.SetTitle(tag); |
| model_associator_->WriteToSyncNode(url, visit_vector, &create_node); |
| } |
| return true; |
| } |
| |
| void TypedUrlChangeProcessor::HandleURLsDeleted( |
| history::URLsDeletedDetails* details) { |
| syncer::WriteTransaction trans(FROM_HERE, share_handle()); |
| |
| // Ignore archivals (we don't want to sync them as deletions, to avoid |
| // extra traffic up to the server, and also to make sure that a client with |
| // a bad clock setting won't go on an archival rampage and delete all |
| // history from every client). The server will gracefully age out the sync DB |
| // entries when they've been idle for long enough. |
| if (details->expired) |
| return; |
| |
| if (details->all_history) { |
| if (!model_associator_->DeleteAllNodes(&trans)) { |
| syncer::SyncError error(FROM_HERE, |
| syncer::SyncError::DATATYPE_ERROR, |
| "Failed to delete local nodes.", |
| syncer::TYPED_URLS); |
| error_handler()->OnSingleDataTypeUnrecoverableError(error); |
| return; |
| } |
| } else { |
| for (history::URLRows::const_iterator row = details->rows.begin(); |
| row != details->rows.end(); ++row) { |
| syncer::WriteNode sync_node(&trans); |
| // The deleted URL could have been non-typed, so it might not be found |
| // in the sync DB. |
| if (sync_node.InitByClientTagLookup(syncer::TYPED_URLS, |
| row->url().spec()) == |
| syncer::BaseNode::INIT_OK) { |
| sync_node.Tombstone(); |
| } |
| } |
| } |
| } |
| |
| bool TypedUrlChangeProcessor::ShouldSyncVisit(int typed_count, |
| ui::PageTransition transition) { |
| // Just use an ad-hoc criteria to determine whether to ignore this |
| // notification. For most users, the distribution of visits is roughly a bell |
| // curve with a long tail - there are lots of URLs with < 5 visits so we want |
| // to make sure we sync up every visit to ensure the proper ordering of |
| // suggestions. But there are relatively few URLs with > 10 visits, and those |
| // tend to be more broadly distributed such that there's no need to sync up |
| // every visit to preserve their relative ordering. |
| return (ui::PageTransitionCoreTypeIs(transition, ui::PAGE_TRANSITION_TYPED) && |
| typed_count > 0 && |
| (typed_count < kTypedUrlVisitThrottleThreshold || |
| (typed_count % kTypedUrlVisitThrottleMultiple) == 0)); |
| } |
| |
| void TypedUrlChangeProcessor::ApplyChangesFromSyncModel( |
| const syncer::BaseTransaction* trans, |
| int64 model_version, |
| const syncer::ImmutableChangeRecordList& changes) { |
| DCHECK(backend_loop_ == base::MessageLoop::current()); |
| |
| base::AutoLock al(disconnect_lock_); |
| if (disconnected_) |
| return; |
| |
| syncer::ReadNode typed_url_root(trans); |
| if (typed_url_root.InitTypeRoot(syncer::TYPED_URLS) != |
| syncer::BaseNode::INIT_OK) { |
| syncer::SyncError error(FROM_HERE, |
| syncer::SyncError::DATATYPE_ERROR, |
| "Failed to init type root.", |
| syncer::TYPED_URLS); |
| error_handler()->OnSingleDataTypeUnrecoverableError(error); |
| return; |
| } |
| |
| DCHECK(pending_new_urls_.empty() && pending_new_visits_.empty() && |
| pending_deleted_visits_.empty() && pending_updated_urls_.empty() && |
| pending_deleted_urls_.empty()); |
| |
| for (syncer::ChangeRecordList::const_iterator it = |
| changes.Get().begin(); it != changes.Get().end(); ++it) { |
| if (syncer::ChangeRecord::ACTION_DELETE == |
| it->action) { |
| DCHECK(it->specifics.has_typed_url()) << |
| "Typed URL delete change does not have necessary specifics."; |
| GURL url(it->specifics.typed_url().url()); |
| pending_deleted_urls_.push_back(url); |
| continue; |
| } |
| |
| syncer::ReadNode sync_node(trans); |
| if (sync_node.InitByIdLookup(it->id) != syncer::BaseNode::INIT_OK) { |
| syncer::SyncError error(FROM_HERE, |
| syncer::SyncError::DATATYPE_ERROR, |
| "Failed to init sync node.", |
| syncer::TYPED_URLS); |
| error_handler()->OnSingleDataTypeUnrecoverableError(error); |
| return; |
| } |
| |
| // Check that the changed node is a child of the typed_urls folder. |
| DCHECK(typed_url_root.GetId() == sync_node.GetParentId()); |
| DCHECK(syncer::TYPED_URLS == sync_node.GetModelType()); |
| |
| const sync_pb::TypedUrlSpecifics& typed_url( |
| sync_node.GetTypedUrlSpecifics()); |
| DCHECK(typed_url.visits_size()); |
| |
| if (model_associator_->ShouldIgnoreUrl(GURL(typed_url.url()))) |
| continue; |
| |
| sync_pb::TypedUrlSpecifics filtered_url = |
| model_associator_->FilterExpiredVisits(typed_url); |
| if (!filtered_url.visits_size()) { |
| continue; |
| } |
| |
| model_associator_->UpdateFromSyncDB( |
| filtered_url, &pending_new_visits_, &pending_deleted_visits_, |
| &pending_updated_urls_, &pending_new_urls_); |
| } |
| } |
| |
| void TypedUrlChangeProcessor::CommitChangesFromSyncModel() { |
| DCHECK(backend_loop_ == base::MessageLoop::current()); |
| |
| base::AutoLock al(disconnect_lock_); |
| if (disconnected_) |
| return; |
| |
| // Make sure we stop listening for changes while we're modifying the backend, |
| // so we don't try to re-apply these changes to the sync DB. |
| ScopedStopObserving<TypedUrlChangeProcessor> stop_observing(this); |
| if (!pending_deleted_urls_.empty()) |
| history_backend_->DeleteURLs(pending_deleted_urls_); |
| |
| model_associator_->WriteToHistoryBackend(&pending_new_urls_, |
| &pending_updated_urls_, |
| &pending_new_visits_, |
| &pending_deleted_visits_); |
| |
| pending_new_urls_.clear(); |
| pending_updated_urls_.clear(); |
| pending_new_visits_.clear(); |
| pending_deleted_visits_.clear(); |
| pending_deleted_urls_.clear(); |
| UMA_HISTOGRAM_PERCENTAGE("Sync.TypedUrlChangeProcessorErrors", |
| model_associator_->GetErrorPercentage()); |
| } |
| |
| void TypedUrlChangeProcessor::Disconnect() { |
| base::AutoLock al(disconnect_lock_); |
| disconnected_ = true; |
| } |
| |
| void TypedUrlChangeProcessor::StartImpl() { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| DCHECK(history_backend_); |
| DCHECK(backend_loop_); |
| backend_loop_->PostTask(FROM_HERE, |
| base::Bind(&TypedUrlChangeProcessor::StartObserving, |
| base::Unretained(this))); |
| } |
| |
| void TypedUrlChangeProcessor::StartObserving() { |
| DCHECK(backend_loop_ == base::MessageLoop::current()); |
| DCHECK(history_backend_); |
| DCHECK(profile_); |
| notification_registrar_.Add( |
| this, chrome::NOTIFICATION_HISTORY_URLS_MODIFIED, |
| content::Source<Profile>(profile_)); |
| notification_registrar_.Add( |
| this, chrome::NOTIFICATION_HISTORY_URLS_DELETED, |
| content::Source<Profile>(profile_)); |
| history_backend_->AddObserver(this); |
| } |
| |
| void TypedUrlChangeProcessor::StopObserving() { |
| DCHECK(backend_loop_ == base::MessageLoop::current()); |
| DCHECK(history_backend_); |
| DCHECK(profile_); |
| notification_registrar_.Remove( |
| this, chrome::NOTIFICATION_HISTORY_URLS_MODIFIED, |
| content::Source<Profile>(profile_)); |
| notification_registrar_.Remove( |
| this, chrome::NOTIFICATION_HISTORY_URLS_DELETED, |
| content::Source<Profile>(profile_)); |
| history_backend_->RemoveObserver(this); |
| } |
| |
| } // namespace browser_sync |