Repository navigation
fix: P0/P1/P2 audit fixes, CI verification build, and developer docs - #4
Merged
Merged
Conversation
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>
3 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 idprobe 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
DensityAdapterderives 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 toDispatchers.IO;validCacheNamesis a synchronised set;memoryCacheis@Volatileand null-safe.rebuildAppList()early return removed); notification dot visible.pendingEnforcementPkgand re-checked.launchOnBoot || isDefaultHomeApppolicy, and both paths relaunch HOME through onehomeIntent()/launchHomeCompatthat survives Android 10+ BAL rules.BootReceiverreturns immediately (no guard loop insidegoAsync());getRunningAppProcesses()no longer treated as authoritative; aggressive memory trim is opt-in; corrupt config JSON is logged before falling back to defaults.Actions.openAppInfo/uninstallsetFLAG_ACTIVITY_NEW_TASKvia a shared helper — they silently did nothing from non-Activity contexts becauserunCatchingswallowed the throw.Security & privacy
BLUETOOTH_CONNECTrequested when the Quick Dashboard opens; on denial it reports unknown rather than a made-up remote name and battery.AppUpdateManager: boundedrunShell,InstallCapabilityprobing off the main thread (async "Checking…" badge), redirect-following download that rejects non-http(s)targets, failure on non-2xx,disconnect()infinally, 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
ci.yml: every PR and push tomainruns two Python static checks, thenassembleDebug+assembleRelease(minified) +testDebugUnitTest+lintDebug; debug APK and reports as artifacts.release.yml(tags/manual) is now the only publisher;build_apk.ymlis manual and artifact-only — no more duplicate releases orv1.0.<run_number>collisions.resolve-version(versionCode = MA*10000 + MI*100 + PA) andprepare-signing(keystore from secrets, alias auto-detect, deletedif: always()). Gradle signs both artifacts; thejarsigner/apksigner signsteps are gone (jarsignerproduced v1-only signatures for the AAB).build.gradle.kts: signing env viaproviders.environmentVariable(configuration-cache safe),-PlocalSign→debugConfig,unitTests.isReturnDefaultValues = true.~/.android/debug.keystore— AGP does not auto-generate one for a custom signing config, so without it every build failed.Tests —
ConfigSerializationTest(5) andLockSecurityTest(10).Docs —
docs/: ARCHITECTURE, CONFIG (field-by-field reference), SECURITY, PERFORMANCE, CI_RELEASE, TESTING, TROUBLESHOOTING, I18N, ISSUE_LOG, ROADMAP, baseline-profile, index. PlusCHANGELOG.md,CONTRIBUTING.md,LICENSE, issue + PR templates. README corrected (five scrim modes, not three; Quick Dashboard entry point; real CI table; docs index) andAGENTS.mdextended with the rules that each cost a real bug.Tools —
tools/ktcheck.py(a real Kotlin scanner: nested block comments, strings,${}templates) andtools/check_resources.py(everyR.*/@type/nameresolves). Both run in CI before Gradle, and both are usable by a contributor with no Android SDK.How did you verify it?
google()/mavenCentral(), so a local Gradle build is impossible.ci.ymlis part of the change rather than a follow-up).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, …).runoruses; issue form body ≤ 12 items (GitHub's limit).docs/baseline-profile.mdreference in the README was caught and written).docs/ISSUE_LOG.mdagainstgit grepso 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.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 ofbinding.*references across the fragment and all six delegates with no compiler available to check the result. Scoped as a standalone PR indocs/ROADMAP.md.docs/I18N.mdwith the mechanical process to finish. Bulk-editing that many references with no compile check is how you ship a first-frameResources$NotFoundException.metadata.jsonleft untouched (platform metadata, not app config) and documented as such.Checklist
androidx.compose.*, no@Composable, noComposeView, nobuildFeatures { compose = true }ConfigStore; cross-process state goes throughProcessHeartbeatin device-protected storageAppLockGate, the a11y service,LockSecurity) and fail secureLauncherConfigfields documented indocs/CONFIG.mdCHANGELOG.mdupdated under Unreleaseddocs/I18N.mdPendingIntent.FLAG_IMMUTABLEbehindSDK_INT >= M)./gradlew assembleDebug testDebugUnitTest lintDebug— cannot run here; CI covers it./gradlew assembleRelease— cannot run here; CI covers itdocs/)Notes for the reviewer
assembleDebugfails, the most likely culprits are the largest hand-edited files:AppUpdateManager.kt,SettingsBottomSheetFragment.kt(itsupdateAdbStatusBadgewas replaced by line-range slicing because the old code contained emoji variation selectors), andConfig.kt.lintstill runs withabortOnError = false— flipping it is roadmap item 2, after the string work reduces theHardcodedTextvolume.LICENSEis the canonical Apache-2.0 text withCopyright 2026 Gothwad Techin 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.git tag v1.0.7 && git push origin v1.0.7(needsRELEASE_KEYSTORE_BASE64,KEYSTORE_PASSWORD,KEY_PASSWORD, optionalKEY_ALIAS).