This file's cross-review was explicitly skipped when it was written — PROGRESS_ANDROID.md says so — and it had no production call sites until this session, so nothing had ever reviewed OR executed it. Wiring it to the home screen finally made an adversarial concurrency review worthwhile. It confirmed the hard parts are right (the check-then-insert dedup critical section, withPermit's exactly-once release with the fallback outside it, the NonCancellable finally, and rethrowing CancellationException) and then found five real defects. Every premise was re-verified from the termux v0.118.0 bytecode before fixing. 1. Thumbnails drew DUPLICATED ROWS. TerminalBuffer.externalToInternalRow is `(mScreenFirstRow + row) % mTotalRows` and mTotalRows is the constructor's transcriptRows, so passing TERMINAL_TRANSCRIPT_ROWS_MIN (100) while rows went up to 200 aliased external rows 100..199 onto 0..99 — and left the emulator's scroll state invalid during append(). A pure ThumbnailGrid now derives transcriptRows = max(rows, MIN). 2. A legitimately-sized session was LMK bait. cols/rows are server-supplied and the protocol validates resize only to [1,1000], so a 1000x1000 session forced 2800x2800 ARGB_8888 = 31.4 MB, twice over for two permits, every poll tick. Rather than clamp the grid — which would CROP a real screen — the cell size is now derived from the target, shrinking the font's own cell by an exact integer fraction with the aspect preserved. Any geometry in 1..1000 now yields at most 480x480 = 900 KiB; a real 161x50 goes from 3.1 MB to 322 KiB. 3. Two stalled fetches wedged the feature PERMANENTLY. There is no OkHttp callTimeout (the per-socket read timeout is reset by every trickled byte) and the render is deliberately uncancellable by any caller, so two slow-trickle previews held both permits until close() and no thumbnail rendered again. A withTimeout now sits inside withPermit, budgeting the work rather than the permit wait. The subtlety that makes this correct: TimeoutCancellationException IS a CancellationException, so it is caught BEFORE that branch and routed to the placeholder — otherwise a slow host would still kill the pipeline instead of degrading. 4. The in-flight entry was leakable, contradicting a comment that claimed it never was. `scope.async` on an already-cancelled scope never runs its body, so the finally never ran: a post-close() request left a dead Deferred that every later request awaited forever, and inFlight grew unbounded. Now guarded inside the mutex, and the comment names the one path that genuinely cannot release (where the map dies with the pipeline anyway). 5. Obsolete renders did guaranteed wasted work. lastOutputAt advances on every PTY byte and the list polls every 5s, so an active session mints a new key each tick; the row cancelled the old caller but the old render ran a full GET plus a full raster whose bitmap was cached under a key nothing would request again — occupying both permits ahead of the key actually on screen, and growing without bound once render latency exceeded arrival rate. A newer key now cancels the older in-flight render for that session. Also made two false statements true rather than leaving them: retainOnly now prunes under the same monitor publish() takes, so a render landing after the prune cannot resurrect a killed session's bitmap; and PreviewCap's KDoc no longer claims to prevent an oversized-body OOM (the body is already buffered and parsed by then) nor to "mirror the server's cap" (the server defaults to 24 KiB, not 256 KiB, and an operator can raise it unbounded). RED was observed for all four behavioural fixes before implementing. Verified: ./gradlew test :app:testDebugUnitTest :app:assembleDebug koverVerify -> BUILD SUCCESSFUL, 901 JVM tests, 0 failures (893 -> 901). The agent distrusted a FROM-CACHE green, forced a real execution, and confirmed the code reached the packaged APK by scanning its dex.
46 lines
676 B
Plaintext
46 lines
676 B
Plaintext
# Gradle
|
|
.gradle/
|
|
build/
|
|
**/build/
|
|
!gradle/wrapper/gradle-wrapper.jar
|
|
!src/**/build/
|
|
|
|
# Gradle caches / config-cache
|
|
.gradle/configuration-cache/
|
|
|
|
# IDE — IntelliJ IDEA / Android Studio
|
|
.idea/
|
|
*.iml
|
|
*.ipr
|
|
*.iws
|
|
captures/
|
|
.navigation/
|
|
|
|
# Local machine config (never commit)
|
|
local.properties
|
|
|
|
# Android build outputs (relevant once the SDK-gated modules are enabled)
|
|
*.apk
|
|
*.aab
|
|
*.ap_
|
|
*.dex
|
|
release/
|
|
proguard/
|
|
|
|
# Secrets — never commit
|
|
google-services.json
|
|
**/google-services.json
|
|
*.keystore
|
|
*.jks
|
|
*.p12
|
|
service-account*.json
|
|
|
|
# OS cruft
|
|
.DS_Store
|
|
|
|
# Kover / test reports
|
|
**/kover/
|
|
|
|
# Kotlin compiler session/error scratch (KGP 2.x writes this at the project root)
|
|
.kotlin/
|