@g9tm/lynxcontrols-native 0.0.0-stage → 1.4.2
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.
- package/README.md +85 -2
- package/android/build.gradle.kts +45 -0
- package/android/consumer-rules.pro +3 -0
- package/android/src/main/AndroidManifest.xml +5 -0
- package/android/src/main/java/com/g9lynx/g9native/G9Bridge.kt +67 -0
- package/android/src/main/java/com/g9lynx/g9native/G9HostModule.kt +172 -0
- package/android/src/main/java/com/g9lynx/g9native/G9SqliteModule.kt +49 -0
- package/android/src/main/java/com/g9lynx/g9native/generated/G9HostModuleSpec.java +48 -0
- package/android/src/main/java/com/g9lynx/g9native/generated/G9SqliteModuleSpec.java +41 -0
- package/android/src/main/java/com/g9lynx/g9native/host/core/G9PreferencesCore.kt +100 -0
- package/android/src/main/java/com/g9lynx/g9native/sqlite/androidx/AndroidxSqlEngine.kt +88 -0
- package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqlEngine.kt +61 -0
- package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqliteCore.kt +260 -0
- package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqliteService.kt +131 -0
- package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqliteTransport.kt +125 -0
- package/android/src/main/java/com/g9lynx/g9native/sqlite/core/G9SqliteValues.kt +71 -0
- package/android/src/main/java/com/g9lynx/g9native/video/G9VideoUI.kt +220 -0
- package/android/src/main/java/com/g9lynx/g9native/video/core/G9VideoCore.kt +67 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/driver.d.ts +20 -0
- package/dist/driver.d.ts.map +1 -0
- package/dist/driver.js +159 -0
- package/dist/driver.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/media.d.ts +60 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.jsx +119 -0
- package/dist/media.jsx.map +1 -0
- package/dist/protocol.d.ts +115 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +1 -0
- package/dist/services.d.ts +36 -0
- package/dist/services.d.ts.map +1 -0
- package/dist/services.js +111 -0
- package/dist/services.js.map +1 -0
- package/dist/transport.d.ts +10 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +77 -0
- package/dist/transport.js.map +1 -0
- package/generated/G9HostModule.ts +71 -0
- package/generated/G9SqliteModule.ts +68 -0
- package/ios/Tests/G9NativeTestSuite.h +25 -0
- package/ios/Tests/G9NativeTestSuite.m +560 -0
- package/ios/Tests/G9NativeTests.m +52 -0
- package/ios/g9lynx-native.podspec +29 -0
- package/ios/src/G9HostModule.h +14 -0
- package/ios/src/G9HostModule.m +121 -0
- package/ios/src/G9SqliteModule.h +14 -0
- package/ios/src/G9SqliteModule.m +167 -0
- package/ios/src/G9VideoUI.h +21 -0
- package/ios/src/G9VideoUI.m +250 -0
- package/ios/src/core/G9HostPaths.h +20 -0
- package/ios/src/core/G9HostPaths.m +23 -0
- package/ios/src/core/G9PreferencesCore.h +43 -0
- package/ios/src/core/G9PreferencesCore.m +114 -0
- package/ios/src/core/G9SqliteCore.h +62 -0
- package/ios/src/core/G9SqliteCore.m +497 -0
- package/ios/src/core/G9SqliteTransport.h +16 -0
- package/ios/src/core/G9SqliteTransport.m +85 -0
- package/ios/src/core/G9VideoCore.h +26 -0
- package/ios/src/core/G9VideoCore.m +37 -0
- package/ios/src/generated/G9HostModuleSpec.h +23 -0
- package/ios/src/generated/G9HostModuleSpec.m +2 -0
- package/ios/src/generated/G9SqliteModuleSpec.h +20 -0
- package/ios/src/generated/G9SqliteModuleSpec.m +2 -0
- package/lynx.lib.json +12 -0
- package/package.json +49 -4
- package/types/platform-native-module.d.ts +46 -0
package/README.md
CHANGED
|
@@ -1,3 +1,86 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @g9tm/lynxcontrols-native
|
|
2
2
|
|
|
3
|
-
|
|
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,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,172 @@
|
|
|
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
|
+
val px = safeInsetsPx(insets)
|
|
90
|
+
val left = px[0] / density
|
|
91
|
+
val right = px[2] / density
|
|
92
|
+
val value =
|
|
93
|
+
mapOf(
|
|
94
|
+
"top" to px[1] / density,
|
|
95
|
+
"bottom" to px[3] / density,
|
|
96
|
+
"start" to if (rtl) right else left,
|
|
97
|
+
"end" to if (rtl) left else right,
|
|
98
|
+
)
|
|
99
|
+
G9Bridge.reply(callback, G9PreferencesCore.ok(value))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@LynxMethod
|
|
104
|
+
override fun moveToBack() = onMain { it.moveTaskToBack(true) }
|
|
105
|
+
|
|
106
|
+
@LynxMethod
|
|
107
|
+
override fun hideKeyboard() =
|
|
108
|
+
onMain { activity ->
|
|
109
|
+
val focus = activity.currentFocus ?: activity.window.decorView
|
|
110
|
+
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
|
111
|
+
imm.hideSoftInputFromWindow(focus.windowToken, 0)
|
|
112
|
+
focus.clearFocus()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@LynxMethod
|
|
116
|
+
override fun setSoftInputMode(mode: String) =
|
|
117
|
+
onMain { activity ->
|
|
118
|
+
@Suppress("DEPRECATION")
|
|
119
|
+
activity.window.setSoftInputMode(
|
|
120
|
+
if (mode == "pan") WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN else WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE,
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** `Context.filesDir`: the directory MAUI `FileSystem.AppDataDirectory` returns on Android, so a MAUI app's files stay put. */
|
|
125
|
+
@LynxMethod
|
|
126
|
+
override fun appDataDirectory(callback: Callback?) =
|
|
127
|
+
G9Bridge.reply(callback, G9PreferencesCore.ok(lynxContext.applicationContext.filesDir.absolutePath))
|
|
128
|
+
|
|
129
|
+
/** `asset:///<path>` for a file under the APK's `assets/` (Fresco and Media3 open that scheme); `not-found` otherwise. */
|
|
130
|
+
@LynxMethod
|
|
131
|
+
override fun assetUri(path: String, callback: Callback?) {
|
|
132
|
+
val clean = path.trimStart('/')
|
|
133
|
+
val exists =
|
|
134
|
+
clean.isNotEmpty() &&
|
|
135
|
+
try {
|
|
136
|
+
lynxContext.applicationContext.assets.open(clean).close()
|
|
137
|
+
true
|
|
138
|
+
} catch (_: java.io.IOException) {
|
|
139
|
+
false
|
|
140
|
+
}
|
|
141
|
+
G9Bridge.reply(
|
|
142
|
+
callback,
|
|
143
|
+
if (exists) {
|
|
144
|
+
G9PreferencesCore.ok("asset:///$clean")
|
|
145
|
+
} else {
|
|
146
|
+
mapOf("ok" to false, "error" to mapOf("code" to "not-found", "message" to "The app ships no asset \"$path\"."))
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@LynxMethod
|
|
152
|
+
override fun hapticTick() =
|
|
153
|
+
onMain { activity ->
|
|
154
|
+
val constant = if (Build.VERSION.SDK_INT >= 27) HapticFeedbackConstants.KEYBOARD_RELEASE else HapticFeedbackConstants.KEYBOARD_TAP
|
|
155
|
+
activity.window.decorView.performHapticFeedback(constant)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Left, top, right, bottom safe insets in pixels: the visible system bars and the display cutout. API 30+ reads them
|
|
160
|
+
* by type, so the soft keyboard never counts (the deprecated `systemWindowInset*` added the IME height when the
|
|
161
|
+
* keyboard was up) and a host that hides the bars (immersive, as the AgriPad MAUI app) gets the cutout only.
|
|
162
|
+
*/
|
|
163
|
+
private fun safeInsetsPx(insets: android.view.WindowInsets?): IntArray {
|
|
164
|
+
if (insets == null) return intArrayOf(0, 0, 0, 0)
|
|
165
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
166
|
+
val i = insets.getInsets(android.view.WindowInsets.Type.systemBars() or android.view.WindowInsets.Type.displayCutout())
|
|
167
|
+
return intArrayOf(i.left, i.top, i.right, i.bottom)
|
|
168
|
+
}
|
|
169
|
+
@Suppress("DEPRECATION")
|
|
170
|
+
return intArrayOf(insets.systemWindowInsetLeft, insets.systemWindowInsetTop, insets.systemWindowInsetRight, insets.systemWindowInsetBottom)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -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
|
+
}
|