Ubisam-MobileGit/tasks/031-commit-label-euckr-fallback-http-timeouts.md
choiminyeong c75b348d08 TASK-031: rename History label to Commit, EUC-KR decode fallback, HTTP timeout fix
- '히스토리' label -> '커밋' across the branch-detail tab, code-browser
  history button, and the one permission-denied sentence that used the
  word, matching what that screen actually shows.
- SourceContentClassifier now tries EUC-KR (strictly, same discipline as
  its existing UTF-8 attempt) before giving up, so legacy Korean-encoded
  .cpp/.h source files render instead of failing as UnsupportedEncoding.
  A byte sequence invalid in both charsets still correctly falls through.
- OkHttpTransport's default client is now one shared, longer-timeout
  OkHttpClient (15s/30s/15s) instead of a fresh unconfigured OkHttpClient()
  per call site -- fixes a large commit history/directory listing on this
  app's home-NAS-hosted Gitea being misreported as offline purely because
  it took longer than the previous 10s default to answer, and lets calls
  to the same host reuse one connection pool instead of ~8 separate ones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 23:25:23 +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 — pending Codex/human review.