Skip to content

Commit 44d81f1

Browse files
committed
android: more fixes, extensions + libraries offline
1 parent 866eeb2 commit 44d81f1

File tree

7 files changed

+151
-37
lines changed

7 files changed

+151
-37
lines changed

android/app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ dependencies {
5656
implementation(libs.androidx.activity)
5757
implementation(libs.androidx.constraintlayout)
5858
implementation(libs.androidx.webkit)
59+
implementation(libs.brotlidec)
5960
testImplementation(libs.junit)
6061
androidTestImplementation(libs.androidx.junit)
6162
androidTestImplementation(libs.androidx.espresso.core)

android/app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,12 @@
1616
<activity
1717
android:name=".MainActivity"
1818
android:exported="true"
19-
android:label="@string/app_name"
20-
android:theme="@style/Theme.TurboWarp">
19+
android:theme="@style/Theme.TurboWarp"
20+
android:configChanges="orientation|screenSize">
2121
<intent-filter>
2222
<action android:name="android.intent.action.MAIN" />
23-
2423
<category android:name="android.intent.category.LAUNCHER" />
2524
</intent-filter>
2625
</activity>
2726
</application>
28-
2927
</manifest>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.turbowarp.android
2+
3+
import android.content.Context
4+
import org.brotli.dec.BrotliInputStream
5+
import java.io.InputStream
6+
7+
fun readAssetAsString(context: Context, path: String): String {
8+
val stream = context.assets.open(path)
9+
val reader = stream.bufferedReader()
10+
return reader.readText()
11+
}
12+
13+
fun readBrotliAssetAsStream(context: Context, path: String): InputStream {
14+
val compressedDataStream = context.assets.open(path)
15+
val decompressedDataStream = BrotliInputStream(compressedDataStream)
16+
return decompressedDataStream
17+
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ object L10N {
99
private fun stringsWithDescriptionToMap(jsonObject: JSONObject): Map<String, String> {
1010
val map = mutableMapOf<String, String>()
1111
for (id in jsonObject.keys()) {
12-
println(id)
1312
val infoObject = jsonObject.getJSONObject(id)
1413
map[id] = infoObject.getString("string")
1514
}

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

Lines changed: 0 additions & 9 deletions
This file was deleted.

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

Lines changed: 129 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.turbowarp.android
22

33
import android.annotation.SuppressLint
44
import android.content.Context
5+
import android.content.Intent
56
import android.graphics.Bitmap
67
import android.net.Uri
78
import android.view.ViewGroup
@@ -22,59 +23,147 @@ import java.net.URLConnection
2223
import androidx.core.net.toUri
2324
import org.json.JSONArray
2425
import org.json.JSONObject
26+
import java.io.InputStream
27+
28+
private fun addIndexIfNeeded(path: String): String {
29+
return if (path.endsWith("/")) {
30+
"$path/index.html"
31+
} else {
32+
path
33+
}
34+
}
35+
36+
private fun makeFetchableResponse(data: InputStream, path: String): WebResourceResponse {
37+
// TODO: use our own mime types instead of the system's
38+
val mimeType = URLConnection.guessContentTypeFromName(path)
39+
40+
return WebResourceResponse(
41+
mimeType,
42+
null,
43+
200,
44+
"OK",
45+
mapOf<String, String>(
46+
"Access-Control-Allow-Origin" to "*"
47+
),
48+
data,
49+
)
50+
}
51+
52+
private fun makeErrorResponse(): WebResourceResponse {
53+
// TODO
54+
return WebResourceResponse(null, null, null)
55+
}
2556

2657
private class ServeAsset(
2758
private val context: Context,
2859
private val subfolder: String
2960
) : WebViewAssetLoader.PathHandler {
3061
override fun handle(path: String): WebResourceResponse? {
3162
return try {
32-
val assetPath = "$subfolder/$path"
33-
val inputStream = context.assets.open(assetPath)
34-
val mimeType = URLConnection.guessContentTypeFromName(assetPath)
35-
WebResourceResponse(mimeType, null, inputStream)
63+
// TODO: probably vulnerable to path traversal
64+
val pathWithIndex = addIndexIfNeeded(path)
65+
val assetPath = "$subfolder/$pathWithIndex"
66+
67+
val stream = context.assets.open(assetPath)
68+
makeFetchableResponse(stream, pathWithIndex)
69+
} catch (_: IOException) {
70+
makeErrorResponse()
71+
}
72+
}
73+
}
74+
75+
private class ServeBrotliAsset(
76+
private val context: Context,
77+
private val subfolder: String
78+
) : WebViewAssetLoader.PathHandler {
79+
override fun handle(path: String): WebResourceResponse? {
80+
return try {
81+
// TODO: probably vulnerable to path traversal
82+
val pathWithIndex = addIndexIfNeeded(path)
83+
val compressedAssetPath = "$subfolder/$pathWithIndex.br"
84+
85+
val stream = readBrotliAssetAsStream(context, compressedAssetPath)
86+
makeFetchableResponse(stream, pathWithIndex)
3687
} catch (_: IOException) {
37-
// TODO: better error page?
38-
WebResourceResponse(null, null, null)
88+
// TODO: does this fall-through to remote or fallthrough?
89+
null
90+
}
91+
}
92+
}
93+
94+
private class ServeLibraryAsset(
95+
private val context: Context,
96+
private val subfolder: String
97+
) : WebViewAssetLoader.PathHandler {
98+
private fun findMd5ext(path: String): String? {
99+
val md5ext = Regex("[0-9a-f]{32}\\.\\w{3}", RegexOption.IGNORE_CASE).find(path)
100+
return md5ext?.value
101+
}
102+
103+
override fun handle(path: String): WebResourceResponse? {
104+
return try {
105+
val md5ext = findMd5ext(path)
106+
107+
if (md5ext == null) {
108+
makeErrorResponse()
109+
} else {
110+
val compressedAssetPath = "$subfolder/$md5ext.br"
111+
val stream = readBrotliAssetAsStream(context, compressedAssetPath)
112+
makeFetchableResponse(stream, md5ext)
113+
}
114+
} catch (_: IOException) {
115+
makeErrorResponse()
39116
}
40117
}
41118
}
42119

43120
private class TurboWarpWebViewClient(
44121
private val context: Context,
45122
private val preloads: List<String>,
123+
private val initialUrl: String
46124
) : WebViewClient() {
47125
private val assetLoaders = mapOf(
48126
"editor.android-assets.turbowarp.org" to WebViewAssetLoader.Builder()
49127
.setDomain("editor.android-assets.turbowarp.org")
50128
.addPathHandler("/", ServeAsset(context, "dist-renderer-webpack/editor"))
51129
.build(),
52130

53-
"packager.android-assets.turbowarp.org" to WebViewAssetLoader.Builder()
54-
.setDomain("packager.android-assets.turbowarp.org")
55-
.addPathHandler("/", ServeAsset(context, "packager"))
131+
"extensions.turbowarp.org" to WebViewAssetLoader.Builder()
132+
.setDomain("extensions.turbowarp.org")
133+
.addPathHandler("/", ServeBrotliAsset(context, "dist-extensions"))
56134
.build(),
57135

58-
"about.android-assets.turbowarp.org" to WebViewAssetLoader.Builder()
59-
.setDomain("about.android-assets.turbowarp.org")
60-
.addPathHandler("/", ServeAsset(context, "about"))
61-
.build()
136+
"assets.scratch.mit.edu" to WebViewAssetLoader.Builder()
137+
.setDomain("assets.scratch.mit.edu")
138+
.addPathHandler("/", ServeLibraryAsset(context, "dist-library-files"))
139+
.build(),
140+
141+
"cdn.assets.scratch.mit.edu" to WebViewAssetLoader.Builder()
142+
.setDomain("cdn.assets.scratch.mit.edu")
143+
.addPathHandler("/", ServeLibraryAsset(context, "dist-library-files"))
144+
.build(),
145+
146+
"packager.turbowarp.org" to WebViewAssetLoader.Builder()
147+
.setDomain("packager.turbowarp.org")
148+
.addPathHandler("/", ServeAsset(context, "packager"))
149+
.build(),
62150
)
63151

64152
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
65153
super.onPageStarted(view, url, favicon)
66154

155+
// Execute preloads in IIFE so that we only expose the variables we want to
67156
val sb = StringBuilder()
68-
sb.append("(function() { 'use strict';\n");
157+
sb.append("(function() { 'use strict';\n")
69158
for (preloadName in preloads) {
70159
// We assume that the preloads variable is trusted, don't need to worry about path
71160
// traversal or anything like that.
72161
val preloadScript = readAssetAsString(context, "preload/$preloadName")
73162
sb.append(preloadScript)
74163
}
75-
sb.append("\n}());");
164+
sb.append("\n}());")
76165

77-
view?.evaluateJavascript(sb.toString(), null);
166+
view?.evaluateJavascript(sb.toString(), null)
78167
}
79168

80169
override fun shouldInterceptRequest(
@@ -84,6 +173,18 @@ private class TurboWarpWebViewClient(
84173
val loader = request.url.host?.let { assetLoaders[it] }
85174
return loader?.shouldInterceptRequest(request.url)
86175
}
176+
177+
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
178+
// Open links in browser app
179+
// TODO: can we make this feel a bit less weird? like that custom tabs thing?
180+
if (request?.url.toString() != initialUrl) {
181+
val intent = Intent(Intent.ACTION_VIEW, request?.url)
182+
view?.context?.startActivity(intent)
183+
return true
184+
}
185+
186+
return super.shouldOverrideUrlLoading(view, request)
187+
}
87188
}
88189

89190
interface IpcHandler {
@@ -152,9 +253,10 @@ fun TurboWarpWebView(
152253
settings.allowFileAccess = false
153254
settings.allowContentAccess = false
154255

155-
// Easier debugging
256+
// To help troubleshooting
156257
val version = BuildConfig.VERSION_NAME
157-
settings.userAgentString += " org.turbowarp.android/$version"
258+
val appId = BuildConfig.APPLICATION_ID
259+
settings.userAgentString += " $appId/$version"
158260

159261
if (ipcHandler != null) {
160262
val origin = getOrigin(url)
@@ -167,11 +269,15 @@ fun TurboWarpWebView(
167269
addJavascriptInterface(IpcSync(ipcHandler), "AndroidIpcSync")
168270
}
169271

170-
webViewClient = TurboWarpWebViewClient(context, if (ipcHandler == null) {
171-
preloads
172-
} else {
173-
listOf("ipc-init.js").plus(preloads)
174-
})
272+
webViewClient = TurboWarpWebViewClient(
273+
context = context,
274+
initialUrl = url,
275+
preloads = if (ipcHandler == null) {
276+
preloads
277+
} else {
278+
listOf("ipc-init.js").plus(preloads)
279+
}
280+
)
175281
}
176282
},
177283
update = { webView ->

android/gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[versions]
22
agp = "8.13.0"
3+
brotlidec = "0.1.2"
34
kotlin = "2.0.21"
45
coreKtx = "1.10.1"
56
junit = "4.13.2"
@@ -17,6 +18,7 @@ webkit = "1.14.0"
1718
[libraries]
1819
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
1920
androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
21+
brotlidec = { module = "org.brotli:dec", version.ref = "brotlidec" }
2022
junit = { group = "junit", name = "junit", version.ref = "junit" }
2123
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
2224
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }

0 commit comments

Comments
 (0)