163 lines
6.8 KiB
Dart
163 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;
|
|
|
|
/// 이번 실행에서 경고를 보였는지 (관리자 결정 10/21: 앱을 켠 뒤 처음 열 때 한 번)
|
|
static bool shizukuWarned = false, rootWarned = false;
|
|
|
|
@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 (!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;
|
|
rootWarned = true;
|
|
}
|
|
try {
|
|
final st = await ShizukuRemote.rootBackend.status();
|
|
if (!st.running) {
|
|
ctl.host.snack(tr('이 기기에는 su 가 없습니다 - 루팅한 기기에서만 쓸 수 있습니다'));
|
|
return;
|
|
}
|
|
if (!st.granted && !await ShizukuRemote.rootBackend.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 (!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;
|
|
shizukuWarned = true;
|
|
}
|
|
try {
|
|
final st = await ShizukuRemote.backend.status();
|
|
if (!st.running) {
|
|
ctl.host.snack(tr('Shizuku 가 켜져 있지 않습니다 - Shizuku 앱을 설치하고 (무선 디버깅 · adb 로) 시작한 뒤 다시 누르세요'));
|
|
return;
|
|
}
|
|
if (!st.granted && !await ShizukuRemote.backend.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));
|
|
}
|
|
}
|