50 Android 가 막은 폴더 (Android/data · obb): 빈 폴더가 아니라 "이 폴더는 Android 가 막아 볼 수 없습니다" · [위 폴더로]

- Android 11 부터 이런 폴더는 오류 없이 빈 목록으로 읽힌다 - 막힌 것이라고 알리고 그 앱의 내보내기 · 공유를 안내
- 다시 해도 같으므로 [다시 시도] 대신 [위 폴더로]
- 시험: file_error_test (경로 판별 · 안내 글)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음3
RyuJongJin 2026-10-09 20:35:17 +09:00
parent de967d190a
commit c25a754035
6 changed files with 78 additions and 2 deletions

View File

@ -102,6 +102,7 @@
"API 키 (필수, 무료)": "API key (required, free)",
"API 키 또는 아이디:비밀번호 (선택)": "API key or user:password (optional)",
"AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "AV1 video cannot be re-encoded on this device. Change the codec to \"Keep original\" to make the MKV. (The PC version can.)",
"Android 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. 그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.": "Since Android 11, no file app can open other apps' data folders (Android/data · Android/obb). Use export or share inside that app to get the files.",
"Android 가 막은 폴더 (Android/data 등) 이거나 \"모든 파일에 대한 접근\" 권한이 없습니다.": "This folder is blocked by Android (such as Android/data), or \"All files access\" is not allowed.",
"Android 는 rsync 를 내려받을 수 없습니다. 직접 지정하세요.": "rsync cannot be downloaded on Android. Specify it yourself.",
"Android 는 버전이 낮은 앱을 지금 앱 위에 설치하지 못합니다. 그래서:\n1. v{0} 설치 파일과 지금 설정 (동영상 목록 · 즐겨찾기 포함) 을 Download/JJ_MKVMaker 에 저장합니다.\n2. 이 앱을 지웁니다 (Android 확인 창).\n3. 파일 앱에서 Download/JJ_MKVMaker 의 APK 를 눌러 설치합니다.\n4. 이 기능이 있는 버전이면 시작할 때 저장해 둔 설정을 되살릴지 묻습니다. 더 예전 버전은 설정을 다시 해야 합니다.\n※ 자막 API 키 · 비밀번호는 저장하지 않으므로 다시 넣어야 합니다.": "Android cannot install a lower version over the current app. So:\n1. Save the v{0} install file and the current settings (including the video list and bookmarks) to Download/JJ_MKVMaker.\n2. Uninstall this app (Android confirmation).\n3. Tap the APK in Download/JJ_MKVMaker in a file manager to install it.\n4. Versions with this feature ask at startup whether to restore the saved settings. Older versions need to be set up again.\n* Subtitle API keys and passwords are not saved, so enter them again.",
@ -1270,6 +1271,7 @@
"웹 페이지 자동 번역": "Translate web pages automatically",
"웹 페이지를 번역하지 못했습니다: {0}": "Could not translate the web page: {0}",
"위 폴더": "Parent folder",
"위 폴더로": "Go up",
"위로": "Up",
"위아래": "Top and bottom",
"위아래 ⇅": "Stacked ⇅",
@ -1331,6 +1333,7 @@
"이 폴더 선택": "Choose this folder",
"이 폴더 쌍은 기억한 옵션을 씁니다 (환경 설정이 아니라 모니터링 > 복사 · rsync 에서 고칩니다)": "This folder pair uses its remembered options (edit them in Monitoring > Copy · rsync, not in settings)",
"이 폴더 전체": "This whole folder",
"이 폴더는 Android 가 막아 볼 수 없습니다": "Android blocks this folder, so it cannot be viewed",
"이 폴더를 rsync 원본 · 대상으로 고르기": "Choose this folder as rsync source · target",
"이 폴더를 볼 권한이 없습니다": "No permission to view this folder",
"이 폴더를 읽을 권한이 없습니다": "No permission to read this folder",

View File

@ -102,6 +102,7 @@
"API 키 (필수, 무료)": "API キー(必須・無料)",
"API 키 또는 아이디:비밀번호 (선택)": "API キーまたは ID:パスワード (任意)",
"AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "AV1 動画はこの端末では再エンコードできません。コーデックを「元のまま」に変えて MKV を作成してください。(PC 版はできます)",
"Android 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. 그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.": "Android 11 以降、他のアプリのデータフォルダー (Android/data・Android/obb) はどのファイルアプリでも開けません。そのアプリ内のエクスポート・共有でファイルを取り出してください。",
"Android 가 막은 폴더 (Android/data 등) 이거나 \"모든 파일에 대한 접근\" 권한이 없습니다.": "Android が制限しているフォルダー (Android/data など) か、「すべてのファイルへのアクセス」権限がありません。",
"Android 는 rsync 를 내려받을 수 없습니다. 직접 지정하세요.": "Android では rsync をダウンロードできません。直接指定してください。",
"Android 는 버전이 낮은 앱을 지금 앱 위에 설치하지 못합니다. 그래서:\n1. v{0} 설치 파일과 지금 설정 (동영상 목록 · 즐겨찾기 포함) 을 Download/JJ_MKVMaker 에 저장합니다.\n2. 이 앱을 지웁니다 (Android 확인 창).\n3. 파일 앱에서 Download/JJ_MKVMaker 의 APK 를 눌러 설치합니다.\n4. 이 기능이 있는 버전이면 시작할 때 저장해 둔 설정을 되살릴지 묻습니다. 더 예전 버전은 설정을 다시 해야 합니다.\n※ 자막 API 키 · 비밀번호는 저장하지 않으므로 다시 넣어야 합니다.": "Android はバージョンの低いアプリを今のアプリの上にインストールできません。そこで:\n1. v{0} のインストールファイルと今の設定 (動画リスト · ブックマークを含む) を Download/JJ_MKVMaker に保存します。\n2. このアプリを削除します (Android の確認画面)。\n3. ファイルアプリで Download/JJ_MKVMaker の APK をタップしてインストールします。\n4. この機能があるバージョンなら起動時に保存した設定を復元するか尋ねます。それより古いバージョンは設定し直す必要があります。\n※ 字幕 API キー · パスワードは保存しないので再入力が必要です。",
@ -1270,6 +1271,7 @@
"웹 페이지 자동 번역": "ウェブページを自動翻訳",
"웹 페이지를 번역하지 못했습니다: {0}": "ウェブページを翻訳できませんでした: {0}",
"위 폴더": "上のフォルダー",
"위 폴더로": "上のフォルダーへ",
"위로": "上へ",
"위아래": "上下",
"위아래 ⇅": "上下 ⇅",
@ -1331,6 +1333,7 @@
"이 폴더 선택": "このフォルダーを選択",
"이 폴더 쌍은 기억한 옵션을 씁니다 (환경 설정이 아니라 모니터링 > 복사 · rsync 에서 고칩니다)": "このフォルダーの組は記憶したオプションを使います (環境設定ではなくモニタリング > コピー・rsync で変更します)",
"이 폴더 전체": "このフォルダー全体",
"이 폴더는 Android 가 막아 볼 수 없습니다": "このフォルダーは Android によってブロックされているため表示できません",
"이 폴더를 rsync 원본 · 대상으로 고르기": "このフォルダーを rsync の元 · 対象に選ぶ",
"이 폴더를 볼 권한이 없습니다": "このフォルダーを表示する権限がありません",
"이 폴더를 읽을 권한이 없습니다": "このフォルダーを読む権限がありません",

View File

@ -102,6 +102,7 @@
"API 키 (필수, 무료)": "API 密钥(必填,免费)",
"API 키 또는 아이디:비밀번호 (선택)": "API 密钥或 用户名:密码 (可选)",
"AV1 영상은 이 기기에서 다시 인코딩할 수 없습니다. 코덱을 \"원본 유지\" 로 바꿔 MKV 를 만드세요. (PC 판은 됩니다)": "此设备无法重新编码 AV1 视频。请将编解码器改为“保持原样”后制作 MKV。(PC 版可以)",
"Android 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. 그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.": "从 Android 11 起,任何文件应用都无法打开其他应用的数据文件夹 (Android/data · Android/obb)。请在该应用内使用导出或分享取出文件。",
"Android 가 막은 폴더 (Android/data 등) 이거나 \"모든 파일에 대한 접근\" 권한이 없습니다.": "这是 Android 限制的文件夹 (如 Android/data),或没有“所有文件访问”权限。",
"Android 는 rsync 를 내려받을 수 없습니다. 직접 지정하세요.": "Android 无法下载 rsync,请自行指定。",
"Android 는 버전이 낮은 앱을 지금 앱 위에 설치하지 못합니다. 그래서:\n1. v{0} 설치 파일과 지금 설정 (동영상 목록 · 즐겨찾기 포함) 을 Download/JJ_MKVMaker 에 저장합니다.\n2. 이 앱을 지웁니다 (Android 확인 창).\n3. 파일 앱에서 Download/JJ_MKVMaker 의 APK 를 눌러 설치합니다.\n4. 이 기능이 있는 버전이면 시작할 때 저장해 둔 설정을 되살릴지 묻습니다. 더 예전 버전은 설정을 다시 해야 합니다.\n※ 자막 API 키 · 비밀번호는 저장하지 않으므로 다시 넣어야 합니다.": "Android 无法在当前应用上安装版本更低的应用。因此:\n1. 将 v{0} 安装文件和当前设置 (包括视频列表 · 书签) 保存到 Download/JJ_MKVMaker。\n2. 卸载此应用 (Android 确认窗口)。\n3. 在文件应用中点按 Download/JJ_MKVMaker 中的 APK 进行安装。\n4. 带有此功能的版本会在启动时询问是否恢复保存的设置。更早的版本需要重新设置。\n※ 字幕 API 密钥 · 密码不会保存,需要重新输入。",
@ -1270,6 +1271,7 @@
"웹 페이지 자동 번역": "自动翻译网页",
"웹 페이지를 번역하지 못했습니다: {0}": "无法翻译网页: {0}",
"위 폴더": "上级文件夹",
"위 폴더로": "返回上级文件夹",
"위로": "向上",
"위아래": "上下",
"위아래 ⇅": "上下 ⇅",
@ -1331,6 +1333,7 @@
"이 폴더 선택": "选择此文件夹",
"이 폴더 쌍은 기억한 옵션을 씁니다 (환경 설정이 아니라 모니터링 > 복사 · rsync 에서 고칩니다)": "此文件夹组合使用记住的选项 (请在 监视 > 复制 · rsync 中修改,而非设置)",
"이 폴더 전체": "整个文件夹",
"이 폴더는 Android 가 막아 볼 수 없습니다": "此文件夹被 Android 阻止,无法查看",
"이 폴더를 rsync 원본 · 대상으로 고르기": "选择此文件夹作为 rsync 源 · 目标",
"이 폴더를 볼 권한이 없습니다": "没有查看此文件夹的权限",
"이 폴더를 읽을 권한이 없습니다": "没有读取此文件夹的权限",

View File

@ -473,6 +473,23 @@ class _ExplorerPageState extends State<ExplorerPage> {
final server = dav ? DavRegistry.server(DavPath.parse(where).server) : null;
final (title, body) = explainFileError(err, dav: dav, noPassword: server != null && server.password.isEmpty);
final locked = isLockedError(err);
// 50: 막힌 폴더는 다시 해도 같으니 [다시 시도] 없이 [위 폴더로]
if (err == androidRestrictedError) {
return _notice(
icon: Icons.lock_outline,
title: '${_displayPath(where)}: $title',
body: body,
actions: [
FilledButton.tonal(
onPressed: () {
pane.errors.remove(where);
_goTo(pane, vDirname(where));
},
child: Text(tr('위 폴더로')),
),
],
);
}
return _notice(
icon: dav ? Icons.cloud_off_outlined : Icons.error_outline,
title: '${_displayPath(where)}: $title',
@ -584,10 +601,15 @@ class _ExplorerPageState extends State<ExplorerPage> {
try {
final list = await listEntries(dir, showHidden: c.settings.explorerShowHidden);
pane.cache[dir] = _sorted(list);
pane.errors.remove(dir);
// 50: Android 가 막은 폴더는 비어 보인다 (오류 없이) - 비어 있는 것이 아니라 막힌 것이라고 알린다
if (list.isEmpty && isAndroidRestricted(dir)) {
pane.errors[dir] = androidRestrictedError;
} else {
pane.errors.remove(dir);
}
} catch (e) {
pane.cache[dir] = const [];
pane.errors[dir] = '$e';
pane.errors[dir] = isAndroidRestricted(dir) ? androidRestrictedError : '$e';
} finally {
pane.loading.remove(dir);
pane.changed();

View File

@ -5,7 +5,21 @@ import '../l10n/tr.dart';
/// 파일 · WebDAV 오류를 사람이 읽을 말로 (제목, 할 일). 원문 예외는 알아볼 수 없을 때만 그대로.
/// 파일 탐색기 · 폴더 고르기 · 그 밖에 WebDAV 를 읽는 화면이 함께 쓴다 (12 · 134).
/// 50: Android 가 막은 폴더 (Android/data · obb) 표시 (파일 탐색기가 오류 자리에 넣는다)
const androidRestrictedError = 'ANDROID_RESTRICTED_FOLDER';
/// 50: Android 11 부터 어떤 파일 앱도 다른 앱의 데이터 폴더 (Android/data · Android/obb) 를 볼 수 없다
bool isAndroidRestricted(String path, {bool? android}) =>
(android ?? Platform.isAndroid) && RegExp(r'/Android/(data|obb)(/|$)', caseSensitive: false).hasMatch(path);
(String, String) explainFileError(String e, {required bool dav, bool noPassword = false}) {
if (e == androidRestrictedError) {
return (
tr('이 폴더는 Android 가 막아 볼 수 없습니다'),
tr('Android 11 부터 다른 앱의 데이터 폴더 (Android/data · Android/obb) 는 어떤 파일 앱도 열 수 없습니다. '
'그 앱 안의 내보내기 · 공유로 파일을 꺼내세요.'),
);
}
// 128 · 133: 마스터 비밀번호를 기다리는 중 (고장이 아님)
if (isLockedError(e)) {
return (tr('마스터 비밀번호가 필요합니다'), tr('이 서버의 저장된 비밀번호를 쓰려면 마스터 비밀번호를 넣으세요.'));

31
test/file_error_test.dart Normal file
View File

@ -0,0 +1,31 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/ui/file_error.dart';
void main() {
test('50: Android 가 막은 폴더 (Android/data · obb) 알아보기', () {
for (final p in [
'/storage/emulated/0/Android/data',
'/storage/emulated/0/Android/data/com.example.app',
'/storage/emulated/0/Android/obb/',
'/storage/1234-ABCD/Android/data/x',
]) {
expect(isAndroidRestricted(p, android: true), isTrue, reason: p);
}
for (final p in [
'/storage/emulated/0/Android',
'/storage/emulated/0/Android/media',
'/storage/emulated/0/Download/Android/datas',
'/storage/emulated/0/Movies',
]) {
expect(isAndroidRestricted(p, android: true), isFalse, reason: p);
}
// Windows 등은 해당 없음
expect(isAndroidRestricted('/storage/emulated/0/Android/data', android: false), isFalse);
});
test('50: 막힌 폴더는 "이 폴더는 Android 가 막아 볼 수 없습니다" 와 할 일', () {
final (title, body) = explainFileError(androidRestrictedError, dav: false);
expect(title, '이 폴더는 Android 가 막아 볼 수 없습니다');
expect(body, contains('내보내기'));
});
}