// 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/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/metrics/histogram.h" #include "base/perftimer.h" #include "base/stl_util.h" #include "base/time.h" #include "chrome/browser/profiles/profile.h" #include "chrome/common/chrome_notification_types.h" #include "chrome/common/extensions/extension.h" #include "content/public/browser/notification_service.h" #include "sync/api/sync_error_factory.h" #include "sync/protocol/app_notification_specifics.pb.h" #include "sync/protocol/sync.pb.h" using content::BrowserThread; typedef std::map SyncDataMap; namespace extensions { namespace { class GuidComparator : public std::binary_function, std::string, bool> { public: bool operator() (linked_ptr notif, const std::string& guid) const { return notif->guid() == guid; } }; 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 syncer::SyncDataList& sync_data, SyncDataMap* data_map) { for (syncer::SyncDataList::const_iterator iter = sync_data.begin(); iter != sync_data.end(); ++iter) { (*data_map)[iter->GetSpecifics().app_notification().guid()] = *iter; } } } // namespace const unsigned int AppNotificationManager::kMaxNotificationPerApp = 5; AppNotificationManager::AppNotificationManager(Profile* profile) : profile_(profile), models_associated_(false), processing_syncer_changes_(false) { registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_UNINSTALLED, content::Source(profile_)); } void AppNotificationManager::Init() { base::FilePath storage_path = profile_->GetPath().AppendASCII("App Notifications"); load_timer_.reset(new PerfTimer()); BrowserThread::PostTask( BrowserThread::FILE, FROM_HERE, base::Bind(&AppNotificationManager::LoadOnFileThread, this, storage_path)); } bool AppNotificationSortPredicate(const linked_ptr a1, const linked_ptr a2) { return a1.get()->creation_time() < a2.get()->creation_time(); } bool AppNotificationManager::Add(AppNotification* item) { // Do this first since we own the incoming item and hence want to delete // it in error paths. linked_ptr linked_item(item); if (!loaded()) return false; const std::string& extension_id = item->extension_id(); AppNotificationList& list = GetAllInternal(extension_id); list.push_back(linked_item); SyncAddChange(*linked_item); std::sort(list.begin(), list.end(), AppNotificationSortPredicate); if (list.size() > AppNotificationManager::kMaxNotificationPerApp) { AppNotification* removed = list.begin()->get(); SyncRemoveChange(*removed); list.erase(list.begin()); } if (storage_.get()) { BrowserThread::PostTask( BrowserThread::FILE, FROM_HERE, base::Bind(&AppNotificationManager::SaveOnFileThread, this, extension_id, CopyAppNotificationList(list))); } content::NotificationService::current()->Notify( chrome::NOTIFICATION_APP_NOTIFICATION_STATE_CHANGED, content::Source(profile_), content::Details(&extension_id)); return true; } const AppNotificationList* AppNotificationManager::GetAll( const std::string& extension_id) const { if (!loaded()) return NULL; if (ContainsKey(*notifications_, extension_id)) return &((*notifications_)[extension_id]); return NULL; } const AppNotification* AppNotificationManager::GetLast( const std::string& extension_id) { if (!loaded()) return NULL; NotificationMap::iterator found = notifications_->find(extension_id); if (found == notifications_->end()) return NULL; const AppNotificationList& list = found->second; if (list.empty()) return NULL; return list.rbegin()->get(); } void AppNotificationManager::ClearAll(const std::string& extension_id) { if (!loaded()) return; 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)); } content::NotificationService::current()->Notify( chrome::NOTIFICATION_APP_NOTIFICATION_STATE_CHANGED, content::Source(profile_), content::Details(&extension_id)); } void AppNotificationManager::Observe( int type, const content::NotificationSource& source, const content::NotificationDetails& details) { CHECK(type == chrome::NOTIFICATION_EXTENSION_UNINSTALLED); ClearAll(content::Details(details).ptr()->id()); } syncer::SyncDataList AppNotificationManager::GetAllSyncData( syncer::ModelType type) const { CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(loaded()); DCHECK_EQ(syncer::APP_NOTIFICATIONS, type); syncer::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; } syncer::SyncError AppNotificationManager::ProcessSyncChanges( const tracked_objects::Location& from_here, const syncer::SyncChangeList& change_list) { CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(loaded()); if (!models_associated_) { return sync_error_factory_->CreateAndUploadError( FROM_HERE, "Models not yet associated."); } base::AutoReset processing_changes(&processing_syncer_changes_, true); syncer::SyncError error; for (syncer::SyncChangeList::const_iterator iter = change_list.begin(); iter != change_list.end(); ++iter) { syncer::SyncData sync_data = iter->sync_data(); DCHECK_EQ(syncer::APP_NOTIFICATIONS, sync_data.GetDataType()); syncer::SyncChange::SyncChangeType change_type = iter->change_type(); scoped_ptr 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 = sync_error_factory_->CreateAndUploadError( FROM_HERE, "ProcessSyncChanges received a local only notification" + syncer::SyncChange::ChangeTypeToString(change_type)); continue; } switch (change_type) { case syncer::SyncChange::ACTION_ADD: if (!existing_notif) { Add(new_notif.release()); } else { DLOG(ERROR) << "Got ADD change for an existing item.\n" << "Existing item: " << existing_notif->ToString() << "\nItem in ADD change: " << new_notif->ToString(); } break; case syncer::SyncChange::ACTION_DELETE: if (existing_notif) { Remove(new_notif->extension_id(), new_notif->guid()); } else { // This should never happen. But we are seeting this sometimes, and // it stops all of sync. See bug http://crbug.com/108088 // So until we figure out the root cause, log an error and ignore. DLOG(ERROR) << "Got DELETE change for non-existing item.\n" << "Item in DELETE change: " << new_notif->ToString(); } break; case syncer::SyncChange::ACTION_UPDATE: // Although app notifications are immutable from the model perspective, // sync can send UPDATE changes due to encryption / meta-data changes. // So ignore UPDATE changes when the exitsing and new notification // objects are the same. Log an error otherwise. if (!existing_notif) { DLOG(ERROR) << "Got UPDATE change for non-existing item." << "Item in UPDATE change: " << new_notif->ToString(); } else if (!existing_notif->Equals(*new_notif)) { DLOG(ERROR) << "Got invalid UPDATE change:" << "New and existing notifications should be the same.\n" << "Existing item: " << existing_notif->ToString() << "\n" << "Item in UPDATE change: " << new_notif->ToString(); } break; default: break; } } return error; } syncer::SyncMergeResult AppNotificationManager::MergeDataAndStartSyncing( syncer::ModelType type, const syncer::SyncDataList& initial_sync_data, scoped_ptr sync_processor, scoped_ptr sync_error_factory) { CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); syncer::SyncMergeResult merge_result(type); // 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, syncer::APP_NOTIFICATIONS); DCHECK(!sync_processor_.get()); DCHECK(sync_processor.get()); DCHECK(sync_error_factory.get()); sync_processor_ = sync_processor.Pass(); sync_error_factory_ = sync_error_factory.Pass(); // We may add, or remove notifications here, so ensure we don't step on // our own toes. base::AutoReset processing_changes(&processing_syncer_changes_, true); SyncDataMap local_data_map; PopulateGuidToSyncDataMap(GetAllSyncData(syncer::APP_NOTIFICATIONS), &local_data_map); for (syncer::SyncDataList::const_iterator iter = initial_sync_data.begin(); iter != initial_sync_data.end(); ++iter) { const syncer::SyncData& sync_data = *iter; DCHECK_EQ(syncer::APP_NOTIFICATIONS, sync_data.GetDataType()); scoped_ptr 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)) { merge_result.set_error(sync_error_factory_->CreateAndUploadError( FROM_HERE, "MergeDataAndStartSyncing failed: local notification and sync " "notification have same guid but different data.")); return merge_result; } } 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. syncer::SyncChangeList new_changes; for (SyncDataMap::const_iterator iter = local_data_map.begin(); iter != local_data_map.end(); ++iter) { new_changes.push_back( syncer::SyncChange(FROM_HERE, syncer::SyncChange::ACTION_ADD, iter->second)); } if (new_changes.size() > 0) { merge_result.set_error( sync_processor_->ProcessSyncChanges(FROM_HERE, new_changes)); } models_associated_ = !merge_result.error().IsSet(); return merge_result; } void AppNotificationManager::StopSyncing(syncer::ModelType type) { DCHECK_EQ(type, syncer::APP_NOTIFICATIONS); models_associated_ = false; sync_processor_.reset(); sync_error_factory_.reset(); } AppNotificationManager::~AppNotificationManager() { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); // Post a task to delete our storage on the file thread. BrowserThread::DeleteSoon(BrowserThread::FILE, FROM_HERE, storage_.release()); } void AppNotificationManager::LoadOnFileThread( const base::FilePath& storage_path) { PerfTimer timer; CHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); DCHECK(!loaded()); storage_.reset(AppNotificationStorage::Create(storage_path)); if (!storage_.get()) return; scoped_ptr result(new NotificationMap()); std::set ids; if (!storage_->GetExtensionIds(&ids)) return; std::set::const_iterator i; for (i = ids.begin(); i != ids.end(); ++i) { const std::string& id = *i; AppNotificationList& list = (*result)[id]; if (!storage_->Get(id, &list)) result->erase(id); } BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, base::Bind(&AppNotificationManager::HandleLoadResults, this, result.release())); UMA_HISTOGRAM_LONG_TIMES("AppNotification.MgrFileThreadLoadTime", timer.Elapsed()); } void AppNotificationManager::HandleLoadResults(NotificationMap* map) { CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); DCHECK(map); DCHECK(!loaded()); notifications_.reset(map); UMA_HISTOGRAM_LONG_TIMES("AppNotification.MgrLoadDelay", load_timer_->Elapsed()); load_timer_.reset(); // Generate STATE_CHANGED notifications for extensions that have at // least one notification loaded. int app_count = 0; int notification_count = 0; NotificationMap::const_iterator i; for (i = map->begin(); i != map->end(); ++i) { const std::string& id = i->first; if (i->second.empty()) continue; app_count++; notification_count += i->second.size(); content::NotificationService::current()->Notify( chrome::NOTIFICATION_APP_NOTIFICATION_STATE_CHANGED, content::Source(profile_), content::Details(&id)); } UMA_HISTOGRAM_COUNTS("AppNotification.MgrLoadAppCount", app_count); UMA_HISTOGRAM_COUNTS("AppNotification.MgrLoadTotalCount", notification_count); // Generate MANAGER_LOADED notification. content::NotificationService::current()->Notify( chrome::NOTIFICATION_APP_NOTIFICATION_MANAGER_LOADED, content::Source(this), content::NotificationService::NoDetails()); } void AppNotificationManager::SaveOnFileThread(const std::string& extension_id, AppNotificationList* list) { // Own the |list|. scoped_ptr scoped_list(list); CHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); storage_->Set(extension_id, *scoped_list); } void AppNotificationManager::DeleteOnFileThread( const std::string& extension_id) { CHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE)); storage_->Delete(extension_id); } 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; } 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, CopyAppNotificationList(list))); } content::NotificationService::current()->Notify( chrome::NOTIFICATION_APP_NOTIFICATION_STATE_CHANGED, content::Source(profile_), content::Details(&extension_id)); } 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::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. syncer::SyncChangeList changes; syncer::SyncData sync_data = CreateSyncDataFromNotification(notif); changes.push_back( syncer::SyncChange(FROM_HERE, syncer::SyncChange::ACTION_ADD, sync_data)); sync_processor_->ProcessSyncChanges(FROM_HERE, changes); } void AppNotificationManager::SyncRemoveChange(const AppNotification& notif) { // Skip if either: // - Sync is not enabled by user. // - Change is generated from within the manager. if (notif.is_local() || !models_associated_) { return; } syncer::SyncChangeList changes; syncer::SyncData sync_data = CreateSyncDataFromNotification(notif); changes.push_back( syncer::SyncChange(FROM_HERE, syncer::SyncChange::ACTION_DELETE, 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; syncer::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(syncer::SyncChange( FROM_HERE, syncer::SyncChange::ACTION_DELETE, CreateSyncDataFromNotification(notif))); } sync_processor_->ProcessSyncChanges(FROM_HERE, changes); } // static syncer::SyncData AppNotificationManager::CreateSyncDataFromNotification( const AppNotification& notification) { DCHECK(!notification.is_local()); sync_pb::EntitySpecifics specifics; sync_pb::AppNotification* notif_specifics = specifics.mutable_app_notification(); notif_specifics->set_app_id(notification.extension_id()); notif_specifics->set_creation_timestamp_ms( notification.creation_time().ToInternalValue()); 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 syncer::SyncData::CreateLocalData( notif_specifics->guid(), notif_specifics->app_id(), specifics); } // static AppNotification* AppNotificationManager::CreateNotificationFromSyncData( const syncer::SyncData& sync_data) { sync_pb::AppNotification specifics = sync_data.GetSpecifics().app_notification(); // Check for mandatory fields. if (!specifics.has_app_id() || !specifics.has_guid() || !specifics.has_title() || !specifics.has_body_text() || !specifics.has_creation_timestamp_ms()) { return NULL; } AppNotification* notification = new AppNotification( false, base::Time::FromInternalValue(specifics.creation_timestamp_ms()), 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; } } // namespace extensions