Move kobo auto-suspension logic out of UIManager (#2933)

pull/2981/head
Hzj_jie 7 years ago committed by GitHub
parent c8be27481c
commit 7d2ed4c3d0

@ -47,7 +47,10 @@ function Dbg:turnOn()
return unpack(values)
end
end
Dbg.dassert = function(check, msg) assert(check, msg) end
Dbg.dassert = function(check, msg)
assert(check, msg)
return check
end
-- TODO: close ev.log fd for children
-- create or clear ev log file
@ -60,7 +63,9 @@ function Dbg:turnOff()
logger:setLevel(logger.levels.info)
function Dbg_mt.__call() end
function Dbg.guard() end
function Dbg.dassert() end
Dbg.dassert = function(check)
return check
end
if self.ev_log then
io.close(self.ev_log)
self.ev_log = nil

@ -59,4 +59,22 @@ function Device:init()
Generic.init(self)
end
function Device:simulateSuspend()
local InfoMessage = require("ui/widget/infomessage")
local UIManager = require("ui/uimanager")
local _ = require("gettext")
UIManager:show(InfoMessage:new{
text = _("Suspend")
})
end
function Device:simulateResume()
local InfoMessage = require("ui/widget/infomessage")
local UIManager = require("ui/uimanager")
local _ = require("gettext")
UIManager:show(InfoMessage:new{
text = _("Resume")
})
end
return Device

@ -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

@ -8,11 +8,10 @@ local Geom = require("ui/geometry")
local dbg = require("dbg")
local logger = require("logger")
local util = require("ffi/util")
local _ = require("gettext")
local Input = Device.input
local Screen = Device.screen
local _ = require("gettext")
local noop = function() end
local MILLION = 1000000
-- there is only one instance of this
@ -34,6 +33,8 @@ local UIManager = {
_refresh_func_stack = {},
_entered_poweroff_stage = false,
_exit_code = nil,
event_hook = require("ui/hook_container"):new()
}
function UIManager:init()
@ -72,28 +73,12 @@ function UIManager:init()
-- 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:_initAutoSuspend()
self.event_handlers["Suspend"] = function()
self:_beforeSuspend()
if self._stopAutoSuspend then
-- TODO(Hzj-jie): Why _stopAutoSuspend could be nil in test cases.
--[[
frontend/ui/uimanager.lua:62: attempt to call method _stopAutoSuspend (a nil value)
stack traceback:
frontend/ui/uimanager.lua:62: in function Suspend
frontend/ui/uimanager.lua:119: in function __default__
frontend/ui/uimanager.lua:662: in function handleInput
frontend/ui/uimanager.lua:707: in function run
spec/front/unit/readerui_spec.lua:32: in function <spec/front/unit/readerui_spec.lua:28>
--]]
self:_stopAutoSuspend()
end
Device:onPowerEvent("Suspend")
end
self.event_handlers["Resume"] = function()
Device:onPowerEvent("Resume")
self:_startAutoSuspend()
self:_afterResume()
end
self.event_handlers["PowerPress"] = function()
@ -167,6 +152,15 @@ function UIManager:init()
Device:usbPlugOut()
self:_afterNotCharging()
end
elseif Device:isSDL() then
self.event_handlers["Suspend"] = function()
self:_beforeSuspend()
Device:simulateSuspend()
end
self.event_handlers["Resume"] = function()
Device:simulateResume()
self:_afterResume()
end
end
end
@ -714,7 +708,9 @@ function UIManager:handleInput()
-- delegate input_event to handler
if input_event then
self:_resetAutoSuspendTimer()
if input_event.handler ~= "onInputError" then
self.event_hook:execute("InputEvent", input_event)
end
local handler = self.event_handlers[input_event]
if handler then
handler(input_event)
@ -780,57 +776,6 @@ function UIManager:runForever()
return self:run()
end
-- Kobo does not have an auto suspend function, so we implement it ourselves.
function UIManager:_initAutoSuspend()
local function isAutoSuspendEnabled()
return Device:isKobo() and self.auto_suspend_sec > 0
end
local sec = G_reader_settings:readSetting("auto_suspend_timeout_seconds")
if sec then
self.auto_suspend_sec = sec
else
-- default setting is 60 minutes
self.auto_suspend_sec = 60 * 60
end
if isAutoSuspendEnabled() then
self.auto_suspend_action = function()
local now = os.time()
-- Do not repeat auto suspend procedure after suspend.
if self.last_action_sec + self.auto_suspend_sec <= now then
self:suspend()
else
self:scheduleIn(
self.last_action_sec + self.auto_suspend_sec - now,
self.auto_suspend_action)
end
end
function UIManager:_startAutoSuspend()
self.last_action_sec = os.time()
self:nextTick(self.auto_suspend_action)
end
dbg:guard(UIManager, '_startAutoSuspend',
function()
assert(isAutoSuspendEnabled())
end)
function UIManager:_stopAutoSuspend()
self:unschedule(self.auto_suspend_action)
end
function UIManager:_resetAutoSuspendTimer()
self.last_action_sec = os.time()
end
self:_startAutoSuspend()
else
self._startAutoSuspend = noop
self._stopAutoSuspend = noop
end
end
-- The common operations should be performed before suspending the device. Ditto.
function UIManager:_beforeSuspend()
self:flushSettings()
@ -853,7 +798,7 @@ end
-- Executes all the operations of a suspending request. This function usually puts the device into
-- suspension.
function UIManager:suspend()
if Device:isKobo() then
if Device:isKobo() or Device:isSDL() then
self.event_handlers["Suspend"]()
elseif Device:isKindle() then
self.event_handlers["IntoSS"]()
@ -862,7 +807,7 @@ end
-- Executes all the operations of a resume request. This function usually wakes up the device.
function UIManager:resume()
if Device:isKobo() then
if Device:isKobo() or Device:isSDL() then
self.event_handlers["Resume"]()
elseif Device:isKindle() then
self.event_handlers["OutOfSS"]()
@ -879,7 +824,5 @@ function UIManager:restartKOReader()
self._exit_code = 85
end
UIManager._resetAutoSuspendTimer = noop
UIManager:init()
return UIManager

@ -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)

@ -18,12 +18,11 @@ describe("device module", function()
end
}
require("commonrequire")
package.unloadAll()
end)
before_each(function()
package.loaded['ffi/framebuffer_mxcfb'] = mock_fb
package.loaded['device/kindle/device'] = nil
package.loaded['device/kobo/device'] = nil
mock_input = require('device/input')
stub(mock_input, "open")
stub(os, "getenv")
@ -171,7 +170,6 @@ describe("device module", function()
stub(Device, "isKobo")
Device.isKobo.returns(true)
local saved_noop = UIManager._resetAutoSuspendTimer
UIManager:init()
ReaderUI:doShowReader(sample_pdf)
@ -185,9 +183,6 @@ describe("device module", function()
Device.powerd.beforeSuspend:revert()
Device.isKobo:revert()
readerui.onFlushSettings:revert()
UIManager._startAutoSuspend = nil
UIManager._stopAutoSuspend = nil
UIManager._resetAutoSuspendTimer = saved_noop
readerui:onClose()
end)
end)
@ -251,6 +246,7 @@ describe("device module", function()
end)
it("oasis should interpret orientation event", function()
package.unload('device/kindle/device')
io.open = function(filename, mode)
if filename == "/proc/usid" then
return {

@ -2,11 +2,12 @@ describe("FileManager module", function()
local FileManager, lfs, docsettings, UIManager, Screen, util
setup(function()
require("commonrequire")
package.unloadAll()
FileManager = require("apps/filemanager/filemanager")
lfs = require("libs/libkoreader-lfs")
docsettings = require("docsettings")
UIManager = require("ui/uimanager")
Screen = require("device").screen
UIManager = require("ui/uimanager")
docsettings = require("docsettings")
lfs = require("libs/libkoreader-lfs")
util = require("ffi/util")
end)
it("should show file manager", function()

@ -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)

@ -1,3 +1,5 @@
local logger = require("logger")
local MockTime = {
original_os_time = os.time,
original_util_time = nil,
@ -11,8 +13,14 @@ function MockTime:install()
self.original_util_time = util.gettime
assert(self.original_util_time ~= nil)
end
os.time = function() return self.value end
util.gettime = function() return self.value, 0 end
os.time = function()
logger.dbg("MockTime:os.time: ", self.value)
return self.value
end
util.gettime = function()
logger.dbg("MockTime:util.gettime: ", self.value)
return self.value, 0
end
end
function MockTime:uninstall()
@ -30,6 +38,7 @@ function MockTime:set(value)
return false
end
self.value = math.floor(value)
logger.dbg("MockTime:set ", self.value)
return true
end
@ -39,6 +48,7 @@ function MockTime:increase(value)
return false
end
self.value = math.floor(self.value + value)
logger.dbg("MockTime:increase ", self.value)
return true
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)

@ -4,13 +4,13 @@ describe("Readerfooter module", function()
setup(function()
require("commonrequire")
package.unloadAll()
DEBUG = require("dbg")
DocumentRegistry = require("document/documentregistry")
ReaderUI = require("apps/reader/readerui")
ReaderUI = require("apps/reader/readerui")
DocSettings = require("docsettings")
UIManager = require("ui/uimanager")
MenuSorter = require("ui/menusorter")
DEBUG = require("dbg")
ReaderUI = require("apps/reader/readerui")
UIManager = require("ui/uimanager")
purgeDir = require("ffi/util").purgeDir
Screen = require("device").screen

@ -2,13 +2,14 @@ describe("Readerhighlight module", function()
local DocumentRegistry, ReaderUI, UIManager, Screen, Geom, dbg, Event
setup(function()
require("commonrequire")
package.unloadAll()
DocumentRegistry = require("document/documentregistry")
Event = require("ui/event")
Geom = require("ui/geometry")
ReaderUI = require("apps/reader/readerui")
UIManager = require("ui/uimanager")
Screen = require("device").screen
Geom = require("ui/geometry")
UIManager = require("ui/uimanager")
dbg = require("dbg")
Event = require("ui/event")
end)
local function highlight_single_word(readerui, pos0)

@ -3,6 +3,7 @@ describe("ReaderLink module", function()
setup(function()
require("commonrequire")
package.unloadAll()
DocumentRegistry = require("document/documentregistry")
Event = require("ui/event")
ReaderUI = require("apps/reader/readerui")

@ -3,6 +3,7 @@ describe("Readerview module", function()
setup(function()
require("commonrequire")
package.unloadAll()
DocumentRegistry = require("document/documentregistry")
Blitbuffer = require("ffi/blitbuffer")
ReaderUI = require("apps/reader/readerui")

@ -164,35 +164,6 @@ describe("UIManager spec", function()
assert.is_true(UIManager._task_queue_dirty)
end)
it("should setup auto suspend on kobo", function()
local old_reset_timer = UIManager._resetAutoSuspendTimer
local noop = old_reset_timer
assert.falsy(UIManager._startAutoSuspend)
assert.falsy(UIManager._stopAutoSuspend)
assert.truthy(old_reset_timer)
G_reader_settings:saveSetting("auto_suspend_timeout_seconds", 3600)
UIManager:run()
UIManager:quit()
-- should skip on non-kobo devices
UIManager:_initAutoSuspend()
assert.is.same(noop, UIManager._startAutoSuspend)
assert.is.same(noop, UIManager._stopAutoSuspend)
assert.truthy(old_reset_timer)
assert.is.same(#UIManager._task_queue, 0)
-- now test kobo devices
local old_is_kobo = Device.isKobo
Device.isKobo = function() return true end
UIManager:_initAutoSuspend()
assert.truthy(UIManager._startAutoSuspend)
assert.truthy(UIManager._stopAutoSuspend)
assert.is_not.same(UIManager._resetAutoSuspendTimer, old_reset_timer)
assert.is.same(#UIManager._task_queue, 1)
assert.is.same(UIManager._task_queue[1].action,
UIManager.auto_suspend_action)
Device.isKobo = old_is_kobo
end)
it("should check active widgets in order", function()
local call_signals = {false, false, false}
UIManager._window_stack = {

Loading…
Cancel
Save