Skip to content

Commit af6be65

Browse files
committed
android: some more groundwork
1 parent c61671b commit af6be65

File tree

13 files changed

+206
-84
lines changed

13 files changed

+206
-84
lines changed

android/app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ dependencies {
5454
implementation(libs.material)
5555
implementation(libs.androidx.activity)
5656
implementation(libs.androidx.constraintlayout)
57+
implementation(libs.androidx.webkit)
5758
testImplementation(libs.junit)
5859
androidTestImplementation(libs.androidx.junit)
5960
androidTestImplementation(libs.androidx.espresso.core)

android/app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
33
xmlns:tools="http://schemas.android.com/tools">
44

5+
<uses-permission android:name="android.permission.INTERNET" />
6+
57
<application
68
android:allowBackup="true"
79
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -11,14 +13,6 @@
1113
android:roundIcon="@mipmap/ic_launcher_round"
1214
android:supportsRtl="true"
1315
android:theme="@style/Theme.TurboWarp">
14-
<activity
15-
android:name=".MainActivity2"
16-
android:exported="false"
17-
android:label="@string/title_activity_main2"
18-
android:theme="@style/Theme.TurboWarp" />
19-
<activity
20-
android:name=".EditorActivity"
21-
android:exported="false" />
2216
<activity
2317
android:name=".MainActivity"
2418
android:exported="true"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
3+
window.EditorPreload = {
4+
5+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const contextBridge = {
2+
3+
};

android/app/src/main/java/org/turbowarp/android/EditorActivity.kt

Lines changed: 0 additions & 47 deletions
This file was deleted.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.turbowarp.android
2+
3+
import androidx.compose.runtime.Composable
4+
5+
@Composable
6+
fun EditorView() {
7+
TurboWarpWebView(
8+
url = "https://editor.android-assets.turbowarp.org/gui/gui.html",
9+
preloads = listOf(
10+
"ipc-init.js",
11+
"editor.js",
12+
)
13+
)
14+
}

android/app/src/main/java/org/turbowarp/android/MainActivity.kt

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,6 @@ import android.os.Bundle
44
import androidx.activity.ComponentActivity
55
import androidx.activity.compose.setContent
66
import androidx.activity.enableEdgeToEdge
7-
import androidx.compose.foundation.layout.fillMaxSize
8-
import androidx.compose.foundation.layout.padding
9-
import androidx.compose.material3.Scaffold
10-
import androidx.compose.material3.Text
11-
import androidx.compose.runtime.Composable
12-
import androidx.compose.ui.Modifier
13-
import androidx.compose.ui.tooling.preview.Preview
147
import org.turbowarp.android.ui.theme.TurboWarpTheme
158

169
class MainActivity : ComponentActivity() {
@@ -19,29 +12,8 @@ class MainActivity : ComponentActivity() {
1912
enableEdgeToEdge()
2013
setContent {
2114
TurboWarpTheme {
22-
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
23-
Greeting(
24-
name = "Android",
25-
modifier = Modifier.padding(innerPadding)
26-
)
27-
}
15+
EditorView()
2816
}
2917
}
3018
}
3119
}
32-
33-
@Composable
34-
fun Greeting(name: String, modifier: Modifier = Modifier) {
35-
Text(
36-
text = "Hello $name!",
37-
modifier = modifier
38-
)
39-
}
40-
41-
@Preview(showBackground = true)
42-
@Composable
43-
fun GreetingPreview() {
44-
TurboWarpTheme {
45-
Greeting("Android")
46-
}
47-
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package org.turbowarp.android
2+
3+
import android.annotation.SuppressLint
4+
import android.content.Context
5+
import android.graphics.Bitmap
6+
import android.net.Uri
7+
import android.view.ViewGroup
8+
import android.webkit.WebResourceRequest
9+
import android.webkit.WebResourceResponse
10+
import android.webkit.WebView
11+
import android.webkit.WebViewClient
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.ui.Modifier
14+
import androidx.compose.ui.viewinterop.AndroidView
15+
import androidx.webkit.JavaScriptReplyProxy
16+
import androidx.webkit.WebMessageCompat
17+
import androidx.webkit.WebViewAssetLoader
18+
import androidx.webkit.WebViewCompat
19+
import java.io.IOException
20+
import java.net.URLConnection
21+
22+
private fun readAssetAsString(context: Context, path: String): String {
23+
val stream = context.assets.open(path)
24+
val reader = stream.bufferedReader()
25+
return reader.readText()
26+
}
27+
28+
private class ServeAsset(
29+
private val context: Context,
30+
private val subfolder: String
31+
) : WebViewAssetLoader.PathHandler {
32+
override fun handle(path: String): WebResourceResponse? {
33+
return try {
34+
val assetPath = "$subfolder/$path"
35+
val inputStream = context.assets.open(assetPath)
36+
val mimeType = URLConnection.guessContentTypeFromName(assetPath)
37+
WebResourceResponse(mimeType, null, inputStream)
38+
} catch (_: IOException) {
39+
// TODO: better error page?
40+
WebResourceResponse(null, null, null)
41+
}
42+
}
43+
}
44+
45+
private class TurboWarpWebViewClient(
46+
private val context: Context,
47+
private val preloads: List<String>,
48+
) : WebViewClient() {
49+
private val assetLoaders = mapOf(
50+
"editor.android-assets.turbowarp.org" to WebViewAssetLoader.Builder()
51+
.setDomain("editor.android-assets.turbowarp.org")
52+
.addPathHandler("/", ServeAsset(context, "dist-renderer-webpack/editor"))
53+
.build(),
54+
55+
"packager.android-assets.turbowarp.org" to WebViewAssetLoader.Builder()
56+
.setDomain("packager.android-assets.turbowarp.org")
57+
.addPathHandler("/", ServeAsset(context, "packager"))
58+
.build(),
59+
60+
"about.android-assets.turbowarp.org" to WebViewAssetLoader.Builder()
61+
.setDomain("about.android-assets.turbowarp.org")
62+
.addPathHandler("/", ServeAsset(context, "about"))
63+
.build()
64+
)
65+
66+
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
67+
super.onPageStarted(view, url, favicon)
68+
69+
val sb = StringBuilder()
70+
sb.append("(function() { 'use strict';\n");
71+
for (preloadName in preloads) {
72+
// We assume that the preloads variable is trusted, don't need to worry about path
73+
// traversal or anything like that.
74+
val preloadScript = readAssetAsString(context, "preload/$preloadName")
75+
sb.append(preloadScript)
76+
}
77+
sb.append("\n}());");
78+
79+
view?.evaluateJavascript(sb.toString(), null);
80+
}
81+
82+
override fun shouldInterceptRequest(
83+
view: WebView,
84+
request: WebResourceRequest
85+
): WebResourceResponse? {
86+
val loader = request.url.host?.let { assetLoaders[it] }
87+
return loader?.shouldInterceptRequest(request.url)
88+
}
89+
}
90+
91+
private class WebViewIPC : WebViewCompat.WebMessageListener {
92+
override fun onPostMessage(
93+
view: WebView,
94+
message: WebMessageCompat,
95+
sourceOrigin: Uri,
96+
isMainFrame: Boolean,
97+
replyProxy: JavaScriptReplyProxy
98+
) {
99+
100+
}
101+
}
102+
103+
private fun getOrigin(url: String): String {
104+
val uri = Uri.parse(url)
105+
val sb = StringBuilder()
106+
sb.append(uri.scheme)
107+
sb.append("://")
108+
sb.append(uri.host)
109+
sb.append(uri.host)
110+
return sb.toString()
111+
}
112+
113+
@SuppressLint("SetJavaScriptEnabled")
114+
@Composable
115+
fun TurboWarpWebView(
116+
url: String,
117+
modifier: Modifier = Modifier,
118+
preloads: List<String> = emptyList(),
119+
) {
120+
AndroidView(
121+
modifier = modifier,
122+
factory = { context ->
123+
WebView(context).apply {
124+
layoutParams = ViewGroup.LayoutParams(
125+
ViewGroup.LayoutParams.MATCH_PARENT,
126+
ViewGroup.LayoutParams.MATCH_PARENT
127+
)
128+
129+
// Enable standard web APIs
130+
settings.javaScriptEnabled = true
131+
settings.domStorageEnabled = true
132+
133+
// All of our assets come through custom asset loaders, so
134+
// don't disable file access, like browsers do.
135+
settings.allowFileAccess = false
136+
settings.allowContentAccess = false
137+
138+
WebViewCompat.addWebMessageListener(
139+
this,
140+
"AndroidIPC",
141+
setOf(getOrigin(url)),
142+
WebViewIPC()
143+
)
144+
145+
webViewClient = TurboWarpWebViewClient(context, preloads)
146+
}
147+
},
148+
update = { webView ->
149+
webView.loadUrl(url)
150+
}
151+
)
152+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<resources>
2+
3+
<style name="Widget.Theme.TurboWarp.MyView" parent="">
4+
<item name="android:background">@color/gray_600</item>
5+
<item name="exampleColor">@color/light_blue_600</item>
6+
</style>
7+
</resources>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<resources>
2+
<declare-styleable name="MyView">
3+
<attr name="exampleString" format="string" />
4+
<attr name="exampleDimension" format="dimension" />
5+
<attr name="exampleColor" format="color" />
6+
<attr name="exampleDrawable" format="color|reference" />
7+
</declare-styleable>
8+
</resources>

0 commit comments

Comments
 (0)