Index: chrome/browser/sync/glue/autofill_model_associator2.cc |
diff --git a/chrome/browser/sync/glue/autofill_model_associator2.cc b/chrome/browser/sync/glue/autofill_model_associator2.cc |
new file mode 100755 |
index 0000000000000000000000000000000000000000..80422e82ff6110a5765629e369c7a2d71f5c206b |
--- /dev/null |
+++ b/chrome/browser/sync/glue/autofill_model_associator2.cc |
@@ -0,0 +1,546 @@ |
+// Copyright (c) 2010 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/autofill_model_associator2.h" |
+ |
+#include <vector> |
+ |
+#include "base/task.h" |
+#include "base/time.h" |
+#include "base/string_number_conversions.h" |
+#include "base/utf_string_conversions.h" |
+#include "chrome/browser/autofill/autofill_profile.h" |
+#include "chrome/browser/browser_thread.h" |
+#include "chrome/browser/profile.h" |
+#include "chrome/browser/sync/engine/syncapi.h" |
+#include "chrome/browser/sync/glue/autofill_change_processor.h" |
+#include "chrome/browser/sync/profile_sync_service.h" |
+#include "chrome/browser/sync/protocol/autofill_specifics.pb.h" |
+#include "chrome/browser/webdata/web_database.h" |
+#include "net/base/escape.h" |
+ |
+using base::TimeTicks; |
+ |
+namespace browser_sync { |
+ |
+extern const char kAutofillTag[]; |
+extern const char kAutofillEntryNamespaceTag[]; |
+const char kAutofillProfileNamespaceTag[] = "autofill_profile|"; |
+ |
+static const int kMaxNumAttemptsToFindUniqueLabel = 100; |
+ |
+struct AutofillModelAssociator2::DataBundle { |
+ std::set<AutofillKey> current_entries; |
+ std::vector<AutofillEntry> new_entries; |
+ std::set<string16> current_profiles; |
+ std::vector<AutoFillProfile*> updated_profiles; |
+ std::vector<AutoFillProfile*> new_profiles; // We own these pointers. |
+ ~DataBundle() { STLDeleteElements(&new_profiles); } |
+}; |
+ |
+AutofillModelAssociator2::DoOptimisticRefreshTask::DoOptimisticRefreshTask( |
+ PersonalDataManager* pdm) : pdm_(pdm) {} |
+ |
+AutofillModelAssociator2::DoOptimisticRefreshTask::~DoOptimisticRefreshTask() {} |
+ |
+void AutofillModelAssociator2::DoOptimisticRefreshTask::Run() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ pdm_->Refresh(); |
+} |
+ |
+AutofillModelAssociator2::AutofillModelAssociator2( |
+ ProfileSyncService* sync_service, |
+ WebDatabase* web_database, |
+ PersonalDataManager* personal_data) |
+ : sync_service_(sync_service), |
+ web_database_(web_database), |
+ personal_data_(personal_data), |
+ autofill_node_id_(sync_api::kInvalidId), |
+ abort_association_pending_(false) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
+ DCHECK(sync_service_); |
+ DCHECK(web_database_); |
+ DCHECK(personal_data_); |
+} |
+ |
+AutofillModelAssociator2::~AutofillModelAssociator2() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
+} |
+ |
+bool AutofillModelAssociator2::TraverseAndAssociateChromeAutofillEntries( |
+ sync_api::WriteTransaction* write_trans, |
+ const sync_api::ReadNode& autofill_root, |
+ const std::vector<AutofillEntry>& all_entries_from_db, |
+ std::set<AutofillKey>* current_entries, |
+ std::vector<AutofillEntry>* new_entries) { |
+ |
+ const std::vector<AutofillEntry>& entries = all_entries_from_db; |
+ for (std::vector<AutofillEntry>::const_iterator ix = entries.begin(); |
+ ix != entries.end(); ++ix) { |
+ std::string tag = KeyToTag(ix->key().name(), ix->key().value()); |
+ if (id_map_.find(tag) != id_map_.end()) { |
+ // It seems that name/value pairs are not unique in the web database. |
+ // As a result, we have to filter out duplicates here. This is probably |
+ // a bug in the database. |
+ continue; |
+ } |
+ |
+ sync_api::ReadNode node(write_trans); |
+ if (node.InitByClientTagLookup(syncable::AUTOFILL, tag)) { |
+ const sync_pb::AutofillSpecifics& autofill(node.GetAutofillSpecifics()); |
+ DCHECK_EQ(tag, KeyToTag(UTF8ToUTF16(autofill.name()), |
+ UTF8ToUTF16(autofill.value()))); |
+ |
+ std::vector<base::Time> timestamps; |
+ if (MergeTimestamps(autofill, ix->timestamps(), ×tamps)) { |
+ AutofillEntry new_entry(ix->key(), timestamps); |
+ new_entries->push_back(new_entry); |
+ |
+ sync_api::WriteNode write_node(write_trans); |
+ if (!write_node.InitByClientTagLookup(syncable::AUTOFILL, tag)) { |
+ LOG(ERROR) << "Failed to write autofill sync node."; |
+ return false; |
+ } |
+ AutofillChangeProcessor::WriteAutofillEntry(new_entry, &write_node); |
+ } |
+ |
+ Associate(&tag, node.GetId()); |
+ } else { |
+ sync_api::WriteNode node(write_trans); |
+ if (!node.InitUniqueByCreation(syncable::AUTOFILL, |
+ autofill_root, tag)) { |
+ LOG(ERROR) << "Failed to create autofill sync node."; |
+ return false; |
+ } |
+ node.SetTitle(UTF8ToWide(tag)); |
+ AutofillChangeProcessor::WriteAutofillEntry(*ix, &node); |
+ Associate(&tag, node.GetId()); |
+ } |
+ |
+ current_entries->insert(ix->key()); |
+ } |
+ return true; |
+} |
+ |
+bool AutofillModelAssociator2::TraverseAndAssociateChromeAutoFillProfiles( |
+ sync_api::WriteTransaction* write_trans, |
+ const sync_api::ReadNode& autofill_root, |
+ const std::vector<AutoFillProfile*>& all_profiles_from_db, |
+ std::set<string16>* current_profiles, |
+ std::vector<AutoFillProfile*>* updated_profiles) { |
+ const std::vector<AutoFillProfile*>& profiles = all_profiles_from_db; |
+ for (std::vector<AutoFillProfile*>::const_iterator ix = profiles.begin(); |
+ ix != profiles.end(); ++ix) { |
+ string16 label((*ix)->Label()); |
+ std::string tag(ProfileLabelToTag(label)); |
+ |
+ sync_api::ReadNode node(write_trans); |
+ if (node.InitByClientTagLookup(syncable::AUTOFILL, tag)) { |
+ const sync_pb::AutofillSpecifics& autofill(node.GetAutofillSpecifics()); |
+ DCHECK(autofill.has_profile()); |
+ DCHECK_EQ(ProfileLabelToTag(UTF8ToUTF16(autofill.profile().label())), |
+ tag); |
+ int64 sync_id = node.GetId(); |
+ if (id_map_.find(tag) != id_map_.end()) { |
+ // We just looked up something we already associated. Move aside. |
+ label = MakeUniqueLabel(label, string16(), write_trans); |
+ if (label.empty()) { |
+ return false; |
+ } |
+ tag = ProfileLabelToTag(label); |
+ // TODO(dhollowa): Replace with |AutoFillProfile::set_guid|. |
+ // http://crbug.com/58813 |
+ (*ix)->set_label(label); |
+ if (!MakeNewAutofillProfileSyncNode(write_trans, autofill_root, |
+ tag, **ix, &sync_id)) { |
+ return false; |
+ } |
+ updated_profiles->push_back(*ix); |
+ } else { |
+ // Overwrite local with cloud state. |
+ if (OverwriteProfileWithServerData(*ix, autofill.profile())) |
+ updated_profiles->push_back(*ix); |
+ sync_id = node.GetId(); |
+ } |
+ |
+ Associate(&tag, sync_id); |
+ } else { |
+ int64 id; |
+ if (!MakeNewAutofillProfileSyncNode(write_trans, autofill_root, |
+ tag, **ix, &id)) { |
+ return false; |
+ } |
+ Associate(&tag, id); |
+ } |
+ current_profiles->insert(label); |
+ } |
+ return true; |
+} |
+ |
+// static |
+string16 AutofillModelAssociator2::MakeUniqueLabel( |
+ const string16& non_unique_label, |
+ const string16& existing_unique_label, |
+ sync_api::BaseTransaction* trans) { |
+ if (!non_unique_label.empty() && non_unique_label == existing_unique_label) { |
+ return existing_unique_label; |
+ } |
+ int unique_id = 1; // Priming so we start by appending "2". |
+ while (unique_id++ < kMaxNumAttemptsToFindUniqueLabel) { |
+ string16 suffix(base::IntToString16(unique_id)); |
+ string16 unique_label = non_unique_label + suffix; |
+ if (unique_label == existing_unique_label) |
+ return unique_label; // We'll use the one we already have. |
+ sync_api::ReadNode node(trans); |
+ if (node.InitByClientTagLookup(syncable::AUTOFILL, |
+ ProfileLabelToTag(unique_label))) { |
+ continue; |
+ } |
+ return unique_label; |
+ } |
+ |
+ LOG(ERROR) << "Couldn't create unique tag for autofill node. Srsly?!"; |
+ return string16(); |
+} |
+ |
+bool AutofillModelAssociator2::MakeNewAutofillProfileSyncNode( |
+ sync_api::WriteTransaction* trans, const sync_api::BaseNode& autofill_root, |
+ const std::string& tag, const AutoFillProfile& profile, int64* sync_id) { |
+ sync_api::WriteNode node(trans); |
+ if (!node.InitUniqueByCreation(syncable::AUTOFILL, autofill_root, tag)) { |
+ LOG(ERROR) << "Failed to create autofill sync node."; |
+ return false; |
+ } |
+ node.SetTitle(UTF8ToWide(tag)); |
+ AutofillChangeProcessor::WriteAutofillProfile(profile, &node); |
+ *sync_id = node.GetId(); |
+ return true; |
+} |
+ |
+ |
+bool AutofillModelAssociator2::LoadAutofillData( |
+ std::vector<AutofillEntry>* entries, |
+ std::vector<AutoFillProfile*>* profiles) { |
+ if (IsAbortPending()) |
+ return false; |
+ if (!web_database_->GetAllAutofillEntries(entries)) |
+ return false; |
+ |
+ if (IsAbortPending()) |
+ return false; |
+ if (!web_database_->GetAutoFillProfiles(profiles)) |
+ return false; |
+ |
+ return true; |
+} |
+ |
+bool AutofillModelAssociator2::AssociateModels() { |
+ VLOG(1) << "Associating Autofill Models"; |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
+ { |
+ AutoLock lock(abort_association_pending_lock_); |
+ abort_association_pending_ = false; |
+ } |
+ |
+ // TODO(zork): Attempt to load the model association from storage. |
+ std::vector<AutofillEntry> entries; |
+ ScopedVector<AutoFillProfile> profiles; |
+ |
+ if (!LoadAutofillData(&entries, &profiles.get())) { |
+ LOG(ERROR) << "Could not get the autofill data from WebDatabase."; |
+ return false; |
+ } |
+ |
+ DataBundle bundle; |
+ { |
+ sync_api::WriteTransaction trans( |
+ sync_service_->backend()->GetUserShareHandle()); |
+ |
+ sync_api::ReadNode autofill_root(&trans); |
+ if (!autofill_root.InitByTagLookup(kAutofillTag)) { |
+ LOG(ERROR) << "Server did not create the top-level autofill node. We " |
+ << "might be running against an out-of-date server."; |
+ return false; |
+ } |
+ |
+ if (!TraverseAndAssociateChromeAutofillEntries(&trans, autofill_root, |
+ entries, &bundle.current_entries, &bundle.new_entries) || |
+ !TraverseAndAssociateChromeAutoFillProfiles(&trans, autofill_root, |
+ profiles.get(), &bundle.current_profiles, |
+ &bundle.updated_profiles) || |
+ !TraverseAndAssociateAllSyncNodes(&trans, autofill_root, &bundle)) { |
+ return false; |
+ } |
+ } |
+ |
+ // Since we're on the DB thread, we don't have to worry about updating |
+ // the autofill database after closing the write transaction, since |
+ // this is the only thread that writes to the database. We also don't have |
+ // to worry about the sync model getting out of sync, because changes are |
+ // propogated to the ChangeProcessor on this thread. |
+ if (!SaveChangesToWebData(bundle)) { |
+ LOG(ERROR) << "Failed to update autofill entries."; |
+ return false; |
+ } |
+ |
+ BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, |
+ new DoOptimisticRefreshTask(personal_data_)); |
+ return true; |
+} |
+ |
+bool AutofillModelAssociator2::SaveChangesToWebData(const DataBundle& bundle) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
+ |
+ if (IsAbortPending()) |
+ return false; |
+ |
+ if (bundle.new_entries.size() && |
+ !web_database_->UpdateAutofillEntries(bundle.new_entries)) { |
+ return false; |
+ } |
+ |
+ for (size_t i = 0; i < bundle.new_profiles.size(); i++) { |
+ if (IsAbortPending()) |
+ return false; |
+ if (!web_database_->AddAutoFillProfile(*bundle.new_profiles[i])) |
+ return false; |
+ } |
+ |
+ for (size_t i = 0; i < bundle.updated_profiles.size(); i++) { |
+ if (IsAbortPending()) |
+ return false; |
+ if (!web_database_->UpdateAutoFillProfile(*bundle.updated_profiles[i])) |
+ return false; |
+ } |
+ return true; |
+} |
+ |
+bool AutofillModelAssociator2::TraverseAndAssociateAllSyncNodes( |
+ sync_api::WriteTransaction* write_trans, |
+ const sync_api::ReadNode& autofill_root, |
+ DataBundle* bundle) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
+ |
+ int64 sync_child_id = autofill_root.GetFirstChildId(); |
+ while (sync_child_id != sync_api::kInvalidId) { |
+ sync_api::ReadNode sync_child(write_trans); |
+ if (!sync_child.InitByIdLookup(sync_child_id)) { |
+ LOG(ERROR) << "Failed to fetch child node."; |
+ return false; |
+ } |
+ const sync_pb::AutofillSpecifics& autofill( |
+ sync_child.GetAutofillSpecifics()); |
+ |
+ if (autofill.has_value()) |
+ AddNativeEntryIfNeeded(autofill, bundle, sync_child); |
+ else if (autofill.has_profile()) |
+ AddNativeProfileIfNeeded(autofill.profile(), bundle, sync_child); |
+ else |
+ NOTREACHED() << "AutofillSpecifics has no autofill data!"; |
+ |
+ sync_child_id = sync_child.GetSuccessorId(); |
+ } |
+ return true; |
+} |
+ |
+void AutofillModelAssociator2::AddNativeEntryIfNeeded( |
+ const sync_pb::AutofillSpecifics& autofill, DataBundle* bundle, |
+ const sync_api::ReadNode& node) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
+ AutofillKey key(UTF8ToUTF16(autofill.name()), UTF8ToUTF16(autofill.value())); |
+ |
+ if (bundle->current_entries.find(key) == bundle->current_entries.end()) { |
+ std::vector<base::Time> timestamps; |
+ int timestamps_count = autofill.usage_timestamp_size(); |
+ for (int c = 0; c < timestamps_count; ++c) { |
+ timestamps.push_back(base::Time::FromInternalValue( |
+ autofill.usage_timestamp(c))); |
+ } |
+ std::string tag(KeyToTag(key.name(), key.value())); |
+ Associate(&tag, node.GetId()); |
+ bundle->new_entries.push_back(AutofillEntry(key, timestamps)); |
+ } |
+} |
+ |
+void AutofillModelAssociator2::AddNativeProfileIfNeeded( |
+ const sync_pb::AutofillProfileSpecifics& profile, DataBundle* bundle, |
+ const sync_api::ReadNode& node) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
+ if (bundle->current_profiles.find(UTF8ToUTF16(profile.label())) == |
+ bundle->current_profiles.end()) { |
+ std::string tag(ProfileLabelToTag(UTF8ToUTF16(profile.label()))); |
+ Associate(&tag, node.GetId()); |
+ AutoFillProfile* p = personal_data_-> |
+ CreateNewEmptyAutoFillProfileForDBThread(UTF8ToUTF16(profile.label())); |
+ OverwriteProfileWithServerData(p, profile); |
+ bundle->new_profiles.push_back(p); |
+ } |
+} |
+ |
+bool AutofillModelAssociator2::DisassociateModels() { |
+ id_map_.clear(); |
+ id_map_inverse_.clear(); |
+ return true; |
+} |
+ |
+bool AutofillModelAssociator2::SyncModelHasUserCreatedNodes(bool* has_nodes) { |
+ DCHECK(has_nodes); |
+ *has_nodes = false; |
+ int64 autofill_sync_id; |
+ if (!GetSyncIdForTaggedNode(kAutofillTag, &autofill_sync_id)) { |
+ LOG(ERROR) << "Server did not create the top-level autofill node. We " |
+ << "might be running against an out-of-date server."; |
+ return false; |
+ } |
+ sync_api::ReadTransaction trans( |
+ sync_service_->backend()->GetUserShareHandle()); |
+ |
+ sync_api::ReadNode autofill_node(&trans); |
+ if (!autofill_node.InitByIdLookup(autofill_sync_id)) { |
+ LOG(ERROR) << "Server did not create the top-level autofill node. We " |
+ << "might be running against an out-of-date server."; |
+ return false; |
+ } |
+ |
+ // The sync model has user created nodes if the autofill folder has any |
+ // children. |
+ *has_nodes = sync_api::kInvalidId != autofill_node.GetFirstChildId(); |
+ return true; |
+} |
+ |
+void AutofillModelAssociator2::AbortAssociation() { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
+ AutoLock lock(abort_association_pending_lock_); |
+ abort_association_pending_ = true; |
+} |
+ |
+const std::string* |
+AutofillModelAssociator2::GetChromeNodeFromSyncId(int64 sync_id) { |
+ return NULL; |
+} |
+ |
+bool AutofillModelAssociator2::InitSyncNodeFromChromeId( |
+ std::string node_id, |
+ sync_api::BaseNode* sync_node) { |
+ return false; |
+} |
+ |
+int64 AutofillModelAssociator2::GetSyncIdFromChromeId( |
+ const std::string autofill) { |
+ AutofillToSyncIdMap::const_iterator iter = id_map_.find(autofill); |
+ return iter == id_map_.end() ? sync_api::kInvalidId : iter->second; |
+} |
+ |
+void AutofillModelAssociator2::Associate( |
+ const std::string* autofill, int64 sync_id) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
+ DCHECK_NE(sync_api::kInvalidId, sync_id); |
+ DCHECK(id_map_.find(*autofill) == id_map_.end()); |
+ DCHECK(id_map_inverse_.find(sync_id) == id_map_inverse_.end()); |
+ id_map_[*autofill] = sync_id; |
+ id_map_inverse_[sync_id] = *autofill; |
+} |
+ |
+void AutofillModelAssociator2::Disassociate(int64 sync_id) { |
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
+ SyncIdToAutofillMap::iterator iter = id_map_inverse_.find(sync_id); |
+ if (iter == id_map_inverse_.end()) |
+ return; |
+ CHECK(id_map_.erase(iter->second)); |
+ id_map_inverse_.erase(iter); |
+} |
+ |
+bool AutofillModelAssociator2::GetSyncIdForTaggedNode(const std::string& tag, |
+ int64* sync_id) { |
+ sync_api::ReadTransaction trans( |
+ sync_service_->backend()->GetUserShareHandle()); |
+ sync_api::ReadNode sync_node(&trans); |
+ if (!sync_node.InitByTagLookup(tag.c_str())) |
+ return false; |
+ *sync_id = sync_node.GetId(); |
+ return true; |
+} |
+ |
+bool AutofillModelAssociator2::IsAbortPending() { |
+ AutoLock lock(abort_association_pending_lock_); |
+ return abort_association_pending_; |
+} |
+ |
+// static |
+std::string AutofillModelAssociator2::KeyToTag(const string16& name, |
+ const string16& value) { |
+ std::string ns(kAutofillEntryNamespaceTag); |
+ return ns + EscapePath(UTF16ToUTF8(name)) + "|" + |
+ EscapePath(UTF16ToUTF8(value)); |
+} |
+ |
+// static |
+std::string AutofillModelAssociator2::ProfileLabelToTag(const string16& label) { |
+ std::string ns(kAutofillProfileNamespaceTag); |
+ return ns + EscapePath(UTF16ToUTF8(label)); |
+} |
+ |
+// static |
+bool AutofillModelAssociator2::MergeTimestamps( |
+ const sync_pb::AutofillSpecifics& autofill, |
+ const std::vector<base::Time>& timestamps, |
+ std::vector<base::Time>* new_timestamps) { |
+ DCHECK(new_timestamps); |
+ std::set<base::Time> timestamp_union(timestamps.begin(), |
+ timestamps.end()); |
+ |
+ size_t timestamps_count = autofill.usage_timestamp_size(); |
+ |
+ bool different = timestamps.size() != timestamps_count; |
+ for (size_t c = 0; c < timestamps_count; ++c) { |
+ if (timestamp_union.insert(base::Time::FromInternalValue( |
+ autofill.usage_timestamp(c))).second) { |
+ different = true; |
+ } |
+ } |
+ |
+ if (different) { |
+ new_timestamps->insert(new_timestamps->begin(), |
+ timestamp_union.begin(), |
+ timestamp_union.end()); |
+ } |
+ return different; |
+} |
+ |
+// Helper to compare the local value and cloud value of a field, merge into |
+// the local value if they differ, and return whether the merge happened. |
+bool MergeField2(FormGroup* f, AutoFillFieldType t, |
+ const std::string& specifics_field) { |
+ if (UTF16ToUTF8(f->GetFieldText(AutoFillType(t))) == specifics_field) |
+ return false; |
+ f->SetInfo(AutoFillType(t), UTF8ToUTF16(specifics_field)); |
+ return true; |
+} |
+ |
+// static |
+bool AutofillModelAssociator2::OverwriteProfileWithServerData( |
+ AutoFillProfile* merge_into, |
+ const sync_pb::AutofillProfileSpecifics& specifics) { |
+ bool diff = false; |
+ AutoFillProfile* p = merge_into; |
+ const sync_pb::AutofillProfileSpecifics& s(specifics); |
+ diff = MergeField2(p, NAME_FIRST, s.name_first()) || diff; |
+ diff = MergeField2(p, NAME_LAST, s.name_last()) || diff; |
+ diff = MergeField2(p, NAME_MIDDLE, s.name_middle()) || diff; |
+ diff = MergeField2(p, ADDRESS_HOME_LINE1, s.address_home_line1()) || diff; |
+ diff = MergeField2(p, ADDRESS_HOME_LINE2, s.address_home_line2()) || diff; |
+ diff = MergeField2(p, ADDRESS_HOME_CITY, s.address_home_city()) || diff; |
+ diff = MergeField2(p, ADDRESS_HOME_STATE, s.address_home_state()) || diff; |
+ diff = MergeField2(p, ADDRESS_HOME_COUNTRY, s.address_home_country()) || diff; |
+ diff = MergeField2(p, ADDRESS_HOME_ZIP, s.address_home_zip()) || diff; |
+ diff = MergeField2(p, EMAIL_ADDRESS, s.email_address()) || diff; |
+ diff = MergeField2(p, COMPANY_NAME, s.company_name()) || diff; |
+ diff = MergeField2(p, PHONE_FAX_WHOLE_NUMBER, s.phone_fax_whole_number()) |
+ || diff; |
+ diff = MergeField2(p, PHONE_HOME_WHOLE_NUMBER, s.phone_home_whole_number()) |
+ || diff; |
+ return diff; |
+} |
+ |
+} // namespace browser_sync |