[feat, Kobo] Autoshutdown (#5335)

The methods used here will likely work on most embedded devices, which is why I put them in their own WakeupMgr interface/scheduler module, separate from Kobo.

See https://www.mobileread.com/forums/showthread.php?p=3886403#post3886403 for more context.

Fixes #3806.
pull/5368/head
Frans de Jonge 5 years ago committed by GitHub
parent 9163a85b3c
commit e257c4e45e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,9 +1,10 @@
local Generic = require("device/generic/device")
local TimeVal = require("ui/timeval")
local Geom = require("ui/geometry")
local TimeVal = require("ui/timeval")
local WakeupMgr = require("device/wakeupmgr")
local logger = require("logger")
local util = require("ffi/util")
local _ = require("gettext")
local logger = require("logger")
local function yes() return true end
local function no() return false end
@ -313,6 +314,7 @@ function Kobo:init()
end,
}
}
self.wakeup_mgr = WakeupMgr:new()
Generic.init(self)
@ -535,20 +537,34 @@ end
local unexpected_wakeup_count = 0
local function check_unexpected_wakeup()
logger.dbg("Kobo suspend: checking unexpected wakeup:",
unexpected_wakeup_count)
if unexpected_wakeup_count == 0 or unexpected_wakeup_count > 20 then
-- Don't put device back to sleep under the following two cases:
-- 1. a resume event triggered Kobo:resume() function
-- 2. trying to put device back to sleep more than 20 times after unexpected wakeup
return
end
logger.err("Kobo suspend: putting device back to sleep, unexpected wakeups:",
unexpected_wakeup_count)
local UIManager = require("ui/uimanager")
-- just in case other events like SleepCoverClosed also scheduled a suspend
require("ui/uimanager"):unschedule(Kobo.suspend)
Kobo.suspend()
UIManager:unschedule(Kobo.suspend)
if WakeupMgr:isWakeupAlarmScheduled() and WakeupMgr:validateWakeupAlarmByProximity() then
logger.dbg("Kobo suspend: scheduled wakeup.")
local res = WakeupMgr:wakeupAction()
if not res then
logger.err("Kobo suspend: wakeup action failed.")
end
logger.dbg("Kobo suspend: putting device back to sleep.")
-- Most wakeup actions are linear, but we need some leeway for the
-- poweroff action to send out close events to all requisite widgets.
UIManager:scheduleIn(30, Kobo.suspend)
else
logger.dbg("Kobo suspend: checking unexpected wakeup:",
unexpected_wakeup_count)
if unexpected_wakeup_count == 0 or unexpected_wakeup_count > 20 then
-- Don't put device back to sleep under the following two cases:
-- 1. a resume event triggered Kobo:resume() function
-- 2. trying to put device back to sleep more than 20 times after unexpected wakeup
return
end
logger.err("Kobo suspend: putting device back to sleep. Unexpected wakeups:",
unexpected_wakeup_count)
Kobo.suspend()
end
end
function Kobo:getUnexpectedWakeup() return unexpected_wakeup_count end
@ -683,7 +699,7 @@ function Kobo:suspend()
-- expected wakeup, which gets checked in check_unexpected_wakeup().
unexpected_wakeup_count = unexpected_wakeup_count + 1
-- assuming Kobo:resume() will be called in 15 seconds
logger.dbg("Kobo suspend: scheduing unexpected wakeup guard")
logger.dbg("Kobo suspend: scheduling unexpected wakeup guard")
UIManager:scheduleIn(15, check_unexpected_wakeup)
end

@ -0,0 +1,187 @@
--[[--
RTC wakeup interface.
Many devices can schedule hardware wakeups with a real time clock alarm.
On embedded devices this can typically be easily manipulated by the user
through `/sys/class/rtc/rtc0/wakealarm`. Some, like the Kobo Aura H2O,
can only schedule wakeups through ioctl.
See @{ffi.rtc} for implementation details.
See also: <https://linux.die.net/man/4/rtc>.
--]]
local RTC = require("ffi/rtc")
local logger = require("logger")
--[[--
WakeupMgr base class.
@table WakeupMgr
--]]
local WakeupMgr = {
dev_rtc = "/dev/rtc0", -- RTC device
_task_queue = {}, -- Table with epoch at which to schedule the task and the function to be scheduled.
}
--[[--
Initiate a WakeupMgr instance.
@usage
local WakeupMgr = require("device/wakeupmgr")
local wakeup_mgr = WakeupMgr:new{
-- The default is `/dev/rtc0`, but some devices have more than one RTC.
-- You might therefore need to use `/dev/rtc1`, etc.
dev_rtc = "/dev/rtc0",
}
--]]
function WakeupMgr:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
if o.init then o:init() end
return o
end
--[[--
Add a task to the queue.
@todo Group by type to avoid useless wakeups.
For example, maintenance, sync, and shutdown.
I'm not sure if the distinction between maintenance and sync makes sense
but it's wifi on vs. off.
--]]
function WakeupMgr:addTask(seconds_from_now, callback)
if not type(seconds_from_now) == "number" and not type(callback) == "function" then return end
local epoch = RTC:secondsFromNowToEpoch(seconds_from_now)
local old_upcoming_task = (self._task_queue[1] or {}).epoch
table.insert(self._task_queue, {
epoch = epoch,
callback = callback,
})
--- @todo Binary insert? This table should be so small that performance doesn't matter.
-- It might be useful to have that available as a utility function regardless.
table.sort(self._task_queue, function(a, b) return a.epoch < b.epoch end)
local new_upcoming_task = self._task_queue[1].epoch
if not old_upcoming_task or (new_upcoming_task < old_upcoming_task) then
self:setWakeupAlarm(self._task_queue[1].epoch)
end
end
--[[--
Remove task from queue.
This method removes a task by either index, scheduled time or callback.
@int idx Task queue index. Mainly useful within this module.
@int epoch The epoch for when this task is scheduled to wake up.
Normally the preferred method for outside callers.
@int callback A scheduled callback function. Store a reference for use
with anonymous functions.
--]]
function WakeupMgr:removeTask(idx, epoch, callback)
if not type(idx) == "number"
and not type(epoch) == "number"
and not type(callback) == "function" then return end
if #self._task_queue < 1 then return end
for k, v in ipairs(self._task_queue) do
if k == idx or epoch == v.epoch or callback == v.callback then
table.remove(self._task_queue, k)
return true
end
end
end
--[[--
Execute wakeup action.
This method should be called by the device resume logic in case of a scheduled wakeup.
It checks if the wakeup was scheduled by us using @{validateWakeupAlarmByProximity},
executes the task, and schedules the next wakeup if any.
@treturn bool
--]]
function WakeupMgr:wakeupAction()
if #self._task_queue > 0 then
local task = self._task_queue[1]
if self:validateWakeupAlarmByProximity(task.epoch) then
task.callback()
self:removeTask(1)
if self._task_queue[1] then
-- Set next scheduled wakeup, if any.
self:setWakeupAlarm(self._task_queue[1].epoch)
end
return true
else
return false
end
end
end
--[[--
Set wakeup alarm.
Simple wrapper for @{ffi.rtc.setWakeupAlarm}.
--]]
function WakeupMgr:setWakeupAlarm(epoch, enabled)
return RTC:setWakeupAlarm(epoch, enabled)
end
--[[--
Unset wakeup alarm.
Simple wrapper for @{ffi.rtc.unsetWakeupAlarm}.
--]]
function WakeupMgr:unsetWakeupAlarm()
return RTC:unsetWakeupAlarm()
end
--[[--
Get wakealarm as set by us.
Simple wrapper for @{ffi.rtc.getWakeupAlarm}.
--]]
function WakeupMgr:getWakeupAlarm()
return RTC:getWakeupAlarm()
end
--[[--
Get RTC wakealarm from system.
Simple wrapper for @{ffi.rtc.getWakeupAlarm}.
--]]
function WakeupMgr:getWakeupAlarmSys()
return RTC:getWakeupAlarmSys()
end
--[[--
Validate wakeup alarm.
Checks if we set the alarm.
Simple wrapper for @{ffi.rtc.validateWakeupAlarmByProximity}.
--]]
function WakeupMgr:validateWakeupAlarmByProximity()
return RTC:validateWakeupAlarmByProximity()
end
--[[--
Check if a wakeup is scheduled.
Simple wrapper for @{ffi.rtc.isWakeupAlarmScheduled}.
--]]
function WakeupMgr:isWakeupAlarmScheduled()
local wakeup_scheduled = RTC:isWakeupAlarmScheduled()
logger.dbg("isWakeupAlarmScheduled", wakeup_scheduled)
return wakeup_scheduled
end
return WakeupMgr

@ -42,6 +42,7 @@ local order = {
"time",
"battery",
"autosuspend",
"autoshutdown",
"ignore_sleepcover",
"ignore_open_sleepcover",
"mass_storage_settings",

@ -61,6 +61,7 @@ local order = {
"time",
"battery",
"autosuspend",
"autoshutdown",
"ignore_sleepcover",
"ignore_open_sleepcover",
"mass_storage_settings",

@ -386,7 +386,7 @@ function UIManager:close(widget, refreshtype, refreshregion, refreshdither)
end
-- schedule an execution task, task queue is in ascendant order
function UIManager:schedule(time, action)
function UIManager:schedule(time, action, ...)
local p, s, e = 1, 1, #self._task_queue
if e ~= 0 then
local us = time[1] * MILLION + time[2]
@ -416,7 +416,11 @@ function UIManager:schedule(time, action)
end
until e < s
end
table.insert(self._task_queue, p, { time = time, action = action })
table.insert(self._task_queue, p, {
time = time,
action = action,
args = {...},
})
self._task_queue_dirty = true
end
dbg:guard(UIManager, 'schedule',
@ -426,7 +430,7 @@ dbg:guard(UIManager, 'schedule',
end)
--- Schedules task in a certain amount of seconds (fractions allowed) from now.
function UIManager:scheduleIn(seconds, action)
function UIManager:scheduleIn(seconds, action, ...)
local when = { util.gettime() }
local s = math.floor(seconds)
local usecs = (seconds - s) * MILLION
@ -436,7 +440,7 @@ function UIManager:scheduleIn(seconds, action)
when[1] = when[1] + 1
when[2] = when[2] - MILLION
end
self:schedule(when, action)
self:schedule(when, action, ...)
end
dbg:guard(UIManager, 'scheduleIn',
function(self, seconds, action)
@ -743,7 +747,7 @@ function UIManager:_checkTasks()
-- task is pending to be executed right now. do it.
-- NOTE: be careful that task.action() might modify
-- _task_queue here. So need to avoid race condition
task.action()
task.action(unpack(task.args or {}))
else
-- queue is sorted in ascendant order, safe to assume all items
-- are future tasks for now

@ -3,4 +3,5 @@ return {
name = "autosuspend",
fullname = _("Auto suspend"),
description = _([[Suspends the device after a period of inactivity.]]),
sorting_hint = "device",
}

@ -11,10 +11,15 @@ local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
local _ = require("gettext")
local T = require("ffi/util").template
local AutoSuspend = {
local default_autoshutdown_timeout_seconds = 3*24*60*60
local AutoSuspend = WidgetContainer:new{
name = "autosuspend",
is_doc_only = false,
autoshutdown_sec = G_reader_settings:readSetting("autoshutdown_timeout_seconds") or default_autoshutdown_timeout_seconds,
settings = LuaSettings:open(DataStorage:getSettingsDir() .. "/koboautosuspend.lua"),
settings_id = 0,
last_action_sec = os.time(),
}
@ -43,54 +48,65 @@ function AutoSuspend:_enabled()
return self.auto_suspend_sec > 0
end
function AutoSuspend:_schedule(settings_id)
function AutoSuspend:_enabledShutdown()
return Device:canPowerOff() and self.autoshutdown_sec > 0
end
function AutoSuspend:_schedule()
if not self:_enabled() then
logger.dbg("AutoSuspend:_schedule is disabled")
return
end
if self.settings_id ~= settings_id then
logger.dbg("AutoSuspend:_schedule registered settings_id ",
settings_id,
" does not equal to current one ",
self.settings_id)
return
end
local delay
local delay_suspend, delay_shutdown
if PluginShare.pause_auto_suspend then
delay = self.auto_suspend_sec
delay_suspend = self.auto_suspend_sec
delay_shutdown = self.autoshutdown_sec
else
delay = self.last_action_sec + self.auto_suspend_sec - os.time()
delay_suspend = self.last_action_sec + self.auto_suspend_sec - os.time()
delay_shutdown = self.last_action_sec + self.autoshutdown_sec - os.time()
end
if delay <= 0 then
if delay_suspend <= 0 then
logger.dbg("AutoSuspend: will suspend the device")
UIManager:suspend()
elseif delay_shutdown <= 0 then
logger.dbg("AutoSuspend: initiating shutdown")
UIManager:poweroff_action()
else
logger.dbg("AutoSuspend: schedule at ", os.time() + delay)
UIManager:scheduleIn(delay, function() self:_schedule(settings_id) end)
if self:_enabled() then
logger.dbg("AutoSuspend: schedule suspend at ", os.time() + delay_suspend)
UIManager:scheduleIn(delay_suspend, self._schedule, self)
end
if self:_enabledShutdown() then
logger.dbg("AutoSuspend: schedule shutdown at ", os.time() + delay_shutdown)
UIManager:scheduleIn(delay_shutdown, self._schedule, self)
end
end
end
function AutoSuspend:_deprecateLastTask()
self.settings_id = self.settings_id + 1
logger.dbg("AutoSuspend: deprecateLastTask ", self.settings_id)
function AutoSuspend:_unschedule()
logger.dbg("AutoSuspend: unschedule")
UIManager:unschedule(self._schedule)
end
function AutoSuspend:_start()
if self:_enabled() then
if self:_enabled() or self:_enabledShutdown() then
logger.dbg("AutoSuspend: start at ", os.time())
self.last_action_sec = os.time()
self:_schedule(self.settings_id)
self:_schedule()
end
end
function AutoSuspend:init()
UIManager.event_hook:registerWidget("InputEvent", self)
self.auto_suspend_sec = self:_readTimeoutSec()
self:_deprecateLastTask()
self:_unschedule()
self:_start()
-- self.ui is nil in the testsuite
if not self.ui or not self.ui.menu then return end
self.ui.menu:registerToMainMenu(self)
end
function AutoSuspend:onInputEvent()
@ -98,11 +114,14 @@ function AutoSuspend:onInputEvent()
self.last_action_sec = os.time()
end
-- We do not want auto suspend procedure to waste battery during suspend. So let's unschedule it
-- when suspending and restart it after resume.
function AutoSuspend:onSuspend()
logger.dbg("AutoSuspend: onSuspend")
self:_deprecateLastTask()
-- We do not want auto suspend procedure to waste battery during suspend. So let's unschedule it
-- when suspending and restart it after resume.
self:_unschedule()
if self:_enabledShutdown() and Device.wakeup_mgr then
Device.wakeup_mgr:addTask(self.autoshutdown_sec, UIManager.poweroff_action)
end
end
function AutoSuspend:onResume()
@ -110,23 +129,9 @@ function AutoSuspend:onResume()
self:_start()
end
AutoSuspend:init()
local AutoSuspendWidget = WidgetContainer:new{
name = "autosuspend",
}
function AutoSuspendWidget:addToMainMenu(menu_items)
function AutoSuspend:addToMainMenu(menu_items)
menu_items.autosuspend = {
text = _("Autosuspend timeout"),
-- This won't ever be registered if the plugin is disabled ;).
--[[
enabled_func = function()
-- NOTE: Pilfered from frontend/pluginloader.lua
local plugins_disabled = G_reader_settings:readSetting("plugins_disabled") or {}
return plugins_disabled["autosuspend"] ~= true
end,
--]]
callback = function()
local InfoMessage = require("ui/widget/infomessage")
local Screen = Device.screen
@ -141,11 +146,53 @@ function AutoSuspendWidget:addToMainMenu(menu_items)
ok_text = _("Set timeout"),
title_text = _("Timeout in minutes"),
callback = function(autosuspend_spin)
G_reader_settings:saveSetting("auto_suspend_timeout_seconds", autosuspend_spin.value * 60)
-- NOTE: Will only take effect after a restart, as we don't have a method to set this live...
local autosuspend_timeout_seconds = autosuspend_spin.value * 60
self.auto_suspend_sec = autosuspend_timeout_seconds
G_reader_settings:saveSetting("auto_suspend_timeout_seconds", autosuspend_timeout_seconds)
UIManager:show(InfoMessage:new{
text = _("This will take effect on next restart."),
text = T(_("The system will automatically suspend after %1 minutes of inactivity."),
string.format("%.2f", autosuspend_timeout_seconds/60)),
timeout = 3,
})
self:_unschedule()
self:_start()
end
}
UIManager:show(autosuspend_spin)
end,
}
if not (Device:canPowerOff() or Device:isEmulator()) then return end
menu_items.autoshutdown = {
text = _("Autoshutdown timeout"),
callback = function()
local InfoMessage = require("ui/widget/infomessage")
local Screen = Device.screen
local SpinWidget = require("ui/widget/spinwidget")
local curr_items = G_reader_settings:readSetting("autoshutdown_timeout_seconds") or default_autoshutdown_timeout_seconds
local autosuspend_spin = SpinWidget:new {
width = Screen:getWidth() * 0.6,
value = curr_items / 60 / 60,
-- About a minute, good for testing and battery life fanatics.
-- Just high enough to avoid an instant shutdown death scenario.
value_min = 0.017,
-- More than three weeks seems a bit excessive if you want to enable authoshutdown,
-- even if the battery can last up to three months.
value_max = 28*24,
value_hold_step = 24,
precision = "%.2f",
ok_text = _("Set timeout"),
title_text = _("Timeout in hours"),
callback = function(autosuspend_spin)
local autoshutdown_timeout_seconds = math.floor(autosuspend_spin.value * 60*60)
self.autoshutdown_timeout_seconds = autoshutdown_timeout_seconds
G_reader_settings:saveSetting("autoshutdown_timeout_seconds", autoshutdown_timeout_seconds)
UIManager:show(InfoMessage:new{
text = T(_("The system will automatically shut down after %1 hours of inactivity."),
string.format("%.2f", autoshutdown_timeout_seconds/60/60)),
timeout = 3,
})
self:_unschedule()
self:_start()
end
}
UIManager:show(autosuspend_spin)
@ -153,22 +200,4 @@ function AutoSuspendWidget:addToMainMenu(menu_items)
}
end
function AutoSuspendWidget:init()
-- self.ui is nil in the testsuite
if not self.ui or not self.ui.menu then return end
self.ui.menu:registerToMainMenu(self)
end
function AutoSuspendWidget:onInputEvent()
AutoSuspend:onInputEvent()
end
function AutoSuspendWidget:onSuspend()
AutoSuspend:onSuspend()
end
function AutoSuspendWidget:onResume()
AutoSuspend:onResume()
end
return AutoSuspendWidget
return AutoSuspend

@ -1,75 +1,120 @@
describe("AutoSuspend widget tests", function()
describe("AutoSuspend", function()
setup(function()
require("commonrequire")
package.unloadAll()
require("document/canvascontext"):init(require("device"))
end)
before_each(function()
local Device = require("device")
stub(Device, "isKobo")
Device.isKobo.returns(true)
Device.input.waitEvent = function() end
local UIManager = require("ui/uimanager")
stub(UIManager, "suspend")
UIManager._run_forever = true
G_reader_settings:saveSetting("auto_suspend_timeout_seconds", 10)
require("mock_time"):install()
end)
describe("suspend", function()
before_each(function()
local Device = require("device")
stub(Device, "isKobo")
Device.isKobo.returns(true)
Device.input.waitEvent = function() end
local UIManager = require("ui/uimanager")
stub(UIManager, "suspend")
UIManager._run_forever = true
G_reader_settings:saveSetting("auto_suspend_timeout_seconds", 10)
require("mock_time"):install()
end)
after_each(function()
require("device").isKobo:revert()
require("ui/uimanager").suspend:revert()
G_reader_settings:delSetting("auto_suspend_timeout_seconds")
require("mock_time"):uninstall()
end)
after_each(function()
require("device").isKobo:revert()
require("ui/uimanager").suspend:revert()
G_reader_settings:delSetting("auto_suspend_timeout_seconds")
require("mock_time"):uninstall()
end)
it("should be able to execute suspend when timing out", function()
local mock_time = require("mock_time")
local widget_class = dofile("plugins/autosuspend.koplugin/main.lua")
local widget = widget_class:new() --luacheck: ignore
local UIManager = require("ui/uimanager")
mock_time:increase(5)
UIManager:handleInput()
assert.stub(UIManager.suspend).was.called(0)
mock_time:increase(6)
UIManager:handleInput()
assert.stub(UIManager.suspend).was.called(1)
mock_time:uninstall()
end)
it("should be able to execute suspend when timing out", function()
local mock_time = require("mock_time")
local widget_class = dofile("plugins/autosuspend.koplugin/main.lua")
local widget = widget_class:new() --luacheck: ignore
local UIManager = require("ui/uimanager")
mock_time:increase(5)
UIManager:handleInput()
assert.stub(UIManager.suspend).was.called(0)
mock_time:increase(6)
UIManager:handleInput()
assert.stub(UIManager.suspend).was.called(1)
mock_time:uninstall()
end)
it("should be able to initialize several times", function()
local mock_time = require("mock_time")
-- AutoSuspend plugin set the last_action_sec each time it is initialized.
local widget_class = dofile("plugins/autosuspend.koplugin/main.lua")
local widget1 = widget_class:new() --luacheck: ignore
-- So if one more initialization happens, it won't sleep after another 5 seconds.
mock_time:increase(5)
local widget2 = widget_class:new() --luacheck: ignore
local UIManager = require("ui/uimanager")
mock_time:increase(6)
UIManager:handleInput()
assert.stub(UIManager.suspend).was.called(1)
mock_time:uninstall()
it("should be able to deprecate last task", function()
local mock_time = require("mock_time")
local widget_class = dofile("plugins/autosuspend.koplugin/main.lua")
local widget = widget_class:new()
mock_time:increase(5)
local UIManager = require("ui/uimanager")
UIManager:handleInput()
assert.stub(UIManager.suspend).was.called(0)
widget:onInputEvent()
widget:onSuspend()
widget:onResume()
mock_time:increase(6)
UIManager:handleInput()
assert.stub(UIManager.suspend).was.called(0)
mock_time:increase(5)
UIManager:handleInput()
assert.stub(UIManager.suspend).was.called(1)
mock_time:uninstall()
end)
end)
it("should be able to deprecate last task", function()
local mock_time = require("mock_time")
local widget_class = dofile("plugins/autosuspend.koplugin/main.lua")
local widget = widget_class:new()
mock_time:increase(5)
local UIManager = require("ui/uimanager")
UIManager:handleInput()
assert.stub(UIManager.suspend).was.called(0)
widget:onInputEvent()
widget:onSuspend()
widget:onResume()
mock_time:increase(6)
UIManager:handleInput()
assert.stub(UIManager.suspend).was.called(0)
mock_time:increase(5)
UIManager:handleInput()
assert.stub(UIManager.suspend).was.called(1)
mock_time:uninstall()
describe("shutdown", function()
--- @todo duplicate with above, elegant way to DRY?
before_each(function()
local Device = require("device")
stub(Device, "isKobo")
Device.isKobo.returns(true)
stub(Device, "canPowerOff")
Device.canPowerOff.returns(true)
Device.input.waitEvent = function() end
local UIManager = require("ui/uimanager")
stub(UIManager, "poweroff_action")
UIManager._run_forever = true
G_reader_settings:saveSetting("autoshutdown_timeout_seconds", 10)
require("mock_time"):install()
end)
after_each(function()
require("device").isKobo:revert()
require("ui/uimanager").poweroff_action:revert()
G_reader_settings:delSetting("autoshutdown_timeout_seconds")
require("mock_time"):uninstall()
end)
it("should be able to execute suspend when timing out", function()
local mock_time = require("mock_time")
local widget_class = dofile("plugins/autosuspend.koplugin/main.lua")
local widget = widget_class:new() --luacheck: ignore
local UIManager = require("ui/uimanager")
mock_time:increase(5)
UIManager:handleInput()
assert.stub(UIManager.poweroff_action).was.called(0)
mock_time:increase(6)
UIManager:handleInput()
assert.stub(UIManager.poweroff_action).was.called(1)
mock_time:uninstall()
end)
it("should be able to deprecate last task", function()
local mock_time = require("mock_time")
local widget_class = dofile("plugins/autosuspend.koplugin/main.lua")
local widget = widget_class:new()
mock_time:increase(5)
local UIManager = require("ui/uimanager")
UIManager:handleInput()
assert.stub(UIManager.poweroff_action).was.called(0)
widget:onInputEvent()
widget:onSuspend()
widget:onResume()
mock_time:increase(6)
UIManager:handleInput()
assert.stub(UIManager.poweroff_action).was.called(0)
mock_time:increase(5)
UIManager:handleInput()
assert.stub(UIManager.poweroff_action).was.called(1)
mock_time:uninstall()
end)
end)
end)

@ -22,9 +22,9 @@ describe("UIManager spec", function()
{ time = future2, action = noop },
}
UIManager:_checkTasks()
assert.are.same(#UIManager._task_queue, 2)
assert.are.same(UIManager._task_queue[1].time, future)
assert.are.same(UIManager._task_queue[2].time, future2)
assert.are.same(2, #UIManager._task_queue, 2)
assert.are.same(future, UIManager._task_queue[1].time)
assert.are.same(future2, UIManager._task_queue[2].time)
end)
it("should calcualte wait_until properly in checkTasks routine", function()
@ -39,7 +39,7 @@ describe("UIManager spec", function()
{ time = {future[1] + 5, future[2]}, action = noop },
}
wait_until, now = UIManager:_checkTasks()
assert.are.same(wait_until, future)
assert.are.same(future, wait_until)
end)
it("should return nil wait_until properly in checkTasks routine", function()
@ -51,7 +51,7 @@ describe("UIManager spec", function()
{ time = now, action = noop },
}
wait_until, now = UIManager:_checkTasks()
assert.are.same(wait_until, nil)
assert.are.same(nil, wait_until)
end)
it("should insert new task properly in empty task queue", function()
@ -61,7 +61,7 @@ describe("UIManager spec", function()
assert.are.same(0, #UIManager._task_queue)
UIManager:scheduleIn(50, 'foo')
assert.are.same(1, #UIManager._task_queue)
assert.are.same(UIManager._task_queue[1].action, 'foo')
assert.are.same('foo', UIManager._task_queue[1].action)
end)
it("should insert new task properly in single task queue", function()
@ -74,7 +74,7 @@ describe("UIManager spec", function()
assert.are.same(1, #UIManager._task_queue)
UIManager:scheduleIn(150, 'quz')
assert.are.same(2, #UIManager._task_queue)
assert.are.same(UIManager._task_queue[1].action, 'quz')
assert.are.same('quz', UIManager._task_queue[1].action)
UIManager:quit()
UIManager._task_queue = {
@ -83,10 +83,10 @@ describe("UIManager spec", function()
assert.are.same(1, #UIManager._task_queue)
UIManager:scheduleIn(150, 'foo')
assert.are.same(2, #UIManager._task_queue)
assert.are.same(UIManager._task_queue[2].action, 'foo')
assert.are.same('foo', UIManager._task_queue[2].action)
UIManager:scheduleIn(155, 'bar')
assert.are.same(3, #UIManager._task_queue)
assert.are.same(UIManager._task_queue[3].action, 'bar')
assert.are.same('bar', UIManager._task_queue[3].action)
end)
it("should insert new task in ascendant order", function()
@ -151,7 +151,7 @@ describe("UIManager spec", function()
{ time = now, action = task_to_remove },
}
UIManager:_checkTasks()
assert.are.same(run_count, 2)
assert.are.same(2, run_count)
end)
it("should clear _task_queue_dirty bit before looping", function()
@ -181,8 +181,8 @@ describe("UIManager spec", function()
end
})
assert.equals(UIManager._window_stack[1].widget.x_prefix_test_number, 2)
assert.equals(UIManager._window_stack[2].widget.x_prefix_test_number, 1)
assert.equals(2, UIManager._window_stack[1].widget.x_prefix_test_number)
assert.equals(1, UIManager._window_stack[2].widget.x_prefix_test_number)
end)
it("should insert second modal widget on top of first modal widget", function()
UIManager:show({
@ -193,9 +193,9 @@ describe("UIManager spec", function()
end
})
assert.equals(UIManager._window_stack[1].widget.x_prefix_test_number, 2)
assert.equals(UIManager._window_stack[2].widget.x_prefix_test_number, 1)
assert.equals(UIManager._window_stack[3].widget.x_prefix_test_number, 3)
assert.equals(2, UIManager._window_stack[1].widget.x_prefix_test_number)
assert.equals(1, UIManager._window_stack[2].widget.x_prefix_test_number)
assert.equals(3, UIManager._window_stack[3].widget.x_prefix_test_number)
end)
end)
@ -306,9 +306,9 @@ describe("UIManager spec", function()
}
UIManager:sendEvent("foo")
assert.is.same(call_signals[1], 1)
assert.is.same(call_signals[2], 1)
assert.is.same(call_signals[3], 1)
assert.is.same(1, call_signals[1])
assert.is.same(1, call_signals[2])
assert.is.same(1, call_signals[3])
end)
it("should handle stack change when broadcasting events", function()
@ -350,7 +350,7 @@ describe("UIManager spec", function()
},
}
UIManager:broadcastEvent("foo")
assert.is.same(#UIManager._window_stack, 0)
assert.is.same(0, #UIManager._window_stack)
end)
it("should handle stack change when closing widgets", function()

@ -0,0 +1,55 @@
describe("WakeupMgr", function()
local RTC
local WakeupMgr
local epoch1, epoch2, epoch3
setup(function()
require("commonrequire")
package.unloadAll()
RTC = require("ffi/rtc")
WakeupMgr = require("device/wakeupmgr")
-- We could theoretically test this by running the tests as root locally.
stub(WakeupMgr, "setWakeupAlarm")
WakeupMgr.validateWakeupAlarmByProximity = spy.new(function() return true end)
epoch1 = RTC:secondsFromNowToEpoch(1234)
epoch2 = RTC:secondsFromNowToEpoch(123)
epoch3 = RTC:secondsFromNowToEpoch(9999)
end)
it("should add a task", function()
WakeupMgr:addTask(1234, function() end)
assert.is_equal(epoch1, WakeupMgr._task_queue[1].epoch)
assert.stub(WakeupMgr.setWakeupAlarm).was.called(1)
end)
it("should add a task in order", function()
WakeupMgr:addTask(9999, function() end)
assert.is_equal(epoch1, WakeupMgr._task_queue[1].epoch)
assert.stub(WakeupMgr.setWakeupAlarm).was.called(1)
WakeupMgr:addTask(123, function() end)
assert.is_equal(epoch2, WakeupMgr._task_queue[1].epoch)
assert.stub(WakeupMgr.setWakeupAlarm).was.called(2)
end)
it("should execute top task", function()
assert.is_true(WakeupMgr:wakeupAction())
end)
it("should have removed executed task from stack", function()
assert.is_equal(epoch1, WakeupMgr._task_queue[1].epoch)
assert.is_equal(epoch3, WakeupMgr._task_queue[2].epoch)
end)
it("should have scheduled next task after execution", function()
assert.stub(WakeupMgr.setWakeupAlarm).was.called(3)
end)
it("should remove arbitrary task from stack", function()
WakeupMgr:removeTask(2)
assert.is_equal(epoch1, WakeupMgr._task_queue[1].epoch)
assert.is_equal(nil, WakeupMgr._task_queue[2])
end)
it("should execute last task", function()
assert.is_true(WakeupMgr:wakeupAction())
end)
it("should not have scheduled a wakeup without a task", function()
assert.stub(WakeupMgr.setWakeupAlarm).was.called(3)
end)
end)
Loading…
Cancel
Save