Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
086370d
fix(ui): real Chrome phone layout, working find-in-page/translate, th…
Oct 14, 2026
bb3b832
security+per-tab desktop mode: PBKDF2 PIN, notification icons, DiffUt…
Oct 14, 2026
2f64e1e
non-destructive Home, input mode switch, honest memory panel
Oct 14, 2026
58b9245
i18n + polish sweep: every visible Settings/menu/home string is a res…
Oct 14, 2026
540c3ca
theme consistency: no hard-coded colours left in layouts, drawables o…
Oct 14, 2026
e6ac3bc
low-end device pass: adaptive live-tab cap, cheaper snapshots, no mai…
Oct 14, 2026
46ebe07
batch 10: tests, repo-hygiene, AGENTS/AUDIT close-out
GothwadTech Oct 14, 2026
91eb545
docs honesty: KDoc, README claims, string-resource escaping
GothwadTech Oct 14, 2026
db93e2c
docs: correct the licence note — the repo has no LICENSE file
GothwadTech Oct 14, 2026
b455097
ci: make gradlew executable — both quality.yml jobs failed in 8s/13s …
GothwadTech Oct 14, 2026
f6b7b3b
ci: gradlew file mode was still 100644 (a later git add re-recorded i…
GothwadTech Oct 14, 2026
415eb8f
ci: repair the workflow my own patch mangled, and make the gate catch…
GothwadTech Oct 14, 2026
5b18119
ci: post the Gradle failure into the job summary
GothwadTech Oct 14, 2026
b7ad6e9
ci: also emit the Gradle failure as an annotation
GothwadTech Oct 14, 2026
379c091
fix(ci-found): MemoryInfo.isLowRamDevice, and CDATA strings that brok…
GothwadTech Oct 14, 2026
d32f2e6
fix(ci-found): am.isLowRamDevice, escaped apostrophes in three new st…
GothwadTech Oct 14, 2026
308fafa
fix(ci-found): NativeHomeView had two companion objects, a wrong lamb…
GothwadTech Oct 14, 2026
1355170
fix(ci-found): desktopModeOverride must be declared before the init b…
GothwadTech Oct 14, 2026
037a73f
fix(ci-found): platform declaration clash on setDesktopModeOverride
GothwadTech Oct 14, 2026
99b95c7
docs: record that CI is green and what the compiler actually caught
GothwadTech Oct 14, 2026
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
116 changes: 116 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
name: Quality Gate & Tests

# The release pipeline (build_apk.yml) only runs on main and only builds. This workflow is the
# cheap, always-on half: the mechanical AGENTS.md rules, the JVM unit tests, and a compile so a
# PR can never land something that does not even build.

on:
pull_request:
push:
branches: [ main, master ]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: quality-${{ github.ref }}
cancel-in-progress: true

jobs:
rules:
name: AGENTS.md rules (grep gate)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

# No JDK, no SDK: this is the feedback loop that should answer in seconds.
- name: Run quality gate
run: bash scripts/quality-gate.sh

test:
name: Unit tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Make Gradle Wrapper Executable
run: chmod +x ./gradlew

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}

- name: Unit tests (app + common)
run: |
set -o pipefail
./gradlew :app:common:testDebugUnitTest testDebugUnitTest --no-daemon --continue --stacktrace 2>&1 | tee gradle.log

# The raw log lives behind an endpoint a sandboxed runner often cannot reach, and a red X
# with no message teaches nobody anything: put the useful lines in the job summary.
- name: Publish the failure in the job summary
if: failure()
run: bash scripts/ci-summarize-gradle.sh

build:
name: Compile (debug) + Android lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Make Gradle Wrapper Executable
run: chmod +x ./gradlew

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}

# debug builds are wired to debug.keystore in app/build.gradle.kts; generate the standard
# debug key when the repo copy is absent, same as build_apk.yml does.
- name: Ensure debug keystore exists
run: |
if [ ! -f debug.keystore ]; then
keytool -genkey -v -keystore debug.keystore \
-storepass android -keypass android \
-keyalg RSA -keysize 2048 -validity 10000 \
-dname "CN=Android Debug,O=Android,C=US" \
-alias androiddebugkey
fi

- name: Compile debug
run: |
set -o pipefail
./gradlew :app:compileDebugKotlin :app:common:compileDebugKotlin --no-daemon --continue --stacktrace 2>&1 | tee gradle.log

- name: Publish the failure in the job summary
if: failure()
run: bash scripts/ci-summarize-gradle.sh

- name: Android lint
continue-on-error: true
run: ./gradlew :app:lintDebug --no-daemon

- name: Upload lint report
if: always()
uses: actions/upload-artifact@v4
with:
name: lint-report
path: app/build/reports/lint-results-*.html
if-no-files-found: ignore
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@
*.keystore
!debug.keystore
*.hprof
/buildSrc/build
/buildSrc/build

/.kotlin
/local.properties
/app/build
/app/common/build
Empty file.
Empty file.
36 changes: 32 additions & 4 deletions AGENTS.md

Large diffs are not rendered by default.

28 changes: 19 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# 🌐 Gothwad TV Browser
### *The Ultimate Desktop-Grade Web Browser for Android TVs, Tablets, Phones, Linux, Windows & macOS*

[![Platform](https://img.shields.io/badge/Platforms-Android%20TV%20%7C%20Tablets%20%7C%20Linux%20%7C%20Windows%20%7C%20macOS-4CAF50?style=for-the-badge&logo=android)](https://github.com/Gothwad/gothwad-browser)
[![Platform](https://img.shields.io/badge/Platforms-Android%20TV%20%7C%20Tablets%20%7C%20Linux%20%7C%20Windows%20%7C%20macOS-4CAF50?style=for-the-badge&logo=android)](https://github.com/GothwadTech/browser)
[![Engine](https://img.shields.io/badge/Engine-Native%20Chromium%20WebView-4285F4?style=for-the-badge&logo=google-chrome)](https://developer.android.com/develop/ui/views/layout/webapps/webview)
[![Language](https://img.shields.io/badge/Language-Kotlin-7F52FF?style=for-the-badge&logo=kotlin)](https://kotlinlang.org)
[![Target Size](https://img.shields.io/badge/APK%20Footprint-%3C%2010%20MB-FF5722?style=for-the-badge)](#-engineering-journey--vision)
Expand All @@ -17,13 +17,23 @@
[![Get on Google Play](https://img.shields.io/badge/Google_Play-Get%20it%20on%20Play%20Store-00875A?style=for-the-badge&logo=google-play&logoColor=white)](https://play.google.com/store/apps/details?id=com.gothwad.browser)
[![Get on JioStore](https://img.shields.io/badge/JioStore-Available%20on%20Jio%20Set--Top%20Box-0A2885?style=for-the-badge&logo=reliance&logoColor=white)](https://www.jio.com/fiber/services/set-top-box)
[![Official Website](https://img.shields.io/badge/Official_Website-Visit%20gothwad.com-FF6F00?style=for-the-badge&logo=google-chrome&logoColor=white)](https://gothwad.com)
[![Direct Download](https://img.shields.io/badge/GitHub-Download%20Latest%20Release-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/Gothwad/gothwad-browser/releases)
[![Direct Download](https://img.shields.io/badge/GitHub-Download%20Latest%20Release-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/GothwadTech/browser/releases)

<br/>

> **"Gothwad Browser is the fastest, securest, and most lightweight browser for Android Phones, TVs, Linux, Windows, and macOS you have ever seen. Engineered with full keyboard and mouse parity, industry-leading TV remote D-Pad navigation, an inbuilt File Manager, TV Notepad, AdBlocker, and Clipboard Manager. Try first, then believe."**
> — *Made in India with ❤️ by Gothwad Tech*

> **Status note (2026-10-14).** The app is in a pre-release hardening pass. Claims below are being
> converted one by one from "aspirational" into "measured": the APK-size figure is a **target**, not a
> CI measurement yet, and the 10+ language claim is qualified under *Global Accessibility* below.
> **Do not publish a build before the licence/attribution question in
> [`docs/AUDIT-2026-10-14.md`](docs/AUDIT-2026-10-14.md) §0 is settled**: this tree derives from the
> GPL-3.0-or-later *TV Bro* project, which cannot be reconciled with the "All Rights Reserved /
> NO OPEN SOURCE LICENSE IS GRANTED" notice below while the in-app About screen advertises "100% Open
> Source Software" — and the repo carries no `LICENSE` file at all. Settling that is the owner's
> decision, not a code fix; nothing here changes the licence terms.

</div>

---
Expand Down Expand Up @@ -73,12 +83,12 @@

Gothwad Browser is built with a singular mission: **bring desktop PC Chrome-grade browsing to Android TV, mobile screens, Linux, Windows, and macOS without the bloat.**

- ⚡ **Ultra-Fast & Featherweight:** Engineered to stay under a **10 MB APK footprint**, booting instantaneously even on budget TV streaming sticks with limited RAM (1GB/2GB).
- ⚡ **Ultra-Fast & Featherweight:** Engineered around a **sub-10 MB APK target**, with the live-page cap, snapshot thumbnails and cache behaviour sized for budget TV streaming sticks (1–2 GB RAM, slow eMMC). The size figure is a target, not a measured CI number yet.
- 🖥️ **PC-Grade Chromium Engine:** Native Chromium rendering fidelity. No synthetic CSS zooms, no layout reflow compromises, and no artificial color shifts—pages render exactly as intended.
- 🛡️ **Integrated Ad & Tracker Protection:** Fast, real-time filter-list ad blocking (Brave `AdBlockClient` engine) directly intercepting network resources asynchronously without degrading render performance.
- 🗂️ **Advanced Tab Architecture:** Dynamic top-tab bar and row/grid tab switchers with proactive LRU memory trimming so you can keep 10+ tabs without background crashes.
- 🔒 **App Lock & Privacy:** Built-in PIN security lock to safeguard your browsing sessions, private files, notes, and history.
- 🌐 **Global Accessibility:** Native localized interfaces across 10+ languages with responsive 10-foot TV UI ergonomics.
- 🔒 **App Lock & Privacy:** PIN lock (PBKDF2-hashed, never stored in plaintext, with attempt lockout and an expiring unlock session), Chromium Safe Browsing **enabled by default**, incognito tabs that never hit disk, and a clipboard manager that respects it.
- 🌐 **Global Accessibility:** Responsive 10-foot TV UI ergonomics, with translated string resources for **11 locales** (de, fa, it, iw, pl, ru, uk, vi, zh, zh-TW + English default). Coverage is currently partial (~150 of ~660 strings per locale): every UI label now lives in `values/strings.xml`, so translations can be added file-by-file instead of chasing hard-coded text — new strings land in English first and are translated on request.

---

Expand All @@ -87,7 +97,7 @@ Gothwad Browser is built with a singular mission: **bring desktop PC Chrome-grad
### 📱 1. Android (TV, Phones & Tablets)
1. **Google Play Store / JioStore:** Search for **"Gothwad TV Browser"** and install with 1-click.
2. **Direct APK Side-loading:**
- Download `GothwadTVBrowser-vX.X.X-Release.apk` from [GitHub Releases](https://github.com/Gothwad/gothwad-browser/releases).
- Download `GothwadTVBrowser-vX.X.X-Release.apk` from [GitHub Releases](https://github.com/GothwadTech/browser/releases).
- Use a file manager, USB drive, or "Send Files to TV" app to install the APK directly on your TV box or phone.

---
Expand All @@ -97,7 +107,7 @@ Gothwad Browser is built with a singular mission: **bring desktop PC Chrome-grad
#### For Debian / Ubuntu / Mint (`.deb`):
```bash
# 1. Download the .deb from Releases
wget https://github.com/Gothwad/gothwad-browser/releases/latest/download/gothwad-browser_all.deb
wget https://github.com/GothwadTech/browser/releases/latest/download/gothwad-browser_all.deb

# 2. Install package
sudo dpkg -i gothwad-browser_all.deb
Expand All @@ -116,7 +126,7 @@ cd gothwad-browser-linux-*

### 🪟 3. Windows (10 & 11)

1. Download **`gothwad-browser-windows-vX.X.X.zip`** from [GitHub Releases](https://github.com/Gothwad/gothwad-browser/releases).
1. Download **`gothwad-browser-windows-vX.X.X.zip`** from [GitHub Releases](https://github.com/GothwadTech/browser/releases).
2. Extract the folder on your PC.
3. Double-click **`Install.bat`**.
- It will automatically create a **Desktop shortcut** and **Start Menu entry** with the official browser icon.
Expand All @@ -128,7 +138,7 @@ cd gothwad-browser-linux-*

### 🍎 4. macOS (Apple Silicon & Intel)

1. Download **`gothwad-browser-macos-vX.X.X.zip`** from [GitHub Releases](https://github.com/Gothwad/gothwad-browser/releases).
1. Download **`gothwad-browser-macos-vX.X.X.zip`** from [GitHub Releases](https://github.com/GothwadTech/browser/releases).
2. Extract the archive.
3. Drag **`Gothwad TV Browser.app`** into your **`/Applications`** folder (or run `./install.sh`).
4. Launch via **Launchpad**, **Spotlight**, or your **Dock**.
Expand Down
97 changes: 95 additions & 2 deletions app/common/src/main/java/com/gothwad/browser/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,19 @@ class Config(val prefs: SharedPreferences) {
const val KEEP_ALIVE_IN_BACKGROUND_KEY = "keep_alive_in_background"
const val MAX_LIVE_TABS_KEY = "max_live_tabs"
const val SAVE_HISTORY_KEY = "save_history"
const val TRANSLATE_TARGET_LANGUAGE_KEY = "translate_target_language"
const val SAFE_BROWSING_KEY = "safe_browsing_enabled"
/**
* Maximum number of tab WebViews kept fully live in memory simultaneously.
* Default is 6: balanced for typical Android TV boxes (1.5GB - 3GB RAM) to allow
* fast tab switching without exhausting system memory.
*/
const val DEFAULT_MAX_LIVE_TABS = 6
/** Cap for the tab-switcher snapshot width; the grid cards are far smaller than a screen. */
const val THUMBNAIL_MAX_WIDTH_PX = 480
const val DESKTOP_MODE_KEY = "desktop_mode"
/** Which input device the user drives the browser with - see [InputMode]. */
const val INPUT_MODE_KEY = "input_mode"
const val DESKTOP_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"

const val ENGINE_WEB_VIEW = "WebView"
Expand Down Expand Up @@ -117,6 +123,23 @@ class Config(val prefs: SharedPreferences) {
BLACK_MIDNIGHT;
}

/**
* How the user drives the browser. This is the one setting that decides pointer handling and
* the default page layout, so that TV users do not have to dig through four different
* sub-screens (virtual cursor, joystick axes, user agent) to get a working remote or a working
* mouse.
*
* - [AUTO] - resolve from the device: touch screen -> [TOUCH], TV with a mouse plugged in ->
* [KEYBOARD_MOUSE], anything else -> [TV_REMOTE].
* - [TV_REMOTE] - D-pad only: the DPAD-driven virtual cursor and axis navigation are on.
* - [KEYBOARD_MOUSE] - a real pointer drives the page, so the drawn virtual cursor is turned
* off (two pointers on screen at once was the classic "double cursor" complaint).
* - [TOUCH] - phone / tablet: no virtual cursor, no axis-to-DPAD translation.
*/
enum class InputMode {
AUTO, TV_REMOTE, KEYBOARD_MOUSE, TOUCH
}

enum class HomePageMode {
HOME_PAGE, SEARCH_ENGINE, CUSTOM, BLANK
}
Expand All @@ -129,7 +152,9 @@ class Config(val prefs: SharedPreferences) {
get() = prefs.getBoolean(INCOGNITO_MODE_KEY, false)
@SuppressLint("ApplySharedPref")
set(value) {
prefs.edit().putBoolean(INCOGNITO_MODE_KEY, value).commit()
// The incognito process reads this flag, so it has to hit disk before the other
// process is started - apply() would race with it.
prefs.edit().putBoolean(INCOGNITO_MODE_KEY, value).commit() // quality-gate: allow
}

var incognitoModeHintSuppress: Boolean
Expand Down Expand Up @@ -197,6 +222,26 @@ class Config(val prefs: SharedPreferences) {
prefs.edit().putBoolean(ENABLE_VIRTUAL_CURSOR_KEY, value).apply()
}

/** Stored input-mode choice (`AUTO` by default, i.e. "follow the device"). */
var inputMode: InputMode
get() = prefs.getString(INPUT_MODE_KEY, null)?.let { saved ->
InputMode.entries.firstOrNull { it.name == saved }
} ?: InputMode.AUTO
set(value) {
prefs.edit().putString(INPUT_MODE_KEY, value.name).apply()
}

/** [InputMode.AUTO] resolved against the actual device / connected hardware. */
fun resolveInputMode(isTvDevice: Boolean, physicalMouseConnected: Boolean): InputMode =
when (inputMode) {
InputMode.AUTO -> when {
!isTvDevice -> InputMode.TOUCH
physicalMouseConnected -> InputMode.KEYBOARD_MOUSE
else -> InputMode.TV_REMOTE
}
else -> inputMode
}

var cursorSizePercent: Int
get() = prefs.getInt(CURSOR_SIZE_PERCENT_KEY, 100)
.coerceIn(CURSOR_SIZE_PERCENT_MIN, CURSOR_SIZE_PERCENT_MAX)
Expand Down Expand Up @@ -332,6 +377,23 @@ class Config(val prefs: SharedPreferences) {
setEffectiveZoom(isDesktopMode(), value)
}

/** Target language code for "Translate this page" ("" = device language). */
var translateTargetLanguage: String
get() = prefs.getString(TRANSLATE_TARGET_LANGUAGE_KEY, "") ?: ""
set(value) {
prefs.edit().putString(TRANSLATE_TARGET_LANGUAGE_KEY, value).apply()
}

/**
* Chrome-like Safe Browsing checks for navigations and downloads. Enabled by default; it can
* be turned off for debugging or on very constrained devices where the extra lookups hurt.
*/
var safeBrowsingEnabled: Boolean
get() = prefs.getBoolean(SAFE_BROWSING_KEY, true)
set(value) {
prefs.edit().putBoolean(SAFE_BROWSING_KEY, value).apply()
}

/** Whether to record visited URLs and searches into local history. */
var saveHistory: Boolean
get() = prefs.getBoolean(SAVE_HISTORY_KEY, true)
Expand Down Expand Up @@ -420,12 +482,43 @@ class Config(val prefs: SharedPreferences) {
var showTopTabBar = ObservableBooleanPreference(false, SHOW_TOP_TAB_BAR_KEY)
var keepAliveInBackground = ObservableBooleanPreference(true, KEEP_ALIVE_IN_BACKGROUND_KEY)

/**
* Cap used when the user never set one. Lowered once at startup by [applyMemoryAwareDefaults]:
* a 1 GB Android TV box cannot keep six renderers alive, since every extra live WebView is a
* full native process plus a painted buffer.
*/
@Volatile
var maxLiveTabsDefault: Int = DEFAULT_MAX_LIVE_TABS

var maxLiveTabs: Int
get() = prefs.getInt(MAX_LIVE_TABS_KEY, DEFAULT_MAX_LIVE_TABS).coerceIn(2, 20)
get() = prefs.getInt(MAX_LIVE_TABS_KEY, maxLiveTabsDefault).coerceIn(2, 20)
set(value) {
prefs.edit().putInt(MAX_LIVE_TABS_KEY, value.coerceIn(2, 20)).apply()
}

/**
* Device-aware defaults, called from `BrowserApp.onCreate` before the first tab is restored.
* Only lowers limits on small-memory hardware; it never raises above the shipped defaults.
*/
fun applyMemoryAwareDefaults(context: android.content.Context) {
try {
val am = context.getSystemService(android.content.Context.ACTIVITY_SERVICE)
as? android.app.ActivityManager ?: return
val info = android.app.ActivityManager.MemoryInfo()
am.getMemoryInfo(info)
val totalMb = if (info.totalMem > 0) info.totalMem / (1024L * 1024L) else 0L
maxLiveTabsDefault = when {
// isLowRamDevice() is a method on ActivityManager (API 19), not on MemoryInfo.
am.isLowRamDevice || totalMb in 1..1536 -> 3
totalMb in 1537..2816 -> 4
totalMb > 2816 -> DEFAULT_MAX_LIVE_TABS
else -> DEFAULT_MAX_LIVE_TABS
}
} catch (e: Throwable) {
// Keep the shipped default when the memory query is unavailable.
}
}

var adBlockEnabled: Boolean = prefs.getBoolean(ADBLOCK_ENABLED_PREF_KEY, true)
set(value) {
field = value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,14 @@ data class WebTabState(@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "wv_state_file")
var wvStateFileName: String? = null,
var adblock: Boolean? = null,
var scale: Float? = null) {
var scale: Float? = null,
/**
* Per-tab "Desktop site" override. `null` means "follow the global default"
* (Config.desktopMode), which keeps existing installs behaving as before until
* the user actually toggles the menu entry for one tab.
*/
@ColumnInfo(name = "desktop_mode")
var desktopMode: Boolean? = null) {
companion object {
val TAG: String = WebTabState::class.java.simpleName

Expand All @@ -64,6 +71,9 @@ data class WebTabState(@PrimaryKey(autoGenerate = true)
var lastLoadingUrl: String? = null //this is last url appeared in WebViewClient.shouldOverrideUrlLoading callback
@Ignore
var lastUrlBeforeHome: String? = null
/** Title of the page currently covered by the native home screen (in-memory only). */
@Ignore
var lastTitleBeforeHome: String? = null
@Ignore
var blockedAds = 0
@Ignore
Expand Down
Loading
Loading