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

149 lines
10 KiB
Markdown

# 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
- [x] The branch-detail tab and the code-browser's "view this file's history" button both read
"커밋" instead of "히스토리".
- [x] `history_permission_denied`'s message no longer contains the word "히스토리".
- [x] A `.cpp`/`.h` (or any text) file encoded in EUC-KR, not UTF-8, renders as text instead of
`UnsupportedEncoding`.
- [x] A byte sequence invalid in both UTF-8 and EUC-KR still correctly classifies as
`UnsupportedEncoding` — the fallback doesn't swallow genuinely unrecognized encodings.
- [x] `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.
- [x] 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
```text
./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.