The great Input/GestureDetector/TimeVal spring cleanup (a.k.a., a saner main loop) (#7415)

* ReaderDictionary: Port delay computations to TimeVal
* ReaderHighlight: Port delay computations to TimeVal
* ReaderView: Port delay computations to TimeVal
* Android: Reset gesture detection state on APP_CMD_TERM_WINDOW.
  This prevents potentially being stuck in bogus gesture states when switching apps.
* GestureDetector:
  * Port delay computations to TimeVal
  * Fixed delay computations to handle time warps (large and negative deltas).
  * Simplified timed callback handling to invalidate timers much earlier, preventing accumulating useless timers that no longer have any chance of ever detecting a gesture.
  * Fixed state clearing to handle the actual effective slots, instead of hard-coding slot 0 & slot 1.
  * Simplified timed callback handling in general, and added support for a timerfd backend for better performance and accuracy.
  * The improved timed callback handling allows us to detect and honor (as much as possible) the three possible clock sources usable by Linux evdev events.
    The only case where synthetic timestamps are used (and that only to handle timed callbacks) is limited to non-timerfd platforms where input events use
    a clock source that is *NOT* MONOTONIC.
    AFAICT, that's pretty much... PocketBook, and that's it?
* Input:
  * Use the <linux/input.h> FFI module instead of re-declaring every constant
  * Fixed (verbose) debug logging of input events to actually translate said constants properly.
  * Completely reset gesture detection state on suspend. This should prevent bogus gesture detection on resume.
  * Refactored the waitEvent loop to make it easier to comprehend (hopefully) and much more efficient.
    Of specific note, it no longer does a crazy select spam every 100µs, instead computing and relying on sane timeouts,
    as afforded by switching the UI event/input loop to the MONOTONIC time base, and the refactored timed callbacks in GestureDetector.
* reMarkable: Stopped enforcing synthetic timestamps on input events, as it should no longer be necessary.
* TimeVal:
  * Refactored and simplified, especially as far as metamethods are concerned (based on <bsd/sys/time.h>).
  * Added a host of new methods to query the various POSIX clock sources, and made :now default to MONOTONIC.
  * Removed the debug guard in __sub, as time going backwards can be a perfectly normal occurrence.
  * New methods:
    * Clock sources: :realtime, :monotonic, :monotonic_coarse, :realtime_coarse, :boottime
    * Utility: :tonumber, :tousecs, :tomsecs, :fromnumber, :isPositive, :isZero
* UIManager:
  * Ported event loop & scheduling to TimeVal, and switched to the MONOTONIC time base.
    This ensures reliable and consistent scheduling, as time is ensured never to go backwards.
  * Added a :getTime() method, that returns a cached TimeVal:now(), updated at the top of every UI frame.
    It's used throughout the codebase to cadge a syscall in circumstances where we are guaranteed that a syscall would return a mostly identical value,
    because very few time has passed.
    The only code left that does live syscalls does it because it's actually necessary for accuracy,
    and the only code left that does that in a REALTIME time base is code that *actually* deals with calendar time (e.g., Statistics).
* DictQuickLookup: Port delay computations to TimeVal
* FootNoteWidget: Port delay computations to TimeVal
* HTMLBoxWidget: Port delay computations to TimeVal
* Notification: Port delay computations to TimeVal
* TextBoxWidget: Port delay computations to TimeVal
* AutoSuspend: Port to TimeVal
* AutoTurn:
  * Fix it so that settings are actually honored.
  * Port to TimeVal
* BackgroundRunner: Port to TimeVal
* Calibre: Port benchmarking code to TimeVal
* BookInfoManager: Removed unnecessary yield in the metadata extraction subprocess now that subprocesses get scheduled properly.

* All in all, these changes reduced the CPU cost of a single tap by a factor of ten (!), and got rid of an insane amount of weird poll/wakeup cycles that must have been hell on CPU schedulers and batteries..
pull/7465/head
NiLuJe 3 years ago committed by GitHub
parent 7a925a3caf
commit 6d53f83286
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1 +1 @@
Subproject commit fd30a65587c5bc7d5830e6a40fe10aa147332945
Subproject commit 3a754c72885c6538cb0418071b4546dbafee8724

@ -13,6 +13,7 @@ local LuaData = require("luadata")
local MultiConfirmBox = require("ui/widget/multiconfirmbox")
local NetworkMgr = require("ui/network/manager")
local SortWidget = require("ui/widget/sortwidget")
local TimeVal = require("ui/timeval")
local Trapper = require("ui/trapper")
local UIManager = require("ui/uimanager")
local ffiUtil = require("ffi/util")
@ -109,7 +110,7 @@ function ReaderDictionary:init()
-- Allow quick interruption or dismiss of search result window
-- with tap if done before this delay. After this delay, the
-- result window is shown and dismiss prevented for a few 100ms
self.quick_dismiss_before_delay = 3
self.quick_dismiss_before_delay = TimeVal:new{ sec = 3 }
-- Gather info about available dictionaries
if not available_ifos then
@ -846,9 +847,9 @@ function ReaderDictionary:stardictLookup(word, dict_names, fuzzy_search, box, li
self:showLookupInfo(word, self.lookup_msg_delay)
self._lookup_start_ts = ffiUtil.getTimestamp()
self._lookup_start_tv = UIManager:getTime()
local results = self:startSdcv(word, dict_names, fuzzy_search)
if results and results.lookup_cancelled and ffiUtil.getDuration(self._lookup_start_ts) <= self.quick_dismiss_before_delay then
if results and results.lookup_cancelled and TimeVal:now() - self._lookup_start_tv <= self.quick_dismiss_before_delay then
-- If interrupted quickly just after launch, don't display anything
-- (this might help avoiding refreshes and the need to dismiss
-- after accidental long-press when holding a device).
@ -907,7 +908,7 @@ function ReaderDictionary:showDict(word, results, box, link)
self:dismissLookupInfo()
if results and results[1] then
UIManager:show(self.dict_window)
if not results.lookup_cancelled and self._lookup_start_ts and ffiUtil.getDuration(self._lookup_start_ts) > self.quick_dismiss_before_delay then
if not results.lookup_cancelled and self._lookup_start_tv and TimeVal:now() - self._lookup_start_tv > self.quick_dismiss_before_delay then
-- If the search took more than a few seconds to be done, discard
-- queued and coming up events to avoid a voluntary dismissal
-- (because the user felt the result would not come) to kill the

@ -338,14 +338,14 @@ end
-- to ensure current highlight has not already been cleared, and that we
-- are not going to clear a new highlight
function ReaderHighlight:getClearId()
self.clear_id = TimeVal.now() -- can act as a unique id
self.clear_id = UIManager:getTime() -- can act as a unique id
return self.clear_id
end
function ReaderHighlight:clear(clear_id)
if clear_id then -- should be provided by delayed call to clear()
if clear_id ~= self.clear_id then
-- if clear_id is no more valid, highlight has already been
-- if clear_id is no longer valid, highlight has already been
-- cleared since this clear_id was given
return
end
@ -681,7 +681,7 @@ function ReaderHighlight:_resetHoldTimer(clear)
if clear then
self.hold_last_tv = nil
else
self.hold_last_tv = TimeVal.now()
self.hold_last_tv = UIManager:getTime()
end
end
@ -1124,9 +1124,8 @@ end
function ReaderHighlight:onHoldRelease()
local long_final_hold = false
if self.hold_last_tv then
local hold_duration = TimeVal.now() - self.hold_last_tv
hold_duration = hold_duration.sec + hold_duration.usec/1000000
if hold_duration > 3.0 then
local hold_duration = UIManager:getTime() - self.hold_last_tv
if hold_duration > TimeVal:new{ sec = 3 } then
-- We stayed 3 seconds before release without updating selection
long_final_hold = true
end

@ -13,6 +13,7 @@ local OverlapGroup = require("ui/widget/overlapgroup")
local ReaderDogear = require("apps/reader/modules/readerdogear")
local ReaderFlipping = require("apps/reader/modules/readerflipping")
local ReaderFooter = require("apps/reader/modules/readerfooter")
local TimeVal = require("ui/timeval")
local UIManager = require("ui/uimanager")
local dbg = require("dbg")
local logger = require("logger")
@ -71,7 +72,7 @@ local ReaderView = OverlapGroup:extend{
-- in flipping state
flipping_visible = false,
-- to ensure periodic flush of settings
settings_last_save_ts = nil,
settings_last_save_tv = nil,
}
function ReaderView:init()
@ -954,17 +955,17 @@ function ReaderView:onCloseDocument()
end
function ReaderView:onReaderReady()
self.settings_last_save_ts = os.time()
self.settings_last_save_tv = UIManager:getTime()
end
function ReaderView:onResume()
-- As settings were saved on suspend, reset this on resume,
-- as there's no need for a possibly immediate save.
self.settings_last_save_ts = os.time()
self.settings_last_save_tv = UIManager:getTime()
end
function ReaderView:checkAutoSaveSettings()
if not self.settings_last_save_ts then -- reader not yet ready
if not self.settings_last_save_tv then -- reader not yet ready
return
end
if G_reader_settings:nilOrFalse("auto_save_settings_interval_minutes") then
@ -973,9 +974,10 @@ function ReaderView:checkAutoSaveSettings()
end
local interval = G_reader_settings:readSetting("auto_save_settings_interval_minutes")
local now_ts = os.time()
if now_ts - self.settings_last_save_ts >= interval*60 then
self.settings_last_save_ts = now_ts
interval = TimeVal:new{ sec = interval*60 }
local now_tv = UIManager:getTime()
if now_tv - self.settings_last_save_tv >= interval then
self.settings_last_save_tv = now_tv
-- I/O, delay until after the pageturn
UIManager:tickAfterNext(function()
self.ui:saveSettings()

@ -143,6 +143,8 @@ function Device:init()
or ev.code == C.APP_CMD_INIT_WINDOW
or ev.code == C.APP_CMD_WINDOW_REDRAW_NEEDED then
this.device.screen:_updateWindow()
elseif ev.code == C.APP_CMD_TERM_WINDOW then
this.device.input:resetState()
elseif ev.code == C.APP_CMD_CONFIG_CHANGED then
-- orientation and size changes
if android.screen.width ~= android.getScreenWidth()

@ -47,6 +47,11 @@ local TimeVal = require("ui/timeval")
local logger = require("logger")
local util = require("util")
-- We're going to need some clockid_t constants
local ffi = require("ffi")
local C = ffi.C
require("ffi/posix_h")
-- default values (all the time parameters are in microseconds)
local TAP_INTERVAL = 0 * 1000
local DOUBLE_TAP_INTERVAL = 300 * 1000
@ -56,11 +61,17 @@ local PAN_DELAYED_INTERVAL = 500 * 1000
local SWIPE_INTERVAL = 900 * 1000
-- current values
local ges_tap_interval = G_reader_settings:readSetting("ges_tap_interval") or TAP_INTERVAL
ges_tap_interval = TimeVal:new{ usec = ges_tap_interval }
local ges_double_tap_interval = G_reader_settings:readSetting("ges_double_tap_interval") or DOUBLE_TAP_INTERVAL
ges_double_tap_interval = TimeVal:new{ usec = ges_double_tap_interval }
local ges_two_finger_tap_duration = G_reader_settings:readSetting("ges_two_finger_tap_duration") or TWO_FINGER_TAP_DURATION
ges_two_finger_tap_duration = TimeVal:new{ usec = ges_two_finger_tap_duration }
local ges_hold_interval = G_reader_settings:readSetting("ges_hold_interval") or HOLD_INTERVAL
ges_hold_interval = TimeVal:new{ usec = ges_hold_interval }
local ges_pan_delayed_interval = G_reader_settings:readSetting("ges_pan_delayed_interval") or PAN_DELAYED_INTERVAL
ges_pan_delayed_interval = TimeVal:new{ usec = ges_pan_delayed_interval }
local ges_swipe_interval = G_reader_settings:readSetting("ges_swipe_interval") or SWIPE_INTERVAL
ges_swipe_interval = TimeVal:new{ usec = ges_swipe_interval }
local GestureDetector = {
-- must be initialized with the Input singleton class
@ -85,7 +96,7 @@ local GestureDetector = {
},
-- states are stored in separated slots
states = {},
hold_timer_id = {},
pending_hold_timer = {},
track_ids = {},
tev_stacks = {},
-- latest feeded touch event in each slots
@ -97,6 +108,8 @@ local GestureDetector = {
detectings = {},
-- for single/double tap
last_taps = {},
-- for timestamp clocksource detection
clock_id = nil,
}
function GestureDetector:new(o)
@ -155,23 +168,42 @@ end
tap2 is the later tap
--]]
function GestureDetector:isTapBounce(tap1, tap2, interval)
-- NOTE: If time went backwards, make the delta infinite to avoid misdetections,
-- as we can no longer compute a sensible value...
local tv_diff = tap2.timev - tap1.timev
if not tv_diff:isPositive() then
tv_diff = TimeVal:new{ sec = math.huge }
end
return (
math.abs(tap1.x - tap2.x) < self.SINGLE_TAP_BOUNCE_DISTANCE and
math.abs(tap1.y - tap2.y) < self.SINGLE_TAP_BOUNCE_DISTANCE and
(tv_diff.sec == 0 and (tv_diff.usec) < interval)
tv_diff < interval
)
end
function GestureDetector:isDoubleTap(tap1, tap2)
local tv_diff = tap2.timev - tap1.timev
if not tv_diff:isPositive() then
tv_diff = TimeVal:new{ sec = math.huge }
end
return (
math.abs(tap1.x - tap2.x) < self.DOUBLE_TAP_DISTANCE and
math.abs(tap1.y - tap2.y) < self.DOUBLE_TAP_DISTANCE and
(tv_diff.sec == 0 and (tv_diff.usec) < ges_double_tap_interval)
tv_diff < ges_double_tap_interval
)
end
-- Takes TimeVals as input, not a tev
function GestureDetector:isHold(t1, t2)
local tv_diff = t2 - t1
if not tv_diff:isPositive() then
tv_diff = TimeVal:new{ sec = 0 }
end
-- NOTE: We cheat by not checking a distance because we're only checking that in tapState,
-- which already ensures a stationary finger, by elimination ;).
return tv_diff >= ges_hold_interval
end
function GestureDetector:isTwoFingerTap()
if self.last_tevs[0] == nil or self.last_tevs[1] == nil then
return false
@ -181,14 +213,20 @@ function GestureDetector:isTwoFingerTap()
local y_diff0 = math.abs(self.last_tevs[0].y - self.first_tevs[0].y)
local y_diff1 = math.abs(self.last_tevs[1].y - self.first_tevs[1].y)
local tv_diff0 = self.last_tevs[0].timev - self.first_tevs[0].timev
if not tv_diff0:isPositive() then
tv_diff0 = TimeVal:new{ sec = math.huge }
end
local tv_diff1 = self.last_tevs[1].timev - self.first_tevs[1].timev
if not tv_diff1:isPositive() then
tv_diff1 = TimeVal:new{ sec = math.huge }
end
return (
x_diff0 < self.TWO_FINGER_TAP_REGION and
x_diff1 < self.TWO_FINGER_TAP_REGION and
y_diff0 < self.TWO_FINGER_TAP_REGION and
y_diff1 < self.TWO_FINGER_TAP_REGION and
tv_diff0.sec == 0 and tv_diff0.usec < ges_two_finger_tap_duration and
tv_diff1.sec == 0 and tv_diff1.usec < ges_two_finger_tap_duration
tv_diff0 < ges_two_finger_tap_duration and
tv_diff1 < ges_two_finger_tap_duration
)
end
@ -227,7 +265,10 @@ end
function GestureDetector:isSwipe(slot)
if not self.first_tevs[slot] or not self.last_tevs[slot] then return end
local tv_diff = self.last_tevs[slot].timev - self.first_tevs[slot].timev
if (tv_diff.sec == 0) and (tv_diff.usec < ges_swipe_interval) then
if not tv_diff:isPositive() then
tv_diff = TimeVal:new{ sec = math.huge }
end
if tv_diff < ges_swipe_interval then
local x_diff = self.last_tevs[slot].x - self.first_tevs[slot].x
local y_diff = self.last_tevs[slot].y - self.first_tevs[slot].y
if x_diff ~= 0 or y_diff ~= 0 then
@ -254,49 +295,54 @@ end
function GestureDetector:clearState(slot)
self.states[slot] = self.initialState
self.hold_timer_id[slot] = nil
self.pending_hold_timer[slot] = nil
self.detectings[slot] = false
self.first_tevs[slot] = nil
self.last_tevs[slot] = nil
self.multiswipe_directions = {}
self.multiswipe_type = nil
-- Also clear any pending hold callbacks on that slot.
-- (single taps call this, so we can't clear double_tap callbacks without being caught in an obvious catch-22 ;)).
self.input:clearTimeout(slot, "hold")
end
function GestureDetector:setNewInterval(type, interval)
if type == "ges_tap_interval" then
ges_tap_interval = interval
ges_tap_interval = TimeVal:new{ usec = interval }
elseif type == "ges_double_tap_interval" then
ges_double_tap_interval = interval
ges_double_tap_interval = TimeVal:new{ usec = interval }
elseif type == "ges_two_finger_tap_duration" then
ges_two_finger_tap_duration = interval
ges_two_finger_tap_duration = TimeVal:new{ usec = interval }
elseif type == "ges_hold_interval" then
ges_hold_interval = interval
ges_hold_interval = TimeVal:new{ usec = interval }
elseif type == "ges_pan_delayed_interval" then
ges_pan_delayed_interval = interval
ges_pan_delayed_interval = TimeVal:new{ usec = interval }
elseif type == "ges_swipe_interval" then
ges_swipe_interval = interval
ges_swipe_interval = TimeVal:new{ usec = interval }
end
end
function GestureDetector:getInterval(type)
if type == "ges_tap_interval" then
return ges_tap_interval
return ges_tap_interval:tousecs()
elseif type == "ges_double_tap_interval" then
return ges_double_tap_interval
return ges_double_tap_interval:tousecs()
elseif type == "ges_two_finger_tap_duration" then
return ges_two_finger_tap_duration
return ges_two_finger_tap_duration:tousecs()
elseif type == "ges_hold_interval" then
return ges_hold_interval
return ges_hold_interval:tousecs()
elseif type == "ges_pan_delayed_interval" then
return ges_pan_delayed_interval
return ges_pan_delayed_interval:tousecs()
elseif type == "ges_swipe_interval" then
return ges_swipe_interval
return ges_swipe_interval:tousecs()
end
end
function GestureDetector:clearStates()
self:clearState(0)
self:clearState(1)
for k, _ in pairs(self.states) do
self:clearState(k)
end
end
function GestureDetector:initialState(tev)
@ -320,10 +366,61 @@ function GestureDetector:initialState(tev)
end
end
--[[--
Attempts to figure out which clock source tap events are using...
]]
function GestureDetector:probeClockSource(timev)
-- We'll check if that timestamp is +/- 2.5s away from the three potential clock sources supported by evdev.
-- We have bigger issues than this if we're parsing events more than 3s late ;).
local threshold = TimeVal:new{ sec = 2, usec = 500000 }
-- Start w/ REALTIME, because it's the easiest to detect ;).
local realtime = TimeVal:realtime_coarse()
-- clock-threshold <= timev <= clock+threshold
if timev >= realtime - threshold and timev <= realtime + threshold then
self.clock_id = C.CLOCK_REALTIME
logger.info("GestureDetector:probeClockSource: Touch event timestamps appear to use CLOCK_REALTIME")
return
end
-- Then MONOTONIC, as it's (hopefully) more common than BOOTTIME (and also guaranteed to be an usable clock source)
local monotonic = TimeVal:monotonic_coarse()
if timev >= monotonic - threshold and timev <= monotonic + threshold then
self.clock_id = C.CLOCK_MONOTONIC
logger.info("GestureDetector:probeClockSource: Touch event timestamps appear to use CLOCK_MONOTONIC")
return
end
-- Finally, BOOTTIME
local boottime = TimeVal:boottime()
if timev >= boottime - threshold and timev <= boottime + threshold then
self.clock_id = C.CLOCK_BOOTTIME
logger.info("GestureDetector:probeClockSource: Touch event timestamps appear to use CLOCK_BOOTTIME")
return
end
-- If we're here, the detection was inconclusive :/
self.clock_id = -1
logger.info("GestureDetector:probeClockSource: Touch event clock source detection was inconclusive")
end
function GestureDetector:getClockSource()
return self.clock_id
end
function GestureDetector:resetClockSource()
self.clock_id = nil
end
--[[--
Handles both single and double tap.
--]]
function GestureDetector:tapState(tev)
-- Attempt to detect the clock source for these events (we reset it on suspend to discriminate MONOTONIC from BOOTTIME).
if not self.clock_id then
self:probeClockSource(tev.timev)
end
logger.dbg("in tap state...")
local slot = tev.slot
if tev.id == -1 then
@ -397,7 +494,7 @@ function GestureDetector:handleDoubleTap(tev)
local tap_interval = self.input.tap_interval_override or ges_tap_interval
-- We do tap bounce detection even when double tap is enabled (so, double tap
-- is triggered when: ges_tap_interval <= delay < ges_double_tap_interval)
if tap_interval > 0 and self.last_taps[slot] ~= nil and self:isTapBounce(self.last_taps[slot], cur_tap, tap_interval) then
if not tap_interval:isZero() and self.last_taps[slot] ~= nil and self:isTapBounce(self.last_taps[slot], cur_tap, tap_interval) then
logger.dbg("tap bounce detected in slot", slot, ": ignored")
-- Simply ignore it, and clear state as this is the end of a touch event
-- (this doesn't clear self.last_taps[slot], so a 3rd tap can be detected
@ -410,6 +507,7 @@ function GestureDetector:handleDoubleTap(tev)
self:isDoubleTap(self.last_taps[slot], cur_tap) then
-- it is a double tap
self:clearState(slot)
self.input:clearTimeout(slot, "double_tap")
ges_ev.ges = "double_tap"
self.last_taps[slot] = nil
logger.dbg("double tap detected in slot", slot)
@ -431,16 +529,9 @@ function GestureDetector:handleDoubleTap(tev)
-- may be the start of a double tap. We'll send it as a single tap after
-- a timer if no second tap happened in the double tap delay.
logger.dbg("set up single/double tap timer")
-- deadline should be calculated by adding current tap time and the interval
-- (No need to compute self._has_real_clock_time_ev_time here, we should always
-- have been thru handleNonTap() where it is computed, before getting here)
local ref_time = self._has_real_clock_time_ev_time and cur_tap.timev or TimeVal:now()
local deadline = ref_time + TimeVal:new{
sec = 0,
usec = not self.input.disable_double_tap and ges_double_tap_interval or 0,
}
self.input:setTimeout(function()
logger.dbg("in single/double tap timer", self.last_taps[slot] ~= nil)
-- setTimeout will handle computing the deadline in the least lossy way possible given the platform.
self.input:setTimeout(slot, "double_tap", function()
logger.dbg("in single/double tap timer, single tap:", self.last_taps[slot] ~= nil)
-- double tap will set last_tap to nil so if it is not, then
-- user has not double-tap'ed: it's a single tap
if self.last_taps[slot] ~= nil then
@ -449,7 +540,7 @@ function GestureDetector:handleDoubleTap(tev)
logger.dbg("single tap detected in slot", slot, ges_ev.pos)
return ges_ev
end
end, deadline)
end, tev.timev, ges_double_tap_interval)
-- we are already at the end of touch event
-- so reset the state
self:clearState(slot)
@ -461,34 +552,21 @@ function GestureDetector:handleNonTap(tev)
-- switched from other state, probably from initialState
-- we return nil in this case
self.states[slot] = self.tapState
if self._has_real_clock_time_ev_time == nil then
if tev.timev.sec < TimeVal:now().sec - 600 then
-- ev.timev is probably the uptime since device boot
-- (which might pause on suspend) that we can't use
-- with setTimeout(): we'll use TimeVal:now()
self._has_real_clock_time_ev_time = false
logger.info("event times are not real clock time: some adjustments will be made")
else
-- assume they are real clock time
self._has_real_clock_time_ev_time = true
logger.info("event times are real clock time: no adjustment needed")
end
end
logger.dbg("set up hold timer")
local ref_time = self._has_real_clock_time_ev_time and tev.timev or TimeVal:now()
local deadline = ref_time + TimeVal:new{
sec = 0, usec = ges_hold_interval
}
-- Be sure the following setTimeout only react to this tapState
local hold_timer_id = tev.timev
self.hold_timer_id[slot] = hold_timer_id
self.input:setTimeout(function()
if self.states[slot] == self.tapState and self.hold_timer_id[slot] == hold_timer_id then
-- timer set in tapState, so we switch to hold
-- Invalidate previous hold timers on that slot so that the following setTimeout will only react to *this* tapState.
self.input:clearTimeout(slot, "hold")
self.pending_hold_timer[slot] = true
self.input:setTimeout(slot, "hold", function()
-- If the pending_hold_timer we set on our first switch to tapState on this slot (e.g., first finger down event),
-- back when the timer was setup, is still relevant (e.g., the slot wasn't run through clearState by a finger up gesture),
-- then check that we're still in a stationary finger down state (e.g., tapState).
if self.pending_hold_timer[slot] and self.states[slot] == self.tapState then
-- That means we can switch to hold
logger.dbg("hold gesture detected in slot", slot)
self.pending_hold_timer[slot] = nil
return self:switchState("holdState", tev, true)
end
end, deadline)
end, tev.timev, ges_hold_interval)
return {
ges = "touch",
pos = Geom:new{
@ -499,12 +577,10 @@ function GestureDetector:handleNonTap(tev)
time = tev.timev,
}
else
-- it is not end of touch event, see if we need to switch to
-- other states
-- We're still inside a stream of input events, see if we need to switch to other states.
if (tev.x and math.abs(tev.x - self.first_tevs[slot].x) >= self.PAN_THRESHOLD) or
(tev.y and math.abs(tev.y - self.first_tevs[slot].y) >= self.PAN_THRESHOLD) then
-- if user's finger moved long enough in X or
-- Y distance, we switch to pan state
-- If user's finger moved far enough on the X or Y axes, switch to pan state.
return self:switchState("panState", tev)
end
end
@ -594,6 +670,9 @@ function GestureDetector:handlePan(tev)
else
local pan_direction, pan_distance = self:getPath(slot)
local tv_diff = self.last_tevs[slot].timev - self.first_tevs[slot].timev
if not tv_diff:isPositive() then
tv_diff = TimeVal:new{ sec = math.huge }
end
local pan_ev = {
ges = "pan",
@ -620,7 +699,7 @@ function GestureDetector:handlePan(tev)
-- delayed pan, used where necessary to reduce potential activation of panning
-- when swiping is intended (e.g., for the menu or for multiswipe)
if not ((tv_diff.sec == 0) and (tv_diff.usec < ges_pan_delayed_interval)) then
if not (tv_diff < ges_pan_delayed_interval) then
pan_ev.relative_delayed.x = tev.x - self.first_tevs[slot].x
pan_ev.relative_delayed.y = tev.y - self.first_tevs[slot].y
pan_ev.distance_delayed = pan_distance
@ -774,7 +853,7 @@ end
function GestureDetector:holdState(tev, hold)
logger.dbg("in hold state...")
local slot = tev.slot
-- when we switch to hold state, we pass additional param "hold"
-- When we switch to hold state, we pass an additional boolean param "hold".
if tev.id ~= -1 and hold and self.last_tevs[slot].x and self.last_tevs[slot].y then
self.states[slot] = self.holdState
return {

@ -13,44 +13,22 @@ local input = require("ffi/input")
local logger = require("logger")
local _ = require("gettext")
-- We're going to need a few <linux/input.h> constants...
local ffi = require("ffi")
local C = ffi.C
require("ffi/posix_h")
require("ffi/linux_input_h")
-- luacheck: push
-- luacheck: ignore
-- constants from <linux/input.h>
local EV_SYN = 0
local EV_KEY = 1
local EV_ABS = 3
local EV_MSC = 4
-- for frontend SDL event handling
local EV_SDL = 53 -- ASCII code for S
-- key press event values (KEY.value)
local EVENT_VALUE_KEY_PRESS = 1
local EVENT_VALUE_KEY_REPEAT = 2
local EVENT_VALUE_KEY_RELEASE = 0
-- Synchronization events (SYN.code).
local SYN_REPORT = 0
local SYN_CONFIG = 1
local SYN_MT_REPORT = 2
-- For single-touch events (ABS.code).
local ABS_X = 00
local ABS_Y = 01
local ABS_PRESSURE = 24
-- For multi-touch events (ABS.code).
local ABS_MT_SLOT = 47
local ABS_MT_TOUCH_MAJOR = 48
local ABS_MT_WIDTH_MAJOR = 50
local ABS_MT_POSITION_X = 53
local ABS_MT_POSITION_Y = 54
local ABS_MT_TRACKING_ID = 57
local ABS_MT_PRESSURE = 58
-- For Kindle Oasis orientation events (ABS.code)
-- the ABS code of orientation event will be adjusted to -24 from 24(ABS_PRESSURE)
-- as ABS_PRESSURE is also used to detect touch input in KOBO devices.
-- the ABS code of orientation event will be adjusted to -24 from 24 (C.ABS_PRESSURE)
-- as C.ABS_PRESSURE is also used to detect touch input in KOBO devices.
local ABS_OASIS_ORIENTATION = -24
local DEVICE_ORIENTATION_PORTRAIT_LEFT = 15
local DEVICE_ORIENTATION_PORTRAIT_RIGHT = 17
@ -61,9 +39,6 @@ local DEVICE_ORIENTATION_PORTRAIT_ROTATED = 20
local DEVICE_ORIENTATION_LANDSCAPE = 21
local DEVICE_ORIENTATION_LANDSCAPE_ROTATED = 22
-- For the events of the Forma accelerometer (MSC.code)
local MSC_RAW = 0x03
-- For the events of the Forma accelerometer (MSC.value)
local MSC_RAW_GSENSOR_PORTRAIT_DOWN = 0x17
local MSC_RAW_GSENSOR_PORTRAIT_UP = 0x18
@ -73,6 +48,56 @@ local MSC_RAW_GSENSOR_LANDSCAPE_LEFT = 0x1a
local MSC_RAW_GSENSOR_BACK = 0x1b
local MSC_RAW_GSENSOR_FRONT = 0x1c
-- For debug logging of ev.type
local linux_evdev_type_map = {
[C.EV_SYN] = "EV_SYN",
[C.EV_KEY] = "EV_KEY",
[C.EV_REL] = "EV_REL",
[C.EV_ABS] = "EV_ABS",
[C.EV_MSC] = "EV_MSC",
[C.EV_SW] = "EV_SW",
[C.EV_LED] = "EV_LED",
[C.EV_SND] = "EV_SND",
[C.EV_REP] = "EV_REP",
[C.EV_FF] = "EV_FF",
[C.EV_PWR] = "EV_PWR",
[C.EV_FF_STATUS] = "EV_FF_STATUS",
[C.EV_MAX] = "EV_MAX",
[C.EV_SDL] = "EV_SDL",
}
-- For debug logging of ev.code
local linux_evdev_syn_code_map = {
[C.SYN_REPORT] = "SYN_REPORT",
[C.SYN_CONFIG] = "SYN_CONFIG",
[C.SYN_MT_REPORT] = "SYN_MT_REPORT",
[C.SYN_DROPPED] = "SYN_DROPPED",
}
local linux_evdev_abs_code_map = {
[C.ABS_X] = "ABS_X",
[C.ABS_Y] = "ABS_Y",
[C.ABS_PRESSURE] = "ABS_PRESSURE",
[C.ABS_MT_SLOT] = "ABS_MT_SLOT",
[C.ABS_MT_TOUCH_MAJOR] = "ABS_MT_TOUCH_MAJOR",
[C.ABS_MT_TOUCH_MINOR] = "ABS_MT_TOUCH_MINOR",
[C.ABS_MT_WIDTH_MAJOR] = "ABS_MT_WIDTH_MAJOR",
[C.ABS_MT_WIDTH_MINOR] = "ABS_MT_WIDTH_MINOR",
[C.ABS_MT_ORIENTATION] = "ABS_MT_ORIENTATION",
[C.ABS_MT_POSITION_X] = "ABS_MT_POSITION_X",
[C.ABS_MT_POSITION_Y] = "ABS_MT_POSITION_Y",
[C.ABS_MT_TOOL_TYPE] = "ABS_MT_TOOL_TYPE",
[C.ABS_MT_BLOB_ID] = "ABS_MT_BLOB_ID",
[C.ABS_MT_TRACKING_ID] = "ABS_MT_TRACKING_ID",
[C.ABS_MT_PRESSURE] = "ABS_MT_PRESSURE",
[C.ABS_MT_DISTANCE] = "ABS_MT_DISTANCE",
[C.ABS_MT_TOOL_X] = "ABS_MT_TOOL_X",
[C.ABS_MT_TOOL_Y] = "ABS_MT_TOOL_Y",
}
local linux_evdev_msc_code_map = {
[C.MSC_RAW] = "MSC_RAW",
}
-- luacheck: pop
local _internal_clipboard_text = nil -- holds the last copied text
@ -236,72 +261,148 @@ end
--- Catalog of predefined hooks.
function Input:adjustTouchSwitchXY(ev)
if ev.type == EV_ABS then
if ev.code == ABS_X then
ev.code = ABS_Y
elseif ev.code == ABS_Y then
ev.code = ABS_X
elseif ev.code == ABS_MT_POSITION_X then
ev.code = ABS_MT_POSITION_Y
elseif ev.code == ABS_MT_POSITION_Y then
ev.code = ABS_MT_POSITION_X
if ev.type == C.EV_ABS then
if ev.code == C.ABS_X then
ev.code = C.ABS_Y
elseif ev.code == C.ABS_Y then
ev.code = C.ABS_X
elseif ev.code == C.ABS_MT_POSITION_X then
ev.code = C.ABS_MT_POSITION_Y
elseif ev.code == C.ABS_MT_POSITION_Y then
ev.code = C.ABS_MT_POSITION_X
end
end
end
function Input:adjustTouchScale(ev, by)
if ev.type == EV_ABS then
if ev.code == ABS_X or ev.code == ABS_MT_POSITION_X then
if ev.type == C.EV_ABS then
if ev.code == C.ABS_X or ev.code == C.ABS_MT_POSITION_X then
ev.value = by.x * ev.value
end
if ev.code == ABS_Y or ev.code == ABS_MT_POSITION_Y then
if ev.code == C.ABS_Y or ev.code == C.ABS_MT_POSITION_Y then
ev.value = by.y * ev.value
end
end
end
function Input:adjustTouchMirrorX(ev, width)
if ev.type == EV_ABS
and (ev.code == ABS_X or ev.code == ABS_MT_POSITION_X) then
if ev.type == C.EV_ABS
and (ev.code == C.ABS_X or ev.code == C.ABS_MT_POSITION_X) then
ev.value = width - ev.value
end
end
function Input:adjustTouchMirrorY(ev, height)
if ev.type == EV_ABS
and (ev.code == ABS_Y or ev.code == ABS_MT_POSITION_Y) then
if ev.type == C.EV_ABS
and (ev.code == C.ABS_Y or ev.code == C.ABS_MT_POSITION_Y) then
ev.value = height - ev.value
end
end
function Input:adjustTouchTranslate(ev, by)
if ev.type == EV_ABS then
if ev.code == ABS_X or ev.code == ABS_MT_POSITION_X then
if ev.type == C.EV_ABS then
if ev.code == C.ABS_X or ev.code == C.ABS_MT_POSITION_X then
ev.value = by.x + ev.value
end
if ev.code == ABS_Y or ev.code == ABS_MT_POSITION_Y then
if ev.code == C.ABS_Y or ev.code == C.ABS_MT_POSITION_Y then
ev.value = by.y + ev.value
end
end
end
function Input:adjustKindleOasisOrientation(ev)
if ev.type == EV_ABS and ev.code == ABS_PRESSURE then
if ev.type == C.EV_ABS and ev.code == C.ABS_PRESSURE then
ev.code = ABS_OASIS_ORIENTATION
end
end
function Input:setTimeout(cb, tv_out)
function Input:setTimeout(slot, ges, cb, origin, delay)
local item = {
slot = slot,
gesture = ges,
callback = cb,
deadline = tv_out,
}
-- We're going to need the clock source id for these events from GestureDetector
local clock_id = self.gesture_detector:getClockSource()
local deadline
-- If we're on a platform with the timerfd backend, handle that
local timerfd
if input.setTimer then
-- If GestureDetector's clock source probing was inconclusive, do this on the UI timescale instead.
if clock_id == -1 then
deadline = TimeVal:now() + delay
else
deadline = origin + delay
end
-- What this does is essentially to ask the kernel to wake us up when the timer expires,
-- instead of ensuring that ourselves via a polling timeout.
-- This ensures perfect accuracy, and allows it to be computed in the event's own timescale.
timerfd = input.setTimer(clock_id, deadline.sec, deadline.usec)
end
if timerfd then
-- It worked, tweak the table a bit to make it clear the deadline will be handled by the kernel
item.timerfd = timerfd
-- We basically only need this for the sorting ;).
item.deadline = deadline
else
-- No timerfd, we'll compute a poll timeout ourselves.
if clock_id == C.CLOCK_MONOTONIC then
-- If the event's clocksource is monotonic, we can use it directly.
deadline = origin + delay
else
-- Otherwise, fudge it by using a current timestamp in the UI's timescale (MONOTONIC).
-- This isn't the end of the world in practice (c.f., #7415).
deadline = TimeVal:now() + delay
end
item.deadline = deadline
end
table.insert(self.timer_callbacks, item)
table.sort(self.timer_callbacks, function(v1,v2)
-- NOTE: While the timescale is monotonic, we may interleave timers based on different delays, so we still need to sort...
table.sort(self.timer_callbacks, function(v1, v2)
return v1.deadline < v2.deadline
end)
end
-- Clear all timeouts for a specific slot (and a specific gesture, if ges is set)
function Input:clearTimeout(slot, ges)
for i = #self.timer_callbacks, 1, -1 do
local item = self.timer_callbacks[i]
if item.slot == slot and (not ges or item.gesture == ges) then
-- If the timerfd backend is in use, close the fd and free the list's node, too.
if item.timerfd then
input.clearTimer(item.timerfd)
end
table.remove(self.timer_callbacks, i)
end
end
end
function Input:clearTimeouts()
-- If the timerfd backend is in use, close the fds, too
if input.setTimer then
for _, item in ipairs(self.timer_callbacks) do
if item.timerfd then
input.clearTimer(item.timerfd)
end
end
end
self.timer_callbacks = {}
end
-- Reset the gesture parsing state to a blank slate
function Input:resetState()
if self.gesture_detector then
self.gesture_detector:clearStates()
-- Resets the clock source probe
self.gesture_detector:resetClockSource()
end
self:clearTimeouts()
end
function Input:handleKeyBoardEv(ev)
local keycode = self.event_map[ev.code]
if not keycode then
@ -434,7 +535,7 @@ From kernel document:
For type B devices, the kernel driver should associate a slot with each
identified contact, and use that slot to propagate changes for the contact.
Creation, replacement and destruction of contacts is achieved by modifying
the ABS_MT_TRACKING_ID of the associated slot. A non-negative tracking id
the C.ABS_MT_TRACKING_ID of the associated slot. A non-negative tracking id
is interpreted as a contact, and the value -1 denotes an unused slot. A
tracking id not previously present is considered new, and a tracking id no
longer present is considered removed. Since only changes are propagated,
@ -443,29 +544,29 @@ end. Upon receiving an MT event, one simply updates the appropriate
attribute of the current slot.
--]]
function Input:handleTouchEv(ev)
if ev.type == EV_ABS then
if ev.type == C.EV_ABS then
if #self.MTSlots == 0 then
table.insert(self.MTSlots, self:getMtSlot(self.cur_slot))
end
if ev.code == ABS_MT_SLOT then
if ev.code == C.ABS_MT_SLOT then
self:addSlotIfChanged(ev.value)
elseif ev.code == ABS_MT_TRACKING_ID then
elseif ev.code == C.ABS_MT_TRACKING_ID then
if self.snow_protocol then
self:addSlotIfChanged(ev.value)
end
self:setCurrentMtSlot("id", ev.value)
elseif ev.code == ABS_MT_POSITION_X then
elseif ev.code == C.ABS_MT_POSITION_X then
self:setCurrentMtSlot("x", ev.value)
elseif ev.code == ABS_MT_POSITION_Y then
elseif ev.code == C.ABS_MT_POSITION_Y then
self:setCurrentMtSlot("y", ev.value)
-- code to emulate mt protocol on kobos
-- we "confirm" abs_x, abs_y only when pressure ~= 0
elseif ev.code == ABS_X then
elseif ev.code == C.ABS_X then
self:setCurrentMtSlot("abs_x", ev.value)
elseif ev.code == ABS_Y then
elseif ev.code == C.ABS_Y then
self:setCurrentMtSlot("abs_y", ev.value)
elseif ev.code == ABS_PRESSURE then
elseif ev.code == C.ABS_PRESSURE then
if ev.value ~= 0 then
self:setCurrentMtSlot("id", 1)
self:confirmAbsxy()
@ -474,8 +575,8 @@ function Input:handleTouchEv(ev)
self:setCurrentMtSlot("id", -1)
end
end
elseif ev.type == EV_SYN then
if ev.code == SYN_REPORT then
elseif ev.type == C.EV_SYN then
if ev.code == C.SYN_REPORT then
for _, MTSlot in pairs(self.MTSlots) do
self:setMtSlot(MTSlot.slot, "timev", TimeVal:new(ev.time))
if self.snow_protocol then
@ -517,49 +618,49 @@ function Input:handleTouchEvPhoenix(ev)
-- Hack on handleTouchEV for the Kobo Aura
-- It seems to be using a custom protocol:
-- finger 0 down:
-- input_report_abs(elan_touch_data.input, ABS_MT_TRACKING_ID, 0);
-- input_report_abs(elan_touch_data.input, ABS_MT_TOUCH_MAJOR, 1);
-- input_report_abs(elan_touch_data.input, ABS_MT_WIDTH_MAJOR, 1);
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_X, x1);
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_Y, y1);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_TRACKING_ID, 0);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_TOUCH_MAJOR, 1);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_WIDTH_MAJOR, 1);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_X, x1);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_Y, y1);
-- input_mt_sync (elan_touch_data.input);
-- finger 1 down:
-- input_report_abs(elan_touch_data.input, ABS_MT_TRACKING_ID, 1);
-- input_report_abs(elan_touch_data.input, ABS_MT_TOUCH_MAJOR, 1);
-- input_report_abs(elan_touch_data.input, ABS_MT_WIDTH_MAJOR, 1);
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_X, x2);
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_Y, y2);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_TRACKING_ID, 1);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_TOUCH_MAJOR, 1);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_WIDTH_MAJOR, 1);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_X, x2);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_Y, y2);
-- input_mt_sync (elan_touch_data.input);
-- finger 0 up:
-- input_report_abs(elan_touch_data.input, ABS_MT_TRACKING_ID, 0);
-- input_report_abs(elan_touch_data.input, ABS_MT_TOUCH_MAJOR, 0);
-- input_report_abs(elan_touch_data.input, ABS_MT_WIDTH_MAJOR, 0);
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_X, last_x);
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_Y, last_y);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_TRACKING_ID, 0);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_TOUCH_MAJOR, 0);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_WIDTH_MAJOR, 0);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_X, last_x);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_Y, last_y);
-- input_mt_sync (elan_touch_data.input);
-- finger 1 up:
-- input_report_abs(elan_touch_data.input, ABS_MT_TRACKING_ID, 1);
-- input_report_abs(elan_touch_data.input, ABS_MT_TOUCH_MAJOR, 0);
-- input_report_abs(elan_touch_data.input, ABS_MT_WIDTH_MAJOR, 0);
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_X, last_x2);
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_Y, last_y2);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_TRACKING_ID, 1);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_TOUCH_MAJOR, 0);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_WIDTH_MAJOR, 0);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_X, last_x2);
-- input_report_abs(elan_touch_data.input, C.ABS_MT_POSITION_Y, last_y2);
-- input_mt_sync (elan_touch_data.input);
if ev.type == EV_ABS then
if ev.type == C.EV_ABS then
if #self.MTSlots == 0 then
table.insert(self.MTSlots, self:getMtSlot(self.cur_slot))
end
if ev.code == ABS_MT_TRACKING_ID then
if ev.code == C.ABS_MT_TRACKING_ID then
self:addSlotIfChanged(ev.value)
self:setCurrentMtSlot("id", ev.value)
elseif ev.code == ABS_MT_TOUCH_MAJOR and ev.value == 0 then
elseif ev.code == C.ABS_MT_TOUCH_MAJOR and ev.value == 0 then
self:setCurrentMtSlot("id", -1)
elseif ev.code == ABS_MT_POSITION_X then
elseif ev.code == C.ABS_MT_POSITION_X then
self:setCurrentMtSlot("x", ev.value)
elseif ev.code == ABS_MT_POSITION_Y then
elseif ev.code == C.ABS_MT_POSITION_Y then
self:setCurrentMtSlot("y", ev.value)
end
elseif ev.type == EV_SYN then
if ev.code == SYN_REPORT then
elseif ev.type == C.EV_SYN then
if ev.code == C.SYN_REPORT then
for _, MTSlot in pairs(self.MTSlots) do
self:setMtSlot(MTSlot.slot, "timev", TimeVal:new(ev.time))
end
@ -578,23 +679,23 @@ end
function Input:handleTouchEvLegacy(ev)
-- Single Touch Protocol. Some devices emit both singletouch and multitouch events.
-- In those devices the 'handleTouchEv' function doesn't work as expected. Use this function instead.
if ev.type == EV_ABS then
if ev.type == C.EV_ABS then
if #self.MTSlots == 0 then
table.insert(self.MTSlots, self:getMtSlot(self.cur_slot))
end
if ev.code == ABS_X then
if ev.code == C.ABS_X then
self:setCurrentMtSlot("x", ev.value)
elseif ev.code == ABS_Y then
elseif ev.code == C.ABS_Y then
self:setCurrentMtSlot("y", ev.value)
elseif ev.code == ABS_PRESSURE then
elseif ev.code == C.ABS_PRESSURE then
if ev.value ~= 0 then
self:setCurrentMtSlot("id", 1)
else
self:setCurrentMtSlot("id", -1)
end
end
elseif ev.type == EV_SYN then
if ev.code == SYN_REPORT then
elseif ev.type == C.EV_SYN then
if ev.code == C.SYN_REPORT then
for _, MTSlot in pairs(self.MTSlots) do
self:setMtSlot(MTSlot.slot, "timev", TimeVal:new(ev.time))
end
@ -651,7 +752,7 @@ end
--- Accelerometer on the Forma, c.f., drivers/hwmon/mma8x5x.c
function Input:handleMiscEvNTX(ev)
local rotation_mode, screen_mode
if ev.code == MSC_RAW then
if ev.code == C.MSC_RAW then
if ev.value == MSC_RAW_GSENSOR_PORTRAIT_UP then
-- i.e., UR
rotation_mode = framebuffer.ORIENTATION_PORTRAIT
@ -774,90 +875,198 @@ end
--- Main event handling.
function Input:waitEvent(timeout_us)
-- `now` corresponds to UIManager:getTime() (a TimeVal), and it's just been updated by UIManager.
-- `deadline` (a TimeVal) is the absolute deadline imposed by UIManager:handleInput() (a.k.a., our main event loop ^^):
-- it's either nil (meaning block forever waiting for input), or the earliest UIManager deadline (in most cases, that's the next scheduled task,
-- in much less common cases, that's the earliest of UIManager.INPUT_TIMEOUT (currently, only KOSync ever sets it) or UIManager.ZMQ_TIMEOUT if there are pending ZMQs).
function Input:waitEvent(now, deadline)
-- On the first iteration of the loop, we don't need to update now, we're following closely (a couple ms at most) behind UIManager.
local ok, ev
-- wrapper for input.waitForEvents that will retry for some cases
-- Wrapper around the platform-specific input.waitForEvent (which itself is generally poll-like).
-- Speaking of input.waitForEvent, it can return:
-- * true, ev: When an input event was read. ev is a table mapped after the input_event <linux/input.h> struct.
-- * false, errno, timerfd: When no input event was read, possibly for benign reasons.
-- One such common case is after a polling timeout, in which case errno is C.ETIME.
-- If the timerfd backend is in use, and the early return was caused by a timerfd expiring,
-- it returns false, C.ETIME, timerfd; where timerfd is a C pointer (i.e., light userdata)
-- to the timerfd node that expired (so as to be able to free it later, c.f., input/timerfd-callbacks.h).
-- Otherwise, errno is the actual error code from the backend (e.g., select's errno for the C backend).
-- * nil: When something terrible happened (e.g., fatal poll/read failure). We abort in such cases.
while true do
if #self.timer_callbacks > 0 then
local wait_deadline = TimeVal:now() + TimeVal:new{
usec = timeout_us
}
-- we don't block if there is any timer, set wait to 10us
-- If we have timers set, we need to honor them once we're done draining the input events.
while #self.timer_callbacks > 0 do
ok, ev = pcall(input.waitForEvent, 100)
-- Choose the earliest deadline between the next timer deadline, and our full timeout deadline.
local deadline_is_timer = false
local poll_deadline
-- If the timer's deadline is handled via timerfd, that's easy
if self.timer_callbacks[1].timerfd then
-- We use the ultimate deadline, as the kernel will just signal us when the timer expires during polling.
poll_deadline = deadline
else
if not deadline then
-- If we don't actually have a full timeout deadline, just honor the timer's.
poll_deadline = self.timer_callbacks[1].deadline
deadline_is_timer = true
else
if self.timer_callbacks[1].deadline < deadline then
poll_deadline = self.timer_callbacks[1].deadline
deadline_is_timer = true
else
poll_deadline = deadline
end
end
end
local poll_timeout
-- With the timerfd backend, poll_deadline is set to deadline, which might be nil, in which case,
-- we can happily block forever, like in the no timer_callbacks branch below ;).
if poll_deadline then
-- If we haven't hit that deadline yet, poll until it expires, otherwise,
-- have select return immediately so that we trip a timeout.
now = now or TimeVal:now()
if poll_deadline > now then
-- Deadline hasn't been blown yet, honor it.
poll_timeout = poll_deadline - now
else
-- We've already blown the deadline: make select return immediately (most likely straight to timeout)
poll_timeout = TimeVal:new{ sec = 0 }
end
end
local timerfd
ok, ev, timerfd = input.waitForEvent(poll_timeout and poll_timeout.sec, poll_timeout and poll_timeout.usec)
-- We got an actual input event, go and process it
if ok then break end
local tv_now = TimeVal:now()
if (not timeout_us or tv_now < wait_deadline) then
-- check whether timer is up
if tv_now >= self.timer_callbacks[1].deadline then
-- If we've drained all pending input events, causing waitForEvent to time out, check our timers
if ok == false and ev == C.ETIME then
-- Check whether the earliest timer to finalize a Gesture detection is up.
-- If we were woken up by a timerfd, or if our actual select deadline was the timer itself,
-- we're guaranteed to have reached it.
-- But if it was a task deadline instead, we to have to check it against the current time.
if timerfd or (deadline_is_timer or TimeVal:now() >= self.timer_callbacks[1].deadline) then
local touch_ges = self.timer_callbacks[1].callback()
table.remove(self.timer_callbacks, 1)
-- If it was a timerfd, we also need to close the fd.
-- NOTE: The fact that deadlines are sorted *should* ensure that the timerfd that expired
-- is actually the first of the list without us having to double-check that...
if timerfd then
input.clearTimer(timerfd)
end
if touch_ges then
-- Do we really need to clear all setTimeout after
-- decided a gesture? FIXME
self.timer_callbacks = {}
-- The timers we'll encounter are for finalizing a hold or (if enabled) double tap gesture,
-- as such, it makes no sense to try to detect *multiple* subsequent gestures.
-- This is why we clear the full list of timers on the first match ;).
self:clearTimeouts()
self:gestureAdjustHook(touch_ges)
return Event:new("Gesture",
self.gesture_detector:adjustGesCoordinate(touch_ges)
)
end -- EOF if touch_ges
end -- EOF if deadline reached
else
break
end -- EOF if not exceed wait timeout
end -- if touch_ges
end -- if poll_deadline reached
end -- if poll returned ETIME
-- Refresh now on the next iteration (e.g., when we have multiple timers to check)
now = nil
end -- while #timer_callbacks > 0
else
ok, ev = pcall(input.waitForEvent, timeout_us)
end -- EOF if #timer_callbacks > 0
if ok then
break
end
-- If there aren't any timers, just block for the requested amount of time.
-- deadline may be nil, in which case waitForEvent blocks indefinitely (i.e., until the next input event ;)).
local poll_timeout
-- If UIManager put us on deadline, enforce it, otherwise, block forever.
if deadline then
-- Convert that absolute deadline to value relative to *now*, as we may loop multiple times between UI ticks.
now = now or TimeVal:now()
if deadline > now then
-- Deadline hasn't been blown yet, honor it.
poll_timeout = deadline - now
else
-- Deadline has been blown: make select return immediately.
poll_timeout = TimeVal:new{ sec = 0 }
end
end
-- ev does contain an error message:
local timeout_err_msg = "Waiting for input failed: timeout\n"
-- ev may not be equal to timeout_err_msg, but it may ends with it
-- ("./ffi/SDL2_0.lua:110: Waiting for input failed: timeout" on the emulator)
if ev and ev.sub and ev:sub(-timeout_err_msg:len()) == timeout_err_msg then
-- don't report an error on timeout
ev = nil
ok, ev = input.waitForEvent(poll_timeout and poll_timeout.sec, poll_timeout and poll_timeout.usec)
end -- if #timer_callbacks > 0
-- Handle errors
if ok then
-- We're good, process the event and go back to UIManager.
break
elseif ev == "application forced to quit" then
--- @todo return an event that can be handled
os.exit(0, true)
end
logger.warn("got error waiting for events:", ev)
if ev ~= "Waiting for input failed: 4\n" then
-- we only abort if the error is not EINTR
elseif ok == false then
if ev == C.ETIME then
-- Don't report an error on ETIME, and go back to UIManager
ev = nil
break
elseif ev == C.EINTR then -- luacheck: ignore
-- Retry on EINTR
else
-- Warn, report, and go back to UIManager
logger.warn("Polling for input events returned an error:", ev)
break
end
elseif ok == nil then
-- Something went horribly wrong, abort.
logger.err("Polling for input events failed catastrophically")
local UIManager = require("ui/uimanager")
UIManager:abort()
break
end
-- We'll need to refresh now on the next iteration, if there is one.
now = nil
end
if ok and ev then
if DEBUG.is_on and ev then
if DEBUG.is_on then
DEBUG:logEv(ev)
logger.dbg(string.format(
"%s event => type: %d, code: %d(%s), value: %s, time: %d.%d",
ev.type == EV_KEY and "key" or "input",
ev.type, ev.code, self.event_map[ev.code], tostring(ev.value),
ev.time.sec, ev.time.usec))
if ev.type == C.EV_KEY then
logger.dbg(string.format(
"key event => code: %d (%s), value: %s, time: %d.%d",
ev.code, self.event_map[ev.code], ev.value,
ev.time.sec, ev.time.usec))
elseif ev.type == C.EV_SYN then
logger.dbg(string.format(
"input event => type: %d (%s), code: %d (%s), value: %s, time: %d.%d",
ev.type, linux_evdev_type_map[ev.type], ev.code, linux_evdev_syn_code_map[ev.code], ev.value,
ev.time.sec, ev.time.usec))
elseif ev.type == C.EV_ABS then
logger.dbg(string.format(
"input event => type: %d (%s), code: %d (%s), value: %s, time: %d.%d",
ev.type, linux_evdev_type_map[ev.type], ev.code, linux_evdev_abs_code_map[ev.code], ev.value,
ev.time.sec, ev.time.usec))
elseif ev.type == C.EV_MSC then
logger.dbg(string.format(
"input event => type: %d (%s), code: %d (%s), value: %s, time: %d.%d",
ev.type, linux_evdev_type_map[ev.type], ev.code, linux_evdev_msc_code_map[ev.code], ev.value,
ev.time.sec, ev.time.usec))
else
logger.dbg(string.format(
"input event => type: %d (%s), code: %d, value: %s, time: %d.%d",
ev.type, linux_evdev_type_map[ev.type], ev.code, ev.value,
ev.time.sec, ev.time.usec))
end
end
self:eventAdjustHook(ev)
if ev.type == EV_KEY then
if ev.type == C.EV_KEY then
return self:handleKeyBoardEv(ev)
elseif ev.type == EV_ABS and ev.code == ABS_OASIS_ORIENTATION then
elseif ev.type == C.EV_ABS and ev.code == ABS_OASIS_ORIENTATION then
return self:handleOasisOrientationEv(ev)
elseif ev.type == EV_ABS or ev.type == EV_SYN then
elseif ev.type == C.EV_ABS or ev.type == C.EV_SYN then
return self:handleTouchEv(ev)
elseif ev.type == EV_MSC then
elseif ev.type == C.EV_MSC then
return self:handleMiscEv(ev)
elseif ev.type == EV_SDL then
elseif ev.type == C.EV_SDL then
return self:handleSdlEv(ev)
else
-- some other kind of event that we do not know yet
-- Received some other kind of event that we do not know how to specifically handle yet
return Event:new("GenericInput", ev)
end
elseif not ok and ev then
elseif ok == false and ev then
return Event:new("InputError", ev)
elseif ok == nil then
-- No ok and no ev? Hu oh...
return Event:new("InputError", "Catastrophic")
end
end

@ -1,5 +1,4 @@
local Generic = require("device/generic/device") -- <= look at this file!
local TimeVal = require("ui/timeval")
local logger = require("logger")
local function yes() return true end
@ -58,7 +57,6 @@ local Remarkable1 = Remarkable:new{
function Remarkable1:adjustTouchEvent(ev, by)
if ev.type == EV_ABS then
ev.time = TimeVal:now()
-- Mirror X and Y and scale up both X & Y as touch input is different res from
-- display
if ev.code == ABS_MT_POSITION_X then
@ -81,7 +79,6 @@ local Remarkable2 = Remarkable:new{
}
function Remarkable2:adjustTouchEvent(ev, by)
ev.time = TimeVal:now()
if ev.type == EV_ABS then
-- Mirror Y and scale up both X & Y as touch input is different res from
-- display

@ -3,9 +3,9 @@ local TimeVal = require("ui/timeval")
local GestureRange = {
-- gesture matching type
ges = nil,
-- spatial range limits the gesture emitting position
-- spatial range, limits the gesture emitting position
range = nil,
-- temproal range limits the gesture emitting rate
-- temporal range, limits the gesture emitting rate
rate = nil,
-- scale limits of this gesture
scale = nil,
@ -23,12 +23,11 @@ function GestureRange:match(gs)
return false
end
if self.range then
-- sometimes widget dimenension is not available when creating a gesturerage
-- for some action, now we accept a range function that will be later called
-- and the result of which will be used to check gesture match
-- Sometimes the widget's dimensions are not available when creating a GestureRange
-- for some action, so we accept a range function that will only be called at match() time instead.
-- e.g. range = function() return self.dimen end
-- for inputcontainer given that the x and y field of `self.dimen` is only
-- filled when the inputcontainer is painted into blitbuffer
-- That's because most widgets' dimensions are only set at paintTo() time:
-- e.g., with InputContainer, the x and y fields of `self.dimen`.
local range
if type(self.range) == "function" then
range = self.range()
@ -41,10 +40,9 @@ function GestureRange:match(gs)
end
if self.rate then
-- This filed restraints the upper limit rate(matches per second).
-- It's most useful for e-ink devices with less powerfull CPUs and
-- screens that cannot handle gesture events that otherwise will be
-- generated
-- This field sets up rate-limiting (in matches per second).
-- It's mostly useful for e-Ink devices with less powerful CPUs
-- and screens that cannot handle the amount of gesture events that would otherwise be generated.
local last_time = self.last_time or TimeVal:new{}
if gs.time - last_time > TimeVal:new{usec = 1000000 / self.rate} then
self.last_time = gs.time

@ -8,20 +8,47 @@ A simple module to module to compare and do arithmetic with time values.
-- Do some stuff.
-- You can add and subtract `TimeVal` objects.
local tv_duration = TimeVal:now() - tv_start
-- If you need more precision (like 2.5 s),
-- you can add the milliseconds to the seconds.
local tv_duration_seconds_float = tv_duration.sec + tv_duration.usec/1000000
-- And convert that object to various more human-readable formats, e.g.,
print(string.format("Stuff took %.3fms", tv_duration:tomsecs()))
]]
local dbg = require("dbg")
local ffi = require("ffi")
require("ffi/posix_h")
local util = require("ffi/util")
local C = ffi.C
-- We prefer CLOCK_MONOTONIC_COARSE if it's available and has a decent resolution,
-- as we generally don't need nano/micro second precision,
-- and it can be more than twice as fast as CLOCK_MONOTONIC/CLOCK_REALTIME/gettimeofday...
local PREFERRED_MONOTONIC_CLOCKID = C.CLOCK_MONOTONIC
-- Ditto for REALTIME (for :realtime_coarse only, :realtime uses gettimeofday ;)).
local PREFERRED_REALTIME_CLOCKID = C.CLOCK_REALTIME
if ffi.os == "Linux" then
-- Unfortunately, it was only implemented in Linux 2.6.32, and we may run on older kernels than that...
-- So, just probe it to see if we can rely on it.
local probe_ts = ffi.new("struct timespec")
if C.clock_getres(C.CLOCK_MONOTONIC_COARSE, probe_ts) == 0 then
-- Now, it usually has a 1ms resolution on modern x86_64 systems,
-- but it only provides a 10ms resolution on all my armv7 devices :/.
if probe_ts.tv_sec == 0 and probe_ts.tv_nsec <= 1000000 then
PREFERRED_MONOTONIC_CLOCKID = C.CLOCK_MONOTONIC_COARSE
end
end
if C.clock_getres(C.CLOCK_REALTIME_COARSE, probe_ts) == 0 then
if probe_ts.tv_sec == 0 and probe_ts.tv_nsec <= 1000000 then
PREFERRED_REALTIME_CLOCKID = C.CLOCK_REALTIME_COARSE
end
end
probe_ts = nil --luacheck: ignore
end
--[[--
TimeVal object.
TimeVal object. Maps to a POSIX struct timeval (<sys/time.h>).
@table TimeVal
@int sec floored number of seconds
@int usec remaining number of milliseconds
@int usec number of microseconds past that second.
]]
local TimeVal = {
sec = 0,
@ -47,7 +74,7 @@ function TimeVal:new(from_o)
if o.usec == nil then
o.usec = 0
elseif o.usec > 1000000 then
o.sec = o.sec + math.floor(o.usec/1000000)
o.sec = o.sec + math.floor(o.usec / 1000000)
o.usec = o.usec % 1000000
end
setmetatable(o, self)
@ -55,55 +82,39 @@ function TimeVal:new(from_o)
return o
end
-- Based on <bsd/sys/time.h>
function TimeVal:__lt(time_b)
if self.sec < time_b.sec then
return true
elseif self.sec > time_b.sec then
return false
if self.sec == time_b.sec then
return self.usec < time_b.usec
else
-- self.sec == time_b.sec
if self.usec < time_b.usec then
return true
else
return false
end
return self.sec < time_b.sec
end
end
function TimeVal:__le(time_b)
if self.sec < time_b.sec then
return true
elseif self.sec > time_b.sec then
return false
if self.sec == time_b.sec then
return self.usec <= time_b.usec
else
-- self.sec == time_b.sec
if self.usec > time_b.usec then
return false
else
return true
end
return self.sec <= time_b.sec
end
end
function TimeVal:__eq(time_b)
if self.sec == time_b.sec and self.usec == time_b.usec then
return true
if self.sec == time_b.sec then
return self.usec == time_b.usec
else
return false
end
end
-- If sec is negative, time went backwards!
function TimeVal:__sub(time_b)
local diff = TimeVal:new{}
diff.sec = self.sec - time_b.sec
diff.usec = self.usec - time_b.usec
if diff.sec < 0 and diff.usec > 0 then
diff.sec = diff.sec + 1
diff.usec = diff.usec - 1000000
elseif diff.sec > 0 and diff.usec < 0 then
if diff.usec < 0 then
diff.sec = diff.sec - 1
diff.usec = diff.usec + 1000000
end
@ -111,48 +122,127 @@ function TimeVal:__sub(time_b)
return diff
end
dbg:guard(TimeVal, '__sub',
function(self, time_b)
assert(self.sec > time_b.sec or (self.sec == time_b.sec and self.usec >= time_b.usec),
"Subtract the first timeval from the latest, not vice versa.")
end)
function TimeVal:__add(time_b)
local sum = TimeVal:new{}
sum.sec = self.sec + time_b.sec
sum.usec = self.usec + time_b.usec
if sum.usec > 1000000 then
sum.usec = sum.usec - 1000000
sum.sec = sum.sec + 1
end
if sum.sec < 0 and sum.usec > 0 then
if sum.usec >= 1000000 then
sum.sec = sum.sec + 1
sum.usec = sum.usec - 1000000
elseif sum.sec > 0 and sum.usec < 0 then
sum.sec = sum.sec - 1
sum.usec = sum.usec + 1000000
end
return sum
end
--[[--
Creates a new TimeVal object based on the current time.
Creates a new TimeVal object based on the current wall clock time.
(e.g., gettimeofday / clock_gettime(CLOCK_REALTIME).
This is a simple wrapper around util.gettime() to get all the niceties of a TimeVal object.
If you don't need sub-second precision, prefer os.time().
Which means that, yes, this is a fancier POSIX Epoch ;).
@usage
local TimeVal = require("ui/timeval")
local tv_start = TimeVal:now()
local tv_start = TimeVal:realtime()
-- Do some stuff.
-- You can add and substract `TimeVal` objects.
local tv_duration = TimeVal:now() - tv_start
local tv_duration = TimeVal:realtime() - tv_start
@treturn TimeVal
]]
function TimeVal:now()
function TimeVal:realtime()
local sec, usec = util.gettime()
return TimeVal:new{sec = sec, usec = usec}
end
--[[--
Creates a new TimeVal object based on the current value from the system's MONOTONIC clock source.
(e.g., clock_gettime(CLOCK_MONOTONIC).)
POSIX guarantees that this clock source will *never* go backwards (but it *may* return the same value multiple times).
On Linux, this will not account for time spent with the device in suspend (unlike CLOCK_BOOTTIME).
@treturn TimeVal
]]
function TimeVal:monotonic()
local timespec = ffi.new("struct timespec")
C.clock_gettime(C.CLOCK_MONOTONIC, timespec)
-- TIMESPEC_TO_TIMEVAL
return TimeVal:new{sec = tonumber(timespec.tv_sec), usec = math.floor(tonumber(timespec.tv_nsec / 1000))}
end
--- Ditto, but w/ CLOCK_MONOTONIC_COARSE if it's available and has a 1ms resolution or better (uses CLOCK_MONOTONIC otherwise).
function TimeVal:monotonic_coarse()
local timespec = ffi.new("struct timespec")
C.clock_gettime(PREFERRED_MONOTONIC_CLOCKID, timespec)
-- TIMESPEC_TO_TIMEVAL
return TimeVal:new{sec = tonumber(timespec.tv_sec), usec = math.floor(tonumber(timespec.tv_nsec / 1000))}
end
--- Ditto, but w/ CLOCK_REALTIME_COARSE if it's available and has a 1ms resolution or better (uses CLOCK_REALTIME otherwise).
function TimeVal:realtime_coarse()
local timespec = ffi.new("struct timespec")
C.clock_gettime(PREFERRED_REALTIME_CLOCKID, timespec)
-- TIMESPEC_TO_TIMEVAL
return TimeVal:new{sec = tonumber(timespec.tv_sec), usec = math.floor(tonumber(timespec.tv_nsec / 1000))}
end
--- Ditto, but w/ CLOCK_BOOTTIME (will return a TimeVal set to 0, 0 if the clock source is unsupported, as it's 2.6.39+)
function TimeVal:boottime()
local timespec = ffi.new("struct timespec")
C.clock_gettime(C.CLOCK_BOOTTIME, timespec)
-- TIMESPEC_TO_TIMEVAL
return TimeVal:new{sec = tonumber(timespec.tv_sec), usec = math.floor(tonumber(timespec.tv_nsec / 1000))}
end
--[[-- Alias for `monotonic_coarse`.
The assumption being anything that requires accurate timestamps expects a monotonic clock source.
This is certainly true for KOReader's UI scheduling.
]]
TimeVal.now = TimeVal.monotonic_coarse
--- Converts a TimeVal object to a Lua (decimal) number (sec.usecs) (accurate to the ms, rounded to 4 decimal places)
function TimeVal:tonumber()
-- Round to 4 decimal places
return math.floor((self.sec + self.usec / 1000000) * 10000) / 10000
end
--- Converts a TimeVal object to a Lua (int) number (resolution: 1µs)
function TimeVal:tousecs()
return math.floor(self.sec * 1000000 + self.usec + 0.5)
end
--[[-- Converts a TimeVal object to a Lua (int) number (resolution: 1ms).
(Mainly useful when computing a time lapse for benchmarking purposes).
]]
function TimeVal:tomsecs()
return self:tousecs() / 1000
end
--- Converts a Lua (decimal) number (sec.usecs) to a TimeVal object
function TimeVal:fromnumber(seconds)
local sec = math.floor(seconds)
local usec = math.floor((seconds - sec) * 1000000 + 0.5)
return TimeVal:new{sec = sec, usec = usec}
end
--- Checks if a TimeVal object is positive
function TimeVal:isPositive()
return self.sec >= 0
end
--- Checks if a TimeVal object is zero
function TimeVal:isZero()
return self.sec == 0 and self.usec == 0
end
return TimeVal

@ -5,6 +5,7 @@ This module manages widgets.
local Device = require("device")
local Event = require("ui/event")
local Geom = require("ui/geometry")
local TimeVal = require("ui/timeval")
local dbg = require("dbg")
local logger = require("logger")
local ffiUtil = require("ffi/util")
@ -13,7 +14,6 @@ local _ = require("gettext")
local Input = Device.input
local Screen = Device.screen
local MILLION = 1000000
local DEFAULT_FULL_REFRESH_COUNT = 6
-- there is only one instance of this
@ -29,6 +29,7 @@ local UIManager = {
event_handlers = nil,
_running = true,
_now = TimeVal:now(),
_window_stack = {},
_task_queue = {},
_task_queue_dirty = false,
@ -512,13 +513,11 @@ end
function UIManager:schedule(time, action, ...)
local p, s, e = 1, 1, #self._task_queue
if e ~= 0 then
local us = time[1] * MILLION + time[2]
-- do a binary insert
repeat
p = math.floor(s + (e - s) / 2)
local ptime = self._task_queue[p].time
local ptus = ptime[1] * MILLION + ptime[2]
if us > ptus then
local p_time = self._task_queue[p].time
if time > p_time then
if s == e then
p = e + 1
break
@ -527,7 +526,7 @@ function UIManager:schedule(time, action, ...)
else
s = p
end
elseif us < ptus then
elseif time < p_time then
e = p
if s == e then
break
@ -549,29 +548,23 @@ function UIManager:schedule(time, action, ...)
end
dbg:guard(UIManager, 'schedule',
function(self, time, action)
assert(time[1] >= 0 and time[2] >= 0, "Only positive time allowed")
assert(time.sec >= 0, "Only positive time allowed")
assert(action ~= nil)
end)
--[[--
Schedules a task to be run a certain amount of seconds from now.
@number seconds scheduling delay in seconds (supports decimal values)
@number seconds scheduling delay in seconds (supports decimal values, 1ms resolution).
@func action reference to the task to be scheduled (may be anonymous)
@param ... optional arguments passed to action
@see unschedule
]]
function UIManager:scheduleIn(seconds, action, ...)
local when = { ffiUtil.gettime() }
local s = math.floor(seconds)
local usecs = (seconds - s) * MILLION
when[1] = when[1] + s
when[2] = when[2] + usecs
if when[2] >= MILLION then
when[1] = when[1] + 1
when[2] = when[2] - MILLION
end
-- We might run significantly late inside an UI frame, so we can't use the cached value here.
-- It would also cause some bad interactions with the way nextTick & co behave.
local when = TimeVal:now() + TimeVal:fromnumber(seconds)
self:schedule(when, action, ...)
end
dbg:guard(UIManager, 'scheduleIn',
@ -1049,7 +1042,7 @@ function UIManager:discardEvents(set_or_seconds)
self._discard_events_till = nil
return
end
local usecs
local delay
if set_or_seconds == true then
-- Use an adequate delay to account for device refresh duration
-- so any events happening in this delay (ie. before a widget
@ -1059,17 +1052,15 @@ function UIManager:discardEvents(set_or_seconds)
-- sometimes > 500ms on some devices/temperatures.
-- So, block for 400ms (to have it displayed) + 400ms
-- for user reaction to it
usecs = 800000
delay = TimeVal:new{ usec = 800000 }
else
-- On non-eInk screen, display is usually instantaneous
usecs = 400000
delay = TimeVal:new{ usec = 400000 }
end
else -- we expect a number
usecs = set_or_seconds * MILLION
delay = TimeVal:new{ sec = set_or_seconds }
end
local now = { ffiUtil.gettime() }
local now_us = now[1] * MILLION + now[2]
self._discard_events_till = now_us + usecs
self._discard_events_till = self._now + delay
end
--[[--
@ -1082,9 +1073,7 @@ function UIManager:sendEvent(event)
-- Ensure discardEvents
if self._discard_events_till then
local now = { ffiUtil.gettime() }
local now_us = now[1] * MILLION + now[2]
if now_us < self._discard_events_till then
if TimeVal:now() < self._discard_events_till then
return
else
self._discard_events_till = nil
@ -1159,8 +1148,7 @@ function UIManager:broadcastEvent(event)
end
function UIManager:_checkTasks()
local now = { ffiUtil.gettime() }
local now_us = now[1] * MILLION + now[2]
self._now = TimeVal:now()
local wait_until = nil
-- task.action may schedule other events
@ -1172,11 +1160,8 @@ function UIManager:_checkTasks()
break
end
local task = self._task_queue[1]
local task_us = 0
if task.time ~= nil then
task_us = task.time[1] * MILLION + task.time[2]
end
if task_us <= now_us then
local task_tv = task.time or TimeVal:new{}
if task_tv <= self._now then
-- remove from table
table.remove(self._task_queue, 1)
-- task is pending to be executed right now. do it.
@ -1191,7 +1176,26 @@ function UIManager:_checkTasks()
end
end
return wait_until, now
return wait_until, self._now
end
--[[--
Returns a TimeVal object corresponding to the last UI tick.
This is essentially a cached TimeVal:now(), computed at the top of every iteration of the main UI loop,
(right before checking/running scheduled tasks).
This is mainly useful to compute/schedule stuff in the same time scale as the UI loop (i.e., MONOTONIC),
without having to resort to a syscall.
It should never be significantly stale (i.e., it should be precise enough),
unless you're blocking the UI for a significant amount of time in the same UI tick.
Prefer the appropriate TimeVal method for your needs if you require perfect accuracy
(e.g., when you're actually working on the event loop *itself* (UIManager, Input, GestureDetector)).
This is *NOT* wall clock time (REALTIME).
]]
function UIManager:getTime()
return self._now
end
-- precedence of refresh modes:
@ -1580,29 +1584,35 @@ function UIManager:handleInput()
self:processZMQs()
-- Figure out how long to wait.
-- Ultimately, that'll be the earliest of INPUT_TIMEOUT, ZMQ_TIMEOUT or the next earliest scheduled task.
local deadline
-- Default to INPUT_TIMEOUT (which may be nil, i.e. block until an event happens).
local wait_us = self.INPUT_TIMEOUT
-- If there's a timed event pending, that puts an upper bound on how long to wait.
if wait_until then
wait_us = math.min(
wait_us or math.huge,
(wait_until[1] - now[1]) * MILLION
+ (wait_until[2] - now[2]))
end
-- If we have any ZMQs registered, ZMQ_TIMEOUT is another upper bound.
if #self._zeromqs > 0 then
wait_us = math.min(wait_us or math.huge, self.ZMQ_TIMEOUT)
end
-- We pass that on as an absolute deadline, not a relative wait time.
if wait_us then
deadline = now + TimeVal:new{ usec = wait_us }
end
-- If there's a scheduled task pending, that puts an upper bound on how long to wait.
if wait_until and (not deadline or wait_until < deadline) then
-- ^ We don't have a TIMEOUT induced deadline, making the choice easy.
-- ^ We have a task scheduled for *before* our TIMEOUT induced deadline.
deadline = wait_until
end
-- If allowed, entering standby (from which we can wake by input) must trigger in response to event
-- this function emits (plugin), or within waitEvent() right after (hardware).
-- Anywhere else breaks preventStandby/allowStandby invariants used by background jobs while UI is left running.
self:_standbyTransition()
-- wait for next event
local input_event = Input:waitEvent(wait_us)
local input_event = Input:waitEvent(now, deadline)
-- delegate input_event to handler
if input_event then
@ -1672,6 +1682,9 @@ end
function UIManager:_beforeSuspend()
self:flushSettings()
self:broadcastEvent(Event:new("Suspend"))
-- Reset gesture detection state to a blank slate (anything power-management related emits KEY events, which don't need gesture detection).
Input:resetState()
end
-- The common operations that should be performed after resuming the device.
@ -1772,5 +1785,11 @@ function UIManager:restartKOReader()
self._exit_code = 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
end
UIManager:init()
return UIManager

@ -20,6 +20,7 @@ local ScrollHtmlWidget = require("ui/widget/scrollhtmlwidget")
local ScrollTextWidget = require("ui/widget/scrolltextwidget")
local Size = require("ui/size")
local TextWidget = require("ui/widget/textwidget")
local TimeVal = require("ui/timeval")
local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan")
@ -153,7 +154,7 @@ function DictQuickLookup:init()
-- callback function when HoldReleaseText is handled as args
args = function(text, hold_duration)
local lookup_target
if hold_duration < 3.0 then
if hold_duration < TimeVal:new{ sec = 3 } then
-- do this lookup in the same domain (dict/wikipedia)
lookup_target = self.is_wiki and "LookupWikipedia" or "LookupWord"
else

@ -13,6 +13,7 @@ local InputContainer = require("ui/widget/container/inputcontainer")
local LineWidget = require("ui/widget/linewidget")
local ScrollHtmlWidget = require("ui/widget/scrollhtmlwidget")
local Size = require("ui/size")
local TimeVal = require("ui/timeval")
local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan")
@ -194,7 +195,7 @@ function FootnoteWidget:init()
-- callback function when HoldReleaseText is handled as args
args = function(text, hold_duration)
if self.dialog then
local lookup_target = hold_duration < 3.0 and "LookupWord" or "LookupWikipedia"
local lookup_target = hold_duration < TimeVal:new{ sec = 3 } and "LookupWord" or "LookupWikipedia"
self.dialog:handleEvent(
Event:new(lookup_target, text)
)

@ -9,7 +9,7 @@ local GestureRange = require("ui/gesturerange")
local InputContainer = require("ui/widget/container/inputcontainer")
local Mupdf = require("ffi/mupdf")
local Screen = Device.screen
local TimeVal = require("ui/timeval")
local UIManager = require("ui/uimanager")
local logger = require("logger")
local util = require("util")
@ -165,7 +165,7 @@ function HtmlBoxWidget:onHoldStartText(_, ges)
return false -- let event be processed by other widgets
end
self.hold_start_tv = TimeVal.now()
self.hold_start_tv = UIManager:getTime()
return true
end
@ -229,8 +229,7 @@ function HtmlBoxWidget:onHoldReleaseText(callback, ges)
return false
end
local hold_duration = TimeVal.now() - self.hold_start_tv
hold_duration = hold_duration.sec + (hold_duration.usec/1000000)
local hold_duration = UIManager:getTime() - self.hold_start_tv
local page = self.document:openPage(self.page_number)
local lines = page:getPageText()

@ -13,6 +13,7 @@ local InputContainer = require("ui/widget/container/inputcontainer")
local RectSpan = require("ui/widget/rectspan")
local Size = require("ui/size")
local TextWidget = require("ui/widget/textwidget")
local TimeVal = require("ui/timeval")
local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup")
local Input = Device.input
@ -73,7 +74,7 @@ function Notification:init()
local notif_height = self.frame:getSize().h
self:_cleanShownStack()
table.insert(Notification._nums_shown, os.time())
table.insert(Notification._nums_shown, UIManager:getTime())
self.num = #Notification._nums_shown
self[1] = VerticalGroup:new{
@ -101,9 +102,9 @@ function Notification:_cleanShownStack(num)
-- to follow what is happening).
-- As a sanity check, we also forget those shown for
-- more than 30s in case no close event was received.
local expire_ts = os.time() - 30
local expire_tv = UIManager:getTime() - TimeVal:new{ sec = 30 }
for i=#Notification._nums_shown, 1, -1 do
if Notification._nums_shown[i] and Notification._nums_shown[i] > expire_ts then
if Notification._nums_shown[i] and Notification._nums_shown[i] > expire_tv then
break -- still shown (or not yet expired)
end
table.remove(Notification._nums_shown, i)

@ -24,7 +24,6 @@ local RenderText = require("ui/rendertext")
local RightContainer = require("ui/widget/container/rightcontainer")
local Size = require("ui/size")
local TextWidget = require("ui/widget/textwidget")
local TimeVal = require("ui/timeval")
local UIManager = require("ui/uimanager")
local Math = require("optmath")
local logger = require("logger")
@ -1790,7 +1789,7 @@ function TextBoxWidget:onHoldStartText(_, ges)
return false -- let event be processed by other widgets
end
self.hold_start_tv = TimeVal.now()
self.hold_start_tv = UIManager:getTime()
return true
end
@ -1822,8 +1821,7 @@ function TextBoxWidget:onHoldReleaseText(callback, ges)
return false
end
local hold_duration = TimeVal.now() - self.hold_start_tv
hold_duration = hold_duration.sec + hold_duration.usec/1000000
local hold_duration = UIManager:getTime() - self.hold_start_tv
-- If page contains an image, check if Hold is on this image and deal
-- with it directly
@ -1917,7 +1915,7 @@ function TextBoxWidget:onHoldReleaseText(callback, ges)
-- to consider when looking for word boundaries)
local selected_text = self._xtext:getSelectedWords(sel_start_idx, sel_end_idx, 50)
logger.dbg("onHoldReleaseText (duration:", hold_duration, ") :",
logger.dbg("onHoldReleaseText (duration:", hold_duration:tonumber(), ") :",
sel_start_idx, ">", sel_end_idx, "=", selected_text)
callback(selected_text, hold_duration)
return true
@ -1935,7 +1933,7 @@ function TextBoxWidget:onHoldReleaseText(callback, ges)
end
local selected_text = table.concat(self.charlist, "", sel_start_idx, sel_end_idx)
logger.dbg("onHoldReleaseText (duration:", hold_duration, ") :", sel_start_idx, ">", sel_end_idx, "=", selected_text)
logger.dbg("onHoldReleaseText (duration:", hold_duration:tonumber(), ") :", sel_start_idx, ">", sel_end_idx, "=", selected_text)
callback(selected_text, hold_duration)
return true
end

@ -16,6 +16,7 @@ local InputContainer = require("ui/widget/container/inputcontainer")
local KeyboardLayoutDialog = require("ui/widget/keyboardlayoutdialog")
local Size = require("ui/size")
local TextWidget = require("ui/widget/textwidget")
local TimeVal = require("ui/timeval")
local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan")
@ -588,6 +589,7 @@ function VirtualKeyPopup:init()
},
}
self.tap_interval_override = G_reader_settings:readSetting("ges_tap_interval_on_keyboard") or 0
self.tap_interval_override = TimeVal:new{ usec = self.tap_interval_override }
if Device:hasDPad() then
self.key_events.PressKey = { {"Press"}, doc = "select key" }
@ -699,6 +701,7 @@ function VirtualKeyboard:init()
self.max_layer = keyboard.max_layer
self:initLayer(self.keyboard_layer)
self.tap_interval_override = G_reader_settings:readSetting("ges_tap_interval_on_keyboard") or 0
self.tap_interval_override = TimeVal:new{ usec = self.tap_interval_override }
if Device:hasDPad() then
self.key_events.PressKey = { {"Press"}, doc = "select key" }
end

@ -63,7 +63,7 @@ function AutoStandby:addToMainMenu(menu_items)
}
end
-- We've received touch/key event, so delay stadby accordingly
-- We've received touch/key event, so delay standby accordingly
function AutoStandby:onInputEvent()
logger.dbg("AutoStandby:onInputevent() instance=", tostring(self))
local config = self.settings.data

@ -10,6 +10,7 @@ if not Device:isCervantes() and
end
local PluginShare = require("pluginshare")
local TimeVal = require("ui/timeval")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
@ -24,7 +25,7 @@ local AutoSuspend = WidgetContainer:new{
is_doc_only = false,
autoshutdown_timeout_seconds = G_reader_settings:readSetting("autoshutdown_timeout_seconds") or default_autoshutdown_timeout_seconds,
auto_suspend_timeout_seconds = G_reader_settings:readSetting("auto_suspend_timeout_seconds") or default_auto_suspend_timeout_seconds,
last_action_sec = os.time(),
last_action_tv = TimeVal:now(),
standby_prevented = false,
}
@ -48,9 +49,11 @@ function AutoSuspend:_schedule(shutdown_only)
delay_suspend = self.auto_suspend_timeout_seconds
delay_shutdown = self.autoshutdown_timeout_seconds
else
local now_ts = os.time()
delay_suspend = self.last_action_sec + self.auto_suspend_timeout_seconds - now_ts
delay_shutdown = self.last_action_sec + self.autoshutdown_timeout_seconds - now_ts
local now_tv = UIManager:getTime()
delay_suspend = self.last_action_tv + TimeVal:new{ sec = self.auto_suspend_timeout_seconds } - now_tv
delay_suspend = delay_suspend:tonumber()
delay_shutdown = self.last_action_tv + TimeVal:new{ sec = self.autoshutdown_timeout_seconds } - now_tv
delay_shutdown = delay_shutdown:tonumber()
end
-- Try to shutdown first, as we may have been woken up from suspend just for the sole purpose of doing that.
@ -79,9 +82,9 @@ end
function AutoSuspend:_start()
if self:_enabled() or self:_enabledShutdown() then
local now_ts = os.time()
logger.dbg("AutoSuspend: start at", now_ts)
self.last_action_sec = now_ts
local now_tv = UIManager:getTime()
logger.dbg("AutoSuspend: start at", now_tv:tonumber())
self.last_action_tv = now_tv
self:_schedule()
end
end
@ -89,9 +92,9 @@ end
-- Variant that only re-engages the shutdown timer for onUnexpectedWakeupLimit
function AutoSuspend:_restart()
if self:_enabledShutdown() then
local now_ts = os.time()
logger.dbg("AutoSuspend: restart at", now_ts)
self.last_action_sec = now_ts
local now_tv = UIManager:getTime()
logger.dbg("AutoSuspend: restart at", now_tv:tonumber())
self.last_action_tv = now_tv
self:_schedule(true)
end
end
@ -108,7 +111,7 @@ end
function AutoSuspend:onInputEvent()
logger.dbg("AutoSuspend: onInputEvent")
self.last_action_sec = os.time()
self.last_action_tv = UIManager:getTime()
end
function AutoSuspend:onSuspend()

@ -1,6 +1,7 @@
local Device = require("device")
local Event = require("ui/event")
local PluginShare = require("pluginshare")
local TimeVal = require("ui/timeval")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
@ -10,11 +11,11 @@ local T = require("ffi/util").template
local AutoTurn = WidgetContainer:new{
name = "autoturn",
is_doc_only = true,
autoturn_sec = G_reader_settings:readSetting("autoturn_timeout_seconds") or 0,
autoturn_distance = G_reader_settings:readSetting("autoturn_distance") or 1,
enabled = G_reader_settings:isTrue("autoturn_enabled"),
autoturn_sec = 0,
autoturn_distance = 1,
enabled = false,
settings_id = 0,
last_action_sec = os.time(),
last_action_tv = TimeVal:now(),
}
function AutoTurn:_enabled()
@ -34,7 +35,8 @@ function AutoTurn:_schedule(settings_id)
return
end
local delay = self.last_action_sec + self.autoturn_sec - os.time()
local delay = self.last_action_tv + TimeVal:new{ sec = self.autoturn_sec } - UIManager:getTime()
delay = delay:tonumber()
if delay <= 0 then
if UIManager:getTopWidget() == "ReaderUI" then
@ -57,10 +59,10 @@ end
function AutoTurn:_start()
if self:_enabled() then
local now_ts = os.time()
logger.dbg("AutoTurn: start at", now_ts)
local now_tv = UIManager:getTime()
logger.dbg("AutoTurn: start at", now_tv:tonumber())
PluginShare.pause_auto_suspend = true
self.last_action_sec = now_ts
self.last_action_tv = now_tv
self:_schedule(self.settings_id)
local text
@ -83,7 +85,10 @@ end
function AutoTurn:init()
UIManager.event_hook:registerWidget("InputEvent", self)
self.autoturn_sec = self.settings
self.autoturn_sec = G_reader_settings:readSetting("autoturn_timeout_seconds") or 0
self.autoturn_distance = G_reader_settings:readSetting("autoturn_distance") or 1
self.enabled = G_reader_settings:isTrue("autoturn_enabled")
self.settings_id = 0
self.ui.menu:registerToMainMenu(self)
self:_deprecateLastTask()
self:_start()
@ -96,7 +101,7 @@ end
function AutoTurn:onInputEvent()
logger.dbg("AutoTurn: onInputEvent")
self.last_action_sec = os.time()
self.last_action_tv = UIManager:getTime()
end
-- We do not want autoturn to turn pages during the suspend process.

@ -1,4 +1,5 @@
local logger = require("logger")
local TimeVal = require("ui/timeval")
local UIManager = require("ui/uimanager")
local CommandRunner = {
@ -36,7 +37,7 @@ function CommandRunner:start(job)
assert(self.pio == nil)
assert(self.job == nil)
self.job = job
self.job.start_sec = os.time()
self.job.start_tv = UIManager:getTime()
assert(type(self.job.executable) == "string")
local command = self:createEnvironment() .. " " ..
"sh plugins/backgroundrunner.koplugin/luawrapper.sh " ..
@ -76,7 +77,7 @@ function CommandRunner:poll()
UIManager:allowStandby()
self.pio:close()
self.pio = nil
self.job.end_sec = os.time()
self.job.end_tv = TimeVal:now()
local job = self.job
self.job = nil
return job

@ -9,13 +9,15 @@ end
local CommandRunner = require("commandrunner")
local PluginShare = require("pluginshare")
local TimeVal = require("ui/timeval")
local UIManager = require("ui/uimanager")
local WidgetContainer = require("ui/widget/container/widgetcontainer")
local logger = require("logger")
local _ = require("gettext")
-- BackgroundRunner is an experimental feature to execute non-critical jobs in
-- background. A job is defined as a table in PluginShare.backgroundJobs table.
-- the background.
-- A job is defined as a table in PluginShare.backgroundJobs table.
-- It contains at least following items:
-- when: number, string or function
-- number: the delay in seconds
@ -26,9 +28,9 @@ local _ = require("gettext")
-- executed immediately.
--
-- repeated: boolean or function or nil or number
-- boolean: true to repeated the job once it finished.
-- function: if the return value of the function is true, repeated the job
-- once it finished. If the function throws an error, it equals to
-- boolean: true to repeat the job once it finished.
-- function: if the return value of the function is true, repeat the job
-- once it finishes. If the function throws an error, it equals to
-- return false.
-- nil: same as false.
-- number: times to repeat.
@ -70,9 +72,10 @@ local _ = require("gettext")
-- bad_command: boolean, whether the command is not found. Not available for
-- function executable.
-- blocked: boolean, whether the job is blocked.
-- start_sec: number, the os.time() when the job was started.
-- end_sec: number, the os.time() when the job was stopped.
-- insert_sec: number, the os.time() when the job was inserted into queue.
-- start_tv: number, the TimeVal when the job was started.
-- end_tv: number, the TimeVal when the job was stopped.
-- insert_tv: number, the TimeVal when the job was inserted into queue.
-- (All of them in the monotonic time scale, like the main event loop & task queue).
local BackgroundRunner = {
jobs = PluginShare.backgroundJobs,
@ -114,7 +117,9 @@ end
function BackgroundRunner:_finishJob(job)
assert(self ~= nil)
if type(job.executable) == "function" then
job.timeout = ((job.end_sec - job.start_sec) > 1)
local tv_diff = job.end_tv - job.start_tv
local threshold = TimeVal:new{ sec = 1 }
job.timeout = (tv_diff > threshold)
end
job.blocked = job.timeout
if not job.blocked and self:_shouldRepeat(job) then
@ -136,7 +141,7 @@ function BackgroundRunner:_executeJob(job)
CommandRunner:start(job)
return true
elseif type(job.executable) == "function" then
job.start_sec = os.time()
job.start_tv = UIManager:getTime()
local status, err = pcall(job.executable)
if status then
job.result = 0
@ -144,7 +149,7 @@ function BackgroundRunner:_executeJob(job)
job.result = 1
job.exception = err
end
job.end_sec = os.time()
job.end_tv = TimeVal:now()
self:_finishJob(job)
return true
else
@ -171,10 +176,10 @@ function BackgroundRunner:_execute()
local round = 0
while #self.jobs > 0 do
local job = table.remove(self.jobs, 1)
if job.insert_sec == nil then
-- Jobs are first inserted to jobs table from external users. So
-- they may not have insert_sec field.
job.insert_sec = os.time()
if job.insert_tv == nil then
-- Jobs are first inserted to jobs table from external users.
-- So they may not have an insert field.
job.insert_tv = UIManager:getTime()
end
local should_execute = false
local should_ignore = false
@ -187,7 +192,7 @@ function BackgroundRunner:_execute()
end
elseif type(job.when) == "number" then
if job.when >= 0 then
should_execute = ((os.time() - job.insert_sec) >= job.when)
should_execute = ((UIManager:getTime() - job.insert_tv) >= TimeVal:fromnumber(job.when))
else
should_ignore = true
end
@ -248,7 +253,7 @@ end
function BackgroundRunner:_insert(job)
assert(self ~= nil)
job.insert_sec = os.time()
job.insert_tv = UIManager:getTime()
table.insert(self.jobs, job)
end

@ -8,6 +8,7 @@ of storing it.
@module koplugin.calibre.metadata
--]]--
local TimeVal = require("ui/timeval")
local lfs = require("libs/libkoreader-lfs")
local rapidjson = require("rapidjson")
local logger = require("logger")
@ -232,14 +233,13 @@ end
-- in a given path. It will find calibre files if they're on disk and
-- try to load info from them.
-- NOTE: you should care about the books table, because it could be huge.
-- NOTE: Take special notice of the books table, because it could be huge.
-- If you're not working with the metadata directly (ie: in wireless connections)
-- you should copy relevant data to another table and free this one to keep things tidy.
function CalibreMetadata:init(dir, is_search)
if not dir then return end
local socket = require("socket")
local start = socket.gettime()
local start = TimeVal:now()
self.path = dir
local ok_meta, ok_drive, file_meta, file_drive = findCalibreFiles(dir)
self.driveinfo = file_drive
@ -256,13 +256,13 @@ function CalibreMetadata:init(dir, is_search)
local msg
if is_search then
msg = string.format("(search) in %f milliseconds: %d books",
(socket.gettime() - start) * 1000, #self.books)
msg = string.format("(search) in %.3f milliseconds: %d books",
(TimeVal:now() - start):tomsecs(), #self.books)
else
local deleted_count = self:prune()
self:cleanUnused()
msg = string.format("in %f milliseconds: %d books. %d pruned",
(socket.gettime() - start) * 1000, #self.books, deleted_count)
msg = string.format("in %.3f milliseconds: %d books. %d pruned",
(TimeVal:now() - start):tomsecs(), #self.books, deleted_count)
end
logger.info(string.format("calibre info loaded from disk %s", msg))
return true

@ -16,9 +16,9 @@ local Menu = require("ui/widget/menu")
local Persist = require("persist")
local Screen = require("device").screen
local Size = require("ui/size")
local TimeVal = require("ui/timeval")
local UIManager = require("ui/uimanager")
local logger = require("logger")
local socket = require("socket")
local util = require("util")
local _ = require("gettext")
local T = require("ffi/util").template
@ -323,7 +323,7 @@ function CalibreSearch:find(option)
end
-- measure time elapsed searching
local start = socket.gettime()
local start = TimeVal:now()
if option == "find" then
local books = self:findBooks(self.search_value)
local result = self:bookCatalog(books)
@ -331,9 +331,8 @@ function CalibreSearch:find(option)
else
self:browse(option,1)
end
local elapsed = socket.gettime() - start
logger.info(string.format("search done in %f milliseconds (%s, %s, %s, %s, %s)",
elapsed * 1000,
logger.info(string.format("search done in %.3f milliseconds (%s, %s, %s, %s, %s)",
(TimeVal:now() - start):tomsecs(),
option == "find" and "books" or option,
"case sensitive: " .. tostring(not self.case_insensitive),
"title: " .. tostring(self.find_by_title),
@ -556,8 +555,8 @@ end
-- get metadata from cache or calibre files
function CalibreSearch:getMetadata()
local start = socket.gettime()
local template = "metadata: %d books imported from %s in %f milliseconds"
local start = TimeVal:now()
local template = "metadata: %d books imported from %s in %.3f milliseconds"
-- try to load metadata from cache
if self.cache_metadata then
@ -581,8 +580,7 @@ function CalibreSearch:getMetadata()
end
end
if is_newer then
local elapsed = socket.gettime() - start
logger.info(string.format(template, #cache, "cache", elapsed * 1000))
logger.info(string.format(template, #cache, "cache", (TimeVal:now() - start):tomsecs()))
return cache
else
logger.warn("cache is older than metadata, ignoring it")
@ -607,8 +605,7 @@ function CalibreSearch:getMetadata()
end
self.cache_books:save(serialized_table)
end
local elapsed = socket.gettime() - start
logger.info(string.format(template, #books, "calibre", elapsed * 1000))
logger.info(string.format(template, #books, "calibre", (TimeVal:now() - start):tomsecs()))
return books
end

@ -691,7 +691,6 @@ function BookInfoManager:extractInBackground(files)
local cover_specs = files[idx].cover_specs
logger.dbg(" BG extracting:", filepath)
self:extractBookInfo(filepath, cover_specs)
FFIUtil.usleep(100000) -- give main process 100ms of free cpu to do its processing
end
logger.dbg(" BG extraction done")
end

@ -46,8 +46,6 @@ if lang_locale then
_.changeLang(lang_locale)
end
local dummy = require("ffi/posix_h")
-- Try to turn the C blitter on/off, and synchronize setting so that UI config reflects real state
local bb = require("ffi/blitbuffer")
bb:setUseCBB(is_cbb_enabled)

@ -8,7 +8,7 @@ package.cpath =
-- set search path for 'ffi.load()'
local ffi = require("ffi")
local dummy = require("ffi/posix_h")
require("ffi/posix_h")
local C = ffi.C
if ffi.os == "Windows" then
C._putenv("PATH=libs;common;")

@ -16,6 +16,8 @@ describe("AutoSuspend", function()
UIManager._run_forever = true
G_reader_settings:saveSetting("auto_suspend_timeout_seconds", 10)
require("mock_time"):install()
-- Reset UIManager:getTime()
UIManager:handleInput()
end)
after_each(function()
@ -36,7 +38,6 @@ describe("AutoSuspend", function()
mock_time:increase(6)
UIManager:handleInput()
assert.stub(UIManager.suspend).was.called(1)
mock_time:uninstall()
end)
it("should be able to deprecate last task", function()
@ -56,7 +57,6 @@ describe("AutoSuspend", function()
mock_time:increase(5)
UIManager:handleInput()
assert.stub(UIManager.suspend).was.called(1)
mock_time:uninstall()
end)
end)
@ -74,6 +74,8 @@ describe("AutoSuspend", function()
UIManager._run_forever = true
G_reader_settings:saveSetting("autoshutdown_timeout_seconds", 10)
require("mock_time"):install()
-- Reset UIManager:getTime()
UIManager:handleInput()
end)
after_each(function()
@ -94,7 +96,6 @@ describe("AutoSuspend", function()
mock_time:increase(6)
UIManager:handleInput()
assert.stub(UIManager.poweroff_action).was.called(1)
mock_time:uninstall()
end)
it("should be able to deprecate last task", function()
@ -114,7 +115,6 @@ describe("AutoSuspend", function()
mock_time:increase(5)
UIManager:handleInput()
assert.stub(UIManager.poweroff_action).was.called(1)
mock_time:uninstall()
end)
end)
end)

@ -132,7 +132,7 @@ describe("BackgroundRunner widget tests", function()
table.insert(PluginShare.backgroundJobs, job)
notifyBackgroundJobsUpdated()
while job.end_sec == nil do
while job.end_tv == nil do
MockTime:increase(2)
UIManager:handleInput()
end
@ -157,7 +157,7 @@ describe("BackgroundRunner widget tests", function()
table.insert(PluginShare.backgroundJobs, job)
notifyBackgroundJobsUpdated()
while job.end_sec == nil do
while job.end_tv == nil do
MockTime:increase(2)
UIManager:handleInput()
end
@ -171,11 +171,11 @@ describe("BackgroundRunner widget tests", function()
ENV1 = "yes",
ENV2 = "no",
}
job.end_sec = nil
job.end_tv = nil
table.insert(PluginShare.backgroundJobs, job)
notifyBackgroundJobsUpdated()
while job.end_sec == nil do
while job.end_tv == nil do
MockTime:increase(2)
UIManager:handleInput()
end
@ -206,7 +206,7 @@ describe("BackgroundRunner widget tests", function()
table.insert(PluginShare.backgroundJobs, job)
notifyBackgroundJobsUpdated()
while job.end_sec == nil do
while job.end_tv == nil do
MockTime:increase(2)
UIManager:handleInput()
end
@ -216,12 +216,12 @@ describe("BackgroundRunner widget tests", function()
assert.is_false(job.timeout)
assert.is_false(job.bad_command)
job.end_sec = nil
job.end_tv = nil
env2 = "no"
table.insert(PluginShare.backgroundJobs, job)
notifyBackgroundJobsUpdated()
while job.end_sec == nil do
while job.end_tv == nil do
MockTime:increase(2)
UIManager:handleInput()
end
@ -244,7 +244,7 @@ describe("BackgroundRunner widget tests", function()
table.insert(PluginShare.backgroundJobs, job)
notifyBackgroundJobsUpdated()
while job.end_sec == nil do
while job.end_tv == nil do
MockTime:increase(2)
UIManager:handleInput()
end

@ -94,13 +94,13 @@ describe("device module", function()
type = EV_ABS,
code = ABS_X,
value = y,
time = TimeVal:now(),
time = TimeVal:realtime(),
}
local ev_y = {
type = EV_ABS,
code = ABS_Y,
value = Screen:getWidth()-x,
time = TimeVal:now(),
time = TimeVal:realtime(),
}
kobo_dev.input:eventAdjustHook(ev_x)
@ -273,7 +273,7 @@ describe("device module", function()
mock_ffi_input = require('ffi/input')
stub(mock_ffi_input, "waitForEvent")
mock_ffi_input.waitForEvent.returns({
mock_ffi_input.waitForEvent.returns(true, {
type = 3,
time = {
usec = 450565,

@ -1,36 +1,181 @@
require("commonrequire")
local TimeVal = require("ui/timeval")
local ffi = require("ffi")
local dummy = require("ffi/posix_h")
local logger = require("logger")
local util = require("ffi/util")
local C = ffi.C
local MockTime = {
original_os_time = os.time,
original_util_time = nil,
value = os.time(),
original_tv_realtime = nil,
original_tv_realtime_coarse = nil,
original_tv_monotonic = nil,
original_tv_monotonic_coarse = nil,
original_tv_boottime = nil,
original_tv_now = nil,
monotonic = 0,
realtime = 0,
boottime = 0,
}
function MockTime:install()
assert(self ~= nil)
local util = require("ffi/util")
if self.original_util_time == nil then
self.original_util_time = util.gettime
assert(self.original_util_time ~= nil)
end
if self.original_tv_realtime == nil then
self.original_tv_realtime = TimeVal.realtime
assert(self.original_tv_realtime ~= nil)
end
if self.original_tv_realtime_coarse == nil then
self.original_tv_realtime_coarse = TimeVal.realtime_coarse
assert(self.original_tv_realtime_coarse ~= nil)
end
if self.original_tv_monotonic == nil then
self.original_tv_monotonic = TimeVal.monotonic
assert(self.original_tv_monotonic ~= nil)
end
if self.original_tv_monotonic_coarse == nil then
self.original_tv_monotonic_coarse = TimeVal.monotonic_coarse
assert(self.original_tv_monotonic_coarse ~= nil)
end
if self.original_tv_boottime == nil then
self.original_tv_boottime = TimeVal.boottime
assert(self.original_tv_boottime ~= nil)
end
if self.original_tv_now == nil then
self.original_tv_now = TimeVal.now
assert(self.original_tv_now ~= nil)
end
-- Store both REALTIME & MONOTONIC clocks
self.realtime = os.time()
local timespec = ffi.new("struct timespec")
C.clock_gettime(C.CLOCK_MONOTONIC_COARSE, timespec)
self.monotonic = tonumber(timespec.tv_sec)
os.time = function() --luacheck: ignore
logger.dbg("MockTime:os.time: ", self.value)
return self.value
logger.dbg("MockTime:os.time: ", self.realtime)
return self.realtime
end
util.gettime = function()
logger.dbg("MockTime:util.gettime: ", self.value)
return self.value, 0
logger.dbg("MockTime:util.gettime: ", self.realtime)
return self.realtime, 0
end
TimeVal.realtime = function()
logger.dbg("MockTime:TimeVal.realtime: ", self.realtime)
return TimeVal:new{ sec = self.realtime }
end
TimeVal.realtime_coarse = function()
logger.dbg("MockTime:TimeVal.realtime_coarse: ", self.realtime)
return TimeVal:new{ sec = self.realtime }
end
TimeVal.monotonic = function()
logger.dbg("MockTime:TimeVal.monotonic: ", self.monotonic)
return TimeVal:new{ sec = self.monotonic }
end
TimeVal.monotonic_coarse = function()
logger.dbg("MockTime:TimeVal.monotonic_coarse: ", self.monotonic)
return TimeVal:new{ sec = self.monotonic }
end
TimeVal.boottime = function()
logger.dbg("MockTime:TimeVal.boottime: ", self.boottime)
return TimeVal:new{ sec = self.boottime }
end
TimeVal.now = function()
logger.dbg("MockTime:TimeVal.now: ", self.monotonic)
return TimeVal:new{ sec = self.monotonic }
end
end
function MockTime:uninstall()
assert(self ~= nil)
local util = require("ffi/util")
os.time = self.original_os_time --luacheck: ignore
if self.original_util_time ~= nil then
util.gettime = self.original_util_time
end
if self.original_tv_realtime ~= nil then
TimeVal.realtime = self.original_tv_realtime
end
if self.original_tv_realtime_coarse ~= nil then
TimeVal.realtime_coarse = self.original_tv_realtime_coarse
end
if self.original_tv_monotonic ~= nil then
TimeVal.monotonic = self.original_tv_monotonic
end
if self.original_tv_monotonic_coarse ~= nil then
TimeVal.monotonic_coarse = self.original_tv_monotonic_coarse
end
if self.original_tv_boottime ~= nil then
TimeVal.boottime = self.original_tv_boottime
end
if self.original_tv_now ~= nil then
TimeVal.now = self.original_tv_now
end
end
function MockTime:set_realtime(value)
assert(self ~= nil)
if type(value) ~= "number" then
return false
end
self.realtime = math.floor(value)
logger.dbg("MockTime:set_realtime ", self.realtime)
return true
end
function MockTime:increase_realtime(value)
assert(self ~= nil)
if type(value) ~= "number" then
return false
end
self.realtime = math.floor(self.realtime + value)
logger.dbg("MockTime:increase_realtime ", self.realtime)
return true
end
function MockTime:set_monotonic(value)
assert(self ~= nil)
if type(value) ~= "number" then
return false
end
self.monotonic = math.floor(value)
logger.dbg("MockTime:set_monotonic ", self.monotonic)
return true
end
function MockTime:increase_monotonic(value)
assert(self ~= nil)
if type(value) ~= "number" then
return false
end
self.monotonic = math.floor(self.monotonic + value)
logger.dbg("MockTime:increase_monotonic ", self.monotonic)
return true
end
function MockTime:set_boottime(value)
assert(self ~= nil)
if type(value) ~= "number" then
return false
end
self.boottime = math.floor(value)
logger.dbg("MockTime:set_boottime ", self.boottime)
return true
end
function MockTime:increase_boottime(value)
assert(self ~= nil)
if type(value) ~= "number" then
return false
end
self.boottime = math.floor(self.boottime + value)
logger.dbg("MockTime:increase_boottime ", self.boottime)
return true
end
function MockTime:set(value)
@ -38,8 +183,12 @@ function MockTime:set(value)
if type(value) ~= "number" then
return false
end
self.value = math.floor(value)
logger.dbg("MockTime:set ", self.value)
self.realtime = math.floor(value)
logger.dbg("MockTime:set (realtime) ", self.realtime)
self.monotonic = math.floor(value)
logger.dbg("MockTime:set (monotonic) ", self.monotonic)
self.boottime = math.floor(value)
logger.dbg("MockTime:set (boottime) ", self.boottime)
return true
end
@ -48,8 +197,12 @@ function MockTime:increase(value)
if type(value) ~= "number" then
return false
end
self.value = math.floor(self.value + value)
logger.dbg("MockTime:increase ", self.value)
self.realtime = math.floor(self.realtime + value)
logger.dbg("MockTime:increase (realtime) ", self.realtime)
self.monotonic = math.floor(self.monotonic + value)
logger.dbg("MockTime:increase (monotonic) ", self.monotonic)
self.boottime = math.floor(self.boottime + value)
logger.dbg("MockTime:increase (boottime) ", self.boottime)
return true
end

@ -20,25 +20,42 @@ describe("TimeVal module", function()
local timev2 = TimeVal:new{ sec = 10, usec = 6000}
local timev3 = TimeVal:new{ sec = 10, usec = 50000000}
assert.is.same({sec = 15,usec = 11000}, timev1 + timev2)
assert.is.same({sec = 65,usec = 5000}, timev1 + timev3)
assert.is.same({sec = 15, usec = 11000}, timev1 + timev2)
assert.is.same({sec = 65, usec = 5000}, timev1 + timev3)
end)
it("should subtract", function()
local timev1 = TimeVal:new{ sec = 5, usec = 5000}
local timev2 = TimeVal:new{ sec = 10, usec = 6000}
assert.is.same({sec = 5,usec = 1000}, timev2 - timev1)
assert.is.same({sec = -5,usec = -1000}, timev1 - timev2)
end)
assert.is.same({sec = 5, usec = 1000}, timev2 - timev1)
local backwards_sub = timev1 - timev2
assert.is.same({sec = -6, usec = 999000}, backwards_sub)
it("should guard against reverse subtraction logic", function()
dbg:turnOn()
TimeVal = package.reload("ui/timeval")
local timev1 = TimeVal:new{ sec = 5, usec = 5000}
local timev2 = TimeVal:new{ sec = 10, usec = 5000}
-- Check that to/from float conversions behave, even for negative values.
assert.is.same(-5.001, backwards_sub:tonumber())
assert.is.same({sec = -6, usec = 999000}, TimeVal:fromnumber(-5.001))
local tv = TimeVal:new{ sec = -6, usec = 1000 }
assert.is.same(-5.999, tv:tonumber())
assert.is.same({sec = -6, usec = 1000}, TimeVal:fromnumber(-5.999))
-- We lose precision because of rounding if we go higher resolution than a ms...
tv = TimeVal:new{ sec = -6, usec = 101 }
assert.is.same(-5.9999, tv:tonumber())
assert.is.same({sec = -6, usec = 100}, TimeVal:fromnumber(-5.9999))
-- ^ precision loss
tv = TimeVal:new{ sec = -6, usec = 11 }
assert.is.same(-6, tv:tonumber())
-- ^ precision loss
assert.is.same({sec = -6, usec = 10}, TimeVal:fromnumber(-5.99999))
-- ^ precision loss
assert.has.errors(function() return timev1 - timev2 end)
tv = TimeVal:new{ sec = -6, usec = 1 }
assert.is.same(-6, tv:tonumber())
-- ^ precision loss
assert.is.same({sec = -6, usec = 1}, TimeVal:fromnumber(-5.999999))
end)
it("should derive sec and usec from more than 1 sec worth of usec", function()

@ -1,22 +1,22 @@
describe("UIManager spec", function()
local UIManager, util
local TimeVal, UIManager
local now, wait_until
local noop = function() end
setup(function()
require("commonrequire")
util = require("ffi/util")
TimeVal = require("ui/timeval")
UIManager = require("ui/uimanager")
end)
it("should consume due tasks", function()
now = { util.gettime() }
local future = { now[1] + 60000, now[2] }
local future2 = {future[1] + 5, future[2]}
now = TimeVal:now()
local future = TimeVal:new{ sec = now.sec + 60000, usec = now.usec }
local future2 = TimeVal:new{ sec = future.sec + 5, usec = future.usec}
UIManager:quit()
UIManager._task_queue = {
{ time = {now[1] - 10, now[2] }, action = noop, args = {}, argc = 0 },
{ time = {now[1], now[2] - 5 }, action = noop, args = {}, argc = 0 },
{ time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = noop, args = {}, argc = 0 },
{ time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = noop, args = {}, argc = 0 },
{ time = now, action = noop, args = {}, argc = 0 },
{ time = future, action = noop, args = {}, argc = 0 },
{ time = future2, action = noop, args = {}, argc = 0 },
@ -28,26 +28,26 @@ describe("UIManager spec", function()
end)
it("should calcualte wait_until properly in checkTasks routine", function()
now = { util.gettime() }
local future = { now[1] + 60000, now[2] }
now = TimeVal:now()
local future = TimeVal:new{ sec = now.sec + 60000, usec = now.usec }
UIManager:quit()
UIManager._task_queue = {
{ time = {now[1] - 10, now[2] }, action = noop, args = {}, argc = 0 },
{ time = {now[1], now[2] - 5 }, action = noop, args = {}, argc = 0 },
{ time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = noop, args = {}, argc = 0 },
{ time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = noop, args = {}, argc = 0 },
{ time = now, action = noop, args = {}, argc = 0 },
{ time = future, action = noop, args = {}, argc = 0 },
{ time = {future[1] + 5, future[2]}, action = noop, args = {}, argc = 0 },
{ time = TimeVal:new{ sec = future.sec + 5, usec = future.usec }, action = noop, args = {}, argc = 0 },
}
wait_until, now = UIManager:_checkTasks()
assert.are.same(future, wait_until)
end)
it("should return nil wait_until properly in checkTasks routine", function()
now = { util.gettime() }
now = TimeVal:now()
UIManager:quit()
UIManager._task_queue = {
{ time = {now[1] - 10, now[2] }, action = noop, args = {}, argc = 0 },
{ time = {now[1], now[2] - 5 }, action = noop, args = {}, argc = 0 },
{ time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = noop, args = {}, argc = 0 },
{ time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = noop, args = {}, argc = 0 },
{ time = now, action = noop, args = {}, argc = 0 },
}
wait_until, now = UIManager:_checkTasks()
@ -55,7 +55,7 @@ describe("UIManager spec", function()
end)
it("should insert new task properly in empty task queue", function()
now = { util.gettime() }
now = TimeVal:now()
UIManager:quit()
UIManager._task_queue = {}
assert.are.same(0, #UIManager._task_queue)
@ -65,8 +65,8 @@ describe("UIManager spec", function()
end)
it("should insert new task properly in single task queue", function()
now = { util.gettime() }
local future = { now[1]+10000, now[2] }
now = TimeVal:now()
local future = TimeVal:new{ sec = now.sec + 10000, usec = now.usec }
UIManager:quit()
UIManager._task_queue = {
{ time = future, action = '1', args = {}, argc = 0 },
@ -90,59 +90,59 @@ describe("UIManager spec", function()
end)
it("should insert new task in ascendant order", function()
now = { util.gettime() }
now = TimeVal:now()
UIManager:quit()
UIManager._task_queue = {
{ time = {now[1] - 10, now[2] }, action = '1', args = {}, argc = 0 },
{ time = {now[1], now[2] - 5 }, action = '2', args = {}, argc = 0 },
{ time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = '1', args = {}, argc = 0 },
{ time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = '2', args = {}, argc = 0 },
{ time = now, action = '3', args = {}, argc = 0 },
}
-- insert into the tail slot
UIManager:scheduleIn(10, 'foo')
assert.are.same('foo', UIManager._task_queue[4].action)
-- insert into the second slot
UIManager:schedule({now[1]-5, now[2]}, 'bar')
UIManager:schedule(TimeVal:new{ sec = now.sec - 5, usec = now.usec }, 'bar')
assert.are.same('bar', UIManager._task_queue[2].action)
-- insert into the head slot
UIManager:schedule({now[1]-15, now[2]}, 'baz')
UIManager:schedule(TimeVal:new{ sec = now.sec - 15, usec = now.usec }, 'baz')
assert.are.same('baz', UIManager._task_queue[1].action)
-- insert into the last second slot
UIManager:scheduleIn(5, 'qux')
assert.are.same('qux', UIManager._task_queue[6].action)
-- insert into the middle slot
UIManager:schedule({now[1], now[2]-1}, 'quux')
UIManager:schedule(TimeVal:new{ sec = now.sec, usec = now.usec - 1 }, 'quux')
assert.are.same('quux', UIManager._task_queue[5].action)
end)
it("should unschedule all the tasks with the same action", function()
now = { util.gettime() }
now = TimeVal:now()
UIManager:quit()
UIManager._task_queue = {
{ time = {now[1] - 15, now[2] }, action = '3', args = {}, argc = 0 },
{ time = {now[1] - 10, now[2] }, action = '1', args = {}, argc = 0 },
{ time = {now[1], now[2] - 6 }, action = '3', args = {}, argc = 0 },
{ time = {now[1], now[2] - 5 }, action = '2', args = {}, argc = 0 },
{ time = TimeVal:new{ sec = now.sec - 15, usec = now.usec }, action = '3', args = {}, argc = 0 },
{ time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = '1', args = {}, argc = 0 },
{ time = TimeVal:new{ sec = now.sec, usec = now.usec - 6 }, action = '3', args = {}, argc = 0 },
{ time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = '2', args = {}, argc = 0 },
{ time = now, action = '3', args = {}, argc = 0 },
}
-- insert into the tail slot
UIManager:unschedule('3')
assert.are.same({
{ time = {now[1] - 10, now[2] }, action = '1', args = {}, argc = 0 },
{ time = {now[1], now[2] - 5 }, action = '2', args = {}, argc = 0 },
{ time = TimeVal:new{ sec = now.sec - 10, usec = now.usec }, action = '1', args = {}, argc = 0 },
{ time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = '2', args = {}, argc = 0 },
}, UIManager._task_queue)
end)
it("should not have race between unschedule and _checkTasks", function()
now = { util.gettime() }
now = TimeVal:now()
local run_count = 0
local task_to_remove = function()
run_count = run_count + 1
end
UIManager:quit()
UIManager._task_queue = {
{ time = { now[1], now[2]-5 }, action = task_to_remove, args = {}, argc = 0 },
{ time = TimeVal:new{ sec = now.sec, usec = now.usec - 5 }, action = task_to_remove, args = {}, argc = 0 },
{
time = { now[1]-10, now[2] },
time = TimeVal:new{ sec = now.sec - 10, usec = now.usec },
action = function()
run_count = run_count + 1
UIManager:unschedule(task_to_remove)

Loading…
Cancel
Save