250 고침 2: Android 에서 덮개가 뜨자마자 걷히던 것 - 덮개는 사용자가 옮길 때만 걷음 · 기록의 예외 이름 빼기

a4 의 932 기기 확인 (노트 10+): 작업 기록에 "웹 페이지를 열지 못함 (…): net::ERR_TUNNEL_CONNECTION_FAILED" 는 남는데
덮개가 보이지 않았다 (버그 수정).
- 원인: Android 는 본문 오류 (onReceivedError) 뒤에 엔진의 오류 페이지를 또 읽어 진행률이 0 → 100 으로 한 번 더 돈다.
  "새로 읽기 시작 (진행률이 1 아래로)" 에서 덮개를 걷어, 뜨자마자 걷혔다. Windows (Edge) 는 이 단계가 없어 통과했다.
- 덮개는 사용자가 옮길 때만 걷는다: 주소 · 뒤로 · 앞으로 · 새로고침 · [다시 시도] (WebNav 를 감싸 알림 - _NavWatch).
  덮개가 웹 뷰를 덮고 있어 그 밖의 길로는 옮겨지지 않는다.
- 덮개를 띄우면 작업 기록에 "DNS 안내 덮개를 띄움 (…)" - 기기에서 "띄웠는데 안 보임" 과 "띄우지 않음" 을 가리게.
- DNS 실패 글에 예외 이름 ("TimeoutException:") 을 빼고 이유만 (a4 932).
- 시험: Android 순서 (본문 오류 → 오류 페이지 진행률 0 → 100) 뒤에도 덮개가 남음. 실제 Edge 통합 시험 다시 통과.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
통합
RyuJongJin 2026-10-12 07:25:27 +09:00
parent 0b0f2617ae
commit f7d73da301
6 changed files with 76 additions and 8 deletions

View File

@ -164,6 +164,7 @@
"DNS 서버가 {0} 의 주소를 찾지 못해 {1}": "The DNS server couldn't find the address of {0}, so {1}",
"DNS 설정: {0}": "DNS setting: {0}",
"DNS 실패 ({0}): {1}": "DNS failure ({0}): {1}",
"DNS 안내 덮개를 띄움 ({0} · DNS {1})": "Showed DNS notice over the page ({0} · DNS {1})",
"DNS: 기기 DNS → 직접 지정 (8.8.8.8 · 8.8.4.4). 앱의 인터넷 연결 (브라우저 · 다운로드 · 업데이트) 은 이 서버로 주소를 찾습니다. 되돌리기: 환경 설정 > 네트워크 · DNS > 시스템 DNS (끄기) 회사 · 학교망처럼 8.8.8.8 이 막힌 곳에서는 닿지 않는다는 안내의 [시스템 DNS 로 바꾸기] 를 누르세요.": "DNS: device DNS → custom (8.8.8.8 · 8.8.4.4). The app's internet connections (browser · downloads · updates) now look up addresses with these servers. To undo: Settings > Network · DNS > System DNS (off) On networks that block 8.8.8.8 (such as company or school networks), press [Switch to system DNS] in the \"can't reach\" notice.",
"DOC · DOCX · HWP · XLS · XLSX 를 PDF 로 바꿔 보기 (변환기를 받음)": "View DOC · DOCX · HWP · XLS · XLSX converted to PDF (downloads a converter)",
"DoH 서버 ({0}) 가 HTTP {1} 로 답했습니다": "The DoH server ({0}) answered HTTP {1}",

View File

@ -164,6 +164,7 @@
"DNS 서버가 {0} 의 주소를 찾지 못해 {1}": "DNS サーバーが {0} のアドレスを見つけられないため、{1}",
"DNS 설정: {0}": "DNS 設定: {0}",
"DNS 실패 ({0}): {1}": "DNS 失敗 ({0}): {1}",
"DNS 안내 덮개를 띄움 ({0} · DNS {1})": "ページに DNS 案内を表示 ({0} · DNS {1})",
"DNS: 기기 DNS → 직접 지정 (8.8.8.8 · 8.8.4.4). 앱의 인터넷 연결 (브라우저 · 다운로드 · 업데이트) 은 이 서버로 주소를 찾습니다. 되돌리기: 환경 설정 > 네트워크 · DNS > 시스템 DNS (끄기) 회사 · 학교망처럼 8.8.8.8 이 막힌 곳에서는 닿지 않는다는 안내의 [시스템 DNS 로 바꾸기] 를 누르세요.": "DNS: 端末の DNS → 直接指定 (8.8.8.8 · 8.8.4.4)。アプリのインターネット接続 (ブラウザ · ダウンロード · 更新) はこのサーバーでアドレスを調べます。元に戻す: 設定 > ネットワーク · DNS > システム DNS (オフ) 会社 · 学校のネットワークなど 8.8.8.8 がブロックされている所では、接続できないという案内の [システム DNS に切り替える] を押してください。",
"DOC · DOCX · HWP · XLS · XLSX 를 PDF 로 바꿔 보기 (변환기를 받음)": "DOC・DOCX・HWP・XLS・XLSX を PDF に変換して表示 (変換ツールをダウンロード)",
"DoH 서버 ({0}) 가 HTTP {1} 로 답했습니다": "DoH サーバー ({0}) が HTTP {1} を返しました",

View File

@ -164,6 +164,7 @@
"DNS 서버가 {0} 의 주소를 찾지 못해 {1}": "DNS 服务器找不到 {0} 的地址,因此{1}",
"DNS 설정: {0}": "DNS 设置: {0}",
"DNS 실패 ({0}): {1}": "DNS 失败 ({0}): {1}",
"DNS 안내 덮개를 띄움 ({0} · DNS {1})": "已在页面上显示 DNS 提示 ({0} · DNS {1})",
"DNS: 기기 DNS → 직접 지정 (8.8.8.8 · 8.8.4.4). 앱의 인터넷 연결 (브라우저 · 다운로드 · 업데이트) 은 이 서버로 주소를 찾습니다. 되돌리기: 환경 설정 > 네트워크 · DNS > 시스템 DNS (끄기) 회사 · 학교망처럼 8.8.8.8 이 막힌 곳에서는 닿지 않는다는 안내의 [시스템 DNS 로 바꾸기] 를 누르세요.": "DNS:设备 DNS → 手动指定 (8.8.8.8 · 8.8.4.4)。应用的网络连接 (浏览器 · 下载 · 更新) 将用这些服务器查找地址。恢复:设置 > 网络 · DNS > 系统 DNS (关闭) 在公司 · 学校等屏蔽 8.8.8.8 的网络中,请点击无法连接提示中的 [切换为系统 DNS]。",
"DOC · DOCX · HWP · XLS · XLSX 를 PDF 로 바꿔 보기 (변환기를 받음)": "将 DOC · DOCX · HWP · XLS · XLSX 转为 PDF 查看 (需下载转换器)",
"DoH 서버 ({0}) 가 HTTP {1} 로 답했습니다": "DoH 服务器 ({0}) 返回 HTTP {1}",

View File

@ -207,7 +207,13 @@ class DnsResolver {
last = e;
}
}
throw DnsException(trf('DNS 서버 ({0}) 에 닿지 않습니다: {1}', [c.label, last]), unreachable: true, server: c.label);
// 작업 기록 · 화면에 예외 이름 ("TimeoutException:") 없이 (a4 932)
final why = switch (last) {
TimeoutException(:final message) => message ?? tr('답이 없습니다'),
SocketException(:final message) => message,
_ => '$last',
};
throw DnsException(trf('DNS 서버 ({0}) 에 닿지 않습니다: {1}', [c.label, why]), unreachable: true, server: c.label);
}
Future<DnsAnswer> _udp(InternetAddress server, String name, int type) async {

View File

@ -63,6 +63,53 @@ abstract class WebNav {
Future<void> setJavaScript(bool on);
}
/// 250: 사용자가 페이지를 옮길 때 (주소 · 뒤로 · 앞으로 · 새로고침 · 다시 시도) 알린다 - DNS 덮개는 이때만 걷는다.
/// 진행률로 걷으면 Android 는 본문 오류 뒤에 엔진의 오류 페이지를 또 읽어 (진행률 0 → 100) 덮개가 뜨자마자 걷혔다 (932 노트 10+)
class _NavWatch implements WebNav {
final WebNav n;
final void Function() onUserNavigate;
_NavWatch(this.n, this.onUserNavigate);
@override
Future<void> load(String url) {
onUserNavigate();
return n.load(url);
}
@override
Future<void> back() {
onUserNavigate();
return n.back();
}
@override
Future<void> forward() {
onUserNavigate();
return n.forward();
}
@override
Future<void> reload() {
onUserNavigate();
return n.reload();
}
@override
Future<void> stop() => n.stop();
@override
Future<void> exportCookies() => n.exportCookies();
@override
Future<void> pauseMedia() => n.pauseMedia();
@override
Future<void> runScript(String js) => n.runScript(js);
@override
Future<void> clearData() => n.clearData();
@override
Future<Object?> evaluate(String js) => n.evaluate(js);
@override
Future<void> setJavaScript(bool on) => n.setJavaScript(on);
}
/// 페이지의 모든 동영상 · 소리를 멈추는 스크립트
const pauseMediaScript = "document.querySelectorAll('video,audio').forEach(function(m){try{m.pause()}catch(e){}})";
@ -168,6 +215,12 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
/// [error]: 엔진이 알려 준 본문 오류. 프록시 터널 실패이고 DNS 서버에 지금 닿지 않는 중이면, 그 이름의 실패 기록이 없어도
/// (Chromium 이 실패한 프록시를 기억해 다시 묻지 않는 때 등) DNS 때문으로 본다.
/// 덮개는 여기서 걷지 않는다 (새로 읽기 시작 · [다시 시도] 에서) - Android 는 오류 뒤에 읽기 끝이 또 온다.
/// 250: 사용자가 다른 곳으로 가거나 다시 읽음 - 덮개를 걷는다 (다시 실패하면 또 뜬다)
void _userNavigated() {
_loadStartAt = DateTime.now();
if (_dnsBlocked != null && mounted) setState(() => _dnsBlocked = null);
}
void _checkDnsBlocked({String? host, String? error}) {
final h = host ?? Uri.tryParse(_url)?.host ?? '';
if (h.isEmpty) return;
@ -177,7 +230,10 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
if (server == null && f != null && error != null && (error.contains('TUNNEL') || error.contains('PROXY'))) server = f.server;
if (server == null) return;
final next = (h, server);
if (next != _dnsBlocked) setState(() => _dnsBlocked = next);
if (next == _dnsBlocked) return;
// 기기에서 "덮개를 띄웠는데 안 보임" 과 "띄우지 않음" 을 가릴 수 있게
widget.c.note(trf('DNS 안내 덮개를 띄움 ({0} · DNS {1})', [h, server]));
setState(() => _dnsBlocked = next);
}
/// 250: DNS 실패면 웹 뷰 위에 덮는다 ([시스템 DNS 로 바꾸기] [다시 시도]).
@ -253,11 +309,8 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
onTitle: (t) => mounted ? setState(() => _title = t) : null,
onProgress: (p) {
if (!mounted) return;
// 250: 새로 읽기 시작 - 지난 DNS 실패 화면을 걷고 시작 시각을 적어 둔다
if (p < 1 && _progress >= 1) {
_loadStartAt = DateTime.now();
_dnsBlocked = null;
}
// 250: 새로 읽기 시작 - 시작 시각을 적어 둔다 (덮개는 여기서 걷지 않는다 - [_userNavigated])
if (p < 1 && _progress >= 1) _loadStartAt = DateTime.now();
setState(() => _progress = p);
if (p >= 1) {
_checkDnsBlocked();
@ -272,7 +325,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
_canFwd = f;
});
},
attach: (n) => _nav = BrowserPage.debugNav = n,
attach: (n) => _nav = BrowserPage.debugNav = _NavWatch(n, _userNavigated),
// 250: 엔진의 본문 오류 (Edge · Android). 읽기 끝 (진행률) 만으로는 Android 에서 덮개가 뜨지 않았다 (931, 노트 10+)
onMainFrameError: (u, e) {
if (!mounted) return;

View File

@ -399,6 +399,12 @@ void main() {
host.onProgress(1);
await tester.pump();
expect(find.text('DNS 서버 (192.0.2.1) 에 닿지 않아 example.org 을(를) 열 수 없습니다.'), findsOneWidget);
// 932 (노트 10+): Android 는 본문 오류 뒤에 엔진의 오류 페이지를 또 읽는다 (진행률 0 → 100) - 덮개가 걷히면 안 된다
host.onProgress(0.1);
await tester.pump();
host.onProgress(1);
await tester.pump();
expect(find.text('DNS 서버 (192.0.2.1) 에 닿지 않아 example.org 을(를) 열 수 없습니다.'), findsOneWidget);
// DNS 와 상관없는 오류 (이름 기록도 없음) 는 엔진 화면 그대로
await tester.tap(find.text('다시 시도'));
await tester.pump();