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..ba2fdc43 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,31 @@ 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 + } + + // java.net.URI.getHost() returns IPv6 addresses with brackets, e.g. "[::1]" + 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 +94,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..be27ad8f --- /dev/null +++ b/mobile/src/test/java/net/activitywatch/android/fragments/WebUIFragmentTest.kt @@ -0,0 +1,28 @@ +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`() { + // 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 + 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")) + } +}