import 'dart:async'; import 'dart:io'; import 'package:flutter/services.dart'; import 'package:path/path.dart' as p; import '../l10n/tr.dart'; import 'remote_fs.dart'; import 'webdav.dart' show DavItem; /// X-plore 비교 5-2 시스템 폴더 보기 (사용자 결정 10/21, 관리자 결정: 1단계는 Shizuku 만 · **읽기 전용** · 처음 끔). /// 경로: `shz://sys/<기기 절대 경로>` (예 shz://sys/storage/emulated/0/Android/data). 목록 · 정보 · 내용 읽기만 하고, /// 쓰기 · 지우기 · 옮기기는 [ShizukuReadOnlyException] (탐색기도 그 전에 막는다 - 압축 안처럼) const shizukuScheme = 'shz://'; const shizukuServer = 'sys'; bool isShizuku(String path) => path.startsWith(shizukuScheme); /// 시스템 폴더 보기를 쓰는 기기 (Android - 시험에서 바꿈) bool Function() shizukuSupported = () => Platform.isAndroid; /// 탐색기 탭이 처음 여는 곳 (Android 11 부터 막힌 Android/data · obb 가 있는 폴더) String shizukuHome(String storageRoot) => '$shizukuScheme$shizukuServer${storageRoot.replaceAll('\\', '/')}/Android'; /// 기기와 이야기하는 쪽 (시험에서 바꿔 끼움) abstract class ShizukuBackend { /// Shizuku 가 켜져 있는지 · 이 앱이 허락을 받았는지 Future<({bool running, bool granted})> status(); /// Shizuku 허락 창 (허락하면 true) Future request(); /// "종류|크기|바뀐 때 (초)|이름" 줄들 Future list(String path); /// 한 항목의 "종류|크기|바뀐 때|이름" (없으면 빈 글) Future stat(String path); /// 내용을 [dest] 에 복사 (바이트 수) Future copyTo(String path, String dest); } class AndroidShizuku implements ShizukuBackend { static const _ch = MethodChannel('jj_mkvmaker/android'); @override Future<({bool running, bool granted})> status() async { final m = await _ch.invokeMethod>('shizukuStatus') ?? const {}; return (running: m['running'] == true, granted: m['granted'] == true); } @override Future request() async => await _ch.invokeMethod('shizukuRequest') ?? false; @override Future list(String path) async => await _ch.invokeMethod('shizukuList', {'path': path}) ?? ''; @override Future stat(String path) async => await _ch.invokeMethod('shizukuStat', {'path': path}) ?? ''; @override Future copyTo(String path, String dest) async => (await _ch.invokeMethod('shizukuCopy', {'path': path, 'dest': dest}))?.toInt() ?? 0; } class ShizukuReadOnlyException extends FileSystemException { ShizukuReadOnlyException(String path) : super(tr('시스템 폴더 (Shizuku) 는 읽기 전용입니다'), path); } /// toybox `stat -c '%F|%s|%Y|%n'` 한 줄 → (폴더인지, 크기, 바뀐 때, 이름). 틀리면 null (bool, int, DateTime, String)? parseShizukuStat(String line) { final parts = line.split('|'); if (parts.length < 4) return null; final name = parts.sublist(3).join('|'); // 이름에 | 가 있어도 final size = int.tryParse(parts[1].trim()); final secs = int.tryParse(parts[2].trim()); if (size == null || secs == null || name.isEmpty) return null; return (parts[0].trim() == 'directory', size, DateTime.fromMillisecondsSinceEpoch(secs * 1000), name); } /// [RemoteClient] 로 탐색기 · 복사 (꺼내기) · 보기가 그대로 쓰게. rel = 기기 절대 경로 class ShizukuRemote implements RemoteClient { static ShizukuBackend backend = AndroidShizuku(); /// 내용을 잠깐 받아 둘 앱 전용 폴더 (시험에서 바꿈) static Future Function() tempDir = () async => Directory.systemTemp.path; static final instance = ShizukuRemote(); String _abs(String rel) => rel.isEmpty ? '/' : rel; @override Future> list(String rel) async { final out = []; for (final line in (await backend.list(_abs(rel))).split('\n')) { final s = parseShizukuStat(line.trimRight()); if (s == null) continue; final (dir, size, modified, name) = s; if (name == '.' || name == '..') continue; out.add(DavItem(rel == '/' ? '/$name' : '$rel/$name', isDir: dir, size: dir ? 0 : size, modified: modified)); } return out; } @override Future stat(String rel) async { final s = parseShizukuStat((await backend.stat(_abs(rel))).trim()); if (s == null) return null; return DavItem(rel, isDir: s.$1, size: s.$1 ? 0 : s.$2, modified: s.$3); } /// 앱 전용 임시 폴더에 받아 두고 읽은 뒤 지운다 @override Future>> openRead(String rel) async { final dir = Directory(p.join(await tempDir(), 'jj_shz_${DateTime.now().microsecondsSinceEpoch}')); await dir.create(recursive: true); final f = File(p.join(dir.path, 'f')); try { await backend.copyTo(_abs(rel), f.path); } catch (_) { await dir.delete(recursive: true).catchError((_) => dir); rethrow; } final ctl = StreamController>(); ctl.addStream(f.openRead()).whenComplete(() async { await ctl.close(); await dir.delete(recursive: true).catchError((_) => dir); }); return ctl.stream; } @override Future mkdirs(String rel) async => throw ShizukuReadOnlyException(rel); @override Future delete(String rel) async => throw ShizukuReadOnlyException(rel); @override Future move(String from, String to, {bool overwrite = false}) async => throw ShizukuReadOnlyException(from); @override Future write(String rel, Stream> data, {int? length}) async => throw ShizukuReadOnlyException(rel); @override Future<(int?, int?)> quota() async => (null, null); @override void close() {} }