Skip to content

fix: P0/P1/P2 audit fixes, CI verification build, and developer docs - #4

Merged
GothwadTech merged 1 commit into
mainfrom
arena/e06d9c5d-launcher
Oct 13, 2026
Merged

GothwadTech merged 1 commit into
mainfrom
arena/e06d9c5d-launcher

Conversation

@GothwadTech

Copy link
Copy Markdown
Owner

What does this change?

Fixes every finding from the full repository audit (P0 → P2), adds the CI job that actually verifies a pull request compiles, and creates the developer documentation the project was missing.

73 files changed, +4359 / −1099.

Why?

The launcher shipped with several defects that only show up on real TV firmware: a UI Scale setting that did nothing, an invisible notification dot, PackageManager work on the main thread, a watchdog that hijacked the screen before first unlock, app locks that failed open on cold start, the hidden-apps reveal code stored in plain text, an ANR every time Settings was opened (a su -c id probe on the main thread), and two workflows both publishing GitHub Releases with colliding tags. There was also no CI that built release (minified) code, no LICENSE file despite the README declaring Apache-2.0, and no documentation beyond a 132-line README.

What's in it

Correctness & stability

  • DensityAdapter derives density from a 960 dp design width × UI_SCALES[uiScale]; the default moved to index 2 (1.0×) and the activity recreates when the effective scale changes.
  • AppRepository.scan() pins itself to Dispatchers.IO; validCacheNames is a synchronised set; memoryCache is @Volatile and null-safe.
  • Grid can be empty again (rebuildAppList() early return removed); notification dot visible.
  • App lock fails closed: until the first DataStore emission lands, the foreground package is parked in pendingEnforcementPkg and re-checked.
  • Watchdog/boot-shield shared state moved to device-protected storage, relaunch gated on a persisted launchOnBoot || isDefaultHomeApp policy, and both paths relaunch HOME through one homeIntent() / launchHomeCompat that survives Android 10+ BAL rules.
  • BootReceiver returns immediately (no guard loop inside goAsync()); getRunningAppProcesses() no longer treated as authoritative; aggressive memory trim is opt-in; corrupt config JSON is logged before falling back to defaults.
  • Actions.openAppInfo / uninstall set FLAG_ACTIVITY_NEW_TASK via a shared helper — they silently did nothing from non-Activity contexts because runCatching swallowed the throw.

Security & privacy

  • Reveal code → salted PBKDF2, constant-time compare, not echoed into the settings field, plain-text codes migrated on first launch.
  • BLUETOOTH_CONNECT requested when the Quick Dashboard opens; on denial it reports unknown rather than a made-up remote name and battery.
  • AppUpdateManager: bounded runShell, InstallCapability probing off the main thread (async "Checking…" badge), redirect-following download that rejects non-http(s) targets, failure on non-2xx, disconnect() in finally, progress dispatched only when the integer percent changes.

Removed — dead config fields (categories, sections, knownApps, autoCategoryOnInstall, iconScale, showCategoryNames, showAppLabels, layout, vpnApp, showVpnButton), CategoryAssigner + its test, layout_a11y_banner.xml, 8 unused drawables, cd_vpn, 9 dead strings. Legacy configs still decode (ignoreUnknownKeys), locked in by a test.

CI / build

  • New ci.yml: every PR and push to main runs two Python static checks, then assembleDebug + assembleRelease (minified) + testDebugUnitTest + lintDebug; debug APK and reports as artifacts.
  • release.yml (tags/manual) is now the only publisher; build_apk.yml is manual and artifact-only — no more duplicate releases or v1.0.<run_number> collisions.
  • Composite actions resolve-version (versionCode = MA*10000 + MI*100 + PA) and prepare-signing (keystore from secrets, alias auto-detect, deleted if: always()). Gradle signs both artifacts; the jarsigner/apksigner sign steps are gone (jarsigner produced v1-only signatures for the AAB).
  • build.gradle.kts: signing env via providers.environmentVariable (configuration-cache safe), -PlocalSign → debugConfig, unitTests.isReturnDefaultValues = true.
  • Each workflow creates ~/.android/debug.keystore — AGP does not auto-generate one for a custom signing config, so without it every build failed.

Tests — ConfigSerializationTest (5) and LockSecurityTest (10).

Docs — docs/: ARCHITECTURE, CONFIG (field-by-field reference), SECURITY, PERFORMANCE, CI_RELEASE, TESTING, TROUBLESHOOTING, I18N, ISSUE_LOG, ROADMAP, baseline-profile, index. Plus CHANGELOG.md, CONTRIBUTING.md, LICENSE, issue + PR templates. README corrected (five scrim modes, not three; Quick Dashboard entry point; real CI table; docs index) and AGENTS.md extended with the rules that each cost a real bug.

Tools — tools/ktcheck.py (a real Kotlin scanner: nested block comments, strings, ${} templates) and tools/check_resources.py (every R.* / @type/name resolves). Both run in CI before Gradle, and both are usable by a contributor with no Android SDK.

How did you verify it?

  • Device / firmware: none available — this environment has no JDK and no Android SDK, and no network route to google() / mavenCentral(), so a local Gradle build is impossible.
  • Build installed: n/a — the CI job on this PR is the compile check (that is why ci.yml is part of the change rather than a follow-up).
  • What I ran instead:
    • python3 tools/ktcheck.py app/src → 56 files, 0 problems (brace/paren/comment/string/template balance, including every file edited here).
    • python3 tools/check_resources.py app/src/main → 0 broken references (15 drawables, 109 strings, 19 layouts, 52 styles, 323 ids, …).
    • YAML parse of all workflows, composite actions and issue forms; every job step has run or uses; issue form body ≤ 12 items (GitHub's limit).
    • Markdown link check across the repo → 0 broken links (this is how the pre-existing docs/baseline-profile.md reference in the README was caught and written).
    • Cross-checked docs/ISSUE_LOG.md against git grep so it lists exactly the issue numbers the repository references (7, 15, 19, 26, 27, 28, 30, 31, 34, 35, 36, 38, 44, 45) and no invented ones.
  • Unit tests added / run: 15 tests added; not executed here (no JVM) — CI runs them.

Deliberately not done (documented instead)

  • sheet_settings.xml (2 110 lines, ~200 views) is not split. ViewBinding does not expose views from an id-less <include>, so splitting rewrites hundreds of binding.* references across the fragment and all six delegates with no compiler available to check the result. Scoped as a standalone PR in docs/ROADMAP.md.
  • ~260 hard-coded strings remain. ~29 were extracted (locks, a11y alert, search, dashboard, settings titles/subtitles, version, self-update badge); the rest are inventoried per file in docs/I18N.md with the mechanical process to finish. Bulk-editing that many references with no compile check is how you ship a first-frame Resources$NotFoundException.
  • No APK checksum verification after download. HTTPS + the installer's own signature check is the current guarantee; publishing a SHA-256 per release is roadmap item 3.
  • metadata.json left untouched (platform metadata, not app config) and documented as such.

Checklist

  • No Jetpack Compose — no androidx.compose.*, no @Composable, no ComposeView, no buildFeatures { compose = true }
  • No blocking work added to the main thread (three instances removed)
  • Only the main process touches ConfigStore; cross-process state goes through ProcessHeartbeat in device-protected storage
  • Security decisions stay in their single owner (AppLockGate, the a11y service, LockSecurity) and fail secure
  • No invented device state — unknown is reported as unknown
  • New/changed LauncherConfig fields documented in docs/CONFIG.md
  • CHANGELOG.md updated under Unreleased
  • [~] User-visible strings are resources — 29 extracted, 260 inventoried in docs/I18N.md
  • Newer-than-API-21 calls are guarded (PendingIntent.FLAG_IMMUTABLE behind SDK_INT >= M)
  • ./gradlew assembleDebug testDebugUnitTest lintDebug — cannot run here; CI covers it
  • ./gradlew assembleRelease — cannot run here; CI covers it
  • Docs invalidated by this change were updated (README, AGENTS.md, and all of docs/)

Notes for the reviewer

  • Watch the CI job first. It is the only compile check this change has had. If assembleDebug fails, the most likely culprits are the largest hand-edited files: AppUpdateManager.kt, SettingsBottomSheetFragment.kt (its updateAdbStatusBadge was replaced by line-range slicing because the old code contained emoji variation selectors), and Config.kt.
  • lint still runs with abortOnError = false — flipping it is roadmap item 2, after the string work reduces the HardcodedText volume.
  • LICENSE is the canonical Apache-2.0 text with Copyright 2026 Gothwad Tech in the appendix, added because the README already declared that license but the file did not exist. Change or drop it if a different license or copyright holder is intended.
  • Release publishing is now tag-driven only. To cut a release: git tag v1.0.7 && git push origin v1.0.7 (needs RELEASE_KEYSTORE_BASE64, KEYSTORE_PASSWORD, KEY_PASSWORD, optional KEY_ALIAS).

Fixes every finding from the repository audit, adds the CI that verifies a
pull request actually compiles, and documents the project.

Correctness / stability
- UI Scale now scales: DensityAdapter derives density from a 960 dp design
  width x UI_SCALES[uiScale]; default moved to the neutral index 2 (1.0x).
- AppRepository.scan() pins itself to Dispatchers.IO; validCacheNames is a
  synchronised set; memoryCache is @volatile and null-safe.
- Status bar notification dot is visible again (was drawn with zero alpha).
- rebuildAppList() can produce an empty grid (early return removed).
- App lock fails closed on cold start: configLoaded parks the foreground
  package and re-checks it once the first DataStore emission lands.
- Watchdog/boot shield: shared state moved to device-protected storage, a
  persisted relaunch policy is honoured, and both relaunch HOME through a
  single homeIntent()/launchHomeCompat that survives Android 10+ BAL rules.
- BootReceiver returns immediately instead of running the guard loop inside
  goAsync(); getRunningAppProcesses() is no longer treated as authoritative.
- Aggressive memory trim is opt-in and off by default.
- Corrupt config JSON is logged before falling back to defaults.
- Actions.openAppInfo/uninstall set FLAG_ACTIVITY_NEW_TASK via a shared
  helper, so they work from a service/receiver/overlay context.

Security / privacy
- Hidden-apps reveal code is stored as a salted PBKDF2 hash, compared in
  constant time, and no longer echoed into the settings field; existing
  plain-text codes are migrated on first launch.
- BLUETOOTH_CONNECT is requested when the Quick Dashboard opens; on denial
  the panel reports "unknown" instead of a placeholder value.
- AppUpdateManager: bounded runShell, InstallCapability probing off the main
  thread (the Settings badge no longer ANRs on su), redirect-following
  download that rejects non-http(s) targets, failure on non-2xx, disconnect
  in finally, and progress dispatched only when the integer percent changes.

Removed
- Dead config fields (categories, sections, knownApps, autoCategoryOnInstall,
  iconScale, showCategoryNames, showAppLabels, layout, vpnApp,
  showVpnButton), CategoryAssigner + its test, layout_a11y_banner.xml, 8
  unused drawables, cd_vpn and 4 banner strings, 5 dead strings.

CI / build
- New ci.yml: every PR and push to main builds debug AND minified release,
  runs unit tests and lint, and runs two Python static checks first.
- release.yml (tags/manual) is the only publisher; build_apk.yml is manual
  and artifact-only - no more duplicate releases or colliding tags.
- Composite actions resolve-version (versionCode = MA*10000+MI*100+PA) and
  prepare-signing (keystore from secrets, alias auto-detect, always cleaned).
- Gradle signs both artifacts; jarsigner/apksigner sign steps removed.
- build.gradle.kts reads signing env through providers.environmentVariable
  (configuration-cache safe), -PlocalSign uses debugConfig, unit tests return
  default values for unmocked framework calls.

Tests
- ConfigSerializationTest (5) incl. "keys removed in a newer version are
  ignored" and "default ui scale is neutral"; LockSecurityTest (10) covering
  hashing, verification, legacy migration, reveal codes and the throttle.

Docs & repo hygiene
- docs/: ARCHITECTURE, CONFIG, SECURITY, PERFORMANCE, CI_RELEASE, TESTING,
  TROUBLESHOOTING, I18N, ISSUE_LOG, ROADMAP, baseline-profile + an index.
- CHANGELOG.md, CONTRIBUTING.md, LICENSE (README declared Apache-2.0 but the
  file was missing), issue and PR templates.
- README updated: five scrim modes, Quick Dashboard entry point, real CI
  table, docs index; AGENTS.md extended with the rules that each cost a bug.
- tools/ktcheck.py and tools/check_resources.py: scanner-based Kotlin balance
  check and resource cross-reference check, for contributors with no SDK.

Verified with tools/ktcheck.py (56 files, 0 problems) and
tools/check_resources.py (0 broken references). No local Gradle build is
possible in this environment, so the CI job on this PR is the compile check.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
@GothwadTech
GothwadTech merged commit 05969d4 into main Oct 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant