Skip to content

feat(android): browser autodetect (rebased from #311) - #372

Merged
ErikBjare merged 5 commits into
ActivityWatch:masterfrom
TimeToBuildBob:feat/browser-autodetect
Oct 19, 2026
Merged

ErikBjare merged 5 commits into
ActivityWatch:masterfrom
TimeToBuildBob:feat/browser-autodetect

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Rebased and conflict-resolved version of #311 by @GRbit.

With #307 (Brave) and #312 (Vanadium/IronFox/Fennec) merged, the built-in extractors now cover those browsers. This PR adds discovery and tracking for browsers not in KNOWN_BROWSER_PACKAGES — e.g. Cromite, Mull, DuckDuckGo — using PackageManager to find installed generic HTTPS handlers.

What this does

  • BrowserDetection.kt: queries installed generic HTTPS handlers and filters out known-browser packages, yielding a set of candidates for autodetect
  • BrowserProbeMemory.kt: per-package/versionCode memory that learns which URL bar style (Chromium or Gecko) worked, with exponential backoff (up to 30 min) for packages that never produce a URL
  • WebWatcher.kt: integrates detection and probe memory; refreshDetectedBrowsers() runs off the main thread and re-runs on package install/remove/change broadcasts

Conflict resolution (over #311 → master)

The only conflicting file was WebWatcher.kt (mechanical):

  • imports: merged master's Handler/Looper/SystemClock/Executors with the PR's broadcast/PackageManager imports
  • master already defines extractChromiumUrl/extractGeckoUrl — dropped the duplicate copies the cherry-pick introduced
  • onDestroy(): kept master's mainHandler.removeCallbacksAndMessages(null) + writer.shutdown() alongside the PR's unregisterReceiver(...)

One substantive fix applied during rebase: the CHROMIUM probe path in in detectedBrowsers now uses rootInActiveWindow with ignoreFocused = true (same as com.brave.browser). Page events in Chromium forks don't include the toolbar, and a focused address bar shows typed text — using root + ignoreFocused avoids recording incomplete/search text as a URL.

Note

The manual test in the original PR (Fennec, IronFox, Vanadium) no longer exercises autodetect — those packages are now in KNOWN_BROWSER_PACKAGES and routed to dedicated extractors. To test autodetect, use a browser that's still unlisted: Cromite, Mull, or DuckDuckGo for Android.

Co-authored-by: Pavel Griaznov grbitt@gmail.com

GRbit added 4 commits October 19, 2026 11:09
Treat every app that opens generic https links as a browser and try
the Chromium and then the Gecko URL bar on it. The list is refreshed
on package install/remove, so events from non-browser apps still cost
only a set lookup.

Link: ActivityWatch#306
Git-Session-Id: 8f3abcba-eb82-5e1d-8b22-3d4382a852ad
…f non-browsers exponentially up to 30 minutes

Git-Session-Id: 8f3abcba-eb82-5e1d-8b22-3d4382a852ad
…ent's package

Git-Session-Id: 8f3abcba-eb82-5e1d-8b22-3d4382a852ad
Git-Session-Id: 8f3abcba-eb82-5e1d-8b22-3d4382a852ad
@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] Adds browser detection and probing logic to the web watcher.

Fix the missed URL retry before merging so first visits in newly detected browsers are not silently lost.

Findings

  1. P1 First browser visits disappear ▶

Summary

Adds automatic discovery and URL tracking for browsers without dedicated readers.

  • ActivityWatch tracks pages in browsers it does not know by name.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Service starts or package changes] --> B[Find generic HTTPS handlers]
    B --> C[Exclude built-in browsers and this app]
    C --> D[Publish package versions]
    E[Browser accessibility event] --> F{Probe due?}
    D --> F
    F -->|No| G[Skip event]
    F -->|Yes| H[Try saved style or both readers]
    H -->|URL found| I[Save style and update browser session]
    H -->|No URL| J[Save backoff deadline]
    J --> F
Loading

Reviews (1) · Last reviewed commit: "perf(web-watcher): detect browsers off t..." · Reviewed by Greptile

Git-Session-Id: b74bbd85-3466-50df-bd8c-5592bea685c6
@TimeToBuildBob

TimeToBuildBob commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 AI code review

Adds PackageManager-based browser autodetection for browsers not in KNOWN_BROWSER_PACKAGES. New BrowserDetection.kt filters resolved HTTPS handlers; new BrowserProbeMemory.kt persists per-package/versionCode probe state (Chromium/Gecko style, failure backoff) as JSON in SharedPreferences. WebWatcher.kt integrates detection on a single-thread executor, re-runs on package broadcasts, probes unknown browsers via probeMemory, and schedules a follow-up probe when a package is backed off. Adds unit tests for detection, probe memory, and Robolectric retry behavior.

Needs a look — P2 only

Confidence 4/5

1 finding · ⚠️ 1 P2

⚠️ P2 medium — mobile/src/main/java/net/activitywatch/android/watcher/WebWatcher.kt:231

In BrowserProbeMemory.extract, when a package has no state (current returns null), a fresh ProbeState(versionCode) is created with failures=0 and nextProbeAtMs=0. The first failed probe sets failures=1 and delay=min(1_000 shl 0, MAX)=1_000ms. But the backoff test expects the first delay to be 1_000ms, which matches. However, the retryDelayMs() returns 0L for a package with no state, and scheduleProbeLookup() treats delay <= 0 as 'no deadline' and returns without scheduling. This means a package that has never been probed (no state) will not get a follow-up probe scheduled when its first event fails to extract a URL. The comment says 'A failed first probe must not depend on the page emitting another event', but the first failure happens inside extract(), which updates state with failures=1 and nextProbeAtMs=now+1000. After that, retryDelayMs returns 1000-now, so a follow-up is scheduled. Wait, the flow: onAccessibilityEvent for unknown browser -> isKnownBrowser true if isActive (no state -> true) -> processBrowserEvent -> extractUrl -> probeMemory.extract -> no style, isDue true (next=0) -> probes both, fails -> update with failures=1, next=now+1000 -> returns null -> processBrowserEvent finally scheduleProbeLookup -> retryDelayMs returns 1000 -> delay>0 -> schedules. So the first failure does schedule. The issue is when a package is not in detectedBrowsers? No, scheduleProbeLookup returns if version null. For a package that is detected but has no state, it schedules. So that's fine. What about a package that is backed off and then a new event arrives before deadline? isActive false -> isKnownBrowser false -> scheduleProbeLookup called (line 231) -> retryDelayMs returns remaining delay -> schedules a follow-up even though the event was skipped. That's intended? The comment says 'A failed first probe must not depend on the page emitting another event' - but if the package is backed off, the event is skipped, and scheduleProbeLookup schedules a retry at the backoff deadline. That seems fine. However, there is a subtle bug: scheduleProbeLookup is called in the finally block of processBrowserEvent for every browser event, and also in the !isKnownBrowser branch. If a package is backed off and an event arrives, scheduleProbeLookup schedules a follow-up. But the follow-up runnable checks root.packageName == browser and then calls processBrowserEvent with a synthetic event. That synthetic event will call extractUrl again, which will call probeMemory.extract. At that point, if the backoff deadline has passed, it will probe again. If it fails again, it updates state with failures+1 and nextProbeAtMs = now + delay. Then processBrowserEvent's finally calls scheduleProbeLookup again, which schedules another follow-up. This creates a chain of follow-ups, one per backoff period, as long as the package remains the active window. That is intended? The comment says 'Only failed probes create a deadline; extraction errors must not busy-loop.' But this is a failed probe, so it creates a deadline. The chain continues until the package is no longer active or a URL is found. That seems intended. However, there is a potential issue: the follow-up runnable sets probeFollowUp = null at the start, then calls processBrowserEvent, which in its finally calls scheduleProbeLookup, which may set probeFollowUp again. That's fine. But what if the follow-up runs and the package is no longer the active window? It returns early, and probeFollowUp is null, so no new follow-up is scheduled. That's fine. What about the case where the package is backed off and the user switches away before the deadline? The follow-up is cancelled in onAccessibilityEvent when a different package event arrives (lines 216-221). That's fine. So the retry logic seems correct. Let me look for other issues.

Use wall-clock for the postAtTime? Handler.postAtTime uses uptime. Could use postDelayed with delay computed from wall-clock difference, but still uptime-based. Better: store nextProbeAtMs as uptime? But persistence across restarts needs wall. Could compute delay = max(0, nextProbeAtMs - System.currentTimeMillis()) and then postAtTime(uptimeMillis + delay). That still suffers from clock jumps between computation and post. The core mismatch is inherent; maybe acceptable. But the finding stands.

How this was verified: Checked scheduleProbeLookup lines 275-300: delay comes from probeMemory.retryDelayMs which uses System.currentTimeMillis() deadlines; dueAt uses uptimeMillis. BrowserProbeMemory.isDue compares nowMs (wall) to nextProbeAtMs (wall).

Consensus: 3/3 passes agreed
Distinct keys: 1 (general)

Files changed (7) — the diff as I read it
  • mobile/src/main/java/net/activitywatch/android/AWPreferences.kt — Adds getBrowserProbeState()/setBrowserProbeState() to persist BrowserProbeMemory JSON under 'browserProbeState'.
  • mobile/src/main/java/net/activitywatch/android/watcher/BrowserDetection.kt — Adds selectDetectedBrowsers() to filter resolved packages against built-in set and own package.
  • mobile/src/main/java/net/activitywatch/android/watcher/BrowserProbeMemory.kt — Adds BrowserProbeMemory class with per-package/versionCode style learning, exponential backoff, JSON encode/decode, and retain.
  • mobile/src/main/java/net/activitywatch/android/watcher/WebWatcher.kt — Adds detectedBrowsers map, probeMemory, detectionExecutor, packageChangeReceiver, refreshDetectedBrowsers(), versionCodeOf(), scheduleProbeLookup(), and routes unknown browsers through probeMemory.extract.
  • mobile/src/test/java/net/activitywatch/android/watcher/BrowserDetectionTest.kt — Adds tests for selectDetectedBrowsers filtering and empty input.
  • mobile/src/test/java/net/activitywatch/android/watcher/BrowserProbeMemoryTest.kt — Adds tests for probe style learning, backoff, restart persistence, clock rollback, retain, and corrupt state.
  • mobile/src/test/java/net/activitywatch/android/watcher/BrowserProbeRetryTest.kt — Adds Robolectric tests for scheduled probe retry after backoff and for not attributing another app's tree.
Previous review passes
commit score findings engine when
5bb0361c9393 5/5 0 llm 2026-10-19 11:43 UTC

Reviewed 5bb0361c9393 · openrouter/deepseek/deepseek-v4-flash-0731 · llm engine · 160s · about this reviewer

Maintainer commands

@TimeToBuildBob review (own line) — fresh review · @TimeToBuildBob fix — a worker acts on the findings. Once per comment; 👀 = received.

@ErikBjare
ErikBjare merged commit a2227b2 into ActivityWatch:master Oct 19, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants