diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6bbf8f8..9071f97 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -18,6 +18,33 @@ set(YYJSON_INSTALL OFF)
add_subdirectory(src/c_src/yyjson EXCLUDE_FROM_ALL)
set_target_properties(yyjson PROPERTIES POSITION_INDEPENDENT_CODE ON)
+# ============================================================================
+# Android backend
+# ============================================================================
+if(ANDROID)
+ add_library(ring_webview_android OBJECT
+ src/c_src/ring_webview_android.c
+ )
+ set_target_properties(ring_webview_android PROPERTIES
+ POSITION_INDEPENDENT_CODE ON
+ )
+ if(TARGET ring)
+ target_link_libraries(ring_webview_android PUBLIC ring)
+ else()
+ target_include_directories(ring_webview_android PUBLIC
+ "${RING_ROOT}/language/include"
+ )
+ endif()
+ target_link_libraries(ring_webview_android PUBLIC
+ android
+ log
+ yyjson
+ )
+ message(STATUS "Ring Webview: Android backend")
+ return()
+endif()
+
+
if(NOT TARGET Ring::Ring)
add_library(Ring::Ring UNKNOWN IMPORTED)
find_path(RING_INCLUDE_DIR ring.h PATHS "${RING_ROOT}/language/include")
diff --git a/README.md b/README.md
index f38bc24..e7e1777 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ This project is made possible by the tiny [webview](https://github.com/webview/w
## ✨ Features
-- **Cross-Platform:** Build applications for Windows, macOS, Linux, and FreeBSD from a single codebase.
+- **Cross-Platform:** Build applications for Windows, macOS, Linux, FreeBSD, and Android from a single codebase.
- **Modern UI:** Use familiar web technologies to design your user interface.
- **Two-Way Binding:** Seamlessly call Ring functions from JavaScript and vice-versa.
- **Easy to Use:** A simple and clean API makes it easy to get started.
@@ -93,6 +93,15 @@ The compiled macOS library in this package uses the built-in WebKit framework.
+
+Click here for instructions on Android
+
+Android apps are built with [ring2apk](https://github.com/ysdragon/ring2apk) (no Gradle).
+You need the Android SDK, the NDK, a JDK, and CMake + Ninja.
+See [📱 Android](#-android) and [`docs/ANDROID.md`](docs/ANDROID.md) for the full guide.
+
+
+
- **Install the library using RingPM:**
```sh
ringpm install webview from ysdragon
@@ -153,6 +162,25 @@ ring main.ring
See [`examples/templates/`](examples/templates/) for details.
+## 📱 Android
+
+The same `main.ring` runs on Android via [ring2apk](https://github.com/ysdragon/ring2apk)
+— no Gradle. See [`examples/android/`](examples/android/) (a
+glassmorphism notes app) and [`docs/ANDROID.md`](docs/ANDROID.md) (backend
+architecture, lifecycle, recovery).
+
+See [`docs/ANDROID.md`](docs/ANDROID.md) to build and run it.
+
+
+
## 📚 API Reference
For a detailed list of all available functions, classes, and methods, please refer to our [API reference documentation](docs/REFERENCE.md).
diff --git a/docs/ANDROID.md b/docs/ANDROID.md
new file mode 100644
index 0000000..0edb7c8
--- /dev/null
+++ b/docs/ANDROID.md
@@ -0,0 +1,74 @@
+# Building Android Apps with Ring WebView
+
+Write `main.ring` once, ship it on desktop and Android. On Android the same
+Ring C-API is backed by `android.webkit.WebView` through JNI — no Java and
+no Gradle required from you.
+
+## Prerequisites
+
+- Ring 1.27+, Android SDK + NDK + JDK, CMake + Ninja
+- [ring2apk](https://github.com/ysdragon/ring2apk): `ringpm install ring2apk from ysdragon`
+
+## Start
+
+```sh
+ring2apk init myapp # scaffold, or copy examples/android/
+cd myapp
+ring2apk build # → build/myapp-debug.apk
+ring2apk run # build + install + launch + logcat
+adb logcat -s RingOutput:D # `see` output
+```
+
+Config lives in `ring2apk.ring` (`:packageId`, `:targets`, `:permissions`,
+`:entryPoint`). The working reference is
+[`examples/android/`](../examples/android/) — a notes
+app; its [README](../examples/android/README.md) covers the layout.
+
+## Write the app
+
+`ring/main.ring` is compiled to embedded bytecode and linked into
+`libmain.so` — no `.ring` files ship. The API matches desktop, so most of
+[USAGE](USAGE.md) and [REFERENCE](REFERENCE.md) apply as-is:
+
+```ring
+load "webview.ring"
+
+aBindList = [["getState", :fetchNotes]]
+
+oWebView = new WebView()
+oWebView {
+ setTitle("My App")
+ onLoad(:handleLoad)
+ onDomReady(:handleDomReady)
+ onClose(:handleClose)
+ bindMany(NULL)
+ setHtml(`
Hello Android
`)
+ run()
+}
+
+func fetchNotes(cId, aReq)
+ oWebView.wreturn(cId, WEBVIEW_ERROR_OK, aNotes)
+```
+
+Bridge rules that matter on mobile:
+
+- Binds receive Ring lists (JS argument array, JSON-decoded) and should
+ `wreturn` results; JS gets promises. See the example's bridge table.
+- Drive the first render from `onDomReady` (`evalJS("refresh()")`) — it runs
+ strictly after the bind shim is injected.
+- Files persist in the app's `filesDir` (the worker starts there); use
+ relative paths.
+- Window-management calls (`setSize`, `minimize`, …) are desktop no-ops
+ that return safely — guard mobile layouts with responsive CSS instead.
+
+## Under the hood
+
+UI thread owns the WebView and only enqueues; a worker thread owns the Ring
+VM and runs a bounded job queue. Rotation re-attaches without destroying;
+a killed renderer rebuilds the view and reloads the last HTML/URL (poison
+pages stop after 3 crashes in 10s). Single webview per process.
+`RingBridge` is callable by any loaded page JS, remote included — validate
+bind arguments.
+
+Backend sources: `src/c_src/ring_webview_android.c`,
+`src/android/io/github/ysdragon/webview/MainActivity.java`.
diff --git a/examples/android/.gitignore b/examples/android/.gitignore
new file mode 100644
index 0000000..eb5234b
--- /dev/null
+++ b/examples/android/.gitignore
@@ -0,0 +1,3 @@
+src/cpp/ring
+build/
+.ring2apk.hash
\ No newline at end of file
diff --git a/examples/android/README.md b/examples/android/README.md
new file mode 100644
index 0000000..4c86752
--- /dev/null
+++ b/examples/android/README.md
@@ -0,0 +1,52 @@
+# Nota — Ring WebView on Android
+
+A real notes app with a glassmorphism mobile UI, built with Ring + WebView.
+Same-language backend as desktop: Ring owns the state, JavaScript renders it.
+
+
+
+## What it does
+
+- Add, edit, delete, pin, and search notes
+- Color-coded glass cards, bottom-sheet editor, toasts
+- Persists to `notes.db` in the app's `filesDir` (survives restarts)
+- Survives rotation and renderer kills (system memory pressure)
+
+## Bridge pattern
+
+Ring never touches JSON text. Binds receive Ring lists (decoded from the
+JS argument array) and `wreturn` the full notes list back; JS re-renders
+from the promise resolution:
+
+| JS call | Ring callback |
+|---|---|
+| `getState()` | `fetchNotes` |
+| `note_save([id, title, body, color, pinned])` | `storeNote` |
+| `note_delete([id])` | `removeNote` |
+| `note_pin([id])` | `toggleNotePin` |
+
+The first pull is driven by native `onDomReady` (`evalJS("refresh()")`),
+which runs strictly after the bind shim is injected — no polling.
+
+## Build
+
+Needs [ring2apk](https://github.com/ysdragon/ring2apk):
+
+```sh
+ringpm install ring2apk from ysdragon
+cd examples/android
+ring2apk build # → build/webviewdemo-debug.apk
+ring2apk run # build + install + launch + logcat
+adb logcat -s RingOutput:D # `see` output
+```
+
+## Layout
+
+```
+ring/main.ring # app: state, binds, persistence, embedded UI
+ring/fulltest.ring # backend conformance suite (20 checks)
+ring/src/ # webview.ring loader (same API as desktop)
+src/cpp/ # libmain.so: Ring VM + backend (ring/ vendored, ignored by git)
+src/java/ # MainActivity mirror (canonical copy in src/android/)
+ring2apk.ring # package io.github.ysdragon.webview, arm64-v8a
+```
diff --git a/examples/android/img/nota-screenshot.png b/examples/android/img/nota-screenshot.png
new file mode 100644
index 0000000..b91ff99
Binary files /dev/null and b/examples/android/img/nota-screenshot.png differ
diff --git a/examples/android/res/mipmap-hdpi/ic_launcher.png b/examples/android/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..14a9b8a
Binary files /dev/null and b/examples/android/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/examples/android/res/mipmap-mdpi/ic_launcher.png b/examples/android/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..2a3e0d1
Binary files /dev/null and b/examples/android/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/examples/android/res/mipmap-xhdpi/ic_launcher.png b/examples/android/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..cda35bf
Binary files /dev/null and b/examples/android/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/examples/android/res/mipmap-xxhdpi/ic_launcher.png b/examples/android/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..c9c9509
Binary files /dev/null and b/examples/android/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/examples/android/res/mipmap-xxxhdpi/ic_launcher.png b/examples/android/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d202917
Binary files /dev/null and b/examples/android/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/examples/android/res/values/colors.xml b/examples/android/res/values/colors.xml
new file mode 100644
index 0000000..030098f
--- /dev/null
+++ b/examples/android/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #6200EE
+ #3700B3
+ #03DAC5
+
diff --git a/examples/android/res/values/strings.xml b/examples/android/res/values/strings.xml
new file mode 100644
index 0000000..70e5284
--- /dev/null
+++ b/examples/android/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Ring WebView
+
diff --git a/examples/android/res/values/styles.xml b/examples/android/res/values/styles.xml
new file mode 100644
index 0000000..23eba66
--- /dev/null
+++ b/examples/android/res/values/styles.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/examples/android/ring/main.ring b/examples/android/ring/main.ring
new file mode 100644
index 0000000..6b4b497
--- /dev/null
+++ b/examples/android/ring/main.ring
@@ -0,0 +1,587 @@
+# Nota: glassmorphism notes app for Ring WebView on Android.
+# Ring owns the notes list and persists it to notes.db (length-prefixed
+# flat file in filesDir). The bridge carries JSON both ways, so Ring only
+# ever handles lists: binds wreturn the full state, JS re-renders from it.
+
+load "webview.ring"
+
+aNotes = []
+nNextId = 1
+cDbFile = "notes.db"
+
+aBindList = [
+ ["getState", :fetchNotes],
+ ["note_save", :storeNote],
+ ["note_delete", :removeNote],
+ ["note_pin", :toggleNotePin]
+]
+
+loadNotes()
+
+oWebView = new WebView()
+
+oWebView {
+ setTitle("Nota")
+
+ onLoad(:handleLoad)
+ onDomReady(:handleDomReady)
+ onClose(:handleClose)
+
+ bindMany(NULL)
+
+ setHtml(`
+
+
+
+
+
+ Nota
+
+
+
+
+
Nota.
loading
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `)
+
+ run()
+}
+
+see "Nota closed." + nl
+
+# --- Persistence: length-prefixed flat file, no escaping needed ---
+# note block: id LF titleLen:title bodyLen:body color LF pinned LF updated LF
+
+func loadNotes
+ if not fexists(cDbFile)
+ return
+ ok
+ c = read(cDbFile)
+ aPos = [1]
+ while aPos[1] <= len(c)
+ cId = takeLine(c, aPos)
+ if cId = ""
+ exit
+ ok
+ cTitle = takeField(c, aPos)
+ cBody = takeField(c, aPos)
+ cColor = takeLine(c, aPos)
+ cPinned = takeLine(c, aPos)
+ cUpdated = takeLine(c, aPos)
+ if cColor = "" or cPinned = "" or cUpdated = ""
+ exit
+ ok
+ aNotes + [number(cId), cTitle, cBody, cColor, number(cPinned), cUpdated]
+ if number(cId) >= nNextId
+ nNextId = number(cId) + 1
+ ok
+ end
+
+func takeLine(cText, aPos)
+ nStart = aPos[1]
+ nEnd = nStart
+ while nEnd <= len(cText) and ascii(cText[nEnd]) != 10
+ nEnd = nEnd + 1
+ end
+ cLine = ""
+ if nEnd > nStart
+ cLine = substr(cText, nStart, nEnd - nStart)
+ ok
+ aPos[1] = nEnd + 1
+ return cLine
+
+func takeField(cText, aPos)
+ nStart = aPos[1]
+ nColon = nStart
+ while nColon <= len(cText) and cText[nColon] != ":"
+ nColon = nColon + 1
+ end
+ if nColon > len(cText)
+ return ""
+ ok
+ nFieldLen = number(substr(cText, nStart, nColon - nStart))
+ nFieldStart = nColon + 1
+ cField = ""
+ if nFieldLen > 0
+ cField = substr(cText, nFieldStart, nFieldLen)
+ ok
+ aPos[1] = nFieldStart + nFieldLen
+ return cField
+
+func persistNotes
+ cOut = ""
+ for aNote in aNotes
+ cOut += "" + aNote[1] + char(10)
+ cOut += "" + len(aNote[2]) + ":" + aNote[2]
+ cOut += "" + len(aNote[3]) + ":" + aNote[3]
+ cOut += aNote[4] + char(10)
+ cOut += "" + aNote[5] + char(10)
+ cOut += aNote[6] + char(10)
+ next
+ write(cDbFile, cOut)
+
+# --- Binds: req[1] is the JS argument array; always wreturn the full state ---
+
+func fetchNotes(cId, aReq)
+ oWebView.wreturn(cId, WEBVIEW_ERROR_OK, aNotes)
+
+func storeNote(cId, aReq)
+ aData = aReq[1]
+ nId = number(aData[1])
+ cTitle = "" + aData[2]
+ cBody = "" + aData[3]
+ cColor = "" + aData[4]
+ nPinned = number(aData[5])
+ cStamp = date() + " " + time()
+ if nId = 0
+ aNotes + [nNextId, cTitle, cBody, cColor, nPinned, cStamp]
+ nNextId++
+ else
+ for i = 1 to len(aNotes)
+ if aNotes[i][1] = nId
+ aNotes[i][2] = cTitle
+ aNotes[i][3] = cBody
+ aNotes[i][4] = cColor
+ aNotes[i][5] = nPinned
+ aNotes[i][6] = cStamp
+ exit
+ ok
+ next
+ ok
+ persistNotes()
+ oWebView.wreturn(cId, WEBVIEW_ERROR_OK, aNotes)
+
+func removeNote(cId, aReq)
+ nId = number(aReq[1][1])
+ for i = 1 to len(aNotes)
+ if aNotes[i][1] = nId
+ del(aNotes, i)
+ exit
+ ok
+ next
+ persistNotes()
+ oWebView.wreturn(cId, WEBVIEW_ERROR_OK, aNotes)
+
+func toggleNotePin(cId, aReq)
+ nId = number(aReq[1][1])
+ for i = 1 to len(aNotes)
+ if aNotes[i][1] = nId
+ aNotes[i][5] = 1 - aNotes[i][5]
+ exit
+ ok
+ next
+ persistNotes()
+ oWebView.wreturn(cId, WEBVIEW_ERROR_OK, aNotes)
+
+# --- Events ---
+
+func handleLoad(cState)
+ see "load: " + cState + nl
+
+func handleDomReady
+ oWebView.evalJS("refresh()")
+
+func handleClose
+ see "close" + nl
diff --git a/examples/android/ring/src/assets.ring b/examples/android/ring/src/assets.ring
new file mode 100644
index 0000000..d066abc
--- /dev/null
+++ b/examples/android/ring/src/assets.ring
@@ -0,0 +1,60 @@
+# assets.ring — embedded-assets API for Ring WebView (embed.FS-style).
+#
+# In dev (`ring main.ring`) assets are read from disk ($cAssetsDir, default
+# "frontend/dist"). In an exe produced by webview-bake they are read from the
+# binary (zero-copy), via the exe-provided C functions:
+# assets_read / assets_exists / assets_count / assets_list
+# App code should always use the Assets* wrappers — never the primitives.
+
+$cAssetsDir = "frontend/dist"
+$bAssetsProbeInit = 0
+$bAssetsPacked = 0
+
+func IsPackedAssets
+ if $bAssetsProbeInit = 0
+ $bAssetsProbeInit = 1
+ $bAssetsPacked = iscfunction("assets_read")
+ ok
+ return $bAssetsPacked
+
+func AssetsDir cDir
+ $cAssetsDir = cDir
+
+func AssetsRead cPath
+ if IsPackedAssets()
+ return assets_read(cPath)
+ ok
+ return read($cAssetsDir + "/" + cPath)
+
+func AssetsExists cPath
+ if IsPackedAssets()
+ return assets_exists(cPath)
+ ok
+ return fexists($cAssetsDir + "/" + cPath)
+
+func AssetsList
+ aOut = []
+ if IsPackedAssets()
+ aOut = assets_list()
+ else
+ assetsScan($cAssetsDir, "", aOut)
+ ok
+ return sort(aOut)
+
+func assetsScan cAbsDir, cRel, aOut
+ # Mirrors webview-bake's walk: relative slash-separated keys, empty files skipped.
+ for aItem in dir(cAbsDir)
+ cName = substr(aItem[1], char(92), "/")
+ cKey = cName
+ if cRel != ""
+ cKey = cRel + "/" + cName
+ ok
+ cFull = cAbsDir + "/" + aItem[1]
+ if aItem[2] = 1
+ assetsScan(cFull, cKey, aOut)
+ else
+ if getfilesize(cFull) > 0
+ aOut + cKey
+ ok
+ ok
+ next
diff --git a/examples/android/ring/src/webview.rh b/examples/android/ring/src/webview.rh
new file mode 100644
index 0000000..f5547bc
--- /dev/null
+++ b/examples/android/ring/src/webview.rh
@@ -0,0 +1,26 @@
+WEBVIEW_HINT_NONE = get_WEBVIEW_HINT_NONE()
+WEBVIEW_HINT_MIN = get_WEBVIEW_HINT_MIN()
+WEBVIEW_HINT_MAX = get_WEBVIEW_HINT_MAX()
+WEBVIEW_HINT_FIXED = get_WEBVIEW_HINT_FIXED()
+WEBVIEW_NATIVE_HANDLE_KIND_UI_WINDOW = get_WEBVIEW_NATIVE_HANDLE_KIND_UI_WINDOW()
+WEBVIEW_NATIVE_HANDLE_KIND_UI_WIDGET = get_WEBVIEW_NATIVE_HANDLE_KIND_UI_WIDGET()
+WEBVIEW_NATIVE_HANDLE_KIND_BROWSER_CONTROLLER = get_WEBVIEW_NATIVE_HANDLE_KIND_BROWSER_CONTROLLER()
+WEBVIEW_ERROR_OK = get_WEBVIEW_ERROR_OK()
+WEBVIEW_ERROR_UNSPECIFIED = get_WEBVIEW_ERROR_UNSPECIFIED()
+WEBVIEW_ERROR_INVALID_ARGUMENT = get_WEBVIEW_ERROR_INVALID_ARGUMENT()
+WEBVIEW_ERROR_INVALID_STATE = get_WEBVIEW_ERROR_INVALID_STATE()
+WEBVIEW_ERROR_CANCELED = get_WEBVIEW_ERROR_CANCELED()
+WEBVIEW_ERROR_MISSING_DEPENDENCY = get_WEBVIEW_ERROR_MISSING_DEPENDENCY()
+WEBVIEW_ERROR_DUPLICATE = get_WEBVIEW_ERROR_DUPLICATE()
+WEBVIEW_ERROR_NOT_FOUND = get_WEBVIEW_ERROR_NOT_FOUND()
+WEBVIEW_VERSION_MAJOR = get_WEBVIEW_VERSION_MAJOR()
+WEBVIEW_VERSION_MINOR = get_WEBVIEW_VERSION_MINOR()
+WEBVIEW_VERSION_PATCH = get_WEBVIEW_VERSION_PATCH()
+WEBVIEW_EDGE_TOP = get_webview_edge_top()
+WEBVIEW_EDGE_BOTTOM = get_webview_edge_bottom()
+WEBVIEW_EDGE_LEFT = get_webview_edge_left()
+WEBVIEW_EDGE_RIGHT = get_webview_edge_right()
+WEBVIEW_EDGE_TOP_LEFT = get_webview_edge_top_left()
+WEBVIEW_EDGE_TOP_RIGHT = get_webview_edge_top_right()
+WEBVIEW_EDGE_BOTTOM_LEFT = get_webview_edge_bottom_left()
+WEBVIEW_EDGE_BOTTOM_RIGHT = get_webview_edge_bottom_right()
\ No newline at end of file
diff --git a/examples/android/ring/src/webview.ring b/examples/android/ring/src/webview.ring
new file mode 100644
index 0000000..eb806bc
--- /dev/null
+++ b/examples/android/ring/src/webview.ring
@@ -0,0 +1,780 @@
+# This file is part of the Ring WebView library.
+
+
+/*
+ * aWebViewConfig: Global configuration for the WebView class.
+ * Fields:
+ * :debug - (Boolean) Enable debug mode for the WebView (default: true).
+ * :window - (Pointer/NULL) Native window handle to associate with the WebView (default: NULL).
+ * Set to NULL to let the library create its own window.
+ */
+aWebViewConfig = [
+ :debug = true,
+ :window = NULL
+]
+
+/*
+ * Internal global list to store objects for method bindings (used by eval in bind()).
+*/
+__aWebViewObjects = []
+
+/**
+ * Class WebView: Represents a webview instance for displaying HTML content.
+ * Provides methods for binding Ring functions to JavaScript, navigating URLs,
+ * and managing the webview lifecycle.
+ */
+Class WebView
+
+ _pWebView
+ _bindings = []
+ _isDestroyed = false
+
+ /**
+ * Initializes the WebView instance using global configuration.
+ */
+ func init
+ self._pWebView = webview_create(aWebViewConfig[:debug], aWebViewConfig[:window])
+ if isNull(self._pWebView) or not isPointer(self._pWebView)
+ raise("Failed to create webview instance.")
+ ok
+
+ # Automatically bind global `aBindList` if it exists.
+ bindMany(NULL)
+
+ /**
+ * Checks if the webview has been destroyed.
+ * @return true if destroyed, false otherwise.
+ */
+ func isDestroyed()
+ return self._isDestroyed
+
+ /**
+ * Runs the webview event loop (blocking call).
+ * Destroys the webview after the loop ends.
+ */
+ func run()
+ if self.isDestroyed()
+ return
+ ok
+ webview_run(self._pWebView)
+ # After run() finishes, the window is closed, so we must destroy.
+ self.destroy()
+
+ /**
+ * Destroys the webview instance and releases resources.
+ */
+ func destroy()
+ if self.isDestroyed()
+ return
+ ok
+
+ self._isDestroyed = true
+
+ webview_destroy(self._pWebView)
+
+ self._pWebView = NULL
+ self._bindings = []
+
+ func terminate()
+ if self.isDestroyed()
+ return
+ ok
+ webview_terminate(self._pWebView)
+
+ /**
+ * Dispatches code to run on the main thread.
+ * @param cCode Code to execute.
+ * @return Result of dispatch.
+ */
+ func dispatch(cCode)
+ if self.isDestroyed()
+ return
+ ok
+
+ return webview_dispatch(self._pWebView, cCode)
+
+ /**
+ * Binds a Ring function or object methods to JavaScript.
+ *
+ * For simple functions:
+ * bind(jsName, ringFuncName)
+ *
+ * For object methods:
+ * bind(oObject, aMethods)
+ *
+ * @param p1 For functions, the JS name. For objects, the Ring object.
+ * @param p2 For functions, the Ring function name. For objects, a list of method pairs.
+ * @return Binding result pointer for simple functions, or nothing for objects.
+ */
+ func bind(p1, p2)
+ if self.isDestroyed()
+ return
+ ok
+
+ # Check for object binding
+ if isObject(p1) and isList(p2)
+ oObject = p1
+ aMethods = p2
+
+ # Store object and get its index
+ nObjectIndex = find(__aWebViewObjects, oObject)
+
+ if nObjectIndex = 0
+ add(__aWebViewObjects, ref(oObject))
+ nObjectIndex = len(__aWebViewObjects)
+ ok
+
+ for aMethodInfo in aMethods
+ if isList(aMethodInfo) and len(aMethodInfo) = 2 and isString(aMethodInfo[1]) and isString(aMethodInfo[2])
+ cJSName = aMethodInfo[1]
+ cMethodName = aMethodInfo[2]
+
+ # Create wrapper function
+ cFunc = eval(print2str(`
+ return func (id, req) { __aWebViewObjects[#{nObjectIndex}].#{cMethodName}(id,req) }
+ `))
+
+ # Bind the generated function
+ self.bind(cJSName, cFunc)
+ else
+ see "Warning: Invalid method definition." + nl
+ ok
+ next
+ else
+ # Simple function binding
+ jsName = p1
+ ringFuncName = p2
+ aBindResult = webview_bind(self._pWebView, jsName, ringFuncName)
+
+ if isPointer(aBindResult)
+ add(self._bindings, aBindResult)
+ ok
+ return aBindResult
+ ok
+
+ /**
+ * Binds multiple Ring functions or object methods to JavaScript.
+ *
+ * This function accepts a list containing a mix of binding types:
+ * - For simple functions: `["jsFunctionName", "ringFunctionName"]`
+ * - For object methods: `[ringObject, [["jsFunctionName", "methodName"], ...]]`
+ *
+ * If `aList` is `NULL`, the function will attempt to use a global list
+ * named `aBindList` as the source of bindings.
+ *
+ * @param aList A list of bindings. If `NULL`, uses the global `aBindList`.
+ */
+ func bindMany(aList)
+ if self.isDestroyed()
+ return
+ ok
+
+ if isNull(aList)
+ if isGlobal("aBindList")
+ aList = aBindList
+ else
+ return # No bindings to process
+ ok
+ ok
+
+ if isList(aList)
+ for aItem in aList
+ if isList(aItem) and len(aItem) = 2
+ self.bind(aItem[1], aItem[2])
+ ok
+ next
+ ok
+
+ /**
+ * Unbinds a JavaScript function name.
+ * @param jsName JavaScript function name.
+ * @return Result of unbind.
+ */
+ func unbind(jsName)
+ if self.isDestroyed()
+ return
+ ok
+
+ return webview_unbind(self._pWebView, jsName)
+
+ /**
+ * Gets the native window handle.
+ * @return Native window handle.
+ */
+ func getWindow()
+ if self.isDestroyed()
+ return
+ ok
+
+ return webview_get_window(self._pWebView)
+
+ /**
+ * Gets the native handle of a specific kind.
+ * @param kind Type of native handle.
+ * @return Native handle.
+ */
+ func getNativeHandle(kind)
+ if self.isDestroyed()
+ return
+ ok
+
+ return webview_get_native_handle(self._pWebView, kind)
+
+ /**
+ * Sets the window title.
+ * @param title Window title string.
+ */
+ func setTitle(title)
+ if self.isDestroyed()
+ return
+ ok
+
+ webview_set_title(self._pWebView, title)
+
+ /**
+ * Sets the window size.
+ * @param width Window width.
+ * @param height Window height.
+ * @param hint Size hint.
+ */
+ func setSize(width, height, hint)
+ if self.isDestroyed()
+ return
+ ok
+
+ webview_set_size(self._pWebView, width, height, hint)
+
+ /**
+ * Navigates to a URL.
+ * @param url URL to navigate to.
+ */
+ func navigate(url)
+ if self.isDestroyed()
+ return
+ ok
+
+ webview_navigate(self._pWebView, url)
+
+ /**
+ * Sets the HTML content of the webview.
+ * @param html HTML string.
+ */
+ func setHtml(html)
+ if self.isDestroyed()
+ return
+ ok
+
+ webview_set_html(self._pWebView, html)
+
+ /**
+ * Injects JavaScript code to run on initialization.
+ * @param js JavaScript code string.
+ */
+ func injectJS(js)
+ if self.isDestroyed()
+ return
+ ok
+
+ webview_init(self._pWebView, js)
+
+ /**
+ * Evaluates JavaScript code in the webview.
+ * @param js JavaScript code string.
+ */
+ func evalJS(js)
+ if self.isDestroyed()
+ return
+ ok
+
+ webview_eval(self._pWebView, js)
+
+ /**
+ * Returns a result to a JavaScript callback.
+ * @param id Callback id.
+ * @param result Result value e.g. WEBVIEW_ERROR_OK.
+ * @param json JSON string.
+ */
+ func wreturn(id, result, json)
+ if self.isDestroyed()
+ return
+ ok
+
+ webview_return(self._pWebView, id, result, json)
+
+ /**
+ * Enables or disables window decorations (title bar, borders).
+ * @param decorated True to show decorations, false for frameless window.
+ * @return 1 on success, 0 on failure.
+ */
+ func setDecorated(decorated)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_set_decorated(self._pWebView, decorated)
+
+ /**
+ * Sets the window transparency level.
+ * @param opacity Value between 0.0 (transparent) and 1.0 (opaque).
+ * @return 1 on success, 0 on failure.
+ */
+ func setOpacity(opacity)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_set_opacity(self._pWebView, opacity)
+
+ /**
+ * Sets whether the window should stay above all other windows.
+ * @param onTop True to keep on top, false for normal behavior.
+ * @return 1 on success, 0 on failure. Not supported on Linux/GTK4.
+ */
+ func setAlwaysOnTop(onTop)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_set_always_on_top(self._pWebView, onTop)
+
+ /**
+ * Minimizes the window to the taskbar/dock.
+ * @return 1 on success, 0 on failure.
+ */
+ func minimize()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_minimize(self._pWebView)
+
+ /**
+ * Maximizes the window to fill the screen.
+ * @return 1 on success, 0 on failure.
+ */
+ func maximize()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_maximize(self._pWebView)
+
+ /**
+ * Restores the window from minimized or maximized state.
+ * @return 1 on success, 0 on failure.
+ */
+ func restore()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_restore(self._pWebView)
+
+ /**
+ * Checks if the window is currently maximized.
+ * @return 1 if maximized, 0 if not.
+ */
+ func isMaximized()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_is_maximized(self._pWebView)
+
+ /**
+ * Initiates window dragging. Call from a mouse-down event on custom title bar.
+ * @return 1 on success, 0 on failure.
+ */
+ func startDrag()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_start_drag(self._pWebView)
+
+ /**
+ * Moves the window to the specified screen coordinates.
+ * @param x The x-coordinate.
+ * @param y The y-coordinate.
+ * @return 1 on success, 0 on failure. Not supported on Linux/Wayland.
+ */
+ func setPosition(x, y)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_set_position(self._pWebView, x, y)
+
+ /**
+ * Gets the current window position.
+ * @return A list [x, y] containing the window coordinates.
+ */
+ func getPosition()
+ if self.isDestroyed()
+ return [0, 0]
+ ok
+ return webview_get_position(self._pWebView)
+
+ /**
+ * Gets the current window size.
+ * @return A list [width, height] containing the window dimensions.
+ */
+ func getSize()
+ if self.isDestroyed()
+ return [0, 0]
+ ok
+ return webview_get_size(self._pWebView)
+
+ /**
+ * Brings the window to the front and gives it focus.
+ * @return 1 on success, 0 on failure.
+ */
+ func focus()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_focus(self._pWebView)
+
+ /**
+ * Hides the window without destroying it.
+ * @return 1 on success, 0 on failure.
+ */
+ func hide()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_hide(self._pWebView)
+
+ /**
+ * Shows a previously hidden window.
+ * @return 1 on success, 0 on failure.
+ */
+ func show()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_show(self._pWebView)
+
+ /**
+ * Initiates window resizing from the specified edge.
+ * @param edge The edge constant (e.g., WEBVIEW_EDGE_RIGHT).
+ * @return 1 on success, 0 on failure.
+ */
+ func startResize(edge)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_start_resize(self._pWebView, edge)
+
+ /**
+ * Enables or disables fullscreen mode.
+ * @param fullscreen True for fullscreen, false for windowed mode.
+ * @return 1 on success, 0 on failure.
+ */
+ func setFullscreen(fullscreen)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_set_fullscreen(self._pWebView, fullscreen)
+
+ /**
+ * Checks if the window is currently in fullscreen mode.
+ * @return 1 if fullscreen, 0 if windowed.
+ */
+ func isFullscreen()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_is_fullscreen(self._pWebView)
+
+ /**
+ * Enables or disables window resizing by the user.
+ * @param resizable True to allow resizing, false to prevent it.
+ * @return 1 on success, 0 on failure.
+ */
+ func setResizable(resizable)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_set_resizable(self._pWebView, resizable)
+
+ /**
+ * Checks if the window is currently resizable.
+ * @return 1 if resizable, 0 if not.
+ */
+ func isResizable()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_is_resizable(self._pWebView)
+
+ /**
+ * Sets the minimum size constraint for the window.
+ * @param width Minimum width in pixels.
+ * @param height Minimum height in pixels.
+ * @return 1 on success, 0 on failure.
+ */
+ func setMinSize(width, height)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_set_min_size(self._pWebView, width, height)
+
+ /**
+ * Sets the maximum size constraint for the window.
+ * @param width Maximum width in pixels.
+ * @param height Maximum height in pixels.
+ * @return 1 on success, 0 on failure.
+ */
+ func setMaxSize(width, height)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_set_max_size(self._pWebView, width, height)
+
+ /**
+ * Sets the background color of the webview.
+ * @param r Red component (0-255).
+ * @param g Green component (0-255).
+ * @param b Blue component (0-255).
+ * @param a Alpha component (0-255). 0 = transparent, 255 = opaque.
+ * @return 1 on success, 0 on failure.
+ */
+ func setBackgroundColor(r, g, b, a)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_set_background_color(self._pWebView, r, g, b, a)
+
+ /**
+ * Navigates back in the browsing history.
+ * @return 1 on success, 0 on failure.
+ */
+ func back()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_back(self._pWebView)
+
+ /**
+ * Navigates forward in the browsing history.
+ * @return 1 on success, 0 on failure.
+ */
+ func forward()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_forward(self._pWebView)
+
+ /**
+ * Reloads the current page.
+ * @return 1 on success, 0 on failure.
+ */
+ func reload()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_reload(self._pWebView)
+
+ /**
+ * Shows or hides the developer tools (inspector).
+ * @param enabled True to show dev tools, false to hide.
+ * @return 1 on success, 0 on failure.
+ */
+ func setDevTools(enabled)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_set_dev_tools(self._pWebView, enabled)
+
+ /**
+ * Gets the URL of the currently loaded page.
+ * @return The current URL string, or empty string if not available.
+ */
+ func getUrl()
+ if self.isDestroyed()
+ return ""
+ ok
+ return webview_get_url(self._pWebView)
+
+ /**
+ * Gets the title of the currently loaded page.
+ * @return The page title string, or empty string if not available.
+ */
+ func getPageTitle()
+ if self.isDestroyed()
+ return ""
+ ok
+ return webview_get_title(self._pWebView)
+
+ /**
+ * Enables or disables the default browser context menu (right-click menu).
+ * @param enabled True to allow context menu, false to suppress it.
+ * @return 1 on success, 0 on failure.
+ */
+ func setContextMenu(enabled)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_set_context_menu(self._pWebView, enabled)
+
+ /**
+ * Sets the window icon from a file.
+ * @param iconPath Path to the icon file (PNG, ICO, etc.).
+ * @return 1 on success, 0 on failure.
+ */
+ func setIcon(iconPath)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_set_icon(self._pWebView, iconPath)
+
+ /**
+ * Checks if the window currently has focus.
+ * @return 1 if focused, 0 if not.
+ */
+ func isFocused()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_is_focused(self._pWebView)
+
+ /**
+ * Checks if the window is currently visible.
+ * @return 1 if visible, 0 if hidden.
+ */
+ func isVisible()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_is_visible(self._pWebView)
+
+ /**
+ * Programmatically closes the window.
+ * @return 1 on success, 0 on failure.
+ */
+ func closeWindow()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_close(self._pWebView)
+
+ /**
+ * Forces dark mode for the application UI.
+ * @param enabled True to force dark mode, false for system default.
+ * @return 1 on success, 0 on failure. Only supported on Linux with libadwaita.
+ */
+ func setForceDark(enabled)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_set_force_dark(self._pWebView, enabled)
+
+ /**
+ * Checks if dark mode is forced.
+ * @return 1 if dark mode is forced, 0 if not.
+ */
+ func isForceDark()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_is_force_dark(self._pWebView)
+
+ /**
+ * Gets information about all connected monitors/screens.
+ * @return A list of lists. Each: [name, x, y, width, height].
+ */
+ func getScreens()
+ if self.isDestroyed()
+ return []
+ ok
+ return webview_get_screens(self._pWebView)
+
+ /**
+ * Enables or disables click-through mode (mouse events pass through).
+ * @param enabled True to enable click-through, false to disable.
+ * @return 1 on success, 0 on failure. Only supported on Windows.
+ */
+ func setClickThrough(enabled)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_set_click_through(self._pWebView, enabled)
+
+ /**
+ * Checks if click-through mode is enabled.
+ * @return 1 if enabled, 0 if not.
+ */
+ func isClickThrough()
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_is_click_through(self._pWebView)
+
+ /**
+ * Sets a callback for when the window close is requested.
+ * @param callback Name of the Ring function: func name(cData).
+ * @return 1 on success, 0 on failure.
+ */
+ func onClose(callback)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_on_close(self._pWebView, callback)
+
+ /**
+ * Sets a callback for when the window is resized.
+ * @param callback Name of the Ring function: func name(cSize) where cSize is "WIDTHxHEIGHT".
+ * @return 1 on success, 0 on failure.
+ */
+ func onResize(callback)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_on_resize(self._pWebView, callback)
+
+ /**
+ * Sets a callback for when the window gains or loses focus.
+ * @param callback Name of the Ring function: func name(cFocused) where cFocused is "true" or "false".
+ * @return 1 on success, 0 on failure.
+ */
+ func onFocus(callback)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_on_focus(self._pWebView, callback)
+
+ /**
+ * Sets a callback for when the DOM is ready.
+ * @param callback Name of the Ring function: func name(cData).
+ * @return 1 on success, 0 on failure.
+ */
+ func onDomReady(callback)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_on_dom_ready(self._pWebView, callback)
+
+ /**
+ * Sets a callback for when a page load starts or finishes.
+ * @param callback Name of the Ring function: func name(cState) where cState is "started" or "finished".
+ * @return 1 on success, 0 on failure.
+ */
+ func onLoad(callback)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_on_load(self._pWebView, callback)
+
+ /**
+ * Sets a callback for when navigation occurs.
+ * @param callback Name of the Ring function: func name(cUrl).
+ * @return 1 on success, 0 on failure.
+ */
+ func onNavigate(callback)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_on_navigate(self._pWebView, callback)
+
+ /**
+ * Sets a callback for when the page title changes.
+ * @param callback Name of the Ring function: func name(cTitle).
+ * @return 1 on success, 0 on failure.
+ */
+ func onTitle(callback)
+ if self.isDestroyed()
+ return 0
+ ok
+ return webview_on_title(self._pWebView, callback)
\ No newline at end of file
diff --git a/examples/android/ring/webview.ring b/examples/android/ring/webview.ring
new file mode 100644
index 0000000..64fe65e
--- /dev/null
+++ b/examples/android/ring/webview.ring
@@ -0,0 +1,5 @@
+# This file is part of the Ring WebView library.
+
+# Load the Ring WebView Class and Constants.
+load "src/webview.ring"
+load "src/webview.rh"
diff --git a/examples/android/ring2apk.ring b/examples/android/ring2apk.ring
new file mode 100644
index 0000000..a71e2c3
--- /dev/null
+++ b/examples/android/ring2apk.ring
@@ -0,0 +1,41 @@
+/*
+ ring2apk configuration file — Ring WebView Android example
+*/
+
+# App configuration
+Ring2ApkConfig = [
+ # App identity
+ :name = "webviewdemo",
+ :packageId = "io.github.ysdragon.webview",
+ :versionCode = 1,
+ :versionName = "1.0.0",
+
+ # Android SDK versions
+ :minSdk = 21,
+ :targetSdk = 36,
+ :compileSdk = 36,
+
+ # Target architectures
+ :targets = ["arm64-v8a"],
+
+ # Directories
+ :assetsDir = "assets",
+ :resDir = "res",
+ :srcDir = "src",
+ :outputDir = "build",
+
+ # Entry point Ring file
+ :entryPoint = "main.ring",
+
+ :ringSrcDir = "ring",
+
+ # App display settings
+ :label = "Ring WebView",
+ :orientation = "unspecified",
+ :theme = "@style/AppTheme",
+
+ # Permissions
+ :permissions = [
+ "android.permission.INTERNET"
+ ]
+]
diff --git a/examples/android/src/cpp/CMakeLists.txt b/examples/android/src/cpp/CMakeLists.txt
new file mode 100644
index 0000000..29438cc
--- /dev/null
+++ b/examples/android/src/cpp/CMakeLists.txt
@@ -0,0 +1,45 @@
+cmake_minimum_required(VERSION 3.22)
+project(myapp C)
+
+# Workaround for large file support on some Android ABIs
+if((ANDROID_ABI STREQUAL "armeabi-v7a" OR ANDROID_ABI STREQUAL "x86") AND ANDROID_PLATFORM_LEVEL LESS 24)
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -U_FILE_OFFSET_BITS")
+endif()
+
+# ============================================================================
+# Ring Language (build from source)
+# ============================================================================
+set(RING_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ring")
+if(NOT EXISTS "${RING_DIR}/src")
+ message(FATAL_ERROR "Ring sources not found! Expected at ${RING_DIR}/src")
+endif()
+
+file(GLOB RING_SOURCES ${RING_DIR}/src/*.c)
+
+add_library(ring STATIC ${RING_SOURCES})
+target_include_directories(ring PUBLIC "${RING_DIR}/include")
+target_link_libraries(ring PUBLIC android log)
+
+# ============================================================================
+# Main Application
+# ============================================================================
+add_library(main SHARED
+ main.c
+ ${CMAKE_SOURCE_DIR}/../../build/gen/ringappcode.c
+)
+
+target_include_directories(main PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_SOURCE_DIR}/../../build/gen
+ ${RING_DIR}/include
+)
+
+target_link_libraries(main PRIVATE
+ ring
+ android
+ log
+)
+
+add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../../../.." "${CMAKE_BINARY_DIR}/ring_webview")
+
+target_link_libraries(main PRIVATE ring_webview_android)
\ No newline at end of file
diff --git a/examples/android/src/cpp/main.c b/examples/android/src/cpp/main.c
new file mode 100644
index 0000000..0516fd8
--- /dev/null
+++ b/examples/android/src/cpp/main.c
@@ -0,0 +1,79 @@
+/* main.c — Ring app entry point for the Android WebView example.
+ *
+ * Owns ALL Ring loading. The backend (ring_webview_android.c) keeps only
+ * JNI; its worker thread calls ring_app_main() and never touches Ring
+ * loading directly.
+ *
+ * Two responsibilities:
+ *
+ * 1. ring_vm_extension() — replicates Ring's ext.c verbatim and appends
+ * the webview backend's ringlib_init() so the webview_* functions are
+ * registered when the VM starts. This file replaces ext.c in the Ring
+ * source list (see CMakeLists.txt), keeping Ring sources pristine.
+ *
+ * 2. ring_app_main() — creates the Ring state and runs the embedded
+ * bytecode from ringappcode.c (generated by `ring -geo -norun
+ * main.ring`, which compiles main.ring and every transitive load into
+ * one self-contained bytecode — no .ring files are read at runtime).
+ */
+
+#include "ring.h"
+#include "ringappcode.h"
+
+/* Provided by ring_webview_android.c */
+extern void ringlib_init(RingState *pRingState);
+
+/* ------------------------------------------------------------------ */
+/* 1. Ring VM extension hook (replaces ext.c) */
+/* ------------------------------------------------------------------ */
+
+void ring_vm_extension(RingState *pRingState) {
+/* List */
+#if RING_VM_LISTFUNCS
+ ring_vm_list_loadfunctions(pRingState);
+#endif
+/* Math */
+#if RING_VM_MATH
+ ring_vm_math_loadfunctions(pRingState);
+#endif
+/* File */
+#if RING_VM_FILE
+ ring_vm_file_loadfunctions(pRingState);
+#endif
+/* OS */
+#if RING_VM_OS
+ ring_vm_os_loadfunctions(pRingState);
+#endif
+/* DLL */
+#if RING_VM_DLL
+ ring_vm_dll_loadfunctions(pRingState);
+#endif
+/* Reflection and Meta-programming */
+#if RING_VM_REFMETA
+ ring_vm_refmeta_loadfunctions(pRingState);
+#endif
+/* VM Information */
+#if RING_VM_INFO
+ ring_vm_info_loadfunctions(pRingState);
+#endif
+/* WebView backend */
+ ringlib_init(pRingState);
+}
+
+/* ------------------------------------------------------------------ */
+/* 2. App entry point — called by the backend's worker thread */
+/* ------------------------------------------------------------------ */
+
+/* Creates the Ring state and runs the embedded bytecode. Returns only
+ * when the Ring program exits (i.e. webview_run() returns and main()
+ * finishes). The backend captures the live RingState itself inside
+ * ring_webview_create() via RING_API_STATE, so no state is passed back. */
+void ring_app_main(void) {
+ RingState *pRingState = ring_state_new();
+ if (!pRingState) {
+ return;
+ }
+ pRingState->lRun = 1;
+ ringappcode_run(pRingState);
+ ring_state_delete(pRingState);
+}
diff --git a/examples/android/src/java/io/github/ysdragon/webview/MainActivity.java b/examples/android/src/java/io/github/ysdragon/webview/MainActivity.java
new file mode 100644
index 0000000..5fa51e2
--- /dev/null
+++ b/examples/android/src/java/io/github/ysdragon/webview/MainActivity.java
@@ -0,0 +1,464 @@
+/*
+ * MainActivity.java
+ * Copyright (c) Youssef Saeed
+ * All rights reserved.
+ */
+package io.github.ysdragon.webview;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.graphics.Color;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Looper;
+import android.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowInsetsController;
+import android.view.WindowManager;
+import android.webkit.ConsoleMessage;
+import android.webkit.JavascriptInterface;
+import android.webkit.JsPromptResult;
+import android.webkit.JsResult;
+import android.webkit.WebChromeClient;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.EditText;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+// Android host: owns the WebView on the UI thread. Ring VM runs on a native
+// worker thread; all cross-talk goes through ring_webview_android.c.
+public class MainActivity extends Activity {
+
+ private static final String TAG = "RingWebView";
+ private static final long SYNC_TIMEOUT_MS = 5000;
+
+ static {
+ System.loadLibrary("main");
+ }
+
+ private WebView webView;
+ private volatile String injectJs = "";
+ private volatile boolean destroyed = false;
+ // Last URL / HTML (crash reload; reset on rotation). Last size (resize on change).
+ // UI thread only.
+ private String lastUrl = "";
+ private String lastHtml = "";
+ private int lastW = -1, lastH = -1;
+ // Crash-loop guard: a poison page must not rebuild forever.
+ private int crashCount = 0;
+ private long lastCrashMs = 0;
+
+ /* ------------------------------------------------------------------ */
+ /* Lifecycle */
+ /* ------------------------------------------------------------------ */
+
+ @SuppressLint("SetJavaScriptEnabled")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ Window window = getWindow();
+ window.setStatusBarColor(Color.TRANSPARENT);
+ if (Build.VERSION.SDK_INT >= 30) {
+ window.setDecorFitsSystemWindows(false);
+ } else {
+ int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+ window.getDecorView().setSystemUiVisibility(flags);
+ }
+
+ webView = createWebView();
+ setContentView(webView);
+
+ // Dark page: keep light status-bar icons.
+ if (Build.VERSION.SDK_INT >= 30) {
+ WindowInsetsController controller = window.getInsetsController();
+ if (controller != null) {
+ controller.setSystemBarsAppearance(
+ 0,
+ WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS);
+ }
+ }
+
+ nativeStart();
+ }
+
+ // Fully configured WebView. Shared by onCreate and crash recovery.
+ // UI thread only.
+ private WebView createWebView() {
+ final WebView v = new WebView(this);
+
+ WebSettings settings = v.getSettings();
+ settings.setJavaScriptEnabled(true);
+ settings.setDomStorageEnabled(true);
+ settings.setAllowFileAccess(true);
+ settings.setAllowContentAccess(true);
+ settings.setMediaPlaybackRequiresUserGesture(false);
+ settings.setLoadWithOverviewMode(true);
+ settings.setUseWideViewPort(true);
+
+ v.setWebViewClient(new WebViewClient() {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ return false;
+ }
+
+ @Override
+ public void onPageStarted(WebView view, String url, android.graphics.Bitmap favicon) {
+ if (url != null) {
+ lastUrl = url;
+ // Real navigation wins over earlier setHtml content.
+ if (!url.equals("about:blank")) lastHtml = "";
+ }
+ nativeOnPageStarted(url);
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ String js = injectJs;
+ if (js != null && js.length() > 0) {
+ view.evaluateJavascript(js, null);
+ }
+ nativeOnPageFinished(url);
+ }
+
+ @Override
+ public boolean onRenderProcessGone(WebView view, android.webkit.RenderProcessGoneDetail detail) {
+ return handleRenderCrash(view, detail);
+ }
+ });
+
+ v.setWebChromeClient(new WebChromeClient() {
+ @Override
+ public void onReceivedTitle(WebView view, String title) {
+ nativeOnTitleChanged(title);
+ }
+
+ @Override
+ public boolean onConsoleMessage(ConsoleMessage cm) {
+ Log.i(TAG, "JS: " + cm.message() + " @" + cm.sourceId() + ":" + cm.lineNumber());
+ return true;
+ }
+
+ @Override
+ public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
+ new AlertDialog.Builder(MainActivity.this, android.R.style.Theme_DeviceDefault_Dialog_Alert)
+ .setMessage(message)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ result.confirm();
+ }
+ })
+ .setCancelable(false)
+ .show();
+ return true;
+ }
+
+ @Override
+ public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
+ new AlertDialog.Builder(MainActivity.this, android.R.style.Theme_DeviceDefault_Dialog_Alert)
+ .setMessage(message)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ result.confirm();
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ result.cancel();
+ }
+ })
+ .setCancelable(false)
+ .show();
+ return true;
+ }
+
+ @Override
+ public boolean onJsPrompt(WebView view, String url, String message, String defaultValue,
+ final JsPromptResult result) {
+ final EditText input = new EditText(MainActivity.this);
+ input.setText(defaultValue);
+ new AlertDialog.Builder(MainActivity.this, android.R.style.Theme_DeviceDefault_Dialog_Alert)
+ .setMessage(message)
+ .setView(input)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ result.confirm(input.getText().toString());
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ result.cancel();
+ }
+ })
+ .setCancelable(false)
+ .show();
+ return true;
+ }
+ });
+
+ v.addJavascriptInterface(new RingBridge(), "RingBridge");
+
+ v.getViewTreeObserver().addOnGlobalLayoutListener(new android.view.ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ int w = v.getWidth(), h = v.getHeight();
+ if (w != lastW || h != lastH) {
+ lastW = w;
+ lastH = h;
+ if (w > 0 && h > 0) nativeOnResize(w, h);
+ }
+ }
+ });
+
+ return v;
+ }
+
+ // Renderer gone: destroy the dead view, build an identical one, reload.
+ // Inject JS persists, so bindings come back with the page. True = handled.
+ // API 26+ only.
+ private boolean handleRenderCrash(WebView view, android.webkit.RenderProcessGoneDetail detail) {
+ boolean crashed = detail != null && detail.didCrash();
+ Log.w(TAG, "WebView render process " + (crashed ? "crashed" : "killed by system")
+ + "; rebuilding view, lastUrl=" + lastUrl);
+ if (view != null) view.destroy();
+ lastW = lastH = -1;
+ webView = createWebView();
+ setContentView(webView);
+ long now = System.currentTimeMillis();
+ if (now - lastCrashMs > 10000) crashCount = 0;
+ lastCrashMs = now;
+ // Same page crashing repeatedly: rebuild empty, stop reloading it.
+ if (++crashCount > 3) {
+ Log.e(TAG, "WebView crashed repeatedly; showing empty view");
+ return true;
+ }
+ if (!lastHtml.isEmpty()) renderHtml(webView, lastHtml);
+ else if (!lastUrl.isEmpty()) webView.loadUrl(lastUrl);
+ return true;
+ }
+
+ private void renderHtml(WebView v, String html) {
+ if (v != null) v.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null);
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ nativeOnFocusChanged(hasFocus);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (webView != null && webView.canGoBack()) {
+ webView.goBack();
+ } else {
+ nativeOnClose();
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ // Rotation destroys too; only a real finish kills the worker.
+ if (isFinishing()) {
+ destroyed = true;
+ nativeDestroy();
+ }
+ if (webView != null) {
+ webView.destroy();
+ webView = null;
+ }
+ super.onDestroy();
+ }
+
+ /* ------------------------------------------------------------------ */
+ /* Called from native (worker thread unless noted) */
+ /* ------------------------------------------------------------------ */
+
+ public void evalJs(final String js) {
+ if (destroyed || webView == null) return;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (webView != null) webView.evaluateJavascript(js, null);
+ }
+ });
+ }
+
+ public void loadUrl(final String url) {
+ if (destroyed || webView == null) return;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (webView != null) webView.loadUrl(url);
+ }
+ });
+ }
+
+ public void loadHtml(final String html) {
+ if (destroyed || webView == null) return;
+ lastHtml = (html == null) ? "" : html;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ renderHtml(webView, html);
+ }
+ });
+ }
+
+ public void goBack() {
+ if (destroyed || webView == null) return;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (webView != null && webView.canGoBack()) webView.goBack();
+ }
+ });
+ }
+
+ public void goForward() {
+ if (destroyed || webView == null) return;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (webView != null && webView.canGoForward()) webView.goForward();
+ }
+ });
+ }
+
+ public void reload() {
+ if (destroyed || webView == null) return;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (webView != null) webView.reload();
+ }
+ });
+ }
+
+ // Sync URL query; blocks the caller on the UI thread.
+ public String getUrlSync() {
+ if (destroyed || webView == null) return "";
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ String u = webView.getUrl();
+ return u == null ? "" : u;
+ }
+ final AtomicReference ref = new AtomicReference("");
+ final CountDownLatch latch = new CountDownLatch(1);
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ String u = (webView != null) ? webView.getUrl() : null;
+ ref.set(u == null ? "" : u);
+ latch.countDown();
+ }
+ });
+ try {
+ boolean ok = latch.await(SYNC_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ if (!ok) {
+ Log.w(TAG, "getUrlSync timed out after " + SYNC_TIMEOUT_MS + "ms; returning \"\"");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ Log.w(TAG, "getUrlSync interrupted; returning \"\"");
+ }
+ return ref.get();
+ }
+
+ // Sync title query; blocks the caller on the UI thread.
+ public String getTitleSync() {
+ if (destroyed || webView == null) return "";
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ String t = webView.getTitle();
+ return t == null ? "" : t;
+ }
+ final AtomicReference ref = new AtomicReference("");
+ final CountDownLatch latch = new CountDownLatch(1);
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ String t = (webView != null) ? webView.getTitle() : null;
+ ref.set(t == null ? "" : t);
+ latch.countDown();
+ }
+ });
+ try {
+ boolean ok = latch.await(SYNC_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ if (!ok) {
+ Log.w(TAG, "getTitleSync timed out after " + SYNC_TIMEOUT_MS + "ms; returning \"\"");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ Log.w(TAG, "getTitleSync interrupted; returning \"\"");
+ }
+ return ref.get();
+ }
+
+ public void setDebug(final boolean enabled) {
+ // setWebContentsDebuggingEnabled must run on the UI thread.
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WebView.setWebContentsDebuggingEnabled(enabled);
+ }
+ });
+ }
+
+ // Script re-injected after every page load.
+ public void setInjectJs(String js) {
+ injectJs = (js == null) ? "" : js;
+ }
+
+ public void finishApp() {
+ if (destroyed) return;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ finish();
+ }
+ });
+ }
+
+ /* ------------------------------------------------------------------ */
+ /* JavaScript bridge */
+ /* ------------------------------------------------------------------ */
+
+ // TRUST BOUNDARY: every page, remote included, can call any bound Ring
+ // function by name. Validate args; no privileged ops.
+ private class RingBridge {
+ @JavascriptInterface
+ public void call(String name, String id, String req) {
+ nativeOnBridgeCall(name, id, req);
+ }
+ }
+
+ /* ------------------------------------------------------------------ */
+ /* Native methods (implemented in ring_webview_android.c) */
+ /* ------------------------------------------------------------------ */
+
+ private native void nativeStart();
+ private native void nativeDestroy();
+ private native void nativeOnBridgeCall(String name, String id, String req);
+ private native void nativeOnPageStarted(String url);
+ private native void nativeOnPageFinished(String url);
+ private native void nativeOnClose();
+ private native void nativeOnTitleChanged(String title);
+ private native void nativeOnFocusChanged(boolean focused);
+ private native void nativeOnResize(int width, int height);
+}
diff --git a/examples/android/src/proguard-webview.pro b/examples/android/src/proguard-webview.pro
new file mode 100644
index 0000000..8c4573a
--- /dev/null
+++ b/examples/android/src/proguard-webview.pro
@@ -0,0 +1,17 @@
+# Ring WebView Android backend — ProGuard / R8 keep rules.
+#
+# The native backend (ring_webview_android.c) looks up MainActivity methods by
+# string name via GetMethodID, and the JS bridge entry point is reached by
+# reflection (@JavascriptInterface). With minification enabled, R8 would rename
+# or strip them and the bridge would break silently at runtime (UnsatisfiedLink
+# errors never fire — calls just stop arriving).
+#
+# Pass this file to the APK packager (ring2apk) whenever minify is enabled.
+# Without minification it is inert documentation of the reflective boundary.
+
+-keep class io.github.ysdragon.webview.MainActivity {
+ *;
+}
+-keepclassmembers class io.github.ysdragon.webview.MainActivity$RingBridge {
+ *;
+}
diff --git a/package.ring b/package.ring
index a87d9c0..d45376b 100644
--- a/package.ring
+++ b/package.ring
@@ -5,12 +5,16 @@ aPackageInfo = [
:developer = "ysdragon",
:email = "youssefelkholey@gmail.com",
:license = "MIT License",
- :version = "1.5.2",
+ :version = "2.0.0",
:ringversion = "1.27",
:versions = [
[
- :version = "1.5.2",
+ :version = "2.0.0",
:branch = "main"
+ ],
+ [
+ :version = "1.5.2",
+ :branch = "v1.5.2"
]
],
:libs = [
@@ -48,11 +52,19 @@ aPackageInfo = [
:name = "weblib",
:version = "1.0.9",
:providerusername = "ringpackages"
+ ],
+ [
+ :name = "ring2apk",
+ :version = "1.1.0",
+ :providerusername = "ysdragon"
]
],
:files = [
"src/c_src/ring_webview.c",
"src/c_src/ring_webview_json.h",
+ "src/c_src/ring_webview_android.c",
+ "src/android/io/github/ysdragon/webview/MainActivity.java",
+ "src/android/proguard-webview.pro",
"CMakeLists.txt",
"lib.ring",
"main.ring",
@@ -204,9 +216,31 @@ aPackageInfo = [
"examples/templates/vue/frontend/tsconfig.json",
"examples/templates/vue/frontend/tsconfig.node.json",
"examples/templates/vue/frontend/vite.config.ts",
+ "examples/android/.gitignore",
+ "examples/android/README.md",
+ "examples/android/img/nota-screenshot.png",
+ "examples/android/res/mipmap-hdpi/ic_launcher.png",
+ "examples/android/res/mipmap-mdpi/ic_launcher.png",
+ "examples/android/res/mipmap-xhdpi/ic_launcher.png",
+ "examples/android/res/mipmap-xxhdpi/ic_launcher.png",
+ "examples/android/res/mipmap-xxxhdpi/ic_launcher.png",
+ "examples/android/res/values/colors.xml",
+ "examples/android/res/values/strings.xml",
+ "examples/android/res/values/styles.xml",
+ "examples/android/ring/main.ring",
+ "examples/android/ring/src/assets.ring",
+ "examples/android/ring/src/webview.rh",
+ "examples/android/ring/src/webview.ring",
+ "examples/android/ring/webview.ring",
+ "examples/android/ring2apk.ring",
+ "examples/android/src/cpp/CMakeLists.txt",
+ "examples/android/src/cpp/main.c",
+ "examples/android/src/java/io/github/ysdragon/webview/MainActivity.java",
+ "examples/android/src/proguard-webview.pro",
"README.md",
"docs/REFERENCE.md",
"docs/USAGE.md",
+ "docs/ANDROID.md",
"LICENSE"
],
:ringfolderfiles = [
diff --git a/src/android/io/github/ysdragon/webview/MainActivity.java b/src/android/io/github/ysdragon/webview/MainActivity.java
new file mode 100644
index 0000000..5fa51e2
--- /dev/null
+++ b/src/android/io/github/ysdragon/webview/MainActivity.java
@@ -0,0 +1,464 @@
+/*
+ * MainActivity.java
+ * Copyright (c) Youssef Saeed
+ * All rights reserved.
+ */
+package io.github.ysdragon.webview;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.graphics.Color;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Looper;
+import android.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowInsetsController;
+import android.view.WindowManager;
+import android.webkit.ConsoleMessage;
+import android.webkit.JavascriptInterface;
+import android.webkit.JsPromptResult;
+import android.webkit.JsResult;
+import android.webkit.WebChromeClient;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.EditText;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+// Android host: owns the WebView on the UI thread. Ring VM runs on a native
+// worker thread; all cross-talk goes through ring_webview_android.c.
+public class MainActivity extends Activity {
+
+ private static final String TAG = "RingWebView";
+ private static final long SYNC_TIMEOUT_MS = 5000;
+
+ static {
+ System.loadLibrary("main");
+ }
+
+ private WebView webView;
+ private volatile String injectJs = "";
+ private volatile boolean destroyed = false;
+ // Last URL / HTML (crash reload; reset on rotation). Last size (resize on change).
+ // UI thread only.
+ private String lastUrl = "";
+ private String lastHtml = "";
+ private int lastW = -1, lastH = -1;
+ // Crash-loop guard: a poison page must not rebuild forever.
+ private int crashCount = 0;
+ private long lastCrashMs = 0;
+
+ /* ------------------------------------------------------------------ */
+ /* Lifecycle */
+ /* ------------------------------------------------------------------ */
+
+ @SuppressLint("SetJavaScriptEnabled")
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ Window window = getWindow();
+ window.setStatusBarColor(Color.TRANSPARENT);
+ if (Build.VERSION.SDK_INT >= 30) {
+ window.setDecorFitsSystemWindows(false);
+ } else {
+ int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+ window.getDecorView().setSystemUiVisibility(flags);
+ }
+
+ webView = createWebView();
+ setContentView(webView);
+
+ // Dark page: keep light status-bar icons.
+ if (Build.VERSION.SDK_INT >= 30) {
+ WindowInsetsController controller = window.getInsetsController();
+ if (controller != null) {
+ controller.setSystemBarsAppearance(
+ 0,
+ WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS);
+ }
+ }
+
+ nativeStart();
+ }
+
+ // Fully configured WebView. Shared by onCreate and crash recovery.
+ // UI thread only.
+ private WebView createWebView() {
+ final WebView v = new WebView(this);
+
+ WebSettings settings = v.getSettings();
+ settings.setJavaScriptEnabled(true);
+ settings.setDomStorageEnabled(true);
+ settings.setAllowFileAccess(true);
+ settings.setAllowContentAccess(true);
+ settings.setMediaPlaybackRequiresUserGesture(false);
+ settings.setLoadWithOverviewMode(true);
+ settings.setUseWideViewPort(true);
+
+ v.setWebViewClient(new WebViewClient() {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ return false;
+ }
+
+ @Override
+ public void onPageStarted(WebView view, String url, android.graphics.Bitmap favicon) {
+ if (url != null) {
+ lastUrl = url;
+ // Real navigation wins over earlier setHtml content.
+ if (!url.equals("about:blank")) lastHtml = "";
+ }
+ nativeOnPageStarted(url);
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ String js = injectJs;
+ if (js != null && js.length() > 0) {
+ view.evaluateJavascript(js, null);
+ }
+ nativeOnPageFinished(url);
+ }
+
+ @Override
+ public boolean onRenderProcessGone(WebView view, android.webkit.RenderProcessGoneDetail detail) {
+ return handleRenderCrash(view, detail);
+ }
+ });
+
+ v.setWebChromeClient(new WebChromeClient() {
+ @Override
+ public void onReceivedTitle(WebView view, String title) {
+ nativeOnTitleChanged(title);
+ }
+
+ @Override
+ public boolean onConsoleMessage(ConsoleMessage cm) {
+ Log.i(TAG, "JS: " + cm.message() + " @" + cm.sourceId() + ":" + cm.lineNumber());
+ return true;
+ }
+
+ @Override
+ public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
+ new AlertDialog.Builder(MainActivity.this, android.R.style.Theme_DeviceDefault_Dialog_Alert)
+ .setMessage(message)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ result.confirm();
+ }
+ })
+ .setCancelable(false)
+ .show();
+ return true;
+ }
+
+ @Override
+ public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
+ new AlertDialog.Builder(MainActivity.this, android.R.style.Theme_DeviceDefault_Dialog_Alert)
+ .setMessage(message)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ result.confirm();
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ result.cancel();
+ }
+ })
+ .setCancelable(false)
+ .show();
+ return true;
+ }
+
+ @Override
+ public boolean onJsPrompt(WebView view, String url, String message, String defaultValue,
+ final JsPromptResult result) {
+ final EditText input = new EditText(MainActivity.this);
+ input.setText(defaultValue);
+ new AlertDialog.Builder(MainActivity.this, android.R.style.Theme_DeviceDefault_Dialog_Alert)
+ .setMessage(message)
+ .setView(input)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ result.confirm(input.getText().toString());
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ result.cancel();
+ }
+ })
+ .setCancelable(false)
+ .show();
+ return true;
+ }
+ });
+
+ v.addJavascriptInterface(new RingBridge(), "RingBridge");
+
+ v.getViewTreeObserver().addOnGlobalLayoutListener(new android.view.ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ int w = v.getWidth(), h = v.getHeight();
+ if (w != lastW || h != lastH) {
+ lastW = w;
+ lastH = h;
+ if (w > 0 && h > 0) nativeOnResize(w, h);
+ }
+ }
+ });
+
+ return v;
+ }
+
+ // Renderer gone: destroy the dead view, build an identical one, reload.
+ // Inject JS persists, so bindings come back with the page. True = handled.
+ // API 26+ only.
+ private boolean handleRenderCrash(WebView view, android.webkit.RenderProcessGoneDetail detail) {
+ boolean crashed = detail != null && detail.didCrash();
+ Log.w(TAG, "WebView render process " + (crashed ? "crashed" : "killed by system")
+ + "; rebuilding view, lastUrl=" + lastUrl);
+ if (view != null) view.destroy();
+ lastW = lastH = -1;
+ webView = createWebView();
+ setContentView(webView);
+ long now = System.currentTimeMillis();
+ if (now - lastCrashMs > 10000) crashCount = 0;
+ lastCrashMs = now;
+ // Same page crashing repeatedly: rebuild empty, stop reloading it.
+ if (++crashCount > 3) {
+ Log.e(TAG, "WebView crashed repeatedly; showing empty view");
+ return true;
+ }
+ if (!lastHtml.isEmpty()) renderHtml(webView, lastHtml);
+ else if (!lastUrl.isEmpty()) webView.loadUrl(lastUrl);
+ return true;
+ }
+
+ private void renderHtml(WebView v, String html) {
+ if (v != null) v.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null);
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ nativeOnFocusChanged(hasFocus);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (webView != null && webView.canGoBack()) {
+ webView.goBack();
+ } else {
+ nativeOnClose();
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ // Rotation destroys too; only a real finish kills the worker.
+ if (isFinishing()) {
+ destroyed = true;
+ nativeDestroy();
+ }
+ if (webView != null) {
+ webView.destroy();
+ webView = null;
+ }
+ super.onDestroy();
+ }
+
+ /* ------------------------------------------------------------------ */
+ /* Called from native (worker thread unless noted) */
+ /* ------------------------------------------------------------------ */
+
+ public void evalJs(final String js) {
+ if (destroyed || webView == null) return;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (webView != null) webView.evaluateJavascript(js, null);
+ }
+ });
+ }
+
+ public void loadUrl(final String url) {
+ if (destroyed || webView == null) return;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (webView != null) webView.loadUrl(url);
+ }
+ });
+ }
+
+ public void loadHtml(final String html) {
+ if (destroyed || webView == null) return;
+ lastHtml = (html == null) ? "" : html;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ renderHtml(webView, html);
+ }
+ });
+ }
+
+ public void goBack() {
+ if (destroyed || webView == null) return;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (webView != null && webView.canGoBack()) webView.goBack();
+ }
+ });
+ }
+
+ public void goForward() {
+ if (destroyed || webView == null) return;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (webView != null && webView.canGoForward()) webView.goForward();
+ }
+ });
+ }
+
+ public void reload() {
+ if (destroyed || webView == null) return;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (webView != null) webView.reload();
+ }
+ });
+ }
+
+ // Sync URL query; blocks the caller on the UI thread.
+ public String getUrlSync() {
+ if (destroyed || webView == null) return "";
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ String u = webView.getUrl();
+ return u == null ? "" : u;
+ }
+ final AtomicReference ref = new AtomicReference("");
+ final CountDownLatch latch = new CountDownLatch(1);
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ String u = (webView != null) ? webView.getUrl() : null;
+ ref.set(u == null ? "" : u);
+ latch.countDown();
+ }
+ });
+ try {
+ boolean ok = latch.await(SYNC_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ if (!ok) {
+ Log.w(TAG, "getUrlSync timed out after " + SYNC_TIMEOUT_MS + "ms; returning \"\"");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ Log.w(TAG, "getUrlSync interrupted; returning \"\"");
+ }
+ return ref.get();
+ }
+
+ // Sync title query; blocks the caller on the UI thread.
+ public String getTitleSync() {
+ if (destroyed || webView == null) return "";
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ String t = webView.getTitle();
+ return t == null ? "" : t;
+ }
+ final AtomicReference ref = new AtomicReference("");
+ final CountDownLatch latch = new CountDownLatch(1);
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ String t = (webView != null) ? webView.getTitle() : null;
+ ref.set(t == null ? "" : t);
+ latch.countDown();
+ }
+ });
+ try {
+ boolean ok = latch.await(SYNC_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ if (!ok) {
+ Log.w(TAG, "getTitleSync timed out after " + SYNC_TIMEOUT_MS + "ms; returning \"\"");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ Log.w(TAG, "getTitleSync interrupted; returning \"\"");
+ }
+ return ref.get();
+ }
+
+ public void setDebug(final boolean enabled) {
+ // setWebContentsDebuggingEnabled must run on the UI thread.
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ WebView.setWebContentsDebuggingEnabled(enabled);
+ }
+ });
+ }
+
+ // Script re-injected after every page load.
+ public void setInjectJs(String js) {
+ injectJs = (js == null) ? "" : js;
+ }
+
+ public void finishApp() {
+ if (destroyed) return;
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ finish();
+ }
+ });
+ }
+
+ /* ------------------------------------------------------------------ */
+ /* JavaScript bridge */
+ /* ------------------------------------------------------------------ */
+
+ // TRUST BOUNDARY: every page, remote included, can call any bound Ring
+ // function by name. Validate args; no privileged ops.
+ private class RingBridge {
+ @JavascriptInterface
+ public void call(String name, String id, String req) {
+ nativeOnBridgeCall(name, id, req);
+ }
+ }
+
+ /* ------------------------------------------------------------------ */
+ /* Native methods (implemented in ring_webview_android.c) */
+ /* ------------------------------------------------------------------ */
+
+ private native void nativeStart();
+ private native void nativeDestroy();
+ private native void nativeOnBridgeCall(String name, String id, String req);
+ private native void nativeOnPageStarted(String url);
+ private native void nativeOnPageFinished(String url);
+ private native void nativeOnClose();
+ private native void nativeOnTitleChanged(String title);
+ private native void nativeOnFocusChanged(boolean focused);
+ private native void nativeOnResize(int width, int height);
+}
diff --git a/src/android/proguard-webview.pro b/src/android/proguard-webview.pro
new file mode 100644
index 0000000..8c4573a
--- /dev/null
+++ b/src/android/proguard-webview.pro
@@ -0,0 +1,17 @@
+# Ring WebView Android backend — ProGuard / R8 keep rules.
+#
+# The native backend (ring_webview_android.c) looks up MainActivity methods by
+# string name via GetMethodID, and the JS bridge entry point is reached by
+# reflection (@JavascriptInterface). With minification enabled, R8 would rename
+# or strip them and the bridge would break silently at runtime (UnsatisfiedLink
+# errors never fire — calls just stop arriving).
+#
+# Pass this file to the APK packager (ring2apk) whenever minify is enabled.
+# Without minification it is inert documentation of the reflective boundary.
+
+-keep class io.github.ysdragon.webview.MainActivity {
+ *;
+}
+-keepclassmembers class io.github.ysdragon.webview.MainActivity$RingBridge {
+ *;
+}
diff --git a/src/c_src/ring_webview_android.c b/src/c_src/ring_webview_android.c
new file mode 100644
index 0000000..3a16f88
--- /dev/null
+++ b/src/c_src/ring_webview_android.c
@@ -0,0 +1,2197 @@
+/*
+ * ring_webview_android.c
+ *
+ * Copyright (c) Youssef Saeed
+ * All rights reserved.
+ *
+ * Android backend: same `webview_*` Ring C-API as ring_webview.c,
+ * backed by android.webkit.WebView through JNI.
+ *
+ * UI thread (Java) owns the WebView; JNI entry points only enqueue.
+ * Worker thread (C) owns the Ring VM and runs the job queue.
+ * g_on* names are worker-only; JNI enqueues raw events, never reads them.
+ * Flags are C11 atomics; queue is bounded + condvar (no polling).
+ * C->Java calls hold g_activityMutex; rotation swaps under the same lock.
+ * Java host is io.github.ysdragon.webview.MainActivity (static JNI naming).
+ *
+ * RingBridge is callable by any page JS, remote included. Validate bind args.
+ */
+
+#include "ring.h"
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define LOG_TAG "RingWebView"
+#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
+#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
+
+// Webview version this binding tracks.
+#define RING_WEBVIEW_ANDROID_VERSION "0.12.0"
+
+#define RING_WEBVIEW_UI_TIMEOUT_MS 5000
+
+// Provided by the app's main.c: creates the Ring state, runs the bytecode.
+extern void ring_app_main(void);
+
+/* ============================================================================
+ * Globals
+ * ============================================================================
+ */
+
+static JavaVM *g_jvm = NULL;
+// Global refs: MainActivity + AssetManager.
+static jobject g_activity = NULL;
+static jobject g_assetMgrJava = NULL;
+static AAssetManager *g_assetMgr = NULL;
+static char g_basePath[1024] = {0};
+static char g_apkPath[1024] = {0};
+
+/* Cached Activity method IDs */
+static jmethodID mid_evalJs = NULL;
+static jmethodID mid_loadUrl = NULL;
+static jmethodID mid_loadHtml = NULL;
+static jmethodID mid_goBack = NULL;
+static jmethodID mid_goForward = NULL;
+static jmethodID mid_reload = NULL;
+static jmethodID mid_getUrlSync = NULL;
+static jmethodID mid_getTitleSync = NULL;
+static jmethodID mid_setDebug = NULL;
+static jmethodID mid_setInjectJs = NULL;
+static jmethodID mid_finishApp = NULL;
+
+/* Cached AssetManager.list method ID */
+static jmethodID mid_assetList = NULL;
+
+// Set in webview_create (worker thread).
+static RingState *g_ringState = NULL;
+
+// Atomics: shared between UI/JNI threads and the worker.
+static atomic_int g_running = 0;
+static atomic_int g_terminate = 0;
+static atomic_int g_webviewCreated = 0;
+static pthread_t g_workerThread;
+// UI thread only (nativeStart), before the worker exists.
+static int g_workerStarted = 0;
+
+// Worker-only: written by SET_EVENT, read by process_job.
+static char *g_onClose = NULL;
+static char *g_onLoad = NULL;
+static char *g_onDomReady = NULL;
+static char *g_onNavigate = NULL;
+static char *g_onTitle = NULL;
+static char *g_onFocus = NULL;
+static char *g_onResize = NULL;
+
+/* Bound functions: jsName -> ringFuncName */
+#define MAX_BINDS 256
+static char *g_bindJsName[MAX_BINDS];
+static char *g_bindRingFunc[MAX_BINDS];
+static int g_bindCount = 0;
+
+/* Init scripts injected on every page load */
+#define MAX_INIT_SCRIPTS 64
+static char *g_initScripts[MAX_INIT_SCRIPTS];
+static int g_initScriptCount = 0;
+
+/* ============================================================================
+ * Job queue (Java threads -> worker thread)
+ * ============================================================================
+ */
+
+// JNI enqueues; worker resolves g_on* at process time.
+typedef enum
+{
+ JOB_BIND_CALL, /* name=jsName, id, req */
+ JOB_PAGE_STARTED, /* arg=url (may be NULL) */
+ JOB_PAGE_FINISHED, /* arg=url (may be NULL) */
+ JOB_TITLE_CHANGED, /* arg=title */
+ JOB_FOCUS_CHANGED, /* arg="true"/"false" (window focus, desktop parity) */
+ JOB_RESIZED, /* arg="WIDTHxHEIGHT" */
+ JOB_CLOSE, /* no arg */
+ JOB_DISPATCH, /* req=Ring code */
+ JOB_REATTACH, /* no arg: re-push shim+scripts (rotation) */
+ JOB_TERMINATE
+} JobType;
+
+typedef struct Job
+{
+ JobType type;
+ char *name;
+ char *id;
+ char *req;
+ char *arg;
+ struct Job *next;
+} Job;
+
+static Job *g_queueHead = NULL;
+static Job *g_queueTail = NULL;
+static pthread_mutex_t g_queueMutex = PTHREAD_MUTEX_INITIALIZER;
+static pthread_cond_t g_queueCond = PTHREAD_COND_INITIALIZER;
+// Drop page/bind jobs past this depth; TERMINATE/REATTACH always pass.
+#define RING_WEBVIEW_MAX_QUEUE 4096
+// Guarded by g_queueMutex.
+static int g_queueLen = 0;
+
+// Guards g_activity swaps (rotation) vs in-flight JNI calls.
+static pthread_mutex_t g_activityMutex = PTHREAD_MUTEX_INITIALIZER;
+
+static void free_job(Job *job);
+
+static char *xstrdup(const char *s)
+{
+ size_t n;
+ char *p;
+ if (!s)
+ return NULL;
+ n = strlen(s);
+ p = (char *)malloc(n + 1);
+ if (p)
+ {
+ memcpy(p, s, n);
+ p[n] = '\0';
+ }
+ return p;
+}
+
+static void enqueue_job(Job *job)
+{
+ pthread_mutex_lock(&g_queueMutex);
+ if (g_queueLen >= RING_WEBVIEW_MAX_QUEUE && job->type != JOB_TERMINATE && job->type != JOB_REATTACH)
+ {
+ pthread_mutex_unlock(&g_queueMutex);
+ LOGE("job queue full (%d); dropping type %d", RING_WEBVIEW_MAX_QUEUE, (int)job->type);
+ free_job(job);
+ return;
+ }
+ job->next = NULL;
+ if (g_queueTail)
+ g_queueTail->next = job;
+ else
+ g_queueHead = job;
+ g_queueTail = job;
+ g_queueLen++;
+ pthread_cond_signal(&g_queueCond);
+ pthread_mutex_unlock(&g_queueMutex);
+}
+
+// Worker pop: waits on condvar. NULL = terminated and empty.
+static Job *dequeue_job_blocking(void)
+{
+ Job *job;
+ pthread_mutex_lock(&g_queueMutex);
+ while (!g_queueHead && !atomic_load_explicit(&g_terminate, memory_order_acquire))
+ pthread_cond_wait(&g_queueCond, &g_queueMutex);
+ job = g_queueHead;
+ if (job)
+ {
+ g_queueHead = job->next;
+ if (!g_queueHead)
+ g_queueTail = NULL;
+ job->next = NULL;
+ g_queueLen--;
+ }
+ pthread_mutex_unlock(&g_queueMutex);
+ return job;
+}
+
+static void queue_signal_all(void)
+{
+ pthread_mutex_lock(&g_queueMutex);
+ pthread_cond_broadcast(&g_queueCond);
+ pthread_mutex_unlock(&g_queueMutex);
+}
+
+// Drain pop; NULL when empty.
+static Job *try_dequeue_job(void)
+{
+ Job *job;
+ pthread_mutex_lock(&g_queueMutex);
+ job = g_queueHead;
+ if (job)
+ {
+ g_queueHead = job->next;
+ if (!g_queueHead)
+ g_queueTail = NULL;
+ job->next = NULL;
+ g_queueLen--;
+ }
+ pthread_mutex_unlock(&g_queueMutex);
+ return job;
+}
+
+static void free_job(Job *job)
+{
+ if (!job)
+ return;
+ if (job->name)
+ free(job->name);
+ if (job->id)
+ free(job->id);
+ if (job->req)
+ free(job->req);
+ if (job->arg)
+ free(job->arg);
+ free(job);
+}
+
+static void enqueue_bind_call(const char *name, const char *id, const char *req)
+{
+ Job *job = (Job *)calloc(1, sizeof(Job));
+ if (!job)
+ return;
+ job->type = JOB_BIND_CALL;
+ job->name = xstrdup(name);
+ job->id = xstrdup(id);
+ job->req = xstrdup(req);
+ enqueue_job(job);
+}
+
+static void enqueue_raw_event(JobType type, const char *arg)
+{
+ Job *job = (Job *)calloc(1, sizeof(Job));
+ if (!job)
+ return;
+ job->type = type;
+ job->arg = arg ? xstrdup(arg) : NULL;
+ enqueue_job(job);
+}
+
+static void enqueue_dispatch(const char *code)
+{
+ Job *job = (Job *)calloc(1, sizeof(Job));
+ if (!job)
+ return;
+ job->type = JOB_DISPATCH;
+ job->req = xstrdup(code);
+ enqueue_job(job);
+}
+
+static void enqueue_terminate(void)
+{
+ Job *job = (Job *)calloc(1, sizeof(Job));
+ if (!job)
+ return;
+ job->type = JOB_TERMINATE;
+ enqueue_job(job);
+}
+
+/* ============================================================================
+ * JNI helpers
+ * ============================================================================
+ */
+
+static JNIEnv *get_env(void)
+{
+ JNIEnv *env = NULL;
+ jint rc;
+ if (!g_jvm)
+ return NULL;
+ rc = (*g_jvm)->GetEnv(g_jvm, (void **)&env, JNI_VERSION_1_6);
+ if (rc == JNI_EDETACHED)
+ {
+ if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) != 0)
+ return NULL;
+ }
+ return env;
+}
+
+// Hold g_activityMutex for the whole call; rotation swaps under the same lock.
+static void call_java_void_string(jmethodID mid, const char *value)
+{
+ JNIEnv *env = get_env();
+ jstring s;
+ jobject activity;
+ if (!env || !mid)
+ return;
+ pthread_mutex_lock(&g_activityMutex);
+ activity = g_activity;
+ if (!activity)
+ {
+ pthread_mutex_unlock(&g_activityMutex);
+ return;
+ }
+ s = (*env)->NewStringUTF(env, value ? value : "");
+ if (!s)
+ {
+ if ((*env)->ExceptionCheck(env))
+ (*env)->ExceptionClear(env);
+ LOGE("NewStringUTF OOM; dropping Java call");
+ pthread_mutex_unlock(&g_activityMutex);
+ return;
+ }
+ (*env)->CallVoidMethod(env, activity, mid, s);
+ (*env)->DeleteLocalRef(env, s);
+ if ((*env)->ExceptionCheck(env))
+ {
+ (*env)->ExceptionDescribe(env);
+ (*env)->ExceptionClear(env);
+ }
+ pthread_mutex_unlock(&g_activityMutex);
+}
+
+static void call_java_void(jmethodID mid)
+{
+ JNIEnv *env = get_env();
+ jobject activity;
+ if (!env || !mid)
+ return;
+ pthread_mutex_lock(&g_activityMutex);
+ activity = g_activity;
+ if (!activity)
+ {
+ pthread_mutex_unlock(&g_activityMutex);
+ return;
+ }
+ (*env)->CallVoidMethod(env, activity, mid);
+ if ((*env)->ExceptionCheck(env))
+ {
+ (*env)->ExceptionDescribe(env);
+ (*env)->ExceptionClear(env);
+ }
+ pthread_mutex_unlock(&g_activityMutex);
+}
+
+// Blocks the worker on the Java latch. Timeout reads back as "".
+static char *call_java_string_sync(jmethodID mid)
+{
+ JNIEnv *env = get_env();
+ jstring s;
+ const char *utf;
+ char *result = NULL;
+ jobject activity;
+ if (!env || !mid)
+ return xstrdup("");
+ pthread_mutex_lock(&g_activityMutex);
+ activity = g_activity;
+ if (!activity)
+ {
+ pthread_mutex_unlock(&g_activityMutex);
+ return xstrdup("");
+ }
+ s = (jstring)(*env)->CallObjectMethod(env, activity, mid);
+ if ((*env)->ExceptionCheck(env))
+ {
+ (*env)->ExceptionDescribe(env);
+ (*env)->ExceptionClear(env);
+ pthread_mutex_unlock(&g_activityMutex);
+ return xstrdup("");
+ }
+ pthread_mutex_unlock(&g_activityMutex);
+ if (!s)
+ {
+ // Null = latch timeout/interrupt (Java logs too).
+ LOGE("sync query timed out; returning \"\"");
+ return xstrdup("");
+ }
+ utf = (*env)->GetStringUTFChars(env, s, NULL);
+ if (!utf)
+ {
+ if ((*env)->ExceptionCheck(env))
+ (*env)->ExceptionClear(env);
+ LOGE("GetStringUTFChars OOM on sync query result");
+ (*env)->DeleteLocalRef(env, s);
+ return xstrdup("");
+ }
+ result = xstrdup(utf);
+ (*env)->ReleaseStringUTFChars(env, s, utf);
+ (*env)->DeleteLocalRef(env, s);
+ return result ? result : xstrdup("");
+}
+
+// Escape for a single-quoted JS string.
+static char *js_escape(const char *s)
+{
+ size_t len, cap;
+ char *out, *o;
+ const unsigned char *p;
+ if (!s)
+ return xstrdup("");
+ len = strlen(s);
+ cap = len * 6 + 1;
+ out = (char *)malloc(cap);
+ if (!out)
+ return xstrdup("");
+ o = out;
+ for (p = (const unsigned char *)s; *p; p++)
+ {
+ unsigned char c = *p;
+ switch (c)
+ {
+ case '\\':
+ *o++ = '\\';
+ *o++ = '\\';
+ break;
+ case '\'':
+ *o++ = '\\';
+ *o++ = '\'';
+ break;
+ case '\n':
+ *o++ = '\\';
+ *o++ = 'n';
+ break;
+ case '\r':
+ *o++ = '\\';
+ *o++ = 'r';
+ break;
+ case '\t':
+ *o++ = '\\';
+ *o++ = 't';
+ break;
+ default:
+ if (c < 0x20)
+ o += sprintf(o, "\\x%02x", c);
+ else
+ *o++ = (char)c;
+ }
+ }
+ *o = '\0';
+ return out;
+}
+
+/* ============================================================================
+ * JS injection shim (Promise-based bind protocol, matches desktop webview)
+ * ============================================================================
+ */
+
+static const char *BASE_SHIM = "(function(){"
+ "if(window.__ringwebview){return;}"
+ "window.__ringwebview={seq:0,pending:{}};"
+ "window.__ringwebview.result=function(id,status,json){"
+ "var p=window.__ringwebview.pending[id];"
+ "if(!p){return;}"
+ "delete window.__ringwebview.pending[id];"
+ "var data;try{data=JSON.parse(json);}catch(e){data=json;}"
+ "if(status===0){p.resolve(data);}else{p.reject(data);}"
+ "};"
+ "})();";
+
+// Safe to eval anytime: shim guards re-init, wrappers re-assign.
+// PREFIX + escName + SUFFIX, sized exactly.
+static const char *BIND_WRAPPER_PREFIX = "(function(){var n='";
+static const char *BIND_WRAPPER_SUFFIX =
+ "';"
+ "window[n]=function(){"
+ "var a=Array.prototype.slice.call(arguments);"
+ "var id=String(++window.__ringwebview.seq);"
+ "return new Promise(function(res,rej){"
+ "window.__ringwebview.pending[id]={resolve:res,reject:rej};"
+ "RingBridge.call(n,id,JSON.stringify(a));"
+ "});};"
+ "})();";
+
+static char *build_bind_js(void)
+{
+ // Escape first, then size from escaped lengths (js_escape expands up to 6x).
+ char *escNames[MAX_BINDS];
+ size_t cap = strlen(BASE_SHIM) + 1;
+ char *js, *o;
+ int i;
+ if (g_bindCount < 0 || g_bindCount > MAX_BINDS)
+ return NULL;
+ for (i = 0; i < g_bindCount; i++)
+ {
+ if (!g_bindJsName[i])
+ {
+ while (--i >= 0)
+ free(escNames[i]);
+ return NULL;
+ }
+ escNames[i] = js_escape(g_bindJsName[i]);
+ if (!escNames[i])
+ {
+ while (--i >= 0)
+ free(escNames[i]);
+ return NULL;
+ }
+ cap += strlen(BIND_WRAPPER_PREFIX) + strlen(escNames[i]) + strlen(BIND_WRAPPER_SUFFIX);
+ }
+
+ js = (char *)malloc(cap);
+ if (!js)
+ {
+ for (i = 0; i < g_bindCount; i++)
+ free(escNames[i]);
+ return NULL;
+ }
+ o = js;
+ // Clamp: truncation must not run past the allocation.
+ {
+ size_t rem = cap - (size_t)(o - js);
+ int w = snprintf(o, rem, "%s", BASE_SHIM);
+ o += (w < 0) ? 0 : ((size_t)w >= rem ? rem - 1 : (size_t)w);
+ }
+
+ for (i = 0; i < g_bindCount; i++)
+ {
+ size_t rem = cap - (size_t)(o - js);
+ int w = snprintf(o, rem, "%s%s%s", BIND_WRAPPER_PREFIX, escNames[i], BIND_WRAPPER_SUFFIX);
+ o += (w < 0) ? 0 : ((size_t)w >= rem ? rem - 1 : (size_t)w);
+ free(escNames[i]);
+ }
+ return js;
+}
+
+// Store for re-inject on page load; eval now for the current page.
+static void rebuild_inject_js(void)
+{
+ size_t cap;
+ char *full, *o, *bindJs;
+ int i;
+
+ bindJs = build_bind_js();
+ if (!bindJs)
+ return;
+
+ // +64 covers the 33-char wrapper; snprintf truncates on miscount.
+ cap = strlen(bindJs) + 1 + 64;
+ for (i = 0; i < g_initScriptCount; i++)
+ cap += strlen(g_initScripts[i]) + 64;
+
+ full = (char *)malloc(cap);
+ if (full)
+ {
+ size_t left = cap;
+ o = full;
+ {
+ size_t rem = left - (size_t)(o - full);
+ int w = snprintf(o, rem, "%s", bindJs);
+ o += (w < 0) ? 0 : ((size_t)w >= rem ? rem - 1 : (size_t)w);
+ }
+ for (i = 0; i < g_initScriptCount; i++)
+ {
+ size_t rem = left - (size_t)(o - full);
+ int w = snprintf(o, rem, "(function(){try{%s}catch(e){}})();", g_initScripts[i]);
+ o += (w < 0) ? 0 : ((size_t)w >= rem ? rem - 1 : (size_t)w);
+ }
+ call_java_void_string(mid_setInjectJs, full);
+ free(full);
+ }
+
+ call_java_void_string(mid_evalJs, bindJs);
+ free(bindJs);
+}
+
+#include "ring_webview_json.h"
+
+/* ============================================================================
+ * Ring callback execution (worker thread)
+ * ============================================================================
+ */
+
+static void call_ring_func(const char *cCallback, const char *cArg)
+{
+ VM *pVM;
+ unsigned int nSP_before, nFuncSP_before, nCallListSize_before;
+
+ if (!g_ringState || !cCallback)
+ return;
+ pVM = g_ringState->pVM;
+ if (!pVM)
+ return;
+ if (!atomic_load_explicit(&g_running, memory_order_acquire))
+ return;
+
+ ring_vm_mutexlock(pVM);
+
+ nSP_before = pVM->nSP;
+ nFuncSP_before = pVM->nFuncSP;
+ nCallListSize_before = RING_VM_FUNCCALLSCOUNT;
+
+ if (!ring_vm_loadfunc2(pVM, (char *)cCallback, RING_FALSE))
+ {
+ pVM->nSP = nSP_before;
+ pVM->nFuncSP = nFuncSP_before;
+ ring_vm_mutexunlock(pVM);
+ return;
+ }
+
+ if (cArg)
+ {
+ RING_VM_STACK_PUSHCVALUE2(cArg, strlen(cArg));
+ }
+
+ ring_vm_call2(pVM);
+ while (RING_VM_FUNCCALLSCOUNT > nCallListSize_before)
+ ring_vm_fetch(pVM);
+
+ pVM->nSP = nSP_before;
+ pVM->nFuncSP = nFuncSP_before;
+
+ ring_vm_mutexunlock(pVM);
+}
+
+static void call_ring_bind_func(const char *cFunc, const char *id, const char *req)
+{
+ VM *pVM;
+ unsigned int nSP_before, nFuncSP_before, nCallListSize_before;
+
+ if (!g_ringState || !cFunc)
+ return;
+ pVM = g_ringState->pVM;
+ if (!pVM)
+ return;
+ if (!atomic_load_explicit(&g_running, memory_order_acquire))
+ return;
+
+ ring_vm_mutexlock(pVM);
+
+ nSP_before = pVM->nSP;
+ nFuncSP_before = pVM->nFuncSP;
+ nCallListSize_before = RING_VM_FUNCCALLSCOUNT;
+
+ if (!ring_vm_loadfunc2(pVM, (char *)cFunc, RING_FALSE))
+ {
+ pVM->nSP = nSP_before;
+ pVM->nFuncSP = nFuncSP_before;
+ ring_vm_mutexunlock(pVM);
+ return;
+ }
+
+ RING_VM_STACK_PUSHCVALUE2(id ? id : "", id ? strlen(id) : 0);
+
+ // req = JSON array of JS args; pass as Ring list.
+ {
+ List *pReqList = json_decode_to_ring_list(pVM, req);
+ if (!pReqList)
+ {
+ LOGE("bind call \"%s\": invalid JSON request; passing an empty list", cFunc);
+ pReqList = ring_vm_api_newlist(pVM);
+ }
+ ring_vm_api_retlist2(pVM, pReqList, RING_OUTPUT_RETLISTBYREF);
+ }
+
+ ring_vm_call2(pVM);
+ while (RING_VM_FUNCCALLSCOUNT > nCallListSize_before)
+ ring_vm_fetch(pVM);
+
+ pVM->nSP = nSP_before;
+ pVM->nFuncSP = nFuncSP_before;
+
+ ring_vm_mutexunlock(pVM);
+}
+
+static const char *find_ring_func(const char *jsName)
+{
+ int i;
+ if (!jsName)
+ return NULL;
+ for (i = 0; i < g_bindCount; i++)
+ {
+ if (g_bindJsName[i] && strcmp(g_bindJsName[i], jsName) == 0)
+ return g_bindRingFunc[i];
+ }
+ return NULL;
+}
+
+// Worker-only state; JNI never touches it.
+static void process_job(Job *job)
+{
+ if (!job)
+ return;
+ switch (job->type)
+ {
+ case JOB_BIND_CALL: {
+ const char *fn = find_ring_func(job->name);
+ if (fn)
+ call_ring_bind_func(fn, job->id, job->req);
+ else
+ LOGE("No Ring binding for JS function: %s", job->name ? job->name : "?");
+ break;
+ }
+ case JOB_PAGE_STARTED:
+ if (g_onLoad)
+ call_ring_func(g_onLoad, "started");
+ if (g_onNavigate && job->arg)
+ call_ring_func(g_onNavigate, job->arg);
+ break;
+ case JOB_PAGE_FINISHED:
+ if (g_onLoad)
+ call_ring_func(g_onLoad, "finished");
+ if (g_onDomReady)
+ call_ring_func(g_onDomReady, NULL);
+ break;
+ case JOB_TITLE_CHANGED:
+ if (g_onTitle && job->arg)
+ call_ring_func(g_onTitle, job->arg);
+ break;
+ case JOB_FOCUS_CHANGED:
+ if (g_onFocus && job->arg)
+ call_ring_func(g_onFocus, job->arg);
+ break;
+ case JOB_RESIZED:
+ if (g_onResize && job->arg)
+ call_ring_func(g_onResize, job->arg);
+ break;
+ case JOB_CLOSE:
+ if (g_onClose)
+ call_ring_func(g_onClose, NULL);
+ break;
+ case JOB_DISPATCH:
+ if (g_ringState && g_ringState->pVM && job->req)
+ ring_vm_runcodefromthread(g_ringState->pVM, job->req);
+ break;
+ case JOB_REATTACH:
+ // Re-push shim+scripts; tables stay single-threaded.
+ rebuild_inject_js();
+ break;
+ case JOB_TERMINATE:
+ atomic_store_explicit(&g_terminate, 1, memory_order_release);
+ break;
+ }
+}
+
+/* ============================================================================
+ * Asset extraction (APK assets -> filesDir) via JNI AssetManager.list()
+ * ============================================================================
+ */
+
+static void mkdirs_for(const char *path)
+{
+ char tmp[1024];
+ char *p;
+ size_t len;
+ int n;
+ if (!path || !*path)
+ return;
+ n = snprintf(tmp, sizeof(tmp), "%s", path);
+ if (n < 0 || (size_t)n >= sizeof(tmp))
+ {
+ LOGE("path too long, skipping mkdirs: %.64s...", path);
+ return;
+ }
+ len = strlen(tmp);
+ if (len > 0 && tmp[len - 1] == '/')
+ tmp[len - 1] = '\0';
+ for (p = tmp + 1; *p; p++)
+ {
+ if (*p == '/')
+ {
+ *p = '\0';
+ mkdir(tmp, 0755);
+ *p = '/';
+ }
+ }
+}
+
+static int asset_is_file(const char *assetPath)
+{
+ AAsset *asset;
+ if (!g_assetMgr)
+ return 0;
+ asset = AAssetManager_open(g_assetMgr, assetPath, AASSET_MODE_STREAMING);
+ if (asset)
+ {
+ AAsset_close(asset);
+ return 1;
+ }
+ return 0;
+}
+
+static void extract_asset_file(const char *assetPath, const char *destPath)
+{
+ AAsset *asset;
+ off_t length;
+ char buf[8192];
+ int rd;
+ FILE *out;
+
+ if (!g_assetMgr)
+ return;
+ asset = AAssetManager_open(g_assetMgr, assetPath, AASSET_MODE_STREAMING);
+ if (!asset)
+ {
+ LOGE("Cannot open asset: %s", assetPath);
+ return;
+ }
+ length = AAsset_getLength(asset);
+ (void)length;
+
+ mkdirs_for(destPath);
+ out = fopen(destPath, "wb");
+ if (!out)
+ {
+ LOGE("Cannot write: %s", destPath);
+ AAsset_close(asset);
+ return;
+ }
+ while ((rd = AAsset_read(asset, buf, sizeof(buf))) > 0)
+ fwrite(buf, 1, rd, out);
+ fclose(out);
+ AAsset_close(asset);
+ LOGI("Extracted: %s", assetPath);
+}
+
+static void extract_dir(JNIEnv *env, const char *assetDir, const char *destDir)
+{
+ jstring jdir;
+ jobjectArray entries;
+ jsize n, i;
+
+ if (!env || !g_assetMgrJava || !mid_assetList)
+ return;
+
+ jdir = (*env)->NewStringUTF(env, assetDir ? assetDir : "");
+ if (!jdir)
+ {
+ if ((*env)->ExceptionCheck(env))
+ (*env)->ExceptionClear(env);
+ LOGE("NewStringUTF OOM in extract_dir");
+ return;
+ }
+ entries = (jobjectArray)(*env)->CallObjectMethod(env, g_assetMgrJava, mid_assetList, jdir);
+ (*env)->DeleteLocalRef(env, jdir);
+ if ((*env)->ExceptionCheck(env))
+ {
+ (*env)->ExceptionDescribe(env);
+ (*env)->ExceptionClear(env);
+ return;
+ }
+ if (!entries)
+ return;
+
+ n = (*env)->GetArrayLength(env, entries);
+ for (i = 0; i < n; i++)
+ {
+ jstring jname = (jstring)(*env)->GetObjectArrayElement(env, entries, i);
+ const char *name;
+ char assetPath[1024];
+ char destPath[1024];
+ int na, nd;
+ if (!jname)
+ continue;
+ if ((*env)->ExceptionCheck(env))
+ {
+ (*env)->ExceptionClear(env);
+ continue;
+ }
+ name = (*env)->GetStringUTFChars(env, jname, NULL);
+ if (!name)
+ {
+ if ((*env)->ExceptionCheck(env))
+ (*env)->ExceptionClear(env);
+ LOGE("GetStringUTFChars OOM in extract_dir");
+ (*env)->DeleteLocalRef(env, jname);
+ continue;
+ }
+
+ if (assetDir && assetDir[0])
+ na = snprintf(assetPath, sizeof(assetPath), "%s/%s", assetDir, name);
+ else
+ na = snprintf(assetPath, sizeof(assetPath), "%s", name);
+ nd = snprintf(destPath, sizeof(destPath), "%s/%s", destDir, name);
+ if (na < 0 || (size_t)na >= sizeof(assetPath) || nd < 0 || (size_t)nd >= sizeof(destPath))
+ {
+ LOGE("asset path too long, skipping: %.64s", name);
+ (*env)->ReleaseStringUTFChars(env, jname, name);
+ (*env)->DeleteLocalRef(env, jname);
+ continue;
+ }
+
+ if (asset_is_file(assetPath))
+ {
+ extract_asset_file(assetPath, destPath);
+ }
+ else
+ {
+ mkdir(destPath, 0755);
+ extract_dir(env, assetPath, destPath);
+ }
+
+ (*env)->ReleaseStringUTFChars(env, jname, name);
+ (*env)->DeleteLocalRef(env, jname);
+ }
+ (*env)->DeleteLocalRef(env, entries);
+}
+
+// 1 = assets present or unreadable (extract); 0 = confirmed none.
+static int apk_has_assets(const char *path)
+{
+ FILE *f;
+ long fsize, scanStart, cdSize, p, i, eocd = -1;
+ static unsigned char buf[65557];
+ unsigned char *cd;
+ unsigned int nEntries, e;
+ int found;
+
+ if (!path || !*path)
+ return 1;
+ f = fopen(path, "rb");
+ if (!f)
+ return 1;
+ if (fseek(f, 0, SEEK_END) != 0 || (fsize = ftell(f)) < 22)
+ {
+ fclose(f);
+ return 1;
+ }
+
+ // Find EOCD scanning backwards.
+ scanStart = fsize - (long)sizeof(buf);
+ if (scanStart < 0)
+ scanStart = 0;
+ if (fseek(f, scanStart, SEEK_SET) != 0)
+ {
+ fclose(f);
+ return 1;
+ }
+ {
+ long n = (long)fread(buf, 1, sizeof(buf), f);
+ for (i = n - 22; i >= 0; i--)
+ if (buf[i] == 0x50 && buf[i + 1] == 0x4b && buf[i + 2] == 0x05 && buf[i + 3] == 0x06)
+ {
+ eocd = i;
+ break;
+ }
+ }
+ if (eocd < 0)
+ {
+ fclose(f);
+ return 1;
+ }
+
+ nEntries = (unsigned int)(buf[eocd + 10] | (buf[eocd + 11] << 8));
+ {
+ long cdOff = (long)((unsigned int)(buf[eocd + 16] | (buf[eocd + 17] << 8) | (buf[eocd + 18] << 16) |
+ ((unsigned int)buf[eocd + 19] << 24)));
+ if (nEntries == 0)
+ {
+ fclose(f);
+ return 0;
+ }
+ if (nEntries == 0xFFFF || cdOff < 0 || cdOff >= fsize)
+ {
+ fclose(f);
+ // zip64/malformed: extract.
+ return 1;
+ }
+
+ cdSize = fsize - cdOff;
+ // Cap the transient malloc; fall back to extracting.
+ if (cdSize > (long)(8 * 1024 * 1024))
+ {
+ fclose(f);
+ LOGI("APK central dir too large (%ld); assuming assets exist", cdSize);
+ return 1;
+ }
+ cd = (unsigned char *)malloc((size_t)cdSize);
+ if (!cd)
+ {
+ fclose(f);
+ return 1;
+ }
+ if (fseek(f, cdOff, SEEK_SET) != 0 || fread(cd, 1, (size_t)cdSize, f) != (size_t)cdSize)
+ {
+ free(cd);
+ fclose(f);
+ return 1;
+ }
+ fclose(f);
+
+ found = 0;
+ for (e = 0, p = 0; e < nEntries; e++)
+ {
+ unsigned int nameLen, extraLen, commentLen;
+ if (p + 46 > cdSize)
+ break;
+ if (!(cd[p] == 0x50 && cd[p + 1] == 0x4b && cd[p + 2] == 0x01 && cd[p + 3] == 0x02))
+ break;
+ nameLen = (unsigned int)(cd[p + 28] | (cd[p + 29] << 8));
+ extraLen = (unsigned int)(cd[p + 30] | (cd[p + 31] << 8));
+ commentLen = (unsigned int)(cd[p + 32] | (cd[p + 33] << 8));
+ if (nameLen >= 7 && memcmp(cd + p + 46, "assets/", 7) == 0)
+ {
+ found = 1;
+ break;
+ }
+ p += 46 + nameLen + extraLen + commentLen;
+ }
+ free(cd);
+ }
+ return found;
+}
+
+static void extract_assets(void)
+{
+ JNIEnv *env = get_env();
+ if (!env)
+ return;
+ // Skip when the APK has no assets/ (merged providers would dump junk).
+ if (!apk_has_assets(g_apkPath))
+ {
+ LOGI("No app assets; skipping extraction");
+ return;
+ }
+ mkdir(g_basePath, 0755);
+ extract_dir(env, "", g_basePath);
+}
+
+/* ============================================================================
+ * stdout/stderr -> logcat pipe (so Ring `see` output is visible in logcat)
+ * ============================================================================
+ */
+
+static int g_logPipe[2];
+static pthread_t g_logThread;
+static atomic_int g_loggerStarted = 0;
+
+static void *log_thread(void *arg)
+{
+ ssize_t rdsz;
+ char buf[256];
+ (void)arg;
+ while ((rdsz = read(g_logPipe[0], buf, sizeof(buf) - 1)) > 0)
+ {
+ buf[rdsz] = '\0';
+ __android_log_write(ANDROID_LOG_INFO, "RingOutput", buf);
+ }
+ return NULL;
+}
+
+static void start_logger(void)
+{
+ // Detached thread, process lifetime; single-start for rotation.
+ if (atomic_exchange_explicit(&g_loggerStarted, 1, memory_order_acq_rel))
+ return;
+ setvbuf(stdout, 0, _IOLBF, 0);
+ setvbuf(stderr, 0, _IONBF, 0);
+ if (pipe(g_logPipe) != 0)
+ {
+ atomic_store_explicit(&g_loggerStarted, 0, memory_order_release);
+ return;
+ }
+ dup2(g_logPipe[1], 1);
+ dup2(g_logPipe[1], 2);
+ if (pthread_create(&g_logThread, 0, log_thread, 0) != 0)
+ return;
+ pthread_detach(g_logThread);
+}
+
+/* ============================================================================
+ * Worker thread: runs the Ring VM
+ * ============================================================================
+ */
+
+static void *worker_thread(void *arg)
+{
+ JNIEnv *env = NULL;
+
+ (void)arg;
+ if (g_jvm)
+ (*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL);
+
+ LOGI("Worker started, base path: %s", g_basePath);
+
+ start_logger();
+
+ extract_assets();
+ if (chdir(g_basePath) != 0)
+ LOGE("chdir failed: %s", g_basePath);
+
+ // Ring loading lives in the app's main.c.
+ ring_app_main();
+ LOGI("Ring app finished");
+
+ // Finish the Activity if still alive.
+ call_java_void(mid_finishApp);
+
+ // Worker is the last native user; drop global refs.
+ {
+ JNIEnv *cleanEnv = NULL;
+ if (g_jvm && (*g_jvm)->GetEnv(g_jvm, (void **)&cleanEnv, JNI_VERSION_1_6) == JNI_OK && cleanEnv)
+ {
+ pthread_mutex_lock(&g_activityMutex);
+ if (g_activity)
+ {
+ (*cleanEnv)->DeleteGlobalRef(cleanEnv, g_activity);
+ g_activity = NULL;
+ }
+ if (g_assetMgrJava)
+ {
+ (*cleanEnv)->DeleteGlobalRef(cleanEnv, g_assetMgrJava);
+ g_assetMgrJava = NULL;
+ }
+ pthread_mutex_unlock(&g_activityMutex);
+ g_assetMgr = NULL;
+ }
+ }
+
+ if (g_jvm)
+ (*g_jvm)->DetachCurrentThread(g_jvm);
+ return NULL;
+}
+
+// Java -> C (io.github.ysdragon.webview.MainActivity). Entry points only enqueue.
+
+JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
+{
+ (void)reserved;
+ g_jvm = vm;
+ LOGI("ring_webview_android loaded");
+ return JNI_VERSION_1_6;
+}
+
+static void cache_activity_methods(JNIEnv *env, jobject thiz)
+{
+ jclass cls = (*env)->GetObjectClass(env, thiz);
+ if (!cls)
+ return;
+ mid_evalJs = (*env)->GetMethodID(env, cls, "evalJs", "(Ljava/lang/String;)V");
+ mid_loadUrl = (*env)->GetMethodID(env, cls, "loadUrl", "(Ljava/lang/String;)V");
+ mid_loadHtml = (*env)->GetMethodID(env, cls, "loadHtml", "(Ljava/lang/String;)V");
+ mid_goBack = (*env)->GetMethodID(env, cls, "goBack", "()V");
+ mid_goForward = (*env)->GetMethodID(env, cls, "goForward", "()V");
+ mid_reload = (*env)->GetMethodID(env, cls, "reload", "()V");
+ mid_getUrlSync = (*env)->GetMethodID(env, cls, "getUrlSync", "()Ljava/lang/String;");
+ mid_getTitleSync = (*env)->GetMethodID(env, cls, "getTitleSync", "()Ljava/lang/String;");
+ mid_setDebug = (*env)->GetMethodID(env, cls, "setDebug", "(Z)V");
+ mid_setInjectJs = (*env)->GetMethodID(env, cls, "setInjectJs", "(Ljava/lang/String;)V");
+ mid_finishApp = (*env)->GetMethodID(env, cls, "finishApp", "()V");
+ (*env)->DeleteLocalRef(env, cls);
+}
+
+JNIEXPORT void JNICALL Java_io_github_ysdragon_webview_MainActivity_nativeStart(JNIEnv *env, jobject thiz)
+{
+ jobject assets;
+ jclass assetCls;
+ jobject filesDir;
+ jmethodID midGetFilesDir, midGetAbsPath, midGetAssets;
+ jstring absPath;
+ const char *utf;
+ jclass clsForPaths;
+
+ // Rotation: worker runs, just swap to the new Activity.
+ if (g_workerStarted)
+ {
+ jobject newRef;
+ jobject oldRef;
+ pthread_mutex_lock(&g_activityMutex);
+ newRef = (*env)->NewGlobalRef(env, thiz);
+ if (newRef)
+ {
+ oldRef = g_activity;
+ g_activity = newRef;
+ cache_activity_methods(env, thiz);
+ if (oldRef)
+ (*env)->DeleteGlobalRef(env, oldRef);
+ LOGI("re-attached to new Activity after rotation");
+ }
+ else
+ {
+ if ((*env)->ExceptionCheck(env))
+ (*env)->ExceptionClear(env);
+ LOGE("NewGlobalRef OOM on re-attach; keeping old Activity");
+ }
+ pthread_mutex_unlock(&g_activityMutex);
+ // New Activity lost injectJs; re-push from worker.
+ enqueue_raw_event(JOB_REATTACH, NULL);
+ return;
+ }
+
+ pthread_mutex_lock(&g_activityMutex);
+ g_activity = (*env)->NewGlobalRef(env, thiz);
+ if (!g_activity)
+ {
+ if ((*env)->ExceptionCheck(env))
+ (*env)->ExceptionClear(env);
+ pthread_mutex_unlock(&g_activityMutex);
+ LOGE("NewGlobalRef OOM in nativeStart");
+ return;
+ }
+ cache_activity_methods(env, thiz);
+ pthread_mutex_unlock(&g_activityMutex);
+ clsForPaths = (*env)->GetObjectClass(env, thiz);
+
+ if (!clsForPaths)
+ {
+ pthread_mutex_lock(&g_activityMutex);
+ if (g_activity)
+ {
+ (*env)->DeleteGlobalRef(env, g_activity);
+ g_activity = NULL;
+ }
+ pthread_mutex_unlock(&g_activityMutex);
+ LOGE("GetObjectClass failed in nativeStart");
+ return;
+ }
+ // AssetManager (Java + native).
+ midGetAssets = (*env)->GetMethodID(env, clsForPaths, "getAssets", "()Landroid/content/res/AssetManager;");
+ assets = midGetAssets ? (*env)->CallObjectMethod(env, thiz, midGetAssets) : NULL;
+ if (assets && !(*env)->ExceptionCheck(env))
+ {
+ jobject assetRef = (*env)->NewGlobalRef(env, assets);
+ if (assetRef)
+ {
+ g_assetMgrJava = assetRef;
+ g_assetMgr = AAssetManager_fromJava(env, assets);
+ assetCls = (*env)->GetObjectClass(env, assets);
+ if (assetCls)
+ {
+ mid_assetList = (*env)->GetMethodID(env, assetCls, "list", "(Ljava/lang/String;)[Ljava/lang/String;");
+ (*env)->DeleteLocalRef(env, assetCls);
+ }
+ }
+ else if ((*env)->ExceptionCheck(env))
+ (*env)->ExceptionClear(env);
+ (*env)->DeleteLocalRef(env, assets);
+ }
+ else if ((*env)->ExceptionCheck(env))
+ {
+ (*env)->ExceptionDescribe(env);
+ (*env)->ExceptionClear(env);
+ if (assets)
+ (*env)->DeleteLocalRef(env, assets);
+ }
+
+ // Base path = filesDir.
+ midGetFilesDir = (*env)->GetMethodID(env, clsForPaths, "getFilesDir", "()Ljava/io/File;");
+ filesDir = midGetFilesDir ? (*env)->CallObjectMethod(env, thiz, midGetFilesDir) : NULL;
+ if (filesDir && !(*env)->ExceptionCheck(env))
+ {
+ jclass fileCls = (*env)->GetObjectClass(env, filesDir);
+ if (fileCls)
+ {
+ midGetAbsPath = (*env)->GetMethodID(env, fileCls, "getAbsolutePath", "()Ljava/lang/String;");
+ absPath = midGetAbsPath ? (jstring)(*env)->CallObjectMethod(env, filesDir, midGetAbsPath) : NULL;
+ if (absPath && !(*env)->ExceptionCheck(env))
+ {
+ utf = (*env)->GetStringUTFChars(env, absPath, NULL);
+ if (utf)
+ {
+ snprintf(g_basePath, sizeof(g_basePath), "%s", utf);
+ (*env)->ReleaseStringUTFChars(env, absPath, utf);
+ }
+ else if ((*env)->ExceptionCheck(env))
+ (*env)->ExceptionClear(env);
+ (*env)->DeleteLocalRef(env, absPath);
+ }
+ else if ((*env)->ExceptionCheck(env))
+ {
+ (*env)->ExceptionClear(env);
+ if (absPath)
+ (*env)->DeleteLocalRef(env, absPath);
+ }
+ (*env)->DeleteLocalRef(env, fileCls);
+ }
+ (*env)->DeleteLocalRef(env, filesDir);
+ }
+ else if ((*env)->ExceptionCheck(env))
+ {
+ (*env)->ExceptionClear(env);
+ if (filesDir)
+ (*env)->DeleteLocalRef(env, filesDir);
+ }
+
+ // APK path (asset-presence check).
+ {
+ jmethodID midGetPkgPath = (*env)->GetMethodID(env, clsForPaths, "getPackageCodePath", "()Ljava/lang/String;");
+ jstring pkgPath = midGetPkgPath ? (jstring)(*env)->CallObjectMethod(env, thiz, midGetPkgPath) : NULL;
+ if (pkgPath && !(*env)->ExceptionCheck(env))
+ {
+ const char *putf = (*env)->GetStringUTFChars(env, pkgPath, NULL);
+ if (putf)
+ {
+ snprintf(g_apkPath, sizeof(g_apkPath), "%s", putf);
+ (*env)->ReleaseStringUTFChars(env, pkgPath, putf);
+ }
+ else if ((*env)->ExceptionCheck(env))
+ (*env)->ExceptionClear(env);
+ (*env)->DeleteLocalRef(env, pkgPath);
+ }
+ else if ((*env)->ExceptionCheck(env))
+ {
+ (*env)->ExceptionClear(env);
+ if (pkgPath)
+ (*env)->DeleteLocalRef(env, pkgPath);
+ }
+ }
+ (*env)->DeleteLocalRef(env, clsForPaths);
+ if (pthread_create(&g_workerThread, NULL, worker_thread, NULL) == 0)
+ g_workerStarted = 1;
+ else
+ LOGE("Failed to create worker thread");
+}
+
+// Java calls this only when finishing; rotation re-attaches instead.
+JNIEXPORT void JNICALL Java_io_github_ysdragon_webview_MainActivity_nativeDestroy(JNIEnv *env, jobject thiz)
+{
+ (void)env;
+ (void)thiz;
+ atomic_store_explicit(&g_terminate, 1, memory_order_release);
+ enqueue_terminate();
+ queue_signal_all();
+}
+
+JNIEXPORT void JNICALL Java_io_github_ysdragon_webview_MainActivity_nativeOnBridgeCall(JNIEnv *env, jobject thiz, jstring jname,
+ jstring jid, jstring jreq)
+{
+ const char *name, *id, *req;
+ (void)thiz;
+ name = jname ? (*env)->GetStringUTFChars(env, jname, NULL) : NULL;
+ id = jid ? (*env)->GetStringUTFChars(env, jid, NULL) : NULL;
+ req = jreq ? (*env)->GetStringUTFChars(env, jreq, NULL) : NULL;
+ if ((jname && !name) || (jid && !id) || (jreq && !req))
+ {
+ if ((*env)->ExceptionCheck(env))
+ (*env)->ExceptionClear(env);
+ LOGE("GetStringUTFChars OOM in bridge call; dropping");
+ }
+ else
+ {
+ enqueue_bind_call(name, id, req);
+ }
+
+ if (name)
+ (*env)->ReleaseStringUTFChars(env, jname, name);
+ if (id)
+ (*env)->ReleaseStringUTFChars(env, jid, id);
+ if (req)
+ (*env)->ReleaseStringUTFChars(env, jreq, req);
+}
+
+JNIEXPORT void JNICALL Java_io_github_ysdragon_webview_MainActivity_nativeOnPageStarted(JNIEnv *env, jobject thiz, jstring jurl)
+{
+ const char *url;
+ (void)thiz;
+ url = jurl ? (*env)->GetStringUTFChars(env, jurl, NULL) : NULL;
+ if (jurl && !url)
+ {
+ if ((*env)->ExceptionCheck(env))
+ (*env)->ExceptionClear(env);
+ LOGE("GetStringUTFChars OOM in onPageStarted");
+ return;
+ }
+ enqueue_raw_event(JOB_PAGE_STARTED, url);
+ if (url)
+ (*env)->ReleaseStringUTFChars(env, jurl, url);
+}
+
+JNIEXPORT void JNICALL Java_io_github_ysdragon_webview_MainActivity_nativeOnPageFinished(JNIEnv *env, jobject thiz, jstring jurl)
+{
+ const char *url;
+ (void)thiz;
+ url = jurl ? (*env)->GetStringUTFChars(env, jurl, NULL) : NULL;
+ if (jurl && !url)
+ {
+ if ((*env)->ExceptionCheck(env))
+ (*env)->ExceptionClear(env);
+ LOGE("GetStringUTFChars OOM in onPageFinished");
+ return;
+ }
+ enqueue_raw_event(JOB_PAGE_FINISHED, url);
+ if (url)
+ (*env)->ReleaseStringUTFChars(env, jurl, url);
+}
+
+JNIEXPORT void JNICALL Java_io_github_ysdragon_webview_MainActivity_nativeOnTitleChanged(JNIEnv *env, jobject thiz,
+ jstring jtitle)
+{
+ const char *title;
+ (void)thiz;
+ title = jtitle ? (*env)->GetStringUTFChars(env, jtitle, NULL) : NULL;
+ if (jtitle && !title)
+ {
+ if ((*env)->ExceptionCheck(env))
+ (*env)->ExceptionClear(env);
+ LOGE("GetStringUTFChars OOM in onTitleChanged");
+ return;
+ }
+ if (title)
+ enqueue_raw_event(JOB_TITLE_CHANGED, title);
+ if (title)
+ (*env)->ReleaseStringUTFChars(env, jtitle, title);
+}
+
+JNIEXPORT void JNICALL Java_io_github_ysdragon_webview_MainActivity_nativeOnFocusChanged(JNIEnv *env, jobject thiz,
+ jboolean focused)
+{
+ (void)env;
+ (void)thiz;
+ enqueue_raw_event(JOB_FOCUS_CHANGED, focused ? "true" : "false");
+}
+
+JNIEXPORT void JNICALL Java_io_github_ysdragon_webview_MainActivity_nativeOnResize(JNIEnv *env, jobject thiz, jint width,
+ jint height)
+{
+ char buf[32];
+ (void)env;
+ (void)thiz;
+ snprintf(buf, sizeof(buf), "%dx%d", (int)width, (int)height);
+ buf[sizeof(buf) - 1] = '\0';
+ enqueue_raw_event(JOB_RESIZED, buf);
+}
+
+JNIEXPORT void JNICALL Java_io_github_ysdragon_webview_MainActivity_nativeOnClose(JNIEnv *env, jobject thiz)
+{
+ (void)env;
+ (void)thiz;
+ enqueue_raw_event(JOB_CLOSE, NULL);
+}
+
+/* ============================================================================
+ * Ring C-API: helpers
+ * ============================================================================
+ */
+
+static void ring_webview_android_free(void *pState, void *pPointer)
+{
+ if (pPointer)
+ ring_state_free(pState, pPointer);
+}
+
+static int api_check_webview(void *pPointer)
+{
+ if (RING_API_PARACOUNT < 1)
+ return 0;
+ if (!RING_API_ISCPOINTER(1))
+ return 0;
+ if (!atomic_load_explicit(&g_webviewCreated, memory_order_acquire))
+ return 0;
+ (void)pPointer;
+ return 1;
+}
+
+/* ============================================================================
+ * Ring C-API: core lifecycle
+ * ============================================================================
+ */
+
+RING_FUNC(ring_webview_create)
+{
+ int debug;
+ int *pHandle;
+
+ if (RING_API_PARACOUNT != 2)
+ {
+ RING_API_ERROR(RING_API_MISS2PARA);
+ return;
+ }
+ if (!RING_API_ISNUMBER(1))
+ {
+ RING_API_ERROR(RING_API_BADPARATYPE);
+ return;
+ }
+ debug = (int)RING_API_GETNUMBER(1);
+
+ // One webview per process; all state is global.
+ if (atomic_exchange_explicit(&g_webviewCreated, 1, memory_order_acq_rel))
+ {
+ RING_API_ERROR("webview_create: Android backend supports a single webview instance");
+ return;
+ }
+ g_ringState = RING_API_STATE;
+ atomic_store_explicit(&g_terminate, 0, memory_order_release);
+
+ // Forward debug flag to Java.
+ {
+ JNIEnv *env = get_env();
+ jobject activity;
+ jmethodID setDebug;
+ pthread_mutex_lock(&g_activityMutex);
+ activity = g_activity;
+ setDebug = mid_setDebug;
+ if (env && activity && setDebug)
+ {
+ (*env)->CallVoidMethod(env, activity, setDebug, debug ? JNI_TRUE : JNI_FALSE);
+ // Clear: a pending exception aborts the next JNI call.
+ if ((*env)->ExceptionCheck(env))
+ {
+ (*env)->ExceptionDescribe(env);
+ (*env)->ExceptionClear(env);
+ }
+ }
+ pthread_mutex_unlock(&g_activityMutex);
+ }
+
+ // Shim now so bind works before the first page load.
+ rebuild_inject_js();
+
+ pHandle = (int *)RING_API_MALLOC(sizeof(int));
+ if (!pHandle)
+ {
+ RING_API_ERROR(RING_OOM);
+ return;
+ }
+ *pHandle = 1;
+ RING_API_RETMANAGEDCPOINTER(pHandle, "webview_t", ring_webview_android_free);
+}
+
+RING_FUNC(ring_webview_run)
+{
+ if (!api_check_webview(pPointer))
+ {
+ RING_API_ERROR(RING_API_BADPARATYPE);
+ return;
+ }
+
+ atomic_store_explicit(&g_running, 1, memory_order_release);
+ while (!atomic_load_explicit(&g_terminate, memory_order_acquire))
+ {
+ Job *job = dequeue_job_blocking();
+ if (job)
+ {
+ process_job(job);
+ free_job(job);
+ }
+ // NULL = terminated with an empty queue.
+ }
+ // Drain jobs that raced terminate; VM still alive.
+ for (;;)
+ {
+ Job *job = try_dequeue_job();
+ if (!job)
+ break;
+ process_job(job);
+ free_job(job);
+ }
+ atomic_store_explicit(&g_running, 0, memory_order_release);
+}
+
+RING_FUNC(ring_webview_terminate)
+{
+ if (!api_check_webview(pPointer))
+ return;
+ atomic_store_explicit(&g_terminate, 1, memory_order_release);
+ enqueue_terminate();
+ queue_signal_all();
+ call_java_void(mid_finishApp);
+}
+
+RING_FUNC(ring_webview_destroy)
+{
+ if (RING_API_PARACOUNT < 1)
+ return;
+ atomic_store_explicit(&g_terminate, 1, memory_order_release);
+ atomic_store_explicit(&g_running, 0, memory_order_release);
+ atomic_store_explicit(&g_webviewCreated, 0, memory_order_release);
+ enqueue_terminate();
+ queue_signal_all();
+ if (RING_API_ISCPOINTER(1))
+ RING_API_SETNULLPOINTER(1);
+}
+
+RING_FUNC(ring_webview_dispatch)
+{
+ if (RING_API_PARACOUNT != 2)
+ {
+ RING_API_ERROR(RING_API_MISS2PARA);
+ return;
+ }
+ if (!RING_API_ISCPOINTER(1) || !RING_API_ISSTRING(2))
+ {
+ RING_API_ERROR(RING_API_BADPARATYPE);
+ return;
+ }
+ enqueue_dispatch(RING_API_GETSTRING(2));
+ // WEBVIEW_ERROR_OK.
+ RING_API_RETNUMBER(0);
+}
+
+/* ============================================================================
+ * Ring C-API: navigation & content
+ * ============================================================================
+ */
+
+RING_FUNC(ring_webview_navigate)
+{
+ if (RING_API_PARACOUNT != 2 || !RING_API_ISCPOINTER(1) || !RING_API_ISSTRING(2))
+ {
+ RING_API_ERROR(RING_API_BADPARATYPE);
+ return;
+ }
+ call_java_void_string(mid_loadUrl, RING_API_GETSTRING(2));
+}
+
+RING_FUNC(ring_webview_set_html)
+{
+ if (RING_API_PARACOUNT != 2 || !RING_API_ISCPOINTER(1) || !RING_API_ISSTRING(2))
+ {
+ RING_API_ERROR(RING_API_BADPARATYPE);
+ return;
+ }
+ call_java_void_string(mid_loadHtml, RING_API_GETSTRING(2));
+}
+
+RING_FUNC(ring_webview_eval)
+{
+ if (RING_API_PARACOUNT != 2 || !RING_API_ISCPOINTER(1) || !RING_API_ISSTRING(2))
+ {
+ RING_API_ERROR(RING_API_BADPARATYPE);
+ return;
+ }
+ call_java_void_string(mid_evalJs, RING_API_GETSTRING(2));
+}
+
+RING_FUNC(ring_webview_init)
+{
+ if (RING_API_PARACOUNT != 2 || !RING_API_ISCPOINTER(1) || !RING_API_ISSTRING(2))
+ {
+ RING_API_ERROR(RING_API_BADPARATYPE);
+ return;
+ }
+ if (g_initScriptCount < MAX_INIT_SCRIPTS)
+ {
+ g_initScripts[g_initScriptCount] = xstrdup(RING_API_GETSTRING(2));
+ g_initScriptCount++;
+ rebuild_inject_js();
+ }
+}
+
+RING_FUNC(ring_webview_back)
+{
+ if (!api_check_webview(pPointer))
+ return;
+ call_java_void(mid_goBack);
+ RING_API_RETNUMBER(1);
+}
+
+RING_FUNC(ring_webview_forward)
+{
+ if (!api_check_webview(pPointer))
+ return;
+ call_java_void(mid_goForward);
+ RING_API_RETNUMBER(1);
+}
+
+RING_FUNC(ring_webview_reload)
+{
+ if (!api_check_webview(pPointer))
+ return;
+ call_java_void(mid_reload);
+ RING_API_RETNUMBER(1);
+}
+
+RING_FUNC(ring_webview_get_url)
+{
+ char *url;
+ if (!api_check_webview(pPointer))
+ {
+ RING_API_RETSTRING("");
+ return;
+ }
+ url = call_java_string_sync(mid_getUrlSync);
+ RING_API_RETSTRING(url ? url : "");
+ if (url)
+ free(url);
+}
+
+RING_FUNC(ring_webview_get_title)
+{
+ char *title;
+ if (!api_check_webview(pPointer))
+ {
+ RING_API_RETSTRING("");
+ return;
+ }
+ title = call_java_string_sync(mid_getTitleSync);
+ RING_API_RETSTRING(title ? title : "");
+ if (title)
+ free(title);
+}
+
+/* ============================================================================
+ * Ring C-API: binding
+ * ============================================================================
+ */
+
+RING_FUNC(ring_webview_bind)
+{
+ const char *jsName;
+ const char *ringFunc;
+ char *lower;
+ char *p;
+ int *h;
+
+ if (RING_API_PARACOUNT != 3)
+ {
+ RING_API_ERROR(RING_API_MISS3PARA);
+ return;
+ }
+ if (!RING_API_ISCPOINTER(1) || !RING_API_ISSTRING(2) || !RING_API_ISSTRING(3))
+ {
+ RING_API_ERROR(RING_API_BADPARATYPE);
+ return;
+ }
+ jsName = RING_API_GETSTRING(2);
+ ringFunc = RING_API_GETSTRING(3);
+
+ if (g_bindCount >= MAX_BINDS)
+ {
+ RING_API_RETNUMBER(-1);
+ return;
+ }
+
+ g_bindJsName[g_bindCount] = xstrdup(jsName);
+ lower = xstrdup(ringFunc);
+ for (p = lower; p && *p; p++)
+ *p = (char)tolower((unsigned char)*p);
+ g_bindRingFunc[g_bindCount] = lower;
+ g_bindCount++;
+
+ rebuild_inject_js();
+
+ h = (int *)RING_API_MALLOC(sizeof(int));
+ if (!h)
+ {
+ RING_API_ERROR(RING_OOM);
+ return;
+ }
+ *h = g_bindCount;
+ RING_API_RETMANAGEDCPOINTER(h, "webview_bind_t", ring_webview_android_free);
+}
+
+RING_FUNC(ring_webview_unbind)
+{
+ const char *jsName;
+ int i;
+ if (RING_API_PARACOUNT != 2 || !RING_API_ISCPOINTER(1) || !RING_API_ISSTRING(2))
+ {
+ RING_API_ERROR(RING_API_BADPARATYPE);
+ return;
+ }
+ jsName = RING_API_GETSTRING(2);
+ for (i = 0; i < g_bindCount; i++)
+ {
+ if (g_bindJsName[i] && strcmp(g_bindJsName[i], jsName) == 0)
+ {
+ free(g_bindJsName[i]);
+ free(g_bindRingFunc[i]);
+ g_bindJsName[i] = g_bindJsName[g_bindCount - 1];
+ g_bindRingFunc[i] = g_bindRingFunc[g_bindCount - 1];
+ g_bindCount--;
+ rebuild_inject_js();
+ RING_API_RETNUMBER(0);
+ return;
+ }
+ }
+ RING_API_RETNUMBER(-1);
+}
+
+RING_FUNC(ring_webview_return)
+{
+ const char *id;
+ int status;
+ const char *json;
+ char *cJsonOwned = NULL;
+ char *escId, *escJson, *js;
+ size_t cap;
+
+ if (RING_API_PARACOUNT != 4)
+ {
+ RING_API_ERROR(RING_API_MISS4PARA);
+ return;
+ }
+ if (!RING_API_ISCPOINTER(1) || !RING_API_ISSTRING(2) || !RING_API_ISNUMBER(3))
+ {
+ RING_API_ERROR(RING_API_BADPARATYPE);
+ return;
+ }
+ id = RING_API_GETSTRING(2);
+ status = (int)RING_API_GETNUMBER(3);
+
+ if (RING_API_ISLIST(4))
+ {
+ cJsonOwned = ring_list_to_json_string(RING_API_STATE, RING_API_GETLIST(4));
+ if (!cJsonOwned)
+ {
+ RING_API_ERROR("Failed to generate JSON string from list.");
+ return;
+ }
+ json = cJsonOwned;
+ }
+ else if (RING_API_ISNUMBER(4))
+ {
+ cJsonOwned = ring_number_to_json_string(RING_API_GETNUMBER(4));
+ if (!cJsonOwned)
+ {
+ RING_API_ERROR("Failed to generate JSON number.");
+ return;
+ }
+ json = cJsonOwned;
+ }
+ else if (RING_API_ISSTRING(4))
+ {
+ json = RING_API_GETSTRING(4);
+ }
+ else
+ {
+ RING_API_ERROR(RING_API_BADPARATYPE);
+ return;
+ }
+
+ escId = js_escape(id);
+ escJson = js_escape(json);
+ // Stale wreturn: page moved on, shim not back yet. Drop; the promise died with the page.
+ // Fixed overhead 71 + int; +128 slack, snprintf truncates on miscount.
+ cap = strlen(escId) + strlen(escJson) + 128;
+ js = (char *)malloc(cap);
+ if (js)
+ {
+ snprintf(js, cap, "(function(){var w=window.__ringwebview;if(w)w.result('%s',%d,'%s');})();", escId, status, escJson);
+ js[cap - 1] = '\0';
+ call_java_void_string(mid_evalJs, js);
+ free(js);
+ }
+ free(escId);
+ free(escJson);
+ if (cJsonOwned)
+ free(cJsonOwned);
+}
+
+RING_FUNC(ring_webview_version)
+{
+ if (RING_API_PARACOUNT != 0)
+ {
+ RING_API_ERROR(RING_API_BADPARACOUNT);
+ return;
+ }
+ RING_API_RETSTRING(RING_WEBVIEW_ANDROID_VERSION);
+}
+
+/* ============================================================================
+ * Ring C-API: event callbacks
+ * ============================================================================
+ */
+
+#define RING_WEBVIEW_ANDROID_SET_EVENT(funcname, member) \
+ RING_FUNC(funcname) \
+ { \
+ if (RING_API_PARACOUNT != 2) \
+ { \
+ RING_API_ERROR(RING_API_MISS2PARA); \
+ return; \
+ } \
+ if (!RING_API_ISCPOINTER(1) || !RING_API_ISSTRING(2)) \
+ { \
+ RING_API_ERROR(RING_API_BADPARATYPE); \
+ return; \
+ } \
+ if (member) \
+ { \
+ free(member); \
+ member = NULL; \
+ } \
+ member = xstrdup(RING_API_GETSTRING(2)); \
+ RING_API_RETNUMBER(1); \
+ }
+
+RING_WEBVIEW_ANDROID_SET_EVENT(ring_webview_on_close, g_onClose)
+RING_WEBVIEW_ANDROID_SET_EVENT(ring_webview_on_resize, g_onResize)
+RING_WEBVIEW_ANDROID_SET_EVENT(ring_webview_on_focus, g_onFocus)
+RING_WEBVIEW_ANDROID_SET_EVENT(ring_webview_on_dom_ready, g_onDomReady)
+RING_WEBVIEW_ANDROID_SET_EVENT(ring_webview_on_load, g_onLoad)
+RING_WEBVIEW_ANDROID_SET_EVENT(ring_webview_on_navigate, g_onNavigate)
+RING_WEBVIEW_ANDROID_SET_EVENT(ring_webview_on_title, g_onTitle)
+
+/* ============================================================================
+ * Ring C-API: window-management no-ops (desktop parity, safe on Android)
+ * ============================================================================
+ */
+
+#define RING_WEBVIEW_ANDROID_NOOP_RET1(funcname) \
+ RING_FUNC(funcname) \
+ { \
+ RING_API_RETNUMBER(1); \
+ }
+
+#define RING_WEBVIEW_ANDROID_NOOP_RET0(funcname) \
+ RING_FUNC(funcname) \
+ { \
+ RING_API_RETNUMBER(0); \
+ }
+
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_set_title)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_set_size)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_set_decorated)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_set_opacity)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_set_always_on_top)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_minimize)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_maximize)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_restore)
+RING_WEBVIEW_ANDROID_NOOP_RET0(ring_webview_is_maximized)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_start_drag)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_set_position)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_focus)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_hide)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_show)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_start_resize)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_set_fullscreen)
+RING_WEBVIEW_ANDROID_NOOP_RET0(ring_webview_is_fullscreen)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_set_resizable)
+RING_WEBVIEW_ANDROID_NOOP_RET0(ring_webview_is_resizable)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_set_min_size)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_set_max_size)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_set_background_color)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_set_icon)
+RING_WEBVIEW_ANDROID_NOOP_RET0(ring_webview_is_focused)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_is_visible)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_close)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_set_dev_tools)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_set_context_menu)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_set_force_dark)
+RING_WEBVIEW_ANDROID_NOOP_RET0(ring_webview_is_force_dark)
+RING_WEBVIEW_ANDROID_NOOP_RET1(ring_webview_set_click_through)
+RING_WEBVIEW_ANDROID_NOOP_RET0(ring_webview_is_click_through)
+
+RING_FUNC(ring_webview_get_window)
+{
+ RING_API_RETCPOINTER(NULL, "void");
+}
+
+RING_FUNC(ring_webview_get_native_handle)
+{
+ RING_API_RETCPOINTER(NULL, "void");
+}
+
+RING_FUNC(ring_webview_get_position)
+{
+ List *pList = ring_list_new(0);
+ ring_list_adddouble_gc(RING_API_STATE, pList, 0);
+ ring_list_adddouble_gc(RING_API_STATE, pList, 0);
+ RING_API_RETLIST(pList);
+}
+
+RING_FUNC(ring_webview_get_size)
+{
+ List *pList = ring_list_new(0);
+ ring_list_adddouble_gc(RING_API_STATE, pList, 0);
+ ring_list_adddouble_gc(RING_API_STATE, pList, 0);
+ RING_API_RETLIST(pList);
+}
+
+RING_FUNC(ring_webview_get_screens)
+{
+ List *pList = ring_list_new(0);
+ RING_API_RETLIST(pList);
+}
+
+/* ============================================================================
+ * Ring C-API: constants
+ * ============================================================================
+ */
+
+RING_FUNC(ring_get_webview_hint_none)
+{
+ RING_API_RETNUMBER(0);
+}
+RING_FUNC(ring_get_webview_hint_min)
+{
+ RING_API_RETNUMBER(1);
+}
+RING_FUNC(ring_get_webview_hint_max)
+{
+ RING_API_RETNUMBER(2);
+}
+RING_FUNC(ring_get_webview_hint_fixed)
+{
+ RING_API_RETNUMBER(3);
+}
+RING_FUNC(ring_get_webview_native_handle_kind_ui_window)
+{
+ RING_API_RETNUMBER(0);
+}
+RING_FUNC(ring_get_webview_native_handle_kind_ui_widget)
+{
+ RING_API_RETNUMBER(1);
+}
+RING_FUNC(ring_get_webview_native_handle_kind_browser_controller)
+{
+ RING_API_RETNUMBER(2);
+}
+RING_FUNC(ring_get_webview_error_ok)
+{
+ RING_API_RETNUMBER(0);
+}
+RING_FUNC(ring_get_webview_error_unspecified)
+{
+ RING_API_RETNUMBER(1);
+}
+RING_FUNC(ring_get_webview_error_invalid_argument)
+{
+ RING_API_RETNUMBER(2);
+}
+RING_FUNC(ring_get_webview_error_invalid_state)
+{
+ RING_API_RETNUMBER(3);
+}
+RING_FUNC(ring_get_webview_error_canceled)
+{
+ RING_API_RETNUMBER(4);
+}
+RING_FUNC(ring_get_webview_error_missing_dependency)
+{
+ RING_API_RETNUMBER(5);
+}
+RING_FUNC(ring_get_webview_error_duplicate)
+{
+ RING_API_RETNUMBER(6);
+}
+RING_FUNC(ring_get_webview_error_not_found)
+{
+ RING_API_RETNUMBER(7);
+}
+RING_FUNC(ring_get_webview_version_major)
+{
+ RING_API_RETNUMBER(0);
+}
+RING_FUNC(ring_get_webview_version_minor)
+{
+ RING_API_RETNUMBER(12);
+}
+RING_FUNC(ring_get_webview_version_patch)
+{
+ RING_API_RETNUMBER(0);
+}
+RING_FUNC(ring_get_webview_edge_top)
+{
+ RING_API_RETNUMBER(0);
+}
+RING_FUNC(ring_get_webview_edge_bottom)
+{
+ RING_API_RETNUMBER(1);
+}
+RING_FUNC(ring_get_webview_edge_left)
+{
+ RING_API_RETNUMBER(2);
+}
+RING_FUNC(ring_get_webview_edge_right)
+{
+ RING_API_RETNUMBER(3);
+}
+RING_FUNC(ring_get_webview_edge_top_left)
+{
+ RING_API_RETNUMBER(4);
+}
+RING_FUNC(ring_get_webview_edge_top_right)
+{
+ RING_API_RETNUMBER(5);
+}
+RING_FUNC(ring_get_webview_edge_bottom_left)
+{
+ RING_API_RETNUMBER(6);
+}
+RING_FUNC(ring_get_webview_edge_bottom_right)
+{
+ RING_API_RETNUMBER(7);
+}
+
+/* ============================================================================
+ * Library initialization
+ * ============================================================================
+ */
+
+RING_LIBINIT
+{
+ /* Core lifecycle */
+ RING_API_REGISTER("webview_create", ring_webview_create);
+ RING_API_REGISTER("webview_destroy", ring_webview_destroy);
+ RING_API_REGISTER("webview_run", ring_webview_run);
+ RING_API_REGISTER("webview_terminate", ring_webview_terminate);
+ RING_API_REGISTER("webview_dispatch", ring_webview_dispatch);
+ RING_API_REGISTER("webview_version", ring_webview_version);
+
+ /* Navigation & content */
+ RING_API_REGISTER("webview_navigate", ring_webview_navigate);
+ RING_API_REGISTER("webview_set_html", ring_webview_set_html);
+ RING_API_REGISTER("webview_eval", ring_webview_eval);
+ RING_API_REGISTER("webview_init", ring_webview_init);
+ RING_API_REGISTER("webview_back", ring_webview_back);
+ RING_API_REGISTER("webview_forward", ring_webview_forward);
+ RING_API_REGISTER("webview_reload", ring_webview_reload);
+ RING_API_REGISTER("webview_get_url", ring_webview_get_url);
+ RING_API_REGISTER("webview_get_title", ring_webview_get_title);
+
+ /* Binding */
+ RING_API_REGISTER("webview_bind", ring_webview_bind);
+ RING_API_REGISTER("webview_unbind", ring_webview_unbind);
+ RING_API_REGISTER("webview_return", ring_webview_return);
+
+ /* Handles */
+ RING_API_REGISTER("webview_get_window", ring_webview_get_window);
+ RING_API_REGISTER("webview_get_native_handle", ring_webview_get_native_handle);
+
+ /* Window management */
+ RING_API_REGISTER("webview_set_title", ring_webview_set_title);
+ RING_API_REGISTER("webview_set_size", ring_webview_set_size);
+ RING_API_REGISTER("webview_set_decorated", ring_webview_set_decorated);
+ RING_API_REGISTER("webview_set_opacity", ring_webview_set_opacity);
+ RING_API_REGISTER("webview_set_always_on_top", ring_webview_set_always_on_top);
+ RING_API_REGISTER("webview_minimize", ring_webview_minimize);
+ RING_API_REGISTER("webview_maximize", ring_webview_maximize);
+ RING_API_REGISTER("webview_restore", ring_webview_restore);
+ RING_API_REGISTER("webview_is_maximized", ring_webview_is_maximized);
+ RING_API_REGISTER("webview_start_drag", ring_webview_start_drag);
+ RING_API_REGISTER("webview_set_position", ring_webview_set_position);
+ RING_API_REGISTER("webview_get_position", ring_webview_get_position);
+ RING_API_REGISTER("webview_get_size", ring_webview_get_size);
+ RING_API_REGISTER("webview_focus", ring_webview_focus);
+ RING_API_REGISTER("webview_hide", ring_webview_hide);
+ RING_API_REGISTER("webview_show", ring_webview_show);
+ RING_API_REGISTER("webview_start_resize", ring_webview_start_resize);
+ RING_API_REGISTER("webview_set_fullscreen", ring_webview_set_fullscreen);
+ RING_API_REGISTER("webview_is_fullscreen", ring_webview_is_fullscreen);
+ RING_API_REGISTER("webview_set_resizable", ring_webview_set_resizable);
+ RING_API_REGISTER("webview_is_resizable", ring_webview_is_resizable);
+ RING_API_REGISTER("webview_set_min_size", ring_webview_set_min_size);
+ RING_API_REGISTER("webview_set_max_size", ring_webview_set_max_size);
+ RING_API_REGISTER("webview_set_background_color", ring_webview_set_background_color);
+ RING_API_REGISTER("webview_set_icon", ring_webview_set_icon);
+ RING_API_REGISTER("webview_is_focused", ring_webview_is_focused);
+ RING_API_REGISTER("webview_is_visible", ring_webview_is_visible);
+ RING_API_REGISTER("webview_close", ring_webview_close);
+ RING_API_REGISTER("webview_set_dev_tools", ring_webview_set_dev_tools);
+ RING_API_REGISTER("webview_set_context_menu", ring_webview_set_context_menu);
+ RING_API_REGISTER("webview_set_force_dark", ring_webview_set_force_dark);
+ RING_API_REGISTER("webview_is_force_dark", ring_webview_is_force_dark);
+ RING_API_REGISTER("webview_get_screens", ring_webview_get_screens);
+ RING_API_REGISTER("webview_set_click_through", ring_webview_set_click_through);
+ RING_API_REGISTER("webview_is_click_through", ring_webview_is_click_through);
+
+ /* Event callbacks */
+ RING_API_REGISTER("webview_on_close", ring_webview_on_close);
+ RING_API_REGISTER("webview_on_resize", ring_webview_on_resize);
+ RING_API_REGISTER("webview_on_focus", ring_webview_on_focus);
+ RING_API_REGISTER("webview_on_dom_ready", ring_webview_on_dom_ready);
+ RING_API_REGISTER("webview_on_load", ring_webview_on_load);
+ RING_API_REGISTER("webview_on_navigate", ring_webview_on_navigate);
+ RING_API_REGISTER("webview_on_title", ring_webview_on_title);
+
+ /* Constants */
+ RING_API_REGISTER("get_webview_hint_none", ring_get_webview_hint_none);
+ RING_API_REGISTER("get_webview_hint_min", ring_get_webview_hint_min);
+ RING_API_REGISTER("get_webview_hint_max", ring_get_webview_hint_max);
+ RING_API_REGISTER("get_webview_hint_fixed", ring_get_webview_hint_fixed);
+ RING_API_REGISTER("get_webview_native_handle_kind_ui_window", ring_get_webview_native_handle_kind_ui_window);
+ RING_API_REGISTER("get_webview_native_handle_kind_ui_widget", ring_get_webview_native_handle_kind_ui_widget);
+ RING_API_REGISTER("get_webview_native_handle_kind_browser_controller",
+ ring_get_webview_native_handle_kind_browser_controller);
+ RING_API_REGISTER("get_webview_error_ok", ring_get_webview_error_ok);
+ RING_API_REGISTER("get_webview_error_unspecified", ring_get_webview_error_unspecified);
+ RING_API_REGISTER("get_webview_error_invalid_argument", ring_get_webview_error_invalid_argument);
+ RING_API_REGISTER("get_webview_error_invalid_state", ring_get_webview_error_invalid_state);
+ RING_API_REGISTER("get_webview_error_canceled", ring_get_webview_error_canceled);
+ RING_API_REGISTER("get_webview_error_missing_dependency", ring_get_webview_error_missing_dependency);
+ RING_API_REGISTER("get_webview_error_duplicate", ring_get_webview_error_duplicate);
+ RING_API_REGISTER("get_webview_error_not_found", ring_get_webview_error_not_found);
+ RING_API_REGISTER("get_webview_version_major", ring_get_webview_version_major);
+ RING_API_REGISTER("get_webview_version_minor", ring_get_webview_version_minor);
+ RING_API_REGISTER("get_webview_version_patch", ring_get_webview_version_patch);
+ RING_API_REGISTER("get_webview_edge_top", ring_get_webview_edge_top);
+ RING_API_REGISTER("get_webview_edge_bottom", ring_get_webview_edge_bottom);
+ RING_API_REGISTER("get_webview_edge_left", ring_get_webview_edge_left);
+ RING_API_REGISTER("get_webview_edge_right", ring_get_webview_edge_right);
+ RING_API_REGISTER("get_webview_edge_top_left", ring_get_webview_edge_top_left);
+ RING_API_REGISTER("get_webview_edge_top_right", ring_get_webview_edge_top_right);
+ RING_API_REGISTER("get_webview_edge_bottom_left", ring_get_webview_edge_bottom_left);
+ RING_API_REGISTER("get_webview_edge_bottom_right", ring_get_webview_edge_bottom_right);
+}