create-mithril-lynx 0.0.2 → 0.1.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.
- package/README.md +63 -0
- package/package.json +2 -2
- package/src/index.js +509 -4
- package/templates/android/app-scripts/android.mjs +232 -0
- package/templates/android/font/AssetFontFaceLoader.kt +39 -0
- package/templates/android/host/app/build.gradle.kts +103 -0
- package/templates/android/host/app/proguard-rules.pro +51 -0
- package/templates/android/host/app/src/main/AndroidManifest.xml +28 -0
- package/templates/android/host/app/src/main/java/package-path/App.kt +23 -0
- package/templates/android/host/app/src/main/java/package-path/AssetTemplateProvider.kt +27 -0
- package/templates/android/host/app/src/main/java/package-path/MainActivity.kt +37 -0
- package/templates/android/host/app/src/main/res/drawable/ic_launcher_background.xml +6 -0
- package/templates/android/host/app/src/main/res/drawable/ic_launcher_foreground.xml +11 -0
- package/templates/android/host/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/templates/android/host/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
- package/templates/android/host/app/src/main/res/values/strings.xml +3 -0
- package/templates/android/host/app/src/main/res/values/themes.xml +17 -0
- package/templates/android/host/build.gradle.kts +4 -0
- package/templates/android/host/gitignore +16 -0
- package/templates/android/host/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/templates/android/host/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/templates/android/host/gradle.properties +3 -0
- package/templates/android/host/gradlew +251 -0
- package/templates/android/host/gradlew.bat +94 -0
- package/templates/android/host/keystore.properties.example +15 -0
- package/templates/android/host/local.properties +4 -0
- package/templates/android/host/settings.gradle.kts +18 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Bridge between this JS project (the Lynx bundle) and its sibling Android host
|
|
4
|
+
* (`{{ANDROID_DIR}}/`).
|
|
5
|
+
*
|
|
6
|
+
* Automates Part C of ANDROID_APK_GUIDE.md: build the bundle, copy it into
|
|
7
|
+
* `app/src/main/assets/`, then build/install and launch the APK.
|
|
8
|
+
*
|
|
9
|
+
* npm run android build + sync + installDebug + launch on the device
|
|
10
|
+
* npm run android:apk build + sync + assembleDebug (build the APK only)
|
|
11
|
+
* npm run android:release build + sync + assembleRelease (signs if keystore.properties exists)
|
|
12
|
+
* npm run android:sync build + sync into assets only (no Gradle)
|
|
13
|
+
* npm run android:keystore generate release.keystore + keystore.properties (once)
|
|
14
|
+
*
|
|
15
|
+
* Extra flags, passed after `--`:
|
|
16
|
+
* npm run android -- --no-build reuse dist/ as-is (don't rebuild)
|
|
17
|
+
* npm run android -- --no-launch don't launch the app over adb at the end
|
|
18
|
+
* npm run android:apk -- -- --info everything after `--` is forwarded to ./gradlew
|
|
19
|
+
*/
|
|
20
|
+
import { spawnSync } from "node:child_process";
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
|
|
25
|
+
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
26
|
+
|
|
27
|
+
/** Sibling Android host, resolved when the project was generated. */
|
|
28
|
+
const ANDROID_DIR = path.resolve(projectRoot, "{{ANDROID_REL_DIR}}");
|
|
29
|
+
|
|
30
|
+
const APPLICATION_ID = "{{PACKAGE_NAME}}";
|
|
31
|
+
const ACTIVITY_CLASS = ".MainActivity";
|
|
32
|
+
const BUNDLE_NAME = "main-thread.bundle";
|
|
33
|
+
|
|
34
|
+
const BUNDLE_SRC = path.join(projectRoot, "dist", BUNDLE_NAME);
|
|
35
|
+
const BUNDLE_DEST = path.join(ANDROID_DIR, "app", "src", "main", "assets", BUNDLE_NAME);
|
|
36
|
+
|
|
37
|
+
const USAGE = `
|
|
38
|
+
Usage: npm run android -- [flags] [-- gradle-args]
|
|
39
|
+
|
|
40
|
+
--apk build the debug APK (assembleDebug) instead of installing it
|
|
41
|
+
--release build the release APK (assembleRelease, signed if keystore.properties exists)
|
|
42
|
+
--sync-only only build the bundle and copy it into assets/ (no Gradle)
|
|
43
|
+
--no-build don't rebuild the bundle; use dist/ as it is
|
|
44
|
+
--no-launch don't launch the app over adb at the end
|
|
45
|
+
--keystore generate release.keystore + keystore.properties and exit
|
|
46
|
+
--help print this
|
|
47
|
+
`.trim();
|
|
48
|
+
|
|
49
|
+
const args = process.argv.slice(2);
|
|
50
|
+
const flags = new Set(args.filter((a) => a.startsWith("-")));
|
|
51
|
+
|
|
52
|
+
function afterDoubleDash() {
|
|
53
|
+
const i = args.indexOf("--");
|
|
54
|
+
return i === -1 ? [] : args.slice(i + 1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fail(message) {
|
|
58
|
+
console.error(`\n ✖ ${message}\n`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function run(command, commandArgs, options = {}) {
|
|
63
|
+
const result = spawnSync(command, commandArgs, {
|
|
64
|
+
cwd: options.cwd ?? projectRoot,
|
|
65
|
+
stdio: options.capture ? "pipe" : "inherit",
|
|
66
|
+
shell: process.platform === "win32",
|
|
67
|
+
encoding: "utf8",
|
|
68
|
+
});
|
|
69
|
+
if (result.error) {
|
|
70
|
+
fail(`Could not run "${command}": ${result.error.message}`);
|
|
71
|
+
}
|
|
72
|
+
if (result.status !== 0) {
|
|
73
|
+
fail(`"${command} ${commandArgs.join(" ")}" exited with code ${result.status}.`);
|
|
74
|
+
}
|
|
75
|
+
return result.stdout ?? "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function detectPackageManager() {
|
|
79
|
+
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
80
|
+
if (userAgent.startsWith("bun")) return "bun";
|
|
81
|
+
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
82
|
+
if (userAgent.startsWith("yarn")) return "yarn";
|
|
83
|
+
return "npm";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function gradlew() {
|
|
87
|
+
return process.platform === "win32" ? "gradlew.bat" : "./gradlew";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function requireAndroidDir() {
|
|
91
|
+
if (!fs.existsSync(path.join(ANDROID_DIR, "settings.gradle.kts"))) {
|
|
92
|
+
fail(
|
|
93
|
+
`Can't find the Android host at ${ANDROID_DIR}.\n` +
|
|
94
|
+
` Generate it with: npm create mithril-lynx@latest <name> --android\n` +
|
|
95
|
+
` (or pass --android-id/--app-name so it doesn't prompt).`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function syncBundle() {
|
|
101
|
+
if (!fs.existsSync(BUNDLE_SRC)) {
|
|
102
|
+
fail(`${path.relative(projectRoot, BUNDLE_SRC)} doesn't exist. Run "npm run build" first (or drop --no-build).`);
|
|
103
|
+
}
|
|
104
|
+
requireAndroidDir();
|
|
105
|
+
fs.mkdirSync(path.dirname(BUNDLE_DEST), { recursive: true });
|
|
106
|
+
fs.copyFileSync(BUNDLE_SRC, BUNDLE_DEST);
|
|
107
|
+
const kb = (fs.statSync(BUNDLE_DEST).size / 1024).toFixed(1);
|
|
108
|
+
console.log(` → ${path.relative(projectRoot, BUNDLE_DEST)} (${kb} kB)`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function generateKeystore() {
|
|
112
|
+
requireAndroidDir();
|
|
113
|
+
const alias = "release";
|
|
114
|
+
const storeFile = path.join(ANDROID_DIR, "release.keystore");
|
|
115
|
+
const propertiesFile = path.join(ANDROID_DIR, "keystore.properties");
|
|
116
|
+
|
|
117
|
+
if (fs.existsSync(propertiesFile)) {
|
|
118
|
+
fail(`${path.relative(projectRoot, propertiesFile)} already exists. Delete it by hand to regenerate.`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const password = process.env.KEYSTORE_PASSWORD;
|
|
122
|
+
if (!password) {
|
|
123
|
+
fail(
|
|
124
|
+
"The KEYSTORE_PASSWORD environment variable is missing.\n" +
|
|
125
|
+
` KEYSTORE_PASSWORD='...' npm run android:keystore`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const keytool = spawnSync(
|
|
130
|
+
"keytool",
|
|
131
|
+
[
|
|
132
|
+
"-genkeypair", "-v",
|
|
133
|
+
"-keystore", storeFile,
|
|
134
|
+
"-alias", alias,
|
|
135
|
+
"-keyalg", "RSA",
|
|
136
|
+
"-keysize", "2048",
|
|
137
|
+
"-validity", "10000",
|
|
138
|
+
"-storepass", password,
|
|
139
|
+
"-keypass", password,
|
|
140
|
+
"-dname", "CN={{APP_NAME}}",
|
|
141
|
+
],
|
|
142
|
+
{ stdio: "inherit" },
|
|
143
|
+
);
|
|
144
|
+
if (keytool.error?.code === "ENOENT") {
|
|
145
|
+
fail("Can't find `keytool` on PATH. It ships with JDK 17 — check JAVA_HOME.");
|
|
146
|
+
}
|
|
147
|
+
if (keytool.status !== 0) fail(`keytool exited with code ${keytool.status}.`);
|
|
148
|
+
|
|
149
|
+
fs.writeFileSync(
|
|
150
|
+
propertiesFile,
|
|
151
|
+
[
|
|
152
|
+
"# Generated by scripts/android.mjs — do NOT commit (see the Android host's .gitignore).",
|
|
153
|
+
"storeFile=release.keystore",
|
|
154
|
+
`storePassword=${password}`,
|
|
155
|
+
`keyAlias=${alias}`,
|
|
156
|
+
`keyPassword=${password}`,
|
|
157
|
+
"",
|
|
158
|
+
].join("\n"),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
console.log(`\n ✔ ${path.relative(projectRoot, storeFile)}`);
|
|
162
|
+
console.log(` ✔ ${path.relative(projectRoot, propertiesFile)}`);
|
|
163
|
+
console.log("\n Next: npm run android:release\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function buildBundle() {
|
|
167
|
+
const manager = detectPackageManager();
|
|
168
|
+
console.log(`\n▸ Building the bundle (${manager} run build)…`);
|
|
169
|
+
run(manager, ["run", "build"]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function runGradle(task, extraArgs) {
|
|
173
|
+
console.log(`\n▸ ./gradlew ${task}…`);
|
|
174
|
+
run(gradlew(), [task, ...extraArgs], { cwd: ANDROID_DIR });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function launch() {
|
|
178
|
+
console.log("\n▸ Launching on the device…\n");
|
|
179
|
+
run("adb", ["shell", "am", "force-stop", APPLICATION_ID]);
|
|
180
|
+
const output = run("adb", ["shell", "am", "start", "-W", "-n", `${APPLICATION_ID}/${ACTIVITY_CLASS}`], {
|
|
181
|
+
capture: true,
|
|
182
|
+
});
|
|
183
|
+
for (const line of output.split("\n")) {
|
|
184
|
+
if (/TotalTime|WaitTime|LaunchState|Error|Exception/.test(line)) console.log(` ${line.trim()}`);
|
|
185
|
+
}
|
|
186
|
+
console.log("\n Logs: adb logcat | grep -i lynx");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function main() {
|
|
190
|
+
if (flags.has("--help") || flags.has("-h")) {
|
|
191
|
+
console.log(USAGE);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (flags.has("--keystore")) {
|
|
195
|
+
generateKeystore();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const syncOnly = flags.has("--sync-only");
|
|
200
|
+
|
|
201
|
+
if (!flags.has("--no-build")) buildBundle();
|
|
202
|
+
syncBundle();
|
|
203
|
+
|
|
204
|
+
if (syncOnly) {
|
|
205
|
+
console.log("\n ✔ Assets synced.\n");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const extraGradleArgs = afterDoubleDash();
|
|
210
|
+
if (flags.has("--release")) {
|
|
211
|
+
runGradle("assembleRelease", extraGradleArgs);
|
|
212
|
+
const apkDir = path.join(ANDROID_DIR, "app", "build", "outputs", "apk", "release");
|
|
213
|
+
for (const apk of fs.existsSync(apkDir) ? fs.readdirSync(apkDir).filter((f) => f.endsWith(".apk")) : []) {
|
|
214
|
+
console.log(`\n ✔ APK: ${path.relative(projectRoot, path.join(apkDir, apk))}`);
|
|
215
|
+
}
|
|
216
|
+
} else if (flags.has("--apk") || process.env.CI) {
|
|
217
|
+
runGradle("assembleDebug", extraGradleArgs);
|
|
218
|
+
console.log(
|
|
219
|
+
`\n ✔ APK: ${path.relative(projectRoot, path.join(ANDROID_DIR, "app", "build", "outputs", "apk", "debug", "app-debug.apk"))}`,
|
|
220
|
+
);
|
|
221
|
+
} else {
|
|
222
|
+
runGradle("installDebug", extraGradleArgs);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!flags.has("--no-launch") && !flags.has("--apk") && !flags.has("--release")) {
|
|
226
|
+
launch();
|
|
227
|
+
} else {
|
|
228
|
+
console.log("");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
main();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.graphics.Typeface
|
|
4
|
+
import com.lynx.tasm.behavior.LynxContext
|
|
5
|
+
import com.lynx.tasm.fontface.FontFace
|
|
6
|
+
import com.lynx.tasm.loader.LynxFontFaceLoader
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolves `asset:///` in `@font-face`, both for the prefetch and for the real
|
|
10
|
+
* lookup.
|
|
11
|
+
*
|
|
12
|
+
* Lynx's default loader (LynxFontFaceLoader$1, decompiled from
|
|
13
|
+
* lynx-4.1.0.aar) is a no-op that never resolves `asset:///`: in
|
|
14
|
+
* FontFaceManager that scheme is only handled inline inside loadTypeface()
|
|
15
|
+
* when a FONT-type LynxResourceProvider is registered, and the non-http/
|
|
16
|
+
* non-data: branch of prefetchFont() (prefetchFontWithLoader) goes through this
|
|
17
|
+
* Loader only, with no fallback of its own. Without registering this,
|
|
18
|
+
* `asset:///` resolves no way at all.
|
|
19
|
+
*
|
|
20
|
+
* Trade-off to keep in mind: FontFaceManager/LynxFontFaceLoader are public
|
|
21
|
+
* classes (not @RestrictTo) but are not documented for this specific use —
|
|
22
|
+
* they could change without notice in a major Lynx SDK release.
|
|
23
|
+
*/
|
|
24
|
+
object AssetFontFaceLoader : LynxFontFaceLoader.Loader() {
|
|
25
|
+
private const val ASSET_PREFIX = "asset:///"
|
|
26
|
+
|
|
27
|
+
override fun onLoadFontFace(
|
|
28
|
+
context: LynxContext,
|
|
29
|
+
type: FontFace.TYPE,
|
|
30
|
+
src: String,
|
|
31
|
+
): Typeface? {
|
|
32
|
+
if (!src.startsWith(ASSET_PREFIX)) return null
|
|
33
|
+
return try {
|
|
34
|
+
Typeface.createFromAsset(context.context.assets, src.removePrefix(ASSET_PREFIX))
|
|
35
|
+
} catch (e: Exception) {
|
|
36
|
+
null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import java.io.FileInputStream
|
|
2
|
+
import java.util.Properties
|
|
3
|
+
|
|
4
|
+
plugins {
|
|
5
|
+
id("com.android.application")
|
|
6
|
+
id("org.jetbrains.kotlin.android")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Optional release signing, driven by keystore.properties (gitignored).
|
|
10
|
+
// Without that file, `./gradlew assembleRelease` simply produces an unsigned
|
|
11
|
+
// APK instead of failing. To generate it:
|
|
12
|
+
// npm run android:keystore
|
|
13
|
+
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
|
14
|
+
val keystoreProperties = Properties().apply {
|
|
15
|
+
if (keystorePropertiesFile.exists()) {
|
|
16
|
+
FileInputStream(keystorePropertiesFile).use { load(it) }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
android {
|
|
21
|
+
namespace = "{{PACKAGE_NAME}}"
|
|
22
|
+
compileSdk = 34
|
|
23
|
+
|
|
24
|
+
defaultConfig {
|
|
25
|
+
applicationId = "{{PACKAGE_NAME}}"
|
|
26
|
+
minSdk = 24
|
|
27
|
+
targetSdk = 34
|
|
28
|
+
versionCode = 1
|
|
29
|
+
versionName = "1.0"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
signingConfigs {
|
|
33
|
+
if (keystorePropertiesFile.exists()) {
|
|
34
|
+
create("release") {
|
|
35
|
+
// storeFile resolves against the Android project root, so
|
|
36
|
+
// "release.keystore" means <android-dir>/release.keystore.
|
|
37
|
+
storeFile = rootProject.file(keystoreProperties.getProperty("storeFile"))
|
|
38
|
+
storePassword = keystoreProperties.getProperty("storePassword")
|
|
39
|
+
keyAlias = keystoreProperties.getProperty("keyAlias")
|
|
40
|
+
keyPassword = keystoreProperties.getProperty("keyPassword")
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
buildTypes {
|
|
46
|
+
debug {
|
|
47
|
+
isMinifyEnabled = false
|
|
48
|
+
}
|
|
49
|
+
release {
|
|
50
|
+
isMinifyEnabled = true
|
|
51
|
+
proguardFiles(
|
|
52
|
+
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
53
|
+
"proguard-rules.pro",
|
|
54
|
+
)
|
|
55
|
+
if (keystorePropertiesFile.exists()) {
|
|
56
|
+
signingConfig = signingConfigs.getByName("release")
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
compileOptions {
|
|
62
|
+
sourceCompatibility = JavaVersion.VERSION_17
|
|
63
|
+
targetCompatibility = JavaVersion.VERSION_17
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
kotlinOptions {
|
|
67
|
+
jvmTarget = "17"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
dependencies {
|
|
72
|
+
// The core artifact — LynxView, LynxViewBuilder, the layout engine, etc.
|
|
73
|
+
implementation("org.lynxsdk.lynx:lynx:4.1.0")
|
|
74
|
+
|
|
75
|
+
// OPT-IN: add these only if you use those elements, but they cost little
|
|
76
|
+
// and without them the failure is silent (the element mounts and never
|
|
77
|
+
// responds), so the scaffold ships them.
|
|
78
|
+
|
|
79
|
+
// <input>/<textarea> — without this those elements take up zero size and
|
|
80
|
+
// never open the keyboard. XElementBehaviors is registered in MainActivity.kt.
|
|
81
|
+
implementation("org.lynxsdk.lynx:xelement:4.1.0")
|
|
82
|
+
implementation("org.lynxsdk.lynx:xelement-input:4.1.0")
|
|
83
|
+
|
|
84
|
+
// <overlay> — without this the element mounts without error but is never
|
|
85
|
+
// visible (mithril-lynx-ui's Dialog/Sheet/Popover need it).
|
|
86
|
+
implementation("org.lynxsdk.lynx:xelement-overlay:4.1.0")
|
|
87
|
+
|
|
88
|
+
// Without a registered ILynxLogService, console.log() and JS errors from
|
|
89
|
+
// the bundle are dropped silently — not even logcat shows them. Remove it
|
|
90
|
+
// (along with its registration in the Application class) in a production
|
|
91
|
+
// build.
|
|
92
|
+
implementation("org.lynxsdk.lynx:lynx-service-log:4.1.0")
|
|
93
|
+
|
|
94
|
+
// <refresh> (pull-to-refresh, inside the xelement artifact) internally
|
|
95
|
+
// depends on SmartRefreshLayout, whose touch-dispatch code references
|
|
96
|
+
// ViewPager2 even though you don't use it — without this, every touch on a
|
|
97
|
+
// <refresh> throws NoClassDefFoundError (caught by the engine, but the
|
|
98
|
+
// gesture then never works).
|
|
99
|
+
implementation("androidx.viewpager2:viewpager2:1.1.0")
|
|
100
|
+
|
|
101
|
+
implementation("androidx.appcompat:appcompat:1.7.0")
|
|
102
|
+
implementation("androidx.core:core-splashscreen:1.0.1")
|
|
103
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# ProGuard/R8 rules for the release build.
|
|
2
|
+
|
|
3
|
+
# ---------------------------------------------------------------------------
|
|
4
|
+
# 1. OPTIONAL Lynx SDK dependencies that are not on the classpath.
|
|
5
|
+
#
|
|
6
|
+
# The `xelement` artifact (and part of the core) references library classes the
|
|
7
|
+
# SDK does NOT declare as transitive dependencies:
|
|
8
|
+
# - Fresco (com.facebook.**) -> SvgDefaultResourceManager's remote SVG path
|
|
9
|
+
# - Gson (com.google.gson.**) -> LynxEnv.GetNativeEnvDebugDescription()
|
|
10
|
+
# - Markdown (com.lynx.markdown.**) -> LynxUIMarkdownShadowNode
|
|
11
|
+
#
|
|
12
|
+
# Without these -dontwarn rules, R8 aborts with "Missing classes detected while
|
|
13
|
+
# running R8" and `./gradlew assembleRelease` fails outright. Verified with
|
|
14
|
+
# lynx:4.1.0 + AGP 8.5.2. They are optional paths a normal mithril-lynx app
|
|
15
|
+
# never uses, so ignoring them is correct (adding the real libraries would only
|
|
16
|
+
# bloat the APK for nothing).
|
|
17
|
+
-dontwarn com.facebook.**
|
|
18
|
+
-dontwarn com.google.gson.**
|
|
19
|
+
-dontwarn com.lynx.markdown.**
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# 1b. Methods Lynx's NATIVE code calls by exact name + signature.
|
|
23
|
+
#
|
|
24
|
+
# The `lynx-base` AAR calls
|
|
25
|
+
# `com.lynx.base.log.LynxLog.log(int, String, String, int, long, int, int)`
|
|
26
|
+
# from C++ via JNI GetStaticMethodID. That class is NOT annotated with
|
|
27
|
+
# @CalledByNative (unlike most of the bridge), so the AAR's consumer rules
|
|
28
|
+
# don't cover it: R8 renames it and startup dies with
|
|
29
|
+
#
|
|
30
|
+
# java.lang.NoSuchMethodError: no static method
|
|
31
|
+
# "Lcom/lynx/base/log/LynxLog;.log(ILjava/lang/String;Ljava/lang/String;IJII)V"
|
|
32
|
+
# JNI DETECTED ERROR IN APPLICATION: mid == null
|
|
33
|
+
# Fatal signal 6 (SIGABRT) <- on the LynxTraceInit thread, before drawing anything
|
|
34
|
+
#
|
|
35
|
+
# Verified on a device (minified release, lynx:4.1.0 + AGP 8.5.2): without this
|
|
36
|
+
# rule the release APK crashes on startup; with it, it starts like the debug one.
|
|
37
|
+
-keep class com.lynx.base.log.LynxLog { *; }
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# 2. Your own classes reached by reflection.
|
|
41
|
+
#
|
|
42
|
+
# The Lynx SDK ships its own "consumer" rules inside the AAR. What it cannot
|
|
43
|
+
# know about is your classes, so anything Lynx reaches by name has to survive
|
|
44
|
+
# minification:
|
|
45
|
+
#
|
|
46
|
+
# - modules registered with builder.registerModule("Name", Class::class.java)
|
|
47
|
+
# - your own @LynxMethod / behaviors
|
|
48
|
+
#
|
|
49
|
+
# Example:
|
|
50
|
+
#
|
|
51
|
+
# -keep class com.example.myapp.MyNativeModule { *; }
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
+
|
|
4
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
5
|
+
|
|
6
|
+
<application
|
|
7
|
+
android:name=".{{APP_CLASS}}"
|
|
8
|
+
android:allowBackup="true"
|
|
9
|
+
android:label="@string/app_name"
|
|
10
|
+
android:icon="@mipmap/ic_launcher"
|
|
11
|
+
android:roundIcon="@mipmap/ic_launcher_round"
|
|
12
|
+
android:hardwareAccelerated="true"
|
|
13
|
+
android:usesCleartextTraffic="true"
|
|
14
|
+
android:theme="@style/AppTheme">
|
|
15
|
+
|
|
16
|
+
<activity
|
|
17
|
+
android:name=".MainActivity"
|
|
18
|
+
android:theme="@style/Theme.App.Starting"
|
|
19
|
+
android:exported="true">
|
|
20
|
+
<intent-filter>
|
|
21
|
+
<action android:name="android.intent.action.MAIN" />
|
|
22
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
23
|
+
</intent-filter>
|
|
24
|
+
</activity>
|
|
25
|
+
|
|
26
|
+
</application>
|
|
27
|
+
|
|
28
|
+
</manifest>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.app.Application
|
|
4
|
+
import com.lynx.service.log.LynxLogService
|
|
5
|
+
import com.lynx.tasm.LynxEnv
|
|
6
|
+
// {{FONT_LOADER_IMPORT}}
|
|
7
|
+
import com.lynx.tasm.service.LynxServiceCenter
|
|
8
|
+
|
|
9
|
+
class {{APP_CLASS}} : Application() {
|
|
10
|
+
override fun onCreate() {
|
|
11
|
+
super.onCreate()
|
|
12
|
+
|
|
13
|
+
// Without a registered ILynxLogService, console.log() and JS errors
|
|
14
|
+
// from the bundle are dropped silently — not even logcat shows them.
|
|
15
|
+
// Drop this (and the lynx-service-log dependency) in production if you
|
|
16
|
+
// don't want it.
|
|
17
|
+
LynxServiceCenter.inst().registerService(LynxLogService)
|
|
18
|
+
LynxLogService.switchLogToSystem(true)
|
|
19
|
+
|
|
20
|
+
// {{FONT_LOADER_REGISTRATION}}
|
|
21
|
+
LynxEnv.inst().init(this, null, null, null)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.lynx.tasm.provider.AbsTemplateProvider
|
|
5
|
+
import java.io.IOException
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Reads the Lynx bundle from assets on a separate thread.
|
|
9
|
+
*
|
|
10
|
+
* Measured on real hardware: reading the same asset SYNCHRONOUSLY in
|
|
11
|
+
* MainActivity.onCreate() (blocking Android's UI thread — distinct from Lynx's
|
|
12
|
+
* internal main/background thread split, but just as harmful to the first
|
|
13
|
+
* frame) cost 2.4-2.8s of cold start under `adb shell am start -W`; with this
|
|
14
|
+
* async provider, ~300ms. This is not optional.
|
|
15
|
+
*/
|
|
16
|
+
class AssetTemplateProvider(private val context: Context) : AbsTemplateProvider() {
|
|
17
|
+
override fun loadTemplate(url: String, callback: Callback) {
|
|
18
|
+
Thread {
|
|
19
|
+
try {
|
|
20
|
+
val bytes = context.assets.open(url).use { it.readBytes() }
|
|
21
|
+
callback.onSuccess(bytes)
|
|
22
|
+
} catch (e: IOException) {
|
|
23
|
+
callback.onFailed(e.toString())
|
|
24
|
+
}
|
|
25
|
+
}.start()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.os.Bundle
|
|
4
|
+
import androidx.appcompat.app.AppCompatActivity
|
|
5
|
+
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|
6
|
+
import com.lynx.tasm.LynxViewBuilder
|
|
7
|
+
import com.lynx.tasm.ThreadStrategyForRendering
|
|
8
|
+
// {{FONT_IMPORT}}
|
|
9
|
+
import com.lynx.xelement.XElementBehaviors
|
|
10
|
+
|
|
11
|
+
class MainActivity : AppCompatActivity() {
|
|
12
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
13
|
+
// Must run BEFORE super.onCreate() — a Splash Screen API requirement.
|
|
14
|
+
installSplashScreen()
|
|
15
|
+
super.onCreate(savedInstanceState)
|
|
16
|
+
|
|
17
|
+
val builder = LynxViewBuilder()
|
|
18
|
+
// <input>/<textarea> are opt-in "xelement" components, not part of the
|
|
19
|
+
// core artifact: without this they measure 0 and never open the
|
|
20
|
+
// keyboard. Remove this line (and the xelement dependencies) if you
|
|
21
|
+
// don't use them.
|
|
22
|
+
builder.addBehaviors(XElementBehaviors().create())
|
|
23
|
+
builder.setThreadStrategyForRendering(ThreadStrategyForRendering.ALL_ON_UI)
|
|
24
|
+
// Reads the bundle from assets on a separate thread — see
|
|
25
|
+
// AssetTemplateProvider's comment: reading it synchronously here cost
|
|
26
|
+
// 2.4-2.8s of cold start measured on a real device, vs ~300ms with this.
|
|
27
|
+
builder.setTemplateProvider(AssetTemplateProvider(this))
|
|
28
|
+
val lynxView = builder.build(this)
|
|
29
|
+
setContentView(lynxView)
|
|
30
|
+
|
|
31
|
+
// {{FONT_PREFETCH}}
|
|
32
|
+
// The bundle lives in app/src/main/assets/main-thread.bundle, copied
|
|
33
|
+
// there by `npm run android` (scripts/android.mjs) from the JS
|
|
34
|
+
// project's dist/. The name has to match exactly.
|
|
35
|
+
lynxView.renderTemplateUrl("main-thread.bundle", "")
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
android:width="108dp" android:height="108dp"
|
|
4
|
+
android:viewportWidth="108" android:viewportHeight="108">
|
|
5
|
+
<path android:pathData="M0,0h108v108h-108z" android:fillColor="#0F1115" />
|
|
6
|
+
</vector>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
android:width="108dp" android:height="108dp"
|
|
4
|
+
android:viewportWidth="108" android:viewportHeight="108">
|
|
5
|
+
<path
|
|
6
|
+
android:pathData="M30,54 L50,74 L78,34"
|
|
7
|
+
android:strokeColor="#6EE7B7"
|
|
8
|
+
android:strokeWidth="6"
|
|
9
|
+
android:strokeLineCap="round"
|
|
10
|
+
android:strokeLineJoin="round" />
|
|
11
|
+
</vector>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
+
<background android:drawable="@drawable/ic_launcher_background" />
|
|
4
|
+
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
5
|
+
</adaptive-icon>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
+
<background android:drawable="@drawable/ic_launcher_background" />
|
|
4
|
+
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
5
|
+
</adaptive-icon>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<resources>
|
|
2
|
+
|
|
3
|
+
<!-- The app's normal theme, handed off to once the splash screen ends.
|
|
4
|
+
No ActionBar: Lynx draws the whole app. -->
|
|
5
|
+
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar" />
|
|
6
|
+
|
|
7
|
+
<!-- Shown by the system from process start until installSplashScreen()
|
|
8
|
+
gets its default signal — which is this Activity's first drawn frame,
|
|
9
|
+
i.e. exactly the real cold-start window (~700-800ms), not an arbitrary
|
|
10
|
+
fixed delay. -->
|
|
11
|
+
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
|
|
12
|
+
<item name="windowSplashScreenBackground">#0F1115</item>
|
|
13
|
+
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
|
|
14
|
+
<item name="postSplashScreenTheme">@style/AppTheme</item>
|
|
15
|
+
</style>
|
|
16
|
+
|
|
17
|
+
</resources>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Gradle / Android
|
|
2
|
+
.gradle/
|
|
3
|
+
build/
|
|
4
|
+
app/build/
|
|
5
|
+
.cxx/
|
|
6
|
+
local.properties
|
|
7
|
+
*.iml
|
|
8
|
+
.idea/
|
|
9
|
+
|
|
10
|
+
# Release signing — never committed (see keystore.properties.example).
|
|
11
|
+
keystore.properties
|
|
12
|
+
*.keystore
|
|
13
|
+
*.jks
|
|
14
|
+
|
|
15
|
+
# Regenerated by `npm run android` from the sibling JS project.
|
|
16
|
+
app/src/main/assets/main-thread.bundle
|
|
Binary file
|