- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / sync / glue / typed_url_change_processor.cc
1 // Copyright (c) 2012 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 "chrome/browser/sync/glue/typed_url_change_processor.h"
6
7 #include "base/location.h"
8 #include "base/metrics/histogram.h"
9 #include "base/strings/string_util.h"
10 #include "base/strings/utf_string_conversions.h"
11 #include "chrome/browser/chrome_notification_types.h"
12 #include "chrome/browser/history/history_backend.h"
13 #include "chrome/browser/history/history_notifications.h"
14 #include "chrome/browser/profiles/profile.h"
15 #include "chrome/browser/sync/glue/typed_url_model_associator.h"
16 #include "chrome/browser/sync/profile_sync_service.h"
17 #include "content/public/browser/notification_service.h"
18 #include "sync/internal_api/public/change_record.h"
19 #include "sync/internal_api/public/read_node.h"
20 #include "sync/internal_api/public/write_node.h"
21 #include "sync/internal_api/public/write_transaction.h"
22 #include "sync/protocol/typed_url_specifics.pb.h"
23 #include "sync/syncable/entry.h"  // TODO(tim): Investigating bug 121587.
24
25 using content::BrowserThread;
26
27 namespace browser_sync {
28
29 // This is the threshold at which we start throttling sync updates for typed
30 // URLs - any URLs with a typed_count >= this threshold will be throttled.
31 static const int kTypedUrlVisitThrottleThreshold = 10;
32
33 // This is the multiple we use when throttling sync updates. If the multiple is
34 // N, we sync up every Nth update (i.e. when typed_count % N == 0).
35 static const int kTypedUrlVisitThrottleMultiple = 10;
36
37 TypedUrlChangeProcessor::TypedUrlChangeProcessor(
38     Profile* profile,
39     TypedUrlModelAssociator* model_associator,
40     history::HistoryBackend* history_backend,
41     DataTypeErrorHandler* error_handler)
42     : ChangeProcessor(error_handler),
43       profile_(profile),
44       model_associator_(model_associator),
45       history_backend_(history_backend),
46       backend_loop_(base::MessageLoop::current()),
47       disconnected_(false) {
48   DCHECK(model_associator);
49   DCHECK(history_backend);
50   DCHECK(error_handler);
51   DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::UI));
52   // When running in unit tests, there is already a NotificationService object.
53   // Since only one can exist at a time per thread, check first.
54   if (!content::NotificationService::current())
55     notification_service_.reset(content::NotificationService::Create());
56 }
57
58 TypedUrlChangeProcessor::~TypedUrlChangeProcessor() {
59   DCHECK(backend_loop_ == base::MessageLoop::current());
60 }
61
62 void TypedUrlChangeProcessor::Observe(
63     int type,
64     const content::NotificationSource& source,
65     const content::NotificationDetails& details) {
66   DCHECK(backend_loop_ == base::MessageLoop::current());
67
68   base::AutoLock al(disconnect_lock_);
69   if (disconnected_)
70     return;
71
72   DVLOG(1) << "Observed typed_url change.";
73   if (type == chrome::NOTIFICATION_HISTORY_URLS_MODIFIED) {
74     HandleURLsModified(
75         content::Details<history::URLsModifiedDetails>(details).ptr());
76   } else if (type == chrome::NOTIFICATION_HISTORY_URLS_DELETED) {
77     HandleURLsDeleted(
78         content::Details<history::URLsDeletedDetails>(details).ptr());
79   } else {
80     DCHECK_EQ(chrome::NOTIFICATION_HISTORY_URL_VISITED, type);
81     HandleURLsVisited(
82         content::Details<history::URLVisitedDetails>(details).ptr());
83   }
84   UMA_HISTOGRAM_PERCENTAGE("Sync.TypedUrlChangeProcessorErrors",
85                            model_associator_->GetErrorPercentage());
86 }
87
88 void TypedUrlChangeProcessor::HandleURLsModified(
89     history::URLsModifiedDetails* details) {
90
91   syncer::WriteTransaction trans(FROM_HERE, share_handle());
92   for (history::URLRows::iterator url = details->changed_urls.begin();
93        url != details->changed_urls.end(); ++url) {
94     if (url->typed_count() > 0) {
95       // If there were any errors updating the sync node, just ignore them and
96       // continue on to process the next URL.
97       CreateOrUpdateSyncNode(*url, &trans);
98     }
99   }
100 }
101
102 bool TypedUrlChangeProcessor::CreateOrUpdateSyncNode(
103     history::URLRow url, syncer::WriteTransaction* trans) {
104   DCHECK_GT(url.typed_count(), 0);
105   // Get the visits for this node.
106   history::VisitVector visit_vector;
107   if (!model_associator_->FixupURLAndGetVisits(&url, &visit_vector)) {
108     DLOG(ERROR) << "Could not load visits for url: " << url.url();
109     return false;
110   }
111
112   syncer::ReadNode typed_url_root(trans);
113   if (typed_url_root.InitByTagLookup(kTypedUrlTag) !=
114           syncer::BaseNode::INIT_OK) {
115     error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
116         "Server did not create the top-level typed_url node. We "
117          "might be running against an out-of-date server.");
118     return false;
119   }
120
121   if (model_associator_->ShouldIgnoreUrl(url.url()))
122     return true;
123
124   DCHECK(!visit_vector.empty());
125   std::string tag = url.url().spec();
126   syncer::WriteNode update_node(trans);
127   syncer::BaseNode::InitByLookupResult result =
128       update_node.InitByClientTagLookup(syncer::TYPED_URLS, tag);
129   if (result == syncer::BaseNode::INIT_OK) {
130     model_associator_->WriteToSyncNode(url, visit_vector, &update_node);
131   } else if (result == syncer::BaseNode::INIT_FAILED_DECRYPT_IF_NECESSARY) {
132     // TODO(tim): Investigating bug 121587.
133     syncer::Cryptographer* crypto = trans->GetCryptographer();
134     syncer::ModelTypeSet encrypted_types(trans->GetEncryptedTypes());
135     const sync_pb::EntitySpecifics& specifics =
136         update_node.GetEntry()->GetSpecifics();
137     CHECK(specifics.has_encrypted());
138     const bool can_decrypt = crypto->CanDecrypt(specifics.encrypted());
139     const bool agreement = encrypted_types.Has(syncer::TYPED_URLS);
140     if (!agreement && !can_decrypt) {
141       error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
142           "Could not InitByIdLookup in CreateOrUpdateSyncNode, "
143           " Cryptographer thinks typed urls not encrypted, and CanDecrypt"
144           " failed.");
145       LOG(ERROR) << "Case 1.";
146     } else if (agreement && can_decrypt) {
147       error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
148           "Could not InitByIdLookup on CreateOrUpdateSyncNode, "
149           " Cryptographer thinks typed urls are encrypted, and CanDecrypt"
150           " succeeded (?!), but DecryptIfNecessary failed.");
151       LOG(ERROR) << "Case 2.";
152     } else if (agreement) {
153       error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
154           "Could not InitByIdLookup on CreateOrUpdateSyncNode, "
155           " Cryptographer thinks typed urls are encrypted, but CanDecrypt"
156           " failed.");
157       LOG(ERROR) << "Case 3.";
158     } else {
159       error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
160           "Could not InitByIdLookup on CreateOrUpdateSyncNode, "
161           " Cryptographer thinks typed urls not encrypted, but CanDecrypt"
162           " succeeded (super weird, btw)");
163       LOG(ERROR) << "Case 4.";
164     }
165   } else {
166     syncer::WriteNode create_node(trans);
167     syncer::WriteNode::InitUniqueByCreationResult result =
168         create_node.InitUniqueByCreation(syncer::TYPED_URLS,
169                                          typed_url_root, tag);
170     if (result != syncer::WriteNode::INIT_SUCCESS) {
171       error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
172           "Failed to create typed_url sync node.");
173       return false;
174     }
175
176     create_node.SetTitle(UTF8ToWide(tag));
177     model_associator_->WriteToSyncNode(url, visit_vector, &create_node);
178   }
179   return true;
180 }
181
182 void TypedUrlChangeProcessor::HandleURLsDeleted(
183     history::URLsDeletedDetails* details) {
184   syncer::WriteTransaction trans(FROM_HERE, share_handle());
185
186   // Ignore archivals (we don't want to sync them as deletions, to avoid
187   // extra traffic up to the server, and also to make sure that a client with
188   // a bad clock setting won't go on an archival rampage and delete all
189   // history from every client). The server will gracefully age out the sync DB
190   // entries when they've been idle for long enough.
191   if (details->archived)
192     return;
193
194   if (details->all_history) {
195     if (!model_associator_->DeleteAllNodes(&trans)) {
196       error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
197           std::string());
198       return;
199     }
200   } else {
201     for (history::URLRows::const_iterator row = details->rows.begin();
202          row != details->rows.end(); ++row) {
203       syncer::WriteNode sync_node(&trans);
204       // The deleted URL could have been non-typed, so it might not be found
205       // in the sync DB.
206       if (sync_node.InitByClientTagLookup(syncer::TYPED_URLS,
207                                           row->url().spec()) ==
208               syncer::BaseNode::INIT_OK) {
209         sync_node.Tombstone();
210       }
211     }
212   }
213 }
214
215 void TypedUrlChangeProcessor::HandleURLsVisited(
216     history::URLVisitedDetails* details) {
217   if (!ShouldSyncVisit(details))
218     return;
219
220   syncer::WriteTransaction trans(FROM_HERE, share_handle());
221   CreateOrUpdateSyncNode(details->row, &trans);
222 }
223
224 bool TypedUrlChangeProcessor::ShouldSyncVisit(
225     history::URLVisitedDetails* details) {
226   int typed_count = details->row.typed_count();
227   content::PageTransition transition = static_cast<content::PageTransition>(
228       details->transition & content::PAGE_TRANSITION_CORE_MASK);
229
230   // Just use an ad-hoc criteria to determine whether to ignore this
231   // notification. For most users, the distribution of visits is roughly a bell
232   // curve with a long tail - there are lots of URLs with < 5 visits so we want
233   // to make sure we sync up every visit to ensure the proper ordering of
234   // suggestions. But there are relatively few URLs with > 10 visits, and those
235   // tend to be more broadly distributed such that there's no need to sync up
236   // every visit to preserve their relative ordering.
237   return (transition == content::PAGE_TRANSITION_TYPED &&
238           typed_count > 0 &&
239           (typed_count < kTypedUrlVisitThrottleThreshold ||
240            (typed_count % kTypedUrlVisitThrottleMultiple) == 0));
241 }
242
243 void TypedUrlChangeProcessor::ApplyChangesFromSyncModel(
244     const syncer::BaseTransaction* trans,
245     int64 model_version,
246     const syncer::ImmutableChangeRecordList& changes) {
247   DCHECK(backend_loop_ == base::MessageLoop::current());
248
249   base::AutoLock al(disconnect_lock_);
250   if (disconnected_)
251     return;
252
253   syncer::ReadNode typed_url_root(trans);
254   if (typed_url_root.InitByTagLookup(kTypedUrlTag) !=
255           syncer::BaseNode::INIT_OK) {
256     error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
257         "TypedUrl root node lookup failed.");
258     return;
259   }
260
261   DCHECK(pending_new_urls_.empty() && pending_new_visits_.empty() &&
262          pending_deleted_visits_.empty() && pending_updated_urls_.empty() &&
263          pending_deleted_urls_.empty());
264
265   for (syncer::ChangeRecordList::const_iterator it =
266            changes.Get().begin(); it != changes.Get().end(); ++it) {
267     if (syncer::ChangeRecord::ACTION_DELETE ==
268         it->action) {
269       DCHECK(it->specifics.has_typed_url()) <<
270           "Typed URL delete change does not have necessary specifics.";
271       GURL url(it->specifics.typed_url().url());
272       pending_deleted_urls_.push_back(url);
273       continue;
274     }
275
276     syncer::ReadNode sync_node(trans);
277     if (sync_node.InitByIdLookup(it->id) != syncer::BaseNode::INIT_OK) {
278       error_handler()->OnSingleDatatypeUnrecoverableError(FROM_HERE,
279           "TypedUrl node lookup failed.");
280       return;
281     }
282
283     // Check that the changed node is a child of the typed_urls folder.
284     DCHECK(typed_url_root.GetId() == sync_node.GetParentId());
285     DCHECK(syncer::TYPED_URLS == sync_node.GetModelType());
286
287     const sync_pb::TypedUrlSpecifics& typed_url(
288         sync_node.GetTypedUrlSpecifics());
289     DCHECK(typed_url.visits_size());
290
291     if (model_associator_->ShouldIgnoreUrl(GURL(typed_url.url())))
292       continue;
293
294     sync_pb::TypedUrlSpecifics filtered_url =
295         model_associator_->FilterExpiredVisits(typed_url);
296     if (!filtered_url.visits_size()) {
297       continue;
298     }
299
300     model_associator_->UpdateFromSyncDB(
301         filtered_url, &pending_new_visits_, &pending_deleted_visits_,
302         &pending_updated_urls_, &pending_new_urls_);
303   }
304 }
305
306 void TypedUrlChangeProcessor::CommitChangesFromSyncModel() {
307   DCHECK(backend_loop_ == base::MessageLoop::current());
308
309   base::AutoLock al(disconnect_lock_);
310   if (disconnected_)
311     return;
312
313   // Make sure we stop listening for changes while we're modifying the backend,
314   // so we don't try to re-apply these changes to the sync DB.
315   ScopedStopObserving<TypedUrlChangeProcessor> stop_observing(this);
316   if (!pending_deleted_urls_.empty())
317     history_backend_->DeleteURLs(pending_deleted_urls_);
318
319   model_associator_->WriteToHistoryBackend(&pending_new_urls_,
320                                            &pending_updated_urls_,
321                                            &pending_new_visits_,
322                                            &pending_deleted_visits_);
323
324   pending_new_urls_.clear();
325   pending_updated_urls_.clear();
326   pending_new_visits_.clear();
327   pending_deleted_visits_.clear();
328   pending_deleted_urls_.clear();
329   UMA_HISTOGRAM_PERCENTAGE("Sync.TypedUrlChangeProcessorErrors",
330                            model_associator_->GetErrorPercentage());
331 }
332
333 void TypedUrlChangeProcessor::Disconnect() {
334   base::AutoLock al(disconnect_lock_);
335   disconnected_ = true;
336 }
337
338 void TypedUrlChangeProcessor::StartImpl(Profile* profile) {
339   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
340   DCHECK_EQ(profile, profile_);
341   DCHECK(history_backend_);
342   DCHECK(backend_loop_);
343   backend_loop_->PostTask(FROM_HERE,
344                           base::Bind(&TypedUrlChangeProcessor::StartObserving,
345                                      base::Unretained(this)));
346 }
347
348 void TypedUrlChangeProcessor::StartObserving() {
349   DCHECK(backend_loop_ == base::MessageLoop::current());
350   DCHECK(profile_);
351   notification_registrar_.Add(
352       this, chrome::NOTIFICATION_HISTORY_URLS_MODIFIED,
353       content::Source<Profile>(profile_));
354   notification_registrar_.Add(
355       this, chrome::NOTIFICATION_HISTORY_URLS_DELETED,
356       content::Source<Profile>(profile_));
357   notification_registrar_.Add(
358       this, chrome::NOTIFICATION_HISTORY_URL_VISITED,
359       content::Source<Profile>(profile_));
360 }
361
362 void TypedUrlChangeProcessor::StopObserving() {
363   DCHECK(backend_loop_ == base::MessageLoop::current());
364   DCHECK(profile_);
365   notification_registrar_.Remove(
366       this, chrome::NOTIFICATION_HISTORY_URLS_MODIFIED,
367       content::Source<Profile>(profile_));
368   notification_registrar_.Remove(
369       this, chrome::NOTIFICATION_HISTORY_URLS_DELETED,
370       content::Source<Profile>(profile_));
371   notification_registrar_.Remove(
372       this, chrome::NOTIFICATION_HISTORY_URL_VISITED,
373       content::Source<Profile>(profile_));
374 }
375
376 }  // namespace browser_sync