blob: 508503b5a0cb2b94b211499e46acc767f7b167e0 [file] [log] [blame]
// 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/chromeos/contacts/google_contact_store.h"
#include <algorithm>
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/contacts/contact.pb.h"
#include "chrome/browser/chromeos/contacts/contact_database.h"
#include "chrome/browser/chromeos/contacts/contact_store_observer.h"
#include "chrome/browser/chromeos/contacts/gdata_contacts_service.h"
#include "chrome/browser/chromeos/profiles/profile_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/profile_oauth2_token_service.h"
#include "chrome/browser/signin/profile_oauth2_token_service_factory.h"
#include "content/public/browser/browser_thread.h"
#include "google_apis/drive/auth_service.h"
#include "google_apis/drive/time_util.h"
using content::BrowserThread;
namespace contacts {
namespace {
// Name of the directory within the profile directory where the contact database
// is stored.
const base::FilePath::CharType kDatabaseDirectoryName[] =
FILE_PATH_LITERAL("Google Contacts");
// We wait this long after the last update has completed successfully before
// updating again.
// TODO(derat): Decide what this should be.
const int kUpdateIntervalSec = 600;
// https://developers.google.com/google-apps/contacts/v3/index says that deleted
// contact (groups?) will only be returned for 30 days after deletion when the
// "showdeleted" parameter is set. If it's been longer than that since the last
// successful update, we do a full refresh to make sure that we haven't missed
// any deletions. Use 29 instead to make sure that we don't run afoul of
// daylight saving time shenanigans or minor skew in the system clock.
const int kForceFullUpdateDays = 29;
// When an update fails, we initially wait this many seconds before retrying.
// The delay increases exponentially in response to repeated failures.
const int kUpdateFailureInitialRetrySec = 5;
// Amount by which |update_delay_on_next_failure_| is multiplied on failure.
const int kUpdateFailureBackoffFactor = 2;
// OAuth2 scope for the Contacts API.
const char kContactsScope[] = "https://www.google.com/m8/feeds/";
} // namespace
GoogleContactStore::TestAPI::TestAPI(GoogleContactStore* store)
: store_(store) {
DCHECK(store);
}
GoogleContactStore::TestAPI::~TestAPI() {
store_ = NULL;
}
void GoogleContactStore::TestAPI::SetDatabase(ContactDatabaseInterface* db) {
store_->DestroyDatabase();
store_->db_ = db;
}
void GoogleContactStore::TestAPI::SetGDataService(
GDataContactsServiceInterface* service) {
store_->gdata_service_.reset(service);
}
void GoogleContactStore::TestAPI::DoUpdate() {
store_->UpdateContacts();
}
void GoogleContactStore::TestAPI::NotifyAboutNetworkStateChange(bool online) {
net::NetworkChangeNotifier::ConnectionType type =
online ?
net::NetworkChangeNotifier::CONNECTION_UNKNOWN :
net::NetworkChangeNotifier::CONNECTION_NONE;
store_->OnConnectionTypeChanged(type);
}
scoped_ptr<ContactPointers> GoogleContactStore::TestAPI::GetLoadedContacts() {
scoped_ptr<ContactPointers> contacts(new ContactPointers);
for (ContactMap::const_iterator it = store_->contacts_.begin();
it != store_->contacts_.end(); ++it) {
contacts->push_back(it->second);
}
return contacts.Pass();
}
GoogleContactStore::GoogleContactStore(
net::URLRequestContextGetter* url_request_context_getter,
Profile* profile)
: url_request_context_getter_(url_request_context_getter),
profile_(profile),
db_(new ContactDatabase),
update_delay_on_next_failure_(
base::TimeDelta::FromSeconds(kUpdateFailureInitialRetrySec)),
is_online_(true),
should_update_when_online_(false),
weak_ptr_factory_(this) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
net::NetworkChangeNotifier::AddConnectionTypeObserver(this);
is_online_ = !net::NetworkChangeNotifier::IsOffline();
}
GoogleContactStore::~GoogleContactStore() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
weak_ptr_factory_.InvalidateWeakPtrs();
net::NetworkChangeNotifier::RemoveConnectionTypeObserver(this);
DestroyDatabase();
}
void GoogleContactStore::Init() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
// Create a GData service if one hasn't already been assigned for testing.
if (!gdata_service_.get()) {
std::vector<std::string> scopes;
scopes.push_back(kContactsScope);
ProfileOAuth2TokenService* oauth2_service =
ProfileOAuth2TokenServiceFactory::GetForProfile(profile_);
gdata_service_.reset(new GDataContactsService(
url_request_context_getter_,
new google_apis::AuthService(
oauth2_service,
oauth2_service->GetPrimaryAccountId(),
url_request_context_getter_, scopes)));
}
base::FilePath db_path = profile_->GetPath().Append(kDatabaseDirectoryName);
VLOG(1) << "Initializing contact database \"" << db_path.value() << "\" for "
<< profile_->GetProfileName();
db_->Init(db_path,
base::Bind(&GoogleContactStore::OnDatabaseInitialized,
weak_ptr_factory_.GetWeakPtr()));
}
void GoogleContactStore::AppendContacts(ContactPointers* contacts_out) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(contacts_out);
for (ContactMap::const_iterator it = contacts_.begin();
it != contacts_.end(); ++it) {
if (!it->second->deleted())
contacts_out->push_back(it->second);
}
}
const Contact* GoogleContactStore::GetContactById(
const std::string& contact_id) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
return contacts_.Find(contact_id);
}
void GoogleContactStore::AddObserver(ContactStoreObserver* observer) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(observer);
observers_.AddObserver(observer);
}
void GoogleContactStore::RemoveObserver(ContactStoreObserver* observer) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(observer);
observers_.RemoveObserver(observer);
}
void GoogleContactStore::OnConnectionTypeChanged(
net::NetworkChangeNotifier::ConnectionType type) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
bool was_online = is_online_;
is_online_ = (type != net::NetworkChangeNotifier::CONNECTION_NONE);
if (!was_online && is_online_ && should_update_when_online_) {
should_update_when_online_ = false;
UpdateContacts();
}
}
base::Time GoogleContactStore::GetCurrentTime() const {
return !current_time_for_testing_.is_null() ?
current_time_for_testing_ :
base::Time::Now();
}
void GoogleContactStore::DestroyDatabase() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (db_) {
db_->DestroyOnUIThread();
db_ = NULL;
}
}
void GoogleContactStore::UpdateContacts() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
// If we're offline, defer the update.
if (!is_online_) {
VLOG(1) << "Deferring contact update due to offline state";
should_update_when_online_ = true;
return;
}
base::Time min_update_time;
base::TimeDelta time_since_last_update =
last_successful_update_start_time_.is_null() ?
base::TimeDelta() :
GetCurrentTime() - last_successful_update_start_time_;
if (!last_contact_update_time_.is_null() &&
time_since_last_update <
base::TimeDelta::FromDays(kForceFullUpdateDays)) {
// TODO(derat): I'm adding one millisecond to the last update time here as I
// don't want to re-download the same most-recently-updated contact each
// time, but what happens if within the same millisecond, contact A is
// updated, we do a sync, and then contact B is updated? I'm probably being
// overly paranoid about this.
min_update_time =
last_contact_update_time_ + base::TimeDelta::FromMilliseconds(1);
}
if (min_update_time.is_null()) {
VLOG(1) << "Downloading all contacts for " << profile_->GetProfileName();
} else {
VLOG(1) << "Downloading contacts updated since "
<< google_apis::util::FormatTimeAsString(min_update_time) << " for "
<< profile_->GetProfileName();
}
gdata_service_->DownloadContacts(
base::Bind(&GoogleContactStore::OnDownloadSuccess,
weak_ptr_factory_.GetWeakPtr(),
min_update_time.is_null(),
GetCurrentTime()),
base::Bind(&GoogleContactStore::OnDownloadFailure,
weak_ptr_factory_.GetWeakPtr()),
min_update_time);
}
void GoogleContactStore::ScheduleUpdate(bool last_update_was_successful) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
base::TimeDelta delay;
if (last_update_was_successful) {
delay = base::TimeDelta::FromSeconds(kUpdateIntervalSec);
update_delay_on_next_failure_ =
base::TimeDelta::FromSeconds(kUpdateFailureInitialRetrySec);
} else {
delay = update_delay_on_next_failure_;
update_delay_on_next_failure_ = std::min(
update_delay_on_next_failure_ * kUpdateFailureBackoffFactor,
base::TimeDelta::FromSeconds(kUpdateIntervalSec));
}
VLOG(1) << "Scheduling update of " << profile_->GetProfileName()
<< " in " << delay.InSeconds() << " second(s)";
update_timer_.Start(
FROM_HERE, delay, this, &GoogleContactStore::UpdateContacts);
}
void GoogleContactStore::MergeContacts(
bool is_full_update,
scoped_ptr<ScopedVector<Contact> > updated_contacts) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (is_full_update) {
contacts_.Clear();
last_contact_update_time_ = base::Time();
}
// Find the maximum update time from |updated_contacts| since contacts whose
// |deleted| flags are set won't be saved to |contacts_|.
for (ScopedVector<Contact>::iterator it = updated_contacts->begin();
it != updated_contacts->end(); ++it) {
last_contact_update_time_ =
std::max(last_contact_update_time_,
base::Time::FromInternalValue((*it)->update_time()));
}
VLOG(1) << "Last contact update time is "
<< google_apis::util::FormatTimeAsString(last_contact_update_time_);
contacts_.Merge(updated_contacts.Pass(), ContactMap::DROP_DELETED_CONTACTS);
}
void GoogleContactStore::OnDownloadSuccess(
bool is_full_update,
const base::Time& update_start_time,
scoped_ptr<ScopedVector<Contact> > updated_contacts) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
VLOG(1) << "Got " << updated_contacts->size() << " contact(s) for "
<< profile_->GetProfileName();
// Copy the pointers so we can update just these contacts in the database.
scoped_ptr<ContactPointers> contacts_to_save_to_db(new ContactPointers);
scoped_ptr<ContactDatabaseInterface::ContactIds>
contact_ids_to_delete_from_db(new ContactDatabaseInterface::ContactIds);
if (db_) {
for (size_t i = 0; i < updated_contacts->size(); ++i) {
Contact* contact = (*updated_contacts)[i];
if (contact->deleted())
contact_ids_to_delete_from_db->push_back(contact->contact_id());
else
contacts_to_save_to_db->push_back(contact);
}
}
bool got_updates = !updated_contacts->empty();
MergeContacts(is_full_update, updated_contacts.Pass());
last_successful_update_start_time_ = update_start_time;
if (is_full_update || got_updates) {
FOR_EACH_OBSERVER(ContactStoreObserver,
observers_,
OnContactsUpdated(this));
}
if (db_) {
// Even if this was an incremental update and we didn't get any updated
// contacts, we still want to write updated metadata containing
// |update_start_time|.
VLOG(1) << "Saving " << contacts_to_save_to_db->size() << " contact(s) to "
<< "database and deleting " << contact_ids_to_delete_from_db->size()
<< " as " << (is_full_update ? "full" : "incremental") << " update";
scoped_ptr<UpdateMetadata> metadata(new UpdateMetadata);
metadata->set_last_update_start_time(update_start_time.ToInternalValue());
metadata->set_last_contact_update_time(
last_contact_update_time_.ToInternalValue());
db_->SaveContacts(
contacts_to_save_to_db.Pass(),
contact_ids_to_delete_from_db.Pass(),
metadata.Pass(),
is_full_update,
base::Bind(&GoogleContactStore::OnDatabaseContactsSaved,
weak_ptr_factory_.GetWeakPtr()));
// We'll schedule an update from OnDatabaseContactsSaved() after we're done
// writing to the database -- we don't want to modify the contacts while
// they're being used by the database.
} else {
ScheduleUpdate(true);
}
}
void GoogleContactStore::OnDownloadFailure() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
LOG(WARNING) << "Contacts download failed for " << profile_->GetProfileName();
ScheduleUpdate(false);
}
void GoogleContactStore::OnDatabaseInitialized(bool success) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (success) {
VLOG(1) << "Contact database initialized for "
<< profile_->GetProfileName();
db_->LoadContacts(base::Bind(&GoogleContactStore::OnDatabaseContactsLoaded,
weak_ptr_factory_.GetWeakPtr()));
} else {
LOG(WARNING) << "Failed to initialize contact database for "
<< profile_->GetProfileName();
// Limp along as best as we can: throw away the database and do an update,
// which will schedule further updates.
DestroyDatabase();
UpdateContacts();
}
}
void GoogleContactStore::OnDatabaseContactsLoaded(
bool success,
scoped_ptr<ScopedVector<Contact> > contacts,
scoped_ptr<UpdateMetadata> metadata) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (success) {
VLOG(1) << "Loaded " << contacts->size() << " contact(s) from database";
MergeContacts(true, contacts.Pass());
last_successful_update_start_time_ =
base::Time::FromInternalValue(metadata->last_update_start_time());
last_contact_update_time_ = std::max(
last_contact_update_time_,
base::Time::FromInternalValue(metadata->last_contact_update_time()));
if (!contacts_.empty()) {
FOR_EACH_OBSERVER(ContactStoreObserver,
observers_,
OnContactsUpdated(this));
}
} else {
LOG(WARNING) << "Failed to load contacts from database";
}
UpdateContacts();
}
void GoogleContactStore::OnDatabaseContactsSaved(bool success) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (!success)
LOG(WARNING) << "Failed to save contacts to database";
// We only update the database when we've successfully downloaded contacts, so
// report success to ScheduleUpdate() even if the database update failed.
ScheduleUpdate(true);
}
GoogleContactStoreFactory::GoogleContactStoreFactory() {
}
GoogleContactStoreFactory::~GoogleContactStoreFactory() {
}
bool GoogleContactStoreFactory::CanCreateContactStoreForProfile(
Profile* profile) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(profile);
return chromeos::IsProfileAssociatedWithGaiaAccount(profile);
}
ContactStore* GoogleContactStoreFactory::CreateContactStore(Profile* profile) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(CanCreateContactStoreForProfile(profile));
return new GoogleContactStore(
g_browser_process->system_request_context(), profile);
}
} // namespace contacts