import 'dart:io'; import 'package:flutter/services.dart'; import 'package:path/path.dart' as p; /// X-plore 비교 5-2 앱 관리자 (사용자 결정 10/21, Android). 관리자 판단: 모든 앱 목록 권한 (QUERY_ALL_PACKAGES) 은 쓰지 않고 /// 매니페스트 `` 의 LAUNCHER 로 "실행할 수 있는 앱" (런처에 보이는 앱) 만 본다. class InstalledApp { final String package, label, version, apk; final DateTime installed, updated; final int size, splits; final bool system; const InstalledApp({ required this.package, required this.label, required this.version, required this.apk, required this.installed, required this.updated, required this.size, this.splits = 0, this.system = false, }); factory InstalledApp.fromMap(Map m) { int n(String k) => (m[k] as num?)?.toInt() ?? 0; return InstalledApp( package: '${m['package'] ?? ''}', label: '${m['label'] ?? m['package'] ?? ''}', version: '${m['version'] ?? ''}', apk: '${m['apk'] ?? ''}', installed: DateTime.fromMillisecondsSinceEpoch(n('installed')), updated: DateTime.fromMillisecondsSinceEpoch(n('updated')), size: n('size'), splits: n('splits'), system: m['system'] == true, ); } } /// 기기와 이야기하는 쪽 (시험에서 바꿔 끼움) abstract class AppManagerBackend { Future> list(); Future icon(String package); Future details(String package); Future uninstall(String package); /// APK 백업 폴더 (Download/JJ_MKVMaker/apk) Future backupDir(); } class AndroidAppManager implements AppManagerBackend { static const _ch = MethodChannel('jj_mkvmaker/android'); @override Future> list() async { final l = await _ch.invokeMethod>('launcherApps') ?? const []; return [for (final m in l) if (m is Map) InstalledApp.fromMap(m)]; } @override Future icon(String package) => _ch.invokeMethod('appIcon', {'package': package, 'size': 96}); @override Future details(String package) async => await _ch.invokeMethod('appDetails', {'package': package}) ?? false; @override Future uninstall(String package) async => await _ch.invokeMethod('uninstallApp', {'package': package}) ?? false; @override Future backupDir() async { final root = await _ch.invokeMethod('storageRoot') ?? '/storage/emulated/0'; return p.join(root, 'Download', 'JJ_MKVMaker', 'apk'); } } /// 파일 이름에 못 쓰는 글자를 _ 로 String _safe(String s) => s.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim(); /// APK 를 [dir] 에 복사한다 ("이름_판.apk", 이미 있으면 " (2)" …). 만든 파일 경로 Future backupApk(InstalledApp app, String dir) async { await Directory(dir).create(recursive: true); final base = '${_safe(app.label)}_${_safe(app.version.isEmpty ? app.package : app.version)}'; var out = p.join(dir, '$base.apk'); for (var i = 2; await File(out).exists(); i++) { out = p.join(dir, '$base ($i).apk'); } await File(app.apk).copy(out); return out; }