Android 업데이트: 설치 허용이 꺼져 있으면 [설치 계속] 으로 받은 파일 그대로 설치

"이 출처의 앱 설치 허용" 설정 화면을 연 뒤 "다시 [업데이트]" 로 끝나
허용하고 돌아오면 같은 APK (약 90MB) 를 처음부터 다시 받아야 했다.
설치 허용 필요 (PERMISSION) 를 따로 알려, 돌아와 [설치 계속] 을 누르면 받은 파일로 바로 설치한다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
RyuJongJin 2026-10-05 03:12:45 +09:00
parent 6dfb4776a2
commit ed9f00118c
8 changed files with 72 additions and 8 deletions

View File

@ -57,6 +57,9 @@ class MainActivity : FlutterActivity() {
"installApk" -> try {
installApk(call.argument<String>("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")

View File

@ -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",

View File

@ -447,8 +447,11 @@
"선택한 파일 재생: 지금 보고 있는 동영상 (여러 개는 목록에서 체크)": "選択したファイルを再生: 今見ている動画(複数はリストでチェック)",
"선택한 파일 재생: 체크한 동영상 {0}개를 목록 순서대로 이어서": "選択したファイルを再生: チェックした動画 {0}個をリスト順に続けて",
"선택한 파일 재생{0}": "選択したファイルを再生{0}",
"설정 화면에서 \"이 출처의 앱 설치 허용\" 을 켠 뒤 돌아와 [설치 계속] 을 누르세요. 받은 파일을 그대로 씁니다 (다시 받지 않음).": "設定画面で「この提供元のアプリを許可」をオンにしてから戻り、[インストールを続ける] を押してください。ダウンロードしたファイルをそのまま使います (再ダウンロードしません)。",
"설치 계속": "インストールを続ける",
"설치 실패": "インストール失敗",
"설치 준비 완료": "インストール準備完了",
"설치 허용 필요": "インストールの許可が必要",
"설치 화면을 열 수 없습니다: {0}": "インストール画面を開けません: {0}",
"설치가 끝나면 프로그램이 다시 시작됩니다.": "インストールが終わるとプログラムが再起動します。",
"설치됨": "インストール済み",

View File

@ -447,8 +447,11 @@
"선택한 파일 재생: 지금 보고 있는 동영상 (여러 개는 목록에서 체크)": "播放所选文件: 当前查看的视频(多个请在列表中勾选)",
"선택한 파일 재생: 체크한 동영상 {0}개를 목록 순서대로 이어서": "播放所选文件: 按列表顺序连续播放勾选的 {0} 个视频",
"선택한 파일 재생{0}": "播放所选文件{0}",
"설정 화면에서 \"이 출처의 앱 설치 허용\" 을 켠 뒤 돌아와 [설치 계속] 을 누르세요. 받은 파일을 그대로 씁니다 (다시 받지 않음).": "请在设置页面开启\"允许来自此来源的应用\",然后返回并点按 [继续安装]。将直接使用已下载的文件 (不会重新下载)。",
"설치 계속": "继续安装",
"설치 실패": "安装失败",
"설치 준비 완료": "准备安装",
"설치 허용 필요": "需要安装权限",
"설치 화면을 열 수 없습니다: {0}": "无法打开安装界面: {0}",
"설치가 끝나면 프로그램이 다시 시작됩니다.": "安装完成后程序将重新启动。",
"설치됨": "已安装",

View File

@ -95,6 +95,7 @@ class AndroidUpdater implements Updater {
try {
await _ch.invokeMethod<void>('installApk', {'path': downloaded});
} on PlatformException catch (e) {
if (e.code == 'PERMISSION') throw InstallPermissionNeeded(e.message ?? '$e');
throw UpdateException(e.message ?? '$e');
}
}

View File

@ -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 안내

View File

@ -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<void> 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<bool>(
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));

View File

@ -27,7 +27,10 @@ Map<String, dynamic> _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 = <String>[];
@override
@ -44,7 +47,13 @@ class _FakeUpdater implements Updater {
}
@override
Future<void> scheduleInstall(String dir) async => calls.add('install $dir');
Future<void> scheduleInstall(String dir) async {
calls.add('install $dir');
if (needPermission > 0) {
needPermission--;
throw const InstallPermissionNeeded('허용 필요');
}
}
@override
Future<void> 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);