@blotch/angular-lynx-native 0.0.0-stage → 22.2.3

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 (80) hide show
  1. package/README.md +44 -2
  2. package/dist/android/build.d.ts +28 -0
  3. package/dist/android/build.d.ts.map +1 -0
  4. package/dist/android/build.js +100 -0
  5. package/dist/android/project.d.ts +20 -0
  6. package/dist/android/project.d.ts.map +1 -0
  7. package/dist/android/project.js +84 -0
  8. package/dist/eject.d.ts +18 -0
  9. package/dist/eject.d.ts.map +1 -0
  10. package/dist/eject.js +68 -0
  11. package/dist/generate.d.ts +60 -0
  12. package/dist/generate.d.ts.map +1 -0
  13. package/dist/generate.js +111 -0
  14. package/dist/index.d.ts +41 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +72 -0
  17. package/dist/ios/build.d.ts +32 -0
  18. package/dist/ios/build.d.ts.map +1 -0
  19. package/dist/ios/build.js +149 -0
  20. package/dist/ios/frameworks.d.ts +38 -0
  21. package/dist/ios/frameworks.d.ts.map +1 -0
  22. package/dist/ios/frameworks.js +109 -0
  23. package/dist/ios/project.d.ts +11 -0
  24. package/dist/ios/project.d.ts.map +1 -0
  25. package/dist/ios/project.js +73 -0
  26. package/dist/options.d.ts +95 -0
  27. package/dist/options.d.ts.map +1 -0
  28. package/dist/options.js +112 -0
  29. package/dist/permissions.d.ts +17 -0
  30. package/dist/permissions.d.ts.map +1 -0
  31. package/dist/permissions.js +38 -0
  32. package/dist/png.d.ts +10 -0
  33. package/dist/png.d.ts.map +1 -0
  34. package/dist/png.js +24 -0
  35. package/dist/process.d.ts +17 -0
  36. package/dist/process.d.ts.map +1 -0
  37. package/dist/process.js +67 -0
  38. package/dist/xml.d.ts +10 -0
  39. package/dist/xml.d.ts.map +1 -0
  40. package/dist/xml.js +32 -0
  41. package/package.json +59 -4
  42. package/templates/android/README.md +44 -0
  43. package/templates/android/angular-lynx.properties +7 -0
  44. package/templates/android/app/build.gradle.kts +158 -0
  45. package/templates/android/app/proguard-rules.pro +65 -0
  46. package/templates/android/app/src/main/AndroidManifest.xml +37 -0
  47. package/templates/android/app/src/main/assets/.gitkeep +0 -0
  48. package/templates/android/app/src/main/java/dev/angularlynx/shell/BundleAssets.kt +45 -0
  49. package/templates/android/app/src/main/java/dev/angularlynx/shell/MainActivity.kt +156 -0
  50. package/templates/android/app/src/main/java/dev/angularlynx/shell/ResourceFetchers.kt +115 -0
  51. package/templates/android/app/src/main/java/dev/angularlynx/shell/ShellApplication.kt +39 -0
  52. package/templates/android/app/src/main/java/dev/angularlynx/shell/ShellModule.kt +40 -0
  53. package/templates/android/app/src/main/res/drawable/ic_launcher_background.xml +4 -0
  54. package/templates/android/app/src/main/res/drawable/ic_launcher_foreground.xml +10 -0
  55. package/templates/android/app/src/main/res/mipmap/ic_launcher.xml +6 -0
  56. package/templates/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
  57. package/templates/android/app/src/main/res/values/themes.xml +13 -0
  58. package/templates/android/app/src/main/res/values-night/themes.xml +8 -0
  59. package/templates/android/app/src/main/res/xml/network_security_config.xml +15 -0
  60. package/templates/android/build.gradle.kts +5 -0
  61. package/templates/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  62. package/templates/android/gradle/wrapper/gradle-wrapper.properties +10 -0
  63. package/templates/android/gradle.properties +7 -0
  64. package/templates/android/gradlew +248 -0
  65. package/templates/android/gradlew.bat +82 -0
  66. package/templates/android/settings.gradle.kts +28 -0
  67. package/templates/ios/App/AppDelegate.swift +52 -0
  68. package/templates/ios/App/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
  69. package/templates/ios/App/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
  70. package/templates/ios/App/Assets.xcassets/Contents.json +6 -0
  71. package/templates/ios/App/Assets.xcassets/LaunchBackground.colorset/Contents.json +20 -0
  72. package/templates/ios/App/BundleResourceFetcher.swift +136 -0
  73. package/templates/ios/App/Info.plist +48 -0
  74. package/templates/ios/App/LynxViewController.swift +108 -0
  75. package/templates/ios/App/XElementRegistry.swift +51 -0
  76. package/templates/ios/App.xcodeproj/project.pbxproj +346 -0
  77. package/templates/ios/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  78. package/templates/ios/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme +76 -0
  79. package/templates/ios/Config/App.xcconfig +9 -0
  80. package/templates/ios/LynxBundle/.gitkeep +0 -0
@@ -0,0 +1,65 @@
1
+ # Lynx's keep rules, from Lynx's integration guide
2
+ # (https://lynxjs.org/guide/start/integrate-with-existing-apps). Only used once
3
+ # isMinifyEnabled is turned on in build.gradle.kts.
4
+ # LYNX START
5
+ # use @Keep to annotate retained classes.
6
+ -dontwarn android.support.annotation.Keep
7
+ -keep @android.support.annotation.Keep class **
8
+ -keep @android.support.annotation.Keep class ** {
9
+ @android.support.annotation.Keep <fields>;
10
+ @android.support.annotation.Keep <methods>;
11
+ }
12
+ -dontwarn androidx.annotation.Keep
13
+ -keep @androidx.annotation.Keep class **
14
+ -keep @androidx.annotation.Keep class ** {
15
+ @androidx.annotation.Keep <fields>;
16
+ @androidx.annotation.Keep <methods>;
17
+ }
18
+ # native method call
19
+ -keepclasseswithmembers,includedescriptorclasses class * {
20
+ native <methods>;
21
+ }
22
+ -keepclasseswithmembers class * {
23
+ @com.lynx.tasm.base.CalledByNative <methods>;
24
+ }
25
+
26
+ # to customize a module, you need to keep the class name and the method annotated as LynxMethod.
27
+ -keepclasseswithmembers class * {
28
+ @com.lynx.jsbridge.LynxMethod <methods>;
29
+ }
30
+
31
+ -keepclassmembers class * {
32
+ @com.lynx.tasm.behavior.LynxProp <methods>;
33
+ @com.lynx.tasm.behavior.LynxPropGroup <methods>;
34
+ @com.lynx.tasm.behavior.LynxUIMethod <methods>;
35
+ }
36
+
37
+ -keepclassmembers class com.lynx.tasm.behavior.ui.UIGroup {
38
+ public boolean needCustomLayout();
39
+ }
40
+
41
+ # in case R8 compiler may remove mLoader in bytecode.
42
+ # as mLoader is not used in java and passed as a WeakRef in JNI.
43
+ -keepclassmembers class com.lynx.tasm.LynxTemplateRender {
44
+ private com.lynx.tasm.core.resource.LynxResourceLoader mLoader;
45
+ private com.lynx.tasm.core.resource.LynxResourceLoader mResourceLoader;
46
+ }
47
+
48
+ # the automatically generated setter classes use the class names of LynxBaseUI and ShadowNode and their subclasses.
49
+ -keep class com.lynx.tasm.behavior.ui.LynxBaseUI
50
+ -keep class com.lynx.tasm.behavior.shadow.ShadowNode
51
+ -keep class com.lynx.jsbridge.LynxModule { *; }
52
+ -keep class * extends com.lynx.tasm.behavior.ui.LynxBaseUI
53
+ -keep class * extends com.lynx.tasm.behavior.shadow.ShadowNode
54
+ -keep class * extends com.lynx.jsbridge.LynxModule { *; }
55
+ -keep class * extends com.lynx.jsbridge.LynxContextModule
56
+ -keep class * implements com.lynx.tasm.behavior.utils.Settable
57
+ -keep class * implements com.lynx.tasm.behavior.utils.LynxUISetter
58
+ -keep class * implements com.lynx.tasm.behavior.utils.LynxUIMethodInvoker
59
+ -keep class com.lynx.tasm.rendernode.compat.**{
60
+ *;
61
+ }
62
+ -keep class com.lynx.tasm.rendernode.compat.RenderNodeFactory{
63
+ *;
64
+ }
65
+ # LYNX END
@@ -0,0 +1,37 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
3
+
4
+ <!-- fetch() and XMLHttpRequest go through Lynx's HTTP service. -->
5
+ <uses-permission android:name="android.permission.INTERNET" />
6
+ <!-- angular-lynx:permissions -->
7
+
8
+ <application
9
+ android:name=".ShellApplication"
10
+ android:allowBackup="true"
11
+ android:icon="@mipmap/ic_launcher"
12
+ android:label="@string/app_name"
13
+ android:networkSecurityConfig="@xml/network_security_config"
14
+ android:supportsRtl="true"
15
+ android:theme="@style/Theme.AngularLynxShell">
16
+ <!--
17
+ One activity holding one LynxView; Angular Router does all navigation
18
+ inside it. configChanges keeps the activity (and the running Angular
19
+ app) alive on rotation, resizing and dark-mode switches instead of
20
+ restarting it; the LynxView re-lays itself out.
21
+ -->
22
+ <activity
23
+ android:name=".MainActivity"
24
+ android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|uiMode"
25
+ android:exported="true"
26
+ android:launchMode="singleTask"
27
+ android:screenOrientation="${screenOrientation}"
28
+ android:windowSoftInputMode="adjustResize">
29
+ <intent-filter>
30
+ <action android:name="android.intent.action.MAIN" />
31
+ <category android:name="android.intent.category.LAUNCHER" />
32
+ </intent-filter>
33
+ <!-- angular-lynx:deep-links -->
34
+ </activity>
35
+ </application>
36
+
37
+ </manifest>
File without changes
@@ -0,0 +1,45 @@
1
+ package dev.angularlynx.shell
2
+
3
+ import android.content.Context
4
+ import android.net.Uri
5
+ import java.io.IOException
6
+
7
+ /**
8
+ * The app's dist/ folder is copied into the APK's assets/ (by the AngularLynx
9
+ * generator), so every bundle and static file the Angular app references is an
10
+ * asset. These helpers turn the URLs Lynx hands us into asset paths.
11
+ */
12
+ object BundleAssets {
13
+ /** The bundle the activity renders. */
14
+ const val MAIN_BUNDLE = "main.lynx.bundle"
15
+
16
+ /**
17
+ * Maps a URL from the bundle to a path inside assets/, or null when it
18
+ * points somewhere else (http(s), data:, content:...).
19
+ *
20
+ * Rspeedy's default asset prefix is "/", so bundled files arrive as
21
+ * "/static/image/x.png" or, for lazy bundles, "/async/x.lynx.bundle".
22
+ * "asset:///" (Fresco's scheme) and plain relative paths also work, so
23
+ * the same bundle runs whatever prefix it was built with.
24
+ */
25
+ fun assetPath(url: String): String? {
26
+ val path = when {
27
+ url.startsWith("asset:///") -> url.removePrefix("asset:///")
28
+ url.startsWith("file:///android_asset/") -> url.removePrefix("file:///android_asset/")
29
+ Uri.parse(url).scheme == null -> url.removePrefix("./").trimStart('/')
30
+ else -> return null
31
+ }
32
+ // Drop the query and fragment; assets are plain files.
33
+ return path.substringBefore('?').substringBefore('#').ifEmpty { null }
34
+ }
35
+
36
+ fun read(context: Context, path: String): ByteArray =
37
+ context.assets.open(path).use { it.readBytes() }
38
+
39
+ fun exists(context: Context, path: String): Boolean = try {
40
+ context.assets.open(path).close()
41
+ true
42
+ } catch (_: IOException) {
43
+ false
44
+ }
45
+ }
@@ -0,0 +1,156 @@
1
+ package dev.angularlynx.shell
2
+
3
+ import android.content.res.Configuration
4
+ import android.os.Bundle
5
+ import android.util.Log
6
+ import android.widget.FrameLayout
7
+ import androidx.activity.ComponentActivity
8
+ import androidx.activity.OnBackPressedCallback
9
+ import androidx.activity.enableEdgeToEdge
10
+ import androidx.core.graphics.Insets
11
+ import androidx.core.os.ConfigurationCompat
12
+ import androidx.core.view.ViewCompat
13
+ import androidx.core.view.WindowInsetsCompat
14
+ import com.lynx.react.bridge.JavaOnlyArray
15
+ import com.lynx.tasm.LynxBooleanOption
16
+ import com.lynx.tasm.LynxError
17
+ import com.lynx.tasm.LynxView
18
+ import com.lynx.tasm.LynxViewBuilder
19
+ import com.lynx.tasm.LynxViewClient
20
+ import com.lynx.xelement.XElementBehaviors
21
+
22
+ /**
23
+ * The whole app: one full-screen LynxView running the Angular app. Angular
24
+ * Router navigates inside it, so there is no native navigation stack.
25
+ */
26
+ class MainActivity : ComponentActivity(), ShellModule.Host {
27
+ private lateinit var lynxView: LynxView
28
+
29
+ // The system bar and cutout insets, and whether the screen has a cutout,
30
+ // as the window last reported them. Sent in the global props.
31
+ private var safeArea = Insets.NONE
32
+ private var hasCutout = false
33
+
34
+ // Disabled until the Angular app enables it (see ShellModule), so by
35
+ // default back leaves the app like any other root screen.
36
+ private val backHandler = object : OnBackPressedCallback(false) {
37
+ override fun handleOnBackPressed() {
38
+ lynxView.sendGlobalEvent(ShellModule.BACK_EVENT, JavaOnlyArray())
39
+ }
40
+ }
41
+
42
+ override fun onCreate(savedInstanceState: Bundle?) {
43
+ // Android 15+ always draws apps edge to edge; opting in on older
44
+ // versions too keeps the layout the same everywhere.
45
+ enableEdgeToEdge()
46
+ super.onCreate(savedInstanceState)
47
+
48
+ lynxView = LynxViewBuilder().apply {
49
+ setTemplateProvider(AssetTemplateProvider(this@MainActivity))
50
+ setTemplateResourceFetcher(AssetTemplateResourceFetcher(this@MainActivity))
51
+ setMediaResourceFetcher(AssetMediaResourceFetcher())
52
+ setEnableGenericResourceFetcher(LynxBooleanOption.TRUE)
53
+ setGenericResourceFetcher(AssetGenericResourceFetcher(this@MainActivity))
54
+ registerModule(ShellModule.NAME, ShellModule::class.java, this@MainActivity)
55
+ // Lynx's core has no <input>, <overlay>, <svg>, <blur-view> etc.;
56
+ // these come from XElement (see app/build.gradle.kts). Lynx
57
+ // Explorer registers them the same way.
58
+ addBehaviors(XElementBehaviors().create())
59
+ }.build(this)
60
+ lynxView.addLynxViewClient(object : LynxViewClient() {
61
+ override fun onReceivedError(error: LynxError?) {
62
+ Log.e(TAG, "Lynx error: ${error?.msg}")
63
+ }
64
+ })
65
+
66
+ // Keep the app inside the safe area (status bar, navigation bar and
67
+ // camera cutout). Lynx has no way to give CSS env(safe-area-inset-*)
68
+ // values on Android, so they're 0, which is right for a page that's
69
+ // already inside the safe area. The iOS shell matches.
70
+ val container = FrameLayout(this)
71
+ container.addView(lynxView, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
72
+ ViewCompat.setOnApplyWindowInsetsListener(container) { view, insets ->
73
+ val safe = insets.getInsets(
74
+ WindowInsetsCompat.Type.systemBars() or
75
+ WindowInsetsCompat.Type.displayCutout() or
76
+ WindowInsetsCompat.Type.ime(),
77
+ )
78
+ view.setPadding(safe.left, safe.top, safe.right, safe.bottom)
79
+ // The keyboard isn't part of the safe area, as on iOS.
80
+ val bars = insets.getInsets(
81
+ WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(),
82
+ )
83
+ val cutout = insets.displayCutout != null
84
+ if (bars != safeArea || cutout != hasCutout) {
85
+ safeArea = bars
86
+ hasCutout = cutout
87
+ lynxView.updateGlobalProps(globalProps(resources.configuration))
88
+ }
89
+ WindowInsetsCompat.CONSUMED
90
+ }
91
+ setContentView(container)
92
+
93
+ onBackPressedDispatcher.addCallback(this, backHandler)
94
+ // Sent before the bundle loads, so the app starts with the theme and
95
+ // the language. The insets follow once the window reports them.
96
+ lynxView.updateGlobalProps(globalProps(resources.configuration))
97
+ lynxView.renderTemplateUrl(BundleAssets.MAIN_BUNDLE, "")
98
+ }
99
+
100
+ override fun setBackHandlerEnabled(enabled: Boolean) {
101
+ backHandler.isEnabled = enabled
102
+ }
103
+
104
+ // The manifest's configChanges keeps the activity, and the running app,
105
+ // through a dark mode switch, so the new theme is sent from here. A
106
+ // language change still restarts the activity, which sends the new
107
+ // appLocale: Angular reads LOCALE_ID only once.
108
+ override fun onConfigurationChanged(newConfig: Configuration) {
109
+ super.onConfigurationChanged(newConfig)
110
+ lynxView.updateGlobalProps(globalProps(newConfig))
111
+ }
112
+
113
+ /**
114
+ * The global props the runtime reads: `theme` (LynxTheme), `isNotchScreen`
115
+ * (LynxSafeArea) and `appLocale` (LynxLocale), plus the safe-area insets
116
+ * in dp. The iOS shell sends the same keys, and Lynx Explorer sends theme
117
+ * and isNotchScreen too.
118
+ */
119
+ private fun globalProps(config: Configuration): Map<String, Any> {
120
+ val density = resources.displayMetrics.density
121
+ val night = config.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
122
+ val props = mutableMapOf<String, Any>(
123
+ "platform" to "android",
124
+ "theme" to if (night) "Dark" else "Light",
125
+ "isNotchScreen" to hasCutout,
126
+ "safeAreaTop" to safeArea.top / density,
127
+ "safeAreaBottom" to safeArea.bottom / density,
128
+ "safeAreaLeft" to safeArea.left / density,
129
+ "safeAreaRight" to safeArea.right / density,
130
+ )
131
+ // The user's language as a BCP 47 tag, such as "fr-FR".
132
+ ConfigurationCompat.getLocales(config)[0]?.let { props["appLocale"] = it.toLanguageTag() }
133
+ return props
134
+ }
135
+
136
+ // Lynx pauses timers and animations in the background and fires the
137
+ // page's visibility events from these.
138
+ override fun onResume() {
139
+ super.onResume()
140
+ lynxView.onEnterForeground()
141
+ }
142
+
143
+ override fun onPause() {
144
+ lynxView.onEnterBackground()
145
+ super.onPause()
146
+ }
147
+
148
+ override fun onDestroy() {
149
+ lynxView.destroy()
150
+ super.onDestroy()
151
+ }
152
+
153
+ private companion object {
154
+ const val TAG = "AngularLynxShell"
155
+ }
156
+ }
@@ -0,0 +1,115 @@
1
+ package dev.angularlynx.shell
2
+
3
+ import android.content.Context
4
+ import com.lynx.tasm.core.LynxThreadPool
5
+ import com.lynx.tasm.provider.AbsTemplateProvider
6
+ import com.lynx.tasm.resourceprovider.LynxResourceCallback
7
+ import com.lynx.tasm.resourceprovider.LynxResourceRequest
8
+ import com.lynx.tasm.resourceprovider.LynxResourceResponse
9
+ import com.lynx.tasm.resourceprovider.generic.LynxGenericResourceFetcher
10
+ import com.lynx.tasm.resourceprovider.media.LynxMediaResourceFetcher
11
+ import com.lynx.tasm.resourceprovider.template.LynxTemplateResourceFetcher
12
+ import com.lynx.tasm.resourceprovider.template.TemplateProviderResult
13
+ import okhttp3.OkHttpClient
14
+ import okhttp3.Request
15
+
16
+ /**
17
+ * Lynx can't load anything by itself: the host app supplies loaders for
18
+ * bundles, images and other resources. Ours read the copy of dist/ inside the
19
+ * APK and fall back to HTTP for absolute http(s) URLs (an API, a CDN, or a dev
20
+ * server).
21
+ */
22
+
23
+ private val http by lazy { OkHttpClient() }
24
+
25
+ // LynxResourceResponse.onFailed is declared without a type parameter, so
26
+ // Kotlin sees a raw type; this gives it the one each callback expects.
27
+ @Suppress("UNCHECKED_CAST")
28
+ private fun <T> failed(error: Throwable): LynxResourceResponse<T> =
29
+ LynxResourceResponse.onFailed(error) as LynxResourceResponse<T>
30
+
31
+ /** Reads [url] from assets/ or over HTTP, off the UI thread. */
32
+ private fun load(
33
+ context: Context,
34
+ url: String,
35
+ onSuccess: (ByteArray) -> Unit,
36
+ onFailure: (Throwable) -> Unit,
37
+ ) {
38
+ LynxThreadPool.getBriefIOExecutor().execute {
39
+ try {
40
+ val asset = BundleAssets.assetPath(url)
41
+ val bytes = if (asset != null) {
42
+ BundleAssets.read(context, asset)
43
+ } else {
44
+ http.newCall(Request.Builder().url(url).build()).execute().use { response ->
45
+ check(response.isSuccessful) { "HTTP ${response.code} for $url" }
46
+ response.body!!.bytes()
47
+ }
48
+ }
49
+ onSuccess(bytes)
50
+ } catch (error: Throwable) {
51
+ onFailure(Exception("Couldn't load $url: ${error.message}", error))
52
+ }
53
+ }
54
+ }
55
+
56
+ /** Loads the bundle passed to LynxView.renderTemplateUrl (the main bundle). */
57
+ class AssetTemplateProvider(context: Context) : AbsTemplateProvider() {
58
+ private val context = context.applicationContext
59
+
60
+ override fun loadTemplate(url: String, callback: Callback) =
61
+ load(context, url, callback::onSuccess) { callback.onFailed(it.message) }
62
+ }
63
+
64
+ /**
65
+ * Loads every other bundle: lazy route chunks and dynamic components, which
66
+ * the main bundle requests by URL at runtime.
67
+ */
68
+ class AssetTemplateResourceFetcher(context: Context) : LynxTemplateResourceFetcher() {
69
+ private val context = context.applicationContext
70
+
71
+ override fun fetchTemplate(
72
+ request: LynxResourceRequest,
73
+ callback: LynxResourceCallback<TemplateProviderResult>,
74
+ ) = load(
75
+ context,
76
+ request.url,
77
+ { callback.onResponse(LynxResourceResponse.onSuccess(TemplateProviderResult.fromBinary(it))) },
78
+ { callback.onResponse(failed(it)) },
79
+ )
80
+
81
+ // AngularLynx doesn't use Lynx's server-side rendering data.
82
+ override fun fetchSSRData(request: LynxResourceRequest, callback: LynxResourceCallback<ByteArray>) =
83
+ callback.onResponse(failed(UnsupportedOperationException("No SSR data")))
84
+ }
85
+
86
+ /**
87
+ * Images: rather than loading them, Lynx asks where they live and hands the
88
+ * answer to Fresco. Bundled images become asset:/// URIs, which Fresco reads
89
+ * from the APK; anything else (https:, data:) passes through unchanged.
90
+ */
91
+ class AssetMediaResourceFetcher : LynxMediaResourceFetcher() {
92
+ override fun shouldRedirectUrl(request: LynxResourceRequest): String {
93
+ val asset = BundleAssets.assetPath(request.url) ?: return request.url
94
+ return "asset:///$asset"
95
+ }
96
+ }
97
+
98
+ /** Fonts, Lottie files and other resources the bundle loads by URL. */
99
+ class AssetGenericResourceFetcher(context: Context) : LynxGenericResourceFetcher() {
100
+ private val context = context.applicationContext
101
+
102
+ override fun fetchResource(request: LynxResourceRequest, callback: LynxResourceCallback<ByteArray>) =
103
+ load(
104
+ context,
105
+ request.url,
106
+ { callback.onResponse(LynxResourceResponse.onSuccess(it)) },
107
+ { callback.onResponse(failed(it)) },
108
+ )
109
+
110
+ // Lynx asks for a file path only for resources it hands to native code
111
+ // that reads files; assets inside an APK have no path, so it falls back
112
+ // to fetchResource.
113
+ override fun fetchResourcePath(request: LynxResourceRequest, callback: LynxResourceCallback<String>) =
114
+ callback.onResponse(failed(UnsupportedOperationException("No file paths for assets")))
115
+ }
@@ -0,0 +1,39 @@
1
+ package dev.angularlynx.shell
2
+
3
+ import android.app.Application
4
+ import com.facebook.drawee.backends.pipeline.Fresco
5
+ import com.facebook.imagepipeline.core.ImagePipelineConfig
6
+ import com.facebook.imagepipeline.memory.PoolConfig
7
+ import com.facebook.imagepipeline.memory.PoolFactory
8
+ import com.lynx.service.http.LynxHttpService
9
+ import com.lynx.service.image.LynxImageService
10
+ import com.lynx.service.log.LynxLogService
11
+ import com.lynx.tasm.LynxEnv
12
+ import com.lynx.tasm.service.LynxServiceCenter
13
+
14
+ /**
15
+ * Starts Lynx once per process. Lynx requires its services and LynxEnv to be
16
+ * set up before the first LynxView is built, and Application.onCreate is the
17
+ * one place guaranteed to run first.
18
+ */
19
+ class ShellApplication : Application() {
20
+ override fun onCreate() {
21
+ super.onCreate()
22
+
23
+ // LynxImageService draws <image> through Fresco, which must be
24
+ // initialized first. Fresco also understands asset:/// URIs natively,
25
+ // which is how bundled images are loaded (see AssetMediaResourceFetcher).
26
+ val pools = PoolFactory(PoolConfig.newBuilder().build())
27
+ Fresco.initialize(
28
+ this,
29
+ ImagePipelineConfig.newBuilder(this).setPoolFactory(pools).build(),
30
+ )
31
+ LynxServiceCenter.inst().registerService(LynxImageService.getInstance())
32
+ LynxServiceCenter.inst().registerService(LynxLogService)
33
+ LynxServiceCenter.inst().registerService(LynxHttpService)
34
+
35
+ // No global template provider or custom elements: MainActivity passes
36
+ // its loaders to the LynxView it builds.
37
+ LynxEnv.inst().init(this, null, null, null)
38
+ }
39
+ }
@@ -0,0 +1,40 @@
1
+ package dev.angularlynx.shell
2
+
3
+ import android.content.Context
4
+ import android.os.Handler
5
+ import android.os.Looper
6
+ import com.lynx.jsbridge.LynxMethod
7
+ import com.lynx.jsbridge.LynxModule
8
+
9
+ /**
10
+ * Native module the AngularLynx runtime calls as
11
+ * `NativeModules.AngularLynxShell` (from the background thread, where
12
+ * NativeModules lives).
13
+ *
14
+ * The system back gesture is synchronous: Android needs to know before the
15
+ * gesture starts whether the app will handle it (predictive back shows a
16
+ * different animation either way). The Angular app can't answer in time, so it
17
+ * tells us ahead of time instead: it enables the back handler while the Router
18
+ * has somewhere to go back to. While enabled, back presses become an
19
+ * `angularLynxBack` global event; while disabled (the default, and whenever the
20
+ * runtime doesn't know about this module) Android's default back behavior
21
+ * applies, which leaves the app.
22
+ */
23
+ class ShellModule(context: Context, param: Any?) : LynxModule(context, param) {
24
+ private val host = param as Host
25
+ private val main = Handler(Looper.getMainLooper())
26
+
27
+ interface Host {
28
+ fun setBackHandlerEnabled(enabled: Boolean)
29
+ }
30
+
31
+ @LynxMethod
32
+ fun setBackHandlerEnabled(enabled: Boolean) {
33
+ main.post { host.setBackHandlerEnabled(enabled) }
34
+ }
35
+
36
+ companion object {
37
+ const val NAME = "AngularLynxShell"
38
+ const val BACK_EVENT = "angularLynxBack"
39
+ }
40
+ }
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <shape xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <solid android:color="#DD0031" />
4
+ </shape>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:width="108dp"
4
+ android:height="108dp"
5
+ android:viewportWidth="108"
6
+ android:viewportHeight="108">
7
+ <path
8
+ android:fillColor="#FFFFFF"
9
+ android:pathData="M54,30 L76,78 L68,78 L63,66 L45,66 L40,78 L32,78 Z M48,59 L60,59 L54,44 Z" />
10
+ </vector>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Android 7 (API 24-25) has no adaptive icons, so it gets the two layers flattened. -->
3
+ <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
4
+ <item android:drawable="@drawable/ic_launcher_background" />
5
+ <item android:drawable="@drawable/ic_launcher_foreground" />
6
+ </layer-list>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Placeholder launcher icon; the generator replaces it with the app's icon. -->
3
+ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
4
+ <background android:drawable="@drawable/ic_launcher_background" />
5
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
6
+ </adaptive-icon>
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <!--
4
+ No action bar: the Angular app draws the whole screen. The window
5
+ background shows before the first Lynx frame, so it matches the default
6
+ light/dark page background instead of flashing.
7
+ -->
8
+ <style name="Theme.AngularLynxShell" parent="android:Theme.Material.Light.NoActionBar">
9
+ <item name="android:windowBackground">@android:color/white</item>
10
+ <item name="android:windowLightStatusBar">true</item>
11
+ <item name="android:windowLightNavigationBar">true</item>
12
+ </style>
13
+ </resources>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <style name="Theme.AngularLynxShell" parent="android:Theme.Material.NoActionBar">
4
+ <item name="android:windowBackground">@android:color/black</item>
5
+ <item name="android:windowLightStatusBar">false</item>
6
+ <item name="android:windowLightNavigationBar">false</item>
7
+ </style>
8
+ </resources>
@@ -0,0 +1,15 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!--
3
+ Allows plain HTTP to this device's own loopback address, and nowhere else.
4
+ Loopback traffic never leaves the device, so there's nothing to intercept:
5
+ it reaches a server on the device itself, or one on the development machine
6
+ through `adb reverse`. Every other host keeps Android's default, which
7
+ blocks cleartext. Without this, fetch() to http://localhost failed with
8
+ "CLEARTEXT communication not permitted", which Lynx reports as status 499.
9
+ -->
10
+ <network-security-config>
11
+ <domain-config cleartextTrafficPermitted="true">
12
+ <domain includeSubdomains="false">localhost</domain>
13
+ <domain includeSubdomains="false">127.0.0.1</domain>
14
+ </domain-config>
15
+ </network-security-config>
@@ -0,0 +1,5 @@
1
+ // AGP 9 compiles Kotlin itself (built-in Kotlin), so there is no separate
2
+ // org.jetbrains.kotlin.android plugin to apply.
3
+ plugins {
4
+ id("com.android.application") version "9.4.1" apply false
5
+ }
@@ -0,0 +1,10 @@
1
+ distributionBase=GRADLE_USER_HOME
2
+ distributionPath=wrapper/dists
3
+ distributionSha256Sum=9c0f7faeeb306cb14e4279a3e084ca6b596894089a0638e68a07c945a32c9e14
4
+ distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
5
+ networkTimeout=10000
6
+ retries=0
7
+ retryBackOffMs=500
8
+ validateDistributionUrl=true
9
+ zipStoreBase=GRADLE_USER_HOME
10
+ zipStorePath=wrapper/dists
@@ -0,0 +1,7 @@
1
+ org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
2
+ org.gradle.parallel=true
3
+ org.gradle.caching=true
4
+ # Fresco, which Lynx's image service uses, depends on AndroidX.
5
+ android.useAndroidX=true
6
+ android.nonTransitiveRClass=true
7
+ kotlin.code.style=official