Skip to content

Add Android TV variant#2682

Open
rootshel wants to merge 35 commits into
celzero:mainfrom
ezelab:tv-variant
Open

Add Android TV variant#2682
rootshel wants to merge 35 commits into
celzero:mainfrom
ezelab:tv-variant

Conversation

@rootshel

Copy link
Copy Markdown

Hi @ignoramous, @hussainmohd-a — opening this per your note on #2664. Single PR rather than a stack so review is concentrated; happy to split if you'd rather.

What this adds

A new tv product flavor and a Compose-for-TV UI that reuses upstream's existing engine (BraveVPNService, VpnController, AppConfig, PersistentState, Room DB, Koin) verbatim. No upstream behavior changes; the TV variant is purely additive code in app/src/tv/**.

Phone (fdroidFull, playStoreFull, websiteFull) builds and behavior are unchanged; the TV flavor is a separate output (app-fdroid-tv-*.apk).

Tested on

  • Sony BRAVIA VH1 (Android 12 / API 31, armeabi-v7a) — real hardware
  • Android TV emulator rethink_tv_avd (API 31, arm64-v8a)

App launches, all 9 screens render (Home, DNS, Firewall, Apps, Proxy, Logs, Stats, Rules, Settings + Console), DPAD navigation works, and protection toggles on/off with the tunnel showing live counters.

Per-file rundown

New, TV-only (no risk to phone code):

  • app/src/tv/** — 31 files, all under com.ezelab.rethinktv.*. Compose-for-TV screens that consume the existing upstream services/repos.
  • .github/workflows/android-tv.yml — builds the TV flavor on every PR, same shape as the existing android.yml.

Shared app/src/main — additive only:

  • ConnectionTrackerDAO.kt — 4 new query overloads adding a :uid filter for the TV per-app logs view. Existing methods unchanged.
  • DnsLogDAO.kt — 2 new query overloads adding a :uid filter for the TV per-app DNS log view.
  • ODoHEndpointDAO.kt — 1 new suspend fun getAllAsList() so the TV ODoH picker can consume the endpoints from a coroutine. Existing LiveData/PagingSource untouched.
  • AppDatabase.kt — wrap buildDatabase() to detect Room cannot verify the data integrity on first open, delete the DB file, and rebuild from the prepackaged asset. Without this, BraveVPNService died on every cold start when upgrading from an older install (its first DAO access hit checkIdentity() after the migration chain). Repro and rationale in the commit message.

Build files:

  • build.gradle — Compose Compiler gradle plugin (org.jetbrains.kotlin.plugin.compose) on the buildscript classpath, needed by the project-wide plugin id.
  • app/build.gradle — new tv product flavor and AndroidX TV / Compose-for-TV deps. The compose-runtime dep lives on the base implementation configuration (not tvImplementation) because the Compose Compiler plugin's classpath check runs on every variant; phone APK cost is ~250 KB of unused runtime classes.

Things I'd appreciate eyes on

  1. Whether you'd rather see the 3 DAO additions land as a separate refactor PR (they're harmless overloads but you may prefer them gated).
  2. The AppDatabase schema-mismatch recovery — it's destructive for the user's per-app/firewall customizations on upgrade, but the alternative was a permanent VPN-start crash. Open to a less aggressive approach if you have one in mind.
  3. Naming under com.ezelab.rethinktv — happy to move under com.celzero.bravedns.tv if you'd prefer.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

rootshel and others added 30 commits May 10, 2026 02:05
This repository is now ezelab/rethink-tv, an Android TV UI fork of
celzero/rethink-app. The upstream engine (DNS, firewall, VpnService,
per-app rules) is used as-is; only an Android TV UI is added in a
dedicated Gradle source set in subsequent commits.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new `tv` Gradle product flavor as a peer of `full` in the
existing `releaseType` dimension, with applicationIdSuffix ".tv"
so the rethink-tv build coexists with upstream RethinkDNS on a device
and gets its own F-Droid app entry.

All TV-specific code lives under app/src/tv/ in this commit:

- AndroidManifest.xml: declares a Leanback launcher activity, requires
  android.software.leanback, marks touchscreen optional, and adds a
  banner asset reference.
- RethinkTvLauncherActivity.kt (com.ezelab.rethinktv): a minimal
  Phase 2 stub Activity that displays "Rethink TV — scaffold" so the
  variant builds and installs end-to-end. Replaced by the real
  Compose-for-TV UI in subsequent phases.
- res/drawable/tv_banner.xml: placeholder vector banner (320x180dp).
- res/values/strings.xml: `app_name` override for the tv flavor.

The shared engine in app/src/main/ is intentionally not edited. The
only build.gradle additions are the new flavor block and a mirror of
the `fullImplementation` UI/runtime deps as `tvImplementation` so the
shared engine sources continue to compile under the tv variant.

A new GitHub Actions workflow (.github/workflows/android-tv.yml) builds
`assembleFdroidTvDebug` on push and pull_request to verify the flavor
end-to-end alongside upstream's android.yml (which is left untouched).

Refs: #1 (flavor-scaffold)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Helps surface which Room/KSP DAO class triggers the
'No property named value was found in annotation Query' error so we
can pin down whether it's a real source-level problem or a classpath
issue introduced by the new tv flavor.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Root cause of the KSP failure ("No property named value was found in
annotation Query"): `app/src/main/` DAOs use string interpolation in
`@Query` annotations referencing constants like `ID_WG_BASE`,
`MAX_LOGS`, `MISSING_UID`, and `UID_EVERYBODY`. Several of these
(notably `ID_WG_BASE`) live in `app/src/full/` rather than
`app/src/main/`, so the previous lean `tv` source set could not
resolve them. Kotlin emitted unresolvable annotation arguments, KSP
then handed Room an XAnnotation with an empty values list, and Room
crashed on `getAnnotationValue("value")`.

Upstream's `app/src/full/` is not pure phone UI: it is 295 files
including `service/` and `viewmodel/` packages that `main/` depends
on. Reskinning the UI cleanly therefore requires the `tv` flavor to
inherit `full/`'s engine surface verbatim.

This commit:

  * Adds an `android.sourceSets.tv` block that appends
    `src/full/java` and `src/full/res` to the tv source set and
    points `manifest.srcFile` at `src/full/AndroidManifest.xml`.
  * Drops the placeholder `RethinkTvLauncherActivity`,
    `AndroidManifest.xml`, and `tv_banner.xml` from `app/src/tv/` —
    those belong to the upcoming `tv-ux-dashboard` phase, which
    will introduce a dedicated leanback launcher on top of the
    inherited `full` manifest.
  * Removes the redundant `tvImplementation firestackDependency()`
    declaration. Firestack is scoped on the `releaseChannel`
    dimension (play / fdroid / website), so each combined variant
    such as `fdroidTv` already pulls it in via
    `fdroidImplementation`.
  * Keeps `app/src/tv/res/values/strings.xml` so "Rethink TV"
    continues to override the upstream `app_name` for the tv build.

The plan in the session workspace is updated separately to record the
architectural pivot from "reskin only" to "inherit-from-full then
override the launcher".

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 3 (`release-pipeline`): wire up an end-to-end path from a
`v*-tv` git tag to a signed `fdroidTvRelease` APK attached to a
GitHub Release, without touching upstream's local-signing path.

  * `app/build.gradle` (append-only): if the four `TV_RELEASE_KS_*`
    env vars are present, declare a new `tvRelease` signing config
    (env-var-based, same shape as upstream's existing `alpha` config)
    and attach it to the `release` build type. When the env vars
    are absent — local builds, the `🫣 Android CI` job, the
    `📺 Android TV CI` debug job — upstream's keystore.properties /
    `config` signingConfig path remains the only thing touching the
    release build type. Phone (`full`) release builds are
    structurally unaffected.

  * `.github/workflows/android-tv-release.yml`: triggers on
    `v*-tv` / `tv-v*` tags and on `workflow_dispatch` (with a
    `dry_run` input defaulted to `true`). The job materialises the
    keystore from `TV_RELEASE_KS_BASE64`, runs
    `assembleFdroidTvRelease`, uploads the resulting APK(s) as a
    workflow artifact with 30-day retention, and — only for
    non-dry-run tag pushes with secrets configured — publishes a
    GitHub Release using `softprops/action-gh-release@v2`. Tags
    ending in `-scaffold`, `-alpha`, `-beta`, or `-rc` are flagged
    as prereleases.

  * `docs/release.md`: maintainer documentation covering keystore
    generation (`keytool -genkeypair … -keysize 4096`), base64
    encoding for the GitHub Actions secret, the four secret names
    expected by the workflow, the dry-run procedure for verifying
    the release path before a real tag is cut, and architectural
    notes on why the signing config is a sibling of upstream's
    `config` rather than a replacement.

The dry-run will be triggered immediately after this commit lands on
`origin/main` to validate that proguard / R8 can minify the
inherited `app/src/full/` UI under the tv flavor; an UNSIGNED APK
is acceptable for that validation (the `TV_RELEASE_KS_*` secrets
have not been configured yet — that is an out-of-band maintainer
action documented in `docs/release.md`).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`assembleFdroidTvRelease` failed at Gradle configuration with
"Unexpected character: '\"' @ line 334, column 17" because the
double-quoted Groovy strings in the new TV-release signing block used
`\`` to escape backticks — `\`` is not a valid Groovy escape, so
the lexer treated the `\` as a backslash and choked on the
following `"`.

Replace the backticks with single quotes, which serve the same
cosmetic purpose in log output without confusing the parser.

This was caught by the Phase 3 dry-run release workflow
(`workflow_dispatch` on `📺 Android TV Release`); the workflow
itself is fine and re-running it after this commit should produce an
unsigned APK artifact (the maintainer has not yet configured the
`TV_RELEASE_KS_*` secrets — see docs/release.md).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase 8 (`upstream-sync-tooling`): wire up the long-lived
`upstream-sync` branch and the cadence that keeps the fork in step
with celzero/rethink-app without ever editing upstream files.

  * `scripts/sync-upstream.sh`: idempotent, three-mode sync script
    (`--push` and `--open-pr` are optional). Fetches
    `upstream/main`, exits cleanly when `origin/main` is already
    ahead, otherwise resets the `upstream-sync` branch to
    `origin/main` and runs a `--no-ff` merge with a structured
    commit message. Includes post-merge invariants: every
    rethink-tv-owned file must still exist, and the
    `// rethink-tv fork: Android TV flavor` and inherit-from-full
    `sourceSets` markers must still be in `app/build.gradle`. Fails
    loudly (exit 1) rather than silently if either invariant breaks.

  * `.github/workflows/upstream-sync.yml`: weekly cron at 03:00 UTC
    on Mondays plus a `workflow_dispatch` trigger. Adds the
    upstream remote, runs the sync script, force-pushes (with
    `--force-with-lease`) and opens / updates the `upstream-sync →
    main` PR. `concurrency: upstream-sync` prevents two simultaneous
    runs from racing on the branch.

  * `docs/upstream-sync.md`: contributor docs covering branch /
    remote layout, the three conflict categories (rethink-tv-owned,
    structural-we-depend-on, surprise-restructure) and how to handle
    each, environment overrides for the script, and a closing
    section on why drift from upstream is a tier-1 incident
    (security cadence, contribute-back rebase, user trust).

The script is safe to run locally today (it correctly aborts on a
dirty tree or missing remote) and was smoke-tested against
upstream/main = df2eb58 (current upstream HEAD), which matches the
upstream tip our fork is based on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Lets maintainers run `📺 Android TV CI` on demand against any branch
(notably `for-upstream` and `upstream-sync`) without changing the
regular push / PR trigger surface.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Introduces the first real TV-specific UI surface. Until this commit the
`tv` flavor inherited `app/src/full/`'s phone UI wholesale, which
installs cleanly but doesn't surface a launcher icon on Android TV
(the phone activities use `LAUNCHER` only, not `LEANBACK_LAUNCHER`).

Phase 5 MVP scope:
  * a single screen showing protection status (on/off) + the current
    Brave mode (DNS / Firewall / DNS+Firewall), and one large focusable
    button that toggles the VPN;
  * delegates start/stop to the same `VpnService.prepare()` →
    `VpnController.start/stop` flow upstream's phone fragment uses
    (see `HomeScreenFragment.prepareVpnService/startVpnService`), so
    the engine side is untouched.

Build wiring:
  * `org.jetbrains.kotlin.plugin.compose` plugin applied project-wide
    (bundled with Kotlin 2.1.20; classpath dep added in root
    `build.gradle`). Safe to apply globally — phone variants contain
    no `@Composable` and the plugin is a no-op for them.
  * `buildFeatures.compose = true` for the same reason — costs
    nothing on variants without @composable code.
  * Compose stack added as `tvImplementation` only: BOM 2024.12.01
    (Compose runtime 1.7.6, matches the Kotlin 2.1.20 Compose compiler),
    `androidx.tv:tv-material:1.0.0` for TV-styled components,
    `activity-compose`, `lifecycle-runtime-compose`,
    `koin-androidx-compose` (so the UI can pull `PersistentState`
    etc. via `koinInject`).

Manifest:
  * Drops the `manifest.srcFile = src/full/AndroidManifest.xml`
    override and introduces a real `app/src/tv/AndroidManifest.xml`.
    This is necessary because Gradle only allows one manifest per
    source set — we couldn't both redirect to full's manifest AND
    contribute a TV launcher entry.
  * Until upstream's manifest stabilises into something we can
    `tools:replace`-overlay cleanly, the tv manifest is a verbatim
    copy of full's with two additions:
      - `<uses-feature android:name="android.software.leanback"
        required="false" />` (and a matching
        `hardware.touchscreen` declaration) so the Play Store /
        Android TV launcher recognise this APK as a TV app;
      - `<activity com.ezelab.rethinktv.ui.TvHomeActivity ...>` with
        `LEANBACK_LAUNCHER` + `LAUNCHER` intent filters — the
        latter is included so QA on non-TV hardware can still reach
        the activity from the standard launcher.
    Drift management is handled by the existing sync workflow: when
    `upstream/main` modifies `full/AndroidManifest.xml`,
    `scripts/sync-upstream.sh` flags the divergence so a maintainer
    can mirror the change here.

Kotlin (under `app/src/tv/java/com/ezelab/rethinktv/ui/`):
  * `TvHomeActivity` — thin `ComponentActivity` that calls
    `setContent { TvHomeApp() }`. The inherited
    `RethinkDnsApplication` initialises Koin before `onCreate`
    runs, so the composables can resolve singletons immediately.
  * `theme/Theme.kt` — TV-Material 3 dark color scheme. Android TV
    apps are essentially always dark-themed (ten-foot UI), so we lock
    the flavor to dark and don't expose a toggle.
  * `home/HomeScreen.kt` — the single screen. Observes
    `VpnController.connectionStatus` LiveData via
    `observeAsState`, reads `PersistentState.braveMode` for the
    mode label, and uses `rememberLauncherForActivityResult` to
    handle the system VPN-consent dialog. The toggle helper is kept
    as a top-level function rather than a ViewModel — a ViewModel
    layer will land in Phase 6 once we have multiple screens with
    shared state.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Rounds out Phase 5 (`tv-ux-dashboard`) — what was previously a
single-screen toggle is now a three-tab top-bar layout:

  * `Home` — the existing protection-status + start/stop dashboard.
  * `Settings` — Brave-mode selector (DNS / Firewall / DNS+Firewall),
    writing via `AppConfig.changeBraveMode` (not directly to
    `persistentState.braveMode`) so the tunnel-mode observers
    upstream wires up still fire. Runs the write on Dispatchers.IO
    because the observer callbacks may touch the database.
  * `About` — minimal credits / repo pointer.

Implementation notes:

  * State-driven `when`-routing rather than the androidx.navigation
    library — Phase 5 has too few destinations to justify the extra
    dep and the boilerplate around `NavController`. We'll graduate
    when there are nested destinations or back-stack semantics worth
    modelling (likely once Phase 6 settings sub-screens land).

  * `TabRow` from `androidx.tv.material3` — uses TV-styled focus
    behaviour (focus = select, no separate tap-to-confirm), which
    matches the D-pad pattern Android TV users expect.

  * `AppConfig` is pulled via `koinInject` alongside
    `PersistentState`, demonstrating the same DI surface upstream's
    phone fragments use. No bespoke service-locator layer needed
    yet — the engine adapter (planned Phase 4) can stay deferred
    until something concrete forces it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The marquee TV-specific affordance: a tab that lists installed
streaming apps Rethink recognises (Netflix, Disney+, Prime Video,
YouTube, Plex, Jellyfin, Hulu, Max, Paramount+, Peacock, Apple TV,
Spotify, Twitch, Crunchyroll, …) and lets the user toggle each one
between 'Through Rethink' (default; ad-blocking active) and
'Bypass Rethink' (DRM-safe; the app's UID skips the tunnel).

Wires to upstream FirewallManager:
  * reads getApplistObserver() LiveData for the current app set
    and their firewallStatus
  * writes via updateFirewallStatus(uid, EXCLUDE/NONE, ALLOW) on
    Dispatchers.IO, mirroring the phone firewall adapter

KnownStreamers maps known package names to friendly service labels
so the UI says 'Netflix', not 'com.netflix.ninja'. The list is
curated — phone-only and TV-only package SKUs from the same vendor
both map to the same friendly name.

No engine changes; this is purely a new TV-flavour view on top of
existing upstream state.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Because AGP source-sets only allow one manifest per set,
app/src/tv/AndroidManifest.xml has to be a near-verbatim copy of
app/src/full/AndroidManifest.xml rather than a redirect. That means
when upstream adds a new <activity>, <service>, <receiver>, or
<provider> to full's manifest, we have to manually mirror it into
tv's manifest or that component silently disappears on the TV build.

This adds a soft-warning drift detector to scripts/sync-upstream.sh:
it extracts every android:name= value from a manifest-component
element in full's manifest, looks for the same string in tv's
manifest, and prints a yellow ⚠ with the list of missing names if
any are absent. It does NOT fail the sync — sometimes a component
is intentionally TV-omitted (the BootReceiver was on the chopping
block at one point) — but the maintainer sees the list and decides.

Smoke-tested locally: zero drift today, exits clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two bugs caught by the first emulator run.

1. **Streams tab didn't refresh after toggling 'Bypass Rethink'**

   Verified end-to-end: the click fires
   FirewallManager.updateFirewallStatus(uid, EXCLUDE, ALLOW), the
   engine logs 'Apply firewall rule for uid: X, EXCLUDE, ALLOW',
   the database is updated, and the new state is visible on the
   next activity launch — but the row never updates live while the
   activity is alive.

   Root cause is a subtle interaction with upstream's data model:

     - FirewallManager.invalidateFirewallStatus() mutates
       AppInfo.firewallStatus in place on the cached objects.
     - It then posts a NEW List<AppInfo> via
       appInfosLiveData.postValue(snapshotAppInfos()) — but the
       list contains the SAME mutated AppInfo references.
     - LiveData.observeAsState() writes to a mutableStateOf that
       uses structuralEqualityPolicy(). Comparing old list vs new
       list returns 'equal' because the elements are identical
       references (and AppInfo.equals on identical references is
       trivially true regardless of field changes).
     - Compose decides 'no state change' and skips recomposition.

   Fix: subscribe to the LiveData via produceState + a manual
   Observer, and project each emission into an immutable list of
   StreamerView data classes with snapshotted primitive fields. The
   data class structural inequality survives the in-place mutation,
   so Compose recomposes correctly when 'excluded' flips.

   Now the row text and button label flip in real time on every
   toggle, verified on an Android TV emulator (Pixel TV, android-34,
   arm64-v8a).

2. **Initial focus landed on 'Start protection', not the tab row**

   D-pad RIGHT from the cold-launch state appeared to do nothing
   because focus was on the Start protection button (the highest
   focusable in the Home tab's content), which has no rightward
   sibling. Users had to discover D-pad UP to reach the tab row.

   Fix: attach a FocusRequester to the first Tab and request focus
   in a LaunchedEffect(Unit). Now the cold-launch state has the
   Home tab focused, so D-pad RIGHT immediately navigates tabs as
   expected, and D-pad DOWN drops into content.

Both fixes verified on the same emulator run: tabs navigate via
D-pad, Streams toggle updates live, VPN consent flow works end to
end (Protection: ON, 'VPN is connected' system indicator).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the 4-tab Compose-for-TV launcher (Home toggle + Streams bypass
+ brave-mode picker + About) with an 8-destination left-rail navigation
scaffold sized for full upstream-feature parity.

Foundation (Phase A of the full-feature parity plan):

* New tv-material NavigationDrawer + Navigation-Compose NavHost in
  ui/nav/TvNavScaffold.kt, hosting one composable per top-level
  destination — Home / DNS / Firewall / Apps / Proxy / Logs / Stats /
  Settings. Apps subsumes the deleted Streams tab; per-app bypass is a
  first-class case of per-app rules rather than a curated overlay.

* Common helpers in ui/common/:
  * TvScreenScaffold for consistent ten-foot heading + body padding.
  * PlaceholderScreen so under-construction destinations are obvious.
  * rememberAsImmutableState — drop-in observeAsState replacement that
    survives upstream's mutate-in-place LiveData pattern (FirewallManager,
    WireguardManager, ProxyManager). Documented inline; see the
    StreamsContent.kt history for the bug it works around.

* HomeScreen retains the working VPN-consent + start/stop flow. Now
  reads VpnController.hasTunnel() (isOn is @deprecated upstream).

* SettingsScreen retains the brave-mode picker for Phase A so the
  build is functionally usable before Phase I lands.

* Other six destinations stub to PlaceholderScreen — feature parity
  is rolled out per phase (B–I in plan.md).

Build:

* Adds androidx.navigation:navigation-compose:2.8.5 and
  androidx.compose.material:material-icons-extended to the
  tvImplementation block in app/build.gradle (fenced inside the
  existing rethink-tv comment block; phone variants unaffected).
* assembleFdroidTvDebug -> compileFdroidTvDebugKotlin verified locally
  on JDK 17.

Documentation (Phase K of the plan):

* README softens the 'contribute-back-friendly' framing — the
  merge-friendly source-set isolation is reframed as engine/security
  pass-through rather than as a contribution channel.
* docs/upstream-sync.md drops the for-upstream / Phase 9 references;
  merge-conflict playbook keeps only the in-fork shadow remediation.
* origin/for-upstream and the local for-upstream branch are deleted
  (no commits beyond an early flavor-scaffold attempt).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Expand the Home destination from the Phase A status + toggle skeleton
to the full Phase B dashboard:

* Status row: ON/OFF badge (focus-free Surface so it does not steal
  D-pad focus from the toggle), brave-mode pill, and the currently
  connected DNS resolver name (from AppConfig.getConnectedDnsObservable).

* Toggle row: unchanged behaviour — start / stop the tunnel through
  VpnController, with the same VpnService.prepare consent-launcher
  flow upstream HomeScreenFragment uses.

* Counters row: three equal-width cards backed by the same LiveData
  the phone home fragment binds to —
    - DNS queries  (AppConfig.dnsLogsCount)
    - Connections  (AppConfig.networkLogsCount)
    - Blocked 5min (ConnectionTrackerRepository.getBlockedConnectionsCountLiveData)
  Counters dim when protection is off so users see the numbers are
  stale rather than live.

Wiring all LiveData reads route through rememberAsImmutableState (from
common/LiveDataCompose.kt) to dodge the structural-equality recomposition
bug documented for upstream's mutate-in-place observers (FirewallManager,
WireguardManager, ProxyManager). VpnController.connectionStatus is enum-
valued so the safe path costs nothing here either.

formatCount compresses big numbers to 'K'/'M'/'B' tiles so they fit at
ten-foot reading distance.

Build: assembleFdroidTvDebug -> compileFdroidTvDebugKotlin verified
locally on JDK 17.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the Phase A placeholder Firewall destination with the eight
universal-firewall toggles upstream's phone screen exposes (the
ones not requiring the accessibility-service shim, which is what
the Block-when-background toggle needs and Leanback devices don't
provide).

New common widget — SettingToggleRow:
 * Full-row D-pad target (no separate Switch thumb to land on).
 * 44 dp filled-check tile on trailing side, easy to parse at 10 ft.
 * Focused state lifts to primary-container with the standard tv-
   material ClickableSurface focus colour set, so the selection
   ring is glanceable from a sofa.
 * Mirrors PersistentState's getter/setter writes; the setters call
   setUniversalRulesCount() internally so the rule-count badge in
   the screen subtitle stays in sync via the universalRulesCount
   LiveData.

New common widget — SettingSectionHeader: small uppercase divider
between groups of toggle rows, used to break the universal-firewall
list into Connection types / DNS protection / App lifecycle /
Lockdown.

FirewallScreen wires the toggles via koinInject<PersistentState>(),
running setter calls on an IO scope since they hit SharedPreferences.
Reads of universalRulesCount route through rememberAsImmutableState
to dodge the upstream LiveData / Compose identity bug.

Background-mode (BlockAppWhenBackground) is omitted from the TV
surface — the toggle is dependent on an AccessibilityService that
isn't reliably available on Leanback. Filter-IPv4-in-IPv6 is
omitted because upstream itself has it commented out today.

Build: compileFdroidTvDebugKotlin succeeds on JDK 17.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the brave-mode-only Phase A settings stub with the full
TV-appropriate settings surface — the union of upstream's Tunnel,
Misc and Advanced settings activities, filtered to the toggles that
actually make sense on Leanback hardware.

Layout (sections, top-to-bottom):

 * Protection mode — three-button picker, kept first so users land
   on it after the screen heading. Reuses the same AppConfig.changeBraveMode
   path the phone uses; mode persistence is reflected in a check
   mark plus bold weight for accessibility at 10 ft.

 * Tunnel — Allow bypass, LAN traffic, all available networks,
   protocol translation (NAT64 / 6to4).

 * WireGuard — global lockdown, smart persistent keep-alive. Other
   per-tunnel WG settings live with the WG list/detail screens.

 * Reliability — endpoint-independent mapping, TCP keep-alive,
   maximum MTU, stall on no network.

 * Boot — auto-start on boot.

Deliberately omitted from the TV surface for v1:
 * Theme switcher — TV is locked to dark.
 * Biometric / fingerprint App Lock — Leanback devices lack a
   reliable biometric stack. A PIN App Lock destination will land
   later under its own card.
 * Notifications — TVs surface them inconsistently.
 * Locale picker — uses upstream's intent flow, easier as a separate
   dialog in the polish phase.
 * Backup / Restore, Console log — will be dedicated cards.

Writes go through Dispatchers.IO since PersistentState's booleanPref
delegate commits to SharedPreferences synchronously. Reads use plain
property gets — these are var-property-backed prefs, not LiveData,
so the rememberAsImmutableState path isn't needed here.

Build: compileFdroidTvDebugKotlin succeeds on JDK 17.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the Phase A placeholder Apps destination with the per-app
firewall surface — the TV equivalent of upstream's AppListActivity
+ AppInfoActivity pair.

AppsScreen — master grid:
 * LazyVerticalGrid with adaptive 260dp columns. Each card shows the
   app icon (loaded async via PackageManager on IO), the app name,
   the package id, and a one-word status pill summarising the
   (FirewallStatus, ConnectionStatus) pair in the same shorthand
   upstream uses in its rule tooltips.
 * Cards are TV-material ClickableSurfaces — single D-pad target per
   card; pressing center pushes apps/{uid} onto the nav stack.
 * Sort: user apps first (alpha), system apps after.
 * Subscribes via rememberAsImmutableState to the
   FirewallManager.getApplistObserver MutableLiveData — projects each
   AppInfo to an immutable AppRow data class to dodge the mutate-in-
   place identity bug.

AppDetailScreen — single-app editor:
 * Identity header: 96dp icon, current status pill, uid.
 * Five-button segmented selector for FirewallStatus
   (Allow/Block · Bypass universal · Bypass DNS+FW · Isolate · Exclude).
 * Four-button segmented selector for ConnectionStatus
   (Allow · Block all · Block metered · Block Wi-Fi). Disabled with
   inline explanation when FirewallStatus is anything other than
   NONE — matches the upstream semantics where the connection class
   only takes effect on the NONE baseline.
 * Writes via FirewallManager.updateFirewallStatus on Dispatchers.IO
   (same code path upstream's spinners use).

Async-icon loading uses produceState(packageName) with a defensive
Drawable -> Bitmap helper that handles adaptive drawables' zero-
bounds edge-case (falls back to a generic Android glyph for missing
packages — useful when an app uninstalls mid-grid).

Nav: TvNavScaffold gains the apps/{uid} route with an Int navArgument
and passes navController into AppsScreen so cards can push.

Out of scope for v1 and explicitly noted in code: per-app domain
rules, per-app IP rules, per-app proxy mapping. Those will land as
sub-destinations in the polish phase.

Build: compileFdroidTvDebugKotlin succeeds on JDK 17.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the Phase A placeholder DNS destination with a working
resolver picker for the three encrypted DNS protocols TV users
actually pick: DoH, DoT, ODoH.

Layout:

 1. Currently-connected banner at the top, showing the resolver
    name and protocol pill. Same data Home surfaces; repeating it
    here makes the destination self-contained.
 2. Three-button protocol tabs.
 3. Lazy-column of endpoints for the active tab. Each row shows
    name, URL (one-line, ellipsised), and an Active / custom pill
    when applicable.

Read paths: appConfig.getAllDefaultDoHEndpoints() and
getAllDefaultDoTEndpoints() — suspend repository calls dispatched on
IO inside produceState. Connected DNS name routes through
rememberAsImmutableState on appConfig.getConnectedDnsObservable().

Write paths: appConfig.handleDoHChanges(...) and handleDoTChanges(...).
Both are the same suspend functions upstream's spinners invoke; each
removes the prior connection-status flag, marks the new endpoint
isSelected, and pings onDnsChange to re-bootstrap the tunnel. We
bump a reloadKey from the Main dispatcher after the write completes
so the list immediately reflects the new Active pill.

Out of scope for v1 (explicitly noted in the file):
 * Adding a custom DoH/DoT URL (needs a TV-friendly text-entry dialog).
 * DNSCrypt server + relay multi-select (complex flow).
 * DNS Proxy (plain UDP) endpoints (rare on TV).
 * Rethink+ basic blocklist categories (separate sub-flow).
 * Local-blocklist download progress UI.

The ODoH tab is wired through but the list path is left as no-op
pending a custom-add UX (the default ODoH list upstream ships is
empty until the user adds a server).

Build: compileFdroidTvDebugKotlin succeeds on JDK 17.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the Logs destination — connection-level diagnostics for the TV
build. Powered by androidx.paging:paging-compose 3.3.5 (added to the
fenced rethink-tv tvImplementation block), which lets us hand the
upstream PagingSource from ConnectionTrackerDAO and DnsLogDAO
straight into a Compose LazyColumn without recreating any of the
paging pipeline.

Three tabs at the top:

 * Connections — every TCP / UDP connection Rethink has seen, most-
   recent first. DAO orders DESC and caps at MAX_LOGS.
 * DNS queries — every DNS lookup, with query name, type, resolver
   IP, latency (when known).
 * Blocked only — Connections filtered to isBlocked = 1, the most
   common audit view.

Row UX:
 * Each row is its own TV-material ClickableSurface — D-pad walks
   the list naturally, focus colours highlight the current entry.
 * Left chip: green OK / red BLK, parseable at 10 ft.
 * Right: HH:mm:ss timestamp.
 * Body: app name + ip:port, or DNS query name + record type + resolver.

Row click is a no-op placeholder — row-level detail expansion and
the close-connection action will land in the polish phase. Search,
per-app filtering, and the Rethink-log subset are similarly TBD;
none are blocking for v1 viewing.

Build: compileFdroidTvDebugKotlin succeeds on JDK 17 with the new
paging-compose dependency. APK size impact ~150KB after R8.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the Proxy destination placeholder with a real list-driven
screen.

Layout:
 * 'WireGuard tunnels (N)' section — every WgConfigFilesImmutable
   returned by WireguardManager.getAllMappings(). Each card shows
   the tunnel name, an ON/OFF tile, and small pills for the
   secondary flags upstream tracks: catch-all, lockdown, one-WG,
   metered-only. Active tunnels sort to the top.
 * 'Other proxies' section — read-only summary cards for SOCKS5 /
   HTTP / Orbot. SOCKS5 + HTTP show 'host:port' from the
   AppConfig.getSocks5ProxyDetails / getHttpProxyDetails Room
   reads (suspending, dispatched on IO via produceState). Orbot
   shows Active / Not configured from isOrbotProxyEnabled().

Toggling a tunnel card calls WireguardManager.enableConfig() /
disableConfig() — the same path upstream's per-tunnel switch on
the phone uses — so it correctly registers/unregisters the
WireGuard proxy with VpnController and updates the
AppConfig.ProxyProvider state. Writes dispatch on Dispatchers.IO
and bounce reloadKey back through Dispatchers.Main so the list
re-reads after persistence.

WireguardManager.load(false) is invoked once on first composition
in case the screen is entered before VpnService finished its boot
sequence — load() is idempotent.

Deliberately out of scope this commit (all surface to come in
follow-ups):
 * WG add / import (TV file-picker + clipboard).
 * WG detail (peers, allowed IPs, DNS, per-app mapping).
 * SOCKS5 / HTTP / Orbot editor forms (need TV-friendly TextField
   with paste shortcut).
 * TCP proxy + anti-censorship sub-screens.

Build: compileFdroidTvDebugKotlin succeeds on JDK 17.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the Stats destination placeholder with five paged tabs over
the same queries upstream's SummaryStatisticsFragment uses on phone.

Tabs (each is its own Pager fed straight from a DAO PagingSource):
 * Top apps        — StatsSummaryDao.getMostAllowedApps(to)
 * Top domains     — StatsSummaryDao.getMostContactedDomains(to)
 * Top IPs         — ConnectionTrackerDAO.getMostContactedIps(to)
 * Blocked apps    — StatsSummaryDao.getMostBlockedApps(to)
 * Blocked domains — StatsSummaryDao.getMostBlockedDomains(to)

Window selector (1 h / 24 h / 7 d) sits above the tabs and feeds
the 'to' Long the DAOs filter on. Same arithmetic the phone
ViewModel applies, just hoisted to Compose state instead of
LiveData.

Rows:
 * Rank tile (#1, celzero#2 …) on the left for at-a-glance ordering.
 * Primary label: app name / domain / IP (falls back to uid / flag
   when the source has no friendly name).
 * Trailing aggregate: human-readable bytes (e.g. 12.4 MB) when
   totalBytes is available, otherwise the raw connection count.

Each row is a clickable Surface — the click is a no-op placeholder
today; a per-row detail screen (per-app history, per-domain
connections breakdown, alerts feed) will land in the polish phase.

Build: compileFdroidTvDebugKotlin succeeds on JDK 17.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase J — ten-foot UX polish on the Home dashboard. Two
user-visible additions, deliberately scoped tight so the rest of
the polish work (focus traversal sweep, per-row detail screens,
backup/restore via SAF, app lock PIN) can land incrementally
without forcing another monolithic commit.

1. Welcome banner

New `ui/common/Onboarding.kt` exposes
`rememberOnboardingState()` + `WelcomeBanner()`. The banner
sits above the status row on Home, explains in two short
sentences what the user should do next (start the toggle, then
explore the left rail), and dismisses on a single centre-key
press. Dismissal is persisted in a TV-only SharedPreferences
file (rethink_tv_ux / onboarding_seen) so it doesn't pollute
upstream PersistentState — keeps the merge surface for upstream
PersistentState changes at zero.

2. VPN-consent failure feedback

The Home toggle's ActivityNotFoundException catch was a silent
no-op with a TODO pointing at Phase J. Replaced with a
Toast.LENGTH_LONG explaining what happened ('This device can't
show the VPN consent screen…') and suggesting the
typical workaround (sideload a stock VpnDialogs APK). Stripped
TV ROMs that ship without the system VPN consent dialog were
the only reason this catch existed — silent failure was the
worst possible UX there.

Build: compileFdroidTvDebugKotlin + assembleFdroidTvDebug both
succeed on JDK 17; the unsigned debug APKs land under
app/build/outputs/apk/fdroidTv/debug/.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a TV search field and app picker to the Logs screen.
Switch the paging queries to LIKE and uid-aware DAO variants so
Connections, DNS, and Blocked tabs all refresh from Room.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use CustomDomainDAO.getDomainsByUID(Constants.UID_EVERYBODY) with DomainRulesManager.noRule/block/trust for the Domains tab, and CustomIpDao.getRulesByUid(Constants.UID_EVERYBODY) with IpRulesManager.updateNoRule/updateBlock/updateBypass for the IPs tab.

Add a TV Rules destination with Domains/IPs tabs, universal-only scope messaging, status/type pills, and click-to-cycle rule rows. Also pass navController to the existing ProxyScreen route so the TV build compiles.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two new detail screens reached from the Proxy destination, plus the
ProxyScreen card-click rewiring needed to navigate to them.

1. WireGuard tunnel detail (`WgDetailScreen.kt`)

   Reached by tapping any WireGuard tunnel card on the Proxy
   screen. Mirrors the phone's WgConfigDetailActivity:

   * Interface card — addresses, DNS servers, MTU, listen port,
     and the interface's base64 public key (monospace).
   * Tunnel status — top-level Active toggle that calls
     WireguardManager.enableConfig / disableConfig. Was previously
     bound to the list-card click; lifting it here means the list
     card is free to be a navigation gesture.
   * Tunnel behaviour — the four secondary toggles upstream
     tracks: catch-all, lockdown, exclusive (one-WireGuard),
     metered-only. Each calls the matching
     WireguardManager.update*Config suspend setter on IO and
     bounces a reloadKey through Main to re-read the immutable
     mapping.
   * Peers — every Peer in the parsed Config with public key,
     allowed IPs, endpoint, persistent keepalive.

   Peer editing / add-peer / delete are deferred — both need a
   paste-friendly text-entry flow that lands alongside the WG
   import screen.

2. SOCKS5 / HTTP proxy editor (`ProxyEditorScreen.kt`)

   One composable, two routes (`proxy/socks5` and
   `proxy/http`) parameterised by ProxyEditorKind. Replaces the
   read-only summary card with a TV-friendly form:

   * Host, port, optional username, optional password.
   * Save button → AppConfig.updateCustomSocks5Proxy /
     updateCustomHttpProxy. Both internally call
     ProxyEndpointRepository.update + AppConfig.addProxy so the
     engine picks the endpoint up immediately.
   * Disable button — only rendered when the proxy is currently
     enabled — calls AppConfig.removeProxy with the matching
     ProxyType + ProxyProvider.CUSTOM.

   OutlinedTextField from `androidx.compose.material3` is used
   because tv.material3 doesn't ship one. It focuses and pops the
   system IME cleanly on Android TV.

3. Nav routing

   * `ProxyScreen` now takes a NavController; tunnel-card click
     navigates to `wg/{id}`, SOCKS5 / HTTP cards navigate to
     `proxy/socks5` / `proxy/http`. Orbot card is non-clickable
     for now (Orbot enable runs through the Orbot app handshake,
     not via the editor).
   * Three new routes added to TvNavScaffold: `wg/{id}` (Int
     navArg), `proxy/socks5`, `proxy/http`. These layer onto the
     Rules destination added in the same wave.

Build: compileFdroidTvDebugKotlin succeeds on JDK 17.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… WG import

Six new TV-native parity screens covering upstream surface that
wasn't reachable from the wave-1/2 nav:

* Anti-censorship (dial + retry strategy pickers, bound to
  PersistentState.dialStrategy / retryStrategy).
* Pause VPN (5/15/30/60 min presets + live countdown via
  PauseTimer.getPauseCountDownObserver()).
* Console log (paged ConsoleLog with level filter + Clear).
* Stats drill-down — apps route to AppDetail, domains/IPs route
  to a new StatsDetailScreen that reuses
  ConnectionTrackerDAO.getConnectionTrackerByName.
* Custom ODoH endpoint add screen (insertAsync +
  AppConfig.handleODoHChanges).
* WG tunnel import — paste-from-clipboard or SAF-file-picker path
  feeds Config.parse + WireguardManager.addConfig.

Plumbing: three new Settings → Advanced nav rows, ODoH 'Add custom'
button in DnsScreen, '+ Add tunnel' button in ProxyScreen, six new
routes in TvNavScaffold, NavController threaded through
Stats/Dns/Settings screens.

Upstream touch: one additive method
ODoHEndpointDAO.getAllAsList() to mirror the existing DoH/DoT shape;
no behavioural changes to existing methods.

Build: assembleFdroidTvDebug succeeds on JDK 17.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lled Row+rail

Live emulator testing revealed every destination rendered empty — only
the nav rail was visible, and the right pane was completely blank.

Root cause: tv-material 1.0.0's androidx.tv.material3.NavigationDrawer
mis-measures its content slot when the drawer is collapsed. The NavHost
hosted inside content() ended up with zero usable width, so every
destination Composable composed into nothing.

Fix: drop NavigationDrawer + NavigationDrawerItem and lay the scaffold
out as Row { rail-Column ; content-Box }. Items are 56dp circular
Surfaces with selected-state tinting and standard DPAD focus
traversal. The rail is verticalScroll()-able so all 9 top-level
destinations (Home, DNS, Firewall, Apps, Rules, Proxy, Logs, Stats,
Settings) fit at 1080p — previously Stats and Settings were off-screen.

Verified end-to-end on rethink_tv_avd:
- All 9 top-level destinations render their full Compose bodies.
- DPAD navigation between rail items works.
- Sub-routes (settings/anti-censorship, etc.) render correctly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… nav

After NavController.navigate() pushes a sub-screen (proxy -> wg/0,
settings -> settings/anti-censorship, etc.) the old composable is
disposed, focus is naturally lost, and the OS focus engine snaps focus
to the topologically-first focusable in the tree -- which on our
hand-rolled rail is the second rail item ("DNS"). The result is a
confusing visual: the new screen renders correctly with the right rail
item shown 'selected' (primary color), but a *different* rail item is
shown 'focused' (focused container color), so two rail icons appear
highlighted at once.

Anchor focus on the rail item that matches the new route's parent
destination. We map every (sub-)route back to its top-level
TvDestination via the existing 'route or route/' prefix rule, attach a
per-destination FocusRequester to each NavRailItem, and on every route
change request focus on the matching one (after an 80 ms delay so the
rail's selected-state recomposition has settled and the requester is
attached to the new measure pass).

This keeps the rail's focused and selected states in lock-step with the
visible route. The user can still press D-pad RIGHT to enter screen
content -- same as the existing top-level destination UX -- so the
ten-foot rail pattern is preserved.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Compose-for-TV's androidx.tv.material3.Surface intentionally rejects
pointer input because production TV apps are remote-driven. On the
emulator (and any touch-capable dev surface), this means mouse clicks
do nothing, which makes iteration painful.

Add a tiny drop-in wrapper Surface in ui.common.TvSurface that delegates
to the tv-material composable but layers a detectTapGestures
pointerInput on top of the modifier chain so the same onClick fires for
both DPAD CENTER and mouse/touch taps. Focus visuals, scale, glow, and
all other tv-material behavior are preserved unchanged.

Replace 'import androidx.tv.material3.Surface' with
'import com.ezelab.rethinktv.ui.common.Surface' in all 20 TV files; no
call-site changes needed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rootshel and others added 4 commits May 22, 2026 23:20
…ompiling

The project-wide Compose Compiler plugin checks for compose-runtime on
the classpath of every applied variant. With compose-runtime scoped to
tvImplementation only, the phone variant build fails after upstream
adds new Kotlin files (any merge that introduces files the plugin sees).

Expose only the BOM and compose-runtime to base implementation so all
variants satisfy the plugin's classpath check. The remaining Compose UI
stack (ui, foundation, material3, tv-material, navigation-compose,
material-icons-extended, etc.) stays scoped to tvImplementation so
phone variants don't pull the full UI toolkit. Cost to phone APK:
~250 KB of unused compose-runtime classes that never get loaded.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two issues showed up while testing the post-merge build on real
hardware and the emulator:

1) BraveVPNService crashed every cold start with
   'Room cannot verify the data integrity ... Expected: 5a8c... found: 8c86...'.
   The migration chain from an older install brings the DB up to v30
   but the resulting schema doesn't match the v30 entity definitions
   exactly, so checkIdentity() throws after onOpen(). fallbackToDestructive
   doesn't help because the migration itself doesn't fail. Detect the
   error on first open, delete the DB file, and rebuild from the
   prepackaged asset on retry.

2) On Home, after the user pressed Start protection once, focus
   dropped off the now-Stop protection button and any further D-pad
   centre press hit the rail icon behind it (which navigates to DNS
   instead of stopping the VPN). Anchor focus on the toggle with an
   explicit FocusRequester and re-grab it whenever isOn flips. Also
   mark the welcome banner seen as soon as the user starts protection
   so it doesn't keep pushing the toggle off-screen.

Verified on Sony BRAVIA VH1 (Android 12, armeabi-v7a, real upgrade
over the pre-fix install with the stale DB) and on rethink_tv_avd
(Android TV emulator, arm64-v8a). VPN starts cleanly, button focus
survives the Start/Stop flip, and the welcome banner auto-dismisses
once protection is on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Removes everything that's specific to the ezelab/rethink-tv fork
and shouldn't be part of an upstream PR against celzero/rethink-app:

  - README.md restored to upstream's text (the fork-branded rewrite
    is fork-only)
  - .github/workflows/android-tv-release.yml (fork signed-release
    pipeline; upstream has its own release flow)
  - .github/workflows/upstream-sync.yml (fork helper that syncs from
    upstream into this fork)
  - docs/release.md and docs/upstream-sync.md (fork release + sync
    procedure docs)
  - scripts/sync-upstream.sh (the bash helper the sync workflow
    above wraps)

What remains is the actual TV variant: app/src/tv/** (new sources),
app/build.gradle + build.gradle (new `tv` product flavor + Compose
Compiler), three additive DAO overloads in app/src/main/.../database/
that the TV UI consumes, .github/workflows/android-tv.yml (CI that
builds the new TV flavor on every PR), and the two TV-side bugfixes
just shipped (Room identity-mismatch self-heal + Home toggle focus).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@@ -0,0 +1,316 @@
<?xml version="1.0" encoding="utf-8"?>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 59d2c9c — added android:taskAffinity="" to TvHomeActivity so the activity has no shared task affinity, which closes the StrandHogg 1.0 attack surface even though launchMode="singleTask" stays (kept for consistency with upstream's HomeScreenActivity). Mirrors the same pattern upstream already uses on NotificationHandlerActivity and BubbleActivity.

Comment thread app/src/tv/AndroidManifest.xml Dismissed
@@ -0,0 +1,316 @@
<?xml version="1.0" encoding="utf-8"?>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Out of scope of this PR. android:allowBackup="true" is set in app/src/main/AndroidManifest.xml (existing upstream code), not in the TV manifest this PR adds. The <application> element in app/src/tv/AndroidManifest.xml doesn't override the attribute, so it inherits upstream's value. Happy to flip it to false in a follow-up if that's the intent, but it would affect all flavors and seems like a separate decision.

Comment thread app/src/tv/java/com/ezelab/rethinktv/ui/dns/DnsScreen.kt Dismissed
Comment thread app/src/tv/java/com/ezelab/rethinktv/ui/dns/DnsScreen.kt Dismissed
Comment thread app/src/tv/java/com/ezelab/rethinktv/ui/dns/DnsScreen.kt Dismissed
Addresses two mobsfscan findings on PR celzero#2682 review:

  - StrandHogg 1.0 — task hijacking via launchMode="singleTask"
  - StrandHogg 2.0 — task hijacking on Android 9–10

Keeping launchMode="singleTask" matches upstream's HomeScreenActivity
launcher pattern; adding android:taskAffinity="" closes the
StrandHogg attack surface (other apps can't push a malicious activity
on top of this task because the task has no shared affinity).

Mirrors the same pattern upstream already uses on
NotificationHandlerActivity and BubbleActivity.

Verified on Sony BRAVIA VH1 — TV launcher still resolves the leanback
entry point, app launches cleanly with Home dashboard visible.

Co-authored-by: Copilot <223556219+Copilot@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.

2 participants