| // Copyright (c) 2006-2008 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/profile_sync_service.h" |
| |
| #include <stack> |
| #include <vector> |
| |
| #include "app/l10n_util.h" |
| #include "base/basictypes.h" |
| #include "base/command_line.h" |
| #include "base/file_path.h" |
| #include "base/file_util.h" |
| #include "base/histogram.h" |
| #include "base/stl_util-inl.h" |
| #include "base/string_util.h" |
| #include "base/time.h" |
| #include "chrome/browser/bookmarks/bookmark_utils.h" |
| #include "chrome/browser/history/history_notifications.h" |
| #include "chrome/browser/history/history_types.h" |
| #include "chrome/browser/profile.h" |
| #include "chrome/browser/sync/engine/syncapi.h" |
| #include "chrome/browser/sync/glue/bookmark_change_processor.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/common/notification_service.h" |
| #include "chrome/common/notification_type.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/common/pref_service.h" |
| #include "chrome/common/time_format.h" |
| #include "grit/generated_resources.h" |
| #include "net/base/cookie_monster.h" |
| #include "views/window/window.h" |
| |
| using browser_sync::BookmarkChangeProcessor; |
| using browser_sync::BookmarkModelAssociator; |
| using browser_sync::ChangeProcessor; |
| using browser_sync::SyncBackendHost; |
| |
| typedef GoogleServiceAuthError AuthError; |
| |
| // Default sync server URL. |
| static const char kSyncServerUrl[] = "https://clients4.google.com/chrome-sync"; |
| |
| ProfileSyncService::ProfileSyncService(Profile* profile) |
| : last_auth_error_(AuthError::None()), |
| profile_(profile), |
| sync_service_url_(kSyncServerUrl), |
| backend_initialized_(false), |
| expecting_first_run_auth_needed_event_(false), |
| is_auth_in_progress_(false), |
| ALLOW_THIS_IN_INITIALIZER_LIST(wizard_(this)), |
| unrecoverable_error_detected_(false) { |
| BookmarkChangeProcessor* processor = new BookmarkChangeProcessor(this); |
| change_processors_.insert(processor); |
| // TODO: Move this back to StartUp |
| BookmarkModelAssociator* associator = new BookmarkModelAssociator(this); |
| processor->set_model_associator(associator); |
| } |
| |
| ProfileSyncService::~ProfileSyncService() { |
| Shutdown(false); |
| STLDeleteElements(&change_processors_); |
| } |
| |
| void ProfileSyncService::set_change_processor( |
| browser_sync::ChangeProcessor* change_processor) { |
| change_processors_.clear(); |
| change_processors_.insert(change_processor); |
| } |
| |
| void ProfileSyncService::Initialize() { |
| InitSettings(); |
| RegisterPreferences(); |
| |
| if (!profile()->GetPrefs()->GetBoolean(prefs::kSyncHasSetupCompleted)) { |
| DisableForUser(); // Clean up in case of previous crash / setup abort. |
| #if defined(OS_CHROMEOS) |
| if (profile()->GetRequestContext()) |
| StartUp(); // We always start sync for Chrome OS. |
| #endif |
| } else { |
| StartUp(); |
| } |
| } |
| |
| void ProfileSyncService::InitSettings() { |
| const CommandLine& command_line = *CommandLine::ForCurrentProcess(); |
| |
| // Override the sync server URL from the command-line, if sync server |
| // command-line argument exists. |
| if (command_line.HasSwitch(switches::kSyncServiceURL)) { |
| std::string value(command_line.GetSwitchValueASCII( |
| switches::kSyncServiceURL)); |
| if (!value.empty()) { |
| GURL custom_sync_url(value); |
| if (custom_sync_url.is_valid()) { |
| sync_service_url_ = custom_sync_url; |
| } else { |
| LOG(WARNING) << "The following sync URL specified at the command-line " |
| << "is invalid: " << value; |
| } |
| } |
| } |
| } |
| |
| void ProfileSyncService::RegisterPreferences() { |
| PrefService* pref_service = profile_->GetPrefs(); |
| if (pref_service->FindPreference(prefs::kSyncLastSyncedTime)) |
| return; |
| pref_service->RegisterInt64Pref(prefs::kSyncLastSyncedTime, 0); |
| pref_service->RegisterBooleanPref(prefs::kSyncHasSetupCompleted, false); |
| } |
| |
| void ProfileSyncService::ClearPreferences() { |
| PrefService* pref_service = profile_->GetPrefs(); |
| pref_service->ClearPref(prefs::kSyncLastSyncedTime); |
| pref_service->ClearPref(prefs::kSyncHasSetupCompleted); |
| |
| pref_service->ScheduleSavePersistentPrefs(); |
| } |
| |
| #if defined(OS_CHROMEOS) |
| // The domain and name of the LSID cookie which we use to bootstrap the sync |
| // authentication in Chrome OS. |
| const char kLsidCookieDomain[] = "www.google.com"; |
| const char kLsidCookieName[] = "LSID"; |
| #endif |
| |
| std::string ProfileSyncService::GetLsidForAuthBootstraping() { |
| #if defined(OS_CHROMEOS) |
| // If we're running inside Chrome OS, bootstrap the sync authentication by |
| // using the LSID cookie provided by the Chrome OS login manager. |
| net::CookieMonster::CookieList cookies = profile()->GetRequestContext()-> |
| GetCookieStore()->GetCookieMonster()->GetAllCookies(); |
| for (net::CookieMonster::CookieList::const_iterator it = cookies.begin(); |
| it != cookies.end(); ++it) { |
| if (kLsidCookieDomain == it->first) { |
| const net::CookieMonster::CanonicalCookie& cookie = it->second; |
| if (kLsidCookieName == cookie.Name()) { |
| return cookie.Value(); |
| } |
| } |
| } |
| #endif |
| return std::string(); |
| } |
| |
| void ProfileSyncService::InitializeBackend() { |
| backend_->Initialize(sync_service_url_, profile_->GetRequestContext(), |
| GetLsidForAuthBootstraping()); |
| } |
| |
| void ProfileSyncService::StartUp() { |
| // Don't start up multiple times. |
| if (backend_.get()) |
| return; |
| |
| last_synced_time_ = base::Time::FromInternalValue( |
| profile_->GetPrefs()->GetInt64(prefs::kSyncLastSyncedTime)); |
| |
| backend_.reset(new SyncBackendHost(this, profile_->GetPath(), |
| change_processors_)); |
| |
| registrar_.Add(this, NotificationType::BOOKMARK_MODEL_LOADED, |
| Source<Profile>(profile_)); |
| |
| InitializeBackend(); |
| } |
| |
| void ProfileSyncService::Shutdown(bool sync_disabled) { |
| registrar_.RemoveAll(); |
| |
| if (backend_.get()) |
| backend_->Shutdown(sync_disabled); |
| |
| for (std::set<ChangeProcessor*>::const_iterator it = |
| change_processors_.begin(); it != change_processors_.end(); ++it) { |
| (*it)->Stop(); |
| } |
| backend_.reset(); |
| |
| // Clear all associations and throw away the association manager instance. |
| for (std::set<ChangeProcessor*>::const_iterator it = |
| change_processors_.begin(); it != change_processors_.end(); ++it) { |
| (*it)->GetModelAssociator()->DisassociateModels(); |
| } |
| |
| // Clear various flags. |
| is_auth_in_progress_ = false; |
| backend_initialized_ = false; |
| expecting_first_run_auth_needed_event_ = false; |
| last_attempted_user_email_.clear(); |
| } |
| |
| void ProfileSyncService::EnableForUser() { |
| if (WizardIsVisible()) { |
| // TODO(timsteele): Focus wizard. |
| return; |
| } |
| expecting_first_run_auth_needed_event_ = true; |
| |
| StartUp(); |
| FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged()); |
| } |
| |
| void ProfileSyncService::DisableForUser() { |
| if (WizardIsVisible()) { |
| // TODO(timsteele): Focus wizard. |
| return; |
| } |
| Shutdown(true); |
| ClearPreferences(); |
| |
| FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged()); |
| } |
| |
| void ProfileSyncService::Observe(NotificationType type, |
| const NotificationSource& source, |
| const NotificationDetails& details) { |
| DCHECK_EQ(NotificationType::BOOKMARK_MODEL_LOADED, type.value); |
| registrar_.RemoveAll(); |
| StartProcessingChangesIfReady(); |
| } |
| |
| bool ProfileSyncService::MergeAndSyncAcceptanceNeeded() const { |
| // If we've shown the dialog before, don't show it again. |
| if (profile_->GetPrefs()->GetBoolean(prefs::kSyncHasSetupCompleted)) |
| return false; |
| |
| for (std::set<ChangeProcessor*>::const_iterator it = |
| change_processors_.begin(); it != change_processors_.end(); ++it) { |
| if ((*it)->GetModelAssociator()->ChromeModelHasUserCreatedNodes() && |
| (*it)->GetModelAssociator()->SyncModelHasUserCreatedNodes()) |
| return true; |
| } |
| return false; |
| } |
| |
| bool ProfileSyncService::HasSyncSetupCompleted() const { |
| return profile_->GetPrefs()->GetBoolean(prefs::kSyncHasSetupCompleted); |
| } |
| |
| void ProfileSyncService::SetSyncSetupCompleted() { |
| PrefService* prefs = profile()->GetPrefs(); |
| prefs->SetBoolean(prefs::kSyncHasSetupCompleted, true); |
| prefs->ScheduleSavePersistentPrefs(); |
| } |
| |
| void ProfileSyncService::UpdateLastSyncedTime() { |
| last_synced_time_ = base::Time::Now(); |
| profile_->GetPrefs()->SetInt64(prefs::kSyncLastSyncedTime, |
| last_synced_time_.ToInternalValue()); |
| profile_->GetPrefs()->ScheduleSavePersistentPrefs(); |
| } |
| |
| // An invariant has been violated. Transition to an error state where we try |
| // to do as little work as possible, to avoid further corruption or crashes. |
| void ProfileSyncService::OnUnrecoverableError() { |
| unrecoverable_error_detected_ = true; |
| for (std::set<ChangeProcessor*>::const_iterator it = |
| change_processors_.begin(); it != change_processors_.end(); ++it) { |
| (*it)->Stop(); |
| } |
| |
| // Tell the wizard so it can inform the user only if it is already open. |
| wizard_.Step(SyncSetupWizard::FATAL_ERROR); |
| FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged()); |
| LOG(ERROR) << "Unrecoverable error detected -- ProfileSyncService unusable."; |
| } |
| |
| void ProfileSyncService::OnBackendInitialized() { |
| backend_initialized_ = true; |
| StartProcessingChangesIfReady(); |
| |
| // The very first time the backend initializes is effectively the first time |
| // we can say we successfully "synced". last_synced_time_ will only be null |
| // in this case, because the pref wasn't restored on StartUp. |
| if (last_synced_time_.is_null()) |
| UpdateLastSyncedTime(); |
| FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged()); |
| } |
| |
| void ProfileSyncService::OnSyncCycleCompleted() { |
| UpdateLastSyncedTime(); |
| FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged()); |
| } |
| |
| void ProfileSyncService::OnAuthError() { |
| last_auth_error_ = backend_->GetAuthError(); |
| // Protect against the in-your-face dialogs that pop out of nowhere. |
| // Require the user to click somewhere to run the setup wizard in the case |
| // of a steady-state auth failure. |
| if (WizardIsVisible() || expecting_first_run_auth_needed_event_) { |
| wizard_.Step(AuthError::NONE == last_auth_error_.state() ? |
| SyncSetupWizard::GAIA_SUCCESS : SyncSetupWizard::GAIA_LOGIN); |
| } |
| |
| if (expecting_first_run_auth_needed_event_) { |
| last_auth_error_ = AuthError::None(); |
| expecting_first_run_auth_needed_event_ = false; |
| } |
| |
| if (!WizardIsVisible()) { |
| auth_error_time_ == base::TimeTicks::Now(); |
| } |
| |
| if (!auth_start_time_.is_null()) { |
| UMA_HISTOGRAM_TIMES("Sync.AuthorizationTimeInNetwork", |
| base::TimeTicks::Now() - auth_start_time_); |
| auth_start_time_ = base::TimeTicks(); |
| } |
| |
| is_auth_in_progress_ = false; |
| // Fan the notification out to interested UI-thread components. |
| FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged()); |
| } |
| |
| void ProfileSyncService::ShowLoginDialog() { |
| if (WizardIsVisible()) |
| return; |
| |
| if (!auth_error_time_.is_null()) { |
| UMA_HISTOGRAM_LONG_TIMES("Sync.ReauthorizationTime", |
| base::TimeTicks::Now() - auth_error_time_); |
| auth_error_time_ = base::TimeTicks(); // Reset auth_error_time_ to null. |
| } |
| |
| if (last_auth_error_.state() != AuthError::NONE) { |
| wizard_.Step(SyncSetupWizard::GAIA_LOGIN); |
| } |
| } |
| |
| SyncBackendHost::StatusSummary ProfileSyncService::QuerySyncStatusSummary() { |
| return backend_->GetStatusSummary(); |
| } |
| |
| SyncBackendHost::Status ProfileSyncService::QueryDetailedSyncStatus() { |
| return backend_->GetDetailedStatus(); |
| } |
| |
| std::wstring ProfileSyncService::BuildSyncStatusSummaryText( |
| const sync_api::SyncManager::Status::Summary& summary) { |
| switch (summary) { |
| case sync_api::SyncManager::Status::OFFLINE: |
| return L"OFFLINE"; |
| case sync_api::SyncManager::Status::OFFLINE_UNSYNCED: |
| return L"OFFLINE_UNSYNCED"; |
| case sync_api::SyncManager::Status::SYNCING: |
| return L"SYNCING"; |
| case sync_api::SyncManager::Status::READY: |
| return L"READY"; |
| case sync_api::SyncManager::Status::CONFLICT: |
| return L"CONFLICT"; |
| case sync_api::SyncManager::Status::OFFLINE_UNUSABLE: |
| return L"OFFLINE_UNUSABLE"; |
| case sync_api::SyncManager::Status::INVALID: // fall through |
| default: |
| return L"UNKNOWN"; |
| } |
| } |
| |
| std::wstring ProfileSyncService::GetLastSyncedTimeString() const { |
| if (last_synced_time_.is_null()) |
| return l10n_util::GetString(IDS_SYNC_TIME_NEVER); |
| |
| base::TimeDelta last_synced = base::Time::Now() - last_synced_time_; |
| |
| if (last_synced < base::TimeDelta::FromMinutes(1)) |
| return l10n_util::GetString(IDS_SYNC_TIME_JUST_NOW); |
| |
| return TimeFormat::TimeElapsed(last_synced); |
| } |
| |
| string16 ProfileSyncService::GetAuthenticatedUsername() const { |
| return backend_->GetAuthenticatedUsername(); |
| } |
| |
| void ProfileSyncService::OnUserSubmittedAuth( |
| const std::string& username, const std::string& password, |
| const std::string& captcha) { |
| last_attempted_user_email_ = username; |
| is_auth_in_progress_ = true; |
| FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged()); |
| |
| auth_start_time_ = base::TimeTicks::Now(); |
| backend_->Authenticate(username, password, captcha); |
| } |
| |
| void ProfileSyncService::OnUserAcceptedMergeAndSync() { |
| base::TimeTicks start_time = base::TimeTicks::Now(); |
| bool not_first_run = false; |
| bool merge_success = true; |
| for (std::set<ChangeProcessor*>::const_iterator it = |
| change_processors_.begin(); it != change_processors_.end(); ++it) { |
| not_first_run |= (*it)->GetModelAssociator()-> |
| SyncModelHasUserCreatedNodes(); |
| // TODO(sync): Figure out what do to when a single associator fails. |
| // http://crbug.com/30038 |
| merge_success &= (*it)->GetModelAssociator()-> |
| AssociateModels(); |
| } |
| UMA_HISTOGRAM_MEDIUM_TIMES("Sync.UserPerceivedBookmarkAssociation", |
| base::TimeTicks::Now() - start_time); |
| if (!merge_success) { |
| LOG(ERROR) << "Model association failed."; |
| OnUnrecoverableError(); |
| return; |
| } |
| |
| wizard_.Step(not_first_run ? SyncSetupWizard::DONE : |
| SyncSetupWizard::DONE_FIRST_TIME); |
| |
| for (std::set<ChangeProcessor*>::const_iterator it = |
| change_processors_.begin(); it != change_processors_.end(); ++it) { |
| (*it)->Start(profile(), backend_->GetUserShareHandle()); |
| } |
| FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged()); |
| } |
| |
| void ProfileSyncService::OnUserCancelledDialog() { |
| if (!profile_->GetPrefs()->GetBoolean(prefs::kSyncHasSetupCompleted)) { |
| // A sync dialog was aborted before authentication or merge acceptance. |
| // Rollback. |
| DisableForUser(); |
| } |
| |
| FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged()); |
| } |
| |
| void ProfileSyncService::StartProcessingChangesIfReady() { |
| BookmarkModel* model = profile_->GetBookmarkModel(); |
| |
| for (std::set<ChangeProcessor*>::const_iterator it = |
| change_processors_.begin(); it != change_processors_.end(); ++it) { |
| DCHECK(!(*it)->IsRunning()); |
| } |
| |
| // First check if the subsystems are ready. We can't proceed until they |
| // both have finished loading. |
| if (!model->IsLoaded()) |
| return; |
| if (!backend_initialized_) |
| return; |
| |
| // Show the sync merge warning dialog if needed. |
| if (MergeAndSyncAcceptanceNeeded()) { |
| ProfileSyncService::SyncEvent(MERGE_AND_SYNC_NEEDED); |
| wizard_.Step(SyncSetupWizard::MERGE_AND_SYNC); |
| return; |
| } |
| |
| // We're ready to merge the models. |
| base::TimeTicks start_time = base::TimeTicks::Now(); |
| bool not_first_run = false; |
| bool merge_success = true; |
| for (std::set<ChangeProcessor*>::const_iterator it = |
| change_processors_.begin(); it != change_processors_.end(); ++it) { |
| not_first_run |= (*it)->GetModelAssociator()-> |
| SyncModelHasUserCreatedNodes(); |
| merge_success &= (*it)->GetModelAssociator()-> |
| AssociateModels(); |
| } |
| UMA_HISTOGRAM_TIMES("Sync.BookmarkAssociationTime", |
| base::TimeTicks::Now() - start_time); |
| if (!merge_success) { |
| LOG(ERROR) << "Model association failed."; |
| OnUnrecoverableError(); |
| return; |
| } |
| |
| wizard_.Step(not_first_run ? SyncSetupWizard::DONE : |
| SyncSetupWizard::DONE_FIRST_TIME); |
| |
| for (std::set<ChangeProcessor*>::const_iterator it = |
| change_processors_.begin(); it != change_processors_.end(); ++it) { |
| (*it)->Start(profile(), backend_->GetUserShareHandle()); |
| } |
| FOR_EACH_OBSERVER(Observer, observers_, OnStateChanged()); |
| } |
| |
| void ProfileSyncService::AddObserver(Observer* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void ProfileSyncService::RemoveObserver(Observer* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void ProfileSyncService::SyncEvent(SyncEventCodes code) { |
| static scoped_refptr<Histogram> histogram = |
| LinearHistogram::LinearHistogramFactoryGet("Sync.EventCodes", |
| MIN_SYNC_EVENT_CODE + 1, MAX_SYNC_EVENT_CODE - 1, |
| MAX_SYNC_EVENT_CODE); |
| histogram->SetFlags(kUmaTargetedHistogramFlag); |
| histogram->Add(code); |
| } |
| |
| bool ProfileSyncService::IsSyncEnabled() { |
| // We have switches::kEnableSync just in case we need to change back to |
| // sync-disabled-by-default on a platform. |
| return !CommandLine::ForCurrentProcess()->HasSwitch(switches::kDisableSync); |
| } |
| |
| bool ProfileSyncService::ShouldPushChanges() { |
| // True only after all bootstrapping has succeeded: the bookmark model is |
| // loaded, the sync backend is initialized, the two domains are |
| // consistent with one another, and no unrecoverable error has transpired. |
| for (std::set<ChangeProcessor*>::const_iterator it = |
| change_processors_.begin(); it != change_processors_.end(); ++it) { |
| if (!(*it)->IsRunning()) return false; |
| } |
| return true; |
| } |