JJ_MKVMaker/lib/ui/explorer/access_flow.dart

162 lines
6.8 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import '../../core/shizuku.dart';
import '../../l10n/tr.dart';
import '../../platform/android/android_storage.dart';
import '../theme.dart';
import 'explorer_controller.dart';
import 'explorer_pane.dart';
import '../confirm_dialog.dart';
/// 약속 (E5d-6): 저장 장치에 닿는 일 - Android "모든 파일에 대한 접근" 권한 ([allFiles] · 듀얼 앱, 앱으로 돌아오면 다시 확인),
/// 시스템 폴더 (Shizuku) · 루트 (su) 탭 (이번 실행에서 처음이면 경고 → 켜져 있고 허락받았을 때만 연다, 읽기 전용),
/// 연결 안 된 드라이브 다시 연결 ([reconnect] - 되면 연다, 아니면 까닭을 알림). 바뀌면 알림
class ExplorerAccess extends ChangeNotifier {
ExplorerAccess(this.ctl, {required this.probe});
final ExplorerController ctl;
/// 드라이브를 열 수 있는지 (화면을 멈추지 않는 비동기)
final Future<bool> Function(String path) probe;
// 이번 실행에서 경고를 보였는지는 ctl.c.session (설계 M5 AppSession - 예전 static)
@override
void dispose() {
if (allFiles != null) _life.dispose();
super.dispose();
}
Future<void> probeDrive(String path) => ctl.drives.check(path, probe);
/// 연결 안 된 드라이브를 누르면 다시 연결 (다시 읽기) - 되면 연다
Future<void> reconnect(ExplorerPane pane, String path) async {
await probeDrive(path);
if (!ctl.host.alive) return;
if (ctl.drives.ok[path] == true) {
pane.cache.remove(path);
await ctl.goTo(pane, path);
} else {
ctl.host.snack(ctl.drives.removable.contains(path)
? trf('{0} 이 비어 있습니다 (카드 · USB · 디스크를 넣은 뒤 다시 누르세요).', [path])
: trf('{0} 에 연결할 수 없습니다. 네트워크 · NAS 가 켜져 있는지 확인하세요.', [path]));
}
}
// ───────── 권한 (Android) ─────────
/// "모든 파일에 대한 접근" 권한 (없으면 폴더만 보이고 파일은 안 보인다). null = 아직 모름
bool? allFiles;
/// 듀얼 앱 (복제한 앱) 인지: 저장소가 /storage/emulated/0 이 아님
bool dualApp = false;
late final AppLifecycleListener _life = AppLifecycleListener(onResume: () {
if (Platform.isAndroid && allFiles == false) unawaited(checkAccess(reload: true));
});
Future<void> checkAccess({bool reload = false}) async {
_life; // 다시 돌아오면 (권한 화면에서 허용하고 오면) 다시 확인
final ok = await AndroidAccess.hasAllFiles();
var dual = false;
try {
final root = await AndroidAccess.storageRoot();
dual = root.contains('/emulated/') && !root.endsWith('/emulated/0');
} catch (_) {}
if (!ctl.host.alive) return;
final changed = ok != allFiles;
allFiles = ok;
dualApp = dual;
notifyListeners();
if (reload && changed && ok) {
for (final pane in ctl.panes) {
pane.cache.clear();
for (final d in pane.expanded.toList()) {
await ctl.load(pane, d, force: true);
}
}
}
}
/// X-plore 비교 5-2: 루트 탭 - 이번 실행에서 처음이면 경고, su 가 있고 허락을 받았을 때만 "/" 로 (관리자 3a 결정)
Future<void> openRoot(BuildContext context, ExplorerPane pane) async {
if (!ctl.c.session.rootWarned) {
// 공용 확인 창: 첫 초점 [취소] (Enter 로 바로 열리지 않음)
final ok = await confirmDialog(
context,
icon: Icon(Icons.security, color: JjColors.warning),
title: () => tr('루트 폴더 보기 (읽기 전용)'),
body: (_, _) => Text(tr('루트 (su) 권한으로 시스템 · 다른 앱의 폴더 전체를 봅니다. 다른 앱의 개인 자료 (대화 · 로그인 정보 · 비밀번호 등) 도 '
'보일 수 있습니다. 이 앱은 읽기만 하고 (쓰기 · 지우기 · 이름 바꾸기 없음), 꺼낸 파일은 고른 곳에만 둡니다.')),
confirmLabel: () => tr('알고 봅니다'),
cancelFirst: true,
);
if (!ok || !context.mounted) return;
ctl.c.session.rootWarned = true;
}
try {
final st = await PrivilegedFs.current.root.status();
if (!st.running) {
ctl.host.snack(tr('이 기기에는 su 가 없습니다 - 루팅한 기기에서만 쓸 수 있습니다'));
return;
}
if (!st.granted && !await PrivilegedFs.current.root.request()) {
ctl.host.snack(tr('루트 허락이 없어 볼 수 없습니다 (su 앱에서 이 앱을 허락하세요)'));
return;
}
} catch (e) {
ctl.host.snack(trf('루트에 연결하지 못했습니다: {0}', [e]));
return;
}
if (!context.mounted) return;
ctl
..active = ctl.panes.indexOf(pane)
..update();
await ctl.goTo(pane, rootHome);
}
/// X-plore 비교 5-2: 시스템 폴더 탭 - 이번 실행에서 처음이면 경고, Shizuku 가 켜져 있고 허락을 받았을 때만 Android 폴더로
Future<void> openShizuku(BuildContext context, ExplorerPane pane) async {
if (!ctl.c.session.shizukuWarned) {
final ok = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
scrollable: true,
icon: Icon(Icons.admin_panel_settings_outlined, color: JjColors.warning),
title: Text(tr('시스템 폴더 보기 (읽기 전용)')),
content: Text(tr('Shizuku 의 권한으로 Android/data · obb 처럼 막힌 폴더를 봅니다. 다른 앱의 데이터 (대화 · 로그인 정보 등) 가 '
'있을 수 있습니다. 이 앱은 읽기만 하고 (쓰기 · 지우기 · 이름 바꾸기 없음), 꺼낸 파일은 고른 곳에만 둡니다.')),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('알고 봅니다'))),
],
),
);
if (ok != true || !context.mounted) return;
ctl.c.session.shizukuWarned = true;
}
try {
final st = await PrivilegedFs.current.shizuku.status();
if (!st.running) {
ctl.host.snack(tr('Shizuku 가 켜져 있지 않습니다 - Shizuku 앱을 설치하고 (무선 디버깅 · adb 로) 시작한 뒤 다시 누르세요'));
return;
}
if (!st.granted && !await PrivilegedFs.current.shizuku.request()) {
ctl.host.snack(tr('Shizuku 허락이 없어 볼 수 없습니다'));
return;
}
} catch (e) {
ctl.host.snack(trf('Shizuku 에 연결하지 못했습니다: {0}', [e]));
return;
}
if (!context.mounted) return;
ctl
..active = ctl.panes.indexOf(pane)
..update();
await ctl.goTo(pane, shizukuHome(ctl.local.first.$1));
}
}