Skip to content

Commit 2242c5d

Browse files
committed
android: editor loads (with error), some i18n groundwork
1 parent af6be65 commit 2242c5d

File tree

9 files changed

+254
-25
lines changed

9 files changed

+254
-25
lines changed

android/app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ android {
3838
}
3939
buildFeatures {
4040
compose = true
41+
buildConfig = true
4142
}
4243
}
4344

android/app/src/main/assets/l10n

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../../src-main/l10n/
Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,89 @@
1+
const {contextBridge, ipcRenderer} = require('electron');
12

3+
contextBridge.exposeInMainWorld('EditorPreload', {
4+
isInitiallyFullscreen: () => ipcRenderer.sendSync('is-initially-fullscreen'),
5+
getInitialFile: () => ipcRenderer.invoke('get-initial-file'),
6+
getFile: (id) => ipcRenderer.invoke('get-file', id),
7+
openedFile: (id) => ipcRenderer.invoke('opened-file', id),
8+
closedFile: () => ipcRenderer.invoke('closed-file'),
9+
showSaveFilePicker: (suggestedName) => ipcRenderer.invoke('show-save-file-picker', suggestedName),
10+
showOpenFilePicker: () => ipcRenderer.invoke('show-open-file-picker'),
11+
setLocale: (locale) => ipcRenderer.sendSync('set-locale', locale),
12+
setChanged: (changed) => ipcRenderer.invoke('set-changed', changed),
13+
openNewWindow: () => ipcRenderer.invoke('open-new-window'),
14+
openAddonSettings: (search) => ipcRenderer.invoke('open-addon-settings', search),
15+
openPackager: () => ipcRenderer.invoke('open-packager'),
16+
openDesktopSettings: () => ipcRenderer.invoke('open-desktop-settings'),
17+
openPrivacy: () => ipcRenderer.invoke('open-privacy'),
18+
openAbout: () => ipcRenderer.invoke('open-about'),
19+
getPreferredMediaDevices: () => ipcRenderer.invoke('get-preferred-media-devices'),
20+
getAdvancedCustomizations: () => ipcRenderer.invoke('get-advanced-customizations'),
21+
setExportForPackager: (callback) => {
22+
exportForPackager = callback;
23+
},
24+
setIsFullScreen: (isFullScreen) => ipcRenderer.invoke('set-is-full-screen', isFullScreen)
25+
});
226

3-
window.EditorPreload = {
27+
let exportForPackager = () => Promise.reject(new Error('exportForPackager missing'));
428

5-
};
29+
ipcRenderer.on('export-project-to-port', (e) => {
30+
const port = e.ports[0];
31+
exportForPackager()
32+
.then(({data, name}) => {
33+
port.postMessage({ data, name });
34+
})
35+
.catch((error) => {
36+
console.error(error);
37+
port.postMessage({ error: true });
38+
});
39+
});
40+
41+
window.addEventListener('message', (e) => {
42+
if (e.source === window) {
43+
const data = e.data;
44+
if (data && typeof data.ipcStartWriteStream === 'string') {
45+
ipcRenderer.postMessage('start-write-stream', data.ipcStartWriteStream, e.ports);
46+
}
47+
}
48+
});
49+
50+
ipcRenderer.on('enumerate-media-devices', (e) => {
51+
navigator.mediaDevices.enumerateDevices()
52+
.then((devices) => {
53+
e.sender.send('enumerated-media-devices', {
54+
devices: devices.map((device) => ({
55+
deviceId: device.deviceId,
56+
kind: device.kind,
57+
label: device.label
58+
}))
59+
});
60+
})
61+
.catch((error) => {
62+
console.error(error);
63+
e.sender.send('enumerated-media-devices', {
64+
error: `${error}`
65+
});
66+
});
67+
});
68+
69+
contextBridge.exposeInMainWorld('PromptsPreload', {
70+
alert: (message) => ipcRenderer.sendSync('alert', message),
71+
confirm: (message) => ipcRenderer.sendSync('confirm', message),
72+
});
73+
74+
// In some Linux environments, people may try to drag & drop files that we don't have access to.
75+
// Remove when https://github.com/electron/electron/issues/30650 is fixed.
76+
if (navigator.userAgent.includes('Linux')) {
77+
document.addEventListener('drop', (e) => {
78+
if (e.isTrusted) {
79+
for (const file of e.dataTransfer.files) {
80+
// Using webUtils is safe as we don't have a legacy build for Linux
81+
const {webUtils} = require('electron');
82+
const path = webUtils.getPathForFile(file);
83+
ipcRenderer.invoke('check-drag-and-drop-path', path);
84+
}
85+
}
86+
}, {
87+
capture: true
88+
});
89+
}
Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
1-
const contextBridge = {
1+
const require = (function() {
2+
const contextBridge = {
3+
exposeInMainWorld: (objectName, objectImplementation) => {
4+
window[objectName] = objectImplementation;
5+
}
6+
};
27

3-
};
8+
const ipcRenderer = {
9+
sendSync: (method, ...args) => {
10+
const response = AndroidIpcSync.sendSync(JSON.stringify({
11+
method,
12+
args
13+
}));
14+
return JSON.parse(response);
15+
},
16+
invoke: (method, ...args) => {}
17+
};
18+
19+
return (moduleName) => {
20+
if (moduleName === "electron") {
21+
return {
22+
contextBridge,
23+
ipcRenderer
24+
};
25+
}
26+
27+
throw new Error(`Mock require() found unknown module: ${moduleName}`);
28+
};
29+
}());
Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,43 @@
11
package org.turbowarp.android
22

33
import androidx.compose.runtime.Composable
4+
import org.json.JSONArray
5+
import org.json.JSONObject
6+
7+
fun mapToJsonObject(map: Map<String, String>): JSONObject {
8+
val jsonObject = JSONObject()
9+
for ((key, value) in map) {
10+
jsonObject.put(key, value)
11+
}
12+
return jsonObject
13+
}
414

515
@Composable
616
fun EditorView() {
717
TurboWarpWebView(
818
url = "https://editor.android-assets.turbowarp.org/gui/gui.html",
919
preloads = listOf(
10-
"ipc-init.js",
1120
"editor.js",
12-
)
21+
),
22+
ipcHandler = object : IpcHandler {
23+
override fun handleSync(
24+
method: String,
25+
args: JSONArray
26+
): Any? {
27+
if (method == "is-initially-fullscreen") {
28+
return false
29+
}
30+
31+
if (method == "set-locale") {
32+
val result = JSONObject()
33+
val strings = L10N.getStrings()
34+
result.put("strings", mapToJsonObject(strings))
35+
println(result)
36+
return result
37+
}
38+
39+
return null
40+
}
41+
}
1342
)
1443
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.turbowarp.android
2+
3+
import android.content.Context
4+
import org.json.JSONObject
5+
6+
object L10N {
7+
private val translations = mutableMapOf<String, Map<String, String>>()
8+
9+
private fun stringsWithDescriptionToMap(jsonObject: JSONObject): Map<String, String> {
10+
val map = mutableMapOf<String, String>()
11+
for (id in jsonObject.keys()) {
12+
println(id)
13+
val infoObject = jsonObject.getJSONObject(id)
14+
map[id] = infoObject.getString("string")
15+
}
16+
return map
17+
}
18+
19+
private fun stringsToMap(jsonObject: JSONObject): Map<String, String> {
20+
val map = mutableMapOf<String, String>()
21+
for (id in jsonObject.keys()) {
22+
map[id] = jsonObject.getString(id)
23+
}
24+
return map
25+
}
26+
27+
fun setup(context: Context) {
28+
val englishStrings = readAssetAsString(context, "l10n/en.json")
29+
val englishStringsObject = JSONObject(englishStrings)
30+
translations["en"] = stringsWithDescriptionToMap(englishStringsObject)
31+
32+
val translatedStrings = readAssetAsString(context, "l10n/generated-translations.json")
33+
val translatedStringsObjects = JSONObject(translatedStrings)
34+
for (locale in translatedStringsObjects.keys()) {
35+
val localeStringsObject = translatedStringsObjects.getJSONObject(locale)
36+
translations[locale] = stringsToMap(localeStringsObject)
37+
}
38+
}
39+
40+
fun getStrings(): Map<String, String> {
41+
return translations["en"]!!
42+
}
43+
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@ import android.os.Bundle
44
import androidx.activity.ComponentActivity
55
import androidx.activity.compose.setContent
66
import androidx.activity.enableEdgeToEdge
7+
import androidx.webkit.WebViewFeature
78
import org.turbowarp.android.ui.theme.TurboWarpTheme
89

10+
// TODO
11+
private fun isDeviceSupported(): Boolean {
12+
return WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)
13+
}
14+
915
class MainActivity : ComponentActivity() {
1016
override fun onCreate(savedInstanceState: Bundle?) {
1117
super.onCreate(savedInstanceState)
1218
enableEdgeToEdge()
19+
L10N.setup(applicationContext)
1320
setContent {
1421
TurboWarpTheme {
1522
EditorView()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.turbowarp.android
2+
3+
import android.content.Context
4+
5+
fun readAssetAsString(context: Context, path: String): String {
6+
val stream = context.assets.open(path)
7+
val reader = stream.bufferedReader()
8+
return reader.readText()
9+
}

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

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.content.Context
55
import android.graphics.Bitmap
66
import android.net.Uri
77
import android.view.ViewGroup
8+
import android.webkit.JavascriptInterface
89
import android.webkit.WebResourceRequest
910
import android.webkit.WebResourceResponse
1011
import android.webkit.WebView
@@ -18,12 +19,9 @@ import androidx.webkit.WebViewAssetLoader
1819
import androidx.webkit.WebViewCompat
1920
import java.io.IOException
2021
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-
}
22+
import androidx.core.net.toUri
23+
import org.json.JSONArray
24+
import org.json.JSONObject
2725

2826
private class ServeAsset(
2927
private val context: Context,
@@ -88,34 +86,53 @@ private class TurboWarpWebViewClient(
8886
}
8987
}
9088

91-
private class WebViewIPC : WebViewCompat.WebMessageListener {
89+
interface IpcHandler {
90+
fun handleSync(method: String, args: JSONArray): Any?
91+
}
92+
93+
private class IpcSync(private val ipcHandler: IpcHandler) {
94+
// Android's JavaScript interface apparently only supports the primitive types.
95+
// So we have to pass around JSON strings. Real fun.
96+
@JavascriptInterface
97+
fun sendSync(jsonRequestString: String): String {
98+
val jsonRequest = JSONObject(jsonRequestString)
99+
val method = jsonRequest.getString("method")
100+
val args = jsonRequest.getJSONArray("args")
101+
102+
val jsonResponse = ipcHandler.handleSync(method, args)
103+
return jsonResponse?.toString() ?: "null"
104+
}
105+
}
106+
107+
private class IpcAsync(private val ipcHandler: IpcHandler) : WebViewCompat.WebMessageListener {
108+
@SuppressLint("RequiresFeature")
92109
override fun onPostMessage(
93110
view: WebView,
94111
message: WebMessageCompat,
95112
sourceOrigin: Uri,
96113
isMainFrame: Boolean,
97114
replyProxy: JavaScriptReplyProxy
98115
) {
99-
116+
replyProxy.postMessage("e")
100117
}
101118
}
102119

103120
private fun getOrigin(url: String): String {
104-
val uri = Uri.parse(url)
121+
val uri = url.toUri()
105122
val sb = StringBuilder()
106123
sb.append(uri.scheme)
107124
sb.append("://")
108125
sb.append(uri.host)
109-
sb.append(uri.host)
110126
return sb.toString()
111127
}
112128

113-
@SuppressLint("SetJavaScriptEnabled")
129+
@SuppressLint("SetJavaScriptEnabled", "RequiresFeature")
114130
@Composable
115131
fun TurboWarpWebView(
116132
url: String,
117133
modifier: Modifier = Modifier,
118134
preloads: List<String> = emptyList(),
135+
ipcHandler: IpcHandler? = null
119136
) {
120137
AndroidView(
121138
modifier = modifier,
@@ -135,14 +152,26 @@ fun TurboWarpWebView(
135152
settings.allowFileAccess = false
136153
settings.allowContentAccess = false
137154

138-
WebViewCompat.addWebMessageListener(
139-
this,
140-
"AndroidIPC",
141-
setOf(getOrigin(url)),
142-
WebViewIPC()
143-
)
144-
145-
webViewClient = TurboWarpWebViewClient(context, preloads)
155+
// Easier debugging
156+
val version = BuildConfig.VERSION_NAME
157+
settings.userAgentString += " org.turbowarp.android/$version"
158+
159+
if (ipcHandler != null) {
160+
val origin = getOrigin(url)
161+
WebViewCompat.addWebMessageListener(
162+
this,
163+
"AndroidIpcAsync",
164+
setOf(origin),
165+
IpcAsync(ipcHandler)
166+
)
167+
addJavascriptInterface(IpcSync(ipcHandler), "AndroidIpcSync")
168+
}
169+
170+
webViewClient = TurboWarpWebViewClient(context, if (ipcHandler == null) {
171+
preloads
172+
} else {
173+
listOf("ipc-init.js").plus(preloads)
174+
})
146175
}
147176
},
148177
update = { webView ->

0 commit comments

Comments
 (0)