91 lines
3.1 KiB
Dart
91 lines
3.1 KiB
Dart
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) 은 쓰지 않고
|
|
/// 매니페스트 `<queries>` 의 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<Object?, Object?> 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<InstalledApp>> list();
|
|
Future<Uint8List?> icon(String package);
|
|
Future<bool> details(String package);
|
|
Future<bool> uninstall(String package);
|
|
|
|
/// APK 백업 폴더 (Download/JJ_MKVMaker/apk)
|
|
Future<String> backupDir();
|
|
}
|
|
|
|
class AndroidAppManager implements AppManagerBackend {
|
|
static const _ch = MethodChannel('jj_mkvmaker/android');
|
|
|
|
@override
|
|
Future<List<InstalledApp>> list() async {
|
|
final l = await _ch.invokeMethod<List<Object?>>('launcherApps') ?? const [];
|
|
return [for (final m in l) if (m is Map) InstalledApp.fromMap(m)];
|
|
}
|
|
|
|
@override
|
|
Future<Uint8List?> icon(String package) => _ch.invokeMethod<Uint8List>('appIcon', {'package': package, 'size': 96});
|
|
|
|
@override
|
|
Future<bool> details(String package) async => await _ch.invokeMethod<bool>('appDetails', {'package': package}) ?? false;
|
|
|
|
@override
|
|
Future<bool> uninstall(String package) async => await _ch.invokeMethod<bool>('uninstallApp', {'package': package}) ?? false;
|
|
|
|
@override
|
|
Future<String> backupDir() async {
|
|
final root = await _ch.invokeMethod<String>('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<String> 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;
|
|
}
|