202 lines
11 KiB
Dart
202 lines
11 KiB
Dart
/// 약속: Android 시작 (창 하나 - 트레이 · 단축키 · 탐색기 연결 · 클립보드 감시는 없다). 공용 시작 단계 ([startCore]) 를 부르고
|
|
/// 자동 백업 판별 · 공용 폴더 · 화면 방향 · 권한 · 다른 앱에서 받은 파일 · 백그라운드 실행을 잇는다.
|
|
library;
|
|
|
|
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:package_info_plus/package_info_plus.dart';
|
|
import 'package:path/path.dart' as p;
|
|
|
|
import '../app/app_controller.dart';
|
|
import '../app/model_backup.dart';
|
|
import '../app/install_marker.dart';
|
|
import '../app/net_guard.dart';
|
|
import '../app/tor_service.dart';
|
|
import '../ui/settings_problem.dart';
|
|
import '../ui/migration_notice.dart';
|
|
import '../ui/master_prompt.dart';
|
|
import '../ui/secret_issue.dart';
|
|
import '../app/settings.dart';
|
|
import '../platform/android/android_download_tools.dart';
|
|
import '../platform/android/android_keep_alive.dart';
|
|
import '../platform/android/android_shell.dart' show applyScreenOrientation;
|
|
import '../platform/android/android_storage.dart';
|
|
import '../ui/all_files_guide.dart';
|
|
import '../services/platform_services.dart';
|
|
import '../ui/app_actions.dart';
|
|
import '../ui/downloads_page.dart';
|
|
import '../ui/monitor_page.dart' show askLiveSyncStart;
|
|
import '../ui/update_dialog.dart';
|
|
import '../ui/version_restore.dart';
|
|
import '../ui/work_panel.dart';
|
|
import '../l10n/tr.dart';
|
|
import '../platform/android/android_updater.dart';
|
|
import '../ui/app_root.dart';
|
|
import 'startup_helpers.dart';
|
|
import 'app_start.dart';
|
|
|
|
/// Android: 창 하나. 트레이 · 단축키 · 탐색기 연결 · 다운로드 (yt-dlp · aria2) · 업데이트는 없다.
|
|
/// 화면 방향은 기본이 자동 (기기를 돌리는 대로). 환경 설정 > 화면 방향에서 가로 · 세로 고정.
|
|
Future<void> runAndroid(String dataDir) async {
|
|
HttpOverrides.global = JjHttpOverrides(); // DNS 강제: HttpClient 를 만들기 전에 (main 과 같은 까닭)
|
|
final services = PlatformServices.create();
|
|
final logs = Directory(p.join(dataDir, 'Logs'))..createSync(recursive: true);
|
|
final controller = AppController(services, settingsStore: SettingsStore())..logFile = p.join(logs.path, 'app.log');
|
|
controller.note(trf('── 시작 {0} (Android) ──', [appTitle]));
|
|
// 버전별 설정 보관: 앱을 지웠다 다시 설치해도 남도록 공용 Download/JJ_MKVMaker 에도 (예전 버전으로 되돌릴 때)
|
|
final freshInstall = !File(p.join(dataDir, 'settings.json')).existsSync();
|
|
// 115: 다시 설치한 뒤 처음 켰는데 설정이 있으면 Android 자동 백업이 되살린 것 (표시 파일은 백업에서 뺐다)
|
|
final marker = InstallMarker(dataDir);
|
|
var autoBackup = false;
|
|
try {
|
|
final info = await PackageInfo.fromPlatform();
|
|
autoBackup = restoredByAutoBackup(
|
|
settingsExisted: !freshInstall, markerExists: marker.exists, installTime: info.installTime, updateTime: info.updateTime);
|
|
} catch (_) {}
|
|
marker.write();
|
|
if (autoBackup) controller.note(tr('Android 자동 백업에서 되살린 설정으로 시작합니다'));
|
|
String? sharedSnapshots;
|
|
try {
|
|
final root = await const MethodChannel('jj_mkvmaker/android').invokeMethod<String>('storageRoot');
|
|
// l10n-skip: 실제 폴더 이름 (예전 보관을 찾아야 하므로 바꾸지 않음)
|
|
if (root != null) sharedSnapshots = p.join(root, 'Download', 'JJ_MKVMaker', '설정 보관');
|
|
if (root != null) ModelBackup.sharedRoot = p.join(root, 'Download', 'JJ_MKVMaker');
|
|
} catch (_) {}
|
|
// 공용 시작 단계 (Windows 와 같은 차례 - app_start). 화면 언어 다음에 화면 방향 · 앱 아이콘
|
|
final (:live, :bookmarks) = await startCore(controller, dataDir, sharedSnapshots: sharedSnapshots, afterLanguage: () async {
|
|
await applyScreenOrientation(controller.settings.display.orientation);
|
|
unawaited(services.shell.setAppIcon(controller.settings.display.appIcon));
|
|
});
|
|
final navigatorKey = GlobalKey<NavigatorState>();
|
|
final messengerKey = GlobalKey<ScaffoldMessengerState>();
|
|
AndroidStorageService.navigatorKey = navigatorKey;
|
|
attachMasterPrompt(navigatorKey);
|
|
|
|
// 다운로드 (앱 안 브라우저의 [다운로드] · 다운로드 목록): 앱에 넣은 yt-dlp · aria2c. 클립보드 감시는 하지 않는다.
|
|
AndroidDownloadTools.log = controller.note;
|
|
final downloads = createDownloads(controller, messengerKey: messengerKey, navigatorKey: navigatorKey);
|
|
watchDnsFailure(messengerKey, controller);
|
|
// 진행 중인 일이 있으면 화면에서 내려가도 계속 (알림에 진행 상황)
|
|
AndroidKeepAlive(controller, downloads);
|
|
// 170: 업데이트 설치의 첫 시도 실패 원문을 작업 기록에
|
|
final updater = services.updater;
|
|
if (updater is AndroidUpdater) updater.log = controller.note;
|
|
// 백그라운드로 실행: 화면 (Activity) 을 닫아도 엔진은 살아 있다가 다시 열면 새 화면에 붙는다 → 화면 방향을 다시 적용
|
|
// (웹뷰는 텍스처로 그려 새 화면에 붙어도 그대로 보인다: browser_page _webSettings)
|
|
// 116: 권한 ("모든 파일에 대한 접근") 이 없어 분석에 실패한 동영상은 허용하고 돌아오면 저절로 다시 분석
|
|
controller.fileAccess = AndroidAccess.hasAllFiles;
|
|
var hadAccess = await AndroidAccess.hasAllFiles();
|
|
if (hadAccess) await allFilesGuideDue(controller, hasAccess: true);
|
|
AppLifecycleListener(onResume: () async {
|
|
await applyScreenOrientation(controller.settings.display.orientation);
|
|
final now = await AndroidAccess.hasAllFiles();
|
|
if (now && !hadAccess) await controller.reanalyzeFailed();
|
|
if (now) await allFilesGuideDue(controller, hasAccess: true);
|
|
hadAccess = now;
|
|
});
|
|
// 동영상 목록 기억 (앱을 껐다 켜도 그대로, 없어진 파일은 뺀다) - Windows 와 같은 파일
|
|
unawaited(controller.shareVideoList(p.join(dataDir, 'videos.json')));
|
|
|
|
// 연결 프로그램 · 공유로 받은 동영상: 앱이 켜져 있을 때 (MainActivity.onNewIntent)
|
|
const android = MethodChannel('jj_mkvmaker/android');
|
|
android.setMethodCallHandler((call) async {
|
|
if (call.method == 'showJobs') {
|
|
// 작업 알림을 눌렀다: 작업 현황 화면으로
|
|
final nav = navigatorKey.currentState;
|
|
if (nav != null) unawaited(WorkStatusPage.open(nav, controller, downloads));
|
|
return null;
|
|
}
|
|
if (call.method == 'openFiles') {
|
|
await openFromOtherApp(navigatorKey, controller, (call.arguments as List).cast<Map<Object?, Object?>>());
|
|
}
|
|
return null;
|
|
});
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
// 연결 프로그램 · 공유로 앱을 켠 경우: 그 동영상부터
|
|
try {
|
|
if (await android.invokeMethod<bool>('takeShowJobs') ?? false) {
|
|
final nav = navigatorKey.currentState;
|
|
if (nav != null) unawaited(WorkStatusPage.open(nav, controller, downloads));
|
|
}
|
|
final opened = await android.invokeMethod<List<Object?>>('takeOpenedFiles') ?? const [];
|
|
if (opened.isNotEmpty) {
|
|
unawaited(openFromOtherApp(navigatorKey, controller, opened.cast<Map<Object?, Object?>>()));
|
|
return;
|
|
}
|
|
} catch (_) {}
|
|
// 동영상을 읽고 옆에 MKV 를 만들려면 저장소 전체 접근이 필요하다.
|
|
// 39: 켤 때마다 띄우지 않고 한 번만. 그 뒤로는 파일 탐색기 위의 안내 · 동영상 항목의 안내가 알려 준다
|
|
// 116: 다른 안내 · 분석 오류보다 먼저 (허용하고 돌아오면 실패한 분석을 다시 한다 - onResume)
|
|
final ctxA = navigatorKey.currentContext;
|
|
if (ctxA != null && ctxA.mounted && await allFilesGuideDue(controller, hasAccess: hadAccess) && ctxA.mounted) {
|
|
await showAllFilesGuide(ctxA, AndroidAccess.request, hasAccess: AndroidAccess.hasAllFiles);
|
|
}
|
|
// 124: "처음 시작 시" 면 마스터 비밀번호
|
|
final ctxM = navigatorKey.currentContext;
|
|
if (ctxM != null && ctxM.mounted) await askMasterAtStartup(ctxM, controller);
|
|
final home = AppNavButtons.homeId(controller.settings);
|
|
final nav0 = navigatorKey.currentState;
|
|
if (home != 'mkv' && nav0 != null) {
|
|
unawaited(AppNavButtons.openPage(nav0, home, c: controller, downloads: downloads, bookmarks: bookmarks));
|
|
}
|
|
// 다시 켤 때 실시간 동기화를 골라서 시작
|
|
final ctx0 = navigatorKey.currentContext;
|
|
if (controller.settings.copy.liveSyncOnStart == 'ask' && ctx0 != null && ctx0.mounted) await askLiveSyncStart(ctx0, live);
|
|
// 다른 버전을 쓰다가 돌아왔거나 앱을 다시 설치했으면 보관해 둔 설정을 되살릴지 (되살리면 앱을 끝내고 다시 켜 달라고)
|
|
final current = await currentAppVersion(services);
|
|
final ctxV = navigatorKey.currentContext;
|
|
if (ctxV != null && ctxV.mounted) await showSettingsProblem(ctxV, controller);
|
|
watchSecretIssue(messengerKey, controller);
|
|
if (ctxV != null && ctxV.mounted) await showMigrationNotice(ctxV, controller);
|
|
if (ctxV != null && ctxV.mounted) {
|
|
await checkVersionRestore(ctxV, controller, current: current, freshInstall: freshInstall, autoBackup: autoBackup,
|
|
restart: () async {
|
|
messengerKey.currentState?.showSnackBar(SnackBar(content: Text(tr('설정을 되살렸습니다. 앱을 끝냅니다 - 다시 켜 주세요.'))));
|
|
await Future<void>.delayed(const Duration(seconds: 2));
|
|
await services.shell.quit();
|
|
});
|
|
}
|
|
// P0: 되돌리느라 공용 폴더에 옮겨 둔 받은 AI 모델 되살리기
|
|
final ctxModels = navigatorKey.currentContext;
|
|
if (ctxModels != null && ctxModels.mounted) await restoreModelBackup(ctxModels, controller);
|
|
// 새 버전 확인 (하루 한 번, 환경 설정에서 끌 수 있음)
|
|
final ctx = navigatorKey.currentContext;
|
|
if (ctx != null && ctx.mounted) unawaited(checkForUpdate(ctx, controller));
|
|
});
|
|
|
|
runApp(JjMkvMakerApp(
|
|
controller: controller,
|
|
downloads: downloads,
|
|
bookmarks: bookmarks,
|
|
navigatorKey: navigatorKey,
|
|
messengerKey: messengerKey,
|
|
onExit: () async {
|
|
final ctx = navigatorKey.currentContext;
|
|
if (ctx != null && ctx.mounted && !await confirmExit(ctx, downloads)) return;
|
|
if (controller.busy && ctx != null && ctx.mounted) {
|
|
final ok = await showDialog<bool>(
|
|
context: ctx,
|
|
builder: (c) => AlertDialog(
|
|
title: Text(tr('종료')),
|
|
content: Text(trf('진행 중인 작업이 있습니다: {0}\n중지하고 끝낼까요?', [controller.currentJob ?? ''])),
|
|
actions: [
|
|
TextButton(onPressed: () => Navigator.pop(c, false), child: Text(tr('취소'))),
|
|
FilledButton(onPressed: () => Navigator.pop(c, true), child: Text(tr('종료'))),
|
|
],
|
|
),
|
|
);
|
|
if (ok != true) return;
|
|
}
|
|
controller.cancel();
|
|
await downloads.shutdown();
|
|
await TorService.instance.stop();
|
|
controller.note(tr('── 정상 종료 ──'));
|
|
await services.shell.quit();
|
|
},
|
|
));
|
|
}
|