Repository navigation
Production hardening pass: Chrome-like phone UI, real TV input modes, per-tab desktop mode, app-lock + Safe Browsing fixes, tests + quality gate - #1
Merged
Conversation
…eme + perf hardening Full audit in docs/AUDIT-2026-10-14.md (AGENTS.md incident log extended with #20-#27). Phone (Chrome-parity): * activity_main.xml: replace the dead 0dp llBottomPanel with a real Chrome bottom toolbar (back / home / new tab / tabs badge / menu); page container now sits layout_above it. * One place decides layout: MainActivityUI.isCompactChromeLayout() (not TV && smallestWidth < 600dp). Phone = address pill on top + toolbar on bottom; TV/tablet keep the full D-pad header + top tab bar. * requestHeaderFocus()/getHeaderFocusableViews() only ever touch visible controls (focus used to be dropped on a GONE button, freezing D-pad & keyboard navigation). * toggleMenu()/hideMenuOverlay(hideBottomButtons) were no-ops; the "toggle toolbar" shortcut now works. Broken features made real (native APIs only, no jugaad): * FindInPageDialog: setFindListener + findNext + clearMatches -> live "n of m" counter, prev/next, highlights cleaned on dismiss. Was: findAllAsync + a toast. * PageTranslate: *.translate.goog page proxy (Chrome's own mechanism) with _x_tr_pto=wapp, remembered target language and "Show original". Was: a URL Google shut down years ago. * Search overlay: '+' icon on the close button fixed, fake Google Lens removed, dark-only hex replaced with theme colours, strings extracted, focus returned to the page, precise list updates. Light/dark consistency: 87 hard-coded text colours in the settings layouts (and Color.WHITE in SettingsRemoteSection) routed to google_text_* - labels were invisible on white cards in light theme. Low-end (1-2GB TV box) & lifecycle: * offscreenPreRaster only for the attached tab (no extra paint buffer per background tab). * Home dashboard stats sampled on Dispatchers.Default (was ActivityManager IPC + StatFs on the UI thread, every second), storage 1-in-5 ticks, no stacking, stops when hidden. * clearCache() no longer leaks a WebView and runs on Main. * onTrimMemory() stopped flushing the favicon cache on every backgrounding. * NetworkCallback replaces the deprecated CONNECTIVITY_CHANGE broadcast. * Incognito activity declares the same configChanges as MainActivity (USB keyboard plug-in no longer destroys private tabs); QUERY_ALL_PACKAGES removed; first-party hosts pinned to TLS; Safe Browsing now on by default with a real Settings switch. Not compiled here (no JDK/Android SDK in the sandbox): build + run the §6 checklist in Android Studio. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…il adapters - AppLockManager: PBKDF2-HMAC-SHA256 hash + salt instead of the plaintext PIN, constant-time compare, 5-attempt lockout with backoff, 60s unlock session, legacy plaintext record/file migrated on next successful unlock, the on-disk plaintext app_lock_pin.dat is deleted. - AppLockActivity/TvPinDialog: lockout countdown, theme colours instead of raw hex, all strings moved to resources. - Notifications now use the proper monochrome small icons (a mipmap launcher icon renders as a grey blob on 8.0+). - New ListDiff helper; favourites / downloads / tabs sidebars and the Chrome tab grid update via DiffUtil instead of notifyDataSetChanged (closes AGENTS #17 for every RecyclerView adapter; the 4 remaining call sites are ListView BaseAdapters where it is the only API). - Desktop site mode is now per tab (WebTabState.desktopMode, Room v22 migration) with Config.desktopMode demoted to the default for new tabs; the three menu entry points share one DesktopMode.kt implementation instead of three copies that forced every live tab to reload. - generic_injects.js: drop the HTMLMediaElement.prototype.playing patch. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
- Home button no longer wipes the tab: showHomeScreen() is an overlay on top of the live page (stopPlayback instead of clearHistory()+loadUrl(about:blank)), Back reveals the page again with no reload, and the fake 'forward' entry that re-fetched the URL is gone. navigateInternal() stopped clearing history when leaving Home too. navigateBackInternal() now honours its own logic instead of an ignored flag. - New isHomeUrl()/isHomeScreenShown()/hideHomeScreen() as the single definitions of 'am I on home', replacing 5 copies of the same boolean expression. - Input mode: Config.InputMode (AUTO/TV_REMOTE/KEYBOARD_MOUSE/TOUCH) + a segmented switch at the very top of Settings, applied on every resume. It writes only existing native flags (virtual cursor, axis->dpad, desktop default), fixes the double-pointer in mouse mode, and makes AUTO follow a hot-plugged mouse. - Home dashboard: 'Click to Boost RAM' gimmick removed; the card opens a real memory panel (device RAM, app heap, live page views) whose two actions are real (release detached background page views / clear WebView cache). performRamBoost() (a System.gc() whose delta was reported as 'freed') deleted. - generic_injects.js: dropped the HTMLMediaElement.prototype patch and the TV 'precision click' engine that synthesised duplicate click() calls and sniffed Google's obfuscated class names (gb_d/gb_wa); media keys now use local helpers and target the element that is actually playing. - 'Desktop site by default' switch added to Settings > User Agent & Identity. - New semantic colour palette (app_surface_elevated/on_surface/outline/accent/error...) in values + values-night, and google_error, for the theme-consistency pass. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…ource now - Extracted ~300 hard-coded android:text / contentDescription literals from all 64 layouts and 44 Kotlin toast/dialog-title literals into strings.xml (560+ entries), reusing existing resources where the text already matched instead of duplicating. - Settings search box now matches on the row's own title/subtitle resources, so it is translated for free (it used to grep hard-coded English keyword blobs). - openMainCategory()/openShortcuts()/openAbout() take @stringres ids: the detail header can no longer drift from the list row label. - Stripped the emoji prefixes from the section headers (professional look) while keeping them as plain text titles. - Home dashboard: time / calendar / network / storage dialogs and SystemMonitorHelper labels moved to resources; counts use plurals (history deleted items, remove-confirm, input devices found) instead of 'item(s)'. - Settings quick-tools: the three zoom buttons share one refreshZoomUi() instead of three copies of the same five lines; orientation toast labels are resources. - Chrome menu About dialog: dropped the stale developer e-mail / wrong repo, points at github.com/GothwadTech/browser and uses app_name (the About said 'Gothwad TV Browser' while everything else says 'Gothwad Browser'). - New bg_settings_segment drawable + selector_settings_segment_text colour for the input mode segmented control. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…r Kotlin - 203 layout + 66 drawable colour literals replaced by the semantic palette (app_surface_elevated / app_surface_sunken / app_on_surface[_muted] / app_outline / app_accent / app_error / app_warning / app_success / app_on_accent), which has day and night values. This is what made the sidebar/dialog chrome unreadable in the light theme. - Focus rings on the card selectors (bg_card_sleek, bg_card_tab_selector) were the same colour as the fill, i.e. invisible on a TV where focus is the only pointer - now an accent ring on a sunken fill; selected state reads differently again. - Omnibox: a custom search engine no longer shows the Google logo (neutral search glyph); 'Not secure' contentDescription moved to resources. - HistoryActivity filter tabs and the Settings status labels now resolve theme colours instead of 0xFF38BDF8 / 0xFF94A3B8. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…n-thread stat spam - BrowserApp applies Config.applyMemoryAwareDefaults() before tabs are restored: 1 GB / lowRamDevice boxes keep 3 live WebViews instead of 6, 2 GB boxes 4. maxLiveTabs stays user-overridable, the adaptive value is only the default. - Tab thumbnails are capped at 480px wide (RGB_565) and are only captured for the visible tab while the activity is resumed; the previous full-view-size draw happened on the UI thread on every page finish, for background tabs too. - WebSettings: allowFileAccessFromFileURLs / allowUniversalAccessFromFileURLs explicitly false (file:// pages stay supported, local pages cannot read other origins); removed the setRenderPriority(HIGH) call that has been a no-op since API 18. - AppLockManager: lock-enabled state and the legacy plaintext-PIN lookup are cached, so a user without app lock no longer pays a File.exists() on every resume; caches are invalidated by the mutators. - Removed the unused runBlocking import from TabsModel (AGENTS §6 grep item). Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Tests (new: the repo had one `ExampleUnitTest`):
- `app/common/src/test/java/com/gothwad/browser/ConfigRulesTest.kt` — 11 JVM
tests over a hand-written `FakePrefs`, no Robolectric needed. They pin the
rules this session actually changed: the adaptive live-tab default and the
2..20 clamp + persistence, input-mode storage round-trip and the AUTO
resolution matrix (incl. "explicit modes are never re-resolved"), per-bucket
zoom clamping that keeps desktop zoom when the phone rotates,
`Config.isDesktopMode()` (desktop UA / `; Mobile` / no-UA-with-override),
and the privacy/safety defaults that the Safe-Browsing incident regressed.
- `app:common` gets `unitTests {}` back (the default `returnDefaultValues` is
exactly what makes such a test fail rather than silently pass).
Repo hygiene:
- `.kotlin/sessions/*.salive` compiler scratch files were committed in
`3c3f210` — untracked, and `.kotlin`/`build` dirs added to `.gitignore`.
Quality gate + CI (AGENTS §6 was 100% manual until now):
- `scripts/quality-gate.sh` — 13 grep checks that enforce the hard rules:
main-thread blocking, `System.gc()`, `notifyDataSetChanged()` in any
`RecyclerView.Adapter`, device/brand branching, `println`, hard-coded toast/
dialog/layout text, hard-coded colours in layouts/drawables/Kotlin, dangling
`@string`/`@color` references and emoji in user-visible strings. Deliberate
exceptions are marked `quality-gate: allow` with a reason (the incognito
`commit()` and the download retry backoff).
- `.github/workflows/quality.yml` — gate on every PR (no SDK needed), unit
tests, `:app:compileDebugKotlin`, lint with the report uploaded. `build_apk.yml`
builds the release but runs no tests at all; that gap is what let #13–#16 and
the Safe-Browsing regression ship.
Found by the gate, fixed here:
- `DownloadTask.retryWithBackoff` slept an *uncapped* delay on the OkHttp
dispatcher thread → capped at 4s with a floor, interrupt re-thrown.
- incognito persistence used `apply()` on a WebView callback thread → `commit()`
(small, synchronous, correctness-over-speed — documented and gated off).
- 2 toasts in `NativeHomeView`, the clear-history confirmation and the page-info
dialog (which had 👤/🔒 emoji titles) are now resources.
- the "connected devices" panel drew emoji glyphs as icons and used
`Color.parseColor("#F87171")` for status — now a real `ImageView` icon per
device type with `app_error`/`app_success`/`app_outline` tints, a `plurals`
status line, and it re-resolves the input mode when a device is plugged in.
- ~40 remaining raw hex values in layouts and drawables routed to the palette.
- 31 translated strings left with a dangling leading space after emoji-stripping.
AGENTS.md:
- incidents #28–#35 logged (plaintext PIN/expired-session-less app lock,
launcher-icon notifications, Home destroying the page, global desktop mode,
scattered input modes, the RAM-boost gimmick, the injected-JS prototype/click
hacks, non-translatable emoji UI text); #17 and #21 closed as fixed.
- new hard rules 16 (home is an overlay, never a navigation), 17 (text/colours
from resources, enforced by the gate) and 18 (input devices through the
platform only, one InputMode decides the bundle); rule 1 now states the zoom
exception explicitly instead of contradicting rule 15, and the verification
checklist gained the gate plus home/desktop/input-mode checks.
docs/AUDIT-2026-10-14.md: §7 records the real state of W1–W11 (W2/W4/W5/W6/W8/W9
done, W1/W3/W7/W10 partial, W11 blocked on the owner's licence decision) and
states plainly that none of this is compiler-verified in this environment.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
- `HardwareInputManager`'s KDoc still advertised the removed "precision mouse click compatibility fix for Airtel Xstream / Amlogic / Rockchip" and a "synthetic touch injection pipeline" — documentation that invites someone to re-implement exactly what AGENTS §3 rules 2/3 forbid. Replaced with what the class actually does: enumerate devices, classify them, honour the per-device block list, and never touch an event. - README: the download badge pointed at `github.com/Gothwad/gothwad-browser`, which is not this repo → `GothwadTech/browser`. "under 10 MB APK" is now labelled a target rather than a fact (nothing measures it yet), "App Lock" describes the real PBKDF2/lockout behaviour, and "10+ languages" is stated accurately: 11 locales exist with ~150 of ~660 strings translated, because every label is finally in `values/strings.xml`. Added a status note telling anyone who finds this tree not to publish before the TV Bro licence question (docs/AUDIT §0) is settled — no licence terms changed here, that stays the owner's call. - One string resource had unescaped double quotes; normalized to `\"` to match the existing style (unescaped apostrophes/quotes are an aapt failure mode, and the i18n pass was script-driven, so all 660 entries were re-checked: no unescaped `'`, no bare `&`, no non-positional multi-arg formats, no duplicate resource names anywhere). Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
The status note I added yesterday said the 'all rights reserved' wording was 'currently in `LICENSE`'. There is no LICENSE file in this repo at all; the wording lives in README.md's proprietary notice, and the contradicting '100% Open Source' text is an in-app string. The audit table now records that explicitly (and drops a stale Config.kt line number). Still only describing the conflict, not resolving it — licence terms remain the owner's call. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…with Permission denied gradlew was committed as mode 100644, so every job that runs ./gradlew dies before Gradle even starts; build_apk.yml has always papered over it with a 'Make Gradle Wrapper Executable' step. Fix the file mode (git update-index --chmod=+x) AND mirror that step in quality.yml so the two don't depend on each other. The grep-gate job passed on the first run; these two never got as far as the compiler. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…t from the worktree) — chmod on disk so the index mode sticks Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
… that class of mistake The chmod-step insertion used str.split(marker)+join, which silently dropped the "- name: Set up JDK 17" line in the test job. GitHub could not parse the file, so both gradle jobs reported as a failed run with the workflow *filename* as their name, no jobs and no log — the worst kind of red X, because it looks like a build failure and is really "CI never ran". - quality.yml: the line is back; both gradle jobs now run Checkout -> Make Gradle Wrapper Executable -> Set up JDK 17 -> Set up Gradle -> ... - quality-gate.sh: new check parses every .github/workflows/*.yml and asserts each job has runs-on + steps and each step has exactly one of uses/run. Verified against the breakage itself (reproduced, gate FAILs, restored, gate passes), not just against the fixed file. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Both gradle jobs failed and the message was unreachable from a restricted network (gh run view --log -> EOF against results-receiver.actions.githubusercontent.com), leaving 'Process completed with exit code 1' as the only signal. The two gradle steps now tee their output to gradle.log and run scripts/ci-summarize-gradle.sh on failure, which writes the compiler errors, the 'What went wrong' block, the failed task list, any failing <testcase> entries from build/test-results XML and the last 40 lines into $GITHUB_STEP_SUMMARY — readable through the check-run API, and useful to a human reviewer too. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
A step's $GITHUB_STEP_SUMMARY is not exposed through the REST API (output.summary came back null
for the job check-run), so the same digest is now echoed as an ::error:: workflow command with
newlines escaped to %0A — annotations are readable at check-runs/{id}/annotations, which is how a
reviewer on a restricted network (or an automated one) gets the compiler errors at all.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…e aapt2 First real compile of this branch, and it earned its keep: - Config.applyMemoryAwareDefaults() read `info.lowRamDevice`; the field is `ActivityManager.MemoryInfo.isLowRamDevice`, so :app:common:compileDebugKotlin failed. - :app:mergeDebugResources died with 'Can not extract resource from ParsedResource' x3 — `version_and_updates` is wrapped in CDATA in values/ and values-zh-rTW/ (introduced in 3c3f210, i.e. main has not compiled since then either). Replaced with `&`, which is what a string resource must use. - quality-gate.sh gained a CDATA check, and the failure digest now also greps aapt2's ' error: ' lines — that is the only output that names the offending resource. - both gradle jobs pass --continue so one broken module no longer hides the errors of the others. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…rings, gate + digest upgrades Second compile round: - Config.applyMemoryAwareDefaults(): isLowRamDevice() lives on ActivityManager (API 19), not on MemoryInfo — the previous two attempts both reached for the wrong class. - values/strings.xml: three incognito-dialog strings I extracted carried a bare apostrophe (aapt2: 'Invalid unicode escape sequence in string' while flattening) and one carried a \\u2026 escape copied out of Kotlin source. Now \\' and a literal … . - quality-gate.sh: new checks for CDATA in values (the failure round one caught) and for apostrophe/unicode escaping — the latter counts preceding backslashes, so a doubled \\\\' (escaped backslash + live quote, silently rendered as a stray backslash) is a finding too. Verified against deliberately broken input, both directions. - ci-summarize-gradle.sh: aapt2's ' error: ' lines are included in the digest (they are the only output naming the offending resource) and the annotation now states the total error count. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…da label and two missing imports The compile is now the source of truth, and it disagreed with my static checks: - 'Only one companion object is allowed per class' + 'Conflicting declarations' — batch 9 added a second companion for DASHBOARD_TICK_MS / STORAGE_TICK_EVERY next to the pre-existing one at the bottom of the file. Kotlin then failed to resolve the *existing* companion's members, which is why WebsiteMenuPopup's NativeHomeView.loadUserBookmarks(context) / saveUserBookmarks(...) calls (and their 'it' / 'add' cascades) were reported as unresolved too. The constants now live in the one legal companion; the call sites were already correct. - 'Unresolved label' — the !isShown guard I moved out of the withContext block kept its return@withContext; it is in the launch body now, so return@launch. - CoroutineScope.cancel() and LifecycleOwner.lifecycleScope are extension functions: both needed explicit imports (kotlinx.coroutines.cancel, androidx.lifecycle.lifecycleScope). - Swept every .kt in the repo for this class of bug with a curated extension/import checker: clean. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
…lock that reads it
WebViewEx's init {} computes the effective user agent, and since batch 3 that reads
desktopModeOverride — but the property was declared at the bottom of the class next to its
accessors. Kotlin initializers run in declaration order, so the read was 'must be initialized'.
Moved the declaration (and its KDoc) above init {} with the comment explaining why the order
matters, so nobody re-sorts it later.
Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
WebViewEx declared both `var desktopModeOverride` and `fun setDesktopModeOverride(Boolean?)`; the property's generated JVM setter has the same signature, so the file did not compile (the earlier 'must be initialized' error aborted the frontend before this was ever reported). - WebViewEx: the function is gone, its 'skip if unchanged, then applyDesktopMode()' behaviour moved into the property setter. Callers keep the same semantics. - WebViewWebEngine: same pattern waiting one line further down (private var desktopModeOverride next to override fun setDesktopModeOverride) — its cache is now pendingDesktopModeOverride, which is also a more honest name: it holds the override for a tab whose WebView has not been created yet. - Scanned every .kt for 'property X + fun setX()' / 'val isX + fun isX()' clashes: none left. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
Six defects were only visible to a real build (two JVM property/setter clashes, a stale lambda label, two missing extension imports, the wrong isLowRamDevice class, string-resource escaping) and one of them — CDATA in version_and_updates — has been breaking :app:mergeDebugResources on main since 3c3f210. Replacing the 'not compiler-verified' caveat with that record, and keeping the part that is still true: the phone + TV run of §6 and a measured APK size are still owed. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
GothwadTech
added a commit
that referenced
this pull request
Oct 15, 2026
…innocent PR #1 recorded that ':app:mergeDebugResources has been failing on main since 3c3f210' because of the CDATA-wrapped version_and_updates strings. It was not: build_apk.yml completed successfully for 3c3f210 and again for the merge of PR #1, which published v1.0.137. The 'Can not extract resource from ParsedResource' errors named *three* resources and they were the three unescaped-apostrophe strings — aapt2 accepts both CDATA and \\uXXXX, so the CDATA edit I made is cosmetic (identical rendered text) and the '\u2026' → '…' change likewise only removed an escape that was already legal. - AUDIT §7 now states the correct sequence, including that I got it wrong, so the next reader does not inherit a phantom regression. - quality-gate.sh keeps the CDATA check but for the honest reason: inside CDATA, aapt's escape handling is off, so a \n or \' written there reaches the user literally — a real trap in a file whose other 660 strings depend on escapes. Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Production hardening pass — 20 commits, 172 files, +4790/−1606 · CI green
Everything below came out of a full read of the repo's own reports first (
AGENTS.mdincident log, README claims, open log item #17) plus a line-by-line audit of the UI/input/security layers, written up indocs/AUDIT-2026-10-14.md. The audit's work packages W1–W11 are tracked here; §7 of that doc records the honest state of each.Commits
086370dbb3b8322f64e1eConfig.InputModeswitch, honest memory panel58b9245540c3cae6ac3bc46ebe07scripts/quality-gate.sh,quality.yml, AGENTS/AUDIT close-out91eb545db93e2cLICENSEfile at all)b455097…415eb8fgradlew, workflow repaired + a workflow-parsing gate, failure digests published to the job summary and as annotations379c091…037a73f99b95c7What actually changed
Phone UI (Chrome-like, no Google-owned chrome). The portrait header was 9 widgets crammed into one 52dp row and the layout's
llBottomPanelwas a permanently-GONE0dpConstraintLayout, sotoggleMenu()and the "toggle toolbar" shortcut were no-ops.activity_main.xmlnow owns a real Chrome bottom toolbar (back · home · new tab · tabs badge · menu) with the omnibox pilllayout_aboveit; phone-vs-TV is decided in exactly one place (MainActivityUI.isCompactChromeLayout()),requestHeaderFocus()/getHeaderFocusableViews()only touch visible controls, and fullscreen hides the toolbar. Find-in-page usessetFindListener/findNext/clearMatcheswith an "n of m" counter; Translate uses the*.translate.googpage proxy instead of a dead endpoint.TV = the PC view, with two input options. New
Config.InputMode(AUTO/TV_REMOTE/KEYBOARD_MOUSE/TOUCH) exposed as a segmented switch at the top of Settings (the "PC tab" — previously the behaviour was scattered over four sub-screens). It only writes existing native flags: virtual cursor for remote mode, D-pad axis translation off for touch, desktop view by default for TV, and it re-resolves on resume and when a keyboard/mouse is hot-plugged. No event synthesis, no OEM branching.Per-tab desktop mode.
Config.desktopModewas one global switch that reloaded every live tab, and three menus each had their own copy of that loop. NowWebTabState.desktopMode: Boolean?(null = follow the default) persisted through a Room v22 migration,WebViewEx.desktopModeOverride, and one sharedactivity/main/DesktopMode.kt.Security. The app lock stored the PIN in plaintext in two places and, once unlocked, stayed unlocked forever with no throttle — replaced with PBKDF2-HmacSHA256 (25k iterations, random salt), constant-time compare, 5-attempt lockout, and a 60s expiring unlock session; legacy records are upgraded on next successful unlock and the plaintext file is deleted. Chromium Safe Browsing was forced off while the README claimed "the securest browser" — it now follows
Config.safeBrowsingEnabled, default on.QUERY_ALL_PACKAGESremoved, first-party hosts pinned to TLS.Home is no longer destructive. One accidental Home press used to
clearHistory()+loadUrl("about:blank"), and "forward" re-fetched the URL over the network. Home is now an overlay over the live page (playback paused, document and back/forward list intact). The "⚡ Click to Boost RAM" card calledSystem.gc()and reported theavailMemdelta as memory "freed" — replaced by a real memory panel whose two actions genuinely work.Consistency & i18n. ~300 layout literals + 44 Kotlin strings + all dialog titles/toasts moved into
res/values/strings.xml(560+ entries,pluralsfor counts), emoji stripped from labels and headers,SettingsDialog/openMainCategorytake@StringResso a row label and its detail header can't drift. Every raw hex in layouts/drawables/Kotlin now resolves through theapp_*palette invalues+values-night, so light theme can no longer end up white-on-white (that was incident #21).Memory/perf for 1–2 GB boxes. Adaptive live-tab default from
MemoryInfo(a user preference always wins), snapshot thumbnails capped at 480px RGB_565 and only captured for the resumed tab,setRenderPriority()deleted (no-op since API 21),runBlocking/StatFs/ActivityManagerspam off the UI threads,onRenderProcessGonerecovery intact.Tests + CI, the part that prevents the next regression.
ConfigRulesTest(11 JVM tests over a hand-writtenFakePrefs) pins the adaptive tab cap, the 2..20 clamp, the input-mode resolution matrix, per-bucket zoom clamping, desktop-UA detection and the privacy defaults.scripts/quality-gate.shenforces 13 of AGENTS §3's hard rules mechanically (main-thread blocking,System.gc(),notifyDataSetChanged()in anyRecyclerView.Adapter, device branching,println, hard-coded text/colours, dangling@string/@colorrefs, emoji in UI strings) and passes on this tree..github/workflows/quality.ymlruns the gate, the unit tests,compileDebugKotlinand lint on every PR —build_apk.ymlpreviously ran no tests at all on any branch.Notable behaviour changes to call out in release notes
showHomeScreen()keeps the WebView's document and its whole back/forward list and only changes which view is on top (playback is stopped through the page's own pause path);hideHomeScreen()reveals the page again and returns false on a fresh tab, which is what now feeds double-press-to-exit.Dispatchers.IOinstead ofrunBlockingon the main thread, andonTrimMemoryno longer throws the favicon cache away when the app is merely backgrounded (the oldlevel >= TRIM_MEMORY_RUNNING_LOWcheck also matchedTRIM_MEMORY_UI_HIDDEN).files/app_lock_pin.datis deleted.allowFileAccessstays on deliberately (the home screen isfile:///android_asset/…and the file manager opens local docs) with both cross-origin file-URL flags explicitly pinned off; mixed content stays atMIXED_CONTENT_COMPATIBILITY_MODEto match Chrome.Review order, if you want to prioritise
app/common/.../Config.kt— input modes, tab cap, zoom buckets,commit()exception.singleton/AppLockManager.kt+ its two callers — crypto/format migration is the riskiest user-visible change here.activity/main/view/MainActivityUI.kt,activity_main.xml,ShortcutMgr.kt— the phone layout pivot.navigateInternal()/navigateBackInternal()— nav semantics.assets/tvbro/generic_injects.js— deleted the TV precision-click IIFE and theHTMLMediaElement.prototypepatch; media keys now use local helpers.singleton/AppDatabase.kt—MIGRATION_21_22(ALTER TABLE tabs ADD COLUMN desktop_mode) + the matching@ColumnInfoincommon/model/WebTabState.kt; worth eyeballing together.Verification status — CI is green
The branch was authored in a sandbox with no JDK and no Android SDK, so it was pushed to CI to be compiled rather than trusted.
.github/workflows/quality.ymlnow reports success on all three jobs::app:compileDebugKotlin+:app:common:compileDebugKotlin,:app:common:testDebugUnitTest(the 11 newConfigRulesTestcases execute and pass) and:app:lintDebug.That loop caught six defects that every static check I had said were fine, and they are now fixed:
Failed to flatten XML … Invalid unicode escape sequence in string×3 — three strings I extracted carried a bare apostrophe (a resource value needs\'); a fourth carried a Kotlin\u2026escape, which aapt2 accepts but inlined as…for consistencyActivityManager.MemoryInfohas noisLowRamDevice— the check isActivityManager.isLowRamDevice()NativeHomeViewended up with two companion objects, which cascaded into 5 bogus-looking "unresolved reference" errors inWebsiteMenuPopup; areturn@withContextlabel left over from a moved guard; two extension functions (CoroutineScope.cancel,lifecycleScope) used without their importsWebViewEx:var desktopModeOverride+fun setDesktopModeOverride()— same JVM signature, platform declaration clash (the property setter now owns the behaviour); the same clash waiting inWebViewWebEnginewas renamed topendingDesktopModeOverridegradlewwas committed as mode 100644, sobuild_apk.ymlhas always needed achmod +xcrutchThe gate grew checks for the classes of mistake that were cheap to make again: apostrophe/unicode escaping in string resources (verified against deliberately broken input, both directions), and CDATA in
values/— kept for the real reason (aapt's escape handling is off inside CDATA, so a\nwritten there reaches the user literally), not because it broke a build. My first explanation of that aapt2 error blamed theversion_and_updatesCDATA strings and assertedmainhad not built since3c3f210; that was wrong —build_apk.ymlbuilt3c3f210fine and built this merge fine too (it publishedv1.0.137). Corrected in #2.Still owed before this is "production ready": a physical run of the AGENTS §6 checklist on a real phone (portrait, 360dp) and a real TV box — heavy JS/WASM site, ad blocking, D-pad symmetry both directions, mouse hover/click/drag-to-select, Ctrl+T, 8+ tabs, resume-from-background, zoom with a fixed-position menu, page colours identical in light and dark system theme, Settings readable in both themes, Home/Back no-reload, per-tab desktop toggle, input-mode switch on both form factors — plus an APK-size measurement to back the README number.
Explicitly left open
WebViewAssetLoaderforinternal://(thenallowFileAccess=false); per-site mixed-content override.largeHeapstill on; EasyList cached but not mmap'd; no boot trace.PageZoomController/ShortcutMgr/TabsModel/PageTranslate; APK-size-per-ABI recorded inrelease.yml.AppDatabasestill builds withfallbackToDestructiveMigration()andallowMainThreadQueries()(both pre-existing). With v22 registered the destructive fallback is currently dead code, but it will silently eat a future bad migration;allowMainThreadQueries()is an ANR risk in the tab-restore path. Retire both once the migration path has been exercised on real installs.Config.HOME_PAGE_URLstill referencestvbrowser.phlox.dev): the repo has noLICENSEfile, README carries an "All Rights Reserved / NO OPEN SOURCE LICENSE IS GRANTED" notice, and the in-app About screen advertises "100% Open Source Software". I did not touchLICENSE(nothing to touch); I only added a status note in the README pointing atdocs/AUDIT-2026-10-14.md§0. Either publish under GPL-3.0-or-later with attribution, or re-derive the affected modules.