Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
import org.gradle.internal.os.OperatingSystem
import org.jetbrains.compose.ExperimentalComposeLibrary
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
Expand Down Expand Up @@ -123,6 +124,14 @@ kotlin {
implementation(files("./src/desktopMain/libs/oonimkall.jar"))
implementation(compose.desktop.currentOs)
implementation(libs.bundles.desktop)

// As JavaFX have platform-specific dependencies, we need to add them manually
val fxParts = listOf("base", "graphics", "controls", "media", "web", "swing")
val jvmVersion = 17
val fxSuffix = getJavaFxSuffix()
fxParts.forEach {
implementation("org.openjfx:javafx-$it:$jvmVersion:$fxSuffix")
}
}
}
// Testing
Expand Down Expand Up @@ -542,3 +551,14 @@ fun isFdroidTaskRequested(): Boolean {
fun isDebugTaskRequested(): Boolean {
return gradle.startParameter.taskRequests.flatMap { it.args }.any { it.contains("Debug") }
}

fun getJavaFxSuffix(): String {
val os = OperatingSystem.current()
val arch = System.getProperty("os.arch")
return when {
os.isMacOsX -> if (arch == "aarch64") "mac-aarch64" else "mac"
os.isWindows -> "win"
os.isLinux -> if (arch == "aarch64") "linux-aarch64" else "linux"
else -> throw IllegalStateException("Unknown OS: $os")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,130 @@ package org.ooni.probe.ui.shared

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.SwingPanel
import javafx.application.Platform
import javafx.concurrent.Worker
import javafx.embed.swing.JFXPanel
import javafx.scene.Scene
import javafx.scene.layout.StackPane
import javafx.scene.web.WebView
import java.net.URL
import kotlin.io.encoding.Base64

@Composable
actual fun OoniWebView(
controller: OoniWebViewController,
modifier: Modifier,
allowedDomains: List<String>,
) {
val event = controller.rememberNextEvent()

SwingPanel(
factory = {
controller.state = OoniWebViewController.State.Initializing

JFXPanel().apply {
Platform.setImplicitExit(false) // Otherwise, webView will not show the second time
Platform.runLater {

val webView = WebView().apply {
isVisible = true
@Suppress("SetJavaScriptEnabled")
engine.isJavaScriptEnabled = true

// Set up load listeners
engine.loadWorker.stateProperty().addListener { _, _, newValue ->
when (newValue) {
Worker.State.SCHEDULED -> {
controller.state = OoniWebViewController.State.Loading(0f)
}

Worker.State.RUNNING -> {
val progress = engine.loadWorker.progress
controller.state =
OoniWebViewController.State.Loading(progress.toFloat())
}

Worker.State.SUCCEEDED -> {
controller.state = OoniWebViewController.State.Successful
controller.canGoBack = engine.history.currentIndex > 0
}

Worker.State.FAILED -> {
controller.state = OoniWebViewController.State.Failure
controller.canGoBack = engine.history.currentIndex > 0
}

else -> {}
}
}

// Domain restriction
engine.locationProperty().addListener { _, _, newLocation ->
try {
val host = URL(newLocation).host
val allowed = allowedDomains.any { domain ->
host.matches(Regex("^(.*\\.)?$domain$"))
}

if (!allowed) {
engine.load("about:blank")
}
} catch (e: Exception) {
// Invalid URL, ignore
}
controller.canGoBack = engine.history.currentIndex > 0
}

val css = """
body {
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none; /* Firefox */
}
body::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
""".trimIndent()
val cssData = Base64.encode(css.encodeToByteArray())
engine.userStyleSheetLocation =
"data:text/css;charset=utf-8;base64,$cssData"
}

val root = StackPane()
root.children.add(webView)
this.scene = Scene(root)
}
}
},
modifier = modifier,
update = { jfxPanel ->
Platform.runLater {
val root = jfxPanel.scene?.root as? StackPane
val webView = (root?.children?.get(0) as? WebView) ?: return@runLater
when (event) {
is OoniWebViewController.Event.Load -> {
val headers = event.additionalHttpHeaders.entries.joinToString {
"\n${it.key}: it.value"
}
// Hack to send HTTP headers by taking advantage of userAgent
webView.engine.userAgent = "ooni$headers"
webView.engine.load(event.url)
}

OoniWebViewController.Event.Reload -> {
webView.engine.reload()
}

OoniWebViewController.Event.Back -> {
if (webView.engine.history.currentIndex > 0) {
webView.engine.history.go(-1)
}
}

null -> Unit
}
event?.let(controller::onEventHandled)
}
},
)
}
Loading