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 MultiConfirmBox = require("ui/widget/multiconfirmbox")
local NetworkMgr = require("ui/network/manager") local NetworkMgr = require("ui/network/manager")
local SortWidget = require("ui/widget/sortwidget") local SortWidget = require("ui/widget/sortwidget")
local TimeVal = require("ui/timeval")
local Trapper = require("ui/trapper") local Trapper = require("ui/trapper")
local UIManager = require("ui/uimanager") local UIManager = require("ui/uimanager")
local ffiUtil = require("ffi/util") local ffiUtil = require("ffi/util")
@ -109,7 +110,7 @@ function ReaderDictionary:init()
-- Allow quick interruption or dismiss of search result window -- Allow quick interruption or dismiss of search result window
-- with tap if done before this delay. After this delay, the -- with tap if done before this delay. After this delay, the
-- result window is shown and dismiss prevented for a few 100ms -- 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 -- Gather info about available dictionaries
if not available_ifos then 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: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) 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 -- If interrupted quickly just after launch, don't display anything
-- (this might help avoiding refreshes and the need to dismiss -- (this might help avoiding refreshes and the need to dismiss
-- after accidental long-press when holding a device). -- after accidental long-press when holding a device).
@ -907,7 +908,7 @@ function ReaderDictionary:showDict(word, results, box, link)
self:dismissLookupInfo() self:dismissLookupInfo()
if results and results[1] then if results and results[1] then
UIManager:show(self.dict_window) 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 -- If the search took more than a few seconds to be done, discard
-- queued and coming up events to avoid a voluntary dismissal -- queued and coming up events to avoid a voluntary dismissal
-- (because the user felt the result would not come) to kill the -- (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 -- to ensure current highlight has not already been cleared, and that we
-- are not going to clear a new highlight -- are not going to clear a new highlight
function ReaderHighlight:getClearId() 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 return self.clear_id
end end
function ReaderHighlight:clear(clear_id) function ReaderHighlight:clear(clear_id)
if clear_id then -- should be provided by delayed call to clear() if clear_id then -- should be provided by delayed call to clear()
if clear_id ~= self.clear_id then 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 -- cleared since this clear_id was given
return return
end end
@ -681,7 +681,7 @@ function ReaderHighlight:_resetHoldTimer(clear)
if clear then if clear then
self.hold_last_tv = nil self.hold_last_tv = nil
else else
self.hold_last_tv = TimeVal.now() self.hold_last_tv = UIManager:getTime()
end end
end end
@ -1124,9 +1124,8 @@ end
function ReaderHighlight:onHoldRelease() function ReaderHighlight:onHoldRelease()
local long_final_hold = false local long_final_hold = false
if self.hold_last_tv then if self.hold_last_tv then
local hold_duration = TimeVal.now() - self.hold_last_tv local hold_duration = UIManager:getTime() - self.hold_last_tv
hold_duration = hold_duration.sec + hold_duration.usec/1000000 if hold_duration > TimeVal:new{ sec = 3 } then
if hold_duration > 3.0 then
-- We stayed 3 seconds before release without updating selection -- We stayed 3 seconds before release without updating selection
long_final_hold = true long_final_hold = true
end end

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

@ -143,6 +143,8 @@ function Device:init()
or ev.code == C.APP_CMD_INIT_WINDOW or ev.code == C.APP_CMD_INIT_WINDOW
or ev.code == C.APP_CMD_WINDOW_REDRAW_NEEDED then or ev.code == C.APP_CMD_WINDOW_REDRAW_NEEDED then
this.device.screen:_updateWindow() 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 elseif ev.code == C.APP_CMD_CONFIG_CHANGED then
-- orientation and size changes -- orientation and size changes
if android.screen.width ~= android.getScreenWidth() if android.screen.width ~= android.getScreenWidth()

@ -47,6 +47,11 @@ local TimeVal = require("ui/timeval")
local logger = require("logger") local logger = require("logger")
local util = require("util") 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) -- default values (all the time parameters are in microseconds)
local TAP_INTERVAL = 0 * 1000 local TAP_INTERVAL = 0 * 1000
local DOUBLE_TAP_INTERVAL = 300 * 1000 local DOUBLE_TAP_INTERVAL = 300 * 1000
@ -56,11 +61,17 @@ local PAN_DELAYED_INTERVAL = 500 * 1000
local SWIPE_INTERVAL = 900 * 1000 local SWIPE_INTERVAL = 900 * 1000
-- current values -- current values
local ges_tap_interval = G_reader_settings:readSetting("ges_tap_interval") or TAP_INTERVAL 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 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 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 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 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 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 = { local GestureDetector = {
-- must be initialized with the Input singleton class -- must be initialized with the Input singleton class
@ -85,7 +96,7 @@ local GestureDetector = {
}, },
-- states are stored in separated slots -- states are stored in separated slots
states = {}, states = {},
hold_timer_id = {}, pending_hold_timer = {},
track_ids = {}, track_ids = {},
tev_stacks = {}, tev_stacks = {},
-- latest feeded touch event in each slots -- latest feeded touch event in each slots
@ -97,6 +108,8 @@ local GestureDetector = {
detectings = {}, detectings = {},
-- for single/double tap -- for single/double tap
last_taps = {}, last_taps = {},
-- for timestamp clocksource detection
clock_id = nil,
} }
function GestureDetector:new(o) function GestureDetector:new(o)
@ -155,23 +168,42 @@ end
tap2 is the later tap tap2 is the later tap
--]] --]]
function GestureDetector:isTapBounce(tap1, tap2, interval) 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 local tv_diff = tap2.timev - tap1.timev
if not tv_diff:isPositive() then
tv_diff = TimeVal:new{ sec = math.huge }
end
return ( return (
math.abs(tap1.x - tap2.x) < self.SINGLE_TAP_BOUNCE_DISTANCE and math.abs(tap1.x - tap2.x) < self.SINGLE_TAP_BOUNCE_DISTANCE and
math.abs(tap1.y - tap2.y) < 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 end
function GestureDetector:isDoubleTap(tap1, tap2) function GestureDetector:isDoubleTap(tap1, tap2)
local tv_diff = tap2.timev - tap1.timev local tv_diff = tap2.timev - tap1.timev
if not tv_diff:isPositive() then
tv_diff = TimeVal:new{ sec = math.huge }
end
return ( return (
math.abs(tap1.x - tap2.x) < self.DOUBLE_TAP_DISTANCE and math.abs(tap1.x - tap2.x) < self.DOUBLE_TAP_DISTANCE and
math.abs(tap1.y - tap2.y) < 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 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() function GestureDetector:isTwoFingerTap()
if self.last_tevs[0] == nil or self.last_tevs[1] == nil then if self.last_tevs[0] == nil or self.last_tevs[1] == nil then
return false 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_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 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 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 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 ( return (
x_diff0 < self.TWO_FINGER_TAP_REGION and x_diff0 < self.TWO_FINGER_TAP_REGION and
x_diff1 < self.TWO_FINGER_TAP_REGION and x_diff1 < self.TWO_FINGER_TAP_REGION and
y_diff0 < self.TWO_FINGER_TAP_REGION and y_diff0 < self.TWO_FINGER_TAP_REGION and
y_diff1 < 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_diff0 < ges_two_finger_tap_duration and
tv_diff1.sec == 0 and tv_diff1.usec < ges_two_finger_tap_duration tv_diff1 < ges_two_finger_tap_duration
) )
end end
@ -227,7 +265,10 @@ end
function GestureDetector:isSwipe(slot) function GestureDetector:isSwipe(slot)
if not self.first_tevs[slot] or not self.last_tevs[slot] then return end 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 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 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 local y_diff = self.last_tevs[slot].y - self.first_tevs[slot].y
if x_diff ~= 0 or y_diff ~= 0 then if x_diff ~= 0 or y_diff ~= 0 then
@ -254,49 +295,54 @@ end
function GestureDetector:clearState(slot) function GestureDetector:clearState(slot)
self.states[slot] = self.initialState self.states[slot] = self.initialState
self.hold_timer_id[slot] = nil self.pending_hold_timer[slot] = nil
self.detectings[slot] = false self.detectings[slot] = false
self.first_tevs[slot] = nil self.first_tevs[slot] = nil
self.last_tevs[slot] = nil self.last_tevs[slot] = nil
self.multiswipe_directions = {} self.multiswipe_directions = {}
self.multiswipe_type = nil 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 end
function GestureDetector:setNewInterval(type, interval) function GestureDetector:setNewInterval(type, interval)
if type == "ges_tap_interval" then if type == "ges_tap_interval" then
ges_tap_interval = interval ges_tap_interval = TimeVal:new{ usec = interval }
elseif type == "ges_double_tap_interval" then 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 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 elseif type == "ges_hold_interval" then
ges_hold_interval = interval ges_hold_interval = TimeVal:new{ usec = interval }
elseif type == "ges_pan_delayed_interval" then 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 elseif type == "ges_swipe_interval" then
ges_swipe_interval = interval ges_swipe_interval = TimeVal:new{ usec = interval }
end end
end end
function GestureDetector:getInterval(type) function GestureDetector:getInterval(type)
if type == "ges_tap_interval" then if type == "ges_tap_interval" then
return ges_tap_interval return ges_tap_interval:tousecs()
elseif type == "ges_double_tap_interval" then 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 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 elseif type == "ges_hold_interval" then
return ges_hold_interval return ges_hold_interval:tousecs()
elseif type == "ges_pan_delayed_interval" then elseif type == "ges_pan_delayed_interval" then
return ges_pan_delayed_interval return ges_pan_delayed_interval:tousecs()
elseif type == "ges_swipe_interval" then elseif type == "ges_swipe_interval" then
return ges_swipe_interval return ges_swipe_interval:tousecs()
end end
end end
function GestureDetector:clearStates() function GestureDetector:clearStates()
self:clearState(0) for k, _ in pairs(self.states) do
self:clearState(1) self:clearState(k)
end
end end
function GestureDetector:initialState(tev) function GestureDetector:initialState(tev)
@ -320,10 +366,61 @@ function GestureDetector:initialState(tev)
end end
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. Handles both single and double tap.
--]] --]]
function GestureDetector:tapState(tev) 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...") logger.dbg("in tap state...")
local slot = tev.slot local slot = tev.slot
if tev.id == -1 then 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 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 -- 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) -- 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") logger.dbg("tap bounce detected in slot", slot, ": ignored")
-- Simply ignore it, and clear state as this is the end of a touch event -- 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 -- (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 self:isDoubleTap(self.last_taps[slot], cur_tap) then
-- it is a double tap -- it is a double tap
self:clearState(slot) self:clearState(slot)
self.input:clearTimeout(slot, "double_tap")
ges_ev.ges = "double_tap" ges_ev.ges = "double_tap"
self.last_taps[slot] = nil self.last_taps[slot] = nil
logger.dbg("double tap detected in slot", slot) 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 -- 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. -- a timer if no second tap happened in the double tap delay.
logger.dbg("set up single/double tap timer") logger.dbg("set up single/double tap timer")
-- deadline should be calculated by adding current tap time and the interval -- setTimeout will handle computing the deadline in the least lossy way possible given the platform.
-- (No need to compute self._has_real_clock_time_ev_time here, we should always self.input:setTimeout(slot, "double_tap", function()
-- have been thru handleNonTap() where it is computed, before getting here) logger.dbg("in single/double tap timer, single tap:", self.last_taps[slot] ~= nil)
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)
-- double tap will set last_tap to nil so if it is not, then -- 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 -- user has not double-tap'ed: it's a single tap
if self.last_taps[slot] ~= nil then 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) logger.dbg("single tap detected in slot", slot, ges_ev.pos)
return ges_ev return ges_ev
end end
end, deadline) end, tev.timev, ges_double_tap_interval)
-- we are already at the end of touch event -- we are already at the end of touch event
-- so reset the state -- so reset the state
self:clearState(slot) self:clearState(slot)
@ -461,34 +552,21 @@ function GestureDetector:handleNonTap(tev)
-- switched from other state, probably from initialState -- switched from other state, probably from initialState
-- we return nil in this case -- we return nil in this case
self.states[slot] = self.tapState 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") logger.dbg("set up hold timer")
local ref_time = self._has_real_clock_time_ev_time and tev.timev or TimeVal:now() -- Invalidate previous hold timers on that slot so that the following setTimeout will only react to *this* tapState.
local deadline = ref_time + TimeVal:new{ self.input:clearTimeout(slot, "hold")
sec = 0, usec = ges_hold_interval self.pending_hold_timer[slot] = true
} self.input:setTimeout(slot, "hold", function()
-- Be sure the following setTimeout only react to this tapState -- If the pending_hold_timer we set on our first switch to tapState on this slot (e.g., first finger down event),
local hold_timer_id = tev.timev -- back when the timer was setup, is still relevant (e.g., the slot wasn't run through clearState by a finger up gesture),
self.hold_timer_id[slot] = hold_timer_id -- then check that we're still in a stationary finger down state (e.g., tapState).
self.input:setTimeout(function() if self.pending_hold_timer[slot] and self.states[slot] == self.tapState then
if self.states[slot] == self.tapState and self.hold_timer_id[slot] == hold_timer_id then -- That means we can switch to hold
-- timer set in tapState, so we switch to hold
logger.dbg("hold gesture detected in slot", slot) logger.dbg("hold gesture detected in slot", slot)
self.pending_hold_timer[slot] = nil
return self:switchState("holdState", tev, true) return self:switchState("holdState", tev, true)
end end
end, deadline) end, tev.timev, ges_hold_interval)
return { return {
ges = "touch", ges = "touch",
pos = Geom:new{ pos = Geom:new{
@ -499,12 +577,10 @@ function GestureDetector:handleNonTap(tev)
time = tev.timev, time = tev.timev,
} }
else else
-- it is not end of touch event, see if we need to switch to -- We're still inside a stream of input events, see if we need to switch to other states.
-- other states
if (tev.x and math.abs(tev.x - self.first_tevs[slot].x) >= self.PAN_THRESHOLD) or 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 (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 -- If user's finger moved far enough on the X or Y axes, switch to pan state.
-- Y distance, we switch to pan state
return self:switchState("panState", tev) return self:switchState("panState", tev)
end end
end end
@ -594,6 +670,9 @@ function GestureDetector:handlePan(tev)
else else
local pan_direction, pan_distance = self:getPath(slot) local pan_direction, pan_distance = self:getPath(slot)
local tv_diff = self.last_tevs[slot].timev - self.first_tevs[slot].timev 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 = { local pan_ev = {
ges = "pan", ges = "pan",
@ -620,7 +699,7 @@ function GestureDetector:handlePan(tev)
-- delayed pan, used where necessary to reduce potential activation of panning -- delayed pan, used where necessary to reduce potential activation of panning
-- when swiping is intended (e.g., for the menu or for multiswipe) -- 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.x = tev.x - self.first_tevs[slot].x
pan_ev.relative_delayed.y = tev.y - self.first_tevs[slot].y pan_ev.relative_delayed.y = tev.y - self.first_tevs[slot].y
pan_ev.distance_delayed = pan_distance pan_ev.distance_delayed = pan_distance
@ -774,7 +853,7 @@ end
function GestureDetector:holdState(tev, hold) function GestureDetector:holdState(tev, hold)
logger.dbg("in hold state...") logger.dbg("in hold state...")
local slot = tev.slot 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 if tev.id ~= -1 and hold and self.last_tevs[slot].x and self.last_tevs[slot].y then
self.states[slot] = self.holdState self.states[slot] = self.holdState
return { return {

@ -13,44 +13,22 @@ local input = require("ffi/input")
local logger = require("logger") local logger = require("logger")
local _ = require("gettext") 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: push
-- luacheck: ignore -- 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) -- key press event values (KEY.value)
local EVENT_VALUE_KEY_PRESS = 1 local EVENT_VALUE_KEY_PRESS = 1
local EVENT_VALUE_KEY_REPEAT = 2 local EVENT_VALUE_KEY_REPEAT = 2
local EVENT_VALUE_KEY_RELEASE = 0 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) -- For Kindle Oasis orientation events (ABS.code)
-- the ABS code of orientation event will be adjusted to -24 from 24(ABS_PRESSURE) -- the ABS code of orientation event will be adjusted to -24 from 24 (C.ABS_PRESSURE)
-- as ABS_PRESSURE is also used to detect touch input in KOBO devices. -- as C.ABS_PRESSURE is also used to detect touch input in KOBO devices.
local ABS_OASIS_ORIENTATION = -24 local ABS_OASIS_ORIENTATION = -24
local DEVICE_ORIENTATION_PORTRAIT_LEFT = 15 local DEVICE_ORIENTATION_PORTRAIT_LEFT = 15
local DEVICE_ORIENTATION_PORTRAIT_RIGHT = 17 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 = 21
local DEVICE_ORIENTATION_LANDSCAPE_ROTATED = 22 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) -- For the events of the Forma accelerometer (MSC.value)
local MSC_RAW_GSENSOR_PORTRAIT_DOWN = 0x17 local MSC_RAW_GSENSOR_PORTRAIT_DOWN = 0x17
local MSC_RAW_GSENSOR_PORTRAIT_UP = 0x18 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_BACK = 0x1b
local MSC_RAW_GSENSOR_FRONT = 0x1c 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 -- luacheck: pop
local _internal_clipboard_text = nil -- holds the last copied text local _internal_clipboard_text = nil -- holds the last copied text
@ -236,72 +261,148 @@ end
--- Catalog of predefined hooks. --- Catalog of predefined hooks.
function Input:adjustTouchSwitchXY(ev) function Input:adjustTouchSwitchXY(ev)
if ev.type == EV_ABS then if ev.type == C.EV_ABS then
if ev.code == ABS_X then if ev.code == C.ABS_X then
ev.code = ABS_Y ev.code = C.ABS_Y
elseif ev.code == ABS_Y then elseif ev.code == C.ABS_Y then
ev.code = ABS_X ev.code = C.ABS_X
elseif ev.code == ABS_MT_POSITION_X then elseif ev.code == C.ABS_MT_POSITION_X then
ev.code = ABS_MT_POSITION_Y ev.code = C.ABS_MT_POSITION_Y
elseif ev.code == ABS_MT_POSITION_Y then elseif ev.code == C.ABS_MT_POSITION_Y then
ev.code = ABS_MT_POSITION_X ev.code = C.ABS_MT_POSITION_X
end end
end end
end end
function Input:adjustTouchScale(ev, by) function Input:adjustTouchScale(ev, by)
if ev.type == EV_ABS then if ev.type == C.EV_ABS then
if ev.code == ABS_X or ev.code == ABS_MT_POSITION_X then if ev.code == C.ABS_X or ev.code == C.ABS_MT_POSITION_X then
ev.value = by.x * ev.value ev.value = by.x * ev.value
end 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 ev.value = by.y * ev.value
end end
end end
end end
function Input:adjustTouchMirrorX(ev, width) function Input:adjustTouchMirrorX(ev, width)
if ev.type == EV_ABS if ev.type == C.EV_ABS
and (ev.code == ABS_X or ev.code == ABS_MT_POSITION_X) then and (ev.code == C.ABS_X or ev.code == C.ABS_MT_POSITION_X) then
ev.value = width - ev.value ev.value = width - ev.value
end end
end end
function Input:adjustTouchMirrorY(ev, height) function Input:adjustTouchMirrorY(ev, height)
if ev.type == EV_ABS if ev.type == C.EV_ABS
and (ev.code == ABS_Y or ev.code == ABS_MT_POSITION_Y) then and (ev.code == C.ABS_Y or ev.code == C.ABS_MT_POSITION_Y) then
ev.value = height - ev.value ev.value = height - ev.value
end end
end end
function Input:adjustTouchTranslate(ev, by) function Input:adjustTouchTranslate(ev, by)
if ev.type == EV_ABS then if ev.type == C.EV_ABS then
if ev.code == ABS_X or ev.code == ABS_MT_POSITION_X then if ev.code == C.ABS_X or ev.code == C.ABS_MT_POSITION_X then
ev.value = by.x + ev.value ev.value = by.x + ev.value
end 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 ev.value = by.y + ev.value
end end
end end
end end
function Input:adjustKindleOasisOrientation(ev) 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 ev.code = ABS_OASIS_ORIENTATION
end end
end end
function Input:setTimeout(cb, tv_out) function Input:setTimeout(slot, ges, cb, origin, delay)
local item = { local item = {
slot = slot,
gesture = ges,
callback = cb, 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.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 return v1.deadline < v2.deadline
end) end)
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) function Input:handleKeyBoardEv(ev)
local keycode = self.event_map[ev.code] local keycode = self.event_map[ev.code]
if not keycode then if not keycode then
@ -434,7 +535,7 @@ From kernel document:
For type B devices, the kernel driver should associate a slot with each 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. identified contact, and use that slot to propagate changes for the contact.
Creation, replacement and destruction of contacts is achieved by modifying 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 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 tracking id not previously present is considered new, and a tracking id no
longer present is considered removed. Since only changes are propagated, 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. attribute of the current slot.
--]] --]]
function Input:handleTouchEv(ev) function Input:handleTouchEv(ev)
if ev.type == EV_ABS then if ev.type == C.EV_ABS then
if #self.MTSlots == 0 then if #self.MTSlots == 0 then
table.insert(self.MTSlots, self:getMtSlot(self.cur_slot)) table.insert(self.MTSlots, self:getMtSlot(self.cur_slot))
end end
if ev.code == ABS_MT_SLOT then if ev.code == C.ABS_MT_SLOT then
self:addSlotIfChanged(ev.value) 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 if self.snow_protocol then
self:addSlotIfChanged(ev.value) self:addSlotIfChanged(ev.value)
end end
self:setCurrentMtSlot("id", ev.value) 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) 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) self:setCurrentMtSlot("y", ev.value)
-- code to emulate mt protocol on kobos -- code to emulate mt protocol on kobos
-- we "confirm" abs_x, abs_y only when pressure ~= 0 -- 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) 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) self:setCurrentMtSlot("abs_y", ev.value)
elseif ev.code == ABS_PRESSURE then elseif ev.code == C.ABS_PRESSURE then
if ev.value ~= 0 then if ev.value ~= 0 then
self:setCurrentMtSlot("id", 1) self:setCurrentMtSlot("id", 1)
self:confirmAbsxy() self:confirmAbsxy()
@ -474,8 +575,8 @@ function Input:handleTouchEv(ev)
self:setCurrentMtSlot("id", -1) self:setCurrentMtSlot("id", -1)
end end
end end
elseif ev.type == EV_SYN then elseif ev.type == C.EV_SYN then
if ev.code == SYN_REPORT then if ev.code == C.SYN_REPORT then
for _, MTSlot in pairs(self.MTSlots) do for _, MTSlot in pairs(self.MTSlots) do
self:setMtSlot(MTSlot.slot, "timev", TimeVal:new(ev.time)) self:setMtSlot(MTSlot.slot, "timev", TimeVal:new(ev.time))
if self.snow_protocol then if self.snow_protocol then
@ -517,49 +618,49 @@ function Input:handleTouchEvPhoenix(ev)
-- Hack on handleTouchEV for the Kobo Aura -- Hack on handleTouchEV for the Kobo Aura
-- It seems to be using a custom protocol: -- It seems to be using a custom protocol:
-- finger 0 down: -- finger 0 down:
-- input_report_abs(elan_touch_data.input, ABS_MT_TRACKING_ID, 0); -- input_report_abs(elan_touch_data.input, C.ABS_MT_TRACKING_ID, 0);
-- input_report_abs(elan_touch_data.input, ABS_MT_TOUCH_MAJOR, 1); -- input_report_abs(elan_touch_data.input, C.ABS_MT_TOUCH_MAJOR, 1);
-- input_report_abs(elan_touch_data.input, ABS_MT_WIDTH_MAJOR, 1); -- input_report_abs(elan_touch_data.input, C.ABS_MT_WIDTH_MAJOR, 1);
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_X, x1); -- input_report_abs(elan_touch_data.input, C.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_POSITION_Y, y1);
-- input_mt_sync (elan_touch_data.input); -- input_mt_sync (elan_touch_data.input);
-- finger 1 down: -- finger 1 down:
-- input_report_abs(elan_touch_data.input, ABS_MT_TRACKING_ID, 1); -- input_report_abs(elan_touch_data.input, C.ABS_MT_TRACKING_ID, 1);
-- input_report_abs(elan_touch_data.input, ABS_MT_TOUCH_MAJOR, 1); -- input_report_abs(elan_touch_data.input, C.ABS_MT_TOUCH_MAJOR, 1);
-- input_report_abs(elan_touch_data.input, ABS_MT_WIDTH_MAJOR, 1); -- input_report_abs(elan_touch_data.input, C.ABS_MT_WIDTH_MAJOR, 1);
-- input_report_abs(elan_touch_data.input, ABS_MT_POSITION_X, x2); -- input_report_abs(elan_touch_data.input, C.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_POSITION_Y, y2);
-- input_mt_sync (elan_touch_data.input); -- input_mt_sync (elan_touch_data.input);
-- finger 0 up: -- finger 0 up:
-- input_report_abs(elan_touch_data.input, ABS_MT_TRACKING_ID, 0); -- input_report_abs(elan_touch_data.input, C.ABS_MT_TRACKING_ID, 0);
-- input_report_abs(elan_touch_data.input, ABS_MT_TOUCH_MAJOR, 0); -- input_report_abs(elan_touch_data.input, C.ABS_MT_TOUCH_MAJOR, 0);
-- input_report_abs(elan_touch_data.input, ABS_MT_WIDTH_MAJOR, 0); -- input_report_abs(elan_touch_data.input, C.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, C.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_POSITION_Y, last_y);
-- input_mt_sync (elan_touch_data.input); -- input_mt_sync (elan_touch_data.input);
-- finger 1 up: -- finger 1 up:
-- input_report_abs(elan_touch_data.input, ABS_MT_TRACKING_ID, 1); -- input_report_abs(elan_touch_data.input, C.ABS_MT_TRACKING_ID, 1);
-- input_report_abs(elan_touch_data.input, ABS_MT_TOUCH_MAJOR, 0); -- input_report_abs(elan_touch_data.input, C.ABS_MT_TOUCH_MAJOR, 0);
-- input_report_abs(elan_touch_data.input, ABS_MT_WIDTH_MAJOR, 0); -- input_report_abs(elan_touch_data.input, C.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, C.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_POSITION_Y, last_y2);
-- input_mt_sync (elan_touch_data.input); -- 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 if #self.MTSlots == 0 then
table.insert(self.MTSlots, self:getMtSlot(self.cur_slot)) table.insert(self.MTSlots, self:getMtSlot(self.cur_slot))
end end
if ev.code == ABS_MT_TRACKING_ID then if ev.code == C.ABS_MT_TRACKING_ID then
self:addSlotIfChanged(ev.value) self:addSlotIfChanged(ev.value)
self:setCurrentMtSlot("id", 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) 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) 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) self:setCurrentMtSlot("y", ev.value)
end end
elseif ev.type == EV_SYN then elseif ev.type == C.EV_SYN then
if ev.code == SYN_REPORT then if ev.code == C.SYN_REPORT then
for _, MTSlot in pairs(self.MTSlots) do for _, MTSlot in pairs(self.MTSlots) do
self:setMtSlot(MTSlot.slot, "timev", TimeVal:new(ev.time)) self:setMtSlot(MTSlot.slot, "timev", TimeVal:new(ev.time))
end end
@ -578,23 +679,23 @@ end
function Input:handleTouchEvLegacy(ev) function Input:handleTouchEvLegacy(ev)
-- Single Touch Protocol. Some devices emit both singletouch and multitouch events. -- 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. -- 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 if #self.MTSlots == 0 then
table.insert(self.MTSlots, self:getMtSlot(self.cur_slot)) table.insert(self.MTSlots, self:getMtSlot(self.cur_slot))
end end
if ev.code == ABS_X then if ev.code == C.ABS_X then
self:setCurrentMtSlot("x", ev.value) self:setCurrentMtSlot("x", ev.value)
elseif ev.code == ABS_Y then elseif ev.code == C.ABS_Y then
self:setCurrentMtSlot("y", ev.value) self:setCurrentMtSlot("y", ev.value)
elseif ev.code == ABS_PRESSURE then elseif ev.code == C.ABS_PRESSURE then
if ev.value ~= 0 then if ev.value ~= 0 then
self:setCurrentMtSlot("id", 1) self:setCurrentMtSlot("id", 1)
else else
self:setCurrentMtSlot("id", -1) self:setCurrentMtSlot("id", -1)
end end
end end
elseif ev.type == EV_SYN then elseif ev.type == C.EV_SYN then
if ev.code == SYN_REPORT then if ev.code == C.SYN_REPORT then
for _, MTSlot in pairs(self.MTSlots) do for _, MTSlot in pairs(self.MTSlots) do
self:setMtSlot(MTSlot.slot, "timev", TimeVal:new(ev.time)) self:setMtSlot(MTSlot.slot, "timev", TimeVal:new(ev.time))
end end
@ -651,7 +752,7 @@ end
--- Accelerometer on the Forma, c.f., drivers/hwmon/mma8x5x.c --- Accelerometer on the Forma, c.f., drivers/hwmon/mma8x5x.c
function Input:handleMiscEvNTX(ev) function Input:handleMiscEvNTX(ev)
local rotation_mode, screen_mode 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 if ev.value == MSC_RAW_GSENSOR_PORTRAIT_UP then
-- i.e., UR -- i.e., UR
rotation_mode = framebuffer.ORIENTATION_PORTRAIT rotation_mode = framebuffer.ORIENTATION_PORTRAIT
@ -774,90 +875,198 @@ end
--- Main event handling. --- 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 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 while true do
if #self.timer_callbacks > 0 then if #self.timer_callbacks > 0 then
local wait_deadline = TimeVal:now() + TimeVal:new{ -- If we have timers set, we need to honor them once we're done draining the input events.
usec = timeout_us
}
-- we don't block if there is any timer, set wait to 10us
while #self.timer_callbacks > 0 do 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 if ok then break end
local tv_now = TimeVal:now()
if (not timeout_us or tv_now < wait_deadline) then -- If we've drained all pending input events, causing waitForEvent to time out, check our timers
-- check whether timer is up if ok == false and ev == C.ETIME then
if tv_now >= self.timer_callbacks[1].deadline 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() local touch_ges = self.timer_callbacks[1].callback()
table.remove(self.timer_callbacks, 1) 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 if touch_ges then
-- Do we really need to clear all setTimeout after -- The timers we'll encounter are for finalizing a hold or (if enabled) double tap gesture,
-- decided a gesture? FIXME -- as such, it makes no sense to try to detect *multiple* subsequent gestures.
self.timer_callbacks = {} -- This is why we clear the full list of timers on the first match ;).
self:clearTimeouts()
self:gestureAdjustHook(touch_ges) self:gestureAdjustHook(touch_ges)
return Event:new("Gesture", return Event:new("Gesture",
self.gesture_detector:adjustGesCoordinate(touch_ges) self.gesture_detector:adjustGesCoordinate(touch_ges)
) )
end -- EOF if touch_ges end -- if touch_ges
end -- EOF if deadline reached end -- if poll_deadline reached
else end -- if poll returned ETIME
break
end -- EOF if not exceed wait timeout -- Refresh now on the next iteration (e.g., when we have multiple timers to check)
now = nil
end -- while #timer_callbacks > 0 end -- while #timer_callbacks > 0
else else
ok, ev = pcall(input.waitForEvent, timeout_us) -- If there aren't any timers, just block for the requested amount of time.
end -- EOF if #timer_callbacks > 0 -- deadline may be nil, in which case waitForEvent blocks indefinitely (i.e., until the next input event ;)).
if ok then local poll_timeout
break -- If UIManager put us on deadline, enforce it, otherwise, block forever.
end 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: ok, ev = input.waitForEvent(poll_timeout and poll_timeout.sec, poll_timeout and poll_timeout.usec)
local timeout_err_msg = "Waiting for input failed: timeout\n" end -- if #timer_callbacks > 0
-- 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) -- Handle errors
if ev and ev.sub and ev:sub(-timeout_err_msg:len()) == timeout_err_msg then if ok then
-- don't report an error on timeout -- We're good, process the event and go back to UIManager.
ev = nil
break break
elseif ev == "application forced to quit" then elseif ok == false then
--- @todo return an event that can be handled if ev == C.ETIME then
os.exit(0, true) -- Don't report an error on ETIME, and go back to UIManager
end ev = nil
logger.warn("got error waiting for events:", ev) break
if ev ~= "Waiting for input failed: 4\n" then elseif ev == C.EINTR then -- luacheck: ignore
-- we only abort if the error is not EINTR -- 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 break
end end
-- We'll need to refresh now on the next iteration, if there is one.
now = nil
end end
if ok and ev then if ok and ev then
if DEBUG.is_on and ev then if DEBUG.is_on then
DEBUG:logEv(ev) DEBUG:logEv(ev)
logger.dbg(string.format( if ev.type == C.EV_KEY then
"%s event => type: %d, code: %d(%s), value: %s, time: %d.%d", logger.dbg(string.format(
ev.type == EV_KEY and "key" or "input", "key event => code: %d (%s), value: %s, time: %d.%d",
ev.type, ev.code, self.event_map[ev.code], tostring(ev.value), ev.code, self.event_map[ev.code], ev.value,
ev.time.sec, ev.time.usec)) 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 end
self:eventAdjustHook(ev) self:eventAdjustHook(ev)
if ev.type == EV_KEY then if ev.type == C.EV_KEY then
return self:handleKeyBoardEv(ev) 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) 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) return self:handleTouchEv(ev)
elseif ev.type == EV_MSC then elseif ev.type == C.EV_MSC then
return self:handleMiscEv(ev) return self:handleMiscEv(ev)
elseif ev.type == EV_SDL then elseif ev.type == C.EV_SDL then
return self:handleSdlEv(ev) return self:handleSdlEv(ev)
else 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) return Event:new("GenericInput", ev)
end end
elseif not ok and ev then elseif ok == false and ev then
return Event:new("InputError", ev) return Event:new("InputError", ev)
elseif ok == nil then
-- No ok and no ev? Hu oh...
return Event:new("InputError", "Catastrophic")
end end
end end

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

@ -3,9 +3,9 @@ local TimeVal = require("ui/timeval")
local GestureRange = { local GestureRange = {
-- gesture matching type -- gesture matching type
ges = nil, ges = nil,
-- spatial range limits the gesture emitting position -- spatial range, limits the gesture emitting position
range = nil, range = nil,
-- temproal range limits the gesture emitting rate -- temporal range, limits the gesture emitting rate
rate = nil, rate = nil,
-- scale limits of this gesture -- scale limits of this gesture
scale = nil, scale = nil,
@ -23,12 +23,11 @@ function GestureRange:match(gs)
return false return false
end end
if self.range then if self.range then
-- sometimes widget dimenension is not available when creating a gesturerage -- Sometimes the widget's dimensions are not available when creating a GestureRange
-- for some action, now we accept a range function that will be later called -- for some action, so we accept a range function that will only be called at match() time instead.
-- and the result of which will be used to check gesture match
-- e.g. range = function() return self.dimen end -- e.g. range = function() return self.dimen end
-- for inputcontainer given that the x and y field of `self.dimen` is only -- That's because most widgets' dimensions are only set at paintTo() time:
-- filled when the inputcontainer is painted into blitbuffer -- e.g., with InputContainer, the x and y fields of `self.dimen`.
local range local range
if type(self.range) == "function" then if type(self.range) == "function" then
range = self.range() range = self.range()
@ -41,10 +40,9 @@ function GestureRange:match(gs)
end end
if self.rate then if self.rate then
-- This filed restraints the upper limit rate(matches per second). -- This field sets up rate-limiting (in matches per second).
-- It's most useful for e-ink devices with less powerfull CPUs and -- It's mostly useful for e-Ink devices with less powerful CPUs
-- screens that cannot handle gesture events that otherwise will be -- and screens that cannot handle the amount of gesture events that would otherwise be generated.
-- generated
local last_time = self.last_time or TimeVal:new{} local last_time = self.last_time or TimeVal:new{}
if gs.time - last_time > TimeVal:new{usec = 1000000 / self.rate} then if gs.time - last_time > TimeVal:new{usec = 1000000 / self.rate} then
self.last_time = gs.time 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. -- Do some stuff.
-- You can add and subtract `TimeVal` objects. -- You can add and subtract `TimeVal` objects.
local tv_duration = TimeVal:now() - tv_start local tv_duration = TimeVal:now() - tv_start
-- If you need more precision (like 2.5 s), -- And convert that object to various more human-readable formats, e.g.,
-- you can add the milliseconds to the seconds. print(string.format("Stuff took %.3fms", tv_duration:tomsecs()))
local tv_duration_seconds_float = tv_duration.sec + tv_duration.usec/1000000
]] ]]
local dbg = require("dbg") local ffi = require("ffi")
require("ffi/posix_h")
local util = require("ffi/util") 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 @table TimeVal
@int sec floored number of seconds @int sec floored number of seconds
@int usec remaining number of milliseconds @int usec number of microseconds past that second.
]] ]]
local TimeVal = { local TimeVal = {
sec = 0, sec = 0,
@ -47,7 +74,7 @@ function TimeVal:new(from_o)
if o.usec == nil then if o.usec == nil then
o.usec = 0 o.usec = 0
elseif o.usec > 1000000 then 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 o.usec = o.usec % 1000000
end end
setmetatable(o, self) setmetatable(o, self)
@ -55,55 +82,39 @@ function TimeVal:new(from_o)
return o return o
end end
-- Based on <bsd/sys/time.h>
function TimeVal:__lt(time_b) function TimeVal:__lt(time_b)
if self.sec < time_b.sec then if self.sec == time_b.sec then
return true return self.usec < time_b.usec
elseif self.sec > time_b.sec then
return false
else else
-- self.sec == time_b.sec return self.sec < time_b.sec
if self.usec < time_b.usec then
return true
else
return false
end
end end
end end
function TimeVal:__le(time_b) function TimeVal:__le(time_b)
if self.sec < time_b.sec then if self.sec == time_b.sec then
return true return self.usec <= time_b.usec
elseif self.sec > time_b.sec then
return false
else else
-- self.sec == time_b.sec return self.sec <= time_b.sec
if self.usec > time_b.usec then
return false
else
return true
end
end end
end end
function TimeVal:__eq(time_b) function TimeVal:__eq(time_b)
if self.sec == time_b.sec and self.usec == time_b.usec then if self.sec == time_b.sec then
return true return self.usec == time_b.usec
else else
return false return false
end end
end end
-- If sec is negative, time went backwards!
function TimeVal:__sub(time_b) function TimeVal:__sub(time_b)
local diff = TimeVal:new{} local diff = TimeVal:new{}
diff.sec = self.sec - time_b.sec diff.sec = self.sec - time_b.sec
diff.usec = self.usec - time_b.usec diff.usec = self.usec - time_b.usec
if diff.sec < 0 and diff.usec > 0 then if diff.usec < 0 then
diff.sec = diff.sec + 1
diff.usec = diff.usec - 1000000
elseif diff.sec > 0 and diff.usec < 0 then
diff.sec = diff.sec - 1 diff.sec = diff.sec - 1
diff.usec = diff.usec + 1000000 diff.usec = diff.usec + 1000000
end end
@ -111,48 +122,127 @@ function TimeVal:__sub(time_b)
return diff return diff
end 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) function TimeVal:__add(time_b)
local sum = TimeVal:new{} local sum = TimeVal:new{}
sum.sec = self.sec + time_b.sec sum.sec = self.sec + time_b.sec
sum.usec = self.usec + time_b.usec 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.sec = sum.sec + 1
sum.usec = sum.usec - 1000000 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 end
return sum return sum
end 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 @usage
local TimeVal = require("ui/timeval") local TimeVal = require("ui/timeval")
local tv_start = TimeVal:now() local tv_start = TimeVal:realtime()
-- Do some stuff. -- Do some stuff.
-- You can add and substract `TimeVal` objects. -- You can add and substract `TimeVal` objects.
local tv_duration = TimeVal:now() - tv_start local tv_duration = TimeVal:realtime() - tv_start
@treturn TimeVal @treturn TimeVal
]] ]]
function TimeVal:now() function TimeVal:realtime()
local sec, usec = util.gettime() local sec, usec = util.gettime()
return TimeVal:new{sec = sec, usec = usec} return TimeVal:new{sec = sec, usec = usec}
end 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 return TimeVal

@ -5,6 +5,7 @@ This module manages widgets.
local Device = require("device") local Device = require("device")
local Event = require("ui/event") local Event = require("ui/event")
local Geom = require("ui/geometry") local Geom = require("ui/geometry")
local TimeVal = require("ui/timeval")
local dbg = require("dbg") local dbg = require("dbg")
local logger = require("logger") local logger = require("logger")
local ffiUtil = require("ffi/util") local ffiUtil = require("ffi/util")
@ -13,7 +14,6 @@ local _ = require("gettext")
local Input = Device.input local Input = Device.input
local Screen = Device.screen local Screen = Device.screen
local MILLION = 1000000
local DEFAULT_FULL_REFRESH_COUNT = 6 local DEFAULT_FULL_REFRESH_COUNT = 6
-- there is only one instance of this -- there is only one instance of this
@ -29,6 +29,7 @@ local UIManager = {
event_handlers = nil, event_handlers = nil,
_running = true, _running = true,
_now = TimeVal:now(),
_window_stack = {}, _window_stack = {},
_task_queue = {}, _task_queue = {},
_task_queue_dirty = false, _task_queue_dirty = false,
@ -512,13 +513,11 @@ end
function UIManager:schedule(time, action, ...) function UIManager:schedule(time, action, ...)
local p, s, e = 1, 1, #self._task_queue local p, s, e = 1, 1, #self._task_queue
if e ~= 0 then if e ~= 0 then
local us = time[1] * MILLION + time[2]
-- do a binary insert -- do a binary insert
repeat repeat
p = math.floor(s + (e - s) / 2) p = math.floor(s + (e - s) / 2)
local ptime = self._task_queue[p].time local p_time = self._task_queue[p].time
local ptus = ptime[1] * MILLION + ptime[2] if time > p_time then
if us > ptus then
if s == e then if s == e then
p = e + 1 p = e + 1
break break
@ -527,7 +526,7 @@ function UIManager:schedule(time, action, ...)
else else
s = p s = p
end end
elseif us < ptus then elseif time < p_time then
e = p e = p
if s == e then if s == e then
break break
@ -549,29 +548,23 @@ function UIManager:schedule(time, action, ...)
end end
dbg:guard(UIManager, 'schedule', dbg:guard(UIManager, 'schedule',
function(self, time, action) 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) assert(action ~= nil)
end) end)
--[[-- --[[--
Schedules a task to be run a certain amount of seconds from now. 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) @func action reference to the task to be scheduled (may be anonymous)
@param ... optional arguments passed to action @param ... optional arguments passed to action
@see unschedule @see unschedule
]] ]]
function UIManager:scheduleIn(seconds, action, ...) function UIManager:scheduleIn(seconds, action, ...)
local when = { ffiUtil.gettime() } -- We might run significantly late inside an UI frame, so we can't use the cached value here.
local s = math.floor(seconds) -- It would also cause some bad interactions with the way nextTick & co behave.
local usecs = (seconds - s) * MILLION local when = TimeVal:now() + TimeVal:fromnumber(seconds)
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
self:schedule(when, action, ...) self:schedule(when, action, ...)
end end
dbg:guard(UIManager, 'scheduleIn', dbg:guard(UIManager, 'scheduleIn',
@ -1049,7 +1042,7 @@ function UIManager:discardEvents(set_or_seconds)
self._discard_events_till = nil self._discard_events_till = nil
return return
end end
local usecs local delay
if set_or_seconds == true then if set_or_seconds == true then
-- Use an adequate delay to account for device refresh duration -- Use an adequate delay to account for device refresh duration
-- so any events happening in this delay (ie. before a widget -- 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. -- sometimes > 500ms on some devices/temperatures.
-- So, block for 400ms (to have it displayed) + 400ms -- So, block for 400ms (to have it displayed) + 400ms
-- for user reaction to it -- for user reaction to it
usecs = 800000 delay = TimeVal:new{ usec = 800000 }
else else
-- On non-eInk screen, display is usually instantaneous -- On non-eInk screen, display is usually instantaneous
usecs = 400000 delay = TimeVal:new{ usec = 400000 }
end end
else -- we expect a number else -- we expect a number
usecs = set_or_seconds * MILLION delay = TimeVal:new{ sec = set_or_seconds }
end end
local now = { ffiUtil.gettime() } self._discard_events_till = self._now + delay
local now_us = now[1] * MILLION + now[2]
self._discard_events_till = now_us + usecs
end end
--[[-- --[[--
@ -1082,9 +1073,7 @@ function UIManager:sendEvent(event)
-- Ensure discardEvents -- Ensure discardEvents
if self._discard_events_till then if self._discard_events_till then
local now = { ffiUtil.gettime() } if TimeVal:now() < self._discard_events_till then
local now_us = now[1] * MILLION + now[2]
if now_us < self._discard_events_till then
return return
else else
self._discard_events_till = nil self._discard_events_till = nil
@ -1159,8 +1148,7 @@ function UIManager:broadcastEvent(event)
end end
function UIManager:_checkTasks() function UIManager:_checkTasks()
local now = { ffiUtil.gettime() } self._now = TimeVal:now()
local now_us = now[1] * MILLION + now[2]
local wait_until = nil local wait_until = nil
-- task.action may schedule other events -- task.action may schedule other events
@ -1172,11 +1160,8 @@ function UIManager:_checkTasks()
break break
end end
local task = self._task_queue[1] local task = self._task_queue[1]
local task_us = 0 local task_tv = task.time or TimeVal:new{}
if task.time ~= nil then if task_tv <= self._now then
task_us = task.time[1] * MILLION + task.time[2]
end
if task_us <= now_us then
-- remove from table -- remove from table
table.remove(self._task_queue, 1) table.remove(self._task_queue, 1)
-- task is pending to be executed right now. do it. -- task is pending to be executed right now. do it.
@ -1191,7 +1176,26 @@ function UIManager:_checkTasks()
end end
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 end
-- precedence of refresh modes: -- precedence of refresh modes:
@ -1580,29 +1584,35 @@ function UIManager:handleInput()
self:processZMQs() self:processZMQs()
-- Figure out how long to wait. -- 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). -- Default to INPUT_TIMEOUT (which may be nil, i.e. block until an event happens).
local wait_us = self.INPUT_TIMEOUT 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 we have any ZMQs registered, ZMQ_TIMEOUT is another upper bound.
if #self._zeromqs > 0 then if #self._zeromqs > 0 then
wait_us = math.min(wait_us or math.huge, self.ZMQ_TIMEOUT) wait_us = math.min(wait_us or math.huge, self.ZMQ_TIMEOUT)
end 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 -- 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). -- 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. -- Anywhere else breaks preventStandby/allowStandby invariants used by background jobs while UI is left running.
self:_standbyTransition() self:_standbyTransition()
-- wait for next event -- wait for next event
local input_event = Input:waitEvent(wait_us) local input_event = Input:waitEvent(now, deadline)
-- delegate input_event to handler -- delegate input_event to handler
if input_event then if input_event then
@ -1672,6 +1682,9 @@ end
function UIManager:_beforeSuspend() function UIManager:_beforeSuspend()
self:flushSettings() self:flushSettings()
self:broadcastEvent(Event:new("Suspend")) 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 end
-- The common operations that should be performed after resuming the device. -- The common operations that should be performed after resuming the device.
@ -1772,5 +1785,11 @@ function UIManager:restartKOReader()
self._exit_code = 85 self._exit_code = 85
end 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() UIManager:init()
return UIManager return UIManager

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

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

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

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

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

@ -16,6 +16,7 @@ local InputContainer = require("ui/widget/container/inputcontainer")
local KeyboardLayoutDialog = require("ui/widget/keyboardlayoutdialog") local KeyboardLayoutDialog = require("ui/widget/keyboardlayoutdialog")
local Size = require("ui/size") local Size = require("ui/size")
local TextWidget = require("ui/widget/textwidget") local TextWidget = require("ui/widget/textwidget")
local TimeVal = require("ui/timeval")
local UIManager = require("ui/uimanager") local UIManager = require("ui/uimanager")
local VerticalGroup = require("ui/widget/verticalgroup") local VerticalGroup = require("ui/widget/verticalgroup")
local VerticalSpan = require("ui/widget/verticalspan") 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 = 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 if Device:hasDPad() then
self.key_events.PressKey = { {"Press"}, doc = "select key" } self.key_events.PressKey = { {"Press"}, doc = "select key" }
@ -699,6 +701,7 @@ function VirtualKeyboard:init()
self.max_layer = keyboard.max_layer self.max_layer = keyboard.max_layer
self:initLayer(self.keyboard_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 = 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 if Device:hasDPad() then
self.key_events.PressKey = { {"Press"}, doc = "select key" } self.key_events.PressKey = { {"Press"}, doc = "select key" }
end end

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,36 +1,181 @@
require("commonrequire") require("commonrequire")
local TimeVal = require("ui/timeval")
local ffi = require("ffi")
local dummy = require("ffi/posix_h")
local logger = require("logger") local logger = require("logger")
local util = require("ffi/util")
local C = ffi.C
local MockTime = { local MockTime = {
original_os_time = os.time, original_os_time = os.time,
original_util_time = nil, 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() function MockTime:install()
assert(self ~= nil) assert(self ~= nil)
local util = require("ffi/util")
if self.original_util_time == nil then if self.original_util_time == nil then
self.original_util_time = util.gettime self.original_util_time = util.gettime
assert(self.original_util_time ~= nil) assert(self.original_util_time ~= nil)
end 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 os.time = function() --luacheck: ignore
logger.dbg("MockTime:os.time: ", self.value) logger.dbg("MockTime:os.time: ", self.realtime)
return self.value return self.realtime
end end
util.gettime = function() util.gettime = function()
logger.dbg("MockTime:util.gettime: ", self.value) logger.dbg("MockTime:util.gettime: ", self.realtime)
return self.value, 0 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
end end
function MockTime:uninstall() function MockTime:uninstall()
assert(self ~= nil) assert(self ~= nil)
local util = require("ffi/util")
os.time = self.original_os_time --luacheck: ignore os.time = self.original_os_time --luacheck: ignore
if self.original_util_time ~= nil then if self.original_util_time ~= nil then
util.gettime = self.original_util_time util.gettime = self.original_util_time
end 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 end
function MockTime:set(value) function MockTime:set(value)
@ -38,8 +183,12 @@ function MockTime:set(value)
if type(value) ~= "number" then if type(value) ~= "number" then
return false return false
end end
self.value = math.floor(value) self.realtime = math.floor(value)
logger.dbg("MockTime:set ", self.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 return true
end end
@ -48,8 +197,12 @@ function MockTime:increase(value)
if type(value) ~= "number" then if type(value) ~= "number" then
return false return false
end end
self.value = math.floor(self.value + value) self.realtime = math.floor(self.realtime + value)
logger.dbg("MockTime:increase ", self.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 return true
end end

@ -20,25 +20,42 @@ describe("TimeVal module", function()
local timev2 = TimeVal:new{ sec = 10, usec = 6000} local timev2 = TimeVal:new{ sec = 10, usec = 6000}
local timev3 = TimeVal:new{ sec = 10, usec = 50000000} local timev3 = TimeVal:new{ sec = 10, usec = 50000000}
assert.is.same({sec = 15,usec = 11000}, timev1 + timev2) assert.is.same({sec = 15, usec = 11000}, timev1 + timev2)
assert.is.same({sec = 65,usec = 5000}, timev1 + timev3) assert.is.same({sec = 65, usec = 5000}, timev1 + timev3)
end) end)
it("should subtract", function() it("should subtract", function()
local timev1 = TimeVal:new{ sec = 5, usec = 5000} local timev1 = TimeVal:new{ sec = 5, usec = 5000}
local timev2 = TimeVal:new{ sec = 10, usec = 6000} local timev2 = TimeVal:new{ sec = 10, usec = 6000}
assert.is.same({sec = 5,usec = 1000}, timev2 - timev1) assert.is.same({sec = 5, usec = 1000}, timev2 - timev1)
assert.is.same({sec = -5,usec = -1000}, timev1 - timev2) local backwards_sub = timev1 - timev2
end) assert.is.same({sec = -6, usec = 999000}, backwards_sub)
it("should guard against reverse subtraction logic", function() -- Check that to/from float conversions behave, even for negative values.
dbg:turnOn() assert.is.same(-5.001, backwards_sub:tonumber())
TimeVal = package.reload("ui/timeval") assert.is.same({sec = -6, usec = 999000}, TimeVal:fromnumber(-5.001))
local timev1 = TimeVal:new{ sec = 5, usec = 5000}
local timev2 = TimeVal:new{ sec = 10, usec = 5000} 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) end)
it("should derive sec and usec from more than 1 sec worth of usec", function() it("should derive sec and usec from more than 1 sec worth of usec", function()

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

Loading…
Cancel
Save