Skip to content

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
GothwadTech merged 20 commits into
mainfrom
arena/18b983bb-browser
Oct 15, 2026
Merged

GothwadTech merged 20 commits into
mainfrom
arena/18b983bb-browser

Conversation

@GothwadTech

@GothwadTech GothwadTech commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

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.md incident log, README claims, open log item #17) plus a line-by-line audit of the UI/input/security layers, written up in docs/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

Commit What
086370d real Chrome phone layout, working find-in-page + translate, settings theming, perf/lifecycle, manifest + network config
bb3b832 PBKDF2 app lock, notification small icons, DiffUtil adapters, semantic palette, per-tab desktop mode
2f64e1e non-destructive Home, Config.InputMode switch, honest memory panel
58b9245 i18n sweep: every visible Settings/menu/home string is a resource
540c3ca theme consistency: no hard-coded colours left in layouts, drawables or Kotlin
e6ac3bc low-end device pass: adaptive live-tab cap, cheaper snapshots, no main-thread stat spam
46ebe07 JVM unit tests, scripts/quality-gate.sh, quality.yml, AGENTS/AUDIT close-out
91eb545 docs honesty: KDoc, README claims, string-resource escaping
db93e2c correct the licence status note (the repo has no LICENSE file at all)
b455097…415eb8f CI plumbing: executable gradlew, workflow repaired + a workflow-parsing gate, failure digests published to the job summary and as annotations
379c091…037a73f the six defects a real compiler found (see Verification)
99b95c7 audit records CI as green

What 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 llBottomPanel was a permanently-GONE 0dp ConstraintLayout, so toggleMenu() and the "toggle toolbar" shortcut were no-ops. activity_main.xml now owns a real Chrome bottom toolbar (back · home · new tab · tabs badge · menu) with the omnibox pill layout_above it; 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 uses setFindListener/findNext/clearMatches with an "n of m" counter; Translate uses the *.translate.goog page 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.desktopMode was one global switch that reloaded every live tab, and three menus each had their own copy of that loop. Now WebTabState.desktopMode: Boolean? (null = follow the default) persisted through a Room v22 migration, WebViewEx.desktopModeOverride, and one shared activity/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_PACKAGES removed, 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 called System.gc() and reported the availMem delta 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, plurals for counts), emoji stripped from labels and headers, SettingsDialog/openMainCategory take @StringRes so a row label and its detail header can't drift. Every raw hex in layouts/drawables/Kotlin now resolves through the app_* palette in values + 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/ActivityManager spam off the UI threads, onRenderProcessGone recovery intact.

Tests + CI, the part that prevents the next regression. ConfigRulesTest (11 JVM tests over a hand-written FakePrefs) 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.sh enforces 13 of AGENTS §3's hard rules mechanically (main-thread blocking, System.gc(), notifyDataSetChanged() in any RecyclerView.Adapter, device branching, println, hard-coded text/colours, dangling @string/@color refs, emoji in UI strings) and passes on this tree. .github/workflows/quality.yml runs the gate, the unit tests, compileDebugKotlin and lint on every PR — build_apk.yml previously ran no tests at all on any branch.

Notable behaviour changes to call out in release notes

  • Home button: 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.
  • Backgrounding: tab state is saved on Dispatchers.IO instead of runBlocking on the main thread, and onTrimMemory no longer throws the favicon cache away when the app is merely backgrounded (the old level >= TRIM_MEMORY_RUNNING_LOW check also matched TRIM_MEMORY_UI_HIDDEN).
  • Existing plaintext-PIN users: their PIN still works, is upgraded on next unlock, and files/app_lock_pin.dat is deleted.
  • The RAM "Boost" card is gone (replaced by the memory panel).
  • allowFileAccess stays on deliberately (the home screen is file:///android_asset/… and the file manager opens local docs) with both cross-origin file-URL flags explicitly pinned off; mixed content stays at MIXED_CONTENT_COMPATIBILITY_MODE to match Chrome.

Review order, if you want to prioritise

  1. app/common/.../Config.kt — input modes, tab cap, zoom buckets, commit() exception.
  2. singleton/AppLockManager.kt + its two callers — crypto/format migration is the riskiest user-visible change here.
  3. activity/main/view/MainActivityUI.kt, activity_main.xml, ShortcutMgr.kt — the phone layout pivot.
  4. Home show/hide + navigateInternal()/navigateBackInternal() — nav semantics.
  5. assets/tvbro/generic_injects.js — deleted the TV precision-click IIFE and the HTMLMediaElement.prototype patch; media keys now use local helpers.
  6. singleton/AppDatabase.kt — MIGRATION_21_22 (ALTER TABLE tabs ADD COLUMN desktop_mode) + the matching @ColumnInfo in common/model/WebTabState.kt; worth eyeballing together.
  7. AGENTS.md (this is a repo instruction file — new hard rules 16/17/18, incidents #28–#35, #17/#21 closed).

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.yml now reports success on all three jobs: :app:compileDebugKotlin + :app:common:compileDebugKotlin, :app:common:testDebugUnitTest (the 11 new ConfigRulesTest cases 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:

Found by Defect
aapt2 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 \u2026 escape, which aapt2 accepts but inlined as … for consistency
kotlinc ActivityManager.MemoryInfo has no isLowRamDevice — the check is ActivityManager.isLowRamDevice()
kotlinc NativeHomeView ended up with two companion objects, which cascaded into 5 bogus-looking "unresolved reference" errors in WebsiteMenuPopup; a return@withContext label left over from a moved guard; two extension functions (CoroutineScope.cancel, lifecycleScope) used without their imports
kotlinc WebViewEx: var desktopModeOverride + fun setDesktopModeOverride() — same JVM signature, platform declaration clash (the property setter now owns the behaviour); the same clash waiting in WebViewWebEngine was renamed to pendingDesktopModeOverride
runner gradlew was committed as mode 100644, so build_apk.yml has always needed a chmod +x crutch

The 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 \n written there reaches the user literally), not because it broke a build. My first explanation of that aapt2 error blamed the version_and_updates CDATA strings and asserted main had not built since 3c3f210; that was wrong — build_apk.yml built 3c3f210 fine and built this merge fine too (it published v1.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

  • W1 remainder: omnibox showing the page title when scrolled to top, suggestion chips, long-press ⋮ → Customize sheet, incognito badge in the bottom bar.
  • W3 remainder: WebViewAssetLoader for internal:// (then allowFileAccess=false); per-site mixed-content override.
  • W7 remainder: largeHeap still on; EasyList cached but not mmap'd; no boot trace.
  • W10 remainder: Robolectric tests for PageZoomController/ShortcutMgr/TabsModel/PageTranslate; APK-size-per-ABI recorded in release.yml.
  • Translations: the new keys exist in English only; the 11 locales cover ~150 of ~660 strings.
  • AppDatabase still builds with fallbackToDestructiveMigration() and allowMainThreadQueries() (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.
  • Licence (blocks publishing, needs an owner decision, not a code fix): this tree derives from the GPL-3.0-or-later TV Bro project (Config.HOME_PAGE_URL still references tvbrowser.phlox.dev): the repo has no LICENSE file, 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 touch LICENSE (nothing to touch); I only added a status note in the README pointing at docs/AUDIT-2026-10-14.md §0. Either publish under GPL-3.0-or-later with attribution, or re-derive the affected modules.

Arena Agent and others added 20 commits October 14, 2026 08:53
…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 `&amp;`, 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
GothwadTech merged commit 3da6b49 into main Oct 15, 2026
3 checks passed
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>
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.

1 participant