From 08dd97384c9011004fd52eba2c559be5337838d5 Mon Sep 17 00:00:00 2001 From: NiLuJe Date: Mon, 3 Jul 2023 01:23:14 +0200 Subject: [PATCH] KOSync: Clarify settings, plus refactor & fixes to make "auto-sync" more reliable (#10605) Fix: #10539, and for context #6489, #6733, #6534 Reorganize and reword most of the settings to make it clear what actually ties into auto sync, and what doesn't. (Specifically, what happens when a pull attempts to sync forward or backward has nothing to do with auto sync, it applies in all cases; while the periodic sync *does* require auto sync). The main point of contention, though, is that auto sync will now *always* attempt to setup network connectivity (i.e., on resume/suspend/close). Periodic sync will *not* though (the intent being that, if you use periodic sync, you're relying on the activity check to actually keep wifi on at all times)). Since this may lead to a large amount of nagging about wifi toggles on devices w/ NetworkManager support, it is now *disabled* by default on those devices. (And given that it wouldn't have worked because of the lack of connectivity, that doesn't really make any practical difference ;p). Additionally, given the fact that there's no way to make this behavior viable if the "before wifi" action is left at its default of "prompt", this feature now *requires* that to be set to "turn_on" (on devices where it can, of course); attempting to toggle it on will warn about that if necessary. This change is retroactive (OTM). Includes an assortment of fixes and cleanups, including migrating to the new LuaSettings API, which is why there's no longer a smattering of superfluous flushes. --- .../reader/modules/readercoptlistener.lua | 5 +- .../apps/reader/modules/readerdictionary.lua | 2 +- frontend/apps/reader/modules/readerfooter.lua | 26 +- .../apps/reader/modules/readerwikipedia.lua | 2 +- frontend/device/generic/device.lua | 4 +- frontend/device/gesturedetector.lua | 2 +- frontend/ui/data/onetime_migration.lua | 57 +- frontend/ui/network/manager.lua | 202 +++--- frontend/ui/network/networklistener.lua | 3 + frontend/ui/widget/infomessage.lua | 3 +- plugins/kosync.koplugin/main.lua | 575 +++++++++++------- 11 files changed, 536 insertions(+), 345 deletions(-) diff --git a/frontend/apps/reader/modules/readercoptlistener.lua b/frontend/apps/reader/modules/readercoptlistener.lua index a0ac59a1f..9a8dee001 100644 --- a/frontend/apps/reader/modules/readercoptlistener.lua +++ b/frontend/apps/reader/modules/readercoptlistener.lua @@ -84,10 +84,7 @@ end function ReaderCoptListener:onCharging() self:headerRefresh() end - -function ReaderCoptListener:onNotCharging() - self:headerRefresh() -end +ReaderCoptListener.onNotCharging = ReaderCoptListener.onCharging function ReaderCoptListener:onTimeFormatChanged() self.ui.document._document:setIntProperty("window.status.clock.12hours", G_reader_settings:isTrue("twelve_hour_clock") and 1 or 0) diff --git a/frontend/apps/reader/modules/readerdictionary.lua b/frontend/apps/reader/modules/readerdictionary.lua index 54179cafb..a0f36e8a7 100644 --- a/frontend/apps/reader/modules/readerdictionary.lua +++ b/frontend/apps/reader/modules/readerdictionary.lua @@ -275,7 +275,7 @@ function ReaderDictionary:addToMainMenu(menu_items) { text = _("Enable fuzzy search"), checked_func = function() - return not self.disable_fuzzy_search == true + return self.disable_fuzzy_search ~= true end, callback = function() self.disable_fuzzy_search = not self.disable_fuzzy_search diff --git a/frontend/apps/reader/modules/readerfooter.lua b/frontend/apps/reader/modules/readerfooter.lua index a025b8202..ffb477f3b 100644 --- a/frontend/apps/reader/modules/readerfooter.lua +++ b/frontend/apps/reader/modules/readerfooter.lua @@ -2485,35 +2485,24 @@ function ReaderFooter:maybeUpdateFooter() self:onUpdateFooter(self:shouldBeRepainted()) end --- is the same as maybeUpdateFooter function ReaderFooter:onFrontlightStateChanged() - self:onUpdateFooter(self:shouldBeRepainted()) + self:maybeUpdateFooter() end +ReaderFooter.onCharging = ReaderFooter.onFrontlightStateChanged +ReaderFooter.onNotCharging = ReaderFooter.onFrontlightStateChanged function ReaderFooter:onNetworkConnected() if self.settings.wifi_status then self:maybeUpdateFooter() end end - -function ReaderFooter:onNetworkDisconnected() - if self.settings.wifi_status then - self:onUpdateFooter(self.view.footer_visible) - end -end - -function ReaderFooter:onCharging() - self:maybeUpdateFooter() -end - -function ReaderFooter:onNotCharging() - self:maybeUpdateFooter() -end +ReaderFooter.onNetworkDisconnected = ReaderFooter.onNetworkConnected function ReaderFooter:onSetRotationMode() self:updateFooterContainer() self:resetLayout(true) end +ReaderFooter.onScreenResize = ReaderFooter.onSetRotationMode function ReaderFooter:onSetPageHorizMargins(h_margins) self.book_margins_footer_width = math.floor((h_margins[1] + h_margins[2])/2) @@ -2523,11 +2512,6 @@ function ReaderFooter:onSetPageHorizMargins(h_margins) end end -function ReaderFooter:onScreenResize() - self:updateFooterContainer() - self:resetLayout(true) -end - function ReaderFooter:onTimeFormatChanged() self:refreshFooter(true, true) end diff --git a/frontend/apps/reader/modules/readerwikipedia.lua b/frontend/apps/reader/modules/readerwikipedia.lua index 4c2fa0c75..5c50ea7e5 100644 --- a/frontend/apps/reader/modules/readerwikipedia.lua +++ b/frontend/apps/reader/modules/readerwikipedia.lua @@ -244,7 +244,7 @@ function ReaderWikipedia:addToMainMenu(menu_items) -- home_dir/Wikipedia/ if not G_reader_settings:readSetting("wikipedia_save_dir") then local home_dir = G_reader_settings:readSetting("home_dir") - if not home_dir or not lfs.attributes(home_dir, "mode") == "directory" then + if not home_dir or lfs.attributes(home_dir, "mode") ~= "directory" then home_dir = require("apps/filemanager/filemanagerutil").getDefaultDir() end home_dir = home_dir:gsub("^(.-)/*$", "%1") -- remove trailing slash diff --git a/frontend/device/generic/device.lua b/frontend/device/generic/device.lua index dcef3b658..05907b888 100644 --- a/frontend/device/generic/device.lua +++ b/frontend/device/generic/device.lua @@ -312,7 +312,7 @@ function Device:onPowerEvent(ev) if self:hasWifiToggle() then local network_manager = require("ui/network/manager") if network_manager:isWifiOn() then - network_manager:releaseIP() + UIManager:broadcastEvent(Event:new("NetworkDisconnecting")) network_manager:turnOffWifi() end end @@ -346,7 +346,7 @@ function Device:onPowerEvent(ev) -- because suspend will at best fail, and at worst deadlock the system if Wi-Fi is on, -- regardless of who enabled it! if network_manager:isWifiOn() then - network_manager:releaseIP() + UIManager:broadcastEvent(Event:new("NetworkDisconnecting")) network_manager:turnOffWifi() end end diff --git a/frontend/device/gesturedetector.lua b/frontend/device/gesturedetector.lua index 5eb42f565..a8f5936c8 100644 --- a/frontend/device/gesturedetector.lua +++ b/frontend/device/gesturedetector.lua @@ -436,7 +436,7 @@ function GestureDetector:probeClockSource(timev) -- Finally, BOOTTIME local boottime = time.boottime() -- NOTE: It was implemented in Linux 2.6.39, so, reject 0, which would mean it's unsupported... - if not boottime == 0 and timev >= boottime - threshold and timev <= boottime + threshold then + if boottime ~= 0 and timev >= boottime - threshold and timev <= boottime + threshold then self.clock_id = C.CLOCK_BOOTTIME logger.dbg("GestureDetector:probeClockSource: Touch event timestamps appear to use CLOCK_BOOTTIME") return diff --git a/frontend/ui/data/onetime_migration.lua b/frontend/ui/data/onetime_migration.lua index 73f83cd4e..a0865f16c 100644 --- a/frontend/ui/data/onetime_migration.lua +++ b/frontend/ui/data/onetime_migration.lua @@ -7,7 +7,7 @@ local lfs = require("libs/libkoreader-lfs") local logger = require("logger") -- Date at which the last migration snippet was added -local CURRENT_MIGRATION_DATE = 20230531 +local CURRENT_MIGRATION_DATE = 20230627 -- Retrieve the date of the previous migration, if any local last_migration_date = G_reader_settings:readSetting("last_migration_date", 0) @@ -145,14 +145,16 @@ if last_migration_date < 20210330 then if not ok or not ReaderStatistics then logger.warn("Error when loading plugins/statistics.koplugin/main.lua:", ReaderStatistics) else - local settings = G_reader_settings:readSetting("statistics", ReaderStatistics.default_settings) - -- Handle a snafu in 2021.03 that could lead to an empty settings table on fresh installs. - for k, v in pairs(ReaderStatistics.default_settings) do - if settings[k] == nil then - settings[k] = v + local settings = G_reader_settings:readSetting("statistics") + if settings then + -- Handle a snafu in 2021.03 that could lead to an empty settings table on fresh installs. + for k, v in pairs(ReaderStatistics.default_settings) do + if settings[k] == nil then + settings[k] = v + end end + G_reader_settings:saveSetting("statistics", settings) end - G_reader_settings:saveSetting("statistics", settings) end end @@ -514,5 +516,46 @@ if last_migration_date < 20230531 then end end +-- 20230627, Migrate to a full settings table, and disable KOSync's auto sync mode if wifi_enable_action is not turn_on +if last_migration_date < 20230627 then + logger.info("Performing one-time migration for 20230627") + + -- c.f., PluginLoader + local package_path = package.path + package.path = string.format("%s/?.lua;%s", "plugins/kosync.koplugin", package_path) + local ok, KOSync = pcall(dofile, "plugins/kosync.koplugin/main.lua") + package.path = package_path + if not ok or not KOSync then + logger.warn("Error when loading plugins/kosync.koplugin/main.lua:", KOSync) + else + local settings = G_reader_settings:readSetting("kosync") + if settings then + -- Make sure the table is complete + for k, v in pairs(KOSync.default_settings) do + if settings[k] == nil then + settings[k] = v + end + end + + -- Migrate the whisper_* keys + settings.sync_forward = settings.whisper_forward or KOSync.default_settings.sync_forward + settings.whisper_forward = nil + settings.sync_backward = settings.whisper_backward or KOSync.default_settings.sync_backward + settings.whisper_backward = nil + + G_reader_settings:saveSetting("kosync", settings) + end + end + + local Device = require("device") + if Device:hasWifiManager() and G_reader_settings:readSetting("wifi_enable_action") ~= "turn_on" then + local kosync = G_reader_settings:readSetting("kosync") + if kosync and kosync.auto_sync then + kosync.auto_sync = false + G_reader_settings:saveSetting("kosync", kosync) + end + end +end + -- We're done, store the current migration date G_reader_settings:saveSetting("last_migration_date", CURRENT_MIGRATION_DATE) diff --git a/frontend/ui/network/manager.lua b/frontend/ui/network/manager.lua index c77cc423c..4369c9057 100644 --- a/frontend/ui/network/manager.lua +++ b/frontend/ui/network/manager.lua @@ -32,14 +32,15 @@ end -- as quite a few things rely on it (KOSync, c.f. #5109; the network activity check, c.f., #6424). function NetworkMgr:connectivityCheck(iter, callback, widget) -- Give up after a while (restoreWifiAsync can take over 45s, so, try to cover that)... - if iter > 25 then - logger.info("Failed to restore Wi-Fi (after", iter, "iterations)!") + if iter > 180 then + logger.info("Failed to restore Wi-Fi (after", iter * 0.25, "seconds)!") self.wifi_was_on = false G_reader_settings:makeFalse("wifi_was_on") -- If we abort, murder Wi-Fi and the async script first... if Device:hasWifiManager() then os.execute("pkill -TERM restore-wifi-async.sh 2>/dev/null") end + -- We were never connected to begin with, so, no disconnecting broadcast required self:turnOffWifi() -- Handle the UI warning if it's from a beforeWifiAction... @@ -54,8 +55,8 @@ function NetworkMgr:connectivityCheck(iter, callback, widget) if self.is_wifi_on and self.is_connected then self.wifi_was_on = true G_reader_settings:makeTrue("wifi_was_on") + logger.info("Wi-Fi successfully restored (after", iter * 0.25, "seconds)!") UIManager:broadcastEvent(Event:new("NetworkConnected")) - logger.info("Wi-Fi successfully restored (after", iter, "iterations)!") -- Handle the UI & callback if it's from a beforeWifiAction... if widget then @@ -74,12 +75,12 @@ function NetworkMgr:connectivityCheck(iter, callback, widget) end end else - UIManager:scheduleIn(2, self.connectivityCheck, self, iter + 1, callback, widget) + UIManager:scheduleIn(0.25, self.connectivityCheck, self, iter + 1, callback, widget) end end function NetworkMgr:scheduleConnectivityCheck(callback, widget) - UIManager:scheduleIn(2, self.connectivityCheck, self, 1, callback, widget) + UIManager:scheduleIn(0.5, self.connectivityCheck, self, 1, callback, widget) end function NetworkMgr:init() @@ -298,6 +299,8 @@ function NetworkMgr:turnOnWifiAndWaitForConnection(callback) -- This will handle sending the proper Event, manage wifi_was_on, as well as tearing down Wi-Fi in case of failures, -- (i.e., much like getWifiToggleMenuTable). self:scheduleConnectivityCheck(callback, info) + + return info end --- This quirky internal flag is used for the rare beforeWifiAction -> afterWifiAction brackets. @@ -322,9 +325,9 @@ function NetworkMgr:beforeWifiAction(callback) local wifi_enable_action = G_reader_settings:readSetting("wifi_enable_action") if wifi_enable_action == "turn_on" then - self:turnOnWifiAndWaitForConnection(callback) + return self:turnOnWifiAndWaitForConnection(callback) else - self:promptWifiOn(callback) + return self:promptWifiOn(callback) end end @@ -344,6 +347,7 @@ function NetworkMgr:afterWifiAction(callback) callback() end elseif wifi_disable_action == "turn_off" then + UIManager:broadcastEvent(Event:new("NetworkDisconnecting")) self:turnOffWifi(callback) else self:promptWifiOff(callback) @@ -461,6 +465,53 @@ function NetworkMgr:willRerunWhenConnected(callback) return false end +-- And this one is for when you absolutely *need* to block until we're online to run something (e.g., because it runs in a finalizer). +function NetworkMgr:goOnlineToRun(callback) + if self:isOnline() then + callback() + return true + end + + -- In case we abort before the beforeWifiAction, we won't pass it the callback, but run it ourselves, + -- to avoid it firing too late (or at the very least being pinned for too long). + local info = self:beforeWifiAction() + -- We'll basically do the same but in a blocking manner... + UIManager:unschedule(self.connectivityCheck) + + local iter = 0 + while not self.is_connected do + iter = iter + 1 + if iter >= 120 then + logger.info("Failed to connect to Wi-Fi after 30s, giving up!") + self.wifi_was_on = false + G_reader_settings:makeFalse("wifi_was_on") + if info then + UIManager:close(info) + end + UIManager:show(InfoMessage:new{ text = _("Error connecting to the network") }) + self:turnOffWifi() + return false + end + ffiutil.usleep(250000) + self:queryNetworkState() + end + + -- Close the initial "Connecting..." InfoMessage from turnOnWifiAndWaitForConnection via beforeWifiAction + if info then + UIManager:close(info) + end + -- We're finally connected! + self.wifi_was_on = true + G_reader_settings:makeTrue("wifi_was_on") + callback() + -- Delay this so it won't fire for dead/dying instances in case we're called by a finalizer... + UIManager:scheduleIn(2, function() + UIManager:broadcastEvent(Event:new("NetworkConnected")) + end) + return true +end + + function NetworkMgr:getWifiMenuTable() if Device:isAndroid() then @@ -701,89 +752,90 @@ end function NetworkMgr:reconnectOrShowNetworkMenu(complete_callback) local info = InfoMessage:new{text = _("Scanning for networks…")} UIManager:show(info) - UIManager:nextTick(function() - local network_list, err = self:getNetworkList() - UIManager:close(info) + UIManager:forceRePaint() + + local network_list, err = self:getNetworkList() + UIManager:close(info) + if network_list == nil then + UIManager:show(InfoMessage:new{text = err}) + return + end + -- NOTE: Fairly hackish workaround for #4387, + -- rescan if the first scan appeared to yield an empty list. + --- @fixme This *might* be an issue better handled in lj-wpaclient... + if #network_list == 0 then + logger.warn("Initial Wi-Fi scan yielded no results, rescanning") + network_list, err = self:getNetworkList() if network_list == nil then UIManager:show(InfoMessage:new{text = err}) return end - -- NOTE: Fairly hackish workaround for #4387, - -- rescan if the first scan appeared to yield an empty list. - --- @fixme This *might* be an issue better handled in lj-wpaclient... - if #network_list == 0 then - logger.warn("Initial Wi-Fi scan yielded no results, rescanning") - network_list, err = self:getNetworkList() - if network_list == nil then - UIManager:show(InfoMessage:new{text = err}) - return - end - end + end - table.sort(network_list, - function(l, r) return l.signal_quality > r.signal_quality end) + table.sort(network_list, + function(l, r) return l.signal_quality > r.signal_quality end) - local success = false - if self.wifi_toggle_long_press then - self.wifi_toggle_long_press = nil - else - local ssid - -- We need to do two passes, as we may have *both* an already connected network (from the global wpa config), - -- *and* preferred networks, and if the prferred networks have a better signal quality, - -- they'll be sorted *earlier*, which would cause us to try to associate to a different AP than - -- what wpa_supplicant is already trying to do... - for dummy, network in ipairs(network_list) do - if network.connected then - -- On platforms where we use wpa_supplicant (if we're calling this, we are), - -- the invocation will check its global config, and if an AP configured there is reachable, - -- it'll already have connected to it on its own. - success = true - ssid = network.ssid - break - end + local success = false + if self.wifi_toggle_long_press then + self.wifi_toggle_long_press = nil + else + local ssid + -- We need to do two passes, as we may have *both* an already connected network (from the global wpa config), + -- *and* preferred networks, and if the prferred networks have a better signal quality, + -- they'll be sorted *earlier*, which would cause us to try to associate to a different AP than + -- what wpa_supplicant is already trying to do... + for dummy, network in ipairs(network_list) do + if network.connected then + -- On platforms where we use wpa_supplicant (if we're calling this, we are), + -- the invocation will check its global config, and if an AP configured there is reachable, + -- it'll already have connected to it on its own. + success = true + ssid = network.ssid + break end + end - -- Next, look for our own prferred networks... - local err_msg = _("Connection failed") - if not success then - for dummy, network in ipairs(network_list) do - if network.password then - -- If we hit a preferred network and we're not already connected, - -- attempt to connect to said preferred network.... - success, err_msg = self:authenticateNetwork(network) - if success then - ssid = network.ssid - break - end + -- Next, look for our own prferred networks... + local err_msg = _("Connection failed") + if not success then + for dummy, network in ipairs(network_list) do + if network.password then + -- If we hit a preferred network and we're not already connected, + -- attempt to connect to said preferred network.... + success, err_msg = self:authenticateNetwork(network) + if success then + ssid = network.ssid + break end end end + end - if success then - self:obtainIP() - if complete_callback then - complete_callback() - end - UIManager:show(InfoMessage:new{ - text = T(_("Connected to network %1"), BD.wrap(ssid)), - timeout = 3, - }) - else - UIManager:show(InfoMessage:new{ - text = err_msg, - timeout = 3, - }) + if success then + self:obtainIP() + if complete_callback then + complete_callback() end - end - if not success then - -- NOTE: Also supports a disconnect_callback, should we use it for something? - -- Tearing down Wi-Fi completely when tapping "disconnect" would feel a bit harsh, though... - UIManager:show(require("ui/widget/networksetting"):new{ - network_list = network_list, - connect_callback = complete_callback, + UIManager:show(InfoMessage:new{ + tag = "NetworkMgr", -- for crazy KOSync purposes + text = T(_("Connected to network %1"), BD.wrap(ssid)), + timeout = 3, + }) + else + UIManager:show(InfoMessage:new{ + text = err_msg, + timeout = 3, }) end - end) + end + if not success then + -- NOTE: Also supports a disconnect_callback, should we use it for something? + -- Tearing down Wi-Fi completely when tapping "disconnect" would feel a bit harsh, though... + UIManager:show(require("ui/widget/networksetting"):new{ + network_list = network_list, + connect_callback = complete_callback, + }) + end end function NetworkMgr:saveNetwork(setting) diff --git a/frontend/ui/network/networklistener.lua b/frontend/ui/network/networklistener.lua index 522980fc3..5376d8ac2 100644 --- a/frontend/ui/network/networklistener.lua +++ b/frontend/ui/network/networklistener.lua @@ -39,6 +39,7 @@ function NetworkListener:onToggleWifi() UIManager:show(toggle_im) UIManager:forceRePaint() + UIManager:broadcastEvent(Event:new("NetworkDisconnecting")) NetworkMgr:turnOffWifi(complete_callback) UIManager:close(toggle_im) @@ -60,6 +61,7 @@ function NetworkListener:onInfoWifiOff() UIManager:show(toggle_im) UIManager:forceRePaint() + UIManager:broadcastEvent(Event:new("NetworkDisconnecting")) NetworkMgr:turnOffWifi(complete_callback) UIManager:close(toggle_im) @@ -166,6 +168,7 @@ function NetworkListener:_scheduleActivityCheck() local complete_callback = function() UIManager:broadcastEvent(Event:new("NetworkDisconnected")) end + UIManager:broadcastEvent(Event:new("NetworkDisconnecting")) NetworkMgr:turnOffWifi(complete_callback) -- NOTE: We leave wifi_was_on as-is on purpose, we wouldn't want to break auto_restore_wifi workflows on the next start... else diff --git a/frontend/ui/widget/infomessage.lua b/frontend/ui/widget/infomessage.lua index 0c4aaec75..6acdd0e31 100644 --- a/frontend/ui/widget/infomessage.lua +++ b/frontend/ui/widget/infomessage.lua @@ -240,8 +240,7 @@ function InfoMessage:onShow() -- schedule us to close ourself if timeout provided if self.timeout then UIManager:scheduleIn(self.timeout, function() - -- In case we're provided with dismiss_callback, also call it - -- on timeout + -- In case we're provided with dismiss_callback, also call it on timeout if self.dismiss_callback then self.dismiss_callback() self.dismiss_callback = nil diff --git a/plugins/kosync.koplugin/main.lua b/plugins/kosync.koplugin/main.lua index efac058cd..14b54aaeb 100644 --- a/plugins/kosync.koplugin/main.lua +++ b/plugins/kosync.koplugin/main.lua @@ -11,6 +11,7 @@ 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") @@ -24,20 +25,21 @@ local KOSync = WidgetContainer:extend{ is_doc_only = true, title = _("Register/login to KOReader server"), - page_update_times = 0, - last_page = -1, - last_page_turn_ticks = 0, + 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 = { - -- Forward and backward whisper sync settings are using different - -- default value, so none of following opinions should be zero. - PROMPT = 1, - WHISPER = 2, + PROMPT = 1, + SILENT = 2, DISABLE = 3, - - DEFAULT_FORWARD = 1, - DEFAULT_BACKWARD = 3, } local CHECKSUM_METHOD = { @@ -45,6 +47,64 @@ local CHECKSUM_METHOD = { 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 on devices w/ NetworkManager support, as wifi is unlikely to be on at all times there, and the nagging enabling this may cause requires careful consideration. + auto_sync = not Device:hasWifiManager(), + 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:hasWifiManager() 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 _("Unavailable") + 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") @@ -109,218 +169,224 @@ function KOSync:onDispatcherRegisterActions() end function KOSync:onReaderReady() - --- @todo: Viable candidate for a port to the new readSetting API - local settings = G_reader_settings:readSetting("kosync") or {} - self.kosync_custom_server = settings.custom_server - self.kosync_username = settings.username - self.kosync_userkey = settings.userkey - self.kosync_auto_sync = settings.auto_sync ~= false - self.kosync_pages_before_update = settings.pages_before_update - self.kosync_whisper_forward = settings.whisper_forward or SYNC_STRATEGY.DEFAULT_FORWARD - self.kosync_whisper_backward = settings.whisper_backward or SYNC_STRATEGY.DEFAULT_BACKWARD - self.kosync_checksum_method = settings.checksum_method or CHECKSUM_METHOD.BINARY - self.kosync_device_id = G_reader_settings:readSetting("device_id") - --assert(self.kosync_device_id) - if self.kosync_auto_sync then - self:_onResume() + -- 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 self:registerEvents() self:onDispatcherRegisterActions() - self.ui.menu:registerToMainMenu(self) - -- Make sure checksum has been calculated at the very first time a document has been opened, to - -- avoid document saving feature to impact the checksum, and eventually impact the document - -- identity in the progress sync feature. - self.view.document:fastDigest(self.ui.doc_settings) + + 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.kosync_userkey and (_("Logout")) + return self.settings.userkey and (_("Logout")) or _("Register") .. " / " .. _("Login") end, keep_menu_open = true, callback_func = function() - if self.kosync_userkey then + if self.settings.userkey then return function(menu) - self._menu_to_update = menu - self:logout() + self:logout(menu) end else return function(menu) - self._menu_to_update = menu - self:login() + self:login(menu) end end end, + separator = true, }, { - text = _("Auto sync now and future"), - checked_func = function() return self.kosync_auto_sync end, + 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() - self.kosync_auto_sync = not self.kosync_auto_sync + -- 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:hasWifiManager() and G_reader_settings:readSetting("wifi_enable_action") ~= "turn_on" 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.kosync_auto_sync then - -- since we will update the progress when closing document, we should pull - -- current progress now to avoid to overwrite it silently. - self:getProgress(true) + 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 document, we should push - -- current progress now to avoid to lose it silently. - self:updateProgress(true) + -- 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 - self:saveSettings() end, }, { - text = _("Whisper sync"), - enabled_func = function() return self.kosync_auto_sync 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() - return T(_("Sync to latest record (%1)"), getNameStrategy(self.kosync_whisper_forward)) + -- 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 = _("Auto"), + text = _("Silently"), checked_func = function() - return self.kosync_whisper_forward == SYNC_STRATEGY.WHISPER + return self.settings.sync_forward == SYNC_STRATEGY.SILENT end, callback = function() - self:setWhisperForward(SYNC_STRATEGY.WHISPER) + self:setSyncForward(SYNC_STRATEGY.SILENT) end, }, { text = _("Prompt"), checked_func = function() - return self.kosync_whisper_forward == SYNC_STRATEGY.PROMPT + return self.settings.sync_forward == SYNC_STRATEGY.PROMPT end, callback = function() - self:setWhisperForward(SYNC_STRATEGY.PROMPT) + self:setSyncForward(SYNC_STRATEGY.PROMPT) end, }, { - text = _("Disable"), + text = _("Never"), checked_func = function() - return self.kosync_whisper_forward == SYNC_STRATEGY.DISABLE + return self.settings.sync_forward == SYNC_STRATEGY.DISABLE end, callback = function() - self:setWhisperForward(SYNC_STRATEGY.DISABLE) + self:setSyncForward(SYNC_STRATEGY.DISABLE) end, }, } }, { text_func = function() - return T(_("Sync to a previous record (%1)"), getNameStrategy(self.kosync_whisper_backward)) + return T(_("Sync to an older state (%1)"), getNameStrategy(self.settings.sync_backward)) end, sub_item_table = { { - text = _("Auto"), + text = _("Silently"), checked_func = function() - return self.kosync_whisper_backward == SYNC_STRATEGY.WHISPER + return self.settings.sync_backward == SYNC_STRATEGY.SILENT end, callback = function() - self:setWhisperBackward(SYNC_STRATEGY.WHISPER) + self:setSyncBackward(SYNC_STRATEGY.SILENT) end, }, { text = _("Prompt"), checked_func = function() - return self.kosync_whisper_backward == SYNC_STRATEGY.PROMPT + return self.settings.sync_backward == SYNC_STRATEGY.PROMPT end, callback = function() - self:setWhisperBackward(SYNC_STRATEGY.PROMPT) + self:setSyncBackward(SYNC_STRATEGY.PROMPT) end, }, { - text = _("Disable"), + text = _("Never"), checked_func = function() - return self.kosync_whisper_backward == SYNC_STRATEGY.DISABLE + return self.settings.sync_backward == SYNC_STRATEGY.DISABLE end, callback = function() - self:setWhisperBackward(SYNC_STRATEGY.DISABLE) + self:setSyncBackward(SYNC_STRATEGY.DISABLE) end, }, } }, }, + separator = true, }, { - text = _("Push progress from this device"), + text = _("Push progress from this device now"), enabled_func = function() - return self.kosync_userkey ~= nil + return self.settings.userkey ~= nil end, callback = function() - self:updateProgress(true) + self:updateProgress(true, true) end, }, { - text = _("Pull progress from other devices"), + text = _("Pull progress from other devices now"), enabled_func = function() - return self.kosync_userkey ~= nil + return self.settings.userkey ~= nil end, callback = function() - self:getProgress(true) - end, - }, - { - 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.kosync_custom_server or "https://", - type = "text", - callback = function(input) - self:setCustomServer(input) - end, - } - end, - }, - { - text = _("Sync every # pages"), - keep_menu_open = true, - callback = function() - 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.kosync_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) - end - } - UIManager:show(items) + self:getProgress(true, true) end, + separator = true, }, { text = _("Document matching method"), sub_item_table = { { - text = _("Binary. Only identical files will sync progress."), + text = _("Binary. Only identical files will be kept in sync."), checked_func = function() - return self.kosync_checksum_method == CHECKSUM_METHOD.BINARY + return self.settings.checksum_method == CHECKSUM_METHOD.BINARY end, callback = function() self:setChecksumMethod(CHECKSUM_METHOD.BINARY) end, }, { - text = _("Filename. Files with the same name will sync progress."), + text = _("Filename. Files with matching names will be kept in sync."), checked_func = function() - return self.kosync_checksum_method == CHECKSUM_METHOD.FILENAME + return self.settings.checksum_method == CHECKSUM_METHOD.FILENAME end, callback = function() self:setChecksumMethod(CHECKSUM_METHOD.FILENAME) @@ -333,33 +399,28 @@ If set to 0, updating progress based on page turns will be disabled.]]), end function KOSync:setPagesBeforeUpdate(pages_before_update) - self.kosync_pages_before_update = pages_before_update > 0 and pages_before_update or nil - self:saveSettings() + self.settings.pages_before_update = pages_before_update > 0 and pages_before_update or nil end function KOSync:setCustomServer(server) - logger.dbg("set custom server", server) - self.kosync_custom_server = server ~= "" and server or nil - self:saveSettings() + logger.dbg("KOSync: Setting custom server to:", server) + self.settings.custom_server = server ~= "" and server or nil end -function KOSync:setWhisperForward(strategy) - self.kosync_whisper_forward = strategy - self:saveSettings() +function KOSync:setSyncForward(strategy) + self.settings.sync_forward = strategy end -function KOSync:setWhisperBackward(strategy) - self.kosync_whisper_backward = strategy - self:saveSettings() +function KOSync:setSyncBackward(strategy) + self.settings.sync_backward = strategy end function KOSync:setChecksumMethod(method) - self.kosync_checksum_method = method - self:saveSettings() + self.settings.checksum_method = method end -function KOSync:login() - if NetworkMgr:willRerunWhenOnline(function() self:login() end) then +function KOSync:login(menu) + if NetworkMgr:willRerunWhenOnline(function() self:login(menu) end) then return end @@ -368,7 +429,7 @@ function KOSync:login() title = self.title, fields = { { - text = self.kosync_username, + text = self.settings.username, hint = "username", }, { @@ -398,7 +459,7 @@ function KOSync:login() else UIManager:close(dialog) UIManager:scheduleIn(0.5, function() - self:doLogin(username, password) + self:doLogin(username, password, menu) end) UIManager:show(InfoMessage:new{ text = _("Logging in. Please wait…"), @@ -420,7 +481,7 @@ function KOSync:login() else UIManager:close(dialog) UIManager:scheduleIn(0.5, function() - self:doRegister(username, password) + self:doRegister(username, password, menu) end) UIManager:show(InfoMessage:new{ text = _("Registering. Please wait…"), @@ -436,10 +497,10 @@ function KOSync:login() dialog:onShowKeyboard() end -function KOSync:doRegister(username, password) +function KOSync:doRegister(username, password, menu) local KOSyncClient = require("KOSyncClient") local client = KOSyncClient:new{ - custom_url = self.kosync_custom_server, + custom_url = self.settings.custom_server, service_spec = self.path .. "/api.json" } -- on Android to avoid ANR (no-op on other platforms) @@ -458,9 +519,11 @@ function KOSync:doRegister(username, password) }) end elseif status then - self.kosync_username = username - self.kosync_userkey = userkey - self._menu_to_update:updateItems() + self.settings.username = username + self.settings.userkey = userkey + if menu then + menu:updateItems() + end UIManager:show(InfoMessage:new{ text = _("Registered to KOReader server."), }) @@ -470,13 +533,12 @@ function KOSync:doRegister(username, password) }) end Device:setIgnoreInput(false) - self:saveSettings() end -function KOSync:doLogin(username, password) +function KOSync:doLogin(username, password, menu) local KOSyncClient = require("KOSyncClient") local client = KOSyncClient:new{ - custom_url = self.kosync_custom_server, + custom_url = self.settings.custom_server, service_spec = self.path .. "/api.json" } Device:setIgnoreInput(true) @@ -496,9 +558,11 @@ function KOSync:doLogin(username, password) Device:setIgnoreInput(false) return elseif status then - self.kosync_username = username - self.kosync_userkey = userkey - self._menu_to_update:updateItems() + self.settings.username = username + self.settings.userkey = userkey + if menu then + menu:updateItems() + end UIManager:show(InfoMessage:new{ text = _("Logged in to KOReader server."), }) @@ -508,14 +572,14 @@ function KOSync:doLogin(username, password) }) end Device:setIgnoreInput(false) - self:saveSettings() end -function KOSync:logout() - self.kosync_userkey = nil - self.kosync_auto_sync = true - self._menu_to_update:updateItems() - self:saveSettings() +function KOSync:logout(menu) + self.settings.userkey = nil + self.settings.auto_sync = true + if menu then + menu:updateItems() + end end function KOSync:getLastPercent() @@ -535,7 +599,7 @@ function KOSync:getLastProgress() end function KOSync:getDocumentDigest() - if self.kosync_checksum_method == CHECKSUM_METHOD.FILENAME then + if self.settings.checksum_method == CHECKSUM_METHOD.FILENAME then return self:getFileNameDigest() else return self:getFileDigest() @@ -543,7 +607,7 @@ function KOSync:getDocumentDigest() end function KOSync:getFileDigest() - return self.view.document:fastDigest() + return self.ui.doc_settings:readSetting("partial_md5_checksum") end function KOSync:getFileNameDigest() @@ -557,7 +621,7 @@ function KOSync:getFileNameDigest() end function KOSync:syncToProgress(progress) - logger.dbg("sync to", 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 @@ -565,21 +629,27 @@ function KOSync:syncToProgress(progress) end end -function KOSync:updateProgress(manual) - if not self.kosync_username or not self.kosync_userkey then - if manual then +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 - if manual and NetworkMgr:willRerunWhenOnline(function() self:updateProgress(manual) end) then + 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.kosync_custom_server, + custom_url = self.settings.custom_server, service_spec = self.path .. "/api.json" } local doc_digest = self:getDocumentDigest() @@ -587,16 +657,17 @@ function KOSync:updateProgress(manual) local percentage = self:getLastPercent() local ok, err = pcall(client.update_progress, client, - self.kosync_username, - self.kosync_userkey, + self.settings.username, + self.settings.userkey, doc_digest, progress, percentage, Device.model, - self.kosync_device_id, + self.device_id, function(ok, body) - logger.dbg("update progress for", self.view.document.file, ok) - if manual then + 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."), @@ -608,45 +679,67 @@ function KOSync:updateProgress(manual) end end) if not ok then - if manual then showSyncError() end + 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(manual) - if not self.kosync_username or not self.kosync_userkey then - if manual then +function KOSync:getProgress(ensure_networking, interactive) + if not self.settings.username or not self.settings.userkey then + if interactive then promptLogin() end return end - if manual and NetworkMgr:willRerunWhenOnline(function() self:getProgress(manual) end) then + 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.kosync_custom_server, + 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.kosync_username, - self.kosync_userkey, + self.settings.username, + self.settings.userkey, doc_digest, function(ok, body) - logger.dbg("get progress for", self.view.document.file, 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 manual then + if interactive then showSyncError() end return end if not body.percentage then - if manual then + if interactive then UIManager:show(InfoMessage:new{ text = _("No progress found for this document."), timeout = 3, @@ -656,8 +749,8 @@ function KOSync:getProgress(manual) end if body.device == Device.model - and body.device_id == self.kosync_device_id then - if manual then + 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, @@ -669,11 +762,11 @@ function KOSync:getProgress(manual) body.percentage = Math.roundPercent(body.percentage) local progress = self:getLastProgress() local percentage = self:getLastPercent() - logger.dbg("current progress", percentage) + logger.dbg("KOSync: Current progress:", percentage * 100, "% =>", progress) if percentage == body.percentage or body.progress == progress then - if manual then + if interactive then UIManager:show(InfoMessage:new{ text = _("The progress has already been synchronized."), timeout = 3, @@ -683,9 +776,9 @@ function KOSync:getProgress(manual) end -- The progress needs to be updated. - if manual then - -- If user actively pulls progress from other devices, we always update the - -- progress without further confirmation. + 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 @@ -693,17 +786,16 @@ function KOSync:getProgress(manual) local self_older if body.timestamp ~= nil then - self_older = (body.timestamp > self.last_page_turn_ticks) + self_older = (body.timestamp > self.last_page_turn_timestamp) else - -- If we are working with old sync server, we can only use - -- percentage field. + -- 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.kosync_whisper_forward == SYNC_STRATEGY.WHISPER then + if self.settings.sync_forward == SYNC_STRATEGY.SILENT then self:syncToProgress(body.progress) showSyncedMessage() - elseif self.kosync_whisper_forward == SYNC_STRATEGY.PROMPT then + 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), @@ -714,10 +806,10 @@ function KOSync:getProgress(manual) }) end else -- if not self_older then - if self.kosync_whisper_backward == SYNC_STRATEGY.WHISPER then + if self.settings.sync_backward == SYNC_STRATEGY.SILENT then self:syncToProgress(body.progress) showSyncedMessage() - elseif self.kosync_whisper_backward == SYNC_STRATEGY.PROMPT then + 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), @@ -730,36 +822,30 @@ function KOSync:getProgress(manual) end end) if not ok then - if manual then showSyncError() end + if interactive then showSyncError() end if err then logger.dbg("err:", err) end end + + self.pull_timestamp = now end -function KOSync:saveSettings() - local settings = { - custom_server = self.kosync_custom_server, - username = self.kosync_username, - userkey = self.kosync_userkey, - auto_sync = self.kosync_auto_sync, - pages_before_update = self.kosync_pages_before_update, - whisper_forward = - (self.kosync_whisper_forward ~= SYNC_STRATEGY.DEFAULT_FORWARD - and self.kosync_whisper_forward - or nil), - whisper_backward = - (self.kosync_whisper_backward ~= SYNC_STRATEGY.DEFAULT_BACKWARD - and self.kosync_whisper_backward - or nil), - checksum_method = self.kosync_checksum_method, - } - G_reader_settings:saveSetting("kosync", settings) +function KOSync:_onCloseDocument() + logger.dbg("KOSync: onCloseDocument") + -- 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:onCloseDocument() - logger.dbg("on close document") - if self.kosync_auto_sync then - self:updateProgress() - 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) @@ -767,54 +853,81 @@ function KOSync:_onPageUpdate(page) return end - if self.last_page == -1 then - self.last_page = page - elseif self.last_page ~= page then + if self.last_page ~= page then self.last_page = page - self.last_page_turn_ticks = os.time() - self.page_update_times = self.page_update_times + 1 - if self.kosync_pages_before_update and self.page_update_times == self.kosync_pages_before_update then - self.page_update_times = 0 - UIManager:scheduleIn(1, function() self:updateProgress() end) + 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() - UIManager:scheduleIn(1, function() self:getProgress() end) + 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:hasWifiManager() 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:_onFlushSettings() - if self.ui == nil or self.ui.document == nil then return end - self:updateProgress() +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() - self:_onResume() + 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() - if not self.kosync_userkey then return end - self:updateProgress(true) + self:updateProgress(true, true) end function KOSync:onKOSyncPullProgress() - if not self.kosync_userkey then return end - self:getProgress(true) + self:getProgress(true, true) end function KOSync:registerEvents() - if self.kosync_auto_sync then + if self.settings.auto_sync then + self.onCloseDocument = self._onCloseDocument self.onPageUpdate = self._onPageUpdate self.onResume = self._onResume - self.onFlushSettings = self._onFlushSettings + self.onSuspend = self._onSuspend self.onNetworkConnected = self._onNetworkConnected + self.onNetworkDisconnecting = self._onNetworkDisconnecting else + self.onCloseDocument = nil self.onPageUpdate = nil self.onResume = nil - self.onFlushSettings = 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