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
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Comment on lines +41 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No port restriction on loopback hosts

isEmbeddedActivityWatchUrl returns true for every port on loopback, not just 5600. If the in-app WebView ever renders a page from 127.0.0.1:5600 that links to another local service on a different port (e.g., a dev server at 127.0.0.1:3000), that link will silently stay inside the WebView rather than opening in the external browser. The original check used url.contains("//localhost:") which implicitly scoped to the scheme+host+port triple; the new helper broadens that. Consider adding a port check (port == 5600, or a small set of known ports) if this widened scope is not intentional.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The broader scope is intentional — ActivityWatch can run on non-default ports and we want to keep all loopback traffic in-WebView regardless of port. The original url.contains("//localhost:") check was actually port-agnostic too (it just required a port to be present). Leaving as-is.

}

/**
* A simple [Fragment] subclass.
* Activities that contain this fragment must implement the
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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/"))
}
Comment on lines +9 to +20
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 HTTPS loopback variant not tested

The isEmbeddedActivityWatchUrl function explicitly allows both http and https schemes for loopback hosts, but the "internal" test only exercises http:// URLs. Adding assertTrue(isEmbeddedActivityWatchUrl("https://localhost:5600/")) and its 127.0.0.1/::1 equivalents would verify the full scheme matrix the function guards.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 696e8d5 — added https variants for all three loopback hosts plus a no-port case.


@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"))
}
}
Loading