Ubisam-MobileGit/tasks/031-commit-label-euckr-fallback-http-timeouts.md
choiminyeong f6ac20c8ed TASK-063: task records say what is actually open
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>
2026-09-29 20:25:49 +09:00

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.kt for the shared, longer-timeout OkHttpClient.
  • 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, the feature/history package) 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, never REPLACE, 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 own Charset documentation 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 throwing UnsupportedCharsetException at 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. 0xFF as a lead byte, valid in neither) still correctly falls through to UnsupportedEncoding — 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's ViewModel.Factory, plus AuthSession) constructed its own fresh, default-configured OkHttpClient() — 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) through GiteaRepositoryBrowserApi.authenticated/GiteaHistoryApi.get, both of which catch IOException broadly (this necessarily includes SocketTimeoutException) and wrap it as ApiFailure.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 shared OkHttpClient (15s connect / 30s read / 15s write, versus the previous unconfigured 10s/10s/10s) is now OkHttpTransport's default, replacing the previous OkHttpClient() 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 of UnsupportedEncoding.
  • 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 default OkHttpClient(), 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/.h files 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_denied reworded to say "커밋 내역".
  • SourceContentClassifier.kt: decodeStrictUtf8 replaced by decodeText (tries UTF-8, then EUC-KR, both strict) built on a new shared decodeStrict(bytes, charset) helper; new EUC_KR charset constant.
  • SourceContentClassifierTest.kt: two new tests — EUC-KR-encoded Korean text now classifies as Text with the correctly decoded string; a byte sequence invalid in both UTF-8 and EUC-KR still classifies as UnsupportedEncoding. 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 private sharedOkHttpClient (lazy, 15s/30s/15s timeouts), now OkHttpTransport's default client parameter, replacing the previous bare OkHttpClient().
  • ./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).