diff options
author | munjal@chromium.org <munjal@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-10-18 20:11:42 +0000 |
---|---|---|
committer | munjal@chromium.org <munjal@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-10-18 20:11:42 +0000 |
commit | 851881b6addae55ecb2f2c02383962f2ca1baab4 (patch) | |
tree | 117bc357c78906fdff5fb81358a08305bf92f4fa | |
parent | 8866f623dfd914c9f4c26ab96f90aecf4a372ef9 (diff) | |
download | chromium_src-851881b6addae55ecb2f2c02383962f2ca1baab4.zip chromium_src-851881b6addae55ecb2f2c02383962f2ca1baab4.tar.gz chromium_src-851881b6addae55ecb2f2c02383962f2ca1baab4.tar.bz2 |
Implement SyncableService in AppNotificationsManager:
- Implement all methods of SyncableService
- Modify existing methods that change the model (Add and ClearAll) to push changes to sync.
- Add some extra properties to AppNotification: guid and extension id
- Disallow operations on model until storage is loaded.
- Ton of unit tests for sync methods
There is anotehr part for the full notifications sync to work that will be done in a separate CL.
Review URL: http://codereview.chromium.org/8263002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@106110 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | chrome/browser/extensions/app_notification.cc | 46 | ||||
-rw-r--r-- | chrome/browser/extensions/app_notification.h | 18 | ||||
-rw-r--r-- | chrome/browser/extensions/app_notification_manager.cc | 422 | ||||
-rw-r--r-- | chrome/browser/extensions/app_notification_manager.h | 97 | ||||
-rw-r--r-- | chrome/browser/extensions/app_notification_manager_sync_unittest.cc | 588 | ||||
-rw-r--r-- | chrome/browser/extensions/app_notification_manager_unittest.cc | 18 | ||||
-rw-r--r-- | chrome/browser/extensions/app_notification_storage_unittest.cc | 10 | ||||
-rw-r--r-- | chrome/browser/extensions/app_notification_test_util.cc | 18 | ||||
-rw-r--r-- | chrome/browser/extensions/app_notification_test_util.h | 9 | ||||
-rw-r--r-- | chrome/browser/extensions/extension_app_api.cc | 5 | ||||
-rw-r--r-- | chrome/chrome_tests.gypi | 1 | ||||
-rw-r--r-- | chrome/common/chrome_notification_types.h | 4 |
12 files changed, 1142 insertions, 94 deletions
diff --git a/chrome/browser/extensions/app_notification.cc b/chrome/browser/extensions/app_notification.cc index f8e60e3..2dbae2b 100644 --- a/chrome/browser/extensions/app_notification.cc +++ b/chrome/browser/extensions/app_notification.cc @@ -5,15 +5,13 @@ #include "chrome/browser/extensions/app_notification.h" #include "base/memory/scoped_ptr.h" - -AppNotification::AppNotification(const std::string& title, - const std::string& body) - : title_(title), body_(body) {} - -AppNotification::~AppNotification() {} +#include "chrome/common/guid.h" namespace { +const char* kIsLocalKey = "is_local"; +const char* kGuidKey = "guid"; +const char* kExtensionIdKey = "extension_id"; const char* kTitleKey = "title"; const char* kBodyKey = "body"; const char* kLinkUrlKey = "link_url"; @@ -21,8 +19,27 @@ const char* kLinkTextKey = "link_text"; } // namespace +AppNotification::AppNotification(bool is_local, + const std::string& guid, + const std::string& extension_id, + const std::string& title, + const std::string& body) + : is_local_(is_local), + extension_id_(extension_id), + title_(title), + body_(body) { + guid_ = guid.empty() ? guid::GenerateGUID() : guid; +} + +AppNotification::~AppNotification() {} + void AppNotification::ToDictionaryValue(DictionaryValue* result) { CHECK(result); + result->SetBoolean(kIsLocalKey, is_local_); + if (!guid_.empty()) + result->SetString(kGuidKey, guid_); + if (!extension_id_.empty()) + result->SetString(kExtensionIdKey, extension_id_); if (!title_.empty()) result->SetString(kTitleKey, title_); if (!body_.empty()) @@ -36,9 +53,17 @@ void AppNotification::ToDictionaryValue(DictionaryValue* result) { // static AppNotification* AppNotification::FromDictionaryValue( const DictionaryValue& value) { + scoped_ptr<AppNotification> result(new AppNotification(true, "", "", "", "")); - scoped_ptr<AppNotification> result(new AppNotification("", "")); - + if (value.HasKey(kIsLocalKey) && !value.GetBoolean( + kIsLocalKey, &result->is_local_)) { + return NULL; + } + if (value.HasKey(kGuidKey) && !value.GetString(kGuidKey, &result->guid_)) + return NULL; + if (value.HasKey(kExtensionIdKey) && + !value.GetString(kExtensionIdKey, &result->extension_id_)) + return NULL; if (value.HasKey(kTitleKey) && !value.GetString(kTitleKey, &result->title_)) return NULL; if (value.HasKey(kBodyKey) && !value.GetString(kBodyKey, &result->body_)) @@ -60,7 +85,10 @@ AppNotification* AppNotification::FromDictionaryValue( } bool AppNotification::Equals(const AppNotification& other) const { - return (title_ == other.title_ && + return (is_local_ == other.is_local_ && + guid_ == other.guid_ && + extension_id_ == other.extension_id_ && + title_ == other.title_ && body_ == other.body_ && link_url_ == other.link_url_ && link_text_ == other.link_text_); diff --git a/chrome/browser/extensions/app_notification.h b/chrome/browser/extensions/app_notification.h index 8fd7743..4a9deb6 100644 --- a/chrome/browser/extensions/app_notification.h +++ b/chrome/browser/extensions/app_notification.h @@ -17,7 +17,14 @@ // displayed on the New Tab Page. class AppNotification { public: - AppNotification(const std::string& title, const std::string& body); + // Creates an instance with the given properties. + // If |is_local| is true, notification is not synced. + // If |guid| is empty, a new guid is automatically created. + AppNotification(bool is_local, + const std::string& guid, + const std::string& extension_id, + const std::string& title, + const std::string& body); ~AppNotification(); // Setters for optional properties. @@ -25,6 +32,9 @@ class AppNotification { void set_link_text(const std::string& text) { link_text_ = text; } // Accessors. + bool is_local() const { return is_local_; } + const std::string& guid() const { return guid_; } + const std::string& extension_id() const { return extension_id_; } const std::string& title() const { return title_; } const std::string& body() const { return body_; } const GURL& link_url() const { return link_url_; } @@ -41,6 +51,12 @@ class AppNotification { // If you add to the list of data members, make sure to add appropriate checks // to the Equals and {To,From}DictionaryValue methods, keeping in mind // backwards compatibility. + + // Whether notification is local only, which means it is not synced + // across machines. + bool is_local_; + std::string guid_; + std::string extension_id_; std::string title_; std::string body_; GURL link_url_; diff --git a/chrome/browser/extensions/app_notification_manager.cc b/chrome/browser/extensions/app_notification_manager.cc index 01a2bbc..53b753d4 100644 --- a/chrome/browser/extensions/app_notification_manager.cc +++ b/chrome/browser/extensions/app_notification_manager.cc @@ -4,31 +4,75 @@ #include "chrome/browser/extensions/app_notification_manager.h" +#include "base/auto_reset.h" #include "base/bind.h" #include "base/file_path.h" +#include "base/location.h" #include "base/stl_util.h" #include "chrome/browser/profiles/profile.h" +#include "chrome/browser/sync/protocol/app_notification_specifics.pb.h" #include "chrome/common/chrome_notification_types.h" #include "chrome/common/extensions/extension.h" #include "content/browser/browser_thread.h" #include "content/common/notification_service.h" -AppNotificationManager::AppNotificationManager(Profile* profile) - : profile_(profile) { - registrar_.Add(this, - chrome::NOTIFICATION_EXTENSION_UNINSTALLED, - Source<Profile>(profile_)); -} +typedef std::map<std::string, SyncData> SyncDataMap; namespace { +class GuidComparator + : public std::binary_function<linked_ptr<AppNotification>, + std::string, + bool> { + public: + bool operator() (linked_ptr<AppNotification> notif, + const std::string& guid) const { + return notif->guid() == guid; + } +}; + void DeleteStorageOnFileThread(AppNotificationStorage* storage) { CHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); delete storage; } +const AppNotification* FindByGuid(const AppNotificationList& list, + const std::string& guid) { + AppNotificationList::const_iterator iter = std::find_if( + list.begin(), list.end(), std::bind2nd(GuidComparator(), guid)); + return iter == list.end() ? NULL : iter->get(); +} + +void RemoveByGuid(AppNotificationList* list, const std::string& guid) { + if (!list) + return; + + AppNotificationList::iterator iter = std::find_if( + list->begin(), list->end(), std::bind2nd(GuidComparator(), guid)); + if (iter != list->end()) + list->erase(iter); +} + +void PopulateGuidToSyncDataMap(const SyncDataList& sync_data, + SyncDataMap* data_map) { + for (SyncDataList::const_iterator iter = sync_data.begin(); + iter != sync_data.end(); ++iter) { + (*data_map)[iter->GetSpecifics().GetExtension( + sync_pb::app_notification).guid()] = *iter; + } +} } // namespace +AppNotificationManager::AppNotificationManager(Profile* profile) + : profile_(profile), + sync_processor_(NULL), + models_associated_(false), + processing_syncer_changes_(false) { + registrar_.Add(this, + chrome::NOTIFICATION_EXTENSION_UNINSTALLED, + Source<Profile>(profile_)); +} + AppNotificationManager::~AppNotificationManager() { // Post a task to delete our storage on the file thread. BrowserThread::PostTask( @@ -42,59 +86,79 @@ void AppNotificationManager::Init() { BrowserThread::PostTask( BrowserThread::FILE, FROM_HERE, - base::Bind( - &AppNotificationManager::LoadOnFileThread, this, storage_path)); + base::Bind(&AppNotificationManager::LoadOnFileThread, + this, storage_path)); } -void AppNotificationManager::Add(const std::string& extension_id, - AppNotification* item) { - NotificationMap::iterator found = notifications_.find(extension_id); - if (found == notifications_.end()) { - notifications_[extension_id] = AppNotificationList(); - found = notifications_.find(extension_id); - } - CHECK(found != notifications_.end()); - AppNotificationList& list = (*found).second; - list.push_back(linked_ptr<AppNotification>(item)); +bool AppNotificationManager::Add(AppNotification* item) { + // Use a scoped_ptr to delete in error paths. + scoped_ptr<AppNotification> scoped_item(item); + if (!loaded()) + return false; + const std::string& extension_id = item->extension_id(); + AppNotificationList& list = GetAllInternal(extension_id); + linked_ptr<AppNotification> linked_item(scoped_item.release()); + list.push_back(linked_ptr<AppNotification>(linked_item)); - if (!storage_.get()) - return; + SyncAddChange(*linked_item); - BrowserThread::PostTask( - BrowserThread::FILE, - FROM_HERE, - base::Bind( - &AppNotificationManager::SaveOnFileThread, this, extension_id, list)); + if (storage_.get()) { + BrowserThread::PostTask( + BrowserThread::FILE, + FROM_HERE, + base::Bind(&AppNotificationManager::SaveOnFileThread, + this, extension_id, list)); + } + return true; } const AppNotificationList* AppNotificationManager::GetAll( - const std::string& extension_id) { - if (ContainsKey(notifications_, extension_id)) - return ¬ifications_[extension_id]; + const std::string& extension_id) const { + if (!loaded()) + return NULL; + if (ContainsKey(*notifications_, extension_id)) + return &((*notifications_)[extension_id]); return NULL; } +AppNotificationList& AppNotificationManager::GetAllInternal( + const std::string& extension_id) { + NotificationMap::iterator found = notifications_->find(extension_id); + if (found == notifications_->end()) { + (*notifications_)[extension_id] = AppNotificationList(); + found = notifications_->find(extension_id); + } + CHECK(found != notifications_->end()); + return found->second; +} + const AppNotification* AppNotificationManager::GetLast( const std::string& extension_id) { - NotificationMap::iterator found = notifications_.find(extension_id); - if (found == notifications_.end()) + if (!loaded()) + return NULL; + NotificationMap::iterator found = notifications_->find(extension_id); + if (found == notifications_->end()) return NULL; const AppNotificationList& list = found->second; return list.rbegin()->get(); } void AppNotificationManager::ClearAll(const std::string& extension_id) { - NotificationMap::iterator found = notifications_.find(extension_id); - if (found != notifications_.end()) - notifications_.erase(found); - - if (!storage_.get()) + if (!loaded()) return; - BrowserThread::PostTask( - BrowserThread::FILE, - FROM_HERE, - base::Bind( - &AppNotificationManager::DeleteOnFileThread, this, extension_id)); + NotificationMap::iterator found = notifications_->find(extension_id); + if (found != notifications_->end()) { + SyncClearAllChange(found->second); + notifications_->erase(found); + } + + if (storage_.get()) { + BrowserThread::PostTask( + BrowserThread::FILE, + FROM_HERE, + base::Bind(&AppNotificationManager::DeleteOnFileThread, + this, extension_id)); + } } void AppNotificationManager::Observe(int type, @@ -106,43 +170,53 @@ void AppNotificationManager::Observe(int type, void AppNotificationManager::LoadOnFileThread(const FilePath& storage_path) { CHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); + DCHECK(!loaded()); storage_.reset(AppNotificationStorage::Create(storage_path)); if (!storage_.get()) return; - NotificationMap result; + scoped_ptr<NotificationMap> result(new NotificationMap()); std::set<std::string> ids; if (!storage_->GetExtensionIds(&ids)) return; std::set<std::string>::const_iterator i; for (i = ids.begin(); i != ids.end(); ++i) { const std::string& id = *i; - AppNotificationList& list = result[id]; + AppNotificationList& list = (*result)[id]; if (!storage_->Get(id, &list)) - result.erase(id); + result->erase(id); } BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, - base::Bind(&AppNotificationManager::HandleLoadResults, this, result)); + base::Bind(&AppNotificationManager::HandleLoadResults, + this, result.release())); } -void AppNotificationManager::HandleLoadResults(const NotificationMap& map) { +void AppNotificationManager::HandleLoadResults(NotificationMap* map) { CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + DCHECK(map); + DCHECK(!loaded()); + notifications_.reset(map); + + // Generate STATE_CHAGNED notifications for extensions that have at + // least one notification loaded. NotificationMap::const_iterator i; - for (i = map.begin(); i != map.end(); ++i) { + for (i = map->begin(); i != map->end(); ++i) { const std::string& id = i->first; - const AppNotificationList& list = i->second; - if (list.empty()) + if (i->second.empty()) continue; - notifications_[id].insert(notifications_[id].begin(), - list.begin(), - list.end()); NotificationService::current()->Notify( chrome::NOTIFICATION_APP_NOTIFICATION_STATE_CHANGED, Source<Profile>(profile_), Details<const std::string>(&id)); } + + // Generate MANAGER_LOADED notification. + NotificationService::current()->Notify( + chrome::NOTIFICATION_APP_NOTIFICATION_MANAGER_LOADED, + Source<AppNotificationManager>(this), + NotificationService::NoDetails()); } void AppNotificationManager::SaveOnFileThread(const std::string& extension_id, @@ -156,3 +230,249 @@ void AppNotificationManager::DeleteOnFileThread( CHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); storage_->Delete(extension_id); } + +SyncDataList AppNotificationManager::GetAllSyncData( + syncable::ModelType type) const { + CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + DCHECK(loaded()); + DCHECK_EQ(syncable::APP_NOTIFICATIONS, type); + SyncDataList data; + for (NotificationMap::const_iterator iter = notifications_->begin(); + iter != notifications_->end(); ++iter) { + + // Skip local notifications since they should not be synced. + const AppNotificationList list = (*iter).second; + for (AppNotificationList::const_iterator list_iter = list.begin(); + list_iter != list.end(); ++list_iter) { + const AppNotification* notification = (*list_iter).get(); + if (notification->is_local()) { + continue; + } + data.push_back(CreateSyncDataFromNotification(*notification)); + } + } + + return data; +} + +SyncError AppNotificationManager::ProcessSyncChanges( + const tracked_objects::Location& from_here, + const SyncChangeList& change_list) { + CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + DCHECK(loaded()); + if (!models_associated_) + return SyncError(FROM_HERE, "Models not yet associated.", + syncable::APP_NOTIFICATIONS); + + AutoReset<bool> processing_changes(&processing_syncer_changes_, true); + + SyncError error; + for (SyncChangeList::const_iterator iter = change_list.begin(); + iter != change_list.end(); ++iter) { + SyncData sync_data = iter->sync_data(); + DCHECK_EQ(syncable::APP_NOTIFICATIONS, sync_data.GetDataType()); + SyncChange::SyncChangeType change_type = iter->change_type(); + + scoped_ptr<AppNotification> new_notif(CreateNotificationFromSyncData( + sync_data)); + if (!new_notif.get()) { + NOTREACHED() << "Failed to read notification."; + continue; + } + const AppNotification* existing_notif = GetNotification( + new_notif->extension_id(), new_notif->guid()); + if (existing_notif && existing_notif->is_local()) { + NOTREACHED() << "Matched with notification marked as local"; + error = SyncError(FROM_HERE, + "ProcessSyncChanges received a local only notification" + + SyncChange::ChangeTypeToString(change_type), + syncable::APP_NOTIFICATIONS); + continue; + } + + if (change_type == SyncChange::ACTION_ADD && !existing_notif) { + Add(new_notif.release()); + } else if (change_type == SyncChange::ACTION_DELETE && existing_notif) { + Remove(existing_notif->extension_id(), existing_notif->guid()); + } else { + // Something really unexpected happened. Either we received an + // ACTION_INVALID, or Sync is in a crazy state: + // - Trying to UPDATE: notifications are immutable. + // . Trying to DELETE a non-existent item. + // - Trying to ADD an item that already exists. + NOTREACHED() << "Unexpected sync change state."; + error = SyncError(FROM_HERE, "ProcessSyncChanges failed on ChangeType " + + SyncChange::ChangeTypeToString(change_type), + syncable::APP_NOTIFICATIONS); + continue; + } + } + + return error; +} + +SyncError AppNotificationManager::MergeDataAndStartSyncing( + syncable::ModelType type, + const SyncDataList& initial_sync_data, + SyncChangeProcessor* sync_processor) { + CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); + // AppNotificationDataTypeController ensures that modei is fully should before + // this method is called by waiting until the load notification is received + // from AppNotificationManager. + DCHECK(loaded()); + DCHECK_EQ(type, syncable::APP_NOTIFICATIONS); + DCHECK(!sync_processor_); + sync_processor_ = sync_processor; + + // We may add, or remove notifications here, so ensure we don't step on + // our own toes. + AutoReset<bool> processing_changes(&processing_syncer_changes_, true); + + SyncDataMap local_data_map; + PopulateGuidToSyncDataMap(GetAllSyncData(syncable::APP_NOTIFICATIONS), + &local_data_map); + + for (SyncDataList::const_iterator iter = initial_sync_data.begin(); + iter != initial_sync_data.end(); ++iter) { + const SyncData& sync_data = *iter; + DCHECK_EQ(syncable::APP_NOTIFICATIONS, sync_data.GetDataType()); + scoped_ptr<AppNotification> sync_notif(CreateNotificationFromSyncData( + sync_data)); + CHECK(sync_notif.get()); + const AppNotification* local_notif = GetNotification( + sync_notif->extension_id(), sync_notif->guid()); + if (local_notif) { + local_data_map.erase(sync_notif->guid()); + // Local notification should always match with sync notification as + // notifications are immutable. + if (local_notif->is_local() || !sync_notif->Equals(*local_notif)) { + return SyncError(FROM_HERE, + "MergeDataAndStartSyncing failed: local notification and sync " + "notification have same guid but different data.", + syncable::APP_NOTIFICATIONS); + } + } else { + // Sync model has a notification that local model does not, add it. + Add(sync_notif.release()); + } + } + + // TODO(munjal): crbug.com/10059. Work with Lingesh/Antony to resolve. + SyncChangeList new_changes; + for (SyncDataMap::const_iterator iter = local_data_map.begin(); + iter != local_data_map.end(); ++iter) { + new_changes.push_back(SyncChange(SyncChange::ACTION_ADD, iter->second)); + } + + SyncError error; + if (new_changes.size() > 0) + error = sync_processor_->ProcessSyncChanges(FROM_HERE, new_changes); + models_associated_ = !error.IsSet(); + return error; +} + +void AppNotificationManager::StopSyncing(syncable::ModelType type) { + DCHECK_EQ(type, syncable::APP_NOTIFICATIONS); + models_associated_ = false; + sync_processor_ = NULL; +} + +void AppNotificationManager::SyncAddChange(const AppNotification& notif) { + // Skip if either: + // - Notification is marked as local. + // - Sync is not enabled by user. + // - Change is generated from within the manager. + if (notif.is_local() || !models_associated_ || processing_syncer_changes_) + return; + + // TODO(munjal): crbug.com/10059. Work with Lingesh/Antony to resolve. + + SyncChangeList changes; + SyncData sync_data = CreateSyncDataFromNotification(notif); + changes.push_back(SyncChange(SyncChange::ACTION_ADD, sync_data)); + sync_processor_->ProcessSyncChanges(FROM_HERE, changes); +} + +void AppNotificationManager::SyncClearAllChange( + const AppNotificationList& list) { + // Skip if either: + // - Sync is not enabled by user. + // - Change is generated from within the manager. + if (!models_associated_ || processing_syncer_changes_) + return; + + SyncChangeList changes; + for (AppNotificationList::const_iterator iter = list.begin(); + iter != list.end(); ++iter) { + const AppNotification& notif = *iter->get(); + // Skip notifications marked as local. + if (notif.is_local()) + continue; + changes.push_back(SyncChange( + SyncChange::ACTION_DELETE, + CreateSyncDataFromNotification(notif))); + } + sync_processor_->ProcessSyncChanges(FROM_HERE, changes); +} + +const AppNotification* AppNotificationManager::GetNotification( + const std::string& extension_id, const std::string& guid) { + DCHECK(loaded()); + const AppNotificationList& list = GetAllInternal(extension_id); + return FindByGuid(list, guid); +} + +void AppNotificationManager::Remove(const std::string& extension_id, + const std::string& guid) { + DCHECK(loaded()); + AppNotificationList& list = GetAllInternal(extension_id); + RemoveByGuid(&list, guid); + + if (storage_.get()) { + BrowserThread::PostTask( + BrowserThread::FILE, + FROM_HERE, + base::Bind(&AppNotificationManager::SaveOnFileThread, + this, extension_id, list)); + } +} + +// static +SyncData AppNotificationManager::CreateSyncDataFromNotification( + const AppNotification& notification) { + DCHECK(!notification.is_local()); + sync_pb::EntitySpecifics specifics; + sync_pb::AppNotificationSpecifics* notif_specifics = + specifics.MutableExtension(sync_pb::app_notification); + notif_specifics->set_app_id(notification.extension_id()); + notif_specifics->set_body_text(notification.body()); + notif_specifics->set_guid(notification.guid()); + notif_specifics->set_link_text(notification.link_text()); + notif_specifics->set_link_url(notification.link_url().spec()); + notif_specifics->set_title(notification.title()); + + return SyncData::CreateLocalData( + notif_specifics->guid(), notif_specifics->app_id(), specifics); +} + +// static +AppNotification* AppNotificationManager::CreateNotificationFromSyncData( + const SyncData& sync_data) { + sync_pb::AppNotificationSpecifics specifics = + sync_data.GetSpecifics().GetExtension(sync_pb::app_notification); + + // Check for mandatory fields. + if (!specifics.has_app_id() || !specifics.has_guid() || + !specifics.has_title() || !specifics.has_body_text()) { + return NULL; + } + + AppNotification* notification = new AppNotification( + false, specifics.guid(), specifics.app_id(), + specifics.title(), specifics.body_text()); + if (specifics.has_link_text()) + notification->set_link_text(specifics.link_text()); + if (specifics.has_link_url()) + notification->set_link_url(GURL(specifics.link_url())); + return notification; +} diff --git a/chrome/browser/extensions/app_notification_manager.h b/chrome/browser/extensions/app_notification_manager.h index 8148a08..7c37cc7 100644 --- a/chrome/browser/extensions/app_notification_manager.h +++ b/chrome/browser/extensions/app_notification_manager.h @@ -9,10 +9,15 @@ #include <map> #include "base/compiler_specific.h" +#include "base/gtest_prod_util.h" #include "base/memory/ref_counted.h" #include "base/memory/scoped_ptr.h" +#include "base/synchronization/waitable_event.h" #include "chrome/browser/extensions/app_notification.h" #include "chrome/browser/extensions/app_notification_storage.h" +#include "chrome/browser/sync/api/sync_change.h" +#include "chrome/browser/sync/api/syncable_service.h" +#include "content/common/notification_details.h" #include "content/common/notification_observer.h" #include "content/common/notification_registrar.h" @@ -21,17 +26,20 @@ class Profile; // This class keeps track of notifications for installed apps. class AppNotificationManager : public base::RefCountedThreadSafe<AppNotificationManager>, - public NotificationObserver { + public NotificationObserver, + public SyncableService { public: explicit AppNotificationManager(Profile* profile); + virtual ~AppNotificationManager(); // Starts up the process of reading from persistent storage. void Init(); - // Takes ownership of |notification|. - void Add(const std::string& extension_id, AppNotification* item); + // Returns whether add was succcessful. + // Takes ownership of |item| in all cases. + bool Add(AppNotification* item); - const AppNotificationList* GetAll(const std::string& extension_id); + const AppNotificationList* GetAll(const std::string& extension_id) const; // Returns the most recently added notification for |extension_id| if there // are any, or NULL otherwise. @@ -45,32 +53,105 @@ class AppNotificationManager const NotificationSource& source, const NotificationDetails& details) OVERRIDE; + bool loaded() const { return notifications_.get() != NULL; } + + // SyncableService implementation. + + // Returns all syncable notifications from this model as SyncData. + virtual SyncDataList GetAllSyncData(syncable::ModelType type) const OVERRIDE; + // Process notifications related changes from Sync, merging them into + // our model. + virtual SyncError ProcessSyncChanges( + const tracked_objects::Location& from_here, + const SyncChangeList& change_list) OVERRIDE; + // Associate and merge sync data model with our data model. + virtual SyncError MergeDataAndStartSyncing( + syncable::ModelType type, + const SyncDataList& initial_sync_data, + SyncChangeProcessor* sync_processor) OVERRIDE; + virtual void StopSyncing(syncable::ModelType type) OVERRIDE; + private: + friend class AppNotificationManagerSyncTest; + FRIEND_TEST_ALL_PREFIXES(AppNotificationManagerSyncTest, + NotificationToSyncDataToNotification); + FRIEND_TEST_ALL_PREFIXES(AppNotificationManagerSyncTest, + GetAllSyncDataNoLocal); + FRIEND_TEST_ALL_PREFIXES(AppNotificationManagerSyncTest, + GetAllSyncDataSomeLocal); + FRIEND_TEST_ALL_PREFIXES(AppNotificationManagerSyncTest, + ModelAssocModelEmpty); + FRIEND_TEST_ALL_PREFIXES(AppNotificationManagerSyncTest, + ModelAssocBothNonEmptyNoOverlap); + FRIEND_TEST_ALL_PREFIXES(AppNotificationManagerSyncTest, + ModelAssocBothNonEmptySomeOverlap); + FRIEND_TEST_ALL_PREFIXES(AppNotificationManagerSyncTest, + ModelAssocBothNonEmptyTitleMismatch); + FRIEND_TEST_ALL_PREFIXES(AppNotificationManagerSyncTest, + ModelAssocBothNonEmptyMatchesLocal); + FRIEND_TEST_ALL_PREFIXES(AppNotificationManagerSyncTest, + ProcessSyncChangesErrorModelAssocNotDone); + FRIEND_TEST_ALL_PREFIXES(AppNotificationManagerSyncTest, + ProcessSyncChangesEmptyModel); + FRIEND_TEST_ALL_PREFIXES(AppNotificationManagerSyncTest, + StopSyncing); + FRIEND_TEST_ALL_PREFIXES(AppNotificationManagerSyncTest, + ClearAllGetsSynced); + friend class base::RefCountedThreadSafe<AppNotificationManager>; // Maps extension id to a list of notifications for that extension. typedef std::map<std::string, AppNotificationList> NotificationMap; - virtual ~AppNotificationManager(); - // Starts loading storage_ using |storage_path|. void LoadOnFileThread(const FilePath& storage_path); // Called on the UI thread to handle the loaded results from storage_. - void HandleLoadResults(const NotificationMap& map); + void HandleLoadResults(NotificationMap* map); void SaveOnFileThread(const std::string& extension_id, const AppNotificationList& list); void DeleteOnFileThread(const std::string& extension_id); + // Gets notficiations for a given extension id. + AppNotificationList& GetAllInternal(const std::string& extension_id); + + // Removes the notification with given extension id and given guid. + void Remove(const std::string& extension_id, const std::string& guid); + + // Returns the notification for the given |extension_id| and |guid|, + // NULL if no such notification exists. + const AppNotification* GetNotification(const std::string& extension_id, + const std::string& guid); + + // Sends a change to syncer to add the given notification. + void SyncAddChange(const AppNotification& notif); + + // Sends changes to syncer to remove all notifications in the given list. + void SyncClearAllChange(const AppNotificationList& list); + + // Converters from AppNotification to SyncData and vice versa. + static SyncData CreateSyncDataFromNotification( + const AppNotification& notification); + static AppNotification* CreateNotificationFromSyncData( + const SyncData& sync_data); + Profile* profile_; NotificationRegistrar registrar_; - NotificationMap notifications_; + scoped_ptr<NotificationMap> notifications_; // This should only be used on the FILE thread. scoped_ptr<AppNotificationStorage> storage_; + // Sync change processor we use to push all our changes. + SyncChangeProcessor* sync_processor_; + // Whether the sync model is associated with the local model. + // In other words, whether we are ready to apply sync changes. + bool models_associated_; + // Whether syncer changes are being processed right now. + bool processing_syncer_changes_; + DISALLOW_COPY_AND_ASSIGN(AppNotificationManager); }; diff --git a/chrome/browser/extensions/app_notification_manager_sync_unittest.cc b/chrome/browser/extensions/app_notification_manager_sync_unittest.cc new file mode 100644 index 0000000..41589d7 --- /dev/null +++ b/chrome/browser/extensions/app_notification_manager_sync_unittest.cc @@ -0,0 +1,588 @@ +// Copyright (c) 2011 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 "base/bind.h" +#include "base/memory/scoped_ptr.h" +#include "base/string_number_conversions.h" +#include "chrome/browser/extensions/app_notification_manager.h" +#include "chrome/browser/extensions/app_notification.h" +#include "chrome/browser/sync/protocol/app_notification_specifics.pb.h" +#include "chrome/test/base/testing_profile.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +// Extract notification guid from SyncData. +std::string GetGuid(const SyncData& sync_data) { + return sync_data.GetSpecifics().GetExtension( + sync_pb::app_notification).guid(); +} + +// Dummy SyncChangeProcessor used to help review what SyncChanges are pushed +// back up to Sync. +class TestChangeProcessor : public SyncChangeProcessor { + public: + TestChangeProcessor() { } + virtual ~TestChangeProcessor() { } + + // Store a copy of all the changes passed in so we can examine them later. + virtual SyncError ProcessSyncChanges( + const tracked_objects::Location& from_here, + const SyncChangeList& change_list) { + // change_map_.erase(change_map_.begin(), change_map_.end()); + for (SyncChangeList::const_iterator iter = change_list.begin(); + iter != change_list.end(); ++iter) { + change_map_[GetGuid(iter->sync_data())] = *iter; + } + + return SyncError(); + } + + bool ContainsGuid(const std::string& guid) { + return change_map_.find(guid) != change_map_.end(); + } + + SyncChange GetChangeByGuid(const std::string& guid) { + DCHECK(ContainsGuid(guid)); + return change_map_[guid]; + } + + int change_list_size() { return change_map_.size(); } + + private: + // Track the changes received in ProcessSyncChanges. + std::map<std::string, SyncChange> change_map_; + + DISALLOW_COPY_AND_ASSIGN(TestChangeProcessor); +}; + +} // namespace + +class AppNotificationManagerSyncTest : public testing::Test { + public: + AppNotificationManagerSyncTest() + : ui_thread_(BrowserThread::UI, &ui_loop_), + file_thread_(BrowserThread::FILE) { } + + ~AppNotificationManagerSyncTest() { + model_ = NULL; + } + + virtual void SetUp() { + ASSERT_TRUE(BrowserThread::CurrentlyOn(BrowserThread::UI)); + file_thread_.Start(); + + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + profile_.reset(new TestingProfile(temp_dir_.path())); + model_ = new AppNotificationManager(profile_.get()); + model_->Init(); + + WaitForFileThread(); + ASSERT_TRUE(model_->loaded()); + } + + virtual void TearDown() { } + + static void PostQuitToUIThread() { + BrowserThread::PostTask(BrowserThread::UI, + FROM_HERE, + new MessageLoop::QuitTask()); + } + + static void WaitForFileThread() { + BrowserThread::PostTask(BrowserThread::FILE, + FROM_HERE, + base::Bind(&PostQuitToUIThread)); + MessageLoop::current()->Run(); + } + + AppNotificationManager* model() { return model_.get(); } + TestChangeProcessor* processor() { return &processor_; } + + // Creates a notification whose properties are set from the given integer. + static AppNotification* CreateNotification(int suffix) { + return CreateNotification(false, suffix); + } + static AppNotification* CreateNotification(bool is_local, int suffix) { + std::string s = base::IntToString(suffix); + return CreateNotification( + is_local, "guid" + s, "ext" + s, "text" + s, "body" + s, + "http://www.url" + s + ".com", "link text " + s); + } + static AppNotification* CreateNotification( + bool is_local, int suffix, const std::string& extension_id) { + std::string s = base::IntToString(suffix); + return CreateNotification( + is_local, "guid" + s, extension_id, "text" + s, "body" + s, + "http://www.url" + s + ".com", "link text " + s); + } + + // Creates a notification whose properties are set from the given integer + // but does not set link url and link text. + static AppNotification* CreateNotificationNoLink(int suffix) { + return CreateNotificationNoLink(false, suffix); + } + static AppNotification* CreateNotificationNoLink(bool is_local, int suffix) { + std::string s = base::IntToString(suffix); + return CreateNotification( + is_local, "guid" + s, "ext" + s, "text" + s, "body" + s, "", ""); + } + + // link_url and link_text are only set if the passed in values are not empty. + static AppNotification* CreateNotification(bool is_local, + const std::string& guid, + const std::string& extension_id, + const std::string& title, + const std::string& body, + const std::string& link_url, + const std::string& link_text) { + AppNotification* notif = new AppNotification( + is_local, guid, extension_id, title, body); + if (!link_url.empty()) + notif->set_link_url(GURL(link_url)); + if (!link_text.empty()) + notif->set_link_text(link_text); + return notif; + } + + static AppNotification* CreateNotification(const AppNotification& notif) { + return CreateNotification( + notif.is_local(), notif.guid(), notif.extension_id(), notif.title(), + notif.body(), notif.link_url().spec(), notif.link_text()); + } + + static SyncData CreateSyncData(int suffix) { + scoped_ptr<AppNotification> notif(CreateNotification(suffix)); + return AppNotificationManager::CreateSyncDataFromNotification(*notif); + } + static SyncData CreateSyncData(int suffix, const std::string& extension_id) { + scoped_ptr<AppNotification> notif( + CreateNotification(false, suffix, extension_id)); + return AppNotificationManager::CreateSyncDataFromNotification(*notif); + } + + // Helper to create SyncChange. Takes ownership of |notif|. + static SyncChange CreateSyncChange(SyncChange::SyncChangeType type, + AppNotification* notif) { + // Take control of notif to clean it up after we create data out of it. + scoped_ptr<AppNotification> scoped_notif(notif); + return SyncChange( + type, AppNotificationManager::CreateSyncDataFromNotification(*notif)); + } + + void AssertSyncChange(const SyncChange& change, + SyncChange::SyncChangeType type, + const AppNotification& notif) { + ASSERT_EQ(type, change.change_type()); + const AppNotification* notif2 = + AppNotificationManager::CreateNotificationFromSyncData( + change.sync_data()); + ASSERT_TRUE(notif.Equals(*notif2)); + } + + protected: + MessageLoop ui_loop_; + BrowserThread ui_thread_; + BrowserThread file_thread_; + + // We keep two TemplateURLServices to test syncing between them. + ScopedTempDir temp_dir_; + scoped_ptr<TestingProfile> profile_; + scoped_refptr<AppNotificationManager> model_; + + TestChangeProcessor processor_; + + DISALLOW_COPY_AND_ASSIGN(AppNotificationManagerSyncTest); +}; + +// Create an AppNotification, convert it to SyncData and convert it back. +TEST_F(AppNotificationManagerSyncTest, NotificationToSyncDataToNotification) { + { // Partial properties set. + scoped_ptr<AppNotification> notif1(CreateNotificationNoLink(1)); + SyncData sync_data = + AppNotificationManager::CreateSyncDataFromNotification(*notif1); + scoped_ptr<AppNotification> notif2( + AppNotificationManager::CreateNotificationFromSyncData(sync_data)); + EXPECT_TRUE(notif2.get()); + EXPECT_TRUE(notif1->Equals(*notif2)); + } + { // All properties set. + scoped_ptr<AppNotification> notif1(CreateNotification(1)); + SyncData sync_data = + AppNotificationManager::CreateSyncDataFromNotification(*notif1); + scoped_ptr<AppNotification> notif2( + AppNotificationManager::CreateNotificationFromSyncData(sync_data)); + EXPECT_TRUE(notif2.get()); + EXPECT_TRUE(notif1->Equals(*notif2)); + } +} + +// GetAllSyncData returns all notifications since none are marked local only. +TEST_F(AppNotificationManagerSyncTest, GetAllSyncDataNoLocal) { + model()->Add(CreateNotificationNoLink(1)); + model()->Add(CreateNotification(2)); + model()->Add(CreateNotification(3)); + SyncDataList all_sync_data = + model()->GetAllSyncData(syncable::APP_NOTIFICATIONS); + + EXPECT_EQ(3U, all_sync_data.size()); + + for (SyncDataList::const_iterator iter = all_sync_data.begin(); + iter != all_sync_data.end(); ++iter) { + scoped_ptr<AppNotification> notif1( + AppNotificationManager::CreateNotificationFromSyncData(*iter)); + + const std::string& guid = notif1->guid(); + const std::string& ext_id = notif1->extension_id(); + + const AppNotification* notif2 = model()->GetNotification(ext_id, guid); + ASSERT_TRUE(notif1->Equals(*notif2)); + } +} + +// GetAllSyncData should not return notifications marked as local only. +TEST_F(AppNotificationManagerSyncTest, GetAllSyncDataSomeLocal) { + model()->Add(CreateNotificationNoLink(1)); + model()->Add(CreateNotification(true, 2)); + model()->Add(CreateNotification(3)); + model()->Add(CreateNotification(true, 4)); + model()->Add(CreateNotification(5)); + SyncDataList all_sync_data = + model()->GetAllSyncData(syncable::APP_NOTIFICATIONS); + + EXPECT_EQ(3U, all_sync_data.size()); + + for (SyncDataList::const_iterator iter = all_sync_data.begin(); + iter != all_sync_data.end(); ++iter) { + scoped_ptr<AppNotification> notif1( + AppNotificationManager::CreateNotificationFromSyncData(*iter)); + const std::string& guid = notif1->guid(); + const std::string& ext_id = notif1->extension_id(); + + const AppNotification* notif2 = model()->GetNotification(ext_id, guid); + ASSERT_TRUE(notif1->Equals(*notif2)); + } +} + +// Model assocation: both models are empty. +TEST_F(AppNotificationManagerSyncTest, ModelAssocBothEmpty) { + model()->MergeDataAndStartSyncing( + syncable::APP_NOTIFICATIONS, + SyncDataList(), // Empty. + processor()); + + EXPECT_EQ(0U, model()->GetAllSyncData(syncable::APP_NOTIFICATIONS).size()); + EXPECT_EQ(0, processor()->change_list_size()); +} + +// Model assocation: empty sync model and non-empty local model. +TEST_F(AppNotificationManagerSyncTest, ModelAssocModelEmpty) { + SyncDataList initial_data; + initial_data.push_back(CreateSyncData(1)); + initial_data.push_back(CreateSyncData(2)); + initial_data.push_back(CreateSyncData(3)); + initial_data.push_back(CreateSyncData(4)); + + model()->MergeDataAndStartSyncing( + syncable::APP_NOTIFICATIONS, + initial_data, + processor()); + + EXPECT_EQ(4U, model()->GetAllSyncData(syncable::APP_NOTIFICATIONS).size()); + // Model should all of the initial sync data. + for (SyncDataList::const_iterator iter = initial_data.begin(); + iter != initial_data.end(); ++iter) { + scoped_ptr<AppNotification> notif1( + AppNotificationManager::CreateNotificationFromSyncData(*iter)); + const std::string& ext_id = notif1->extension_id(); + const std::string& guid = notif1->guid(); + const AppNotification* notif2 = model()->GetNotification(ext_id, guid); + EXPECT_TRUE(notif2); + EXPECT_TRUE(notif1->Equals(*notif2)); + } + + EXPECT_EQ(0, processor()->change_list_size()); +} + +// Model has some notifications, some of them are local only. Sync has some +// notifications. No items match up. +TEST_F(AppNotificationManagerSyncTest, ModelAssocBothNonEmptyNoOverlap) { + AppNotification* n1 = CreateNotification(1); + model()->Add(n1); + AppNotification* n2 = CreateNotification(true, 2); + model()->Add(n2); + AppNotification* n3 = CreateNotification(3); + model()->Add(n3); + + SyncDataList initial_data; + initial_data.push_back(CreateSyncData(4)); + initial_data.push_back(CreateSyncData(5)); + initial_data.push_back(CreateSyncData(6)); + initial_data.push_back(CreateSyncData(7)); + + model()->MergeDataAndStartSyncing( + syncable::APP_NOTIFICATIONS, + initial_data, + processor()); + + EXPECT_EQ(6U, model()->GetAllSyncData(syncable::APP_NOTIFICATIONS).size()); + for (SyncDataList::const_iterator iter = initial_data.begin(); + iter != initial_data.end(); ++iter) { + scoped_ptr<AppNotification> notif1( + AppNotificationManager::CreateNotificationFromSyncData(*iter)); + const std::string& ext_id = notif1->extension_id(); + const std::string& guid = notif1->guid(); + const AppNotification* notif2 = model()->GetNotification(ext_id, guid); + EXPECT_TRUE(notif2); + EXPECT_TRUE(notif1->Equals(*notif2)); + } + EXPECT_TRUE(model()->GetNotification(n1->extension_id(), n1->guid())); + EXPECT_TRUE(model()->GetNotification(n2->extension_id(), n2->guid())); + EXPECT_TRUE(model()->GetNotification(n3->extension_id(), n3->guid())); + + EXPECT_EQ(2, processor()->change_list_size()); + EXPECT_TRUE(processor()->ContainsGuid(n1->guid())); + EXPECT_EQ(SyncChange::ACTION_ADD, + processor()->GetChangeByGuid(n1->guid()).change_type()); + EXPECT_FALSE(processor()->ContainsGuid(n2->guid())); + EXPECT_TRUE(processor()->ContainsGuid(n3->guid())); + EXPECT_EQ(SyncChange::ACTION_ADD, + processor()->GetChangeByGuid(n3->guid()).change_type()); +} + +// Model has some notifications, some of them are local only. Sync has some +// notifications. Some items match up. +TEST_F(AppNotificationManagerSyncTest, ModelAssocBothNonEmptySomeOverlap) { + AppNotification* n1 = CreateNotification(1); + model()->Add(n1); + AppNotification* n2 = CreateNotification(true, 2); + model()->Add(n2); + AppNotification* n3 = CreateNotification(3); + model()->Add(n3); + AppNotification* n4 = CreateNotification(4); + model()->Add(n4); + + SyncDataList initial_data; + initial_data.push_back(CreateSyncData(5)); + initial_data.push_back( + AppNotificationManager::CreateSyncDataFromNotification(*n1)); + initial_data.push_back(CreateSyncData(6)); + initial_data.push_back( + AppNotificationManager::CreateSyncDataFromNotification(*n4)); + initial_data.push_back(CreateSyncData(7)); + + model()->MergeDataAndStartSyncing( + syncable::APP_NOTIFICATIONS, + initial_data, + processor()); + + EXPECT_EQ(6U, model()->GetAllSyncData(syncable::APP_NOTIFICATIONS).size()); + for (SyncDataList::const_iterator iter = initial_data.begin(); + iter != initial_data.end(); ++iter) { + scoped_ptr<AppNotification> notif1( + AppNotificationManager::CreateNotificationFromSyncData(*iter)); + const std::string& ext_id = notif1->extension_id(); + const std::string& guid = notif1->guid(); + const AppNotification* notif2 = model()->GetNotification(ext_id, guid); + EXPECT_TRUE(notif2); + EXPECT_TRUE(notif1->Equals(*notif2)); + } + EXPECT_TRUE(model()->GetNotification(n1->extension_id(), n1->guid())); + EXPECT_TRUE(model()->GetNotification(n2->extension_id(), n2->guid())); + EXPECT_TRUE(model()->GetNotification(n3->extension_id(), n3->guid())); + EXPECT_TRUE(model()->GetNotification(n4->extension_id(), n4->guid())); + + EXPECT_EQ(1, processor()->change_list_size()); + EXPECT_FALSE(processor()->ContainsGuid(n1->guid())); + EXPECT_FALSE(processor()->ContainsGuid(n2->guid())); + EXPECT_TRUE(processor()->ContainsGuid(n3->guid())); + EXPECT_EQ(SyncChange::ACTION_ADD, + processor()->GetChangeByGuid(n3->guid()).change_type()); + EXPECT_FALSE(processor()->ContainsGuid(n4->guid())); +} + +// When an item that matches up in model and sync is different, an error +// should be returned. +TEST_F(AppNotificationManagerSyncTest, ModelAssocBothNonEmptyTitleMismatch) { + AppNotification* n1 = CreateNotification(1); + model()->Add(n1); + AppNotification* n2 = CreateNotification(true, 2); + model()->Add(n2); + + SyncDataList initial_data; + initial_data.push_back(CreateSyncData(1)); + scoped_ptr<AppNotification> n1_a(CreateNotification( + n1->is_local(), n1->guid(), n1->extension_id(), + n1->title() + "_changed", // different title + n1->body(), n1->link_url().spec(), n1->link_text())); + initial_data.push_back( + AppNotificationManager::CreateSyncDataFromNotification(*n1_a)); + + SyncError sync_error = model()->MergeDataAndStartSyncing( + syncable::APP_NOTIFICATIONS, + initial_data, + processor()); + + EXPECT_TRUE(sync_error.IsSet()); + EXPECT_EQ(syncable::APP_NOTIFICATIONS, sync_error.type()); + EXPECT_EQ(0, processor()->change_list_size()); +} + +// When an item in sync matches with a local-only item in model, an error +// should be returned. +TEST_F(AppNotificationManagerSyncTest, ModelAssocBothNonEmptyMatchesLocal) { + AppNotification* n1 = CreateNotification(1); + model()->Add(n1); + AppNotification* n2 = CreateNotification(true, 2); + model()->Add(n2); + + SyncDataList initial_data; + initial_data.push_back(CreateSyncData(1)); + scoped_ptr<AppNotification> n2_a(CreateNotification(2)); + initial_data.push_back( + AppNotificationManager::CreateSyncDataFromNotification(*n2_a)); + + SyncError sync_error = model()->MergeDataAndStartSyncing( + syncable::APP_NOTIFICATIONS, + initial_data, + processor()); + + EXPECT_TRUE(sync_error.IsSet()); + EXPECT_EQ(syncable::APP_NOTIFICATIONS, sync_error.type()); + EXPECT_EQ(0, processor()->change_list_size()); +} + +// Process sync changes when model is empty. +TEST_F(AppNotificationManagerSyncTest, ProcessSyncChangesEmptyModel) { + // We initially have no data. + model()->MergeDataAndStartSyncing( + syncable::APP_NOTIFICATIONS, + SyncDataList(), + processor()); + + // Set up a bunch of ADDs. + SyncChangeList changes; + changes.push_back(CreateSyncChange( + SyncChange::ACTION_ADD, CreateNotification(1))); + changes.push_back(CreateSyncChange( + SyncChange::ACTION_ADD, CreateNotification(2))); + changes.push_back(CreateSyncChange( + SyncChange::ACTION_ADD, CreateNotification(3))); + + model()->ProcessSyncChanges(FROM_HERE, changes); + + EXPECT_EQ(3U, model()->GetAllSyncData(syncable::APP_NOTIFICATIONS).size()); + EXPECT_EQ(0, processor()->change_list_size()); +} + +// Process sync changes when model is not empty. +TEST_F(AppNotificationManagerSyncTest, ProcessSyncChangesNonEmptyModel) { + AppNotification* n1 = CreateNotification(1); + model()->Add(n1); + AppNotification* n2 = CreateNotification(2); + model()->Add(n2); + model()->MergeDataAndStartSyncing( + syncable::APP_NOTIFICATIONS, + SyncDataList(), + processor()); + + // Some adds and some deletes. + SyncChangeList changes; + changes.push_back(CreateSyncChange( + SyncChange::ACTION_ADD, CreateNotification(3))); + changes.push_back(CreateSyncChange( + SyncChange::ACTION_DELETE, CreateNotification(*n1))); + changes.push_back(CreateSyncChange( + SyncChange::ACTION_ADD, CreateNotification(4))); + + model()->ProcessSyncChanges(FROM_HERE, changes); + + EXPECT_EQ(3U, model()->GetAllSyncData(syncable::APP_NOTIFICATIONS).size()); + EXPECT_EQ(2, processor()->change_list_size()); +} + +// Process sync changes should return error if model association is not done. +TEST_F(AppNotificationManagerSyncTest, + ProcessSyncChangesErrorModelAssocNotDone) { + SyncChangeList changes; + + SyncError sync_error = model()->ProcessSyncChanges(FROM_HERE, changes); + EXPECT_TRUE(sync_error.IsSet()); + EXPECT_EQ(syncable::APP_NOTIFICATIONS, sync_error.type()); + EXPECT_EQ(0, processor()->change_list_size()); +} + +// Stop syncing sets state correctly. +TEST_F(AppNotificationManagerSyncTest, StopSyncing) { + EXPECT_FALSE(model()->sync_processor_); + EXPECT_FALSE(model()->models_associated_); + + model()->MergeDataAndStartSyncing( + syncable::APP_NOTIFICATIONS, + SyncDataList(), + processor()); + + EXPECT_TRUE(model()->sync_processor_); + EXPECT_TRUE(model()->models_associated_); + + model()->StopSyncing(syncable::APP_NOTIFICATIONS); + EXPECT_FALSE(model()->sync_processor_); + EXPECT_FALSE(model()->models_associated_); +} + +// Adds get pushed to sync but local only are skipped. +TEST_F(AppNotificationManagerSyncTest, AddsGetsSynced) { + model()->MergeDataAndStartSyncing( + syncable::APP_NOTIFICATIONS, + SyncDataList(), + processor()); + + AppNotification* n1 = CreateNotification(1); + model()->Add(n1); + AppNotification* n2 = CreateNotification(2); + model()->Add(n2); + AppNotification* n3 = CreateNotification(true, 2); + model()->Add(n3); + + EXPECT_EQ(2, processor()->change_list_size()); + EXPECT_TRUE(processor()->ContainsGuid(n1->guid())); + SyncChange c1 = processor()->GetChangeByGuid(n1->guid()); + AssertSyncChange(c1, SyncChange::ACTION_ADD, *n1); + SyncChange c2 = processor()->GetChangeByGuid(n2->guid()); + AssertSyncChange(c2, SyncChange::ACTION_ADD, *n2); +} + +// Clear all gets pushed to sync. +TEST_F(AppNotificationManagerSyncTest, ClearAllGetsSynced) { + const std::string& ext_id = "e1"; + scoped_ptr<AppNotification> n1(CreateNotification(false, 1, ext_id)); + scoped_ptr<AppNotification> n2(CreateNotification(false, 2, ext_id)); + scoped_ptr<AppNotification> n3(CreateNotification(false, 3, ext_id)); + scoped_ptr<AppNotification> n4(CreateNotification(4)); + + SyncDataList initial_data; + initial_data.push_back( + AppNotificationManager::CreateSyncDataFromNotification(*n1)); + initial_data.push_back( + AppNotificationManager::CreateSyncDataFromNotification(*n2)); + initial_data.push_back( + AppNotificationManager::CreateSyncDataFromNotification(*n3)); + initial_data.push_back( + AppNotificationManager::CreateSyncDataFromNotification(*n4)); + model()->MergeDataAndStartSyncing( + syncable::APP_NOTIFICATIONS, + initial_data, + processor()); + + model()->ClearAll(ext_id); + + EXPECT_EQ(3, processor()->change_list_size()); + EXPECT_TRUE(processor()->ContainsGuid(n1->guid())); + SyncChange c1 = processor()->GetChangeByGuid(n1->guid()); + AssertSyncChange(c1, SyncChange::ACTION_DELETE, *n1); + SyncChange c2 = processor()->GetChangeByGuid(n2->guid()); + AssertSyncChange(c2, SyncChange::ACTION_DELETE, *n2); + SyncChange c3 = processor()->GetChangeByGuid(n3->guid()); + AssertSyncChange(c3, SyncChange::ACTION_DELETE, *n3); +} diff --git a/chrome/browser/extensions/app_notification_manager_unittest.cc b/chrome/browser/extensions/app_notification_manager_unittest.cc index 37aa6c6..cdc2ea0 100644 --- a/chrome/browser/extensions/app_notification_manager_unittest.cc +++ b/chrome/browser/extensions/app_notification_manager_unittest.cc @@ -71,14 +71,18 @@ class AppNotificationManagerTest : public testing::Test { TEST_F(AppNotificationManagerTest, Simple) { std::string id = extension_test_util::MakeId("whatever"); AppNotificationList list; - util::AddNotifications(&list, 2, "foo"); - util::AddCopiesFromList(mgr_.get(), id, list); + util::AddNotifications(&list, id, 2, "foo"); + EXPECT_TRUE(util::AddCopiesFromList(mgr_.get(), list)); // Cause |mgr_| to be recreated, re-reading from its storage. InitializeManager(); const AppNotification* tmp = mgr_->GetLast(id); - EXPECT_TRUE(tmp && list[1]->Equals(*tmp)); + ASSERT_TRUE(tmp); + EXPECT_EQ(list[1]->guid(), tmp->guid()); + EXPECT_EQ(list[1]->extension_id(), tmp->extension_id()); + EXPECT_EQ(list[1]->is_local(), tmp->is_local()); + EXPECT_TRUE(list[1]->Equals(*tmp)); const AppNotificationList* tmp_list = mgr_->GetAll(id); ASSERT_TRUE(tmp_list != NULL); util::ExpectListsEqual(list, *tmp_list); @@ -101,10 +105,10 @@ TEST_F(AppNotificationManagerTest, MAYBE_ExtensionUninstall) { std::string id2 = extension_test_util::MakeId("id2"); AppNotificationList list1; AppNotificationList list2; - util::AddNotifications(&list1, 5, "foo1"); - util::AddNotifications(&list2, 3, "foo2"); - util::AddCopiesFromList(mgr_.get(), id1, list1); - util::AddCopiesFromList(mgr_.get(), id2, list2); + util::AddNotifications(&list1, id1, 5, "foo1"); + util::AddNotifications(&list2, id2, 3, "foo2"); + util::AddCopiesFromList(mgr_.get(), list1); + util::AddCopiesFromList(mgr_.get(), list2); util::ExpectListsEqual(list1, *mgr_->GetAll(id1)); util::ExpectListsEqual(list2, *mgr_->GetAll(id2)); diff --git a/chrome/browser/extensions/app_notification_storage_unittest.cc b/chrome/browser/extensions/app_notification_storage_unittest.cc index 84b836f..2fcb251 100644 --- a/chrome/browser/extensions/app_notification_storage_unittest.cc +++ b/chrome/browser/extensions/app_notification_storage_unittest.cc @@ -62,7 +62,7 @@ TEST_F(AppNotificationStorageTest, Basics) { // Add some items. AppNotificationList list; - util::AddNotifications(&list, 2, "whatever"); + util::AddNotifications(&list, id1, 2, "whatever"); EXPECT_TRUE(storage_->Set(id1, list)); EXPECT_TRUE(DatabaseExistsOnDisk()); @@ -82,12 +82,12 @@ TEST_F(AppNotificationStorageTest, MultipleExtensions) { // Add items for id1. AppNotificationList list1; - util::AddNotifications(&list1, 2, "one"); + util::AddNotifications(&list1, id1, 2, "one"); EXPECT_TRUE(storage_->Set(id1, list1)); // Add items for id2. AppNotificationList list2; - util::AddNotifications(&list2, 3, "two"); + util::AddNotifications(&list2, id2, 3, "two"); EXPECT_TRUE(storage_->Set(id2, list2)); // Verify the items are present @@ -124,8 +124,8 @@ TEST_F(AppNotificationStorageTest, ReplaceExisting) { std::string id = extension_test_util::MakeId("1"); AppNotificationList list1; AppNotificationList list2; - util::AddNotifications(&list1, 5, "one"); - util::AddNotifications(&list2, 7, "two"); + util::AddNotifications(&list1, id, 5, "one"); + util::AddNotifications(&list2, id, 7, "two"); // Put list1 in, then replace with list2 and verify we get list2 back. EXPECT_TRUE(storage_->Set(id, list1)); diff --git a/chrome/browser/extensions/app_notification_test_util.cc b/chrome/browser/extensions/app_notification_test_util.cc index a148e50..acef4e1 100644 --- a/chrome/browser/extensions/app_notification_test_util.cc +++ b/chrome/browser/extensions/app_notification_test_util.cc @@ -21,12 +21,15 @@ void ExpectListsEqual(const AppNotificationList& one, } void AddNotifications(AppNotificationList* list, + const std::string& extension_id, int count, - std::string prefix) { + const std::string& prefix) { for (int i = 0; i < count; i++) { + std::string guid = prefix + "_guid_" + IntToString(i); std::string title = prefix + "_title_" + IntToString(i); std::string body = prefix + "_body_" + IntToString(i); - AppNotification* item = new AppNotification(title, body); + AppNotification* item = new AppNotification( + true, guid, extension_id, title, body); if (i % 2 == 0) { item->set_link_url(GURL("http://www.example.com/" + prefix)); item->set_link_text(prefix + "_link_" + IntToString(i)); @@ -36,20 +39,23 @@ void AddNotifications(AppNotificationList* list, } AppNotification* CopyAppNotification(const AppNotification& source) { - AppNotification* copy = new AppNotification(source.title(), source.body()); + AppNotification* copy = new AppNotification( + source.is_local(), source.guid(), source.extension_id(), + source.title(), source.body()); copy->set_link_url(source.link_url()); copy->set_link_text(source.link_text()); return copy; } -void AddCopiesFromList(AppNotificationManager* manager, - const std::string& extension_id, +bool AddCopiesFromList(AppNotificationManager* manager, const AppNotificationList& list) { + bool result = true; for (AppNotificationList::const_iterator i = list.begin(); i != list.end(); ++i) { - manager->Add(extension_id, CopyAppNotification(*(i->get()))); + result = result && manager->Add(CopyAppNotification(*(i->get()))); } + return result; } } // namespace app_notification_test_util diff --git a/chrome/browser/extensions/app_notification_test_util.h b/chrome/browser/extensions/app_notification_test_util.h index 0f38e5a..d17d0b6 100644 --- a/chrome/browser/extensions/app_notification_test_util.h +++ b/chrome/browser/extensions/app_notification_test_util.h @@ -20,16 +20,15 @@ void ExpectListsEqual(const AppNotificationList& one, // Helper for inserting |count| dummy notifications with |prefix| in their // title and body into |list|. void AddNotifications(AppNotificationList* list, + const std::string& extension_id, int count, - std::string prefix); + const std::string& prefix); // Copy the contents of |source| into a new object. AppNotification* CopyAppNotification(const AppNotification& source); -// Adds a copy of each item in |list| to |manager| using |extension_id| as the -// extension id. -void AddCopiesFromList(AppNotificationManager* manager, - const std::string& extension_id, +// Adds a copy of each item in |list| to |manager|. +bool AddCopiesFromList(AppNotificationManager* manager, const AppNotificationList& list); } // namespace app_notification_test_util diff --git a/chrome/browser/extensions/extension_app_api.cc b/chrome/browser/extensions/extension_app_api.cc index d89526c..45e666a 100644 --- a/chrome/browser/extensions/extension_app_api.cc +++ b/chrome/browser/extensions/extension_app_api.cc @@ -45,7 +45,8 @@ bool AppNotifyFunction::RunImpl() { if (details->HasKey(kBodyTextKey)) EXTENSION_FUNCTION_VALIDATE(details->GetString(kBodyTextKey, &body)); - scoped_ptr<AppNotification> item(new AppNotification(title, body)); + scoped_ptr<AppNotification> item(new AppNotification( + true, "", id, title, body)); if (details->HasKey(kLinkUrlKey)) { std::string link_url; @@ -68,7 +69,7 @@ bool AppNotifyFunction::RunImpl() { AppNotificationManager* manager = profile()->GetExtensionService()->app_notification_manager(); - manager->Add(id, item.release()); + manager->Add(item.release()); NotificationService::current()->Notify( chrome::NOTIFICATION_APP_NOTIFICATION_STATE_CHANGED, diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index 8444fdd..e121b79 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -1273,6 +1273,7 @@ 'browser/event_disposition.cc', 'browser/event_disposition.h', 'browser/extensions/app_notification_manager_unittest.cc', + 'browser/extensions/app_notification_manager_sync_unittest.cc', 'browser/extensions/app_notification_storage_unittest.cc', 'browser/extensions/app_notification_test_util.cc', 'browser/extensions/app_notify_channel_setup_unittest.cc', diff --git a/chrome/common/chrome_notification_types.h b/chrome/common/chrome_notification_types.h index e6cee1b..4dfcd1b 100644 --- a/chrome/common/chrome_notification_types.h +++ b/chrome/common/chrome_notification_types.h @@ -538,6 +538,10 @@ enum NotificationType { // with the extension id of the app. NOTIFICATION_APP_NOTIFICATION_STATE_CHANGED, + // Finished loading app notification manager. + // The source is AppNotificationManager, and the details are NoDetails. + NOTIFICATION_APP_NOTIFICATION_MANAGER_LOADED, + // Component Updater ------------------------------------------------------- // Sent when the component updater starts doing update checks. If no |