mirror of
https://github.com/koreader/koreader.git
synced 2025-08-10 00:52:38 +00:00
* 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.
941 lines
35 KiB
Lua
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
|