Android 업데이트: 설치 허용이 꺼져 있으면 [설치 계속] 으로 받은 파일 그대로 설치
"이 출처의 앱 설치 허용" 설정 화면을 연 뒤 "다시 [업데이트]" 로 끝나 허용하고 돌아오면 같은 APK (약 90MB) 를 처음부터 다시 받아야 했다. 설치 허용 필요 (PERMISSION) 를 따로 알려, 돌아와 [설치 계속] 을 누르면 받은 파일로 바로 설치한다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
parent
6dfb4776a2
commit
ed9f00118c
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -447,8 +447,11 @@
|
|||
"선택한 파일 재생: 지금 보고 있는 동영상 (여러 개는 목록에서 체크)": "選択したファイルを再生: 今見ている動画(複数はリストでチェック)",
|
||||
"선택한 파일 재생: 체크한 동영상 {0}개를 목록 순서대로 이어서": "選択したファイルを再生: チェックした動画 {0}個をリスト順に続けて",
|
||||
"선택한 파일 재생{0}": "選択したファイルを再生{0}",
|
||||
"설정 화면에서 \"이 출처의 앱 설치 허용\" 을 켠 뒤 돌아와 [설치 계속] 을 누르세요. 받은 파일을 그대로 씁니다 (다시 받지 않음).": "設定画面で「この提供元のアプリを許可」をオンにしてから戻り、[インストールを続ける] を押してください。ダウンロードしたファイルをそのまま使います (再ダウンロードしません)。",
|
||||
"설치 계속": "インストールを続ける",
|
||||
"설치 실패": "インストール失敗",
|
||||
"설치 준비 완료": "インストール準備完了",
|
||||
"설치 허용 필요": "インストールの許可が必要",
|
||||
"설치 화면을 열 수 없습니다: {0}": "インストール画面を開けません: {0}",
|
||||
"설치가 끝나면 프로그램이 다시 시작됩니다.": "インストールが終わるとプログラムが再起動します。",
|
||||
"설치됨": "インストール済み",
|
||||
|
|
|
|||
|
|
@ -447,8 +447,11 @@
|
|||
"선택한 파일 재생: 지금 보고 있는 동영상 (여러 개는 목록에서 체크)": "播放所选文件: 当前查看的视频(多个请在列表中勾选)",
|
||||
"선택한 파일 재생: 체크한 동영상 {0}개를 목록 순서대로 이어서": "播放所选文件: 按列表顺序连续播放勾选的 {0} 个视频",
|
||||
"선택한 파일 재생{0}": "播放所选文件{0}",
|
||||
"설정 화면에서 \"이 출처의 앱 설치 허용\" 을 켠 뒤 돌아와 [설치 계속] 을 누르세요. 받은 파일을 그대로 씁니다 (다시 받지 않음).": "请在设置页面开启\"允许来自此来源的应用\",然后返回并点按 [继续安装]。将直接使用已下载的文件 (不会重新下载)。",
|
||||
"설치 계속": "继续安装",
|
||||
"설치 실패": "安装失败",
|
||||
"설치 준비 완료": "准备安装",
|
||||
"설치 허용 필요": "需要安装权限",
|
||||
"설치 화면을 열 수 없습니다: {0}": "无法打开安装界面: {0}",
|
||||
"설치가 끝나면 프로그램이 다시 시작됩니다.": "安装完成后程序将重新启动。",
|
||||
"설치됨": "已安装",
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 안내
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue