diff options
Diffstat (limited to 'chromium/extensions/browser/api/alarms')
-rw-r--r-- | chromium/extensions/browser/api/alarms/OWNERS | 1 | ||||
-rw-r--r-- | chromium/extensions/browser/api/alarms/alarm_manager.cc | 473 | ||||
-rw-r--r-- | chromium/extensions/browser/api/alarms/alarm_manager.h | 246 | ||||
-rw-r--r-- | chromium/extensions/browser/api/alarms/alarms_api.cc | 204 | ||||
-rw-r--r-- | chromium/extensions/browser/api/alarms/alarms_api.h | 93 | ||||
-rw-r--r-- | chromium/extensions/browser/api/alarms/alarms_api_unittest.cc | 700 |
6 files changed, 1717 insertions, 0 deletions
diff --git a/chromium/extensions/browser/api/alarms/OWNERS b/chromium/extensions/browser/api/alarms/OWNERS new file mode 100644 index 00000000000..39cb68cb187 --- /dev/null +++ b/chromium/extensions/browser/api/alarms/OWNERS @@ -0,0 +1 @@ +mpcomplete@chromium.org diff --git a/chromium/extensions/browser/api/alarms/alarm_manager.cc b/chromium/extensions/browser/api/alarms/alarm_manager.cc new file mode 100644 index 00000000000..0616481fc75 --- /dev/null +++ b/chromium/extensions/browser/api/alarms/alarm_manager.cc @@ -0,0 +1,473 @@ +// 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 "extensions/browser/api/alarms/alarm_manager.h" + +#include <stddef.h> + +#include <utility> + +#include "base/bind.h" +#include "base/json/json_writer.h" +#include "base/lazy_instance.h" +#include "base/message_loop/message_loop.h" +#include "base/time/clock.h" +#include "base/time/default_clock.h" +#include "base/time/time.h" +#include "base/value_conversions.h" +#include "base/values.h" +#include "extensions/browser/event_router.h" +#include "extensions/browser/extension_registry.h" +#include "extensions/browser/extension_system.h" +#include "extensions/browser/state_store.h" +#include "extensions/common/api/alarms.h" + +namespace extensions { + +namespace alarms = api::alarms; + +namespace { + +// A list of alarms that this extension has set. +const char kRegisteredAlarms[] = "alarms"; +const char kAlarmGranularity[] = "granularity"; + +// The minimum period between polling for alarms to run. +const base::TimeDelta kDefaultMinPollPeriod() { + return base::TimeDelta::FromDays(1); +} + +class DefaultAlarmDelegate : public AlarmManager::Delegate { + public: + explicit DefaultAlarmDelegate(content::BrowserContext* context) + : browser_context_(context) {} + ~DefaultAlarmDelegate() override {} + + void OnAlarm(const std::string& extension_id, const Alarm& alarm) override { + scoped_ptr<base::ListValue> args(new base::ListValue()); + args->Append(alarm.js_alarm->ToValue().release()); + scoped_ptr<Event> event(new Event( + events::ALARMS_ON_ALARM, alarms::OnAlarm::kEventName, std::move(args))); + EventRouter::Get(browser_context_) + ->DispatchEventToExtension(extension_id, std::move(event)); + } + + private: + content::BrowserContext* browser_context_; +}; + +// Creates a TimeDelta from a delay as specified in the API. +base::TimeDelta TimeDeltaFromDelay(double delay_in_minutes) { + return base::TimeDelta::FromMicroseconds(delay_in_minutes * + base::Time::kMicrosecondsPerMinute); +} + +std::vector<Alarm> AlarmsFromValue(const base::ListValue* list) { + std::vector<Alarm> alarms; + for (size_t i = 0; i < list->GetSize(); ++i) { + const base::DictionaryValue* alarm_dict = NULL; + Alarm alarm; + if (list->GetDictionary(i, &alarm_dict) && + alarms::Alarm::Populate(*alarm_dict, alarm.js_alarm.get())) { + const base::Value* time_value = NULL; + if (alarm_dict->Get(kAlarmGranularity, &time_value)) + base::GetValueAsTimeDelta(*time_value, &alarm.granularity); + alarms.push_back(alarm); + } + } + return alarms; +} + +scoped_ptr<base::ListValue> AlarmsToValue(const std::vector<Alarm>& alarms) { + scoped_ptr<base::ListValue> list(new base::ListValue()); + for (size_t i = 0; i < alarms.size(); ++i) { + scoped_ptr<base::DictionaryValue> alarm = alarms[i].js_alarm->ToValue(); + alarm->Set(kAlarmGranularity, + base::CreateTimeDeltaValue(alarms[i].granularity)); + list->Append(alarm.release()); + } + return list; +} + +} // namespace + +// AlarmManager + +AlarmManager::AlarmManager(content::BrowserContext* context) + : browser_context_(context), + clock_(new base::DefaultClock()), + delegate_(new DefaultAlarmDelegate(context)), + extension_registry_observer_(this) { + extension_registry_observer_.Add(ExtensionRegistry::Get(browser_context_)); + + StateStore* storage = ExtensionSystem::Get(browser_context_)->state_store(); + if (storage) + storage->RegisterKey(kRegisteredAlarms); +} + +AlarmManager::~AlarmManager() { +} + +void AlarmManager::AddAlarm(const std::string& extension_id, + const Alarm& alarm, + const AddAlarmCallback& callback) { + RunWhenReady(extension_id, base::Bind(&AlarmManager::AddAlarmWhenReady, + AsWeakPtr(), alarm, callback)); +} + +void AlarmManager::GetAlarm(const std::string& extension_id, + const std::string& name, + const GetAlarmCallback& callback) { + RunWhenReady(extension_id, base::Bind(&AlarmManager::GetAlarmWhenReady, + AsWeakPtr(), name, callback)); +} + +void AlarmManager::GetAllAlarms(const std::string& extension_id, + const GetAllAlarmsCallback& callback) { + RunWhenReady(extension_id, base::Bind(&AlarmManager::GetAllAlarmsWhenReady, + AsWeakPtr(), callback)); +} + +void AlarmManager::RemoveAlarm(const std::string& extension_id, + const std::string& name, + const RemoveAlarmCallback& callback) { + RunWhenReady(extension_id, base::Bind(&AlarmManager::RemoveAlarmWhenReady, + AsWeakPtr(), name, callback)); +} + +void AlarmManager::RemoveAllAlarms(const std::string& extension_id, + const RemoveAllAlarmsCallback& callback) { + RunWhenReady(extension_id, base::Bind(&AlarmManager::RemoveAllAlarmsWhenReady, + AsWeakPtr(), callback)); +} + +void AlarmManager::AddAlarmWhenReady(const Alarm& alarm, + const AddAlarmCallback& callback, + const std::string& extension_id) { + AddAlarmImpl(extension_id, alarm); + WriteToStorage(extension_id); + callback.Run(); +} + +void AlarmManager::GetAlarmWhenReady(const std::string& name, + const GetAlarmCallback& callback, + const std::string& extension_id) { + AlarmIterator it = GetAlarmIterator(extension_id, name); + callback.Run(it.first != alarms_.end() ? &*it.second : NULL); +} + +void AlarmManager::GetAllAlarmsWhenReady(const GetAllAlarmsCallback& callback, + const std::string& extension_id) { + AlarmMap::iterator list = alarms_.find(extension_id); + callback.Run(list != alarms_.end() ? &list->second : NULL); +} + +void AlarmManager::RemoveAlarmWhenReady(const std::string& name, + const RemoveAlarmCallback& callback, + const std::string& extension_id) { + AlarmIterator it = GetAlarmIterator(extension_id, name); + if (it.first == alarms_.end()) { + callback.Run(false); + return; + } + + RemoveAlarmIterator(it); + WriteToStorage(extension_id); + callback.Run(true); +} + +void AlarmManager::RemoveAllAlarmsWhenReady( + const RemoveAllAlarmsCallback& callback, + const std::string& extension_id) { + AlarmMap::iterator list = alarms_.find(extension_id); + if (list != alarms_.end()) { + // Note: I'm using indices rather than iterators here because + // RemoveAlarmIterator will delete the list when it becomes empty. + for (size_t i = 0, size = list->second.size(); i < size; ++i) + RemoveAlarmIterator(AlarmIterator(list, list->second.begin())); + + CHECK(alarms_.find(extension_id) == alarms_.end()); + WriteToStorage(extension_id); + } + callback.Run(); +} + +AlarmManager::AlarmIterator AlarmManager::GetAlarmIterator( + const std::string& extension_id, + const std::string& name) { + AlarmMap::iterator list = alarms_.find(extension_id); + if (list == alarms_.end()) + return make_pair(alarms_.end(), AlarmList::iterator()); + + for (AlarmList::iterator it = list->second.begin(); it != list->second.end(); + ++it) { + if (it->js_alarm->name == name) + return make_pair(list, it); + } + + return make_pair(alarms_.end(), AlarmList::iterator()); +} + +void AlarmManager::SetClockForTesting(base::Clock* clock) { + clock_.reset(clock); +} + +static base::LazyInstance<BrowserContextKeyedAPIFactory<AlarmManager>> + g_factory = LAZY_INSTANCE_INITIALIZER; + +// static +BrowserContextKeyedAPIFactory<AlarmManager>* +AlarmManager::GetFactoryInstance() { + return g_factory.Pointer(); +} + +// static +AlarmManager* AlarmManager::Get(content::BrowserContext* browser_context) { + return BrowserContextKeyedAPIFactory<AlarmManager>::Get(browser_context); +} + +void AlarmManager::RemoveAlarmIterator(const AlarmIterator& iter) { + AlarmList& list = iter.first->second; + list.erase(iter.second); + if (list.empty()) + alarms_.erase(iter.first); + + // Cancel the timer if there are no more alarms. + // We don't need to reschedule the poll otherwise, because in + // the worst case we would just poll one extra time. + if (alarms_.empty()) { + timer_.Stop(); + next_poll_time_ = base::Time(); + } +} + +void AlarmManager::OnAlarm(AlarmIterator it) { + CHECK(it.first != alarms_.end()); + Alarm& alarm = *it.second; + std::string extension_id_copy(it.first->first); + delegate_->OnAlarm(extension_id_copy, alarm); + + // Update our scheduled time for the next alarm. + if (double* period_in_minutes = alarm.js_alarm->period_in_minutes.get()) { + // Get the timer's delay in JS time (i.e., convert it from minutes to + // milliseconds). + double period_in_js_time = *period_in_minutes * + base::Time::kMicrosecondsPerMinute / + base::Time::kMicrosecondsPerMillisecond; + // Find out how many periods have transpired since the alarm last went off + // (it's possible that we missed some). + int transpired_periods = + (last_poll_time_.ToJsTime() - alarm.js_alarm->scheduled_time) / + period_in_js_time; + // Schedule the alarm for the next period that is in-line with the original + // scheduling. + alarm.js_alarm->scheduled_time += + period_in_js_time * (transpired_periods + 1); + } else { + RemoveAlarmIterator(it); + } + WriteToStorage(extension_id_copy); +} + +void AlarmManager::AddAlarmImpl(const std::string& extension_id, + const Alarm& alarm) { + // Override any old alarm with the same name. + AlarmIterator old_alarm = + GetAlarmIterator(extension_id, alarm.js_alarm->name); + if (old_alarm.first != alarms_.end()) + RemoveAlarmIterator(old_alarm); + + alarms_[extension_id].push_back(alarm); + base::Time alarm_time = + base::Time::FromJsTime(alarm.js_alarm->scheduled_time); + if (next_poll_time_.is_null() || alarm_time < next_poll_time_) + SetNextPollTime(alarm_time); +} + +void AlarmManager::WriteToStorage(const std::string& extension_id) { + StateStore* storage = ExtensionSystem::Get(browser_context_)->state_store(); + if (!storage) + return; + + scoped_ptr<base::Value> alarms; + AlarmMap::iterator list = alarms_.find(extension_id); + if (list != alarms_.end()) + alarms.reset(AlarmsToValue(list->second).release()); + else + alarms.reset(AlarmsToValue(std::vector<Alarm>()).release()); + storage->SetExtensionValue(extension_id, kRegisteredAlarms, + std::move(alarms)); +} + +void AlarmManager::ReadFromStorage(const std::string& extension_id, + scoped_ptr<base::Value> value) { + base::ListValue* list = NULL; + if (value.get() && value->GetAsList(&list)) { + std::vector<Alarm> alarm_states = AlarmsFromValue(list); + for (size_t i = 0; i < alarm_states.size(); ++i) + AddAlarmImpl(extension_id, alarm_states[i]); + } + + ReadyQueue& extension_ready_queue = ready_actions_[extension_id]; + while (!extension_ready_queue.empty()) { + extension_ready_queue.front().Run(extension_id); + extension_ready_queue.pop(); + } + ready_actions_.erase(extension_id); +} + +void AlarmManager::SetNextPollTime(const base::Time& time) { + next_poll_time_ = time; + timer_.Start(FROM_HERE, + std::max(base::TimeDelta::FromSeconds(0), time - clock_->Now()), + this, &AlarmManager::PollAlarms); +} + +void AlarmManager::ScheduleNextPoll() { + // If there are no alarms, stop the timer. + if (alarms_.empty()) { + timer_.Stop(); + next_poll_time_ = base::Time(); + return; + } + + // Find the soonest alarm that is scheduled to run and the smallest + // granularity of any alarm. + // alarms_ guarantees that none of its contained lists are empty. + base::Time soonest_alarm_time = base::Time::FromJsTime( + alarms_.begin()->second.begin()->js_alarm->scheduled_time); + base::TimeDelta min_granularity = kDefaultMinPollPeriod(); + for (AlarmMap::const_iterator m_it = alarms_.begin(), m_end = alarms_.end(); + m_it != m_end; ++m_it) { + for (AlarmList::const_iterator l_it = m_it->second.begin(); + l_it != m_it->second.end(); ++l_it) { + base::Time cur_alarm_time = + base::Time::FromJsTime(l_it->js_alarm->scheduled_time); + if (cur_alarm_time < soonest_alarm_time) + soonest_alarm_time = cur_alarm_time; + if (l_it->granularity < min_granularity) + min_granularity = l_it->granularity; + base::TimeDelta cur_alarm_delta = cur_alarm_time - last_poll_time_; + if (cur_alarm_delta < l_it->minimum_granularity) + cur_alarm_delta = l_it->minimum_granularity; + if (cur_alarm_delta < min_granularity) + min_granularity = cur_alarm_delta; + } + } + + base::Time next_poll(last_poll_time_ + min_granularity); + // If the next alarm is more than min_granularity in the future, wait for it. + // Otherwise, only poll as often as min_granularity. + // As a special case, if we've never checked for an alarm before + // (e.g. during startup), let alarms fire asap. + if (last_poll_time_.is_null() || next_poll < soonest_alarm_time) + next_poll = soonest_alarm_time; + + // Schedule the poll. + SetNextPollTime(next_poll); +} + +void AlarmManager::PollAlarms() { + last_poll_time_ = clock_->Now(); + + // Run any alarms scheduled in the past. OnAlarm uses vector::erase to remove + // elements from the AlarmList, and map::erase to remove AlarmLists from the + // AlarmMap. + for (AlarmMap::iterator m_it = alarms_.begin(), m_end = alarms_.end(); + m_it != m_end;) { + AlarmMap::iterator cur_extension = m_it++; + + // Iterate (a) backwards so that removing elements doesn't affect + // upcoming iterations, and (b) with indices so that if the last + // iteration destroys the AlarmList, I'm not about to use the end + // iterator that the destruction invalidates. + for (size_t i = cur_extension->second.size(); i > 0; --i) { + AlarmList::iterator cur_alarm = cur_extension->second.begin() + i - 1; + if (base::Time::FromJsTime(cur_alarm->js_alarm->scheduled_time) <= + last_poll_time_) { + OnAlarm(make_pair(cur_extension, cur_alarm)); + } + } + } + + ScheduleNextPoll(); +} + +static void RemoveAllOnUninstallCallback() { +} + +void AlarmManager::RunWhenReady(const std::string& extension_id, + const ReadyAction& action) { + ReadyMap::iterator it = ready_actions_.find(extension_id); + + if (it == ready_actions_.end()) + action.Run(extension_id); + else + it->second.push(action); +} + +void AlarmManager::OnExtensionLoaded(content::BrowserContext* browser_context, + const Extension* extension) { + StateStore* storage = ExtensionSystem::Get(browser_context_)->state_store(); + if (storage) { + ready_actions_.insert(ReadyMap::value_type(extension->id(), ReadyQueue())); + storage->GetExtensionValue(extension->id(), kRegisteredAlarms, + base::Bind(&AlarmManager::ReadFromStorage, + AsWeakPtr(), extension->id())); + } +} + +void AlarmManager::OnExtensionUninstalled( + content::BrowserContext* browser_context, + const Extension* extension, + extensions::UninstallReason reason) { + RemoveAllAlarms(extension->id(), base::Bind(RemoveAllOnUninstallCallback)); +} + +// AlarmManager::Alarm + +Alarm::Alarm() : js_alarm(new alarms::Alarm()) { +} + +Alarm::Alarm(const std::string& name, + const alarms::AlarmCreateInfo& create_info, + base::TimeDelta min_granularity, + base::Time now) + : js_alarm(new alarms::Alarm()) { + js_alarm->name = name; + minimum_granularity = min_granularity; + + if (create_info.when.get()) { + // Absolute scheduling. + js_alarm->scheduled_time = *create_info.when; + granularity = base::Time::FromJsTime(js_alarm->scheduled_time) - now; + } else { + // Relative scheduling. + double* delay_in_minutes = create_info.delay_in_minutes.get(); + if (delay_in_minutes == NULL) + delay_in_minutes = create_info.period_in_minutes.get(); + CHECK(delay_in_minutes != NULL) + << "ValidateAlarmCreateInfo in alarms_api.cc should have " + << "prevented this call."; + base::TimeDelta delay = TimeDeltaFromDelay(*delay_in_minutes); + js_alarm->scheduled_time = (now + delay).ToJsTime(); + granularity = delay; + } + + if (granularity < min_granularity) + granularity = min_granularity; + + // Check for repetition. + if (create_info.period_in_minutes.get()) { + js_alarm->period_in_minutes.reset( + new double(*create_info.period_in_minutes)); + } +} + +Alarm::Alarm(const Alarm& other) = default; + +Alarm::~Alarm() { +} + +} // namespace extensions diff --git a/chromium/extensions/browser/api/alarms/alarm_manager.h b/chromium/extensions/browser/api/alarms/alarm_manager.h new file mode 100644 index 00000000000..66e5fb105d5 --- /dev/null +++ b/chromium/extensions/browser/api/alarms/alarm_manager.h @@ -0,0 +1,246 @@ +// 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. + +#ifndef EXTENSIONS_BROWSER_API_ALARMS_ALARM_MANAGER_H_ +#define EXTENSIONS_BROWSER_API_ALARMS_ALARM_MANAGER_H_ + +#include <map> +#include <queue> +#include <string> +#include <vector> + +#include "base/callback.h" +#include "base/gtest_prod_util.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/scoped_observer.h" +#include "base/timer/timer.h" +#include "extensions/browser/browser_context_keyed_api_factory.h" +#include "extensions/browser/extension_registry_observer.h" +#include "extensions/common/api/alarms.h" + +namespace base { +class Clock; +} // namespace base + +namespace content { +class BrowserContext; +} // namespace content + +namespace extensions { +class ExtensionAlarmsSchedulingTest; +class ExtensionRegistry; + +struct Alarm { + Alarm(); + Alarm(const std::string& name, + const api::alarms::AlarmCreateInfo& create_info, + base::TimeDelta min_granularity, + base::Time now); + Alarm(const Alarm& other); + ~Alarm(); + + linked_ptr<api::alarms::Alarm> js_alarm; + // The granularity isn't exposed to the extension's javascript, but we poll at + // least as often as the shortest alarm's granularity. It's initialized as + // the relative delay requested in creation, even if creation uses an absolute + // time. This will always be at least as large as the min_granularity + // constructor argument. + base::TimeDelta granularity; + // The minimum granularity is the minimum allowed polling rate. This stops + // alarms from polling too often. + base::TimeDelta minimum_granularity; +}; + +// Manages the currently pending alarms for every extension in a profile. +// There is one manager per virtual Profile. +class AlarmManager : public BrowserContextKeyedAPI, + public ExtensionRegistryObserver, + public base::SupportsWeakPtr<AlarmManager> { + public: + typedef std::vector<Alarm> AlarmList; + + class Delegate { + public: + virtual ~Delegate() {} + // Called when an alarm fires. + virtual void OnAlarm(const std::string& extension_id, + const Alarm& alarm) = 0; + }; + + explicit AlarmManager(content::BrowserContext* context); + ~AlarmManager() override; + + // Override the default delegate. Callee assumes onwership. Used for testing. + void set_delegate(Delegate* delegate) { delegate_.reset(delegate); } + + typedef base::Callback<void()> AddAlarmCallback; + // Adds |alarm| for the given extension, and starts the timer. Invokes + // |callback| when done. + void AddAlarm(const std::string& extension_id, + const Alarm& alarm, + const AddAlarmCallback& callback); + + typedef base::Callback<void(Alarm*)> GetAlarmCallback; + // Passes the alarm with the given name, or NULL if none exists, to + // |callback|. + void GetAlarm(const std::string& extension_id, + const std::string& name, + const GetAlarmCallback& callback); + + typedef base::Callback<void(const AlarmList*)> GetAllAlarmsCallback; + // Passes the list of pending alarms for the given extension, or + // NULL if none exist, to |callback|. + void GetAllAlarms(const std::string& extension_id, + const GetAllAlarmsCallback& callback); + + typedef base::Callback<void(bool)> RemoveAlarmCallback; + // Cancels and removes the alarm with the given name. Invokes |callback| when + // done. + void RemoveAlarm(const std::string& extension_id, + const std::string& name, + const RemoveAlarmCallback& callback); + + typedef base::Callback<void()> RemoveAllAlarmsCallback; + // Cancels and removes all alarms for the given extension. Invokes |callback| + // when done. + void RemoveAllAlarms(const std::string& extension_id, + const RemoveAllAlarmsCallback& callback); + + // Replaces AlarmManager's owned clock with |clock| and takes ownership of it. + void SetClockForTesting(base::Clock* clock); + + // BrowserContextKeyedAPI implementation. + static BrowserContextKeyedAPIFactory<AlarmManager>* GetFactoryInstance(); + + // Convenience method to get the AlarmManager for a content::BrowserContext. + static AlarmManager* Get(content::BrowserContext* browser_context); + + private: + friend void RunScheduleNextPoll(AlarmManager*); + friend class ExtensionAlarmsSchedulingTest; + FRIEND_TEST_ALL_PREFIXES(ExtensionAlarmsSchedulingTest, PollScheduling); + FRIEND_TEST_ALL_PREFIXES(ExtensionAlarmsSchedulingTest, + ReleasedExtensionPollsInfrequently); + FRIEND_TEST_ALL_PREFIXES(ExtensionAlarmsSchedulingTest, TimerRunning); + FRIEND_TEST_ALL_PREFIXES(ExtensionAlarmsSchedulingTest, MinimumGranularity); + FRIEND_TEST_ALL_PREFIXES(ExtensionAlarmsSchedulingTest, + DifferentMinimumGranularities); + FRIEND_TEST_ALL_PREFIXES(ExtensionAlarmsSchedulingTest, + RepeatingAlarmsScheduledPredictably); + friend class BrowserContextKeyedAPIFactory<AlarmManager>; + + typedef std::string ExtensionId; + typedef std::map<ExtensionId, AlarmList> AlarmMap; + + typedef base::Callback<void(const std::string&)> ReadyAction; + typedef std::queue<ReadyAction> ReadyQueue; + typedef std::map<ExtensionId, ReadyQueue> ReadyMap; + + // Iterator used to identify a particular alarm within the Map/List pair. + // "Not found" is represented by <alarms_.end(), invalid_iterator>. + typedef std::pair<AlarmMap::iterator, AlarmList::iterator> AlarmIterator; + + // Part of AddAlarm that is executed after alarms are loaded. + void AddAlarmWhenReady(const Alarm& alarm, + const AddAlarmCallback& callback, + const std::string& extension_id); + + // Part of GetAlarm that is executed after alarms are loaded. + void GetAlarmWhenReady(const std::string& name, + const GetAlarmCallback& callback, + const std::string& extension_id); + + // Part of GetAllAlarms that is executed after alarms are loaded. + void GetAllAlarmsWhenReady(const GetAllAlarmsCallback& callback, + const std::string& extension_id); + + // Part of RemoveAlarm that is executed after alarms are loaded. + void RemoveAlarmWhenReady(const std::string& name, + const RemoveAlarmCallback& callback, + const std::string& extension_id); + + // Part of RemoveAllAlarms that is executed after alarms are loaded. + void RemoveAllAlarmsWhenReady(const RemoveAllAlarmsCallback& callback, + const std::string& extension_id); + + // Helper to return the iterators within the AlarmMap and AlarmList for the + // matching alarm, or an iterator to the end of the AlarmMap if none were + // found. + AlarmIterator GetAlarmIterator(const std::string& extension_id, + const std::string& name); + + // Helper to cancel and remove the alarm at the given iterator. The iterator + // must be valid. + void RemoveAlarmIterator(const AlarmIterator& iter); + + // Callback for when an alarm fires. + void OnAlarm(AlarmIterator iter); + + // Internal helper to add an alarm and start the timer with the given delay. + void AddAlarmImpl(const std::string& extension_id, const Alarm& alarm); + + // Syncs our alarm data for the given extension to/from the state storage. + void WriteToStorage(const std::string& extension_id); + void ReadFromStorage(const std::string& extension_id, + scoped_ptr<base::Value> value); + + // Set the timer to go off at the specified |time|, and set |next_poll_time| + // appropriately. + void SetNextPollTime(const base::Time& time); + + // Schedules the next poll of alarms for when the next soonest alarm runs, + // but not more often than the minimum granularity of all alarms. + void ScheduleNextPoll(); + + // Polls the alarms, running any that have elapsed. After running them and + // rescheduling repeating alarms, schedule the next poll. + void PollAlarms(); + + // Executes |action| for given extension, making sure that the extension's + // alarm data has been synced from the storage. + void RunWhenReady(const std::string& extension_id, const ReadyAction& action); + + // ExtensionRegistryObserver implementation. + void OnExtensionLoaded(content::BrowserContext* browser_context, + const Extension* extension) override; + void OnExtensionUninstalled(content::BrowserContext* browser_context, + const Extension* extension, + extensions::UninstallReason reason) override; + + // BrowserContextKeyedAPI implementation. + static const char* service_name() { return "AlarmManager"; } + static const bool kServiceHasOwnInstanceInIncognito = true; + + content::BrowserContext* const browser_context_; + scoped_ptr<base::Clock> clock_; + scoped_ptr<Delegate> delegate_; + + // Listen to extension load notifications. + ScopedObserver<ExtensionRegistry, ExtensionRegistryObserver> + extension_registry_observer_; + + // The timer for this alarm manager. + base::OneShotTimer timer_; + + // A map of our pending alarms, per extension. + // Invariant: None of the AlarmLists are empty. + AlarmMap alarms_; + + // A map of actions waiting for alarm data to be synced from storage, per + // extension. + ReadyMap ready_actions_; + + // The previous time that alarms were run. + base::Time last_poll_time_; + + // Next poll's time. + base::Time next_poll_time_; + + DISALLOW_COPY_AND_ASSIGN(AlarmManager); +}; + +} // namespace extensions + +#endif // EXTENSIONS_BROWSER_API_ALARMS_ALARM_MANAGER_H_ diff --git a/chromium/extensions/browser/api/alarms/alarms_api.cc b/chromium/extensions/browser/api/alarms/alarms_api.cc new file mode 100644 index 00000000000..102bce4f863 --- /dev/null +++ b/chromium/extensions/browser/api/alarms/alarms_api.cc @@ -0,0 +1,204 @@ +// 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 "extensions/browser/api/alarms/alarms_api.h" + +#include <stddef.h> + +#include "base/strings/string_number_conversions.h" +#include "base/time/clock.h" +#include "base/time/default_clock.h" +#include "base/values.h" +#include "extensions/browser/api/alarms/alarm_manager.h" +#include "extensions/common/api/alarms.h" +#include "extensions/common/error_utils.h" + +namespace extensions { + +namespace alarms = api::alarms; + +namespace { + +const char kDefaultAlarmName[] = ""; +const char kBothRelativeAndAbsoluteTime[] = + "Cannot set both when and delayInMinutes."; +const char kNoScheduledTime[] = + "Must set at least one of when, delayInMinutes, or periodInMinutes."; +const int kReleaseDelayMinimum = 1; +const int kDevDelayMinimum = 0; + +bool ValidateAlarmCreateInfo(const std::string& alarm_name, + const alarms::AlarmCreateInfo& create_info, + const Extension* extension, + std::string* error, + std::vector<std::string>* warnings) { + if (create_info.delay_in_minutes.get() && create_info.when.get()) { + *error = kBothRelativeAndAbsoluteTime; + return false; + } + if (create_info.delay_in_minutes == NULL && create_info.when == NULL && + create_info.period_in_minutes == NULL) { + *error = kNoScheduledTime; + return false; + } + + // Users can always use an absolute timeout to request an arbitrarily-short or + // negative delay. We won't honor the short timeout, but we can't check it + // and warn the user because it would introduce race conditions (say they + // compute a long-enough timeout, but then the call into the alarms interface + // gets delayed past the boundary). However, it's still worth warning about + // relative delays that are shorter than we'll honor. + if (create_info.delay_in_minutes.get()) { + if (*create_info.delay_in_minutes < kReleaseDelayMinimum) { + static_assert(kReleaseDelayMinimum == 1, + "warning message must be updated"); + if (Manifest::IsUnpackedLocation(extension->location())) + warnings->push_back(ErrorUtils::FormatErrorMessage( + "Alarm delay is less than minimum of 1 minutes." + " In released .crx, alarm \"*\" will fire in approximately" + " 1 minutes.", + alarm_name)); + else + warnings->push_back(ErrorUtils::FormatErrorMessage( + "Alarm delay is less than minimum of 1 minutes." + " Alarm \"*\" will fire in approximately 1 minutes.", + alarm_name)); + } + } + if (create_info.period_in_minutes.get()) { + if (*create_info.period_in_minutes < kReleaseDelayMinimum) { + static_assert(kReleaseDelayMinimum == 1, + "warning message must be updated"); + if (Manifest::IsUnpackedLocation(extension->location())) + warnings->push_back(ErrorUtils::FormatErrorMessage( + "Alarm period is less than minimum of 1 minutes." + " In released .crx, alarm \"*\" will fire approximately" + " every 1 minutes.", + alarm_name)); + else + warnings->push_back(ErrorUtils::FormatErrorMessage( + "Alarm period is less than minimum of 1 minutes." + " Alarm \"*\" will fire approximately every 1 minutes.", + alarm_name)); + } + } + + return true; +} + +} // namespace + +AlarmsCreateFunction::AlarmsCreateFunction() + : clock_(new base::DefaultClock()), owns_clock_(true) { +} + +AlarmsCreateFunction::AlarmsCreateFunction(base::Clock* clock) + : clock_(clock), owns_clock_(false) { +} + +AlarmsCreateFunction::~AlarmsCreateFunction() { + if (owns_clock_) + delete clock_; +} + +bool AlarmsCreateFunction::RunAsync() { + scoped_ptr<alarms::Create::Params> params( + alarms::Create::Params::Create(*args_)); + EXTENSION_FUNCTION_VALIDATE(params.get()); + const std::string& alarm_name = + params->name.get() ? *params->name : kDefaultAlarmName; + std::vector<std::string> warnings; + if (!ValidateAlarmCreateInfo(alarm_name, params->alarm_info, extension(), + &error_, &warnings)) { + return false; + } + for (std::vector<std::string>::const_iterator it = warnings.begin(); + it != warnings.end(); ++it) + WriteToConsole(content::CONSOLE_MESSAGE_LEVEL_WARNING, *it); + + Alarm alarm(alarm_name, params->alarm_info, + base::TimeDelta::FromMinutes( + Manifest::IsUnpackedLocation(extension()->location()) + ? kDevDelayMinimum + : kReleaseDelayMinimum), + clock_->Now()); + AlarmManager::Get(browser_context()) + ->AddAlarm(extension_id(), alarm, + base::Bind(&AlarmsCreateFunction::Callback, this)); + + return true; +} + +void AlarmsCreateFunction::Callback() { + SendResponse(true); +} + +bool AlarmsGetFunction::RunAsync() { + scoped_ptr<alarms::Get::Params> params(alarms::Get::Params::Create(*args_)); + EXTENSION_FUNCTION_VALIDATE(params.get()); + + std::string name = params->name.get() ? *params->name : kDefaultAlarmName; + AlarmManager::Get(browser_context()) + ->GetAlarm(extension_id(), name, + base::Bind(&AlarmsGetFunction::Callback, this, name)); + + return true; +} + +void AlarmsGetFunction::Callback(const std::string& name, + extensions::Alarm* alarm) { + if (alarm) { + results_ = alarms::Get::Results::Create(*alarm->js_alarm); + } + SendResponse(true); +} + +bool AlarmsGetAllFunction::RunAsync() { + AlarmManager::Get(browser_context()) + ->GetAllAlarms(extension_id(), + base::Bind(&AlarmsGetAllFunction::Callback, this)); + return true; +} + +void AlarmsGetAllFunction::Callback(const AlarmList* alarms) { + scoped_ptr<base::ListValue> alarms_value(new base::ListValue()); + if (alarms) { + for (const Alarm& alarm : *alarms) + alarms_value->Append(alarm.js_alarm->ToValue()); + } + SetResult(std::move(alarms_value)); + SendResponse(true); +} + +bool AlarmsClearFunction::RunAsync() { + scoped_ptr<alarms::Clear::Params> params( + alarms::Clear::Params::Create(*args_)); + EXTENSION_FUNCTION_VALIDATE(params.get()); + + std::string name = params->name.get() ? *params->name : kDefaultAlarmName; + AlarmManager::Get(browser_context()) + ->RemoveAlarm(extension_id(), name, + base::Bind(&AlarmsClearFunction::Callback, this, name)); + + return true; +} + +void AlarmsClearFunction::Callback(const std::string& name, bool success) { + SetResult(new base::FundamentalValue(success)); + SendResponse(true); +} + +bool AlarmsClearAllFunction::RunAsync() { + AlarmManager::Get(browser_context()) + ->RemoveAllAlarms(extension_id(), + base::Bind(&AlarmsClearAllFunction::Callback, this)); + return true; +} + +void AlarmsClearAllFunction::Callback() { + SetResult(new base::FundamentalValue(true)); + SendResponse(true); +} + +} // namespace extensions diff --git a/chromium/extensions/browser/api/alarms/alarms_api.h b/chromium/extensions/browser/api/alarms/alarms_api.h new file mode 100644 index 00000000000..837da909620 --- /dev/null +++ b/chromium/extensions/browser/api/alarms/alarms_api.h @@ -0,0 +1,93 @@ +// 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. + +#ifndef EXTENSIONS_BROWSER_API_ALARMS_ALARMS_API_H_ +#define EXTENSIONS_BROWSER_API_ALARMS_ALARMS_API_H_ + +#include <vector> + +#include "extensions/browser/extension_function.h" + +namespace base { +class Clock; +} // namespace base + +namespace extensions { +struct Alarm; +typedef std::vector<Alarm> AlarmList; + +class AlarmsCreateFunction : public AsyncExtensionFunction { + public: + AlarmsCreateFunction(); + // Use |clock| instead of the default clock. Does not take ownership + // of |clock|. Used for testing. + explicit AlarmsCreateFunction(base::Clock* clock); + + protected: + ~AlarmsCreateFunction() override; + + // ExtensionFunction: + bool RunAsync() override; + DECLARE_EXTENSION_FUNCTION("alarms.create", ALARMS_CREATE) + private: + void Callback(); + + base::Clock* const clock_; + // Whether or not we own |clock_|. This is needed because we own it + // when we create it ourselves, but not when it's passed in for + // testing. + bool owns_clock_; +}; + +class AlarmsGetFunction : public AsyncExtensionFunction { + protected: + ~AlarmsGetFunction() override {} + + // ExtensionFunction: + bool RunAsync() override; + + private: + void Callback(const std::string& name, Alarm* alarm); + DECLARE_EXTENSION_FUNCTION("alarms.get", ALARMS_GET) +}; + +class AlarmsGetAllFunction : public AsyncExtensionFunction { + protected: + ~AlarmsGetAllFunction() override {} + + // ExtensionFunction: + bool RunAsync() override; + + private: + void Callback(const AlarmList* alarms); + DECLARE_EXTENSION_FUNCTION("alarms.getAll", ALARMS_GETALL) +}; + +class AlarmsClearFunction : public AsyncExtensionFunction { + protected: + ~AlarmsClearFunction() override {} + + // ExtensionFunction: + bool RunAsync() override; + + private: + void Callback(const std::string& name, bool success); + DECLARE_EXTENSION_FUNCTION("alarms.clear", ALARMS_CLEAR) +}; + +class AlarmsClearAllFunction : public AsyncExtensionFunction { + protected: + ~AlarmsClearAllFunction() override {} + + // ExtensionFunction: + bool RunAsync() override; + + private: + void Callback(); + DECLARE_EXTENSION_FUNCTION("alarms.clearAll", ALARMS_CLEARALL) +}; + +} // namespace extensions + +#endif // EXTENSIONS_BROWSER_API_ALARMS_ALARMS_API_H_ diff --git a/chromium/extensions/browser/api/alarms/alarms_api_unittest.cc b/chromium/extensions/browser/api/alarms/alarms_api_unittest.cc new file mode 100644 index 00000000000..b0626f2641c --- /dev/null +++ b/chromium/extensions/browser/api/alarms/alarms_api_unittest.cc @@ -0,0 +1,700 @@ +// 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. + +// This file tests the chrome.alarms extension API. + +#include <stddef.h> + +#include "base/test/simple_test_clock.h" +#include "base/values.h" +#include "content/public/browser/web_contents.h" +#include "content/public/test/mock_render_process_host.h" +#include "extensions/browser/api/alarms/alarm_manager.h" +#include "extensions/browser/api/alarms/alarms_api.h" +#include "extensions/browser/api_test_utils.h" +#include "extensions/browser/api_unittest.h" +#include "extensions/common/extension_messages.h" +#include "ipc/ipc_test_sink.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +typedef extensions::api::alarms::Alarm JsAlarm; + +namespace extensions { + +namespace utils = api_test_utils; + +namespace { + +// Test delegate which quits the message loop when an alarm fires. +class AlarmDelegate : public AlarmManager::Delegate { + public: + ~AlarmDelegate() override {} + void OnAlarm(const std::string& extension_id, const Alarm& alarm) override { + alarms_seen.push_back(alarm.js_alarm->name); + if (base::MessageLoop::current()->is_running()) + base::MessageLoop::current()->QuitWhenIdle(); + } + + std::vector<std::string> alarms_seen; +}; + +} // namespace + +void RunScheduleNextPoll(AlarmManager* alarm_manager) { + alarm_manager->ScheduleNextPoll(); +} + +class ExtensionAlarmsTest : public ApiUnitTest { + public: + using ApiUnitTest::RunFunction; + + void SetUp() override { + ApiUnitTest::SetUp(); + + test_clock_ = new base::SimpleTestClock(); + alarm_manager_ = AlarmManager::Get(browser_context()); + alarm_manager_->SetClockForTesting(test_clock_); + + alarm_delegate_ = new AlarmDelegate(); + alarm_manager_->set_delegate(alarm_delegate_); + + // Make sure there's a RenderViewHost for alarms to warn into. + CreateBackgroundPage(); + + test_clock_->SetNow(base::Time::FromDoubleT(10)); + } + + void CreateAlarm(const std::string& args) { + RunFunction(new AlarmsCreateFunction(test_clock_), args); + } + + // Takes a JSON result from a function and converts it to a vector of + // JsAlarms. + std::vector<linked_ptr<JsAlarm>> ToAlarmList(base::ListValue* value) { + std::vector<linked_ptr<JsAlarm>> list; + for (size_t i = 0; i < value->GetSize(); ++i) { + linked_ptr<JsAlarm> alarm(new JsAlarm); + base::DictionaryValue* alarm_value; + if (!value->GetDictionary(i, &alarm_value)) { + ADD_FAILURE() << "Expected a list of Alarm objects."; + return list; + } + EXPECT_TRUE(JsAlarm::Populate(*alarm_value, alarm.get())); + list.push_back(alarm); + } + return list; + } + + // Creates up to 3 alarms using the extension API. + void CreateAlarms(size_t num_alarms) { + CHECK_LE(num_alarms, 3U); + + const char* const kCreateArgs[] = { + "[null, {\"periodInMinutes\": 0.001}]", + "[\"7\", {\"periodInMinutes\": 7}]", + "[\"0\", {\"delayInMinutes\": 0}]", + }; + for (size_t i = 0; i < num_alarms; ++i) { + scoped_ptr<base::DictionaryValue> result(RunFunctionAndReturnDictionary( + new AlarmsCreateFunction(test_clock_), kCreateArgs[i])); + EXPECT_FALSE(result.get()); + } + } + + base::SimpleTestClock* test_clock_; + AlarmManager* alarm_manager_; + AlarmDelegate* alarm_delegate_; +}; + +void ExtensionAlarmsTestGetAllAlarmsCallback( + const AlarmManager::AlarmList* alarms) { + // Ensure the alarm is gone. + ASSERT_FALSE(alarms); +} + +void ExtensionAlarmsTestGetAlarmCallback(ExtensionAlarmsTest* test, + Alarm* alarm) { + ASSERT_TRUE(alarm); + EXPECT_EQ("", alarm->js_alarm->name); + EXPECT_DOUBLE_EQ(10000, alarm->js_alarm->scheduled_time); + EXPECT_FALSE(alarm->js_alarm->period_in_minutes.get()); + + // Now wait for the alarm to fire. Our test delegate will quit the + // MessageLoop when that happens. + base::MessageLoop::current()->Run(); + + ASSERT_EQ(1u, test->alarm_delegate_->alarms_seen.size()); + EXPECT_EQ("", test->alarm_delegate_->alarms_seen[0]); + + // Ensure the alarm is gone. + test->alarm_manager_->GetAllAlarms( + test->extension()->id(), + base::Bind(ExtensionAlarmsTestGetAllAlarmsCallback)); +} + +TEST_F(ExtensionAlarmsTest, Create) { + test_clock_->SetNow(base::Time::FromDoubleT(10)); + // Create 1 non-repeating alarm. + CreateAlarm("[null, {\"delayInMinutes\": 0}]"); + + alarm_manager_->GetAlarm( + extension()->id(), std::string(), + base::Bind(ExtensionAlarmsTestGetAlarmCallback, this)); +} + +void ExtensionAlarmsTestCreateRepeatingGetAlarmCallback( + ExtensionAlarmsTest* test, + Alarm* alarm) { + ASSERT_TRUE(alarm); + EXPECT_EQ("", alarm->js_alarm->name); + EXPECT_DOUBLE_EQ(10060, alarm->js_alarm->scheduled_time); + EXPECT_THAT(alarm->js_alarm->period_in_minutes, + testing::Pointee(testing::DoubleEq(0.001))); + + test->test_clock_->Advance(base::TimeDelta::FromSeconds(1)); + // Now wait for the alarm to fire. Our test delegate will quit the + // MessageLoop when that happens. + base::MessageLoop::current()->Run(); + + test->test_clock_->Advance(base::TimeDelta::FromSeconds(1)); + // Wait again, and ensure the alarm fires again. + RunScheduleNextPoll(test->alarm_manager_); + base::MessageLoop::current()->Run(); + + ASSERT_EQ(2u, test->alarm_delegate_->alarms_seen.size()); + EXPECT_EQ("", test->alarm_delegate_->alarms_seen[0]); +} + +TEST_F(ExtensionAlarmsTest, CreateRepeating) { + test_clock_->SetNow(base::Time::FromDoubleT(10)); + + // Create 1 repeating alarm. + CreateAlarm("[null, {\"periodInMinutes\": 0.001}]"); + + alarm_manager_->GetAlarm( + extension()->id(), std::string(), + base::Bind(ExtensionAlarmsTestCreateRepeatingGetAlarmCallback, this)); +} + +void ExtensionAlarmsTestCreateAbsoluteGetAlarm2Callback( + ExtensionAlarmsTest* test, + Alarm* alarm) { + ASSERT_FALSE(alarm); + + ASSERT_EQ(1u, test->alarm_delegate_->alarms_seen.size()); + EXPECT_EQ("", test->alarm_delegate_->alarms_seen[0]); +} + +void ExtensionAlarmsTestCreateAbsoluteGetAlarm1Callback( + ExtensionAlarmsTest* test, + Alarm* alarm) { + ASSERT_TRUE(alarm); + EXPECT_EQ("", alarm->js_alarm->name); + EXPECT_DOUBLE_EQ(10001, alarm->js_alarm->scheduled_time); + EXPECT_THAT(alarm->js_alarm->period_in_minutes, testing::IsNull()); + + test->test_clock_->SetNow(base::Time::FromDoubleT(10.1)); + // Now wait for the alarm to fire. Our test delegate will quit the + // MessageLoop when that happens. + base::MessageLoop::current()->Run(); + + test->alarm_manager_->GetAlarm( + test->extension()->id(), std::string(), + base::Bind(ExtensionAlarmsTestCreateAbsoluteGetAlarm2Callback, test)); +} + +TEST_F(ExtensionAlarmsTest, CreateAbsolute) { + test_clock_->SetNow(base::Time::FromDoubleT(9.99)); + CreateAlarm("[null, {\"when\": 10001}]"); + + alarm_manager_->GetAlarm( + extension()->id(), std::string(), + base::Bind(ExtensionAlarmsTestCreateAbsoluteGetAlarm1Callback, this)); +} + +void ExtensionAlarmsTestCreateRepeatingWithQuickFirstCallGetAlarm3Callback( + ExtensionAlarmsTest* test, + Alarm* alarm) { + ASSERT_TRUE(alarm); + EXPECT_THAT(test->alarm_delegate_->alarms_seen, testing::ElementsAre("", "")); +} + +void ExtensionAlarmsTestCreateRepeatingWithQuickFirstCallGetAlarm2Callback( + ExtensionAlarmsTest* test, + Alarm* alarm) { + ASSERT_TRUE(alarm); + EXPECT_THAT(test->alarm_delegate_->alarms_seen, testing::ElementsAre("")); + + test->test_clock_->SetNow(base::Time::FromDoubleT(10.7)); + base::MessageLoop::current()->Run(); + + test->alarm_manager_->GetAlarm( + test->extension()->id(), std::string(), + base::Bind( + ExtensionAlarmsTestCreateRepeatingWithQuickFirstCallGetAlarm3Callback, + test)); +} + +void ExtensionAlarmsTestCreateRepeatingWithQuickFirstCallGetAlarm1Callback( + ExtensionAlarmsTest* test, + Alarm* alarm) { + ASSERT_TRUE(alarm); + EXPECT_EQ("", alarm->js_alarm->name); + EXPECT_DOUBLE_EQ(10001, alarm->js_alarm->scheduled_time); + EXPECT_THAT(alarm->js_alarm->period_in_minutes, + testing::Pointee(testing::DoubleEq(0.001))); + + test->test_clock_->SetNow(base::Time::FromDoubleT(10.1)); + // Now wait for the alarm to fire. Our test delegate will quit the + // MessageLoop when that happens. + base::MessageLoop::current()->Run(); + + test->alarm_manager_->GetAlarm( + test->extension()->id(), std::string(), + base::Bind( + ExtensionAlarmsTestCreateRepeatingWithQuickFirstCallGetAlarm2Callback, + test)); +} + +TEST_F(ExtensionAlarmsTest, CreateRepeatingWithQuickFirstCall) { + test_clock_->SetNow(base::Time::FromDoubleT(9.99)); + CreateAlarm("[null, {\"when\": 10001, \"periodInMinutes\": 0.001}]"); + + alarm_manager_->GetAlarm( + extension()->id(), std::string(), + base::Bind( + ExtensionAlarmsTestCreateRepeatingWithQuickFirstCallGetAlarm1Callback, + this)); +} + +void ExtensionAlarmsTestCreateDupeGetAllAlarmsCallback( + const AlarmManager::AlarmList* alarms) { + ASSERT_TRUE(alarms); + EXPECT_EQ(1u, alarms->size()); + EXPECT_DOUBLE_EQ(430000, (*alarms)[0].js_alarm->scheduled_time); +} + +TEST_F(ExtensionAlarmsTest, CreateDupe) { + test_clock_->SetNow(base::Time::FromDoubleT(10)); + + // Create 2 duplicate alarms. The first should be overridden. + CreateAlarm("[\"dup\", {\"delayInMinutes\": 1}]"); + CreateAlarm("[\"dup\", {\"delayInMinutes\": 7}]"); + + alarm_manager_->GetAllAlarms( + extension()->id(), + base::Bind(ExtensionAlarmsTestCreateDupeGetAllAlarmsCallback)); +} + +TEST_F(ExtensionAlarmsTest, CreateDelayBelowMinimum) { + // Create an alarm with delay below the minimum accepted value. + IPC::TestSink& sink = + static_cast<content::MockRenderProcessHost*>( + contents()->GetRenderViewHost()->GetProcess())->sink(); + size_t initial_message_count = sink.message_count(); + CreateAlarm("[\"negative\", {\"delayInMinutes\": -0.2}]"); + // A new message should have been added. + ASSERT_GT(sink.message_count(), initial_message_count); + + // All of this would be cleaner if we could read the message as a + // FrameMsg_AddMessageToConsole, but that would be a layering violation. + // Better yet would be an observer method for frames adding console messages, + // but it's not worth adding just for a test. + const IPC::Message* warning = + sink.GetMessageAt(initial_message_count /* 0-based */); + ASSERT_TRUE(warning); + + int level = 0; + base::PickleIterator iter(*warning); + ASSERT_TRUE(iter.ReadInt(&level)); + std::string message; + ASSERT_TRUE(iter.ReadString(&message)); + + EXPECT_EQ(content::CONSOLE_MESSAGE_LEVEL_WARNING, + static_cast<content::ConsoleMessageLevel>(level)); + EXPECT_THAT(message, testing::HasSubstr("delay is less than minimum of 1")); +} + +TEST_F(ExtensionAlarmsTest, Get) { + test_clock_->SetNow(base::Time::FromDoubleT(4)); + + // Create 2 alarms, and make sure we can query them. + CreateAlarms(2); + + // Get the default one. + { + JsAlarm alarm; + scoped_ptr<base::DictionaryValue> result( + RunFunctionAndReturnDictionary(new AlarmsGetFunction(), "[null]")); + ASSERT_TRUE(result.get()); + EXPECT_TRUE(JsAlarm::Populate(*result, &alarm)); + EXPECT_EQ("", alarm.name); + EXPECT_DOUBLE_EQ(4060, alarm.scheduled_time); + EXPECT_THAT(alarm.period_in_minutes, + testing::Pointee(testing::DoubleEq(0.001))); + } + + // Get "7". + { + JsAlarm alarm; + scoped_ptr<base::DictionaryValue> result( + RunFunctionAndReturnDictionary(new AlarmsGetFunction(), "[\"7\"]")); + ASSERT_TRUE(result.get()); + EXPECT_TRUE(JsAlarm::Populate(*result, &alarm)); + EXPECT_EQ("7", alarm.name); + EXPECT_EQ(424000, alarm.scheduled_time); + EXPECT_THAT(alarm.period_in_minutes, testing::Pointee(7)); + } + + // Get a non-existent one. + { + scoped_ptr<base::DictionaryValue> result(RunFunctionAndReturnDictionary( + new AlarmsGetFunction(), "[\"nobody\"]")); + ASSERT_FALSE(result.get()); + } +} + +TEST_F(ExtensionAlarmsTest, GetAll) { + // Test getAll with 0 alarms. + { + scoped_ptr<base::ListValue> result( + RunFunctionAndReturnList(new AlarmsGetAllFunction(), "[]")); + std::vector<linked_ptr<JsAlarm>> alarms = ToAlarmList(result.get()); + EXPECT_EQ(0u, alarms.size()); + } + + // Create 2 alarms, and make sure we can query them. + CreateAlarms(2); + + { + scoped_ptr<base::ListValue> result( + RunFunctionAndReturnList(new AlarmsGetAllFunction(), "[null]")); + std::vector<linked_ptr<JsAlarm>> alarms = ToAlarmList(result.get()); + EXPECT_EQ(2u, alarms.size()); + + // Test the "7" alarm. + JsAlarm* alarm = alarms[0].get(); + if (alarm->name != "7") + alarm = alarms[1].get(); + EXPECT_EQ("7", alarm->name); + EXPECT_THAT(alarm->period_in_minutes, testing::Pointee(7)); + } +} + +void ExtensionAlarmsTestClearGetAllAlarms2Callback( + const AlarmManager::AlarmList* alarms) { + // Ensure the 0.001-minute alarm is still there, since it's repeating. + ASSERT_TRUE(alarms); + EXPECT_EQ(1u, alarms->size()); + EXPECT_THAT((*alarms)[0].js_alarm->period_in_minutes, + testing::Pointee(0.001)); +} + +void ExtensionAlarmsTestClearGetAllAlarms1Callback( + ExtensionAlarmsTest* test, + const AlarmManager::AlarmList* alarms) { + ASSERT_TRUE(alarms); + EXPECT_EQ(1u, alarms->size()); + EXPECT_THAT((*alarms)[0].js_alarm->period_in_minutes, + testing::Pointee(0.001)); + + // Now wait for the alarms to fire, and ensure the cancelled alarms don't + // fire. + test->test_clock_->Advance(base::TimeDelta::FromMilliseconds(60)); + RunScheduleNextPoll(test->alarm_manager_); + base::MessageLoop::current()->Run(); + + ASSERT_EQ(1u, test->alarm_delegate_->alarms_seen.size()); + EXPECT_EQ("", test->alarm_delegate_->alarms_seen[0]); + + // Ensure the 0.001-minute alarm is still there, since it's repeating. + test->alarm_manager_->GetAllAlarms( + test->extension()->id(), + base::Bind(ExtensionAlarmsTestClearGetAllAlarms2Callback)); +} + +TEST_F(ExtensionAlarmsTest, Clear) { + // Clear a non-existent one. + { + scoped_ptr<base::Value> result( + RunFunctionAndReturnValue(new AlarmsClearFunction(), "[\"nobody\"]")); + bool copy_bool_result = false; + ASSERT_TRUE(result->GetAsBoolean(©_bool_result)); + EXPECT_FALSE(copy_bool_result); + } + + // Create 3 alarms. + CreateAlarms(3); + + // Clear all but the 0.001-minute alarm. + { + scoped_ptr<base::Value> result( + RunFunctionAndReturnValue(new AlarmsClearFunction(), "[\"7\"]")); + bool copy_bool_result = false; + ASSERT_TRUE(result->GetAsBoolean(©_bool_result)); + EXPECT_TRUE(copy_bool_result); + } + { + scoped_ptr<base::Value> result( + RunFunctionAndReturnValue(new AlarmsClearFunction(), "[\"0\"]")); + bool copy_bool_result = false; + ASSERT_TRUE(result->GetAsBoolean(©_bool_result)); + EXPECT_TRUE(copy_bool_result); + } + + alarm_manager_->GetAllAlarms( + extension()->id(), + base::Bind(ExtensionAlarmsTestClearGetAllAlarms1Callback, this)); +} + +void ExtensionAlarmsTestClearAllGetAllAlarms2Callback( + const AlarmManager::AlarmList* alarms) { + ASSERT_FALSE(alarms); +} + +void ExtensionAlarmsTestClearAllGetAllAlarms1Callback( + ExtensionAlarmsTest* test, + const AlarmManager::AlarmList* alarms) { + ASSERT_TRUE(alarms); + EXPECT_EQ(3u, alarms->size()); + + // Clear them. + test->RunFunction(new AlarmsClearAllFunction(), "[]"); + test->alarm_manager_->GetAllAlarms( + test->extension()->id(), + base::Bind(ExtensionAlarmsTestClearAllGetAllAlarms2Callback)); +} + +TEST_F(ExtensionAlarmsTest, ClearAll) { + // ClearAll with no alarms set. + { + scoped_ptr<base::Value> result( + RunFunctionAndReturnValue(new AlarmsClearAllFunction(), "[]")); + bool copy_bool_result = false; + ASSERT_TRUE(result->GetAsBoolean(©_bool_result)); + EXPECT_TRUE(copy_bool_result); + } + + // Create 3 alarms. + CreateAlarms(3); + alarm_manager_->GetAllAlarms( + extension()->id(), + base::Bind(ExtensionAlarmsTestClearAllGetAllAlarms1Callback, this)); +} + +class ExtensionAlarmsSchedulingTest : public ExtensionAlarmsTest { + void GetAlarmCallback(Alarm* alarm) { + CHECK(alarm); + const base::Time scheduled_time = + base::Time::FromJsTime(alarm->js_alarm->scheduled_time); + EXPECT_EQ(scheduled_time, alarm_manager_->next_poll_time_); + } + + static void RemoveAlarmCallback(bool success) { EXPECT_TRUE(success); } + static void RemoveAllAlarmsCallback() {} + + public: + // Get the time that the alarm named is scheduled to run. + void VerifyScheduledTime(const std::string& alarm_name) { + alarm_manager_->GetAlarm( + extension()->id(), alarm_name, + base::Bind(&ExtensionAlarmsSchedulingTest::GetAlarmCallback, + base::Unretained(this))); + } + + void RemoveAlarm(const std::string& name) { + alarm_manager_->RemoveAlarm( + extension()->id(), name, + base::Bind(&ExtensionAlarmsSchedulingTest::RemoveAlarmCallback)); + } + + void RemoveAllAlarms() { + alarm_manager_->RemoveAllAlarms( + extension()->id(), + base::Bind(&ExtensionAlarmsSchedulingTest::RemoveAllAlarmsCallback)); + } +}; + +TEST_F(ExtensionAlarmsSchedulingTest, PollScheduling) { + { + CreateAlarm("[\"a\", {\"periodInMinutes\": 6}]"); + CreateAlarm("[\"bb\", {\"periodInMinutes\": 8}]"); + VerifyScheduledTime("a"); + RemoveAllAlarms(); + } + { + CreateAlarm("[\"a\", {\"delayInMinutes\": 10}]"); + CreateAlarm("[\"bb\", {\"delayInMinutes\": 21}]"); + VerifyScheduledTime("a"); + RemoveAllAlarms(); + } + { + test_clock_->SetNow(base::Time::FromDoubleT(10)); + CreateAlarm("[\"a\", {\"periodInMinutes\": 10}]"); + Alarm alarm; + alarm.js_alarm->name = "bb"; + alarm.js_alarm->scheduled_time = 30 * 60000; + alarm.js_alarm->period_in_minutes.reset(new double(30)); + alarm_manager_->AddAlarmImpl(extension()->id(), alarm); + VerifyScheduledTime("a"); + RemoveAllAlarms(); + } + { + test_clock_->SetNow(base::Time::FromDoubleT(3 * 60 + 1)); + Alarm alarm; + alarm.js_alarm->name = "bb"; + alarm.js_alarm->scheduled_time = 3 * 60000; + alarm.js_alarm->period_in_minutes.reset(new double(3)); + alarm_manager_->AddAlarmImpl(extension()->id(), alarm); + base::MessageLoop::current()->Run(); + EXPECT_EQ( + base::Time::FromJsTime(3 * 60000) + base::TimeDelta::FromMinutes(3), + alarm_manager_->next_poll_time_); + RemoveAllAlarms(); + } + { + test_clock_->SetNow(base::Time::FromDoubleT(4 * 60 + 1)); + CreateAlarm("[\"a\", {\"periodInMinutes\": 2}]"); + RemoveAlarm("a"); + Alarm alarm2; + alarm2.js_alarm->name = "bb"; + alarm2.js_alarm->scheduled_time = 4 * 60000; + alarm2.js_alarm->period_in_minutes.reset(new double(4)); + alarm_manager_->AddAlarmImpl(extension()->id(), alarm2); + Alarm alarm3; + alarm3.js_alarm->name = "ccc"; + alarm3.js_alarm->scheduled_time = 25 * 60000; + alarm3.js_alarm->period_in_minutes.reset(new double(25)); + alarm_manager_->AddAlarmImpl(extension()->id(), alarm3); + base::MessageLoop::current()->Run(); + EXPECT_EQ( + base::Time::FromJsTime(4 * 60000) + base::TimeDelta::FromMinutes(4), + alarm_manager_->next_poll_time_); + RemoveAllAlarms(); + } +} + +TEST_F(ExtensionAlarmsSchedulingTest, ReleasedExtensionPollsInfrequently) { + set_extension( + utils::CreateEmptyExtensionWithLocation(extensions::Manifest::INTERNAL)); + test_clock_->SetNow(base::Time::FromJsTime(300000)); + CreateAlarm("[\"a\", {\"when\": 300010}]"); + CreateAlarm("[\"b\", {\"when\": 340000}]"); + + // On startup (when there's no "last poll"), we let alarms fire as + // soon as they're scheduled. + EXPECT_DOUBLE_EQ(300010, alarm_manager_->next_poll_time_.ToJsTime()); + + alarm_manager_->last_poll_time_ = base::Time::FromJsTime(290000); + // In released extensions, we set the granularity to at least 1 + // minute, which makes AddAlarm schedule the next poll after the + // extension requested. + alarm_manager_->ScheduleNextPoll(); + EXPECT_DOUBLE_EQ((alarm_manager_->last_poll_time_ + + base::TimeDelta::FromMinutes(1)).ToJsTime(), + alarm_manager_->next_poll_time_.ToJsTime()); +} + +TEST_F(ExtensionAlarmsSchedulingTest, TimerRunning) { + EXPECT_FALSE(alarm_manager_->timer_.IsRunning()); + CreateAlarm("[\"a\", {\"delayInMinutes\": 0.001}]"); + EXPECT_TRUE(alarm_manager_->timer_.IsRunning()); + test_clock_->Advance(base::TimeDelta::FromMilliseconds(60)); + base::MessageLoop::current()->Run(); + EXPECT_FALSE(alarm_manager_->timer_.IsRunning()); + CreateAlarm("[\"bb\", {\"delayInMinutes\": 10}]"); + EXPECT_TRUE(alarm_manager_->timer_.IsRunning()); + RemoveAllAlarms(); + EXPECT_FALSE(alarm_manager_->timer_.IsRunning()); +} + +TEST_F(ExtensionAlarmsSchedulingTest, MinimumGranularity) { + set_extension( + utils::CreateEmptyExtensionWithLocation(extensions::Manifest::INTERNAL)); + test_clock_->SetNow(base::Time::FromJsTime(0)); + CreateAlarm("[\"a\", {\"periodInMinutes\": 2}]"); + test_clock_->Advance(base::TimeDelta::FromSeconds(1)); + CreateAlarm("[\"b\", {\"periodInMinutes\": 2}]"); + test_clock_->Advance(base::TimeDelta::FromMinutes(2)); + + alarm_manager_->last_poll_time_ = base::Time::FromJsTime(2 * 60000); + // In released extensions, we set the granularity to at least 1 + // minute, which makes scheduler set it to 1 minute, rather than + // 1 second later (when b is supposed to go off). + alarm_manager_->ScheduleNextPoll(); + EXPECT_DOUBLE_EQ((alarm_manager_->last_poll_time_ + + base::TimeDelta::FromMinutes(1)).ToJsTime(), + alarm_manager_->next_poll_time_.ToJsTime()); +} + +TEST_F(ExtensionAlarmsSchedulingTest, DifferentMinimumGranularities) { + test_clock_->SetNow(base::Time::FromJsTime(0)); + // Create an alarm to go off in 12 seconds. This uses the default, unpacked + // extension - so there is no minimum granularity. + CreateAlarm("[\"a\", {\"periodInMinutes\": 0.2}]"); // 12 seconds. + + // Create a new extension, which is packed, and has a granularity of 1 minute. + // CreateAlarm() uses extension_, so keep a ref of the old one around, and + // repopulate extension_. + scoped_refptr<Extension> extension2(extension_ref()); + set_extension( + utils::CreateEmptyExtensionWithLocation(extensions::Manifest::INTERNAL)); + + CreateAlarm("[\"b\", {\"periodInMinutes\": 2}]"); + + alarm_manager_->last_poll_time_ = base::Time::FromJsTime(0); + alarm_manager_->ScheduleNextPoll(); + + // The next poll time should be 12 seconds from now - the time at which the + // first alarm should go off. + EXPECT_DOUBLE_EQ((alarm_manager_->last_poll_time_ + + base::TimeDelta::FromSeconds(12)).ToJsTime(), + alarm_manager_->next_poll_time_.ToJsTime()); +} + +// Test that scheduled alarms go off at set intervals, even if their actual +// trigger is off. +TEST_F(ExtensionAlarmsSchedulingTest, RepeatingAlarmsScheduledPredictably) { + test_clock_->SetNow(base::Time::FromJsTime(0)); + CreateAlarm("[\"a\", {\"periodInMinutes\": 2}]"); + + alarm_manager_->last_poll_time_ = base::Time::FromJsTime(0); + alarm_manager_->ScheduleNextPoll(); + + // We expect the first poll to happen two minutes from the start. + EXPECT_DOUBLE_EQ((alarm_manager_->last_poll_time_ + + base::TimeDelta::FromSeconds(120)).ToJsTime(), + alarm_manager_->next_poll_time_.ToJsTime()); + + // Poll more than two minutes later. + test_clock_->Advance(base::TimeDelta::FromSeconds(125)); + alarm_manager_->PollAlarms(); + + // The alarm should have triggered once. + EXPECT_EQ(1u, alarm_delegate_->alarms_seen.size()); + + // The next poll should still be scheduled for four minutes from the start, + // even though this is less than two minutes since the last alarm. + // Last poll was at 125 seconds; next poll should be at 240 seconds. + EXPECT_DOUBLE_EQ((alarm_manager_->last_poll_time_ + + base::TimeDelta::FromSeconds(115)).ToJsTime(), + alarm_manager_->next_poll_time_.ToJsTime()); + + // Completely miss a scheduled trigger. + test_clock_->Advance(base::TimeDelta::FromSeconds(255)); // Total Time: 380s + alarm_manager_->PollAlarms(); + + // The alarm should have triggered again at this last poll. + EXPECT_EQ(2u, alarm_delegate_->alarms_seen.size()); + + // The next poll should be the first poll that hasn't happened and is in-line + // with the original scheduling. + // Last poll was at 380 seconds; next poll should be at 480 seconds. + EXPECT_DOUBLE_EQ((alarm_manager_->last_poll_time_ + + base::TimeDelta::FromSeconds(100)).ToJsTime(), + alarm_manager_->next_poll_time_.ToJsTime()); +} + +} // namespace extensions |