@g9tm/lynxcontrols-native 0.0.0-stage → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +85 -2
  2. package/android/build.gradle.kts +45 -0
  3. package/android/consumer-rules.pro +3 -0
  4. package/android/src/main/AndroidManifest.xml +5 -0
  5. package/android/src/main/java/com/g9lynx/g9native/G9Bridge.kt +67 -0
  6. package/android/src/main/java/com/g9lynx/g9native/G9HostModule.kt +159 -0
  7. package/android/src/main/java/com/g9lynx/g9native/G9SqliteModule.kt +49 -0
  8. package/android/src/main/java/com/g9lynx/g9native/generated/G9HostModuleSpec.java +48 -0
  9. package/android/src/main/java/com/g9lynx/g9native/generated/G9SqliteModuleSpec.java +41 -0
  10. package/android/src/main/java/com/g9lynx/g9native/host/core/G9PreferencesCore.kt +100 -0
  11. package/android/src/main/java/com/g9lynx/g9native/sqlite/androidx/AndroidxSqlEngine.kt +88 -0
  12. package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqlEngine.kt +61 -0
  13. package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqliteCore.kt +260 -0
  14. package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqliteService.kt +131 -0
  15. package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqliteTransport.kt +125 -0
  16. package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqliteValues.kt +71 -0
  17. package/android/src/main/java/com/g9lynx/g9native/video/G9VideoUI.kt +214 -0
  18. package/android/src/main/java/com/g9lynx/g9native/video/core/G9VideoCore.kt +67 -0
  19. package/dist/.tsbuildinfo +1 -0
  20. package/dist/driver.d.ts +20 -0
  21. package/dist/driver.d.ts.map +1 -0
  22. package/dist/driver.js +159 -0
  23. package/dist/driver.js.map +1 -0
  24. package/dist/index.d.ts +5 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +3 -0
  27. package/dist/media.d.ts +60 -0
  28. package/dist/media.d.ts.map +1 -0
  29. package/dist/media.jsx +119 -0
  30. package/dist/media.jsx.map +1 -0
  31. package/dist/protocol.d.ts +115 -0
  32. package/dist/protocol.d.ts.map +1 -0
  33. package/dist/protocol.js +1 -0
  34. package/dist/services.d.ts +36 -0
  35. package/dist/services.d.ts.map +1 -0
  36. package/dist/services.js +111 -0
  37. package/dist/services.js.map +1 -0
  38. package/dist/transport.d.ts +10 -0
  39. package/dist/transport.d.ts.map +1 -0
  40. package/dist/transport.js +77 -0
  41. package/dist/transport.js.map +1 -0
  42. package/generated/G9HostModule.ts +71 -0
  43. package/generated/G9SqliteModule.ts +68 -0
  44. package/ios/Tests/G9NativeTestSuite.h +25 -0
  45. package/ios/Tests/G9NativeTestSuite.m +560 -0
  46. package/ios/Tests/G9NativeTests.m +52 -0
  47. package/ios/g9lynx-native.podspec +29 -0
  48. package/ios/src/G9HostModule.h +14 -0
  49. package/ios/src/G9HostModule.m +121 -0
  50. package/ios/src/G9SqliteModule.h +14 -0
  51. package/ios/src/G9SqliteModule.m +167 -0
  52. package/ios/src/G9VideoUI.h +21 -0
  53. package/ios/src/G9VideoUI.m +250 -0
  54. package/ios/src/core/G9HostPaths.h +20 -0
  55. package/ios/src/core/G9HostPaths.m +23 -0
  56. package/ios/src/core/G9PreferencesCore.h +43 -0
  57. package/ios/src/core/G9PreferencesCore.m +114 -0
  58. package/ios/src/core/G9SqliteCore.h +62 -0
  59. package/ios/src/core/G9SqliteCore.m +497 -0
  60. package/ios/src/core/G9SqliteTransport.h +16 -0
  61. package/ios/src/core/G9SqliteTransport.m +85 -0
  62. package/ios/src/core/G9VideoCore.h +26 -0
  63. package/ios/src/core/G9VideoCore.m +37 -0
  64. package/ios/src/generated/G9HostModuleSpec.h +23 -0
  65. package/ios/src/generated/G9HostModuleSpec.m +2 -0
  66. package/ios/src/generated/G9SqliteModuleSpec.h +20 -0
  67. package/ios/src/generated/G9SqliteModuleSpec.m +2 -0
  68. package/lynx.lib.json +12 -0
  69. package/package.json +49 -4
  70. package/types/platform-native-module.d.ts +46 -0
package/README.md CHANGED
@@ -1,3 +1,86 @@
1
- # Temporary Holding Version
1
+ # @g9tm/lynxcontrols-native
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Android and iOS platform layer for G9: two Lynx native modules and one element (`<g9-video>`), linked into a host by
4
+ **Lynx Autolink** from `node_modules`, plus their JS adapters.
5
+
6
+ | Module | JS adapter | Android (Kotlin) | iOS (Objective-C) |
7
+ | --- | --- | --- | --- |
8
+ | `G9SqliteModule` | `createNativeSqliteDriver()` | androidx.sqlite `BundledSQLiteDriver` 2.7.1 | system `sqlite3` (iOS 15+, serial queue per session, `sqlite3_interrupt`) |
9
+ | `G9HostModule` | `createNativeHostServices()` | `SharedPreferences`, `moveTaskToBack`, `InputMethodManager`, haptics, insets, `appDataDirectory()` = `filesDir` | `NSUserDefaults` suite, `resignFirstResponder`, `UIImpactFeedbackGenerator`, safe-area insets, `appDataDirectory()` = `Library` |
10
+ | `<g9-video>` element | `createNativeMediaProvider()` (`@g9tm/lynxcontrols-native/media`) | `G9VideoUI`: Media3 ExoPlayer 1.8.0 into a `TextureView` (opacity fades work); register `G9VideoBehaviors.create()` | `G9VideoUI`: `AVQueuePlayer` + `AVPlayerLooper` in an `AVPlayerLayer`; self-registers (`LYNX_REGISTER_UI`) |
11
+
12
+ ```ts
13
+ import { createG9Sqlite, singleFileDatabaseLocator } from '@g9tm/lynxcontrols-sqlite';
14
+ import { createNativeHostServices, createNativeSqliteDriver } from '@g9tm/lynxcontrols-native';
15
+
16
+ const { host, preferences, appDataDirectory } = createNativeHostServices();
17
+ const root = await appDataDirectory(); // MAUI FileSystem.AppDataDirectory: the MAUI app's database stays found
18
+ const db = createG9Sqlite({ driver: createNativeSqliteDriver(), locator: singleFileDatabaseLocator(`${root}/g9.db`), /* … */ });
19
+ // <G9Provider services={{ clock, host, preferences }}>
20
+ ```
21
+
22
+ ## How it works
23
+
24
+ - **Video** (the intro carousel's `services.media`). `<g9-video>` props: `src`, `paused`, `loop`, `muted`,
25
+ `start-seconds`, `near-end-seconds`, `object-fit`; events `ready`, `nearend`, `looped`, `error`. The provider's
26
+ player renders it with reactive props (no UI-method calls) and unmounts it on `dispose()`, which releases the native
27
+ player. `resolve()` maps bundled assets with `G9HostModule.assetUri` (`not-found` when the app does not ship the
28
+ file). The near-end/loop and start-seek rules are the platform-free cores `video/core/G9VideoCore.kt` and
29
+ `ios/src/core/G9VideoCore.m`, tested on the JVM and on Linux.
30
+
31
+ - **Contract.** The driver passes the shared contract `@g9tm/lynxcontrols-sqlite/contract/cases.json` (27 cases): on the JVM through
32
+ the Kotlin core (`npm run test:android-jvm`), and in JS through the module protocol (`src/native.test.ts`).
33
+ - **Threads.** One single-thread executor per open database with a bounded queue (256; overflow → `busy`). Replies
34
+ are envelopes `{ ok, value | error }`, so `G9SqliteError` fields (`code`, `sqliteCode`, `sql`,
35
+ `completedStatements`) survive the bridge.
36
+ - **Cancellation.** A batch aborted while queued in JS never reaches native code.
37
+ - Aborting a running batch calls `interrupt(session)`.
38
+ - iOS: `sqlite3_interrupt` stops the statement, and the batch fails with `interrupted`.
39
+ - Android: androidx connections have no interrupt, so the statement finishes (`capabilities().interrupt === false`).
40
+ - **Transport** (`plan/specs/06` §6). `columnar` (default): cell tags, float64 numbers and blob bytes as ArrayBuffers,
41
+ text as strings. `rows`: tagged cells. Choose with `createNativeSqliteDriver({ transport })`.
42
+ `contract/transport-golden.json` pins the bytes: the Kotlin encoder writes it, and the JS decoder test reads it.
43
+ - **Values.** Safe integers cross as numbers, others as `{ int64 }`, BLOBs as ArrayBuffer. NUL in text, `NaN` and
44
+ `undefined` are rejected with `misuse`, as on every driver.
45
+
46
+ ## Layout
47
+
48
+ ```text
49
+ types/platform-native-module.d.ts codegen input (`npm run codegen` → generated/, android/…/generated, ios/src/generated;
50
+ then scripts/android-bridge-types.mjs --fix gives the Android specs the parameter
51
+ types Lynx's bridge accepts — never run the bare generator)
52
+ scripts/android-bridge-types.mjs `--fix` after codegen; without it, the check (`npm run check:android-types`)
53
+ android/src/main/java/com/g9lynx/g9native/
54
+ sqlite/core/ pure Kotlin: G9SqliteCore, G9SqliteService (executors), G9SqliteTransport, values/errors
55
+ sqlite/androidx/ AndroidxSqlEngine (BundledSQLiteDriver)
56
+ host/core/ G9PreferencesCore
57
+ G9SqliteModule.kt, G9HostModule.kt, G9Bridge.kt Lynx modules (kapt + lynx-processor → LynxLibraryProviderImpl)
58
+ android/src/test/ JVM tests: contract runner, core, service, transport golden, preferences
59
+ ios/src/core/ portable Objective-C (Foundation + sqlite3): G9SqliteCore, G9SqliteTransport, G9PreferencesCore
60
+ ios/src/ G9SqliteModule.m, G9HostModule.m (dispatch queues, UIKit); register them on the host's LynxConfig
61
+ ios/Tests/ G9NativeTestSuite (XCTest on a simulator; Linux harness in Tests/linux)
62
+ jvm/ JVM-only Gradle build of the core + tests (no Android SDK needed)
63
+ android-check/ cloud compile check of module + reference host against the published Lynx 4.1.0 classes, and
64
+ LynxBridgeSignatureTest: every @LynxMethod through Lynx's own LynxMethodWrapper
65
+ ```
66
+
67
+ The Android package is `com.g9lynx.g9native`, not `com.g9lynx.native`: `native` is a reserved word in Java, so the
68
+ generated Java specs could not compile under that name.
69
+
70
+ The native identifiers are internal and did not follow the npm rename to `@g9tm/lynxcontrols-native` (decision D12):
71
+ the Android package `com.g9lynx.g9native`, the iOS pod `g9lynx-native`, the module classes `G9SqliteModule` and
72
+ `G9HostModule`, and the preference store `g9lynx.preferences` keep their names, so hosts and stored data are not
73
+ affected. The pod's version follows the package version (= the pinned G9MAUIControls version).
74
+
75
+ ## Commands
76
+
77
+ | Command | What it proves |
78
+ | --- | --- |
79
+ | `npm run test:android-jvm` | Kotlin core: contract 27/27 over both transports (JDBC engine), unit tests, JaCoCo gate (lines ≥ 95 %, branches ≥ 85 %) |
80
+ | `gradle -p packages/native/jvm check -Pg9.bundled=true` | The same on androidx `BundledSQLiteDriver` (needs Google Maven) |
81
+ | `npm run check:android-module` | Module and host compile against Lynx 4.1.0, and the Autolink provider is generated |
82
+ | `npm run test:ios-core` | iOS core on Linux (clang + GNUstep): contract 27/27 × 2, golden byte-identical with Kotlin, interrupt, ARC syntax |
83
+ | `npm run bench:transport` | JS decode cost of `columnar` vs `rows` |
84
+
85
+ Host integration: [`docs/integration/android.md`](../../docs/integration/android.md) and
86
+ [`docs/integration/ios.md`](../../docs/integration/ios.md) (reference hosts: `apps/android-host`, `apps/ios-host`).
@@ -0,0 +1,45 @@
1
+ // @g9tm/lynxcontrols-native for Android (plan/specs/06 §4, specs/08 §3). Linked into a host by Lynx Autolink from
2
+ // node_modules; the host's settings supply the plugin versions (AGP 8.13.0, Kotlin 2.3.20).
3
+ plugins {
4
+ id("com.android.library")
5
+ id("org.jetbrains.kotlin.android")
6
+ id("org.jetbrains.kotlin.kapt")
7
+ }
8
+
9
+ android {
10
+ namespace = "com.g9lynx.g9native"
11
+ compileSdk = 36
12
+
13
+ defaultConfig {
14
+ minSdk = 26
15
+ consumerProguardFiles("consumer-rules.pro")
16
+ }
17
+
18
+ compileOptions {
19
+ sourceCompatibility = JavaVersion.VERSION_17
20
+ targetCompatibility = JavaVersion.VERSION_17
21
+ }
22
+
23
+ testOptions { unitTests.isReturnDefaultValues = true }
24
+ }
25
+
26
+ kotlin { jvmToolchain(17) }
27
+
28
+ val lynx = "4.1.0"
29
+
30
+ dependencies {
31
+ compileOnly("org.lynxsdk.lynx:lynx:$lynx")
32
+ compileOnly("org.lynxsdk.lynx:service-api:$lynx")
33
+ kapt("org.lynxsdk.lynx:lynx-processor:$lynx")
34
+ // The generated specs import androidx.annotation and lynx-processor's output uses @Keep, but Lynx's POMs declare it
35
+ // at runtime scope only: without this line kapt depends on androidx.sqlite / media3 bringing it in transitively.
36
+ compileOnly("androidx.annotation:annotation:1.9.1")
37
+ implementation("androidx.sqlite:sqlite-bundled:2.7.1")
38
+ // <g9-video> (video/G9VideoUI.kt): ExoPlayer drawing into a TextureView. Google Maven only.
39
+ implementation("androidx.media3:media3-exoplayer:1.8.0")
40
+
41
+ // The JVM contract suite also runs as Android local unit tests (same sources as ../jvm).
42
+ testImplementation("junit:junit:4.13.2")
43
+ testImplementation("org.json:json:20250517")
44
+ testImplementation("org.xerial:sqlite-jdbc:3.50.3.0")
45
+ }
@@ -0,0 +1,3 @@
1
+ # Lynx looks modules up by annotation name; keep them and their methods.
2
+ -keep @com.lynx.jsbridge.LynxNativeModule class * { *; }
3
+ -keepclassmembers class * { @com.lynx.jsbridge.LynxMethod *; }
@@ -0,0 +1,5 @@
1
+ <manifest
2
+ xmlns:android="http://schemas.android.com/apk/res/android"
3
+ package="com.g9lynx.g9native">
4
+ <application />
5
+ </manifest>
@@ -0,0 +1,67 @@
1
+ package com.g9lynx.g9native
2
+
3
+ import com.lynx.react.bridge.Callback
4
+ import com.lynx.react.bridge.JavaOnlyArray
5
+ import com.lynx.react.bridge.JavaOnlyMap
6
+ import com.lynx.react.bridge.ReadableArray
7
+ import com.lynx.react.bridge.ReadableMap
8
+
9
+ /**
10
+ * Copies between Lynx bridge containers and the plain Kotlin data the cores speak (`Map`, `List`, `String`, `Double`,
11
+ * `Boolean`, `ByteArray` ⇄ ArrayBuffer, `null`). Everything else of the modules is plain JVM code.
12
+ */
13
+ internal object G9Bridge {
14
+ fun map(value: Any?): Map<*, *> =
15
+ when (value) {
16
+ is ReadableMap -> value.toHashMap()
17
+ is Map<*, *> -> value
18
+ else -> emptyMap<String, Any?>()
19
+ }
20
+
21
+ fun list(value: Any?): List<*> =
22
+ when (value) {
23
+ is ReadableArray -> value.toArrayList()
24
+ is List<*> -> value
25
+ else -> emptyList<Any?>()
26
+ }
27
+
28
+ fun toMap(value: Map<*, *>): JavaOnlyMap {
29
+ val out = JavaOnlyMap()
30
+ for ((k, v) in value) {
31
+ val key = k as String
32
+ when (v) {
33
+ null -> out.putNull(key)
34
+ is Boolean -> out.putBoolean(key, v)
35
+ is Number -> out.putDouble(key, v.toDouble())
36
+ is String -> out.putString(key, v)
37
+ is ByteArray -> out.putByteArray(key, v)
38
+ is Map<*, *> -> out.putMap(key, toMap(v))
39
+ is List<*> -> out.putArray(key, toArray(v))
40
+ else -> out.putString(key, v.toString())
41
+ }
42
+ }
43
+ return out
44
+ }
45
+
46
+ fun toArray(value: List<*>): JavaOnlyArray {
47
+ val out = JavaOnlyArray()
48
+ for (v in value) {
49
+ when (v) {
50
+ null -> out.pushNull()
51
+ is Boolean -> out.pushBoolean(v)
52
+ is Number -> out.pushDouble(v.toDouble())
53
+ is String -> out.pushString(v)
54
+ is ByteArray -> out.pushByteArray(v)
55
+ is Map<*, *> -> out.pushMap(toMap(v))
56
+ is List<*> -> out.pushArray(toArray(v))
57
+ else -> out.pushString(v.toString())
58
+ }
59
+ }
60
+ return out
61
+ }
62
+
63
+ /** Invokes a JS callback with one envelope (Lynx marshals the call to the JS thread). */
64
+ fun reply(callback: Callback?, envelope: Map<String, Any?>) {
65
+ callback?.invoke(toMap(envelope))
66
+ }
67
+ }
@@ -0,0 +1,159 @@
1
+ package com.g9lynx.g9native
2
+
3
+ import android.app.Activity
4
+ import android.content.Context
5
+ import android.content.ContextWrapper
6
+ import android.os.Build
7
+ import android.os.Handler
8
+ import android.os.Looper
9
+ import android.view.HapticFeedbackConstants
10
+ import android.view.View
11
+ import android.view.WindowManager
12
+ import android.view.inputmethod.InputMethodManager
13
+ import com.g9lynx.g9native.generated.G9HostModuleSpec
14
+ import com.g9lynx.g9native.host.core.G9PreferenceBackend
15
+ import com.g9lynx.g9native.host.core.G9PreferencesCore
16
+ import com.lynx.jsbridge.LynxMethod
17
+ import com.lynx.jsbridge.LynxNativeModule
18
+ import com.lynx.react.bridge.Callback
19
+ import com.lynx.tasm.behavior.LynxContext
20
+
21
+ /**
22
+ * `NativeModules.G9HostModule` (plan/specs/06 §7): preferences in `SharedPreferences` (typed int/string, like MAUI's
23
+ * `Preferences`), move-to-back when back is unhandled, hide keyboard, haptic tick, soft-input mode, safe-area insets.
24
+ * UI calls run on the main thread.
25
+ */
26
+ @LynxNativeModule(name = "G9HostModule")
27
+ class G9HostModule(private val lynxContext: LynxContext) : G9HostModuleSpec(lynxContext) {
28
+ private val main = Handler(Looper.getMainLooper())
29
+
30
+ private val preferences: G9PreferencesCore by lazy {
31
+ val prefs = lynxContext.applicationContext.getSharedPreferences("g9lynx.preferences", Context.MODE_PRIVATE)
32
+ G9PreferencesCore(
33
+ object : G9PreferenceBackend {
34
+ override fun all(): Map<String, *> = prefs.all
35
+
36
+ override fun putInt(key: String, value: Int) = prefs.edit().putInt(key, value).apply()
37
+
38
+ override fun putString(key: String, value: String) = prefs.edit().putString(key, value).apply()
39
+
40
+ override fun remove(key: String) = prefs.edit().remove(key).apply()
41
+ },
42
+ )
43
+ }
44
+
45
+ private fun activity(): Activity? {
46
+ var context: Context? = lynxContext
47
+ while (context is ContextWrapper) {
48
+ if (context is Activity) return context
49
+ context = context.baseContext
50
+ }
51
+ return null
52
+ }
53
+
54
+ private fun onMain(block: (Activity) -> Unit) {
55
+ val activity = activity() ?: return
56
+ main.post { if (!activity.isFinishing) block(activity) }
57
+ }
58
+
59
+ @LynxMethod
60
+ override fun capabilities(callback: Callback?) =
61
+ G9Bridge.reply(
62
+ callback,
63
+ G9PreferencesCore.ok(mapOf("haptics" to true, "softInputMode" to true, "video" to true, "platform" to "android")),
64
+ )
65
+
66
+ @LynxMethod
67
+ override fun getPreference(key: String, callback: Callback?) =
68
+ G9Bridge.reply(callback, G9PreferencesCore.envelope { preferences.get(key) })
69
+
70
+ @LynxMethod
71
+ override fun setPreference(key: String, kind: String, value: String?, callback: Callback?) =
72
+ G9Bridge.reply(callback, G9PreferencesCore.envelope { preferences.set(key, kind, value) })
73
+
74
+ @LynxMethod
75
+ override fun preferenceEntries(prefix: String, callback: Callback?) =
76
+ G9Bridge.reply(callback, G9PreferencesCore.envelope { preferences.entries(prefix) })
77
+
78
+ @LynxMethod
79
+ override fun safeInsets(callback: Callback?) {
80
+ val activity = activity()
81
+ if (activity == null) {
82
+ G9Bridge.reply(callback, G9PreferencesCore.ok(mapOf("top" to 0, "end" to 0, "bottom" to 0, "start" to 0)))
83
+ return
84
+ }
85
+ main.post {
86
+ val density = activity.resources.displayMetrics.density
87
+ val insets = activity.window.decorView.rootWindowInsets
88
+ val rtl = activity.window.decorView.layoutDirection == View.LAYOUT_DIRECTION_RTL
89
+ @Suppress("DEPRECATION")
90
+ val left = (insets?.systemWindowInsetLeft ?: 0) / density
91
+ @Suppress("DEPRECATION")
92
+ val right = (insets?.systemWindowInsetRight ?: 0) / density
93
+ @Suppress("DEPRECATION")
94
+ val value =
95
+ mapOf(
96
+ "top" to (insets?.systemWindowInsetTop ?: 0) / density,
97
+ "bottom" to (insets?.systemWindowInsetBottom ?: 0) / density,
98
+ "start" to if (rtl) right else left,
99
+ "end" to if (rtl) left else right,
100
+ )
101
+ G9Bridge.reply(callback, G9PreferencesCore.ok(value))
102
+ }
103
+ }
104
+
105
+ @LynxMethod
106
+ override fun moveToBack() = onMain { it.moveTaskToBack(true) }
107
+
108
+ @LynxMethod
109
+ override fun hideKeyboard() =
110
+ onMain { activity ->
111
+ val focus = activity.currentFocus ?: activity.window.decorView
112
+ val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
113
+ imm.hideSoftInputFromWindow(focus.windowToken, 0)
114
+ focus.clearFocus()
115
+ }
116
+
117
+ @LynxMethod
118
+ override fun setSoftInputMode(mode: String) =
119
+ onMain { activity ->
120
+ @Suppress("DEPRECATION")
121
+ activity.window.setSoftInputMode(
122
+ if (mode == "pan") WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN else WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE,
123
+ )
124
+ }
125
+
126
+ /** `Context.filesDir`: the directory MAUI `FileSystem.AppDataDirectory` returns on Android, so a MAUI app's files stay put. */
127
+ @LynxMethod
128
+ override fun appDataDirectory(callback: Callback?) =
129
+ G9Bridge.reply(callback, G9PreferencesCore.ok(lynxContext.applicationContext.filesDir.absolutePath))
130
+
131
+ /** `asset:///<path>` for a file under the APK's `assets/` (Fresco and Media3 open that scheme); `not-found` otherwise. */
132
+ @LynxMethod
133
+ override fun assetUri(path: String, callback: Callback?) {
134
+ val clean = path.trimStart('/')
135
+ val exists =
136
+ clean.isNotEmpty() &&
137
+ try {
138
+ lynxContext.applicationContext.assets.open(clean).close()
139
+ true
140
+ } catch (_: java.io.IOException) {
141
+ false
142
+ }
143
+ G9Bridge.reply(
144
+ callback,
145
+ if (exists) {
146
+ G9PreferencesCore.ok("asset:///$clean")
147
+ } else {
148
+ mapOf("ok" to false, "error" to mapOf("code" to "not-found", "message" to "The app ships no asset \"$path\"."))
149
+ },
150
+ )
151
+ }
152
+
153
+ @LynxMethod
154
+ override fun hapticTick() =
155
+ onMain { activity ->
156
+ val constant = if (Build.VERSION.SDK_INT >= 27) HapticFeedbackConstants.KEYBOARD_RELEASE else HapticFeedbackConstants.KEYBOARD_TAP
157
+ activity.window.decorView.performHapticFeedback(constant)
158
+ }
159
+ }
@@ -0,0 +1,49 @@
1
+ package com.g9lynx.g9native
2
+
3
+ import com.g9lynx.g9native.generated.G9SqliteModuleSpec
4
+ import com.g9lynx.g9native.sqlite.androidx.AndroidxSqlEngine
5
+ import com.g9lynx.g9native.sqlite.core.G9SqliteCore
6
+ import com.g9lynx.g9native.sqlite.core.G9SqliteService
7
+ import com.lynx.jsbridge.LynxMethod
8
+ import com.lynx.jsbridge.LynxNativeModule
9
+ import com.lynx.react.bridge.Callback
10
+ import com.lynx.react.bridge.ReadableArray
11
+ import com.lynx.react.bridge.ReadableMap
12
+ import com.lynx.tasm.behavior.LynxContext
13
+
14
+ /**
15
+ * `NativeModules.G9SqliteModule` (plan/specs/06 §4): the Lynx face of [G9SqliteService] over androidx.sqlite
16
+ * `BundledSQLiteDriver`. One process-wide service, so every LynxView of the app shares the open databases.
17
+ */
18
+ @LynxNativeModule(name = "G9SqliteModule")
19
+ class G9SqliteModule(context: LynxContext) : G9SqliteModuleSpec(context) {
20
+ companion object {
21
+ val service: G9SqliteService by lazy { G9SqliteService(G9SqliteCore(AndroidxSqlEngine())) }
22
+ }
23
+
24
+ @LynxMethod
25
+ override fun capabilities(callback: Callback?) = service.capabilities { G9Bridge.reply(callback, it) }
26
+
27
+ @LynxMethod
28
+ override fun open(options: ReadableMap?, callback: Callback?) = service.open(G9Bridge.map(options)) { G9Bridge.reply(callback, it) }
29
+
30
+ @LynxMethod
31
+ override fun run(session: String, statements: ReadableArray?, options: ReadableMap?, callback: Callback?) =
32
+ service.run(session, G9Bridge.list(statements), G9Bridge.map(options)) { G9Bridge.reply(callback, it) }
33
+
34
+ @LynxMethod
35
+ override fun begin(session: String, mode: String, callback: Callback?) = service.begin(session, mode) { G9Bridge.reply(callback, it) }
36
+
37
+ @LynxMethod
38
+ override fun commit(tx: String, callback: Callback?) = service.commit(tx) { G9Bridge.reply(callback, it) }
39
+
40
+ @LynxMethod
41
+ override fun rollback(tx: String, callback: Callback?) = service.rollback(tx) { G9Bridge.reply(callback, it) }
42
+
43
+ @LynxMethod
44
+ override fun close(session: String, callback: Callback?) = service.close(session) { G9Bridge.reply(callback, it) }
45
+
46
+ /** androidx connections expose no `sqlite3_interrupt`: a running statement finishes (capabilities say so). */
47
+ @LynxMethod
48
+ override fun interrupt(session: String) = Unit
49
+ }
@@ -0,0 +1,48 @@
1
+ // Generated by @lynx-js/autolink-codegen. Do not edit.
2
+ // Parameter types: scripts/android-bridge-types.mjs --fix (Lynx's bridge rejects the generator's Object).
3
+ package com.g9lynx.g9native.generated;
4
+
5
+ import androidx.annotation.Nullable;
6
+ import com.lynx.jsbridge.LynxContextModule;
7
+ import com.lynx.jsbridge.LynxMethod;
8
+ import com.lynx.react.bridge.Callback;
9
+ import com.lynx.tasm.behavior.LynxContext;
10
+
11
+ public abstract class G9HostModuleSpec extends LynxContextModule {
12
+ public G9HostModuleSpec(LynxContext context) {
13
+ super(context);
14
+ }
15
+
16
+ @LynxMethod
17
+ public abstract void capabilities(Callback callback);
18
+
19
+ @LynxMethod
20
+ public abstract void getPreference(String key, Callback callback);
21
+
22
+ @LynxMethod
23
+ public abstract void setPreference(String key, String kind, @Nullable String value, Callback callback);
24
+
25
+ @LynxMethod
26
+ public abstract void preferenceEntries(String prefix, Callback callback);
27
+
28
+ @LynxMethod
29
+ public abstract void safeInsets(Callback callback);
30
+
31
+ @LynxMethod
32
+ public abstract void moveToBack();
33
+
34
+ @LynxMethod
35
+ public abstract void hideKeyboard();
36
+
37
+ @LynxMethod
38
+ public abstract void setSoftInputMode(String mode);
39
+
40
+ @LynxMethod
41
+ public abstract void hapticTick();
42
+
43
+ @LynxMethod
44
+ public abstract void appDataDirectory(Callback callback);
45
+
46
+ @LynxMethod
47
+ public abstract void assetUri(String path, Callback callback);
48
+ }
@@ -0,0 +1,41 @@
1
+ // Generated by @lynx-js/autolink-codegen. Do not edit.
2
+ // Parameter types: scripts/android-bridge-types.mjs --fix (Lynx's bridge rejects the generator's Object).
3
+ package com.g9lynx.g9native.generated;
4
+
5
+ import androidx.annotation.Nullable;
6
+ import com.lynx.jsbridge.LynxContextModule;
7
+ import com.lynx.jsbridge.LynxMethod;
8
+ import com.lynx.react.bridge.Callback;
9
+ import com.lynx.react.bridge.ReadableArray;
10
+ import com.lynx.react.bridge.ReadableMap;
11
+ import com.lynx.tasm.behavior.LynxContext;
12
+
13
+ public abstract class G9SqliteModuleSpec extends LynxContextModule {
14
+ public G9SqliteModuleSpec(LynxContext context) {
15
+ super(context);
16
+ }
17
+
18
+ @LynxMethod
19
+ public abstract void capabilities(Callback callback);
20
+
21
+ @LynxMethod
22
+ public abstract void open(ReadableMap options, Callback callback);
23
+
24
+ @LynxMethod
25
+ public abstract void run(String session, ReadableArray statements, ReadableMap options, Callback callback);
26
+
27
+ @LynxMethod
28
+ public abstract void begin(String session, String mode, Callback callback);
29
+
30
+ @LynxMethod
31
+ public abstract void commit(String tx, Callback callback);
32
+
33
+ @LynxMethod
34
+ public abstract void rollback(String tx, Callback callback);
35
+
36
+ @LynxMethod
37
+ public abstract void close(String session, Callback callback);
38
+
39
+ @LynxMethod
40
+ public abstract void interrupt(String session);
41
+ }
@@ -0,0 +1,100 @@
1
+ package com.g9lynx.g9native.host.core
2
+
3
+ /** Where preferences live: Android `SharedPreferences` in the host module, a map in tests. Values are `Int` or `String`. */
4
+ interface G9PreferenceBackend {
5
+ fun all(): Map<String, *>
6
+
7
+ fun putInt(key: String, value: Int)
8
+
9
+ fun putString(key: String, value: String)
10
+
11
+ fun remove(key: String)
12
+ }
13
+
14
+ /** A preference-store failure with the G9 error code the JS side raises (`preference-key`, `preference-value`). */
15
+ class G9PreferenceException(val code: String, message: String) : RuntimeException(message)
16
+
17
+ /**
18
+ * The `G9PreferenceStore` operations of `G9HostModule` (plan/specs/06 §7) as bridge data: typed values (a 32-bit
19
+ * integer is not readable as a string and vice versa, as in MAUI), `null` string removes, prefix enumeration for
20
+ * hydration. Keys arrive already prefixed by `createPreferences`.
21
+ */
22
+ class G9PreferencesCore(private val backend: G9PreferenceBackend) {
23
+ private fun checkKey(key: String) {
24
+ if (key.isEmpty()) throw G9PreferenceException("preference-key", "A preference key must be a non-empty string.")
25
+ }
26
+
27
+ /** `{ type: 'int' | 'string', value }`, or `null` when absent. */
28
+ fun get(key: String): Map<String, Any>? =
29
+ when (val value = backend.all()[key]) {
30
+ is Int -> mapOf("type" to "int", "value" to value.toDouble())
31
+ is String -> mapOf("type" to "string", "value" to value)
32
+ else -> null
33
+ }
34
+
35
+ /** `kind`: `int` (value = decimal text of a 32-bit integer), `string` (`null` removes) or `remove`. */
36
+ fun set(key: String, kind: String, value: String?) {
37
+ checkKey(key)
38
+ when (kind) {
39
+ "int" -> {
40
+ val number =
41
+ value?.toIntOrNull()
42
+ ?: throw G9PreferenceException("preference-value", "Preference '$key' needs a 32-bit integer, got $value.")
43
+ backend.putInt(key, number)
44
+ }
45
+ "string" -> if (value == null) backend.remove(key) else backend.putString(key, value)
46
+ "remove" -> backend.remove(key)
47
+ else -> throw G9PreferenceException("preference-value", "Unknown preference kind '$kind'.")
48
+ }
49
+ }
50
+
51
+ /** Every `[key, value]` whose key starts with `prefix`, sorted by key (stable hydration order). */
52
+ fun entries(prefix: String): List<List<Any>> =
53
+ backend
54
+ .all()
55
+ .entries
56
+ .filter { it.key.startsWith(prefix) && (it.value is Int || it.value is String) }
57
+ .sortedBy { it.key }
58
+ .map { (key, value) -> listOf(key, if (value is Int) value.toDouble() else value as Any) }
59
+
60
+ companion object {
61
+ fun ok(value: Any?): Map<String, Any?> = mapOf("ok" to true, "value" to value)
62
+
63
+ fun failure(error: Throwable): Map<String, Any?> =
64
+ mapOf(
65
+ "ok" to false,
66
+ "error" to
67
+ mapOf(
68
+ "code" to ((error as? G9PreferenceException)?.code ?: "preference-store"),
69
+ "message" to (error.message ?: error.toString()),
70
+ ),
71
+ )
72
+
73
+ /** Runs one request and wraps its outcome in an envelope. */
74
+ inline fun envelope(block: () -> Any?): Map<String, Any?> =
75
+ try {
76
+ ok(block())
77
+ } catch (error: Throwable) {
78
+ failure(error)
79
+ }
80
+ }
81
+ }
82
+
83
+ /** An in-memory backend (tests, previews). */
84
+ class G9MemoryPreferenceBackend : G9PreferenceBackend {
85
+ private val values = java.util.concurrent.ConcurrentHashMap<String, Any>()
86
+
87
+ override fun all(): Map<String, *> = HashMap(values)
88
+
89
+ override fun putInt(key: String, value: Int) {
90
+ values[key] = value
91
+ }
92
+
93
+ override fun putString(key: String, value: String) {
94
+ values[key] = value
95
+ }
96
+
97
+ override fun remove(key: String) {
98
+ values.remove(key)
99
+ }
100
+ }