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

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 — no Codex review pending (Codex retired, 2026-09-29).