From 8d33c364d145f55b6a7f220065d6c99b6b910590 Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 17 Jun 2026 10:58:34 -0600 Subject: [PATCH 1/2] android: pre-cache epdRootView to fix EPD updates from NativeThread window.decorView.findViewById() must not be called from a non-UI thread. einkUpdate() and performHapticFeedback() are invoked from the NativeThread (the LuaJIT event loop), so accessing window.decorView there is unsafe and can crash on some devices. Pre-cache the root view on the main thread during onCreate() and use the cached reference in all EPD/haptic calls. If the view is not yet ready the call returns early rather than crashing. --- .../main/java/org/koreader/launcher/MainActivity.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/koreader/launcher/MainActivity.kt b/app/src/main/java/org/koreader/launcher/MainActivity.kt index 8256a34ff..69bb48561 100644 --- a/app/src/main/java/org/koreader/launcher/MainActivity.kt +++ b/app/src/main/java/org/koreader/launcher/MainActivity.kt @@ -56,6 +56,9 @@ class MainActivity : NativeActivity(), LuaInterface, // surface used on devices that need a view private var view: NativeSurfaceView? = null + + // cached root view for EPD mode calls from NativeThread (avoids window.decorView on non-UI thread) + private var epdRootView: android.view.View? = null private class NativeSurfaceView(context: Context): SurfaceView(context), SurfaceHolder.Callback { init { holder.addCallback(this) } @@ -133,6 +136,10 @@ class MainActivity : NativeActivity(), LuaInterface, } Log.v(TAG_SURFACE, "Using $surfaceKind implementation") + // Cache root view for EPD calls from NativeThread (window.decorView must not be + // accessed from a non-UI thread; pre-cache it here on the main thread instead). + epdRootView = view ?: window.decorView.findViewById(android.R.id.content) + @Suppress("DEPRECATION") screenIsLandscape = windowManager.defaultDisplay.width > windowManager.defaultDisplay.height @@ -314,7 +321,7 @@ class MainActivity : NativeActivity(), LuaInterface, } override fun einkUpdate(mode: Int) { - val rootView = view ?: window.decorView.findViewById(android.R.id.content) + val rootView = epdRootView ?: return val modeName = when (mode) { 1 -> "EPD_FULL" 2 -> "EPD_PART" @@ -332,7 +339,7 @@ class MainActivity : NativeActivity(), LuaInterface, } override fun einkUpdate(mode: Int, delay: Long, x: Int, y: Int, width: Int, height: Int) { - val rootView = view ?: window.decorView.findViewById(android.R.id.content) + val rootView = epdRootView ?: return Log.v(tag, String.format(Locale.US, "requesting epd update, mode:%d, delay:%d, [x:%d, y:%d, w:%d, h:%d]", mode, delay, x, y, width, height)) @@ -645,7 +652,7 @@ class MainActivity : NativeActivity(), LuaInterface, } override fun performHapticFeedback(constant: Int, force: Int) { - val rootView = view ?: window.decorView.findViewById(android.R.id.content) + val rootView = epdRootView ?: return hapticFeedback(constant, force > 0, rootView) } From 0ca81a541d4764511275b4aa2eca983c55e643af Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 17 Jun 2026 15:05:31 -0600 Subject: [PATCH 2/2] MainActivity: refresh epdRootView in surfaceCreated Ensures the cached root view is always fresh when the surface is (re)created, rather than relying solely on the onCreate assignment. Co-Authored-By: Claude Sonnet 4.6 --- app/src/main/java/org/koreader/launcher/MainActivity.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/koreader/launcher/MainActivity.kt b/app/src/main/java/org/koreader/launcher/MainActivity.kt index 69bb48561..c8044dba4 100644 --- a/app/src/main/java/org/koreader/launcher/MainActivity.kt +++ b/app/src/main/java/org/koreader/launcher/MainActivity.kt @@ -172,6 +172,7 @@ class MainActivity : NativeActivity(), LuaInterface, override fun surfaceCreated(holder: SurfaceHolder) { super.surfaceCreated(holder) + epdRootView = view ?: window.decorView.rootView drawSplashScreen(holder) }