Skip to content

Commit 2e9e733

Browse files
fix(webui): keep embedded ActivityWatch URLs inside WebView (#161)
* fix(webui): keep loopback ActivityWatch URLs inside WebView * fix(webui): fix IPv6 bracket handling and add https/no-port test coverage java.net.URI.getHost() returns IPv6 addresses with brackets ("[::1]" not "::1"), so the loopback check was silently misclassifying IPv6 loopback URLs as external. Fix by matching "[::1]" directly. Also add https loopback variants and a no-port case to the unit test to cover the full scheme matrix the function guards.
1 parent 9d32040 commit 2e9e733

2 files changed

Lines changed: 49 additions & 1 deletion

File tree

mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,31 @@ import android.webkit.WebResourceRequest
1919
import android.webkit.WebViewClient
2020
import net.activitywatch.android.R
2121
import java.lang.Thread.sleep
22+
import java.net.URI
2223

2324
private const val TAG = "WebUI"
2425

2526
private const val ARG_URL = "url"
2627

28+
// The embedded server lives on loopback, so keep those navigations inside the app WebView.
29+
internal fun isEmbeddedActivityWatchUrl(url: String): Boolean {
30+
val uri = try {
31+
URI(url)
32+
} catch (_: Exception) {
33+
return false
34+
}
35+
36+
if (uri.scheme != "http" && uri.scheme != "https") {
37+
return false
38+
}
39+
40+
// java.net.URI.getHost() returns IPv6 addresses with brackets, e.g. "[::1]"
41+
return when (uri.host?.lowercase()) {
42+
"localhost", "127.0.0.1", "[::1]" -> true
43+
else -> false
44+
}
45+
}
46+
2747
/**
2848
* A simple [Fragment] subclass.
2949
* Activities that contain this fragment must implement the
@@ -74,7 +94,7 @@ class WebUIFragment : Fragment() {
7494
val url = request?.url.toString()
7595
if (URLUtil.isNetworkUrl(url)) {
7696
if (url.startsWith("http://") || url.startsWith("https://")) {
77-
if (!url.contains("//localhost:")) {
97+
if (!isEmbeddedActivityWatchUrl(url)) {
7898
// Open the URL in an external browser
7999
val i = Intent(Intent.ACTION_VIEW, Uri.parse(url))
80100
startActivity(i)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package net.activitywatch.android.fragments
2+
3+
import org.junit.Assert.assertFalse
4+
import org.junit.Assert.assertTrue
5+
import org.junit.Test
6+
7+
class WebUIFragmentTest {
8+
@Test
9+
fun `treats local embedded server hosts as internal`() {
10+
// http variants
11+
assertTrue(isEmbeddedActivityWatchUrl("http://127.0.0.1:5600/#/settings/"))
12+
assertTrue(isEmbeddedActivityWatchUrl("http://localhost:5600/#/settings/"))
13+
assertTrue(isEmbeddedActivityWatchUrl("http://[::1]:5600/#/settings/"))
14+
// https variants (function explicitly allows both schemes)
15+
assertTrue(isEmbeddedActivityWatchUrl("https://127.0.0.1:5600/"))
16+
assertTrue(isEmbeddedActivityWatchUrl("https://localhost:5600/"))
17+
assertTrue(isEmbeddedActivityWatchUrl("https://[::1]:5600/"))
18+
// no-port case
19+
assertTrue(isEmbeddedActivityWatchUrl("http://localhost/"))
20+
}
21+
22+
@Test
23+
fun `treats non-loopback hosts as external`() {
24+
assertFalse(isEmbeddedActivityWatchUrl("https://activitywatch.net"))
25+
assertFalse(isEmbeddedActivityWatchUrl("http://192.168.1.10:5600"))
26+
assertFalse(isEmbeddedActivityWatchUrl("not a url"))
27+
}
28+
}

0 commit comments

Comments
 (0)