From 9bf19d1bb3b5c601383af71400003e71b7a78ed4 Mon Sep 17 00:00:00 2001 From: NiLuJe Date: Sun, 2 Oct 2022 03:01:49 +0200 Subject: [PATCH] Assorted bag'o tweaks & fixes (#9569) * UIManager: Support more specialized update modes for corner-cases: * A2, which we'll use for the VirtualKeyboards keys (they'd... inadvertently switched to UI with the highlight refactor). * NO_MERGE variants of ui & partial (for sunxi). Use `[ui]` in ReaderHighlight's popup, because of a Sage kernel bug that could otherwise make it translucent, sometimes completely so (*sigh*). * UIManager: Assorted code cleanups & simplifications. * Logger & dbg: Unify logging style, and code cleanups. * SDL: Unbreak suspend/resume outside of the emulator (fix #9567). * NetworkMgr: Cache the network status, and allow it to be queried. (Used by AutoSuspend to avoid repeatedly poking the system when computing the standby schedule delay). * OneTimeMigration: Don't forget about `NETWORK_PROXY` & `STARDICT_DATA_DIR` when migrating `defaults.persistent.lua` (fix #9573) * WakeupMgr: Workaround an apparent limitation of the RTC found on i.MX5 Kobo devices, where setting a wakealarm further than UINT16_MAX seconds in the future would apparently overflow and wraparound... (fix #8039, many thanks to @yfede for the extensive deep-dive and for actually accurately pinpointing the issue!). * Kobo: Handle standby transitions at full CPU clock speeds, in order to limit the latency hit. * UIManager: Properly quit on reboot & exit. This ensures our exit code is preserved, as we exit on our own terms (instead of being killed by the init system). This is important on platforms where exit codes are semantically meaningful (e.g., Kobo). * UIManager: Speaking of reboot & exit, make sure the Screensaver shows in all circumstances (e.g., autoshutdown, re: #9542)), and that there aren't any extraneous refreshes triggered. (Additionally, fix a minor regression since #9448 about tracking this very transient state on Kobo & Cervantes). * Kindle: ID the upcoming Scribe. * Bump base (https://github.com/koreader/koreader-base/pull/1524) --- base | 2 +- .../apps/reader/modules/readerhighlight.lua | 4 +- .../apps/reader/modules/readerrolling.lua | 2 +- frontend/dbg.lua | 86 +++--- frontend/device/cervantes/device.lua | 36 +-- frontend/device/generic/device.lua | 19 +- frontend/device/generic/powerd.lua | 2 +- frontend/device/input.lua | 1 - frontend/device/kindle/device.lua | 55 +++- frontend/device/kobo/device.lua | 269 ++++++++++------ frontend/device/pocketbook/device.lua | 4 +- frontend/device/remarkable/device.lua | 12 +- frontend/device/sdl/device.lua | 17 +- frontend/device/sony-prstux/device.lua | 26 +- frontend/device/wakeupmgr.lua | 53 +++- frontend/logger.lua | 76 +++-- frontend/ui/data/onetime_migration.lua | 12 +- .../elements/common_settings_menu_table.lua | 3 +- frontend/ui/elements/mass_storage.lua | 6 +- frontend/ui/event.lua | 2 +- frontend/ui/network/manager.lua | 85 ++++-- frontend/ui/network/networklistener.lua | 9 + frontend/ui/uimanager.lua | 287 +++++++++--------- frontend/ui/widget/virtualkeyboard.lua | 24 +- plugins/autosuspend.koplugin/main.lua | 8 +- reader.lua | 16 +- spec/unit/commonrequire.lua | 1 - spec/unit/device_spec.lua | 16 +- 28 files changed, 673 insertions(+), 460 deletions(-) diff --git a/base b/base index 9e4e7e899..99143a2a1 160000 --- a/base +++ b/base @@ -1 +1 @@ -Subproject commit 9e4e7e899e06853305d0a2e4b1f103d2a0840068 +Subproject commit 99143a2a11f40fb9ea635bfe876ec3dd8ae7b2d4 diff --git a/frontend/apps/reader/modules/readerhighlight.lua b/frontend/apps/reader/modules/readerhighlight.lua index a812102e1..7c9a896d5 100644 --- a/frontend/apps/reader/modules/readerhighlight.lua +++ b/frontend/apps/reader/modules/readerhighlight.lua @@ -941,7 +941,9 @@ function ReaderHighlight:onShowHighlightMenu(page, index) buttons = highlight_buttons, tap_close_callback = function() self:handleEvent(Event:new("Tap")) end, } - UIManager:show(self.highlight_dialog) + -- NOTE: Disable merging for this update, + -- or the buggy Sage kernel may alpha-blend it into the page (with a bogus alpha value, to boot)... + UIManager:show(self.highlight_dialog, "[ui]") end dbg:guard(ReaderHighlight, "onShowHighlightMenu", function(self) diff --git a/frontend/apps/reader/modules/readerrolling.lua b/frontend/apps/reader/modules/readerrolling.lua index d5276918d..46deff1cf 100644 --- a/frontend/apps/reader/modules/readerrolling.lua +++ b/frontend/apps/reader/modules/readerrolling.lua @@ -787,7 +787,7 @@ function ReaderRolling:onRestoreBookLocation(saved_location) end function ReaderRolling:onGotoViewRel(diff) - logger.dbg("goto relative screen:", diff, ", in mode: ", self.view.view_mode) + logger.dbg("goto relative screen:", diff, ", in mode:", self.view.view_mode) if self.view.view_mode == "scroll" then local footer_height = ((self.view.footer_visible and not self.view.footer.settings.reclaim_height) and 1 or 0) * self.view.footer:getHeight() local page_visible_height = self.ui.dimen.h - footer_height diff --git a/frontend/dbg.lua b/frontend/dbg.lua index 315533c6d..404ca52b1 100644 --- a/frontend/dbg.lua +++ b/frontend/dbg.lua @@ -26,28 +26,45 @@ local Dbg = { -- set to nil so first debug:turnOff call won't be skipped is_on = nil, is_verbose = nil, - ev_log = nil, } local Dbg_mt = {} -local function LvDEBUG(lv, ...) - local line = "" - for i,v in ipairs({...}) do - if type(v) == "table" then - line = line .. " " .. dump(v, lv) - else - line = line .. " " .. tostring(v) +local LvDEBUG +if isAndroid then + LvDEBUG = function(...) + local line = {} + for _, v in ipairs({...}) do + if type(v) == "table" then + table.insert(line, dump(v, math.huge)) + else + table.insert(line, tostring(v)) + end end + return android.LOGV(table.concat(line, " ")) end - if isAndroid then - android.LOGV(line) - else - io.stdout:write(string.format("# %s %s\n", os.date("%x-%X"), line)) - io.stdout:flush() +else + LvDEBUG = function(...) + local line = { + os.date("%x-%X DEBUG"), + } + for _, v in ipairs({...}) do + if type(v) == "table" then + table.insert(line, dump(v, math.huge)) + else + table.insert(line, tostring(v)) + end + end + table.insert(line, "\n") + return io.write(table.concat(line, " ")) end end +--- Helper function to help dealing with nils in Dbg:guard... +local function pack_values(...) + return select("#", ...), {...} +end + --- Turn on debug mode. -- This should only be used in tests and at the user's request. function Dbg:turnOn() @@ -55,7 +72,7 @@ function Dbg:turnOn() self.is_on = true logger:setLevel(logger.levels.dbg) - Dbg_mt.__call = function(dbg, ...) LvDEBUG(math.huge, ...) end + Dbg_mt.__call = function(_, ...) return LvDEBUG(...) end --- Pass a guard function to detect bad input values. Dbg.guard = function(_, mod, method, pre_guard, post_guard) local old_method = mod[method] @@ -63,11 +80,11 @@ function Dbg:turnOn() if pre_guard then pre_guard(...) end - local values = {old_method(...)} + local n, values = pack_values(old_method(...)) if post_guard then post_guard(...) end - return unpack(values) + return unpack(values, 1, n) end end --- Use this instead of a regular Lua @{assert}(). @@ -75,19 +92,6 @@ function Dbg:turnOn() assert(check, msg) return check end - - -- create or clear ev log file - --- @note: On Linux, use CLOEXEC to avoid polluting the fd table of our child processes. - --- Otherwise, it can be problematic w/ wpa_supplicant & USBMS... - --- Note that this is entirely undocumented, but at least LuaJIT passes the mode as-is to fopen, so, we're good. - local open_flags = "w" - if jit.os == "Linux" then - -- Oldest Kindle devices are too old to support O_CLOEXEC... - if os.getenv("KINDLE_LEGACY") ~= "yes" then - open_flags = "we" - end - end - self.ev_log = io.open("ev.log", open_flags) end --- Turn off debug mode. @@ -96,15 +100,12 @@ function Dbg:turnOff() if self.is_on == false then return end self.is_on = false logger:setLevel(logger.levels.info) - function Dbg_mt.__call() end - function Dbg.guard() end + Dbg_mt.__call = function() end + -- NOTE: This doesn't actually disengage previously wrapped methods! + Dbg.guard = function() end Dbg.dassert = function(check) return check end - if self.ev_log then - self.ev_log:close() - self.ev_log = nil - end end --- Turn on verbose mode. @@ -116,24 +117,13 @@ end --- Simple table dump. function Dbg:v(...) if self.is_verbose then - LvDEBUG(math.huge, ...) - end -end - ---- Log @{ui.event|Event} to dedicated log file. -function Dbg:logEv(ev) - local ev_value = tostring(ev.value) - local log = ev.type.."|"..ev.code.."|" - ..ev_value.."|"..ev.time.sec.."|"..ev.time.usec.."\n" - if self.ev_log then - self.ev_log:write(log) - self.ev_log:flush() + return LvDEBUG(...) end end --- Simple traceback. function Dbg:traceback() - LvDEBUG(math.huge, debug.traceback()) + return LvDEBUG(debug.traceback()) end setmetatable(Dbg, Dbg_mt) diff --git a/frontend/device/cervantes/device.lua b/frontend/device/cervantes/device.lua index 862d61160..de2888038 100644 --- a/frontend/device/cervantes/device.lua +++ b/frontend/device/cervantes/device.lua @@ -230,10 +230,10 @@ function Cervantes:resume() os.execute("./resume.sh") end function Cervantes:reboot() - os.execute("reboot") + os.execute("sleep 1 && reboot &") end function Cervantes:powerOff() - os.execute("halt") + os.execute("sleep 1 && halt &") end -- This method is the same as the one in kobo/device.lua except the sleep cover part. @@ -241,11 +241,11 @@ function Cervantes:setEventHandlers(UIManager) -- We do not want auto suspend procedure to waste battery during -- suspend. So let's unschedule it when suspending, and restart it after -- resume. Done via the plugin's onSuspend/onResume handlers. - UIManager.event_handlers["Suspend"] = function() + UIManager.event_handlers.Suspend = function() self:_beforeSuspend() self:onPowerEvent("Suspend") end - UIManager.event_handlers["Resume"] = function() + UIManager.event_handlers.Resume = function() -- MONOTONIC doesn't tick during suspend, -- invalidate the last battery capacity pull time so that we get up to date data immediately. self:getPowerDevice():invalidateCapacityCache() @@ -253,59 +253,59 @@ function Cervantes:setEventHandlers(UIManager) self:onPowerEvent("Resume") self:_afterResume() end - UIManager.event_handlers["PowerPress"] = function() + UIManager.event_handlers.PowerPress = function() -- Always schedule power off. -- Press the power button for 2+ seconds to shutdown directly from suspend. UIManager:scheduleIn(2, UIManager.poweroff_action) end - UIManager.event_handlers["PowerRelease"] = function() - if not self._entered_poweroff_stage then + UIManager.event_handlers.PowerRelease = function() + if not UIManager._entered_poweroff_stage then UIManager:unschedule(UIManager.poweroff_action) -- resume if we were suspended if self.screen_saver_mode then - UIManager.event_handlers["Resume"]() + UIManager.event_handlers.Resume() else - UIManager.event_handlers["Suspend"]() + UIManager.event_handlers.Suspend() end end end - UIManager.event_handlers["Light"] = function() + UIManager.event_handlers.Light = function() self:getPowerDevice():toggleFrontlight() end -- USB plug events with a power-only charger - UIManager.event_handlers["Charging"] = function() + UIManager.event_handlers.Charging = function() self:_beforeCharging() -- NOTE: Plug/unplug events will wake the device up, which is why we put it back to sleep. if self.screen_saver_mode then - UIManager.event_handlers["Suspend"]() + UIManager.event_handlers.Suspend() end end - UIManager.event_handlers["NotCharging"] = function() + UIManager.event_handlers.NotCharging = function() -- We need to put the device into suspension, other things need to be done before it. self:usbPlugOut() self:_afterNotCharging() if self.screen_saver_mode then - UIManager.event_handlers["Suspend"]() + UIManager.event_handlers.Suspend() end end -- USB plug events with a data-aware host - UIManager.event_handlers["UsbPlugIn"] = function() + UIManager.event_handlers.UsbPlugIn = function() self:_beforeCharging() -- NOTE: Plug/unplug events will wake the device up, which is why we put it back to sleep. if self.screen_saver_mode then - UIManager.event_handlers["Suspend"]() + UIManager.event_handlers.Suspend() else -- Potentially start an USBMS session local MassStorage = require("ui/elements/mass_storage") MassStorage:start() end end - UIManager.event_handlers["UsbPlugOut"] = function() + UIManager.event_handlers.UsbPlugOut = function() -- We need to put the device into suspension, other things need to be done before it. self:usbPlugOut() self:_afterNotCharging() if self.screen_saver_mode then - UIManager.event_handlers["Suspend"]() + UIManager.event_handlers.Suspend() else -- Potentially dismiss the USBMS ConfirmBox local MassStorage = require("ui/elements/mass_storage") diff --git a/frontend/device/generic/device.lua b/frontend/device/generic/device.lua index df7553e4a..0b98038a2 100644 --- a/frontend/device/generic/device.lua +++ b/frontend/device/generic/device.lua @@ -350,8 +350,7 @@ function Device:install() ok_callback = function() local save_quit = function() self:saveSettings() - UIManager:quit() - UIManager._exit_code = 85 + UIManager:quit(85) end UIManager:broadcastEvent(Event:new("Exit", save_quit)) end, @@ -384,9 +383,11 @@ function Device:suspend() end -- Hardware specific method to resume the device function Device:resume() end +-- NOTE: These two should ideally run in the background, and only trip the action after a small delay, +-- to give us time to quit first. +-- e.g., os.execute("sleep 1 && shutdown -r now &") -- Hardware specific method to power off the device function Device:powerOff() end - -- Hardware specific method to reboot the device function Device:reboot() end @@ -576,7 +577,7 @@ end -- Set device event handlers common to all devices function Device:_setEventHandlers(UIManager) if self:canReboot() then - UIManager.event_handlers["Reboot"] = function() + UIManager.event_handlers.Reboot = function() local ConfirmBox = require("ui/widget/confirmbox") UIManager:show(ConfirmBox:new{ text = _("Are you sure you want to reboot the device?"), @@ -589,11 +590,11 @@ function Device:_setEventHandlers(UIManager) }) end else - UIManager.event_handlers["Reboot"] = function() end + UIManager.event_handlers.Reboot = function() end end if self:canPowerOff() then - UIManager.event_handlers["PowerOff"] = function() + UIManager.event_handlers.PowerOff = function() local ConfirmBox = require("ui/widget/confirmbox") UIManager:show(ConfirmBox:new{ text = _("Are you sure you want to power off the device?"), @@ -606,7 +607,7 @@ function Device:_setEventHandlers(UIManager) }) end else - UIManager.event_handlers["PowerOff"] = function() end + UIManager.event_handlers.PowerOff = function() end end self:setEventHandlers(UIManager) @@ -615,10 +616,10 @@ end -- Devices can add additional event handlers by overwriting this method. function Device:setEventHandlers(UIManager) -- These will be most probably overwritten in the device specific `setEventHandlers` - UIManager.event_handlers["Suspend"] = function() + UIManager.event_handlers.Suspend = function() self:_beforeSuspend(false) end - UIManager.event_handlers["Resume"] = function() + UIManager.event_handlers.Resume = function() self:_afterResume(false) end end diff --git a/frontend/device/generic/powerd.lua b/frontend/device/generic/powerd.lua index 3adb81332..3fd72b621 100644 --- a/frontend/device/generic/powerd.lua +++ b/frontend/device/generic/powerd.lua @@ -155,7 +155,7 @@ function BasePowerD:unchecked_read_int_file(file) fd:close() return int else - return + return nil end end diff --git a/frontend/device/input.lua b/frontend/device/input.lua index bc63f2c65..80a5e41a3 100644 --- a/frontend/device/input.lua +++ b/frontend/device/input.lua @@ -1287,7 +1287,6 @@ function Input:waitEvent(now, deadline) -- NOTE: This is rather spammy and computationally intensive, -- and we can't conditionally prevent evalutation of function arguments, -- so, just hide the whole thing behind a branch ;). - DEBUG:logEv(event) if event.type == C.EV_KEY then logger.dbg(string.format( "key event => code: %d (%s), value: %s, time: %d.%06d", diff --git a/frontend/device/kindle/device.lua b/frontend/device/kindle/device.lua index bac1ffe0f..1fcf644a4 100644 --- a/frontend/device/kindle/device.lua +++ b/frontend/device/kindle/device.lua @@ -369,29 +369,29 @@ function Kindle:readyToSuspend() end function Kindle:setEventHandlers(UIManager) - UIManager.event_handlers["Suspend"] = function() + UIManager.event_handlers.Suspend = function() self.powerd:toggleSuspend() end - UIManager.event_handlers["IntoSS"] = function() + UIManager.event_handlers.IntoSS = function() self:_beforeSuspend() self:intoScreenSaver() end - UIManager.event_handlers["OutOfSS"] = function() + UIManager.event_handlers.OutOfSS = function() self:outofScreenSaver() self:_afterResume() end - UIManager.event_handlers["Charging"] = function() + UIManager.event_handlers.Charging = function() self:_beforeCharging() self:usbPlugIn() end - UIManager.event_handlers["NotCharging"] = function() + UIManager.event_handlers.NotCharging = function() self:usbPlugOut() self:_afterNotCharging() end - UIManager.event_handlers["WakeupFromSuspend"] = function() + UIManager.event_handlers.WakeupFromSuspend = function() self:wakeupFromSuspend() end - UIManager.event_handlers["ReadyToSuspend"] = function() + UIManager.event_handlers.ReadyToSuspend = function() self:readyToSuspend() end end @@ -599,6 +599,20 @@ local KindlePaperWhite5 = Kindle:new{ canDoSwipeAnimation = yes, } +local KindleScribe = Kindle:new{ + model = "KindleScribe", + isMTK = yes, + isTouchDevice = yes, + hasFrontlight = yes, + hasNaturalLight = yes, + hasNaturalLightMixer = yes, + display_dpi = 300, + -- TBD + touch_dev = "/dev/input/by-path/platform-1001e000.i2c-event", + canHWDither = no, + canDoSwipeAnimation = yes, +} + function Kindle2:init() self.screen = require("ffi/framebuffer_einkfb"):new{device = self, debug = logger.dbg} self.powerd = require("device/kindle/powerd"):new{ @@ -1096,6 +1110,27 @@ function KindlePaperWhite5:init() self.input.open("fake_events") end +function KindleScribe:init() + self.screen = require("ffi/framebuffer_mxcfb"):new{device = self, debug = logger.dbg} + -- TBD, assume PW5 for now + self.powerd = require("device/kindle/powerd"):new{ + device = self, + fl_intensity_file = "/sys/class/backlight/fp9966-bl1/brightness", + warmth_intensity_file = "/sys/class/backlight/fp9966-bl0/brightness", + batt_capacity_file = "/sys/class/power_supply/bd71827_bat/capacity", + is_charging_file = "/sys/class/power_supply/bd71827_bat/charging", + batt_status_file = "/sys/class/power_supply/bd71827_bat/status", + } + + -- Enable the so-called "fast" mode, so as to prevent the driver from silently promoting refreshes to REAGL. + self.screen:_MTK_ToggleFastMode(true) + + Kindle.init(self) + + self.input.open(self.touch_dev) + self.input.open("fake_events") +end + function KindleTouch:exit() if self:isMTK() then -- Disable the so-called "fast" mode @@ -1135,6 +1170,7 @@ KindlePaperWhite4.exit = KindleTouch.exit KindleBasic3.exit = KindleTouch.exit KindleOasis3.exit = KindleTouch.exit KindlePaperWhite5.exit = KindleTouch.exit +KindleScribe.exit = KindleTouch.exit function Kindle3:exit() -- send double menu key press events to trigger screen refresh @@ -1188,7 +1224,8 @@ local pw4_set = Set { "0PP", "0T1", "0T2", "0T3", "0T4", "0T5", "0T6", "16Q", "16R", "16S", "16T", "16U", "16V" } local kt4_set = Set { "10L", "0WF", "0WG", "0WH", "0WJ", "0VB" } local koa3_set = Set { "11L", "0WQ", "0WP", "0WN", "0WM", "0WL" } -local pw5_set = Set { "1LG", "1Q0", "1PX", "1VD", "219", "21A", "2BH", "2BJ" } +local pw5_set = Set { "1LG", "1Q0", "1PX", "1VD", "219", "21A", "2BH", "2BJ", "2DK" } +local scribe_set = Set { "22D", "25T", "23A", "2AQ", "2AP", "1XH", "22C" } if kindle_sn_lead == "B" or kindle_sn_lead == "9" then local kindle_devcode = string.sub(kindle_sn, 3, 4) @@ -1233,6 +1270,8 @@ else return KindleOasis3 elseif pw5_set[kindle_devcode_v2] then return KindlePaperWhite5 + elseif scribe_set[kindle_devcode_v2] then + return KindleScribe end end diff --git a/frontend/device/kobo/device.lua b/frontend/device/kobo/device.lua index 44f4fd88d..414b0da92 100644 --- a/frontend/device/kobo/device.lua +++ b/frontend/device/kobo/device.lua @@ -1,6 +1,8 @@ local Generic = require("device/generic/device") local Geom = require("ui/geometry") +local UIManager -- Updated on UIManager init local WakeupMgr = require("device/wakeupmgr") +local time = require("ui/time") local ffiUtil = require("ffi/util") local lfs = require("libs/libkoreader-lfs") local logger = require("logger") @@ -27,6 +29,31 @@ local function koboEnableWifi(toggle) end end +-- NOTE: Cheap-ass way of checking if Wi-Fi seems to be enabled... +-- Since the crux of the issues lies in race-y module unloading, this is perfectly fine for our usage. +local function koboIsWifiOn() + local needle = os.getenv("WIFI_MODULE") or "sdio_wifi_pwr" + local nlen = #needle + -- /proc/modules is usually empty, unless Wi-Fi or USB is enabled + -- We could alternatively check if lfs.attributes("/proc/sys/net/ipv4/conf/" .. os.getenv("INTERFACE"), "mode") == "directory" + -- c.f., also what Cervantes does via /sys/class/net/eth0/carrier to check if the interface is up. + -- That said, since we only care about whether *modules* are loaded, this does the job nicely. + local f = io.open("/proc/modules", "re") + if not f then + return false + end + + local found = false + for haystack in f:lines() do + if haystack:sub(1, nlen) == needle then + found = true + break + end + end + f:close() + return found +end + -- checks if standby is available on the device local function checkStandby() logger.dbg("Kobo: checking if standby is possible ...") @@ -63,6 +90,47 @@ local function writeToSys(val, file) return nw == bytes end +-- Return the highest core number +local function getCPUCount() + local fd = io.open("/sys/devices/system/cpu/possible", "re") + if fd then + local str = fd:read("*line") + fd:close() + + -- Format is n-N, where n is the first core, and N the last (e.g., 0-3) + return tonumber(str:match("%d+$")) or 1 + else + return 1 + end +end + +local function getCPUGovernor(knob) + local fd = io.open(knob, "re") + if fd then + local str = fd:read("*line") + fd:close() + -- If we're currently using the userspace governor, fudge that to conservative, as we won't ever standby with Wi-Fi on. + -- (userspace is only used on i.MX5 for DVFS shenanigans when Wi-Fi is enabled) + if str == "userspace" then + str = "conservative" + end + return str + else + return nil + end +end + +local function getRTCName() + local fd = io.open("/sys/class/rtc/rtc0/name", "re") + if fd then + local str = fd:read("*line") + fd:close() + return str + else + return nil + end +end + local Kobo = Generic:new{ model = "Kobo", isKobo = yes, @@ -74,6 +142,7 @@ local Kobo = Generic:new{ canReboot = yes, canPowerOff = yes, canSuspend = yes, + supportsScreensaver = yes, -- most Kobos have X/Y switched for the touch screen touch_switch_xy = true, -- most Kobos have also mirrored X coordinates @@ -477,7 +546,6 @@ end function Kobo:getKeyRepeat() -- Sanity check (mostly for the testsuite's benefit...) if not self.ntx_fd then - self.hasKeyRepeat = false return false end @@ -485,20 +553,14 @@ function Kobo:getKeyRepeat() if C.ioctl(self.ntx_fd, C.EVIOCGREP, self.key_repeat) < 0 then local err = ffi.errno() logger.warn("Device:getKeyRepeat: EVIOCGREP ioctl failed:", ffi.string(C.strerror(err))) - self.hasKeyRepeat = false + return false else - self.hasKeyRepeat = true logger.dbg("Key repeat is set up to repeat every", self.key_repeat[C.REP_PERIOD], "ms after a delay of", self.key_repeat[C.REP_DELAY], "ms") + return true end - - return self.hasKeyRepeat end function Kobo:disableKeyRepeat() - if not self.hasKeyRepeat then - return - end - -- NOTE: LuaJIT zero inits, and PERIOD == 0 with DELAY == 0 disables repeats ;). local key_repeat = ffi.new("unsigned int[?]", C.REP_CNT) if C.ioctl(self.ntx_fd, C.EVIOCSREP, key_repeat) < 0 then @@ -508,10 +570,6 @@ function Kobo:disableKeyRepeat() end function Kobo:restoreKeyRepeat() - if not self.hasKeyRepeat then - return - end - if C.ioctl(self.ntx_fd, C.EVIOCSREP, self.key_repeat) < 0 then local err = ffi.errno() logger.warn("Device:restoreKeyRepeat: EVIOCSREP ioctl failed:", ffi.string(C.strerror(err))) @@ -606,6 +664,35 @@ function Kobo:init() end end + -- NOTE: i.MX5 devices have a wonky RTC that doesn't like alarms set further away that UINT16_MAX seconds from now... + -- (c.f., WakeupMgr for more details). + -- NOTE: getRTCName is currently hardcoded to rtc0 (which is also WakeupMgr's default). + local dodgy_rtc = false + if getRTCName() == "pmic_rtc" then + -- This *should* match the 'RTC' (46) NTX HWConfig field being set to 'MSP430' (0). + dodgy_rtc = true + end + + -- Detect the various CPU governor sysfs knobs... + if util.pathExists("/sys/devices/system/cpu/cpufreq/policy0") then + self.cpu_governor_knob = "/sys/devices/system/cpu/cpufreq/policy0/scaling_governor" + else + self.cpu_governor_knob = "/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor" + end + self.default_cpu_governor = getCPUGovernor(self.cpu_governor_knob) + -- NOP unsupported methods + if not self.default_cpu_governor then + self.performanceCPUGovernor = function() end + self.defaultCPUGovernor = function() end + end + + -- And while we're on CPU-related endeavors... + self.cpu_count = self:isSMP() and getCPUCount() or 1 + -- NOP unsupported methods + if self.cpu_count == 1 then + self.enableCPUCores = function() end + end + -- Automagically set this so we never have to remember to do it manually ;p if self:hasNaturalLight() and self.frontlight_settings and self.frontlight_settings.frontlight_mixer then self.hasNaturalLightMixer = yes @@ -651,7 +738,9 @@ function Kobo:init() main_finger_slot = self.main_finger_slot or 0, pressure_event = self.pressure_event, } - self.wakeup_mgr = WakeupMgr:new() + self.wakeup_mgr = WakeupMgr:new{ + dodgy_rtc = dodgy_rtc, + } Generic.init(self) @@ -677,7 +766,17 @@ function Kobo:init() self:initEventAdjustHooks() -- See if the device supports key repeat - self:getKeyRepeat() + if not self:getKeyRepeat() then + -- NOP unsupported methods + self.disableKeyRepeat = function() end + self.restoreKeyRepeat = function() end + end + + -- NOP unsupported methods + if not self:canToggleChargingLED() then + self.toggleChargingLED = function() end + self.setupChargingLED = function() end + end -- We have no way of querying the current state of the charging LED, so, start from scratch. -- Much like Nickel, start by turning it off. @@ -754,34 +853,9 @@ function Kobo:initNetworkManager(NetworkMgr) os.execute("./restore-wifi-async.sh") end - -- NOTE: Cheap-ass way of checking if Wi-Fi seems to be enabled... - -- Since the crux of the issues lies in race-y module unloading, this is perfectly fine for our usage. - function NetworkMgr:isWifiOn() - local needle = os.getenv("WIFI_MODULE") or "sdio_wifi_pwr" - local nlen = #needle - -- /proc/modules is usually empty, unless Wi-Fi or USB is enabled - -- We could alternatively check if lfs.attributes("/proc/sys/net/ipv4/conf/" .. os.getenv("INTERFACE"), "mode") == "directory" - -- c.f., also what Cervantes does via /sys/class/net/eth0/carrier to check if the interface is up. - -- That said, since we only care about whether *modules* are loaded, this does the job nicely. - local f = io.open("/proc/modules", "re") - if not f then - return false - end - - local found = false - for haystack in f:lines() do - if haystack:sub(1, nlen) == needle then - found = true - break - end - end - f:close() - return found - end + NetworkMgr.isWifiOn = koboIsWifiOn end -function Kobo:supportsScreensaver() return true end - function Kobo:setTouchEventHandler() if self.touch_snow_protocol then self.input.snow_protocol = true @@ -872,14 +946,12 @@ end -- NOTE: We overload this to make sure checkUnexpectedWakeup doesn't trip *before* the newly scheduled suspend function Kobo:rescheduleSuspend() - local UIManager = require("ui/uimanager") UIManager:unschedule(self.suspend) UIManager:unschedule(self.checkUnexpectedWakeup) UIManager:scheduleIn(self.suspend_wait_timeout, self.suspend, self) end function Kobo:checkUnexpectedWakeup() - local UIManager = require("ui/uimanager") -- Just in case another event like SleepCoverClosed also scheduled a suspend UIManager:unschedule(self.suspend) @@ -911,6 +983,20 @@ end --- The function to put the device into standby, with enabled touchscreen. -- max_duration ... maximum time for the next standby, can wake earlier (e.g. Tap, Button ...) function Kobo:standby(max_duration) + -- NOTE: Switch to the performance CPU governor, in order to speed up the resume process so as to lower its latency cost... + -- (It won't have any impact on power efficiency *during* suspend, so there's not really any drawback). + self:performanceCPUGovernor() + + --[[ + -- On most devices, attempting to PM with a Wi-Fi module loaded will horribly crash the kernel, so, don't? + -- NOTE: Much like suspend, our caller should ensure this never happens, hence this being commented out ;). + if koboIsWifiOn() then + -- AutoSuspend relies on NetworkMgr:getWifiState to prevent this, so, if we ever trip this, it's a bug ;). + logger.err("Kobo standby: cannot standby with Wi-Fi modules loaded! (NetworkMgr is confused: this is a bug)") + return + end + --]] + -- We don't really have anything to schedule, we just need an alarm out of WakeupMgr ;). local function standby_alarm() end @@ -919,7 +1005,6 @@ function Kobo:standby(max_duration) self.wakeup_mgr:addTask(max_duration, standby_alarm) end - local time = require("ui/time") logger.info("Kobo standby: asking to enter standby . . .") local standby_time = time.boottime_or_realtime_coarse() @@ -949,11 +1034,13 @@ function Kobo:standby(max_duration) --]] self.wakeup_mgr:removeTasks(nil, standby_alarm) end + + -- And restore the standard CPU scheduler once we're done dealing with the wakeup event. + UIManager:tickAfterNext(self.defaultCPUGovernor, self) end function Kobo:suspend() logger.info("Kobo suspend: going to sleep . . .") - local UIManager = require("ui/uimanager") UIManager:unschedule(self.checkUnexpectedWakeup) -- NOTE: Sleep as little as possible here, sleeping has a tendency to make -- everything mysteriously hang... @@ -1020,7 +1107,6 @@ function Kobo:suspend() end --]] - local time = require("ui/time") logger.info("Kobo suspend: asking for a suspend to RAM . . .") local suspend_time = time.boottime_or_realtime_coarse() @@ -1072,7 +1158,6 @@ function Kobo:resume() -- Reset unexpected_wakeup_count ASAP self.unexpected_wakeup_count = 0 -- Unschedule the checkUnexpectedWakeup shenanigans. - local UIManager = require("ui/uimanager") UIManager:unschedule(self.checkUnexpectedWakeup) UIManager:unschedule(self.suspend) @@ -1122,11 +1207,11 @@ function Kobo:powerOff() self.wakeup_mgr:unsetWakeupAlarm() -- Then shut down without init's help - os.execute("poweroff -f") + os.execute("sleep 1 && poweroff -f &") end function Kobo:reboot() - os.execute("reboot") + os.execute("sleep 1 && reboot &") end function Kobo:toggleGSensor(toggle) @@ -1138,10 +1223,6 @@ function Kobo:toggleGSensor(toggle) end function Kobo:toggleChargingLED(toggle) - if not self:canToggleChargingLED() then - return - end - -- We have no way of querying the current state from the HW! if toggle == nil then return @@ -1204,33 +1285,20 @@ function Kobo:toggleChargingLED(toggle) end -- Return the highest core number -local function getCPUCount() +function Kobo:getCPUCount() local fd = io.open("/sys/devices/system/cpu/possible", "re") if fd then local str = fd:read("*line") fd:close() -- Format is n-N, where n is the first core, and N the last (e.g., 0-3) - return tonumber(str:match("%d+$")) or 0 + return tonumber(str:match("%d+$")) or 1 else - return 0 + return 1 end end function Kobo:enableCPUCores(amount) - if not self:isSMP() then - return - end - - if not self.cpu_count then - self.cpu_count = getCPUCount() - end - - -- Not actually SMP or getCPUCount failed... - if self.cpu_count == 0 then - return - end - -- CPU0 is *always* online ;). for n=1, self.cpu_count do local path = "/sys/devices/system/cpu/cpu" .. n .. "/online" @@ -1249,6 +1317,14 @@ function Kobo:enableCPUCores(amount) end end +function Kobo:performanceCPUGovernor() + writeToSys("performance", self.cpu_governor_knob) +end + +function Kobo:defaultCPUGovernor() + writeToSys(self.default_cpu_governor, self.cpu_governor_knob) +end + function Kobo:isStartupScriptUpToDate() -- Compare the hash of the *active* script (i.e., the one in /tmp) to the *potential* one (i.e., the one in KOREADER_DIR) local current_script = "/tmp/koreader.sh" @@ -1258,15 +1334,18 @@ function Kobo:isStartupScriptUpToDate() return md5.sumFile(current_script) == md5.sumFile(new_script) end -function Kobo:setEventHandlers(UIManager) +function Kobo:setEventHandlers(uimgr) + -- Update our module-local + UIManager = uimgr + -- We do not want auto suspend procedure to waste battery during -- suspend. So let's unschedule it when suspending, and restart it after -- resume. Done via the plugin's onSuspend/onResume handlers. - UIManager.event_handlers["Suspend"] = function() + UIManager.event_handlers.Suspend = function() self:_beforeSuspend() self:onPowerEvent("Suspend") end - UIManager.event_handlers["Resume"] = function() + UIManager.event_handlers.Resume = function() -- MONOTONIC doesn't tick during suspend, -- invalidate the last battery capacity pull time so that we get up to date data immediately. self:getPowerDevice():invalidateCapacityCache() @@ -1274,59 +1353,59 @@ function Kobo:setEventHandlers(UIManager) self:onPowerEvent("Resume") self:_afterResume() end - UIManager.event_handlers["PowerPress"] = function() + UIManager.event_handlers.PowerPress = function() -- Always schedule power off. -- Press the power button for 2+ seconds to shutdown directly from suspend. UIManager:scheduleIn(2, UIManager.poweroff_action) end - UIManager.event_handlers["PowerRelease"] = function() - if not self._entered_poweroff_stage then + UIManager.event_handlers.PowerRelease = function() + if not UIManager._entered_poweroff_stage then UIManager:unschedule(UIManager.poweroff_action) -- resume if we were suspended if self.screen_saver_mode then - UIManager.event_handlers["Resume"]() + UIManager.event_handlers.Resume() else - UIManager.event_handlers["Suspend"]() + UIManager.event_handlers.Suspend() end end end - UIManager.event_handlers["Light"] = function() + UIManager.event_handlers.Light = function() self:getPowerDevice():toggleFrontlight() end -- USB plug events with a power-only charger - UIManager.event_handlers["Charging"] = function() + UIManager.event_handlers.Charging = function() self:_beforeCharging() -- NOTE: Plug/unplug events will wake the device up, which is why we put it back to sleep. if self.screen_saver_mode then - UIManager.event_handlers["Suspend"]() + UIManager.event_handlers.Suspend() end end - UIManager.event_handlers["NotCharging"] = function() + UIManager.event_handlers.NotCharging = function() -- We need to put the device into suspension, other things need to be done before it. self:usbPlugOut() self:_afterNotCharging() if self.screen_saver_mode then - UIManager.event_handlers["Suspend"]() + UIManager.event_handlers.Suspend() end end -- USB plug events with a data-aware host - UIManager.event_handlers["UsbPlugIn"] = function() + UIManager.event_handlers.UsbPlugIn = function() self:_beforeCharging() -- NOTE: Plug/unplug events will wake the device up, which is why we put it back to sleep. if self.screen_saver_mode then - UIManager.event_handlers["Suspend"]() + UIManager.event_handlers.Suspend() else -- Potentially start an USBMS session local MassStorage = require("ui/elements/mass_storage") MassStorage:start() end end - UIManager.event_handlers["UsbPlugOut"] = function() + UIManager.event_handlers.UsbPlugOut = function() -- We need to put the device into suspension, other things need to be done before it. self:usbPlugOut() self:_afterNotCharging() if self.screen_saver_mode then - UIManager.event_handlers["Suspend"]() + UIManager.event_handlers.Suspend() else -- Potentially dismiss the USBMS ConfirmBox local MassStorage = require("ui/elements/mass_storage") @@ -1337,25 +1416,25 @@ function Kobo:setEventHandlers(UIManager) if G_reader_settings:isTrue("ignore_power_sleepcover") then -- NOTE: The hardware event itself will wake the kernel up if it's in suspend (:/). -- Let the unexpected wakeup guard handle that. - UIManager.event_handlers["SleepCoverClosed"] = nil - UIManager.event_handlers["SleepCoverOpened"] = nil + UIManager.event_handlers.SleepCoverClosed = nil + UIManager.event_handlers.SleepCoverOpened = nil elseif G_reader_settings:isTrue("ignore_open_sleepcover") then -- Just ignore wakeup events, and do NOT set is_cover_closed, -- so device/generic/device will let us use the power button to wake ;). - UIManager.event_handlers["SleepCoverClosed"] = function() - UIManager.event_handlers["Suspend"]() + UIManager.event_handlers.SleepCoverClosed = function() + UIManager.event_handlers.Suspend() end - UIManager.event_handlers["SleepCoverOpened"] = function() + UIManager.event_handlers.SleepCoverOpened = function() self.is_cover_closed = false end else - UIManager.event_handlers["SleepCoverClosed"] = function() + UIManager.event_handlers.SleepCoverClosed = function() self.is_cover_closed = true - UIManager.event_handlers["Suspend"]() + UIManager.event_handlers.Suspend() end - UIManager.event_handlers["SleepCoverOpened"] = function() + UIManager.event_handlers.SleepCoverOpened = function() self.is_cover_closed = false - UIManager.event_handlers["Resume"]() + UIManager.event_handlers.Resume() end end end diff --git a/frontend/device/pocketbook/device.lua b/frontend/device/pocketbook/device.lua index 52ba32eb1..7eb6043b2 100644 --- a/frontend/device/pocketbook/device.lua +++ b/frontend/device/pocketbook/device.lua @@ -390,10 +390,10 @@ end function PocketBook:setEventHandlers(UIManager) -- Only fg/bg state plugin notifiers, not real power event. - UIManager.event_handlers["Suspend"] = function() + UIManager.event_handlers.Suspend = function() self:_beforeSuspend() end - UIManager.event_handlers["Resume"] = function() + UIManager.event_handlers.Resume = function() self:_afterResume() end end diff --git a/frontend/device/remarkable/device.lua b/frontend/device/remarkable/device.lua index 93f4b0623..c37a753eb 100644 --- a/frontend/device/remarkable/device.lua +++ b/frontend/device/remarkable/device.lua @@ -233,25 +233,25 @@ function Remarkable:getDefaultCoverPath() end function Remarkable:setEventHandlers(UIManager) - UIManager.event_handlers["Suspend"] = function() + UIManager.event_handlers.Suspend = function() self:_beforeSuspend() self:onPowerEvent("Suspend") end - UIManager.event_handlers["Resume"] = function() + UIManager.event_handlers.Resume = function() self:onPowerEvent("Resume") self:_afterResume() end - UIManager.event_handlers["PowerPress"] = function() + UIManager.event_handlers.PowerPress = function() UIManager:scheduleIn(2, UIManager.poweroff_action) end - UIManager.event_handlers["PowerRelease"] = function() + UIManager.event_handlers.PowerRelease = function() if not UIManager._entered_poweroff_stage then UIManager:unschedule(UIManager.poweroff_action) -- resume if we were suspended if self.screen_saver_mode then - UIManager.event_handlers["Resume"]() + UIManager.event_handlers.Resume() else - UIManager.event_handlers["Suspend"]() + UIManager.event_handlers.Suspend() end end end diff --git a/frontend/device/sdl/device.lua b/frontend/device/sdl/device.lua index 4c287d51b..20fea0249 100644 --- a/frontend/device/sdl/device.lua +++ b/frontend/device/sdl/device.lua @@ -344,20 +344,27 @@ function Device:toggleFullscreen() end function Device:setEventHandlers(UIManager) - UIManager.event_handlers["Suspend"] = function() + if not self:canSuspend() then + -- If we can't suspend, we have no business even trying to, as we may not have overloaded `Device:simulateResume`, + -- and since the empty Generic prototype doesn't flip `Device.screen_saver_mode`, we'd be stuck if we tried... + -- Instead, rely on the Generic Suspend/Resume handlers, which are sane ;). + return + end + + UIManager.event_handlers.Suspend = function() self:_beforeSuspend() self:simulateSuspend() end - UIManager.event_handlers["Resume"] = function() + UIManager.event_handlers.Resume = function() self:simulateResume() self:_afterResume() end - UIManager.event_handlers["PowerRelease"] = function() + UIManager.event_handlers.PowerRelease = function() -- Resume if we were suspended if self.screen_saver_mode then - UIManager.event_handlers["Resume"]() + UIManager.event_handlers.Resume() else - UIManager.event_handlers["Suspend"]() + UIManager.event_handlers.Suspend() end end end diff --git a/frontend/device/sony-prstux/device.lua b/frontend/device/sony-prstux/device.lua index 0da49ff63..dcf055127 100644 --- a/frontend/device/sony-prstux/device.lua +++ b/frontend/device/sony-prstux/device.lua @@ -119,11 +119,11 @@ function SonyPRSTUX:resume() end function SonyPRSTUX:powerOff() - os.execute("poweroff") + os.execute("sleep 1 && poweroff &") end function SonyPRSTUX:reboot() - os.execute("reboot") + os.execute("sleep 1 && reboot &") end function SonyPRSTUX:usbPlugIn() @@ -189,37 +189,37 @@ function SonyPRSTUX:getDeviceModel() end function SonyPRSTUX:setEventHandlers(UIManager) - UIManager.event_handlers["Suspend"] = function() + UIManager.event_handlers.Suspend = function() self:_beforeSuspend() self:intoScreenSaver() self:suspend() end - UIManager.event_handlers["Resume"] = function() + UIManager.event_handlers.Resume = function() self:resume() self:outofScreenSaver() self:_afterResume() end - UIManager.event_handlers["PowerPress"] = function() + UIManager.event_handlers.PowerPress = function() UIManager:scheduleIn(2, UIManager.poweroff_action) end - UIManager.event_handlers["PowerRelease"] = function() + UIManager.event_handlers.PowerRelease = function() if not UIManager._entered_poweroff_stage then UIManager:unschedule(UIManager.poweroff_action) -- resume if we were suspended if self.screen_saver_mode then - UIManager.event_handlers["Resume"]() + UIManager.event_handlers.Resume() else - UIManager.event_handlers["Suspend"]() + UIManager.event_handlers.Suspend() end end end - UIManager.event_handlers["Charging"] = function() + UIManager.event_handlers.Charging = function() self:_beforeCharging() end - UIManager.event_handlers["NotCharging"] = function() + UIManager.event_handlers.NotCharging = function() self:_afterNotCharging() end - UIManager.event_handlers["UsbPlugIn"] = function() + UIManager.event_handlers.UsbPlugIn = function() if self.screen_saver_mode then self:resume() self:outofScreenSaver() @@ -227,10 +227,10 @@ function SonyPRSTUX:setEventHandlers(UIManager) end self:usbPlugIn() end - UIManager.event_handlers["UsbPlugOut"] = function() + UIManager.event_handlers.UsbPlugOut = function() self:usbPlugOut() end - UIManager.event_handlers["__default__"] = function(input_event) + UIManager.event_handlers.__default__ = function(input_event) -- Same as in Kobo: we want to ignore keys during suspension if not self.screen_saver_mode then UIManager:sendEvent(input_event) diff --git a/frontend/device/wakeupmgr.lua b/frontend/device/wakeupmgr.lua index 19d831ec5..30a0d6bd0 100644 --- a/frontend/device/wakeupmgr.lua +++ b/frontend/device/wakeupmgr.lua @@ -23,6 +23,7 @@ local WakeupMgr = { dev_rtc = "/dev/rtc0", -- RTC device _task_queue = {}, -- Table with epoch at which to schedule the task and the function to be scheduled. rtc = RTC, -- The RTC implementation to use, defaults to the RTC module. + dodgy_rtc = false, -- If the RTC has trouble with timers further away than UINT16_MAX (e.g., on i.MX5). } --[[-- @@ -44,6 +45,11 @@ function WakeupMgr:new(o) return o end +-- This is a dummy task we use when working around i.MX5 RTC issues. +-- We need to be able to recognize it so that we can deal with it in removeTasks... +function WakeupMgr.DummyTaskCallback() +end + --[[-- Add a task to the queue. @@ -57,17 +63,37 @@ function WakeupMgr:addTask(seconds_from_now, callback) assert(type(seconds_from_now) == "number", "delay is not a number") assert(type(callback) == "function", "callback is not a function") - local epoch = RTC:secondsFromNowToEpoch(seconds_from_now) - logger.info("WakeupMgr: scheduling wakeup in", seconds_from_now) - local old_upcoming_task = (self._task_queue[1] or {}).epoch - table.insert(self._task_queue, { - epoch = epoch, - callback = callback, - }) - --- @todo Binary insert? This table should be so small that performance doesn't matter. - -- It might be useful to have that available as a utility function regardless. + -- NOTE: Apparently, some RTCs have trouble with timers further away than UINT16_MAX, so, + -- if necessary, setup an alarm chain to work it around... + -- c.f., https://github.com/koreader/koreader/issues/8039#issuecomment-1263547625 + if self.dodgy_rtc and seconds_from_now > 0xFFFF then + logger.info("WakeupMgr: scheduling a chain of alarms for a wakeup in", seconds_from_now) + + local seconds_left = seconds_from_now + while seconds_left > 0 do + local epoch = RTC:secondsFromNowToEpoch(seconds_left) + logger.info("WakeupMgr: scheduling wakeup in", seconds_left, "->", epoch) + + -- We only need a callback for the final wakeup, we take care of not breaking the chain when an action is pop'ed. + table.insert(self._task_queue, { + epoch = epoch, + callback = seconds_left == seconds_from_now and callback or self.DummyTaskCallback, + }) + + seconds_left = seconds_left - 0xFFFF + end + else + local epoch = RTC:secondsFromNowToEpoch(seconds_from_now) + logger.info("WakeupMgr: scheduling wakeup in", seconds_from_now, "->", epoch) + + table.insert(self._task_queue, { + epoch = epoch, + callback = callback, + }) + end + table.sort(self._task_queue, function(a, b) return a.epoch < b.epoch end) local new_upcoming_task = self._task_queue[1].epoch @@ -93,9 +119,15 @@ function WakeupMgr:removeTasks(epoch, callback) local removed = false local reschedule = false + local match_epoch = epoch for k = #self._task_queue, 1, -1 do local v = self._task_queue[k] - if epoch == v.epoch or callback == v.callback then + -- NOTE: For the DummyTaskCallback shenanigans, we at least try to only remove those that come earlier than our match... + if (epoch == v.epoch or callback == v.callback) or + (self.dodgy_rtc and match_epoch and self.DummyTaskCallback == v.callback and v.epoch < match_epoch) then + if not match_epoch then + match_epoch = v.epoch + end table.remove(self._task_queue, k) removed = true -- If we've successfuly pop'ed the upcoming task, we need to schedule the next one (if any) on exit. @@ -170,6 +202,7 @@ Set wakeup alarm. Simple wrapper for @{ffi.rtc.setWakeupAlarm}. --]] function WakeupMgr:setWakeupAlarm(epoch, enabled) + logger.dbg("WakeupMgr:setWakeupAlarm for", epoch, os.date("(%F %T %z)", epoch)) return self.rtc:setWakeupAlarm(epoch, enabled) end diff --git a/frontend/logger.lua b/frontend/logger.lua index bbfcda8a0..49b94528f 100644 --- a/frontend/logger.lua +++ b/frontend/logger.lua @@ -21,17 +21,17 @@ local DEFAULT_DUMP_LVL = 10 -- @field warn warning -- @field err error local LOG_LVL = { - dbg = 1, + dbg = 1, info = 2, warn = 3, - err = 4, + err = 4, } local LOG_PREFIX = { - dbg = 'DEBUG', - info = 'INFO ', - warn = 'WARN ', - err = 'ERROR', + dbg = "DEBUG", + info = "INFO ", + warn = "WARN ", + err = "ERROR", } local noop = function() end @@ -40,39 +40,55 @@ local Logger = { levels = LOG_LVL, } -local function log(log_lvl, dump_lvl, ...) - local line = "" - for i,v in ipairs({...}) do - if type(v) == "table" then - line = line .. " " .. dump(v, dump_lvl) - else - line = line .. " " .. tostring(v) +local log +if isAndroid then + local ANDROID_LOG_FNS = { + dbg = android.LOGV, + info = android.LOGI, + warn = android.LOGW, + err = android.LOGE, + } + + log = function(log_lvl, ...) + local line = {} + for _, v in ipairs({...}) do + if type(v) == "table" then + table.insert(line, dump(v, DEFAULT_DUMP_LVL)) + else + table.insert(line, tostring(v)) + end end + return ANDROID_LOG_FNS[log_lvl](table.concat(line, " ")) end - if isAndroid then - if log_lvl == "dbg" then - android.LOGV(line) - elseif log_lvl == "info" then - android.LOGI(line) - elseif log_lvl == "warn" then - android.LOGW(line) - elseif log_lvl == "err" then - android.LOGE(line) +else + log = function(log_lvl, ...) + local line = { + os.date("%x-%X"), + LOG_PREFIX[log_lvl], + } + for _, v in ipairs({...}) do + if type(v) == "table" then + table.insert(line, dump(v, DEFAULT_DUMP_LVL)) + else + table.insert(line, tostring(v)) + end end - else - io.stdout:write(os.date("%x-%X"), " ", LOG_PREFIX[log_lvl], line, "\n") - io.stdout:flush() + + -- NOTE: Either we add the LF to the table and we get an extra space before it because of table.concat, + -- or we pass it to write after a comma, and it generates an extra write syscall... + -- That, or just rewrite every logger call to handle spacing themselves ;). + table.insert(line, "\n") + return io.write(table.concat(line, " ")) end end local LVL_FUNCTIONS = { - dbg = function(...) log('dbg', DEFAULT_DUMP_LVL, ...) end, - info = function(...) log('info', DEFAULT_DUMP_LVL, ...) end, - warn = function(...) log('warn', DEFAULT_DUMP_LVL, ...) end, - err = function(...) log('err', DEFAULT_DUMP_LVL, ...) end, + dbg = function(...) return log("dbg", ...) end, + info = function(...) return log("info", ...) end, + warn = function(...) return log("warn", ...) end, + err = function(...) return log("err", ...) end, } - --[[-- Set logging level. By default, level is set to info. diff --git a/frontend/ui/data/onetime_migration.lua b/frontend/ui/data/onetime_migration.lua index 15f0d2cab..b33006cae 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 = 20220922 +local CURRENT_MIGRATION_DATE = 20220930 -- Retrieve the date of the previous migration, if any local last_migration_date = G_reader_settings:readSetting("last_migration_date", 0) @@ -446,9 +446,9 @@ if last_migration_date < 20220914 then end end --- The great defaults.persistent.lua migration to LuaDefaults -if last_migration_date < 20220922 then - logger.info("Performing one-time migration for 20220922") +-- The great defaults.persistent.lua migration to LuaDefaults (#9546) +if last_migration_date < 20220930 then + logger.info("Performing one-time migration for 20220930") local defaults_path = DataStorage:getDataDir() .. "/defaults.persistent.lua" local defaults = {} @@ -465,6 +465,10 @@ if last_migration_date < 20220922 then G_defaults:saveSetting(k, v) end end + -- Handle NETWORK_PROXY & STARDICT_DATA_DIR, which default to nil (and as such don't actually exist in G_defaults). + G_defaults:saveSetting("NETWORK_PROXY", defaults.NETWORK_PROXY) + G_defaults:saveSetting("STARDICT_DATA_DIR", defaults.STARDICT_DATA_DIR) + G_defaults:flush() local archived_path = DataStorage:getDataDir() .. "/defaults.legacy.lua" diff --git a/frontend/ui/elements/common_settings_menu_table.lua b/frontend/ui/elements/common_settings_menu_table.lua index 64cad7286..95cdcf214 100644 --- a/frontend/ui/elements/common_settings_menu_table.lua +++ b/frontend/ui/elements/common_settings_menu_table.lua @@ -18,8 +18,7 @@ if Device:isCervantes() then common_settings.start_bq = { text = T(_("Start %1 reader app"), "BQ"), callback = function() - UIManager:quit() - UIManager._exit_code = 87 + UIManager:quit(87) end, } end diff --git a/frontend/ui/elements/mass_storage.lua b/frontend/ui/elements/mass_storage.lua index ba3967493..7a5c3b099 100644 --- a/frontend/ui/elements/mass_storage.lua +++ b/frontend/ui/elements/mass_storage.lua @@ -61,9 +61,8 @@ function MassStorage:start(never_ask) ok_callback = function() -- save settings before activating USBMS: UIManager:flushSettings() - UIManager._exit_code = 86 UIManager:broadcastEvent(Event:new("Close")) - UIManager:quit() + UIManager:quit(86) end, cancel_callback = function() self:dismiss() @@ -74,9 +73,8 @@ function MassStorage:start(never_ask) else -- save settings before activating USBMS: UIManager:flushSettings() - UIManager._exit_code = 86 UIManager:broadcastEvent(Event:new("Close")) - UIManager:quit() + UIManager:quit(86) end end diff --git a/frontend/ui/event.lua b/frontend/ui/event.lua index f7f5f1a24..946178456 100644 --- a/frontend/ui/event.lua +++ b/frontend/ui/event.lua @@ -32,7 +32,7 @@ function Event:new(name, ...) handler = "on"..name, -- Minor trickery to handle nils, c.f., http://lua-users.org/wiki/VarargTheSecondClassCitizen --- @fixme: Move to table.pack() (which stores the count in the field `n`) here & table.unpack() in @{ui.widget.eventlistener|EventListener} once we build LuaJIT w/ 5.2 compat. - argc = select('#', ...), + argc = select("#", ...), args = {...} } setmetatable(o, self) diff --git a/frontend/ui/network/manager.lua b/frontend/ui/network/manager.lua index 29218e41b..d6212e5c6 100644 --- a/frontend/ui/network/manager.lua +++ b/frontend/ui/network/manager.lua @@ -12,7 +12,10 @@ local logger = require("logger") local _ = require("gettext") local T = ffiutil.template -local NetworkMgr = {} +local NetworkMgr = { + is_wifi_on = false, + is_connected = false, +} function NetworkMgr:readNWSettings() self.nw_settings = LuaSettings:open(DataStorage:getSettingsDir().."/network.lua") @@ -30,7 +33,7 @@ function NetworkMgr:connectivityCheck(iter, callback, widget) if Device:hasWifiManager() and not Device:isEmulator() then os.execute("pkill -TERM restore-wifi-async.sh 2>/dev/null") end - NetworkMgr:turnOffWifi() + self:turnOffWifi() -- Handle the UI warning if it's from a beforeWifiAction... if widget then @@ -40,7 +43,8 @@ function NetworkMgr:connectivityCheck(iter, callback, widget) return end - if NetworkMgr:isWifiOn() and NetworkMgr:isConnected() then + self:queryNetworkState() + if self.is_wifi_on and self.is_connected then self.wifi_was_on = true G_reader_settings:makeTrue("wifi_was_on") UIManager:broadcastEvent(Event:new("NetworkConnected")) @@ -63,12 +67,12 @@ function NetworkMgr:connectivityCheck(iter, callback, widget) end end else - UIManager:scheduleIn(2, function() NetworkMgr:connectivityCheck(iter + 1, callback, widget) end) + UIManager:scheduleIn(2, self.connectivityCheck, self, iter + 1, callback, widget) end end function NetworkMgr:scheduleConnectivityCheck(callback, widget) - UIManager:scheduleIn(2, function() NetworkMgr:connectivityCheck(1, callback, widget) end) + UIManager:scheduleIn(2, self.connectivityCheck, self, 1, callback, widget) end function NetworkMgr:init() @@ -79,18 +83,19 @@ function NetworkMgr:init() self:turnOffWifi() end + self:queryNetworkState() self.wifi_was_on = G_reader_settings:isTrue("wifi_was_on") if self.wifi_was_on and G_reader_settings:isTrue("auto_restore_wifi") then -- Don't bother if WiFi is already up... - if not (self:isWifiOn() and self:isConnected()) then + if not self.is_connected then self:restoreWifiAsync() end self:scheduleConnectivityCheck() else -- Trigger an initial NetworkConnected event if WiFi was already up when we were launched - if NetworkMgr:isWifiOn() and NetworkMgr:isConnected() then + if self.is_connected then -- NOTE: This needs to be delayed because NetworkListener is initialized slightly later by the FM/Reader app... - UIManager:scheduleIn(2, function() UIManager:broadcastEvent(Event:new("NetworkConnected")) end) + UIManager:scheduleIn(2, UIManager.broadcastEvent, UIManager, Event:new("NetworkConnected")) end end end @@ -108,7 +113,7 @@ function NetworkMgr:authenticateNetwork() end function NetworkMgr:disconnectNetwork() end function NetworkMgr:obtainIP() end function NetworkMgr:releaseIP() end --- This function should unblockly call both turnOnWifi() and obtainIP(). +-- This function should call both turnOnWifi() and obtainIP() in a non-blocking manner. function NetworkMgr:restoreWifiAsync() end -- End of device specific methods @@ -212,9 +217,9 @@ function NetworkMgr:beforeWifiAction(callback) local wifi_enable_action = G_reader_settings:readSetting("wifi_enable_action") if wifi_enable_action == "turn_on" then - NetworkMgr:turnOnWifiAndWaitForConnection(callback) + self:turnOnWifiAndWaitForConnection(callback) else - NetworkMgr:promptWifiOn(callback) + self:promptWifiOn(callback) end end @@ -234,9 +239,9 @@ function NetworkMgr:afterWifiAction(callback) callback() end elseif wifi_disable_action == "turn_off" then - NetworkMgr:turnOffWifi(callback) + self:turnOffWifi(callback) else - NetworkMgr:promptWifiOff(callback) + self:promptWifiOff(callback) end end @@ -274,6 +279,27 @@ function NetworkMgr:isOnline() return socket.dns.toip("dns.msftncsi.com") ~= nil end +-- Update our cached network status +function NetworkMgr:queryNetworkState() + self.is_wifi_on = self:isWifiOn() + self.is_connected = self.is_wifi_on and self:isConnected() +end + +-- These do not call the actual Device methods, but what we, NetworkMgr, think the state is based on our own behavior. +function NetworkMgr:getWifiState() + return self.is_wifi_on +end +function NetworkMgr:setWifiState(bool) + self.is_wifi_on = bool +end +function NetworkMgr:getConnectionState() + return self.is_connected +end +function NetworkMgr:setConnectionState(bool) + self.is_connected = bool +end + + function NetworkMgr:isNetworkInfoAvailable() if Device:isAndroid() then -- always available @@ -362,7 +388,7 @@ function NetworkMgr:getWifiMenuTable() return { text = _("Wi-Fi settings"), enabled_func = function() return true end, - callback = function() NetworkMgr:openSettings() end, + callback = function() self:openSettings() end, } else return self:getWifiToggleMenuTable() @@ -371,10 +397,11 @@ end function NetworkMgr:getWifiToggleMenuTable() local toggleCallback = function(touchmenu_instance, long_press) - local is_wifi_on = NetworkMgr:isWifiOn() - local is_connected = NetworkMgr:isConnected() - local fully_connected = is_wifi_on and is_connected + self:queryNetworkState() + local fully_connected = self.is_wifi_on and self.is_connected local complete_callback = function() + -- Check the connection status again + self:queryNetworkState() -- Notify TouchMenu to update item check state touchmenu_instance:updateItems() -- If Wi-Fi was on when the menu was shown, this means the tap meant to turn the Wi-Fi *off*, @@ -385,9 +412,9 @@ function NetworkMgr:getWifiToggleMenuTable() -- On hasWifiManager devices that play with kernel modules directly, -- double-check that the connection attempt was actually successful... if Device:isKobo() or Device:isCervantes() then - if NetworkMgr:isWifiOn() and NetworkMgr:isConnected() then + if self.is_wifi_on and self.is_connected then UIManager:broadcastEvent(Event:new("NetworkConnected")) - elseif NetworkMgr:isWifiOn() and not NetworkMgr:isConnected() then + elseif self.is_wifi_on and not self.is_connected then -- If we can't ping the gateway, despite a successful authentication w/ the AP, display a warning, -- because this means that Wi-Fi is technically still enabled (e.g., modules are loaded). -- We can't really enforce a turnOffWifi right now, because the user might want to try another AP or something. @@ -408,19 +435,19 @@ function NetworkMgr:getWifiToggleMenuTable() end end if fully_connected then - NetworkMgr:toggleWifiOff(complete_callback) - elseif is_wifi_on and not is_connected then + self:toggleWifiOff(complete_callback) + elseif self.is_wifi_on and not self.is_connected then -- ask whether user wants to connect or turn off wifi - NetworkMgr:promptWifi(complete_callback, long_press) + self:promptWifi(complete_callback, long_press) else - NetworkMgr:toggleWifiOn(complete_callback, long_press) + self:toggleWifiOn(complete_callback, long_press) end end return { text = _("Wi-Fi connection"), enabled_func = function() return Device:hasWifiToggle() end, - checked_func = function() return NetworkMgr:isWifiOn() end, + checked_func = function() return self:isWifiOn() end, callback = toggleCallback, hold_callback = function(touchmenu_instance) toggleCallback(touchmenu_instance, true) @@ -442,9 +469,9 @@ function NetworkMgr:getProxyMenuTable() checked_func = function() return proxy_enabled() end, callback = function() if not proxy_enabled() and proxy() then - NetworkMgr:setHTTPProxy(proxy()) + self:setHTTPProxy(proxy()) elseif proxy_enabled() then - NetworkMgr:setHTTPProxy(nil) + self:setHTTPProxy(nil) end if not proxy() then UIManager:show(InfoMessage:new{ @@ -458,7 +485,7 @@ function NetworkMgr:getProxyMenuTable() hint = proxy() or "", callback = function(input) if input ~= "" then - NetworkMgr:setHTTPProxy(input) + self:setHTTPProxy(input) end end, } @@ -644,7 +671,7 @@ function NetworkMgr:reconnectOrShowNetworkMenu(complete_callback) 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 = NetworkMgr:authenticateNetwork(network) + success, err_msg = self:authenticateNetwork(network) if success then ssid = network.ssid break @@ -654,7 +681,7 @@ function NetworkMgr:reconnectOrShowNetworkMenu(complete_callback) end if success then - NetworkMgr:obtainIP() + self:obtainIP() if complete_callback then complete_callback() end diff --git a/frontend/ui/network/networklistener.lua b/frontend/ui/network/networklistener.lua index de7d84208..f30813be2 100644 --- a/frontend/ui/network/networklistener.lua +++ b/frontend/ui/network/networklistener.lua @@ -197,10 +197,15 @@ function NetworkListener:_scheduleActivityCheck() end function NetworkListener:onNetworkConnected() + logger.dbg("NetworkListener: onNetworkConnected") if not (Device:hasWifiManager() and not Device:isEmulator()) then return end + -- This is for the sake of events that don't emanate from NetworkMgr itself... + NetworkMgr:setWifiState(true) + NetworkMgr:setConnectionState(true) + if not G_reader_settings:isTrue("auto_disable_wifi") then return end @@ -212,10 +217,14 @@ function NetworkListener:onNetworkConnected() end function NetworkListener:onNetworkDisconnected() + logger.dbg("NetworkListener: onNetworkDisconnected") if not (Device:hasWifiManager() and not Device:isEmulator()) then return end + NetworkMgr:setWifiState(false) + NetworkMgr:setConnectionState(false) + if not G_reader_settings:isTrue("auto_disable_wifi") then return end diff --git a/frontend/ui/uimanager.lua b/frontend/ui/uimanager.lua index bc3fec1c3..bb4f15628 100644 --- a/frontend/ui/uimanager.lua +++ b/frontend/ui/uimanager.lua @@ -61,51 +61,29 @@ function UIManager:init() self.poweroff_action = function() self._entered_poweroff_stage = true Device.orig_rotation_mode = Device.screen:getRotationMode() + self:broadcastEvent(Event:new("Close")) Screen:setRotationMode(Screen.ORIENTATION_PORTRAIT) local Screensaver = require("ui/screensaver") Screensaver:setup("poweroff", _("Powered off")) - if Device:hasEinkScreen() and Screensaver:modeIsImage() then - if Screensaver:withBackground() then - Screen:clear() - end - Screen:refreshFull() - end Screensaver:show() - if Device:needsScreenRefreshAfterResume() then - Screen:refreshFull() - end - UIManager:nextTick(function() + self:nextTick(function() Device:saveSettings() - if Device:isKobo() then - self._exit_code = 88 - end - self:broadcastEvent(Event:new("Close")) Device:powerOff() + self:quit(Device:isKobo() and 88) end) end self.reboot_action = function() self._entered_poweroff_stage = true Device.orig_rotation_mode = Device.screen:getRotationMode() + self:broadcastEvent(Event:new("Close")) Screen:setRotationMode(Screen.ORIENTATION_PORTRAIT) local Screensaver = require("ui/screensaver") Screensaver:setup("reboot", _("Rebooting…")) - if Device:hasEinkScreen() and Screensaver:modeIsImage() then - if Screensaver:withBackground() then - Screen:clear() - end - Screen:refreshFull() - end Screensaver:show() - if Device:needsScreenRefreshAfterResume() then - Screen:refreshFull() - end - UIManager:nextTick(function() + self:nextTick(function() Device:saveSettings() - if Device:isKobo() then - self._exit_code = 88 - end - self:broadcastEvent(Event:new("Close")) Device:reboot() + self:quit(Device:isKobo() and 88) end) end @@ -126,7 +104,7 @@ For more details about refreshtype, refreshregion & refreshdither see the descri If refreshtype is omitted, no refresh will be enqueued at this time. @param widget a @{ui.widget.widget|widget} object -@string refreshtype `"full"`, `"flashpartial"`, `"flashui"`, `"partial"`, `"ui"`, `"fast"` (optional) +@string refreshtype `"full"`, `"flashpartial"`, `"flashui"`, `"[partial]"`, `"[ui]"`, `"partial"`, `"ui"`, `"fast"`, `"a2"` (optional) @param refreshregion a rectangle @{ui.geometry.Geom|Geom} object (optional, requires refreshtype to be set) @int x horizontal screen offset (optional, `0` if omitted) @int y vertical screen offset (optional, `0` if omitted) @@ -135,7 +113,7 @@ If refreshtype is omitted, no refresh will be enqueued at this time. ]] function UIManager:show(widget, refreshtype, refreshregion, x, y, refreshdither) if not widget then - logger.dbg("widget not exist to be shown") + logger.dbg("attempted to show a nil widget") return end logger.dbg("show widget:", widget.id or widget.name or tostring(widget)) @@ -181,14 +159,14 @@ For more details about refreshtype, refreshregion & refreshdither see the descri If refreshtype is omitted, no extra refresh will be enqueued at this time, leaving only those from the uncovered widgets. @param widget a @{ui.widget.widget|widget} object -@string refreshtype `"full"`, `"flashpartial"`, `"flashui"`, `"partial"`, `"ui"`, `"fast"` (optional) +@string refreshtype `"full"`, `"flashpartial"`, `"flashui"`, `"[partial]"`, `"[ui]"`, `"partial"`, `"ui"`, `"fast"`, `"a2"` (optional) @param refreshregion a rectangle @{ui.geometry.Geom|Geom} object (optional, requires refreshtype to be set) @bool refreshdither `true` if the refresh requires dithering (optional, requires refreshtype to be set) @see setDirty ]] function UIManager:close(widget, refreshtype, refreshregion, refreshdither) if not widget then - logger.dbg("widget to be closed does not exist") + logger.dbg("attempted to close a nil widget") return end logger.dbg("close widget:", widget.name or widget.id or tostring(widget)) @@ -204,38 +182,39 @@ function UIManager:close(widget, refreshtype, refreshregion, refreshdither) local start_idx = 1 -- Then remove all references to that widget on stack and refresh. for i = #self._window_stack, 1, -1 do - if self._window_stack[i].widget == widget then - self._dirty[self._window_stack[i].widget] = nil + local w = self._window_stack[i].widget + if w == widget then + self._dirty[w] = nil table.remove(self._window_stack, i) dirty = true else -- If anything else on the stack not already hidden by (i.e., below) a fullscreen widget was dithered, honor the hint - if self._window_stack[i].widget.dithered and not is_covered then + if w.dithered and not is_covered then refreshdither = true - logger.dbg("Lower widget", self._window_stack[i].widget.name or self._window_stack[i].widget.id or tostring(self._window_stack[i].widget), "was dithered, honoring the dithering hint") + logger.dbg("Lower widget", w.name or w.id or tostring(w), "was dithered, honoring the dithering hint") end -- Remember the uppermost widget that covers the full screen, so we don't bother calling setDirty on hidden (i.e., lower) widgets in the following dirty loop. -- _repaint already does that later on to skip the actual paintTo calls, so this ensures we limit the refresh queue to stuff that will actually get painted. - if not is_covered and self._window_stack[i].widget.covers_fullscreen then + if not is_covered and w.covers_fullscreen then is_covered = true start_idx = i - logger.dbg("Lower widget", self._window_stack[i].widget.name or self._window_stack[i].widget.id or tostring(self._window_stack[i].widget), "covers the full screen") + logger.dbg("Lower widget", w.name or w.id or tostring(w), "covers the full screen") if i > 1 then logger.dbg("not refreshing", i-1, "covered widget(s)") end end -- Set double tap to how the topmost specifying widget wants it - if requested_disable_double_tap == nil and self._window_stack[i].widget.disable_double_tap ~= nil then - requested_disable_double_tap = self._window_stack[i].widget.disable_double_tap + if requested_disable_double_tap == nil and w.disable_double_tap ~= nil then + requested_disable_double_tap = w.disable_double_tap end end end if requested_disable_double_tap ~= nil then Input.disable_double_tap = requested_disable_double_tap end - if #self._window_stack > 0 then + if self._window_stack[1] then -- set tap interval override to what the topmost widget specifies (when it doesn't, nil restores the default) Input.tap_interval_override = self._window_stack[#self._window_stack].widget.tap_interval_override end @@ -281,7 +260,7 @@ function UIManager:schedule(sched_time, action, ...) table.insert(self._task_queue, p, { time = sched_time, action = action, - argc = select('#', ...), + argc = select("#", ...), args = {...}, }) self._task_queue_dirty = true @@ -336,16 +315,12 @@ necessary if the caller wants to unschedule action *before* it actually gets ins @see nextTick ]] function UIManager:tickAfterNext(action, ...) - -- Storing varargs is a bit iffy as we don't build LuaJIT w/ 5.2 compat, so we don't have access to table.pack... - -- c.f., http://lua-users.org/wiki/VarargTheSecondClassCitizen - local n = select('#', ...) - local va = {...} -- We need to keep a reference to this anonymous function, as it is *NOT* quite `action` yet, -- and the caller might want to unschedule it early... - local action_wrapper = function() - self:nextTick(action, unpack(va, 1, n)) + local action_wrapper = function(...) + self:nextTick(action, ...) end - self:nextTick(action_wrapper) + self:nextTick(action_wrapper, ...) return action_wrapper end @@ -416,14 +391,20 @@ Here's a quick rundown of what each refreshtype should be used for: Can be promoted to flashing after `FULL_REFRESH_COUNT` refreshes. Don't abuse to avoid spurious flashes. In practice, this means this should mostly always be limited to ReaderUI. +* `[partial]`: variant of partial that asks the driver not to merge this update with surrounding ones. + Equivalent to partial on platforms where this distinction is not implemented. * `ui`: medium fidelity refresh (e.g., mixed content). Should apply to most UI elements. When in doubt, use this. -* `fast`: low fidelity refresh (e.g., monochrome content). +* `[ui]`: variant of ui that asks the driver not to merge this update with surrounding ones. + Equivalent to ui on platforms where this distinction is not implemented. +* `fast`: low fidelity refresh (e.g., monochrome content (technically, from any to B&W)). Should apply to most highlighting effects achieved through inversion. Note that if your highlighted element contains text, you might want to keep the unhighlight refresh as `"ui"` instead, for crisper text. (Or optimize that refresh away entirely, if you can get away with it). +* `a2`: low fidelity refresh (e.g., monochrome content (technically, from B&W to B&W only)). + Should be limited to very specific use-cases (e.g., keyboard) * `flashui`: like `ui`, but flashing. Can be used when showing a UI element for the first time, or when closing one, to avoid ghosting. * `flashpartial`: like `partial`, but flashing (and not counting towards flashing promotions). @@ -501,7 +482,7 @@ UIManager:setDirty(self.widget, "partial", Geom:new{x=10,y=10,w=100,h=50}) UIManager:setDirty(self.widget, function() return "ui", self.someelement.dimen end) @param widget a window-level widget object, `"all"`, or `nil` -@param refreshtype `"full"`, `"flashpartial"`, `"flashui"`, `"partial"`, `"ui"`, `"fast"` (or a lambda, see description above) +@param refreshtype `"full"`, `"flashpartial"`, `"flashui"`, `"[partial]"`, `"[ui]"`, `"partial"`, `"ui"`, `"fast"`, `"a2"` (or a lambda, see description above) @param refreshregion a rectangle @{ui.geometry.Geom|Geom} object (optional, omitting it means the region will cover the full screen) @bool refreshdither `true` if widget requires dithering (optional) ]] @@ -509,10 +490,11 @@ function UIManager:setDirty(widget, refreshtype, refreshregion, refreshdither) if widget then if widget == "all" then -- special case: set all top-level widgets as being "dirty". - for i = 1, #self._window_stack do - self._dirty[self._window_stack[i].widget] = true + for _, window in ipairs(self._window_stack) do + local w = window.widget + self._dirty[w] = true -- If any of 'em were dithered, honor their dithering hint - if self._window_stack[i].widget.dithered then + if w.dithered then -- NOTE: That works when refreshtype is NOT a function, -- which is why _repaint does another pass of this check ;). logger.dbg("setDirty on all widgets: found a dithered widget, infecting the refresh queue") @@ -528,16 +510,17 @@ function UIManager:setDirty(widget, refreshtype, refreshregion, refreshdither) -- NOTE: We only ever check the dirty flag on top-level widgets, so only set it there! -- Enable verbose debug to catch misbehaving widgets via our post-guard. for i = #self._window_stack, 1, -1 do + local w = self._window_stack[i].widget if handle_alpha then - self._dirty[self._window_stack[i].widget] = true - logger.dbg("setDirty: Marking as dirty widget:", self._window_stack[i].widget.name or self._window_stack[i].widget.id or tostring(self._window_stack[i].widget), "because it's below translucent widget:", widget.name or widget.id or tostring(widget)) + self._dirty[w] = true + logger.dbg("setDirty: Marking as dirty widget:", w.name or w.id or tostring(w), "because it's below translucent widget:", widget.name or widget.id or tostring(widget)) -- Stop flagging widgets at the uppermost one that covers the full screen - if self._window_stack[i].widget.covers_fullscreen then + if w.covers_fullscreen then break end end - if self._window_stack[i].widget == widget then + if w == widget then self._dirty[widget] = true -- We've got a match, now check if it's translucent... @@ -592,6 +575,11 @@ function UIManager:setDirty(widget, refreshtype, refreshregion, refreshdither) end end end +--[[ +-- NOTE: While nice in theory, this is *extremely* verbose in practice, +-- because most widgets will call setDirty at least once during their initialization, +-- and that happens before they make it to the window stack... +-- Plus, setDirty(nil, ...) is a completely valid use-case with documented semantics... dbg:guard(UIManager, 'setDirty', nil, function(self, widget, refreshtype, refreshregion, refreshdither) @@ -609,6 +597,7 @@ dbg:guard(UIManager, 'setDirty', dbg:v("INFO: invalid widget for setDirty()", debug.traceback()) end end) +--]] --[[-- Clear the full repaint & refresh queues. @@ -684,14 +673,16 @@ end --- Get top widget (name if possible, ref otherwise). function UIManager:getTopWidget() - local top = self._window_stack[#self._window_stack] - if not top or not top.widget then + if not self._window_stack[1] then + -- No widgets in the stack, bye! return nil end - if top.widget.name then - return top.widget.name + + local widget = self._window_stack[#self._window_stack].widget + if widget.name then + return widget.name end - return top.widget + return widget end --[[-- @@ -710,19 +701,16 @@ function UIManager:getSecondTopmostWidget() -- Because everything is terrible, you can actually instantiate multiple VirtualKeyboards, -- and they'll stack at the top, so, loop until we get something that *isn't* VK... for i = #self._window_stack - 1, 1, -1 do - local sec = self._window_stack[i] - if not sec or not sec.widget then - return nil - end + local widget = self._window_stack[i].widget - if sec.widget.name then - if sec.widget.name ~= "VirtualKeyboard" then - return sec.widget.name + if widget.name then + if widget.name ~= "VirtualKeyboard" then + return widget.name end -- Meaning if name is set, and is set to VK => continue, as we want the *next* widget. -- I *really* miss the continue keyword, Lua :/. else - return sec.widget + return widget end end @@ -732,9 +720,10 @@ end --- Check if a widget is still in the window stack, or is a subwidget of a widget still in the window stack. function UIManager:isSubwidgetShown(widget, max_depth) for i = #self._window_stack, 1, -1 do - local matched, depth = util.arrayReferences(self._window_stack[i].widget, widget, max_depth) + local w = self._window_stack[i].widget + local matched, depth = util.arrayReferences(w, widget, max_depth) if matched then - return matched, depth, self._window_stack[i].widget + return matched, depth, w end end return false @@ -750,19 +739,11 @@ function UIManager:isWidgetShown(widget) return false end ---[[-- -Returns the region of the previous refresh. - -@return a rectangle @{ui.geometry.Geom|Geom} object -]] -function UIManager:getPreviousRefreshRegion() - return self._last_refresh_region -end - --- Signals to quit. -function UIManager:quit() +function UIManager:quit(exit_code) if not self._running then return end - logger.info("quitting uimanager") + logger.info("quitting uimanager with exit code:", exit_code or 0) + self._exit_code = exit_code self._task_queue_dirty = false self._running = false self._run_forever = nil @@ -791,25 +772,29 @@ which itself will take care of propagating an event to its members. @param event an @{ui.event.Event|Event} object ]] function UIManager:sendEvent(event) - if #self._window_stack == 0 then return end + if not self._window_stack[1] then + -- No widgets in the stack! + return + end -- The top widget gets to be the first to get the event - local top_widget = self._window_stack[#self._window_stack] + local top_widget = self._window_stack[#self._window_stack].widget - -- A toast widget gets closed by any event, and - -- lets the event be handled by a lower widget - -- (Notification is our single widget with toast=true) - while top_widget.widget.toast do -- close them all - self:close(top_widget.widget) - if #self._window_stack == 0 then return end - top_widget = self._window_stack[#self._window_stack] + -- A toast widget gets closed by any event, and lets the event be handled by a lower widget. + -- (Notification is our only widget flagged as such). + while top_widget.toast do -- close them all + self:close(top_widget) + if not self._window_stack[1] then + return + end + top_widget = self._window_stack[#self._window_stack].widget end - if top_widget.widget:handleEvent(event) then + if top_widget:handleEvent(event) then return end - if top_widget.widget.active_widgets then - for _, active_widget in ipairs(top_widget.widget.active_widgets) do + if top_widget.active_widgets then + for _, active_widget in ipairs(top_widget.active_widgets) do if active_widget:handleEvent(event) then return end end end @@ -824,20 +809,24 @@ function UIManager:sendEvent(event) local checked_widgets = {top_widget} local i = #self._window_stack while i > 0 do - local widget = self._window_stack[i] - if checked_widgets[widget] == nil then + local widget = self._window_stack[i].widget + if not checked_widgets[widget] then checked_widgets[widget] = true -- Widget's active widgets have precedence to handle this event - -- NOTE: While FileManager only has a single (screenshotter), ReaderUI has a few active_widgets. - if widget.widget.active_widgets then - for _, active_widget in ipairs(widget.widget.active_widgets) do - if active_widget:handleEvent(event) then return end + -- NOTE: ReaderUI & FileManager have their registered modules referenced as such. + if widget.active_widgets then + for _, active_widget in ipairs(widget.active_widgets) do + if active_widget:handleEvent(event) then + return + end end end - if widget.widget.is_always_active then + if widget.is_always_active then -- Widget itself is flagged always active, let it handle the event - -- NOTE: is_always_active widgets currently are widgets that want to show a VirtualKeyboard or listen to Dispatcher events - if widget.widget:handleEvent(event) then return end + -- NOTE: is_always_active widgets are currently widgets that want to show a VirtualKeyboard or listen to Dispatcher events + if widget:handleEvent(event) then + return + end end i = #self._window_stack else @@ -857,10 +846,10 @@ function UIManager:broadcastEvent(event) local checked_widgets = {} local i = #self._window_stack while i > 0 do - local widget = self._window_stack[i] - if checked_widgets[widget] == nil then + local widget = self._window_stack[i].widget + if not checked_widgets[widget] then checked_widgets[widget] = true - widget.widget:handleEvent(event) + widget:handleEvent(event) i = #self._window_stack else i = i - 1 @@ -944,17 +933,20 @@ function UIManager:getElapsedTimeSinceBoot() end -- precedence of refresh modes: -local refresh_modes = { fast = 1, ui = 2, partial = 3, flashui = 4, flashpartial = 5, full = 6 } --- NOTE: We might want to introduce a "force_fast" that points to fast, but has the highest priority, +local refresh_modes = { a2 = 1, fast = 2, ui = 3, partial = 4, ["[ui]"] = 5, ["[partial]"] = 6, flashui = 7, flashpartial = 8, full = 9 } +-- NOTE: We might want to introduce a "force_a2" that points to fast, but has the highest priority, -- for the few cases where we might *really* want to enforce fast (for stuff like panning or skimming?). -- refresh methods in framebuffer implementation local refresh_methods = { - fast = "refreshFast", - ui = "refreshUI", - partial = "refreshPartial", - flashui = "refreshFlashUI", - flashpartial = "refreshFlashPartial", - full = "refreshFull", + a2 = Screen.refreshA2, + fast = Screen.refreshFast, + ui = Screen.refreshUI, + partial = Screen.refreshPartial, + ["[ui]"] = Screen.refreshNoMergeUI, + ["[partial]"] = Screen.refreshNoMergePartial, + flashui = Screen.refreshFlashUI, + flashpartial = Screen.refreshFlashPartial, + full = Screen.refreshFull, } --[[ @@ -992,7 +984,7 @@ Widgets call this in their `paintTo()` method in order to notify UIManager that a certain part of the screen is to be refreshed. @string mode - refresh mode (`"full"`, `"flashpartial"`, `"flashui"`, `"partial"`, `"ui"`, `"fast"`) + refresh mode (`"full"`, `"flashpartial"`, `"flashui"`, `"[partial]"`, `"[ui]"`, `"partial"`, `"ui"`, `"fast"`, `"a2"`) @param region A rectangle @{ui.geometry.Geom|Geom} object that specifies the region to be updated. Optional, update will affect whole screen if not specified. @@ -1071,16 +1063,16 @@ function UIManager:_refresh(mode, region, dither) -- (e.g., multiple setDirty calls queued when showing/closing a widget because of update mechanisms), -- as well as a few actually effective merges -- (e.g., the disappearance of a selection HL with the following menu update). - for i = 1, #self._refresh_stack do + for i, refresh in ipairs(self._refresh_stack) do -- Check for collision with refreshes that are already enqueued -- NOTE: intersect *means* intersect: we won't merge edge-to-edge regions (but the EPDC probably will). - if region:intersectWith(self._refresh_stack[i].region) then + if region:intersectWith(refresh.region) then -- combine both refreshes' regions - local combined = region:combine(self._refresh_stack[i].region) + local combined = region:combine(refresh.region) -- update the mode, if needed - mode = update_mode(mode, self._refresh_stack[i].mode) + mode = update_mode(mode, refresh.mode) -- dithering hints are viral, one is enough to infect the whole queue - dither = update_dither(dither, self._refresh_stack[i].dither) + dither = update_dither(dither, refresh.dither) -- remove colliding refresh table.remove(self._refresh_stack, i) -- and try again with combined data @@ -1134,26 +1126,27 @@ function UIManager:_repaint() --]] for i = start_idx, #self._window_stack do - local widget = self._window_stack[i] + local window = self._window_stack[i] + local widget = window.widget -- paint if current widget or any widget underneath is dirty - if dirty or self._dirty[widget.widget] then + if dirty or self._dirty[widget] then -- pass hint to widget that we got when setting widget dirty -- the widget can use this to decide which parts should be refreshed - logger.dbg("painting widget:", widget.widget.name or widget.widget.id or tostring(widget)) + logger.dbg("painting widget:", widget.name or widget.id or tostring(widget)) Screen:beforePaint() -- NOTE: Nothing actually seems to use the final argument? -- Could be used by widgets to know whether they're being repainted because they're actually dirty (it's true), -- or because something below them was (it's nil). - widget.widget:paintTo(Screen.bb, widget.x, widget.y, self._dirty[widget.widget]) + widget:paintTo(Screen.bb, window.x, window.y, self._dirty[widget]) -- and remove from list after painting - self._dirty[widget.widget] = nil + self._dirty[widget] = nil -- trigger a repaint for every widget above us, too dirty = true -- if any of 'em were dithered, we'll want to dither the final refresh - if widget.widget.dithered then + if widget.dithered then logger.dbg("_repaint: it was dithered, infecting the refresh queue") dithered = true end @@ -1165,13 +1158,15 @@ function UIManager:_repaint() local refreshtype, region, dither = refreshfunc() -- honor dithering hints from *anywhere* in the dirty stack dither = update_dither(dither, dithered) - if refreshtype then self:_refresh(refreshtype, region, dither) end + if refreshtype then + self:_refresh(refreshtype, region, dither) + end end self._refresh_func_stack = {} -- we should have at least one refresh if we did repaint. If we don't, we -- add one now and log a warning if we are debugging - if dirty and #self._refresh_stack == 0 then + if dirty and not self._refresh_stack[1] then logger.dbg("no refresh got enqueued. Will do a partial full screen refresh, which might be inefficient") self:_refresh("partial") end @@ -1185,9 +1180,12 @@ function UIManager:_repaint() refresh.dither = nil end dbg:v("triggering refresh", refresh) + + --[[ -- Remember the refresh region - self._last_refresh_region = refresh.region - Screen[refresh_methods[refresh.mode]](Screen, + self._last_refresh_region = refresh.region:copy() + --]] + refresh_methods[refresh.mode](Screen, refresh.region.x, refresh.region.y, refresh.region.w, refresh.region.h, refresh.dither) @@ -1313,7 +1311,7 @@ function UIManager:handleInputEvent(input_event) if handler then handler(input_event) else - self.event_handlers["__default__"](input_event) + self.event_handlers.__default__(input_event) end end @@ -1345,7 +1343,7 @@ function UIManager:handleInput() --dbg("---------------------------------------------------") -- stop when we have no window to show - if #self._window_stack == 0 and not self._run_forever then + if not self._window_stack[1] and not self._run_forever then logger.info("no dialog left to show") self:quit() return nil @@ -1365,7 +1363,7 @@ function UIManager:handleInput() local wait_us = self.INPUT_TIMEOUT -- If we have any ZMQs registered, ZMQ_TIMEOUT is another upper bound. - if #self._zeromqs > 0 then + if self._zeromqs[1] then wait_us = math.min(wait_us or math.huge, self.ZMQ_TIMEOUT) end @@ -1419,7 +1417,6 @@ function UIManager:handleInput() xpcall(function() self:handleInput() end, function(err) io.stderr:write(err .. "\n") io.stderr:write(debug.traceback() .. "\n") - io.stderr:flush() self.looper:close() os.exit(1, true) end) @@ -1480,28 +1477,28 @@ This function usually puts the device into suspension. ]] function UIManager:suspend() -- Should always exist, as defined in `generic/device` or overwritten with `setEventHandlers` - if self.event_handlers["Suspend"] then + if self.event_handlers.Suspend then -- Give the other event handlers a chance to be executed. -- `Suspend` and `Resume` events will be sent by the handler - UIManager:nextTick(self.event_handlers["Suspend"]) + UIManager:nextTick(self.event_handlers.Suspend) end end function UIManager:reboot() -- Should always exist, as defined in `generic/device` or overwritten with `setEventHandlers` - if self.event_handlers["Reboot"] then + if self.event_handlers.Reboot then -- Give the other event handlers a chance to be executed. -- 'Reboot' event will be sent by the handler - UIManager:nextTick(self.event_handlers["Reboot"]) + UIManager:nextTick(self.event_handlers.Reboot) end end function UIManager:powerOff() -- Should always exist, as defined in `generic/device` or overwritten with `setEventHandlers` - if self.event_handlers["PowerOff"] then + if self.event_handlers.PowerOff then -- Give the other event handlers a chance to be executed. -- 'PowerOff' event will be sent by the handler - UIManager:nextTick(self.event_handlers["PowerOff"]) + UIManager:nextTick(self.event_handlers.PowerOff) end end @@ -1557,15 +1554,13 @@ end --- Sanely restart KOReader (on supported platforms). function UIManager:restartKOReader() - self:quit() -- This is just a magic number to indicate the restart request for shell scripts. - self._exit_code = 85 + self:quit(85) end --- Sanely abort KOReader (e.g., exit sanely, but with a non-zero return code). function UIManager:abort() - self:quit() - self._exit_code = 1 + self:quit(1) end UIManager:init() diff --git a/frontend/ui/widget/virtualkeyboard.lua b/frontend/ui/widget/virtualkeyboard.lua index 13f5eefab..049ac1395 100644 --- a/frontend/ui/widget/virtualkeyboard.lua +++ b/frontend/ui/widget/virtualkeyboard.lua @@ -319,24 +319,28 @@ function VirtualKey:genKeyboardLayoutKeyChars() end -- NOTE: We currently don't ever set want_flash to true (c.f., our invert method). -function VirtualKey:update_keyboard(want_flash, want_fast) - -- NOTE: We mainly use "fast" when inverted & "ui" when not, with a cherry on top: - -- we flash the *full* keyboard instead when we release a hold. +function VirtualKey:update_keyboard(want_flash, want_a2) + -- NOTE: We use "a2" for the highlights. + -- We flash the *full* keyboard when we release a hold. if want_flash then UIManager:setDirty(self.keyboard, function() return "flashui", self.keyboard[1][1].dimen end) else local refresh_type = "ui" - if want_fast then - refresh_type = "fast" + if want_a2 then + refresh_type = "a2" end -- Only repaint the key itself, not the full board... UIManager:widgetRepaint(self[1], self[1].dimen.x, self[1].dimen.y) - UIManager:setDirty(nil, function() - logger.dbg("update key region", self[1].dimen) - return refresh_type, self[1].dimen - end) + logger.dbg("update key", self.key) + UIManager:setDirty(nil, refresh_type, self[1].dimen) + + -- NOTE: On MTK, we'd have to forcibly stall a bit for the highlights to actually show. + --[[ + UIManager:forceRePaint() + UIManager:yieldToEPDC(3000) + --]] end end @@ -449,7 +453,7 @@ function VirtualKey:invert(invert, hold) else self[1].inner_bordersize = 0 end - self:update_keyboard(hold, false) + self:update_keyboard(hold, true) end VirtualKeyPopup = FocusManager:new{ diff --git a/plugins/autosuspend.koplugin/main.lua b/plugins/autosuspend.koplugin/main.lua index 29554b749..2774de1b5 100644 --- a/plugins/autosuspend.koplugin/main.lua +++ b/plugins/autosuspend.koplugin/main.lua @@ -220,12 +220,16 @@ function AutoSuspend:_schedule_standby() -- When we're in a state where entering suspend is undesirable, we simply postpone the check by the full delay. local standby_delay_seconds - if NetworkMgr:isWifiOn() then + -- NOTE: As this may fire repeatedly, we don't want to poke the actual Device implementation every few seconds, + -- instead, we rely on NetworkMgr's last known status. (i.e., this *should* match NetworkMgr:isWifiOn). + if NetworkMgr:getWifiState() then -- Don't enter standby if wifi is on, as this will break in fun and interesting ways (from Wi-Fi issues to kernel deadlocks). --logger.dbg("AutoSuspend: WiFi is on, delaying standby") standby_delay_seconds = self.auto_standby_timeout_seconds elseif Device.powerd:isCharging() and not Device:canPowerSaveWhileCharging() then - -- Don't enter standby when charging on devices where charging prevents entering low power states. + -- Don't enter standby when charging on devices where charging *may* prevent entering low power states. + -- (*May*, because depending on the USB controller, it might depend on what it's plugged to, and how it's setup: + -- e.g., generally, on those devices, USBNet being enabled is guaranteed to prevent PM). -- NOTE: Minor simplification here, we currently don't do the hasAuxBattery dance like in _schedule, -- because all the hasAuxBattery devices can currently enter PM states while charging ;). --logger.dbg("AutoSuspend: charging, delaying standby") diff --git a/reader.lua b/reader.lua index 588d4b32f..8350e17e7 100755 --- a/reader.lua +++ b/reader.lua @@ -1,5 +1,15 @@ #!./luajit -io.stdout:write([[ + +-- Enforce line-buffering for stdout (this is the default if it points to a tty, but we redirect to a file on most platforms). +local ffi = require("ffi") +local C = ffi.C +ffi.cdef[[ +extern struct _IO_FILE *stdout; +void setlinebuf(struct _IO_FILE *); +]] +C.setlinebuf(C.stdout) + +io.write([[ --------------------------------------------- launching... _ _____ ____ _ @@ -11,7 +21,6 @@ io.stdout:write([[ It's a scroll... It's a codex... It's KOReader! [*] Current time: ]], os.date("%x-%X"), "\n") -io.stdout:flush() -- Set up Lua and ffi search paths require("setupkoenv") @@ -21,8 +30,7 @@ local userpatch = require("userpatch") userpatch.applyPatches(userpatch.early_once) userpatch.applyPatches(userpatch.early) -io.stdout:write(" [*] Version: ", require("version"):getCurrentRevision(), "\n\n") -io.stdout:flush() +io.write(" [*] Version: ", require("version"):getCurrentRevision(), "\n\n") -- Load default settings G_defaults = require("luadefaults"):open() diff --git a/spec/unit/commonrequire.lua b/spec/unit/commonrequire.lua index ae92a3ae4..5757994ab 100644 --- a/spec/unit/commonrequire.lua +++ b/spec/unit/commonrequire.lua @@ -34,7 +34,6 @@ if not busted_ok then end end -require "defaults" package.path = "?.lua;common/?.lua;rocks/share/lua/5.1/?.lua;frontend/?.lua;" .. package.path package.cpath = "?.so;common/?.so;/usr/lib/lua/?.so;rocks/lib/lua/5.1/?.so;" .. package.cpath diff --git a/spec/unit/device_spec.lua b/spec/unit/device_spec.lua index f94dec70f..febbae462 100644 --- a/spec/unit/device_spec.lua +++ b/spec/unit/device_spec.lua @@ -299,8 +299,8 @@ describe("device module", function() ReaderUI:doShowReader(sample_pdf) local readerui = ReaderUI._getRunningInstance() stub(readerui, "onFlushSettings") - UIManager.event_handlers["PowerPress"]() - UIManager.event_handlers["PowerRelease"]() + UIManager.event_handlers.PowerPress() + UIManager.event_handlers.PowerRelease() assert.stub(readerui.onFlushSettings).was_called() Device.suspend:revert() @@ -343,8 +343,8 @@ describe("device module", function() ReaderUI:doShowReader(sample_pdf) local readerui = ReaderUI._getRunningInstance() stub(readerui, "onFlushSettings") - UIManager.event_handlers["PowerPress"]() - UIManager.event_handlers["PowerRelease"]() + UIManager.event_handlers.PowerPress() + UIManager.event_handlers.PowerRelease() assert.stub(readerui.onFlushSettings).was_called() Device.suspend:revert() @@ -374,8 +374,8 @@ describe("device module", function() ReaderUI:doShowReader(sample_pdf) local readerui = ReaderUI._getRunningInstance() stub(readerui, "onFlushSettings") - UIManager.event_handlers["PowerPress"]() - UIManager.event_handlers["PowerRelease"]() + UIManager.event_handlers.PowerPress() + UIManager.event_handlers.PowerRelease() assert.stub(readerui.onFlushSettings).was_called() Device.suspend:revert() @@ -424,8 +424,8 @@ describe("device module", function() ReaderUI:doShowReader(sample_pdf) local readerui = ReaderUI._getRunningInstance() stub(readerui, "onFlushSettings") - UIManager.event_handlers["PowerPress"]() - UIManager.event_handlers["PowerRelease"]() + UIManager.event_handlers.PowerPress() + UIManager.event_handlers.PowerRelease() assert.stub(readerui.onFlushSettings).was_called() Device.suspend:revert()