No task waits on a Codex review; the redesign tasks are confirmed on the product owner's Z Fold7; TASK-016's leftovers are on hold; no biometric reauthentication. Agent: main (opus) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
10 KiB
TASK-031: "커밋" label rename, EUC-KR decode fallback, HTTP timeout/connection-pooling fix
Status: implemented; all required checks pass; live-installed, awaiting the human product owner's confirmation Owner: Claude Reviewer: Codex
Outcome
Three small, independent fixes reported live in the same testing session: the "히스토리" (History)
tab/button label is now "커밋" (Commit), matching the human product owner's own correct
description of what that screen actually shows; .cpp/.h (and any other legacy-encoded) source
files that use EUC-KR instead of UTF-8 — common in older or Windows-authored Korean C/C++
codebases — now render instead of being rejected as UnsupportedEncoding; and a large repository's
commit history (and, more generally, any slow-but-working request against this app's home-NAS-hosted
Gitea instance) no longer gets misreported as "서버에 연결할 수 없습니다. 네트워크를 확인해
주세요." (can't reach the server) purely because it took longer than the previous 10-second default
timeout to answer.
Scope
- Included:
app/src/main/res/values/strings_branchdetail.xml,strings_codebrowser.xml,strings_history.xml(label/wording changes only);feature/codebrowser/content/SourceContentClassifier.kt(+ matching test) for the EUC-KR fallback;core/network/HttpTransport.ktfor the shared, longer-timeoutOkHttpClient. - Excluded: No change to the actual commit-history or file-browsing functionality themselves — this batch is a label, a decode fallback, and a network-client tuning change, not new features. No attempt to distinguish "genuinely offline" from "slow but reachable" in the error message itself (see Known limitations) — the fix is to stop timing out prematurely, not to redesign the error taxonomy.
Design notes
- Label rename: the human product owner directly confirmed, then asked for, this: the
"History" tab (branch detail) and "History" button (code browser, jumps to a file's commit
history) both said "히스토리"; both are now "커밋" to match what a Gitea user actually expects
that label to mean.
history_permission_denied's sentence ("이 저장소의 히스토리를 볼 권한이 없습니다") was reworded to "커밋 내역을 볼 권한이 없습니다" for the same reason — it's the only other user-facing string in the app that used the word "히스토리" on its own (a full res/values scan confirmed these were the only three occurrences). Internal identifiers (HistoryScreen,HistoryViewModel,history_*resource-name prefixes, thefeature/historypackage) were left alone — renaming those is a much larger, purely-internal refactor with no user-visible benefit, and touching them risked the exact kind of unrelated churn this session's standing "don't change functionality" constraint warns against. - EUC-KR fallback, not a blind fallback: the classifier's own design principle — decode with
CodingErrorAction.REPORT, neverREPLACE, so a byte sequence that isn't cleanly one specific encoding is refused rather than rendered as mojibake — is preserved exactly; a second charset is now tried with the same strict decode, not a permissive one. EUC-KR specifically (not the more complete MS949/CP949 superset some of the same files may technically use) because Android's ownCharsetdocumentation lists EUC-KR as present on every device via bundled ICU data, while CP949/MS949 support is not guaranteed — using it would have risked compiling and passing this project's host-JVM unit tests while throwingUnsupportedCharsetExceptionat runtime on the actual device, which is exactly the kind of gap this project's tests wouldn't have caught without a live device check. A byte sequence invalid in both UTF-8 and EUC-KR (e.g.0xFFas a lead byte, valid in neither) still correctly falls through toUnsupportedEncoding— this is not a catch-all "always render something" fallback, it's specifically shaped around the one legacy encoding this app's real, live-tested repositories actually turned out to use. - HTTP timeouts and connection sharing, one root cause for two symptoms: every one of the
app's ~8
OkHttpTransport()call sites (one per feature'sViewModel.Factory, plusAuthSession) constructed its own fresh, default-configuredOkHttpClient()— default 10-second connect/read/ write timeouts. Traced directly from the live bug report (commit listing on a real, larger-history repository — visible on device as "LGD-P9-CLCI-4K" — reliably produced the generic offline error) throughGiteaRepositoryBrowserApi.authenticated/GiteaHistoryApi.get, both of which catchIOExceptionbroadly (this necessarily includesSocketTimeoutException) and wrap it asApiFailure.Network, which every feature's error mapping renders identically to a truly offline device. This app's actual Gitea host is a home Synology NAS, not a datacenter service, so a legitimately slow-but-working response — a large commit history walk, a directory listing with many entries (the second, related complaint: loading feeling too slow as file counts grow) — was being cut off and misreported before it had a real chance to finish. One sharedOkHttpClient(15s connect / 30s read / 15s write, versus the previous unconfigured 10s/10s/10s) is nowOkHttpTransport's default, replacing the previousOkHttpClient()bare default: longer timeouts give a slow response room to actually complete instead of erroring out early, and because it's now one shared instance (not ~8 independently-constructed ones), OkHttp's own connection pool is reused across every feature's calls to the same host within a session — a second call to the same Gitea host during the same app session can reuse an already-open TCP/TLS connection instead of paying a fresh handshake, which is a genuine (not just perceived) latency win on top of the timeout fix.
Acceptance criteria
- The branch-detail tab and the code-browser's "view this file's history" button both read "커밋" instead of "히스토리".
history_permission_denied's message no longer contains the word "히스토리".- A
.cpp/.h(or any text) file encoded in EUC-KR, not UTF-8, renders as text instead ofUnsupportedEncoding. - A byte sequence invalid in both UTF-8 and EUC-KR still correctly classifies as
UnsupportedEncoding— the fallback doesn't swallow genuinely unrecognized encodings. OkHttpTransport's default client now uses longer, explicit timeouts (15s connect / 30s read / 15s write) instead of an unconfigured defaultOkHttpClient(), and is shared (one instance, one connection pool) across every call site instead of one fresh client per call site.- Build, unit tests, and lint pass.
- Live confirmation from the human product owner that the "LGD-P9-CLCI-4K" repository's commit
history now loads instead of failing, that
.cpp/.hfiles open correctly, and that the "커밋" label reads correctly — the build has been reinstalled on the device but not yet re-tested against those exact cases in this session.
Required checks
./gradlew compileDebugKotlin compileDebugUnitTestKotlin
./gradlew testDebugUnitTest
./gradlew assembleDebug lintDebug
Evidence
strings_branchdetail.xml/strings_codebrowser.xml:히스토리→커밋.strings_history.xml:history_permission_deniedreworded to say "커밋 내역".SourceContentClassifier.kt:decodeStrictUtf8replaced bydecodeText(tries UTF-8, then EUC-KR, both strict) built on a new shareddecodeStrict(bytes, charset)helper; newEUC_KRcharset constant.SourceContentClassifierTest.kt: two new tests — EUC-KR-encoded Korean text now classifies asTextwith the correctly decoded string; a byte sequence invalid in both UTF-8 and EUC-KR still classifies asUnsupportedEncoding. The two pre-existing "malformed UTF-8" tests were checked (not just left alone) to confirm their specific byte sequences are also invalid under EUC-KR's own lead/trail-byte rules, so they still correctly exercise the "both attempts fail" path rather than accidentally now succeeding via the new fallback.HttpTransport.kt: new privatesharedOkHttpClient(lazy, 15s/30s/15s timeouts), nowOkHttpTransport's defaultclientparameter, replacing the previous bareOkHttpClient()../gradlew compileDebugKotlin compileDebugUnitTestKotlin: BUILD SUCCESSFUL../gradlew testDebugUnitTest: BUILD SUCCESSFUL, all unit tests passing (including the two new ones above)../gradlew assembleDebug lintDebug: BUILD SUCCESSFUL, 0 lint errors.- Installed on the human product owner's device over wireless
adb(adb install -r, streamed install succeeded); not yet re-tested live against the specific repository/files that originally reported these bugs in this session.
Known limitations
- The error message itself still doesn't distinguish "genuinely offline" from "reachable but slow" — a request that's still too slow even at the new 30-second read timeout (or a device that's truly offline) both still render as the same "서버에 연결할 수 없습니다" message. The fix here is to stop the common case of a merely-slow-but-working NAS response from being misclassified, not a full error-taxonomy redesign.
- EUC-KR only, not the larger CP949/MS949 superset — a file using a Hangul syllable or symbol only
representable in CP949's extended completion range (not present in the older EUC-KR/KS X 1001
set) will still classify as
UnsupportedEncoding. This was a deliberate choice given Android's charset-availability guarantees (see Design notes), not an oversight. - No caching or ETag/conditional-request support was added — the connection-pool sharing reduces handshake overhead for repeated calls to the same host, but each call still does a full request/ response round trip; a directory or commit list re-opened a second time still re-fetches from scratch.
- Live confirmation against the exact repository and files that originally reported these bugs is still pending in this session (see Acceptance criteria).
Review findings
None yet — no Codex review pending (Codex retired, 2026-09-29).