diff --git a/android/app/src/main/kotlin/com/jj/jj_mkvmaker/MainActivity.kt b/android/app/src/main/kotlin/com/jj/jj_mkvmaker/MainActivity.kt index 2ff388b..a2b23ac 100644 --- a/android/app/src/main/kotlin/com/jj/jj_mkvmaker/MainActivity.kt +++ b/android/app/src/main/kotlin/com/jj/jj_mkvmaker/MainActivity.kt @@ -57,6 +57,9 @@ class MainActivity : FlutterActivity() { "installApk" -> try { installApk(call.argument("path") ?: "") result.success(null) + } catch (e: IllegalStateException) { + // 설치 허용 설정 화면을 열었다: 앱이 받은 파일로 다시 설치하게 따로 알린다 + result.error("PERMISSION", e.message ?: e.toString(), null) } catch (e: Exception) { result.error("INSTALL", e.message ?: e.toString(), null) } @@ -80,7 +83,7 @@ class MainActivity : FlutterActivity() { } /// 받은 업데이트 APK 로 Android 설치 화면을 연다. 앱 전용 폴더의 파일이라 FileProvider 로 넘긴다. - /// 처음이면 "이 출처의 앱 설치 허용" 설정 화면을 먼저 연다 (허용한 뒤 다시 [업데이트]) + /// 처음이면 "이 출처의 앱 설치 허용" 설정 화면을 먼저 연다 (허용하고 돌아와 [설치 계속] - 받은 파일을 그대로 쓴다) private fun installApk(path: String) { val src = File(path) if (!src.isFile) throw IllegalArgumentException("APK 파일이 없습니다: $path") diff --git a/assets/l10n/en.json b/assets/l10n/en.json index f533c63..ca9b44b 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -447,8 +447,11 @@ "선택한 파일 재생: 지금 보고 있는 동영상 (여러 개는 목록에서 체크)": "Play selected files: the video you are viewing (check several in the list)", "선택한 파일 재생: 체크한 동영상 {0}개를 목록 순서대로 이어서": "Play selected files: the {0} checked videos in list order", "선택한 파일 재생{0}": "Play selected files{0}", + "설정 화면에서 \"이 출처의 앱 설치 허용\" 을 켠 뒤 돌아와 [설치 계속] 을 누르세요. 받은 파일을 그대로 씁니다 (다시 받지 않음).": "Turn on \"Allow from this source\" in the settings screen, then come back and tap [Continue install]. The downloaded file is used as is (no download again).", + "설치 계속": "Continue install", "설치 실패": "Install failed", "설치 준비 완료": "Ready to install", + "설치 허용 필요": "Permission needed to install", "설치 화면을 열 수 없습니다: {0}": "Cannot open the install screen: {0}", "설치가 끝나면 프로그램이 다시 시작됩니다.": "The program restarts when the installation finishes.", "설치됨": "Installed", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 7e09860..d37e1b0 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -447,8 +447,11 @@ "선택한 파일 재생: 지금 보고 있는 동영상 (여러 개는 목록에서 체크)": "選択したファイルを再生: 今見ている動画(複数はリストでチェック)", "선택한 파일 재생: 체크한 동영상 {0}개를 목록 순서대로 이어서": "選択したファイルを再生: チェックした動画 {0}個をリスト順に続けて", "선택한 파일 재생{0}": "選択したファイルを再生{0}", + "설정 화면에서 \"이 출처의 앱 설치 허용\" 을 켠 뒤 돌아와 [설치 계속] 을 누르세요. 받은 파일을 그대로 씁니다 (다시 받지 않음).": "設定画面で「この提供元のアプリを許可」をオンにしてから戻り、[インストールを続ける] を押してください。ダウンロードしたファイルをそのまま使います (再ダウンロードしません)。", + "설치 계속": "インストールを続ける", "설치 실패": "インストール失敗", "설치 준비 완료": "インストール準備完了", + "설치 허용 필요": "インストールの許可が必要", "설치 화면을 열 수 없습니다: {0}": "インストール画面を開けません: {0}", "설치가 끝나면 프로그램이 다시 시작됩니다.": "インストールが終わるとプログラムが再起動します。", "설치됨": "インストール済み", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 92d4640..00d6a93 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -447,8 +447,11 @@ "선택한 파일 재생: 지금 보고 있는 동영상 (여러 개는 목록에서 체크)": "播放所选文件: 当前查看的视频(多个请在列表中勾选)", "선택한 파일 재생: 체크한 동영상 {0}개를 목록 순서대로 이어서": "播放所选文件: 按列表顺序连续播放勾选的 {0} 个视频", "선택한 파일 재생{0}": "播放所选文件{0}", + "설정 화면에서 \"이 출처의 앱 설치 허용\" 을 켠 뒤 돌아와 [설치 계속] 을 누르세요. 받은 파일을 그대로 씁니다 (다시 받지 않음).": "请在设置页面开启\"允许来自此来源的应用\",然后返回并点按 [继续安装]。将直接使用已下载的文件 (不会重新下载)。", + "설치 계속": "继续安装", "설치 실패": "安装失败", "설치 준비 완료": "准备安装", + "설치 허용 필요": "需要安装权限", "설치 화면을 열 수 없습니다: {0}": "无法打开安装界面: {0}", "설치가 끝나면 프로그램이 다시 시작됩니다.": "安装完成后程序将重新启动。", "설치됨": "已安装", diff --git a/lib/platform/android/android_updater.dart b/lib/platform/android/android_updater.dart index f01ad93..ea4ce73 100644 --- a/lib/platform/android/android_updater.dart +++ b/lib/platform/android/android_updater.dart @@ -95,6 +95,7 @@ class AndroidUpdater implements Updater { try { await _ch.invokeMethod('installApk', {'path': downloaded}); } on PlatformException catch (e) { + if (e.code == 'PERMISSION') throw InstallPermissionNeeded(e.message ?? '$e'); throw UpdateException(e.message ?? '$e'); } } diff --git a/lib/services/updater.dart b/lib/services/updater.dart index f5c9f67..6405193 100644 --- a/lib/services/updater.dart +++ b/lib/services/updater.dart @@ -7,6 +7,11 @@ class UpdateException implements Exception { String toString() => message; } +/// Android: "이 출처의 앱 설치 허용" 이 꺼져 있어 설정 화면을 열었다. 켜고 돌아오면 받은 파일로 다시 설치하면 된다. +class InstallPermissionNeeded extends UpdateException { + const InstallPermissionNeeded(super.message); +} + /// 새 버전 확인 · 설치 경계 /// Windows: platform/windows/github_updater.dart (zip 받아 파일 교체 후 다시 시작) /// Android: (이식 시) 스토어 / APK 안내 diff --git a/lib/ui/update_dialog.dart b/lib/ui/update_dialog.dart index e185664..f1363b5 100644 --- a/lib/ui/update_dialog.dart +++ b/lib/ui/update_dialog.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../app/app_controller.dart'; import '../core/app_update.dart'; +import '../services/updater.dart' show InstallPermissionNeeded; import 'theme.dart'; import '../l10n/tr.dart'; @@ -154,12 +155,31 @@ Future checkForUpdate(BuildContext context, AppController c, {bool manual if (go != true) return; // Android: 설치 화면만 연다 (앱을 끝내지 않음, 설치되면 Android 가 앱을 다시 띄움) if (up.installsInPlace) { - try { - await up.scheduleInstall(dir); - } catch (e) { - snack(trf('설치 화면을 열 수 없습니다: {0}', [e])); + while (true) { + try { + await up.scheduleInstall(dir); + return; + } on InstallPermissionNeeded { + // 설치 허용 설정 화면이 열렸다: 켜고 돌아와 [설치 계속] → 받은 파일로 다시 (처음부터 다시 받지 않게) + if (!context.mounted) return; + final again = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(tr('설치 허용 필요')), + content: Text(tr('설정 화면에서 "이 출처의 앱 설치 허용" 을 켠 뒤 돌아와 [설치 계속] 을 누르세요. ' + '받은 파일을 그대로 씁니다 (다시 받지 않음).')), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('나중에'))), + FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('설치 계속'))), + ], + ), + ); + if (again != true) return; + } catch (e) { + snack(trf('설치 화면을 열 수 없습니다: {0}', [e])); + return; + } } - return; } // 다운로드 중이면 기존 종료 확인 절차 final ok = await (c.confirmQuit?.call() ?? Future.value(true)); diff --git a/test/update_ui_test.dart b/test/update_ui_test.dart index 6fcf2d6..765dea7 100644 --- a/test/update_ui_test.dart +++ b/test/update_ui_test.dart @@ -27,7 +27,10 @@ Map _release(String tag, {String? digest, String body = '', boo class _FakeUpdater implements Updater { @override - bool get installsInPlace => false; + bool installsInPlace = false; + + /// 설치할 때 "설치 허용 필요" 로 실패할 횟수 (Android) + int needPermission = 0; ReleaseInfo? next; final calls = []; @override @@ -44,7 +47,13 @@ class _FakeUpdater implements Updater { } @override - Future scheduleInstall(String dir) async => calls.add('install $dir'); + Future scheduleInstall(String dir) async { + calls.add('install $dir'); + if (needPermission > 0) { + needPermission--; + throw const InstallPermissionNeeded('허용 필요'); + } + } @override Future openPage(ReleaseInfo r) async => calls.add('page'); } @@ -97,6 +106,23 @@ void main() { expect(up.calls, ['download 1.1.0', r'install C:\tmp\new']); }); + testWidgets('Android: 설치 허용이 꺼져 있으면 [설치 계속] → 받은 파일로 다시 설치 (다시 받지 않음)', (tester) async { + up + ..installsInPlace = true + ..needPermission = 1 + ..next = parseLatestRelease(_release('v1.1.0')); + await open(tester); + await tester.tap(find.text('업데이트')); + await tester.pumpAndSettle(); + await tester.tap(find.text('지금 설치')); + await tester.pumpAndSettle(); + expect(find.text('설치 허용 필요'), findsOneWidget); + await tester.tap(find.text('설치 계속')); + await tester.pumpAndSettle(); + expect(find.text('설치 허용 필요'), findsNothing); + expect(up.calls, ['download 1.1.0', r'install C:\tmp\new', r'install C:\tmp\new']); + }); + testWidgets('이 버전 건너뛰기 → 자동 확인에서 다시 묻지 않음', (tester) async { up.next = parseLatestRelease(_release('v1.1.0')); await open(tester);