Assorted bag'o tweaks & fixes (#9569)

* UIManager: Support more specialized update modes for corner-cases:
  * A2, which we'll use for the VirtualKeyboards keys (they'd... inadvertently switched to UI with the highlight refactor).
  * NO_MERGE variants of ui & partial (for sunxi). Use `[ui]` in ReaderHighlight's popup, because of a Sage kernel bug that could otherwise make it translucent, sometimes completely so (*sigh*).
* UIManager: Assorted code cleanups & simplifications.
* Logger & dbg: Unify logging style, and code cleanups.
* SDL: Unbreak suspend/resume outside of the emulator (fix #9567).
* NetworkMgr: Cache the network status, and allow it to be queried. (Used by AutoSuspend to avoid repeatedly poking the system when computing the standby schedule delay).
* OneTimeMigration: Don't forget about `NETWORK_PROXY` & `STARDICT_DATA_DIR` when migrating `defaults.persistent.lua` (fix #9573)
* WakeupMgr: Workaround an apparent limitation of the RTC found on i.MX5 Kobo devices, where setting a wakealarm further than UINT16_MAX seconds in the future would apparently overflow and wraparound... (fix #8039, many thanks to @yfede for the extensive deep-dive and for actually accurately pinpointing the issue!).
* Kobo: Handle standby transitions at full CPU clock speeds, in order to limit the latency hit.
* UIManager: Properly quit on reboot & exit. This ensures our exit code is preserved, as we exit on our own terms (instead of being killed by the init system). This is important on platforms where exit codes are semantically meaningful (e.g., Kobo).
* UIManager: Speaking of reboot & exit, make sure the Screensaver shows in all circumstances (e.g., autoshutdown, re: #9542)), and that there aren't any extraneous refreshes triggered. (Additionally, fix a minor regression since #9448 about tracking this very transient state on Kobo & Cervantes).
* Kindle: ID the upcoming Scribe.
* Bump base (https://github.com/koreader/koreader-base/pull/1524)
reviewable/pr9582/r1
NiLuJe 2 years ago committed by GitHub
parent 5d9f036331
commit 9bf19d1bb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1 +1 @@
Subproject commit 9e4e7e899e06853305d0a2e4b1f103d2a0840068
Subproject commit 99143a2a11f40fb9ea635bfe876ec3dd8ae7b2d4

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

@ -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

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

@ -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")

@ -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

@ -155,7 +155,7 @@ function BasePowerD:unchecked_read_int_file(file)
fd:close()
return int
else
return
return nil
end
end

@ -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",

@ -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

@ -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

@ -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

@ -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

@ -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

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

@ -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

@ -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.

@ -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"

@ -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

@ -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

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

@ -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

@ -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

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

@ -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{

@ -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")

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

@ -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

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

Loading…
Cancel
Save