Files
koreader/plugins/kosync.koplugin/main.lua
NiLuJe 34ba2fab30 NetworkMgr: Handle non-blocking turnOnWifi implementations better (#10863)
* Device: Add a `hasSeamlessWifiToggle` devcap to complement `hasWifiToggle`, to denote platforms where we can toggle WiFi without losing focus, as this has obvious UX impacts, and less obvious technical impacts on some of the NetworkMgr innards...
* Android: Mark as `!hasSeamlessWifiToggle`, as it requires losing focus to the system settings. Moreover, `turnOnWifi` returns *immediately* and we *still* run in the background during that time, for extra spiciness...
* NetworkMgr: Ensure only *one* call to `turnOnWifi` will actually go on when stuff gets re-scheduled by the `beforeWifiAction` framework.
* NetworkMgr: Ensure the `beforeWifiAction` framework will not re-schedule the same thing *ad vitam aeternam* if a previous connection attempt is still ongoing. (i.e., previously, on Android, if you backed out of the system settings, you entered the Benny Hill dimension, as NetworkMgr would keep throwing you back into the system settings ;p). This has a few implications on callbacks requested by subsequent connection attempts, though. Generally, we'll try to honor *explicitly interactive* callbacks, but `beforeWifiAction` stuff will be dropped (only the original cb is preserved). That's what prevents the aforementioned infinite loop, as the `beforeWifiAction` framework was based on the assumption that `turnOnWifi` somewhat guaranteed `isConnected` to be true on return, something which is only actually true on `hasWifiManager` platforms.
* NetworkMgr: In `prompt` mode, the above implies that the prompt will not even be shown for concurrent attempts, as it's otherwise extremely confusing (KOSync on Android being a prime example, as it has a pair of Suspend/Resume handlers, so the initial attempt trips those two because of the focus switch >_<").
* NetworkMgr: Don't attempt to kill wifi when aborting a connection attempt on `!hasSeamlessWifiToggle` (because, again, it'll break UX, and also because it might run at very awkward times (e.g., I managed to go back to KOReader *between* a FM/Reader switch at one point, which promptly caused `UIManager` to exit because there was nothing to show ;p).
* NetworkMgr: Don't drop the connectivity callback when `beforeWifiAction` is set to prompt and the target happens to use a connectivity check in its `turnOnWifi` implementation (e.g., on Kindle).
* Android: Add an `"ignore"` `beforeWifiAction` mode, that'll do nothing but schedule the connectivity check with its callback (with the intent being the system will eventually enable wifi on its own Soon(TM)). If you're already online, the callback will run immediately, obviously. If you followed the early discussions on this PR, this closely matches what happens on `!hasWifiToggle` platforms (as flagging Android that way was one of the possible approaches here).
* NetworkMgr: Bail out early in `goOnlineToRun` if `beforeWifiAction` isn't `"turn_on"`. Prompt cannot work there, and while ignore technically could, it would serve very little purpose given its intended use case.
* KOSync: Neuter the Resume/Suspend handlers early on `CloseDocument`, as this is how focus switches are handled on Android, and if `beforeWifiAction` is `turn_on` and you were offline at the time, we'd trip them because of the swap to system settings to enable wifi.
* KOSync: Allow `auto_sync` to be enabled regardless of the `beforeWifiAction` mode on `!hasSeamlessWifiToggle` platforms. Prompt is still a terrible idea, but given that `goOnlineToRun` now aborts early if the mode is not supported, it's less of a problem.
2023-09-21 18:21:09 +02:00

941 lines
35 KiB
Lua

local ConfirmBox = require("ui/widget/confirmbox")
local Device = require("device")
local Dispatcher = require("dispatcher")
local Event = require("ui/event")
local InfoMessage = require("ui/widget/infomessage")
local Math = require("optmath")
local MultiInputDialog = require("ui/widget/multiinputdialog")
local NetworkMgr = require("ui/network/manager")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
local md5 = require("ffi/sha2").md5
local random = require("random")
local time = require("ui/time")
local util = require("util")
local T = require("ffi/util").template
local _ = require("gettext")
if G_reader_settings:hasNot("device_id") then
G_reader_settings:saveSetting("device_id", random.uuid())
end
local KOSync = WidgetContainer:extend{
name = "kosync",
is_doc_only = true,
title = _("Register/login to KOReader server"),
push_timestamp = nil,
pull_timestamp = nil,
page_update_counter = nil,
last_page = nil,
last_page_turn_timestamp = nil,
periodic_push_task = nil,
periodic_push_scheduled = nil,
settings = nil,
}
local SYNC_STRATEGY = {
PROMPT = 1,
SILENT = 2,
DISABLE = 3,
}
local CHECKSUM_METHOD = {
BINARY = 0,
FILENAME = 1
}
-- Debounce push/pull attempts
local API_CALL_DEBOUNCE_DELAY = time.s(25)
-- NOTE: This is used in a migration script by ui/data/onetime_migration,
-- which is why it's public.
KOSync.default_settings = {
custom_server = nil,
username = nil,
userkey = nil,
-- Do *not* default to auto-sync, as wifi may not be on at all times, and the nagging enabling this may cause requires careful consideration.
auto_sync = false,
pages_before_update = nil,
sync_forward = SYNC_STRATEGY.PROMPT,
sync_backward = SYNC_STRATEGY.DISABLE,
checksum_method = CHECKSUM_METHOD.BINARY,
}
function KOSync:init()
self.push_timestamp = 0
self.pull_timestamp = 0
self.page_update_counter = 0
self.last_page = -1
self.last_page_turn_timestamp = 0
self.periodic_push_scheduled = false
-- Like AutoSuspend, we need an instance-specific task for scheduling/resource management reasons.
self.periodic_push_task = function()
self.periodic_push_scheduled = false
self.page_update_counter = 0
-- We do *NOT* want to make sure networking is up here, as the nagging would be extremely annoying; we're leaving that to the network activity check...
self:updateProgress(false, false)
end
self.settings = G_reader_settings:readSetting("kosync", self.default_settings)
self.device_id = G_reader_settings:readSetting("device_id")
-- Disable auto-sync if beforeWifiAction was reset to "prompt" behind our back...
if self.settings.auto_sync and Device:hasSeamlessWifiToggle() and G_reader_settings:readSetting("wifi_enable_action") ~= "turn_on" then
self.settings.auto_sync = false
logger.warn("KOSync: Automatic sync has been disabled because wifi_enable_action is *not* turn_on")
end
self.ui.menu:registerToMainMenu(self)
end
function KOSync:getSyncPeriod()
if not self.settings.auto_sync then
return _("Not available")
end
local period = self.settings.pages_before_update
if period and period > 0 then
return period
else
return _("Never")
end
end
local function getNameStrategy(type)
if type == 1 then
return _("Prompt")
elseif type == 2 then
return _("Auto")
else
return _("Disable")
end
end
local function showSyncedMessage()
UIManager:show(InfoMessage:new{
text = _("Progress has been synchronized."),
timeout = 3,
})
end
local function promptLogin()
UIManager:show(InfoMessage:new{
text = _("Please register or login before using the progress synchronization feature."),
timeout = 3,
})
end
local function showSyncError()
UIManager:show(InfoMessage:new{
text = _("Something went wrong when syncing progress, please check your network connection and try again later."),
timeout = 3,
})
end
local function validate(entry)
if not entry then return false end
if type(entry) == "string" then
if entry == "" or not entry:match("%S") then return false end
end
return true
end
local function validateUser(user, pass)
local error_message = nil
local user_ok = validate(user)
local pass_ok = validate(pass)
if not user_ok and not pass_ok then
error_message = _("invalid username and password")
elseif not user_ok then
error_message = _("invalid username")
elseif not pass_ok then
error_message = _("invalid password")
end
if not error_message then
return user_ok and pass_ok
else
return user_ok and pass_ok, error_message
end
end
function KOSync:onDispatcherRegisterActions()
Dispatcher:registerAction("kosync_push_progress", { category="none", event="KOSyncPushProgress", title=_("Push progress from this device"), reader=true,})
Dispatcher:registerAction("kosync_pull_progress", { category="none", event="KOSyncPullProgress", title=_("Pull progress from other devices"), reader=true, separator=true,})
end
function KOSync:onReaderReady()
-- Make sure checksum has been calculated before we ever query it,
-- to prevent document saving features from affecting the checksum,
-- and eventually affecting the document identity for the progress sync feature.
self.view.document:fastDigest(self.ui.doc_settings)
if self.settings.auto_sync then
UIManager:nextTick(function()
self:getProgress(true, false)
end)
end
-- NOTE: Keep in mind that, on Android, turning on WiFi requires a focus switch, which will trip a Suspend/Resume pair.
-- NetworkMgr will attempt to hide the damage to avoid a useless pull -> push -> pull dance instead of the single pull requested.
-- Plus, if wifi_enable_action is set to prompt, that also avoids stacking three prompts on top of each other...
self:registerEvents()
self:onDispatcherRegisterActions()
self.last_page = self.ui:getCurrentPage()
end
function KOSync:addToMainMenu(menu_items)
menu_items.progress_sync = {
text = _("Progress sync"),
sub_item_table = {
{
text = _("Custom sync server"),
keep_menu_open = true,
tap_input_func = function()
return {
-- @translators Server address defined by user for progress sync.
title = _("Custom progress sync server address"),
input = self.settings.custom_server or "https://",
type = "text",
callback = function(input)
self:setCustomServer(input)
end,
}
end,
},
{
text_func = function()
return self.settings.userkey and (_("Logout"))
or _("Register") .. " / " .. _("Login")
end,
keep_menu_open = true,
callback_func = function()
if self.settings.userkey then
return function(menu)
self:logout(menu)
end
else
return function(menu)
self:login(menu)
end
end
end,
separator = true,
},
{
text = _("Automatically keep documents in sync"),
checked_func = function() return self.settings.auto_sync end,
help_text = _([[This may lead to nagging about toggling WiFi on document close and suspend/resume, depending on the device's connectivity.]]),
callback = function()
-- Actively recommend switching the before wifi action to "turn_on" instead of prompt, as prompt will just not be practical (or even plain usable) here.
if Device:hasSeamlessWifiToggle() and G_reader_settings:readSetting("wifi_enable_action") ~= "turn_on" and not self.settings.auto_sync then
UIManager:show(InfoMessage:new{ text = _("You will have to switch the 'Action when Wi-Fi is off' Network setting to 'turn on' to be able to enable this feature!") })
return
end
self.settings.auto_sync = not self.settings.auto_sync
self:registerEvents()
if self.settings.auto_sync then
-- Since we will update the progress when closing the document,
-- pull the current progress now so as not to silently overwrite it.
self:getProgress(true, true)
else
-- Since we won't update the progress when closing the document,
-- push the current progress now so as not to lose it.
self:updateProgress(true, true)
end
end,
},
{
text_func = function()
return T(_("Periodically sync every # pages (%1)"), self:getSyncPeriod())
end,
enabled_func = function() return self.settings.auto_sync end,
-- This is the condition that allows enabling auto_disable_wifi in NetworkManager ;).
help_text = NetworkMgr:getNetworkInterfaceName() and _([[Unlike the automatic sync above, this will *not* attempt to setup a network connection, but instead relies on it being already up, and may trigger enough network activity to passively keep WiFi enabled!]]),
keep_menu_open = true,
callback = function(touchmenu_instance)
local SpinWidget = require("ui/widget/spinwidget")
local items = SpinWidget:new{
text = _([[This value determines how many page turns it takes to update book progress.
If set to 0, updating progress based on page turns will be disabled.]]),
value = self.settings.pages_before_update or 0,
value_min = 0,
value_max = 999,
value_step = 1,
value_hold_step = 10,
ok_text = _("Set"),
title_text = _("Number of pages before update"),
default_value = 0,
callback = function(spin)
self:setPagesBeforeUpdate(spin.value)
if touchmenu_instance then touchmenu_instance:updateItems() end
end
}
UIManager:show(items)
end,
separator = true,
},
{
text = _("Sync behavior"),
sub_item_table = {
{
text_func = function()
-- NOTE: With an up-to-date Sync server, "forward" means *newer*, not necessarily ahead in the document.
return T(_("Sync to a newer state (%1)"), getNameStrategy(self.settings.sync_forward))
end,
sub_item_table = {
{
text = _("Silently"),
checked_func = function()
return self.settings.sync_forward == SYNC_STRATEGY.SILENT
end,
callback = function()
self:setSyncForward(SYNC_STRATEGY.SILENT)
end,
},
{
text = _("Prompt"),
checked_func = function()
return self.settings.sync_forward == SYNC_STRATEGY.PROMPT
end,
callback = function()
self:setSyncForward(SYNC_STRATEGY.PROMPT)
end,
},
{
text = _("Never"),
checked_func = function()
return self.settings.sync_forward == SYNC_STRATEGY.DISABLE
end,
callback = function()
self:setSyncForward(SYNC_STRATEGY.DISABLE)
end,
},
}
},
{
text_func = function()
return T(_("Sync to an older state (%1)"), getNameStrategy(self.settings.sync_backward))
end,
sub_item_table = {
{
text = _("Silently"),
checked_func = function()
return self.settings.sync_backward == SYNC_STRATEGY.SILENT
end,
callback = function()
self:setSyncBackward(SYNC_STRATEGY.SILENT)
end,
},
{
text = _("Prompt"),
checked_func = function()
return self.settings.sync_backward == SYNC_STRATEGY.PROMPT
end,
callback = function()
self:setSyncBackward(SYNC_STRATEGY.PROMPT)
end,
},
{
text = _("Never"),
checked_func = function()
return self.settings.sync_backward == SYNC_STRATEGY.DISABLE
end,
callback = function()
self:setSyncBackward(SYNC_STRATEGY.DISABLE)
end,
},
}
},
},
separator = true,
},
{
text = _("Push progress from this device now"),
enabled_func = function()
return self.settings.userkey ~= nil
end,
callback = function()
self:updateProgress(true, true)
end,
},
{
text = _("Pull progress from other devices now"),
enabled_func = function()
return self.settings.userkey ~= nil
end,
callback = function()
self:getProgress(true, true)
end,
separator = true,
},
{
text = _("Document matching method"),
sub_item_table = {
{
text = _("Binary. Only identical files will be kept in sync."),
checked_func = function()
return self.settings.checksum_method == CHECKSUM_METHOD.BINARY
end,
callback = function()
self:setChecksumMethod(CHECKSUM_METHOD.BINARY)
end,
},
{
text = _("Filename. Files with matching names will be kept in sync."),
checked_func = function()
return self.settings.checksum_method == CHECKSUM_METHOD.FILENAME
end,
callback = function()
self:setChecksumMethod(CHECKSUM_METHOD.FILENAME)
end,
},
}
},
}
}
end
function KOSync:setPagesBeforeUpdate(pages_before_update)
self.settings.pages_before_update = pages_before_update > 0 and pages_before_update or nil
end
function KOSync:setCustomServer(server)
logger.dbg("KOSync: Setting custom server to:", server)
self.settings.custom_server = server ~= "" and server or nil
end
function KOSync:setSyncForward(strategy)
self.settings.sync_forward = strategy
end
function KOSync:setSyncBackward(strategy)
self.settings.sync_backward = strategy
end
function KOSync:setChecksumMethod(method)
self.settings.checksum_method = method
end
function KOSync:login(menu)
if NetworkMgr:willRerunWhenOnline(function() self:login(menu) end) then
return
end
local dialog
dialog = MultiInputDialog:new{
title = self.title,
fields = {
{
text = self.settings.username,
hint = "username",
},
{
hint = "password",
text_type = "password",
},
},
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(dialog)
end,
},
{
text = _("Login"),
callback = function()
local username, password = unpack(dialog:getFields())
local ok, err = validateUser(username, password)
if not ok then
UIManager:show(InfoMessage:new{
text = T(_("Cannot login: %1"), err),
timeout = 2,
})
else
UIManager:close(dialog)
UIManager:scheduleIn(0.5, function()
self:doLogin(username, password, menu)
end)
UIManager:show(InfoMessage:new{
text = _("Logging in. Please wait…"),
timeout = 1,
})
end
end,
},
{
text = _("Register"),
callback = function()
local username, password = unpack(dialog:getFields())
local ok, err = validateUser(username, password)
if not ok then
UIManager:show(InfoMessage:new{
text = T(_("Cannot register: %1"), err),
timeout = 2,
})
else
UIManager:close(dialog)
UIManager:scheduleIn(0.5, function()
self:doRegister(username, password, menu)
end)
UIManager:show(InfoMessage:new{
text = _("Registering. Please wait…"),
timeout = 1,
})
end
end,
},
},
},
}
UIManager:show(dialog)
dialog:onShowKeyboard()
end
function KOSync:doRegister(username, password, menu)
local KOSyncClient = require("KOSyncClient")
local client = KOSyncClient:new{
custom_url = self.settings.custom_server,
service_spec = self.path .. "/api.json"
}
-- on Android to avoid ANR (no-op on other platforms)
Device:setIgnoreInput(true)
local userkey = md5(password)
local ok, status, body = pcall(client.register, client, username, userkey)
if not ok then
if status then
UIManager:show(InfoMessage:new{
text = _("An error occurred while registering:") ..
"\n" .. status,
})
else
UIManager:show(InfoMessage:new{
text = _("An unknown error occurred while registering."),
})
end
elseif status then
self.settings.username = username
self.settings.userkey = userkey
if menu then
menu:updateItems()
end
UIManager:show(InfoMessage:new{
text = _("Registered to KOReader server."),
})
else
UIManager:show(InfoMessage:new{
text = body and body.message or _("Unknown server error"),
})
end
Device:setIgnoreInput(false)
end
function KOSync:doLogin(username, password, menu)
local KOSyncClient = require("KOSyncClient")
local client = KOSyncClient:new{
custom_url = self.settings.custom_server,
service_spec = self.path .. "/api.json"
}
Device:setIgnoreInput(true)
local userkey = md5(password)
local ok, status, body = pcall(client.authorize, client, username, userkey)
if not ok then
if status then
UIManager:show(InfoMessage:new{
text = _("An error occurred while logging in:") ..
"\n" .. status,
})
else
UIManager:show(InfoMessage:new{
text = _("An unknown error occurred while logging in."),
})
end
Device:setIgnoreInput(false)
return
elseif status then
self.settings.username = username
self.settings.userkey = userkey
if menu then
menu:updateItems()
end
UIManager:show(InfoMessage:new{
text = _("Logged in to KOReader server."),
})
else
UIManager:show(InfoMessage:new{
text = body and body.message or _("Unknown server error"),
})
end
Device:setIgnoreInput(false)
end
function KOSync:logout(menu)
self.settings.userkey = nil
self.settings.auto_sync = true
if menu then
menu:updateItems()
end
end
function KOSync:getLastPercent()
if self.ui.document.info.has_pages then
return Math.roundPercent(self.ui.paging:getLastPercent())
else
return Math.roundPercent(self.ui.rolling:getLastPercent())
end
end
function KOSync:getLastProgress()
if self.ui.document.info.has_pages then
return self.ui.paging:getLastProgress()
else
return self.ui.rolling:getLastProgress()
end
end
function KOSync:getDocumentDigest()
if self.settings.checksum_method == CHECKSUM_METHOD.FILENAME then
return self:getFileNameDigest()
else
return self:getFileDigest()
end
end
function KOSync:getFileDigest()
return self.ui.doc_settings:readSetting("partial_md5_checksum")
end
function KOSync:getFileNameDigest()
local file = self.ui.document.file
if not file then return end
local file_path, file_name = util.splitFilePathName(file) -- luacheck: no unused
if not file_name then return end
return md5(file_name)
end
function KOSync:syncToProgress(progress)
logger.dbg("KOSync: [Sync] progress to", progress)
if self.ui.document.info.has_pages then
self.ui:handleEvent(Event:new("GotoPage", tonumber(progress)))
else
self.ui:handleEvent(Event:new("GotoXPointer", progress))
end
end
function KOSync:updateProgress(ensure_networking, interactive, refresh_on_success)
if not self.settings.username or not self.settings.userkey then
if interactive then
promptLogin()
end
return
end
local now = UIManager:getElapsedTimeSinceBoot()
if not interactive and now - self.push_timestamp <= API_CALL_DEBOUNCE_DELAY then
logger.dbg("KOSync: We've already pushed progress less than 25s ago!")
return
end
if ensure_networking and NetworkMgr:willRerunWhenOnline(function() self:updateProgress(ensure_networking, interactive, refresh_on_success) end) then
return
end
local KOSyncClient = require("KOSyncClient")
local client = KOSyncClient:new{
custom_url = self.settings.custom_server,
service_spec = self.path .. "/api.json"
}
local doc_digest = self:getDocumentDigest()
local progress = self:getLastProgress()
local percentage = self:getLastPercent()
local ok, err = pcall(client.update_progress,
client,
self.settings.username,
self.settings.userkey,
doc_digest,
progress,
percentage,
Device.model,
self.device_id,
function(ok, body)
logger.dbg("KOSync: [Push] progress to", percentage * 100, "% =>", progress, "for", self.view.document.file)
logger.dbg("KOSync: ok:", ok, "body:", body)
if interactive then
if ok then
UIManager:show(InfoMessage:new{
text = _("Progress has been pushed."),
timeout = 3,
})
else
showSyncError()
end
end
end)
if not ok then
if interactive then showSyncError() end
if err then logger.dbg("err:", err) end
else
-- This is solely for onSuspend's sake, to clear the ghosting left by the the "Connected" InfoMessage
if refresh_on_success then
-- Our top-level widget should be the "Connected to network" InfoMessage from NetworkMgr's reconnectOrShowNetworkMenu
local widget = UIManager:getTopmostVisibleWidget()
if widget and widget.modal and widget.tag == "NetworkMgr" and not widget.dismiss_callback then
-- We want a full-screen flash on dismiss
widget.dismiss_callback = function()
-- Enqueued, because we run before the InfoMessage's close
UIManager:setDirty(nil, "full")
end
end
end
end
self.push_timestamp = now
end
function KOSync:getProgress(ensure_networking, interactive)
if not self.settings.username or not self.settings.userkey then
if interactive then
promptLogin()
end
return
end
local now = UIManager:getElapsedTimeSinceBoot()
if not interactive and now - self.pull_timestamp <= API_CALL_DEBOUNCE_DELAY then
logger.dbg("KOSync: We've already pulled progress less than 25s ago!")
return
end
if ensure_networking and NetworkMgr:willRerunWhenOnline(function() self:getProgress(ensure_networking, interactive) end) then
return
end
local KOSyncClient = require("KOSyncClient")
local client = KOSyncClient:new{
custom_url = self.settings.custom_server,
service_spec = self.path .. "/api.json"
}
local doc_digest = self:getDocumentDigest()
local ok, err = pcall(client.get_progress,
client,
self.settings.username,
self.settings.userkey,
doc_digest,
function(ok, body)
logger.dbg("KOSync: [Pull] progress for", self.view.document.file)
logger.dbg("KOSync: ok:", ok, "body:", body)
if not ok or not body then
if interactive then
showSyncError()
end
return
end
if not body.percentage then
if interactive then
UIManager:show(InfoMessage:new{
text = _("No progress found for this document."),
timeout = 3,
})
end
return
end
if body.device == Device.model
and body.device_id == self.device_id then
if interactive then
UIManager:show(InfoMessage:new{
text = _("Latest progress is coming from this device."),
timeout = 3,
})
end
return
end
body.percentage = Math.roundPercent(body.percentage)
local progress = self:getLastProgress()
local percentage = self:getLastPercent()
logger.dbg("KOSync: Current progress:", percentage * 100, "% =>", progress)
if percentage == body.percentage
or body.progress == progress then
if interactive then
UIManager:show(InfoMessage:new{
text = _("The progress has already been synchronized."),
timeout = 3,
})
end
return
end
-- The progress needs to be updated.
if interactive then
-- If user actively pulls progress from other devices,
-- we always update the progress without further confirmation.
self:syncToProgress(body.progress)
showSyncedMessage()
return
end
local self_older
if body.timestamp ~= nil then
self_older = (body.timestamp > self.last_page_turn_timestamp)
else
-- If we are working with an old sync server, we can only use the percentage field.
self_older = (body.percentage > percentage)
end
if self_older then
if self.settings.sync_forward == SYNC_STRATEGY.SILENT then
self:syncToProgress(body.progress)
showSyncedMessage()
elseif self.settings.sync_forward == SYNC_STRATEGY.PROMPT then
UIManager:show(ConfirmBox:new{
text = T(_("Sync to latest location %1% from device '%2'?"),
Math.round(body.percentage * 100),
body.device),
ok_callback = function()
self:syncToProgress(body.progress)
end,
})
end
else -- if not self_older then
if self.settings.sync_backward == SYNC_STRATEGY.SILENT then
self:syncToProgress(body.progress)
showSyncedMessage()
elseif self.settings.sync_backward == SYNC_STRATEGY.PROMPT then
UIManager:show(ConfirmBox:new{
text = T(_("Sync to previous location %1% from device '%2'?"),
Math.round(body.percentage * 100),
body.device),
ok_callback = function()
self:syncToProgress(body.progress)
end,
})
end
end
end)
if not ok then
if interactive then showSyncError() end
if err then logger.dbg("err:", err) end
end
self.pull_timestamp = now
end
function KOSync:_onCloseDocument()
logger.dbg("KOSync: onCloseDocument")
-- NOTE: Because everything is terrible, on Android, opening the system settings to enable WiFi means we lose focus,
-- and we handle those system focus events via... Suspend & Resume events, so we need to neuter those handlers early.
self.onResume = nil
self.onSuspend = nil
-- NOTE: Because we'll lose the document instance on return, we need to *block* until the connection is actually up here,
-- we cannot rely on willRerunWhenOnline, because if we're not currently online,
-- it *will* return early, and that means the actual callback *will* run *after* teardown of the document instance
-- (and quite likely ours, too).
NetworkMgr:goOnlineToRun(function()
-- Drop the inner willRerunWhenOnline ;).
self:updateProgress(false, false)
end)
end
function KOSync:schedulePeriodicPush()
UIManager:unschedule(self.periodic_push_task)
-- Use a sizable delay to make debouncing this on skim feasible...
UIManager:scheduleIn(10, self.periodic_push_task)
self.periodic_push_scheduled = true
end
function KOSync:_onPageUpdate(page)
if page == nil then
return
end
if self.last_page ~= page then
self.last_page = page
self.last_page_turn_timestamp = os.time()
self.page_update_counter = self.page_update_counter + 1
-- If we've already scheduled a push, regardless of the counter's state, delay it until we're *actually* idle
if self.periodic_push_scheduled or self.settings.pages_before_update and self.page_update_counter >= self.settings.pages_before_update then
self:schedulePeriodicPush()
end
end
end
function KOSync:_onResume()
logger.dbg("KOSync: onResume")
-- If we have auto_restore_wifi enabled, skip this to prevent both the "Connecting..." UI to pop-up,
-- *and* a duplicate NetworkConnected event from firing...
if Device:hasWifiRestore() and NetworkMgr.wifi_was_on and G_reader_settings:isTrue("auto_restore_wifi") then
return
end
-- And if we don't, this *will* (attempt to) trigger a connection and as such a NetworkConnected event,
-- but only a single pull will happen, since getProgress debounces itself.
UIManager:scheduleIn(1, function()
self:getProgress(true, false)
end)
end
function KOSync:_onSuspend()
logger.dbg("KOSync: onSuspend")
-- We request an extra flashing refresh on success, to deal with potential ghosting left by the NetworkMgr UI
self:updateProgress(true, false, true)
end
function KOSync:_onNetworkConnected()
logger.dbg("KOSync: onNetworkConnected")
UIManager:scheduleIn(0.5, function()
-- Network is supposed to be on already, don't wrap this in willRerunWhenOnline
self:getProgress(false, false)
end)
end
function KOSync:_onNetworkDisconnecting()
logger.dbg("KOSync: onNetworkDisconnecting")
-- Network is supposed to be on already, don't wrap this in willRerunWhenOnline
self:updateProgress(false, false)
end
function KOSync:onKOSyncPushProgress()
self:updateProgress(true, true)
end
function KOSync:onKOSyncPullProgress()
self:getProgress(true, true)
end
function KOSync:registerEvents()
if self.settings.auto_sync then
self.onCloseDocument = self._onCloseDocument
self.onPageUpdate = self._onPageUpdate
self.onResume = self._onResume
self.onSuspend = self._onSuspend
self.onNetworkConnected = self._onNetworkConnected
self.onNetworkDisconnecting = self._onNetworkDisconnecting
else
self.onCloseDocument = nil
self.onPageUpdate = nil
self.onResume = nil
self.onSuspend = nil
self.onNetworkConnected = nil
self.onNetworkDisconnecting = nil
end
end
function KOSync:onCloseWidget()
UIManager:unschedule(self.periodic_push_task)
self.periodic_push_task = nil
end
return KOSync