mirror of https://github.com/koreader/koreader
Move kobo auto-suspension logic out of UIManager (#2933)
parent
c8be27481c
commit
7d2ed4c3d0
@ -0,0 +1,101 @@
|
|||||||
|
--[[--
|
||||||
|
HookContainer allows listeners to register and unregister a hook for speakers to execute.
|
||||||
|
|
||||||
|
It's an experimental feature: use with cautions, it can easily pin an object in memory and unblock
|
||||||
|
GC from recycling the memory.
|
||||||
|
]]
|
||||||
|
|
||||||
|
local HookContainer = {}
|
||||||
|
|
||||||
|
function HookContainer:new(o)
|
||||||
|
o = o or {}
|
||||||
|
setmetatable(o, self)
|
||||||
|
self.__index = self
|
||||||
|
return o
|
||||||
|
end
|
||||||
|
|
||||||
|
function HookContainer:_assertIsValidName(name)
|
||||||
|
assert(self ~= nil)
|
||||||
|
assert(type(name) == "string")
|
||||||
|
assert(string.len(name) > 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function HookContainer:_assertIsValidFunction(func)
|
||||||
|
assert(self ~= nil)
|
||||||
|
assert(type(func) == "function" or type(func) == "table")
|
||||||
|
end
|
||||||
|
|
||||||
|
function HookContainer:_assertIsValidFunctionOrNil(func)
|
||||||
|
assert(self ~= nil)
|
||||||
|
if func == nil then return end
|
||||||
|
self:_assertIsValidFunction(func)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Register a function to name. Must be called with self.
|
||||||
|
-- @tparam string name The name of the hook. Can only be an non-empty string.
|
||||||
|
-- @tparam function func The function to handle the hook. Can only be a function.
|
||||||
|
function HookContainer:register(name, func)
|
||||||
|
self:_assertIsValidName(name)
|
||||||
|
self:_assertIsValidFunction(func)
|
||||||
|
if self[name] == nil then
|
||||||
|
self[name] = {}
|
||||||
|
end
|
||||||
|
table.insert(self[name], func)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Register a widget to name. Must be called with self.
|
||||||
|
-- @tparam string name The name of the hook. Can only be an non-empty string.
|
||||||
|
-- @tparam table widget The widget to handle the hook. Can only be a table with required functions.
|
||||||
|
function HookContainer:registerWidget(name, widget)
|
||||||
|
self:_assertIsValidName(name)
|
||||||
|
assert(type(widget) == "table")
|
||||||
|
self:register(name, function(args)
|
||||||
|
local f = widget["on" .. name]
|
||||||
|
self:_assertIsValidFunction(f)
|
||||||
|
f(widget, args)
|
||||||
|
end)
|
||||||
|
local original_close_widget = widget.onCloseWidget
|
||||||
|
self:_assertIsValidFunctionOrNil(original_close_widget)
|
||||||
|
widget.onCloseWidget = function()
|
||||||
|
if original_close_widget then original_close_widget(widget) end
|
||||||
|
self:unregister(name, widget["on" .. name])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Unregister a function from name. Must be called with self.
|
||||||
|
-- @tparam string name The name of the hook. Can only be an non-empty string.
|
||||||
|
-- @tparam function func The function to handle the hook. Can only be a function.
|
||||||
|
-- @treturn boolean Return true if the function is found and removed, otherwise false.
|
||||||
|
function HookContainer:unregister(name, func)
|
||||||
|
self:_assertIsValidName(name)
|
||||||
|
self:_assertIsValidFunction(func)
|
||||||
|
if self[name] == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
for i, f in ipairs(self[name]) do
|
||||||
|
if f == func then
|
||||||
|
table.remove(self[name], i)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Execute all registered functions of name. Must be called with self.
|
||||||
|
-- @tparam string name The name of the hook. Can only be an non-empty string.
|
||||||
|
-- @param args Any kind of arguments sending to the functions.
|
||||||
|
-- @treturn number The number of functions have been executed.
|
||||||
|
function HookContainer:execute(name, args)
|
||||||
|
self:_assertIsValidName(name)
|
||||||
|
if self[name] == nil or #self[name] == 0 then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, f in ipairs(self[name]) do
|
||||||
|
f(args)
|
||||||
|
end
|
||||||
|
return #self[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
return HookContainer
|
@ -0,0 +1,121 @@
|
|||||||
|
local Device = require("device")
|
||||||
|
|
||||||
|
if not Device:isKobo() and not Device:isSDL() then return { disabled = true, } end
|
||||||
|
|
||||||
|
local DataStorage = require("datastorage")
|
||||||
|
local LuaSettings = require("luasettings")
|
||||||
|
local UIManager = require("ui/uimanager")
|
||||||
|
local WidgetContainer = require("ui/widget/container/widgetcontainer")
|
||||||
|
local logger = require("logger")
|
||||||
|
local _ = require("gettext")
|
||||||
|
|
||||||
|
local AutoSuspend = {
|
||||||
|
settings = LuaSettings:open(DataStorage:getSettingsDir() .. "/koboautosuspend.lua"),
|
||||||
|
settings_id = 0,
|
||||||
|
last_action_sec = os.time(),
|
||||||
|
}
|
||||||
|
|
||||||
|
function AutoSuspend:_readTimeoutSecFrom(settings)
|
||||||
|
local sec = settings:readSetting("auto_suspend_timeout_seconds")
|
||||||
|
if type(sec) == "number" then
|
||||||
|
return sec
|
||||||
|
end
|
||||||
|
return -1
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoSuspend:_readTimeoutSec()
|
||||||
|
local candidates = { self.settings, G_reader_settings }
|
||||||
|
for _, candidate in ipairs(candidates) do
|
||||||
|
local sec = self:_readTimeoutSecFrom(candidate)
|
||||||
|
if sec ~= -1 then
|
||||||
|
return sec
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- default setting is 60 minutes
|
||||||
|
return 60 * 60
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoSuspend:_enabled()
|
||||||
|
return self.auto_suspend_sec > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoSuspend:_schedule(settings_id)
|
||||||
|
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 = self.last_action_sec + self.auto_suspend_sec - os.time()
|
||||||
|
if delay <= 0 then
|
||||||
|
logger.dbg("AutoSuspend: will suspend the device")
|
||||||
|
UIManager:suspend()
|
||||||
|
else
|
||||||
|
logger.dbg("AutoSuspend: schedule at ", os.time() + delay)
|
||||||
|
UIManager:scheduleIn(delay, function() self:_schedule(settings_id) end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoSuspend:_deprecateLastTask()
|
||||||
|
self.settings_id = self.settings_id + 1
|
||||||
|
logger.dbg("AutoSuspend: deprecateLastTask ", self.settings_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoSuspend:_start()
|
||||||
|
if self:_enabled() then
|
||||||
|
logger.dbg("AutoSuspend: start at ", os.time())
|
||||||
|
self.last_action_sec = os.time()
|
||||||
|
self:_schedule(self.settings_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoSuspend:init()
|
||||||
|
UIManager.event_hook:registerWidget("InputEvent", self)
|
||||||
|
self.auto_suspend_sec = self:_readTimeoutSec()
|
||||||
|
self:_deprecateLastTask()
|
||||||
|
self:_start()
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoSuspend:onInputEvent()
|
||||||
|
logger.dbg("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()
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoSuspend:onResume()
|
||||||
|
logger.dbg("AutoSuspend: onResume")
|
||||||
|
self:_start()
|
||||||
|
end
|
||||||
|
|
||||||
|
AutoSuspend:init()
|
||||||
|
|
||||||
|
local AutoSuspendWidget = WidgetContainer:new{
|
||||||
|
name = "AutoSuspend",
|
||||||
|
}
|
||||||
|
|
||||||
|
function AutoSuspendWidget:onInputEvent()
|
||||||
|
AutoSuspend:onInputEvent()
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoSuspendWidget:onSuspend()
|
||||||
|
AutoSuspend:onSuspend()
|
||||||
|
end
|
||||||
|
|
||||||
|
function AutoSuspendWidget:onResume()
|
||||||
|
AutoSuspend:onResume()
|
||||||
|
end
|
||||||
|
|
||||||
|
return AutoSuspendWidget
|
@ -0,0 +1,74 @@
|
|||||||
|
describe("AutoSuspend widget tests", function()
|
||||||
|
setup(function()
|
||||||
|
require("commonrequire")
|
||||||
|
package.unloadAll()
|
||||||
|
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)
|
||||||
|
|
||||||
|
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()
|
||||||
|
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()
|
||||||
|
-- So if one more initialization happens, it won't sleep after another 5 seconds.
|
||||||
|
mock_time:increase(5)
|
||||||
|
local widget2 = widget_class:new()
|
||||||
|
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 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)
|
@ -0,0 +1,72 @@
|
|||||||
|
describe("HookContainer tests", function()
|
||||||
|
setup(function()
|
||||||
|
require("commonrequire")
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should register and unregister functions", function()
|
||||||
|
local HookContainer = require("ui/hook_container"):new()
|
||||||
|
local f1 = spy.new(function() end)
|
||||||
|
local f2 = spy.new(function() end)
|
||||||
|
local f3 = spy.new(function() end)
|
||||||
|
HookContainer:register("a", f1)
|
||||||
|
HookContainer:register("a", f2)
|
||||||
|
HookContainer:register("b", f3)
|
||||||
|
assert.are.equal(HookContainer:execute("a", 100), 2)
|
||||||
|
assert.are.equal(HookContainer:execute("b", 200), 1)
|
||||||
|
assert.spy(f1).was_called(1)
|
||||||
|
assert.spy(f1).was_called_with(100)
|
||||||
|
assert.spy(f2).was_called(1)
|
||||||
|
assert.spy(f2).was_called_with(100)
|
||||||
|
assert.spy(f3).was_called(1)
|
||||||
|
assert.spy(f3).was_called_with(200)
|
||||||
|
|
||||||
|
assert.is.truthy(HookContainer:unregister("a", f1))
|
||||||
|
assert.is.falsy(HookContainer:unregister("b", f2))
|
||||||
|
|
||||||
|
assert.are.equal(HookContainer:execute("a", 300), 1)
|
||||||
|
assert.are.equal(HookContainer:execute("b", 400), 1)
|
||||||
|
assert.spy(f1).was_called(1)
|
||||||
|
assert.spy(f1).was_called_with(100)
|
||||||
|
assert.spy(f2).was_called(2)
|
||||||
|
assert.spy(f2).was_called_with(300)
|
||||||
|
assert.spy(f3).was_called(2)
|
||||||
|
assert.spy(f3).was_called_with(400)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should register and automatically unregister widget", function()
|
||||||
|
local HookContainer = require("ui/hook_container"):new()
|
||||||
|
local widget = require("ui/widget/widget"):new()
|
||||||
|
widget.onEvent = spy.new(function() end)
|
||||||
|
local close_widget = spy.new(function() end)
|
||||||
|
widget.onCloseWidget = close_widget
|
||||||
|
HookContainer:registerWidget("Event", widget)
|
||||||
|
assert.are.equal(HookContainer:execute("Event", { a = 100, b = 200 }), 1)
|
||||||
|
assert.spy(widget.onEvent).was_called(1)
|
||||||
|
assert.spy(widget.onEvent).was_called_with(widget, { a = 100, b = 200 })
|
||||||
|
|
||||||
|
widget:onCloseWidget()
|
||||||
|
assert.spy(close_widget).was_called(1)
|
||||||
|
assert.spy(close_widget).was_called_with(widget)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should pass widget itself", function()
|
||||||
|
local HookContainer = require("ui/hook_container"):new()
|
||||||
|
local widget = require("ui/widget/widget"):new()
|
||||||
|
local onEvent_called = false
|
||||||
|
local onCloseWidget_called = false
|
||||||
|
function widget:onEvent(args)
|
||||||
|
assert.is.truthy(self ~= nil)
|
||||||
|
assert.are.same(args, { c = 300, d = 400 })
|
||||||
|
onEvent_called = true
|
||||||
|
end
|
||||||
|
function widget:onCloseWidget()
|
||||||
|
assert.is.truthy(self ~= nil)
|
||||||
|
onCloseWidget_called = true
|
||||||
|
end
|
||||||
|
HookContainer:registerWidget("Event", widget)
|
||||||
|
assert.are.equal(HookContainer:execute("Event", { c = 300, d = 400 }), 1)
|
||||||
|
widget:onCloseWidget()
|
||||||
|
assert.is.truthy(onEvent_called)
|
||||||
|
assert.is.truthy(onCloseWidget_called)
|
||||||
|
end)
|
||||||
|
end)
|
@ -0,0 +1,52 @@
|
|||||||
|
describe("MockTime tests", function()
|
||||||
|
teardown(function()
|
||||||
|
require("mock_time"):uninstall()
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should be able to install and uninstall", function()
|
||||||
|
local mock_time = require("mock_time")
|
||||||
|
local util = require("ffi/util")
|
||||||
|
local current_time = os.time()
|
||||||
|
local current_highres_sec = util.gettime()
|
||||||
|
mock_time:install()
|
||||||
|
assert.is.truthy(mock_time:set(10))
|
||||||
|
assert.are.equal(os.time(), 10)
|
||||||
|
local sec, usec = util.gettime()
|
||||||
|
assert.are.equal(sec, 10)
|
||||||
|
assert.are.equal(usec, 0)
|
||||||
|
mock_time:uninstall()
|
||||||
|
assert.is.truthy(os.time() >= current_time)
|
||||||
|
assert.is.truthy(util.gettime() >= current_highres_sec)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should be able to install several times", function()
|
||||||
|
local mock_time = require("mock_time")
|
||||||
|
local util = require("ffi/util")
|
||||||
|
local current_time = os.time()
|
||||||
|
local current_highres_sec = util.gettime()
|
||||||
|
mock_time:install()
|
||||||
|
mock_time:install()
|
||||||
|
mock_time:uninstall()
|
||||||
|
assert.is.truthy(os.time() >= current_time)
|
||||||
|
assert.is.truthy(util.gettime() >= current_highres_sec)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should reject invalid value", function()
|
||||||
|
local mock_time = require("mock_time")
|
||||||
|
assert.is.falsy(mock_time:set("100"))
|
||||||
|
assert.is.falsy(mock_time:set(true))
|
||||||
|
assert.is.falsy(mock_time:set(nil))
|
||||||
|
assert.is.falsy(mock_time:set(function() end))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it("should increase time", function()
|
||||||
|
local mock_time = require("mock_time")
|
||||||
|
local current_time = os.time()
|
||||||
|
mock_time:install()
|
||||||
|
assert.is.truthy(mock_time:set(10.1))
|
||||||
|
assert.are.equal(os.time(), 10)
|
||||||
|
mock_time:increase(1)
|
||||||
|
assert.are.equal(os.time(), 11)
|
||||||
|
mock_time:uninstall()
|
||||||
|
end)
|
||||||
|
end)
|
Loading…
Reference in New Issue