From 88314aaf84f4ec1375e3e750032ce6fa0af691fd Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 22 May 2026 13:08:09 +0000 Subject: [PATCH 1/2] fix(webui): keep loopback ActivityWatch URLs inside WebView --- .../android/fragments/WebUIFragment.kt | 21 ++++++++++++++++++- .../android/fragments/WebUIFragmentTest.kt | 21 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 mobile/src/test/java/net/activitywatch/android/fragments/WebUIFragmentTest.kt diff --git a/mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt b/mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt index cf77b5e3..e3bbc154 100644 --- a/mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt +++ b/mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt @@ -19,11 +19,30 @@ import android.webkit.WebResourceRequest import android.webkit.WebViewClient import net.activitywatch.android.R import java.lang.Thread.sleep +import java.net.URI private const val TAG = "WebUI" private const val ARG_URL = "url" +// The embedded server lives on loopback, so keep those navigations inside the app WebView. +internal fun isEmbeddedActivityWatchUrl(url: String): Boolean { + val uri = try { + URI(url) + } catch (_: Exception) { + return false + } + + if (uri.scheme != "http" && uri.scheme != "https") { + return false + } + + return when (uri.host?.lowercase()) { + "localhost", "127.0.0.1", "::1" -> true + else -> false + } +} + /** * A simple [Fragment] subclass. * Activities that contain this fragment must implement the @@ -74,7 +93,7 @@ class WebUIFragment : Fragment() { val url = request?.url.toString() if (URLUtil.isNetworkUrl(url)) { if (url.startsWith("http://") || url.startsWith("https://")) { - if (!url.contains("//localhost:")) { + if (!isEmbeddedActivityWatchUrl(url)) { // Open the URL in an external browser val i = Intent(Intent.ACTION_VIEW, Uri.parse(url)) startActivity(i) diff --git a/mobile/src/test/java/net/activitywatch/android/fragments/WebUIFragmentTest.kt b/mobile/src/test/java/net/activitywatch/android/fragments/WebUIFragmentTest.kt new file mode 100644 index 00000000..3a4f1f52 --- /dev/null +++ b/mobile/src/test/java/net/activitywatch/android/fragments/WebUIFragmentTest.kt @@ -0,0 +1,21 @@ +package net.activitywatch.android.fragments + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class WebUIFragmentTest { + @Test + fun `treats local embedded server hosts as internal`() { + assertTrue(isEmbeddedActivityWatchUrl("http://127.0.0.1:5600/#/settings/")) + assertTrue(isEmbeddedActivityWatchUrl("http://localhost:5600/#/settings/")) + assertTrue(isEmbeddedActivityWatchUrl("http://[::1]:5600/#/settings/")) + } + + @Test + fun `treats non-loopback hosts as external`() { + assertFalse(isEmbeddedActivityWatchUrl("https://activitywatch.net")) + assertFalse(isEmbeddedActivityWatchUrl("http://192.168.1.10:5600")) + assertFalse(isEmbeddedActivityWatchUrl("not a url")) + } +} From 696e8d5a958bde1d5e9430ae293e22dbe11a846d Mon Sep 17 00:00:00 2001 From: Bob Date: Fri, 22 May 2026 13:23:44 +0000 Subject: [PATCH 2/2] 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. --- .../net/activitywatch/android/fragments/WebUIFragment.kt | 3 ++- .../activitywatch/android/fragments/WebUIFragmentTest.kt | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt b/mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt index e3bbc154..ba2fdc43 100644 --- a/mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt +++ b/mobile/src/main/java/net/activitywatch/android/fragments/WebUIFragment.kt @@ -37,8 +37,9 @@ internal fun isEmbeddedActivityWatchUrl(url: String): Boolean { return false } + // java.net.URI.getHost() returns IPv6 addresses with brackets, e.g. "[::1]" return when (uri.host?.lowercase()) { - "localhost", "127.0.0.1", "::1" -> true + "localhost", "127.0.0.1", "[::1]" -> true else -> false } } diff --git a/mobile/src/test/java/net/activitywatch/android/fragments/WebUIFragmentTest.kt b/mobile/src/test/java/net/activitywatch/android/fragments/WebUIFragmentTest.kt index 3a4f1f52..be27ad8f 100644 --- a/mobile/src/test/java/net/activitywatch/android/fragments/WebUIFragmentTest.kt +++ b/mobile/src/test/java/net/activitywatch/android/fragments/WebUIFragmentTest.kt @@ -7,9 +7,16 @@ import org.junit.Test class WebUIFragmentTest { @Test fun `treats local embedded server hosts as internal`() { + // http variants assertTrue(isEmbeddedActivityWatchUrl("http://127.0.0.1:5600/#/settings/")) assertTrue(isEmbeddedActivityWatchUrl("http://localhost:5600/#/settings/")) assertTrue(isEmbeddedActivityWatchUrl("http://[::1]:5600/#/settings/")) + // https variants (function explicitly allows both schemes) + assertTrue(isEmbeddedActivityWatchUrl("https://127.0.0.1:5600/")) + assertTrue(isEmbeddedActivityWatchUrl("https://localhost:5600/")) + assertTrue(isEmbeddedActivityWatchUrl("https://[::1]:5600/")) + // no-port case + assertTrue(isEmbeddedActivityWatchUrl("http://localhost/")) } @Test