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()