145 lines
5.6 KiB
Dart
145 lines
5.6 KiB
Dart
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<bool> request();
|
|
|
|
/// "종류|크기|바뀐 때 (초)|이름" 줄들
|
|
Future<String> list(String path);
|
|
|
|
/// 한 항목의 "종류|크기|바뀐 때|이름" (없으면 빈 글)
|
|
Future<String> stat(String path);
|
|
|
|
/// 내용을 [dest] 에 복사 (바이트 수)
|
|
Future<int> 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<Map<Object?, Object?>>('shizukuStatus') ?? const {};
|
|
return (running: m['running'] == true, granted: m['granted'] == true);
|
|
}
|
|
|
|
@override
|
|
Future<bool> request() async => await _ch.invokeMethod<bool>('shizukuRequest') ?? false;
|
|
|
|
@override
|
|
Future<String> list(String path) async => await _ch.invokeMethod<String>('shizukuList', {'path': path}) ?? '';
|
|
|
|
@override
|
|
Future<String> stat(String path) async => await _ch.invokeMethod<String>('shizukuStat', {'path': path}) ?? '';
|
|
|
|
@override
|
|
Future<int> copyTo(String path, String dest) async =>
|
|
(await _ch.invokeMethod<num>('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<String> Function() tempDir = () async => Directory.systemTemp.path;
|
|
|
|
static final instance = ShizukuRemote();
|
|
|
|
String _abs(String rel) => rel.isEmpty ? '/' : rel;
|
|
|
|
@override
|
|
Future<List<DavItem>> list(String rel) async {
|
|
final out = <DavItem>[];
|
|
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<DavItem?> 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<Stream<List<int>>> 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<List<int>>();
|
|
ctl.addStream(f.openRead()).whenComplete(() async {
|
|
await ctl.close();
|
|
await dir.delete(recursive: true).catchError((_) => dir);
|
|
});
|
|
return ctl.stream;
|
|
}
|
|
|
|
@override
|
|
Future<void> mkdirs(String rel) async => throw ShizukuReadOnlyException(rel);
|
|
@override
|
|
Future<void> delete(String rel) async => throw ShizukuReadOnlyException(rel);
|
|
@override
|
|
Future<void> move(String from, String to, {bool overwrite = false}) async => throw ShizukuReadOnlyException(from);
|
|
@override
|
|
Future<void> write(String rel, Stream<List<int>> data, {int? length}) async => throw ShizukuReadOnlyException(rel);
|
|
@override
|
|
Future<(int?, int?)> quota() async => (null, null);
|
|
@override
|
|
void close() {}
|
|
}
|