feat(koplugin): pull sync on device wake with book open (#4928)
Waking the device with a book already open now pulls progress, annotations and stats like reopening the book does, instead of requiring a manual sync or book reopen. The pull is delayed 1s so Wi-Fi can come back up after wake (same delay upstream kosync uses on resume), debounced against rapid Suspend/Resume pairs (Android fires them on focus changes), and the pending task is dropped on widget close so it cannot run against a torn-down ReaderUI. Closes #4924 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -797,6 +797,33 @@ function ReadestSync:onPageUpdate(page)
|
||||
end
|
||||
end
|
||||
|
||||
-- Waking the device with a book already open should pull like reopening
|
||||
-- the book does (issue #4924). Delayed because Wi-Fi is usually still
|
||||
-- coming back up right after wake (kosync uses the same 1s delay); the
|
||||
-- pull helpers rerun themselves when online if that isn't enough. On
|
||||
-- devices where Suspend/Resume also fire on focus changes (Android),
|
||||
-- the debounce keeps this from hammering the API.
|
||||
function ReadestSync:onResume()
|
||||
if not (self.settings.auto_sync and self.settings.access_token and self.ui.document) then
|
||||
return
|
||||
end
|
||||
local now = os.time()
|
||||
if now - (self.last_resume_sync_timestamp or 0) <= API_CALL_DEBOUNCE_DELAY then
|
||||
return
|
||||
end
|
||||
self.last_resume_sync_timestamp = now
|
||||
if self.resume_pull_task then
|
||||
UIManager:unschedule(self.resume_pull_task)
|
||||
end
|
||||
self.resume_pull_task = function()
|
||||
self.resume_pull_task = nil
|
||||
self:pullBookConfig(false)
|
||||
self:pullBookNotes(false)
|
||||
self:pullBookStats(false)
|
||||
end
|
||||
UIManager:scheduleIn(1, self.resume_pull_task)
|
||||
end
|
||||
|
||||
function ReadestSync:onAnnotationsModified(items)
|
||||
-- A removal fires AnnotationsModified with a negative index_modified and the
|
||||
-- deleted item at items[1]. Capture a tombstone now, before the item is gone
|
||||
@@ -818,6 +845,10 @@ function ReadestSync:onCloseWidget()
|
||||
UIManager:unschedule(self.delayed_push_task)
|
||||
self.delayed_push_task = nil
|
||||
end
|
||||
if self.resume_pull_task then
|
||||
UIManager:unschedule(self.resume_pull_task)
|
||||
self.resume_pull_task = nil
|
||||
end
|
||||
end
|
||||
|
||||
function ReadestSync:deletePluginSettings()
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
-- main_resume_spec.lua
|
||||
-- Tests for ReadestSync:onResume (issue #4924): waking the device with a
|
||||
-- book already open should pull config/notes/stats like reopening the book
|
||||
-- does, debounced, and the delayed task must not outlive the widget.
|
||||
|
||||
require("spec_helper")
|
||||
|
||||
-- Minimal KOReader stubs main.lua pulls at require-time.
|
||||
local UIManagerStub = {
|
||||
_scheduled = {},
|
||||
show = function() end,
|
||||
nextTick = function(self, fn)
|
||||
table.insert(self._scheduled, { delay = 0, fn = fn })
|
||||
end,
|
||||
scheduleIn = function(self, delay, fn)
|
||||
table.insert(self._scheduled, { delay = delay, fn = fn })
|
||||
end,
|
||||
unschedule = function(self, fn)
|
||||
for i = #self._scheduled, 1, -1 do
|
||||
if self._scheduled[i].fn == fn then
|
||||
table.remove(self._scheduled, i)
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
package.preload["dispatcher"] = function()
|
||||
return { registerAction = function() end }
|
||||
end
|
||||
package.preload["ui/event"] = function()
|
||||
return { new = function(_, name) return { name = name } end }
|
||||
end
|
||||
package.preload["ui/widget/infomessage"] = function()
|
||||
return { new = function(_, o) return o or {} end }
|
||||
end
|
||||
package.preload["ui/widget/keyvaluepage"] = function()
|
||||
return { new = function(_, o) return o or {} end }
|
||||
end
|
||||
package.preload["ui/widget/multiinputdialog"] = function()
|
||||
return { new = function(_, o) return o or {} end }
|
||||
end
|
||||
package.preload["ui/widget/container/widgetcontainer"] = function()
|
||||
return {
|
||||
new = function(self, o)
|
||||
o = o or {}
|
||||
setmetatable(o, { __index = self })
|
||||
if o.init then o:init() end
|
||||
return o
|
||||
end,
|
||||
}
|
||||
end
|
||||
package.preload["ui/network/manager"] = function()
|
||||
return {
|
||||
willRerunWhenOnline = function() return false end,
|
||||
goOnlineToRun = function(_, cb) cb() end,
|
||||
}
|
||||
end
|
||||
package.preload["ui/uimanager"] = function() return UIManagerStub end
|
||||
package.preload["ffi/sha2"] = function()
|
||||
return { base64_to_bin = function(s) return s end }
|
||||
end
|
||||
package.preload["ffi/util"] = function()
|
||||
return { template = function(s) return s end }
|
||||
end
|
||||
package.preload["util"] = function() return {} end
|
||||
package.preload["readest_i18n"] = function()
|
||||
return function(s) return s end
|
||||
end
|
||||
|
||||
local ReadestSync = require("main")
|
||||
|
||||
-- Bare plugin instance: skips init() (menu/dispatcher/meta wiring) and
|
||||
-- fakes the pull methods so tests observe what onResume triggers.
|
||||
local function makePlugin(opts)
|
||||
local plugin = setmetatable({
|
||||
settings = {
|
||||
auto_sync = opts.auto_sync,
|
||||
access_token = opts.access_token,
|
||||
},
|
||||
ui = { document = opts.document },
|
||||
pull_calls = {},
|
||||
}, { __index = ReadestSync })
|
||||
for _, method in ipairs({ "pullBookConfig", "pullBookNotes", "pullBookStats" }) do
|
||||
plugin[method] = function(self, interactive)
|
||||
table.insert(self.pull_calls, { method = method, interactive = interactive })
|
||||
end
|
||||
end
|
||||
return plugin
|
||||
end
|
||||
|
||||
describe("ReadestSync:onResume", function()
|
||||
before_each(function()
|
||||
UIManagerStub._scheduled = {}
|
||||
end)
|
||||
|
||||
it("schedules a delayed pull of config, notes and stats", function()
|
||||
local plugin = makePlugin({ auto_sync = true, access_token = "tok", document = {} })
|
||||
|
||||
plugin:onResume()
|
||||
|
||||
assert.are.equal(1, #UIManagerStub._scheduled)
|
||||
-- Delayed, not immediate: Wi-Fi is still coming back up right after wake.
|
||||
assert.is_true(UIManagerStub._scheduled[1].delay > 0)
|
||||
|
||||
UIManagerStub._scheduled[1].fn()
|
||||
assert.are.equal(3, #plugin.pull_calls)
|
||||
local pulled = {}
|
||||
for _, call in ipairs(plugin.pull_calls) do
|
||||
pulled[call.method] = true
|
||||
assert.is_false(call.interactive)
|
||||
end
|
||||
assert.is_true(pulled.pullBookConfig)
|
||||
assert.is_true(pulled.pullBookNotes)
|
||||
assert.is_true(pulled.pullBookStats)
|
||||
end)
|
||||
|
||||
it("does nothing without an open document (FileManager context)", function()
|
||||
local plugin = makePlugin({ auto_sync = true, access_token = "tok", document = nil })
|
||||
plugin:onResume()
|
||||
assert.are.equal(0, #UIManagerStub._scheduled)
|
||||
end)
|
||||
|
||||
it("does nothing when auto sync is disabled", function()
|
||||
local plugin = makePlugin({ auto_sync = false, access_token = "tok", document = {} })
|
||||
plugin:onResume()
|
||||
assert.are.equal(0, #UIManagerStub._scheduled)
|
||||
end)
|
||||
|
||||
it("does nothing when signed out", function()
|
||||
local plugin = makePlugin({ auto_sync = true, access_token = nil, document = {} })
|
||||
plugin:onResume()
|
||||
assert.are.equal(0, #UIManagerStub._scheduled)
|
||||
end)
|
||||
|
||||
it("debounces resume events fired in quick succession", function()
|
||||
local plugin = makePlugin({ auto_sync = true, access_token = "tok", document = {} })
|
||||
|
||||
plugin:onResume()
|
||||
plugin:onResume()
|
||||
assert.are.equal(1, #UIManagerStub._scheduled)
|
||||
|
||||
-- Simulate the pending task firing.
|
||||
local task = table.remove(UIManagerStub._scheduled, 1)
|
||||
task.fn()
|
||||
|
||||
-- Still inside the debounce window: no new pull.
|
||||
plugin:onResume()
|
||||
assert.are.equal(0, #UIManagerStub._scheduled)
|
||||
|
||||
-- Once the debounce window has passed, resume pulls again.
|
||||
plugin.last_resume_sync_timestamp = os.time() - 60
|
||||
plugin:onResume()
|
||||
assert.are.equal(1, #UIManagerStub._scheduled)
|
||||
end)
|
||||
|
||||
it("unschedules the pending pull when the widget closes", function()
|
||||
local plugin = makePlugin({ auto_sync = true, access_token = "tok", document = {} })
|
||||
|
||||
plugin:onResume()
|
||||
assert.are.equal(1, #UIManagerStub._scheduled)
|
||||
|
||||
plugin:onCloseWidget()
|
||||
assert.are.equal(0, #UIManagerStub._scheduled)
|
||||
end)
|
||||
end)
|
||||
Reference in New Issue
Block a user