mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
[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.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
187
frontend/device/wakeupmgr.lua
Normal file
187
frontend/device/wakeupmgr.lua
Normal file
@@ -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()
|
||||
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)
|
||||
|
||||
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 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)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
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)
|
||||
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 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()
|
||||
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.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()
|
||||
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()
|
||||
|
||||
55
spec/unit/wakeupmgr_spec.lua
Normal file
55
spec/unit/wakeupmgr_spec.lua
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user