Initial commit: EventTracker desktop app + Android companion

Desktop: GTK3 + matplotlib app tracking arbitrary events (button ->
timestamp -> CSV), with per-event stats, trend charts, settings
(rename/add events, relocate data file), and a reset-with-backup flow.
Portable desktop launcher via a .desktop.in template + install script,
no machine-specific paths baked in.

Android: minimal Kivy v1 companion reading/writing the same CSV format,
plus buildozer build tooling isolated in a venv and a local p4a recipe
override for kivy (see CLAUDE.md for why).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
AI_Assistant 2026-08-22 07:52:54 +00:00
commit cd9f9456e9
No known key found for this signature in database
17 changed files with 1722 additions and 0 deletions

View file

@ -0,0 +1,110 @@
# Local override of p4a's built-in kivy recipe — identical except
# python_depends is emptied. See buildozer.spec for why: upstream's
# ['certifi', 'chardet', 'idna', 'requests', 'urllib3', 'filetype'] pulls
# in requests -> charset-normalizer, whose newest release ships an
# Android-tagged wheel that p4a's own install step can't actually install
# (see comment in buildozer.spec). We never call kivy.network.UrlRequest,
# so dropping the whole chain is correct, not just a workaround.
from os.path import join
import sys
import packaging.version
import sh
from pythonforandroid.recipe import PyProjectRecipe
from pythonforandroid.toolchain import current_directory, shprint
def get_kivy_version(recipe, arch):
with current_directory(join(recipe.get_build_dir(arch.arch), "kivy")):
return shprint(
sh.Command(sys.executable),
"-c",
"import _version; print(_version.__version__)",
)
def is_kivy_affected_by_deadlock_issue(recipe=None, arch=None):
return packaging.version.parse(
str(get_kivy_version(recipe, arch))
) < packaging.version.Version("2.2.0.dev0")
def is_kivy_less_than_3(recipe=None, arch=None):
return packaging.version.parse(
str(get_kivy_version(recipe, arch))
) < packaging.version.Version("3.0.0.dev0")
class KivyRecipe(PyProjectRecipe):
version = '2.3.1'
url = 'https://github.com/kivy/kivy/archive/{version}.zip'
name = 'kivy'
depends = [('sdl2', 'sdl3'), 'pyjnius', 'setuptools', 'android']
python_depends = [] # upstream: ['certifi', 'chardet', 'idna', 'requests', 'urllib3', 'filetype'] — unused, see buildozer.spec
hostpython_prerequisites = ["cython>=0.29.1,<=3.0.12"]
# sdl-gl-swapwindow-nogil.patch is needed to avoid a deadlock.
# See: https://github.com/kivy/kivy/pull/8025
# WARNING: Remove this patch when a new Kivy version is released.
patches = [
("sdl-gl-swapwindow-nogil.patch", is_kivy_affected_by_deadlock_issue),
("use_cython.patch", is_kivy_less_than_3),
"no-ast-str.patch"
]
@property
def need_stl_shared(self):
if "sdl3" in self.ctx.recipe_build_order:
return True
else:
return False
def get_recipe_env(self, arch, **kwargs):
env = super().get_recipe_env(arch, **kwargs)
# Taken from CythonRecipe
env['LDFLAGS'] = env['LDFLAGS'] + ' -L{} '.format(
self.ctx.get_libs_dir(arch.arch) +
' -L{} '.format(self.ctx.libs_dir) +
' -L{}'.format(join(self.ctx.bootstrap.build_dir, 'obj', 'local',
arch.arch)))
env['LDSHARED'] = env['CC'] + ' -shared'
env['LIBLINK'] = 'NOTNONE'
# NDKPLATFORM is our switch for detecting Android platform, so can't be None
env['NDKPLATFORM'] = "NOTNONE"
if not is_kivy_less_than_3(self, arch):
env['KIVY_CROSS_PLATFORM'] = 'android'
if 'sdl2' in self.ctx.recipe_build_order:
env['USE_SDL2'] = '1'
env['KIVY_SPLIT_EXAMPLES'] = '1'
sdl2_mixer_recipe = self.get_recipe('sdl2_mixer', self.ctx)
sdl2_image_recipe = self.get_recipe('sdl2_image', self.ctx)
env['KIVY_SDL2_PATH'] = ':'.join([
join(self.ctx.bootstrap.build_dir, 'jni', 'SDL', 'include'),
*sdl2_image_recipe.get_include_dirs(arch),
*sdl2_mixer_recipe.get_include_dirs(arch),
join(self.ctx.bootstrap.build_dir, 'jni', 'SDL2_ttf'),
])
if "sdl3" in self.ctx.recipe_build_order:
sdl3_mixer_recipe = self.get_recipe("sdl3_mixer", self.ctx)
sdl3_image_recipe = self.get_recipe("sdl3_image", self.ctx)
sdl3_ttf_recipe = self.get_recipe("sdl3_ttf", self.ctx)
sdl3_recipe = self.get_recipe("sdl3", self.ctx)
env["USE_SDL3"] = "1"
env["KIVY_SPLIT_EXAMPLES"] = "1"
env["KIVY_SDL3_PATH"] = ":".join(
[
*sdl3_mixer_recipe.get_include_dirs(arch),
*sdl3_image_recipe.get_include_dirs(arch),
*sdl3_ttf_recipe.get_include_dirs(arch),
*sdl3_recipe.get_include_dirs(arch),
]
)
return env
recipe = KivyRecipe()

View file

@ -0,0 +1,16 @@
diff -ur kivy-2.3.1b/kivy/lang/parser.py kivy-2.3.1/kivy/lang/parser.py
--- kivy-2.3.1b/kivy/lang/parser.py 2025-10-19 13:04:51.542798827 +1300
+++ kivy-2.3.1/kivy/lang/parser.py 2025-10-19 13:05:16.007104601 +1300
@@ -230,11 +230,7 @@
if isinstance(node, (ast.JoinedStr, ast.BoolOp)):
for n in node.values:
- if isinstance(n, ast.Str):
- # NOTE: required for python3.6
- yield from cls.get_names_from_expression(n.s)
- else:
- yield from cls.get_names_from_expression(n.value)
+ yield from cls.get_names_from_expression(n.value)
if isinstance(node, ast.BinOp):
yield from cls.get_names_from_expression(node.right)

View file

@ -0,0 +1,32 @@
diff --git a/kivy/core/window/_window_sdl2.pyx b/kivy/core/window/_window_sdl2.pyx
index 46e15ec63..5002cd0f9 100644
--- a/kivy/core/window/_window_sdl2.pyx
+++ b/kivy/core/window/_window_sdl2.pyx
@@ -746,7 +746,13 @@ cdef class _WindowSDL2Storage:
pass
def flip(self):
- SDL_GL_SwapWindow(self.win)
+ # On Android (and potentially other platforms), SDL_GL_SwapWindow may
+ # lock the thread waiting for a mutex from another thread to be
+ # released. Calling SDL_GL_SwapWindow with the GIL released allow the
+ # other thread to run (e.g. to process the event filter callback) and
+ # release the mutex SDL_GL_SwapWindow is waiting for.
+ with nogil:
+ SDL_GL_SwapWindow(self.win)
def save_bytes_in_png(self, filename, data, int width, int height):
cdef SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(
diff --git a/kivy/lib/sdl2.pxi b/kivy/lib/sdl2.pxi
index 6a539de6d..3a5a69d23 100644
--- a/kivy/lib/sdl2.pxi
+++ b/kivy/lib/sdl2.pxi
@@ -627,7 +627,7 @@ cdef extern from "SDL.h":
cdef SDL_GLContext SDL_GL_GetCurrentContext()
cdef int SDL_GL_SetSwapInterval(int interval)
cdef int SDL_GL_GetSwapInterval()
- cdef void SDL_GL_SwapWindow(SDL_Window * window)
+ cdef void SDL_GL_SwapWindow(SDL_Window * window) nogil
cdef void SDL_GL_DeleteContext(SDL_GLContext context)
cdef int SDL_NumJoysticks()

View file

@ -0,0 +1,11 @@
--- kivy-master/setup.py 2025-02-25 03:08:18.000000000 +0530
+++ kivy-master.mod/setup.py 2025-03-01 13:10:24.227808612 +0530
@@ -249,7 +249,7 @@
# This determines whether Cython specific functionality may be used.
can_use_cython = True
-if platform in ('ios', 'android'):
+if platform in ('ios'):
# NEVER use or declare cython on these platforms
print('Not using cython on %s' % platform)
can_use_cython = False