M3 PrivilegedFs: 시스템 폴더 · 루트 읽기의 static 넷 · 전역 함수 둘을 주입으로
- core/shizuku.dart PrivilegedFs: Shizuku 뒤쪽 · 루트 뒤쪽 · 임시 폴더를 한 곳에서. 앱은 켤 때 PlatformServices.privileged 를 건다 (Android 조립만 PrivilegedFs.device(앱 임시 폴더) 를 넘김, 원격 서버 목록처럼 한 번). 예전 ShizukuRemote.backend · rootBackend · tempDir · SuRoot.start (static) 를 대신 - SuRoot 의 프로세스 시작은 생성자로 넘김 - shizukuSupported · rootSupported (전역 함수) 를 빼고 c.services.platform.isAndroid (M1 PlatformInfo) 로 - Android 에서 Shizuku · 루트 내용을 잠깐 받는 곳이 시스템 TEMP (앱 캐시) → 앱 임시 폴더 (앱 데이터/tmp, 켤 때 비움) - 둘 다 앱 전용 - 시험: privileged_fs_contract_test (넘기면 걸고 안 넘기면 그대로 · device 꼴). shizuku · root · 보관함 시험은 PrivilegedFs.use 로 2회 통과 - 사용자가 보게 될 동작: 없음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>modular
parent
f1624759ba
commit
26d6b01752
|
|
@ -30,6 +30,7 @@ PlatformServices shotServices() {
|
||||||
diskSpace: (_) async => (107374182400, 536870912000),
|
diskSpace: (_) async => (107374182400, 536870912000),
|
||||||
clock: r.clock,
|
clock: r.clock,
|
||||||
vault: r.vault,
|
vault: r.vault,
|
||||||
|
privileged: r.privileged,
|
||||||
updater: r.updater,
|
updater: r.updater,
|
||||||
usage: r.usage == null ? null : const FixedUsage(),
|
usage: r.usage == null ? null : const FixedUsage(),
|
||||||
models: r.models,
|
models: r.models,
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import '../core/srt.dart';
|
||||||
import '../core/subtitle_search.dart';
|
import '../core/subtitle_search.dart';
|
||||||
import '../services/subtitle_provider.dart';
|
import '../services/subtitle_provider.dart';
|
||||||
import '../services/media_tool.dart';
|
import '../services/media_tool.dart';
|
||||||
|
import '../core/shizuku.dart' show PrivilegedFs;
|
||||||
import '../services/platform_services.dart';
|
import '../services/platform_services.dart';
|
||||||
import '../services/system_usage.dart';
|
import '../services/system_usage.dart';
|
||||||
import 'settings.dart';
|
import 'settings.dart';
|
||||||
|
|
@ -42,7 +43,10 @@ class AppController extends ChangeNotifier {
|
||||||
final SettingsStore? settingsStore;
|
final SettingsStore? settingsStore;
|
||||||
AppSettings settings = AppSettings();
|
AppSettings settings = AppSettings();
|
||||||
|
|
||||||
AppController(this.services, {this.settingsStore});
|
AppController(this.services, {this.settingsStore}) {
|
||||||
|
// 설계 M3: 시스템 폴더 · 루트 읽기는 앱 조립이 넘긴 것으로 (넘기지 않으면 그대로)
|
||||||
|
if (services.privileged != null) PrivilegedFs.use(services.privileged!);
|
||||||
|
}
|
||||||
|
|
||||||
/// 종료 전 확인 (다운로드 중이면 목록 확인). main 에서 연결, 업데이트 설치 때 사용
|
/// 종료 전 확인 (다운로드 중이면 목록 확인). main 에서 연결, 업데이트 설치 때 사용
|
||||||
Future<bool> Function()? confirmQuit;
|
Future<bool> Function()? confirmQuit;
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,6 @@ const shizukuServer = 'sys';
|
||||||
|
|
||||||
bool isShizuku(String path) => path.startsWith(shizukuScheme);
|
bool isShizuku(String path) => path.startsWith(shizukuScheme);
|
||||||
|
|
||||||
/// 시스템 폴더 보기를 쓰는 기기 (Android - 시험에서 바꿈)
|
|
||||||
bool Function() shizukuSupported = () => Platform.isAndroid;
|
|
||||||
|
|
||||||
/// 탐색기 탭이 처음 여는 곳 (Android 11 부터 막힌 Android/data · obb 가 있는 폴더)
|
/// 탐색기 탭이 처음 여는 곳 (Android 11 부터 막힌 Android/data · obb 가 있는 폴더)
|
||||||
String shizukuHome(String storageRoot) => '$shizukuScheme$shizukuServer${storageRoot.replaceAll('\\', '/')}/Android';
|
String shizukuHome(String storageRoot) => '$shizukuScheme$shizukuServer${storageRoot.replaceAll('\\', '/')}/Android';
|
||||||
|
|
||||||
|
|
@ -29,8 +26,7 @@ const rootServer = 'root';
|
||||||
|
|
||||||
bool isRootPath(String path) => path.startsWith('$shizukuScheme$rootServer/') || path == '$shizukuScheme$rootServer';
|
bool isRootPath(String path) => path.startsWith('$shizukuScheme$rootServer/') || path == '$shizukuScheme$rootServer';
|
||||||
|
|
||||||
/// 루트 보기를 쓰는 기기 (Android - 시험에서 바꿈)
|
// 시스템 폴더 · 루트 보기를 쓰는 기기인지는 PlatformInfo.isAndroid (c.services.platform - 예전 전역 함수 shizukuSupported · rootSupported)
|
||||||
bool Function() rootSupported = () => Platform.isAndroid;
|
|
||||||
|
|
||||||
/// 루트 탭이 처음 여는 곳 (관리자 3a 결정 10/21: "/" - 다른 앱의 개인 자료가 있는 /data 를 고르지 않았는데 펼치지 않게)
|
/// 루트 탭이 처음 여는 곳 (관리자 3a 결정 10/21: "/" - 다른 앱의 개인 자료가 있는 /data 를 고르지 않았는데 펼치지 않게)
|
||||||
const rootHome = '$shizukuScheme$rootServer/';
|
const rootHome = '$shizukuScheme$rootServer/';
|
||||||
|
|
@ -49,8 +45,9 @@ String shellCatScript(String path) => 'cat -- ${shellPath(path)}';
|
||||||
|
|
||||||
/// su 로 (루팅한 기기). 이번 실행에서 허락을 받았는지만 기억한다 (허락 · 거절은 Magisk 같은 su 앱이 기억)
|
/// su 로 (루팅한 기기). 이번 실행에서 허락을 받았는지만 기억한다 (허락 · 거절은 Magisk 같은 su 앱이 기억)
|
||||||
class SuRoot implements ShizukuBackend {
|
class SuRoot implements ShizukuBackend {
|
||||||
/// 프로세스 시작 (시험에서 바꿈)
|
/// 프로세스 시작 (처음 값 = 실제 Process.start - 시험은 가짜 su 를 넘긴다)
|
||||||
static Future<Process> Function(String exe, List<String> args) start = (exe, args) => Process.start(exe, args);
|
final Future<Process> Function(String exe, List<String> args) start;
|
||||||
|
SuRoot({Future<Process> Function(String exe, List<String> args)? start}) : start = start ?? Process.start;
|
||||||
|
|
||||||
static bool _granted = false;
|
static bool _granted = false;
|
||||||
|
|
||||||
|
|
@ -187,23 +184,34 @@ class ShizukuReadOnlyException extends FileSystemException {
|
||||||
return (parts[0].trim() == 'directory', size, DateTime.fromMillisecondsSinceEpoch(secs * 1000), name);
|
return (parts[0].trim() == 'directory', size, DateTime.fromMillisecondsSinceEpoch(secs * 1000), name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [RemoteClient] 로 탐색기 · 복사 (꺼내기) · 보기가 그대로 쓰게. rel = 기기 절대 경로
|
/// 약속 (M3 PrivilegedFs): 시스템 폴더 (Shizuku) · 루트 (su) 읽기의 뒤쪽 둘과 내용을 잠깐 받아 둘 폴더 - 한 곳에서 정한다.
|
||||||
|
/// [device] = 실제 (AndroidShizuku · su, 임시는 넘긴 폴더). 앱은 켤 때 PlatformServices.privileged 를 [use] 로 건다 (원격 서버 목록처럼 한 번).
|
||||||
|
/// 시험은 [use] 로 가짜 뒤쪽을 걸고 끝에 되돌린다. 예전의 ShizukuRemote.backend · rootBackend · tempDir · SuRoot.start (따로 바꾸던 static 넷) 를 대신한다
|
||||||
|
class PrivilegedFs {
|
||||||
|
final ShizukuBackend shizuku;
|
||||||
|
final ShizukuBackend root;
|
||||||
|
|
||||||
|
/// 내용을 잠깐 받아 둘 폴더
|
||||||
|
final Future<String> Function() tempDir;
|
||||||
|
const PrivilegedFs({required this.shizuku, required this.root, required this.tempDir});
|
||||||
|
|
||||||
|
factory PrivilegedFs.device(Future<String> Function() tempDir) => PrivilegedFs(shizuku: AndroidShizuku(), root: SuRoot(), tempDir: tempDir);
|
||||||
|
|
||||||
|
/// 지금 쓰는 것 (처음 값 = 실제 뒤쪽 · 시스템 임시 폴더)
|
||||||
|
static PrivilegedFs current = PrivilegedFs.device(() async => Directory.systemTemp.path);
|
||||||
|
|
||||||
|
static void use(PrivilegedFs fs) => current = fs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [RemoteClient] 로 탐색기 · 복사 (꺼내기) · 보기가 그대로 쓰게. rel = 기기 절대 경로. 뒤쪽 · 임시 폴더는 [PrivilegedFs.current]
|
||||||
class ShizukuRemote implements RemoteClient {
|
class ShizukuRemote implements RemoteClient {
|
||||||
static ShizukuBackend backend = AndroidShizuku();
|
|
||||||
|
|
||||||
/// 루트 (su) 쪽 - 시험에서 바꿈
|
|
||||||
static ShizukuBackend rootBackend = SuRoot();
|
|
||||||
|
|
||||||
/// 내용을 잠깐 받아 둘 앱 전용 폴더 (시험에서 바꿈)
|
|
||||||
static Future<String> Function() tempDir = () async => Directory.systemTemp.path;
|
|
||||||
|
|
||||||
static final instance = ShizukuRemote();
|
static final instance = ShizukuRemote();
|
||||||
static final root = ShizukuRemote(isRoot: true);
|
static final root = ShizukuRemote(isRoot: true);
|
||||||
|
|
||||||
/// 루트 (shz://root/…) 인지 - 아니면 Shizuku (shz://sys/…)
|
/// 루트 (shz://root/…) 인지 - 아니면 Shizuku (shz://sys/…)
|
||||||
final bool isRoot;
|
final bool isRoot;
|
||||||
ShizukuRemote({this.isRoot = false});
|
ShizukuRemote({this.isRoot = false});
|
||||||
ShizukuBackend get _b => isRoot ? rootBackend : backend;
|
ShizukuBackend get _b => isRoot ? PrivilegedFs.current.root : PrivilegedFs.current.shizuku;
|
||||||
|
|
||||||
String _abs(String rel) => rel.isEmpty ? '/' : rel;
|
String _abs(String rel) => rel.isEmpty ? '/' : rel;
|
||||||
|
|
||||||
|
|
@ -230,7 +238,7 @@ class ShizukuRemote implements RemoteClient {
|
||||||
/// 앱 전용 임시 폴더에 받아 두고 읽은 뒤 지운다
|
/// 앱 전용 임시 폴더에 받아 두고 읽은 뒤 지운다
|
||||||
@override
|
@override
|
||||||
Future<Stream<List<int>>> openRead(String rel) async {
|
Future<Stream<List<int>>> openRead(String rel) async {
|
||||||
final dir = Directory(p.join(await tempDir(), 'jj_shz_${DateTime.now().microsecondsSinceEpoch}'));
|
final dir = Directory(p.join(await PrivilegedFs.current.tempDir(), 'jj_shz_${DateTime.now().microsecondsSinceEpoch}'));
|
||||||
await dir.create(recursive: true);
|
await dir.create(recursive: true);
|
||||||
final f = File(p.join(dir.path, 'f'));
|
final f = File(p.join(dir.path, 'f'));
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import '../platform/common/media_kit_player.dart';
|
||||||
import '../app/ai_upscale.dart' show AiUpscaler;
|
import '../app/ai_upscale.dart' show AiUpscaler;
|
||||||
import '../app/settings.dart';
|
import '../app/settings.dart';
|
||||||
import '../app/vault_location.dart';
|
import '../app/vault_location.dart';
|
||||||
|
import '../core/shizuku.dart' show PrivilegedFs;
|
||||||
import '../core/download_detect.dart';
|
import '../core/download_detect.dart';
|
||||||
import '../core/lan_scan.dart' show LanHost;
|
import '../core/lan_scan.dart' show LanHost;
|
||||||
import '../platform/common/nllb_translator.dart';
|
import '../platform/common/nllb_translator.dart';
|
||||||
|
|
@ -83,6 +84,10 @@ class PlatformServices {
|
||||||
/// 손으로 조립하면 처음 값은 앱 임시 폴더 아래 ([VaultLocation.under] - 사용자 보관함 · 앱 데이터를 건드리지 않음)
|
/// 손으로 조립하면 처음 값은 앱 임시 폴더 아래 ([VaultLocation.under] - 사용자 보관함 · 앱 데이터를 건드리지 않음)
|
||||||
final VaultLocation vault;
|
final VaultLocation vault;
|
||||||
|
|
||||||
|
/// 시스템 폴더 (Shizuku) · 루트 (su) 읽기 (설계 M3). 있으면 AppController 가 켤 때 PrivilegedFs.use 로 건다 - Android 조립만 넘기고,
|
||||||
|
/// 없으면 PrivilegedFs.current 를 그대로 둔다 (시험이 건 가짜를 덮지 않게)
|
||||||
|
final PrivilegedFs? privileged;
|
||||||
|
|
||||||
/// 이 기기 (Android · Windows · 메모리) - 기기마다 다르게 도는 곳은 이것을 본다 (M1, 처음 값 = 이 기기)
|
/// 이 기기 (Android · Windows · 메모리) - 기기마다 다르게 도는 곳은 이것을 본다 (M1, 처음 값 = 이 기기)
|
||||||
final PlatformInfo platform;
|
final PlatformInfo platform;
|
||||||
|
|
||||||
|
|
@ -108,6 +113,7 @@ class PlatformServices {
|
||||||
AppShell? shell,
|
AppShell? shell,
|
||||||
PlatformInfo? platform,
|
PlatformInfo? platform,
|
||||||
VaultLocation? vault,
|
VaultLocation? vault,
|
||||||
|
this.privileged,
|
||||||
}) : models = models ?? ModelStore(),
|
}) : models = models ?? ModelStore(),
|
||||||
vault = vault ?? VaultLocation.under(storage.tempDirectory),
|
vault = vault ?? VaultLocation.under(storage.tempDirectory),
|
||||||
platform = platform ?? PlatformInfo.host,
|
platform = platform ?? PlatformInfo.host,
|
||||||
|
|
@ -159,6 +165,7 @@ class PlatformServices {
|
||||||
final storage = AndroidStorageService();
|
final storage = AndroidStorageService();
|
||||||
return PlatformServices(
|
return PlatformServices(
|
||||||
vault: VaultLocation.device,
|
vault: VaultLocation.device,
|
||||||
|
privileged: PrivilegedFs.device(storage.tempDirectory),
|
||||||
mediaTool: FfmpegKitMediaTool(),
|
mediaTool: FfmpegKitMediaTool(),
|
||||||
storage: storage,
|
storage: storage,
|
||||||
updater: AndroidUpdater(storage),
|
updater: AndroidUpdater(storage),
|
||||||
|
|
|
||||||
|
|
@ -99,12 +99,12 @@ class ExplorerAccess extends ChangeNotifier {
|
||||||
rootWarned = true;
|
rootWarned = true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final st = await ShizukuRemote.rootBackend.status();
|
final st = await PrivilegedFs.current.root.status();
|
||||||
if (!st.running) {
|
if (!st.running) {
|
||||||
ctl.host.snack(tr('이 기기에는 su 가 없습니다 - 루팅한 기기에서만 쓸 수 있습니다'));
|
ctl.host.snack(tr('이 기기에는 su 가 없습니다 - 루팅한 기기에서만 쓸 수 있습니다'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!st.granted && !await ShizukuRemote.rootBackend.request()) {
|
if (!st.granted && !await PrivilegedFs.current.root.request()) {
|
||||||
ctl.host.snack(tr('루트 허락이 없어 볼 수 없습니다 (su 앱에서 이 앱을 허락하세요)'));
|
ctl.host.snack(tr('루트 허락이 없어 볼 수 없습니다 (su 앱에서 이 앱을 허락하세요)'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -140,12 +140,12 @@ class ExplorerAccess extends ChangeNotifier {
|
||||||
shizukuWarned = true;
|
shizukuWarned = true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final st = await ShizukuRemote.backend.status();
|
final st = await PrivilegedFs.current.shizuku.status();
|
||||||
if (!st.running) {
|
if (!st.running) {
|
||||||
ctl.host.snack(tr('Shizuku 가 켜져 있지 않습니다 - Shizuku 앱을 설치하고 (무선 디버깅 · adb 로) 시작한 뒤 다시 누르세요'));
|
ctl.host.snack(tr('Shizuku 가 켜져 있지 않습니다 - Shizuku 앱을 설치하고 (무선 디버깅 · adb 로) 시작한 뒤 다시 누르세요'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!st.granted && !await ShizukuRemote.backend.request()) {
|
if (!st.granted && !await PrivilegedFs.current.shizuku.request()) {
|
||||||
ctl.host.snack(tr('Shizuku 허락이 없어 볼 수 없습니다'));
|
ctl.host.snack(tr('Shizuku 허락이 없어 볼 수 없습니다'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,8 +92,8 @@ class ExplorerController extends ChangeNotifier {
|
||||||
for (final s in c.settings.remote.smbServers) ('$smbScheme${s.id}/', s.label),
|
for (final s in c.settings.remote.smbServers) ('$smbScheme${s.id}/', s.label),
|
||||||
// l: 평문 FTP 는 이름 앞에 ⚠
|
// l: 평문 FTP 는 이름 앞에 ⚠
|
||||||
for (final s in c.settings.remote.ftpServers) ('$ftpScheme${s.id}/', s.tls ? s.label : '⚠ ${s.label}'),
|
for (final s in c.settings.remote.ftpServers) ('$ftpScheme${s.id}/', s.tls ? s.label : '⚠ ${s.label}'),
|
||||||
if (c.settings.tools.shizukuEnabled && shizukuSupported()) ('$shizukuScheme$shizukuServer/', tr('시스템 (읽기 전용)')),
|
if (c.settings.tools.shizukuEnabled && c.services.platform.isAndroid) ('$shizukuScheme$shizukuServer/', tr('시스템 (읽기 전용)')),
|
||||||
if (c.settings.tools.rootEnabled && rootSupported()) (rootHome, tr('루트 (읽기 전용)')),
|
if (c.settings.tools.rootEnabled && c.services.platform.isAndroid) (rootHome, tr('루트 (읽기 전용)')),
|
||||||
];
|
];
|
||||||
|
|
||||||
String volumeOf(String path) => vol.volumeOf(path, local);
|
String volumeOf(String path) => vol.volumeOf(path, local);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import '../smb_settings.dart';
|
||||||
import '../webdav_settings.dart';
|
import '../webdav_settings.dart';
|
||||||
import '../explorer_look.dart' show ExplorerStyle;
|
import '../explorer_look.dart' show ExplorerStyle;
|
||||||
import '../explorer_keys_page.dart';
|
import '../explorer_keys_page.dart';
|
||||||
import '../../core/shizuku.dart' show rootSupported, shizukuSupported;
|
|
||||||
import '../theme.dart';
|
import '../theme.dart';
|
||||||
import '../../l10n/tr.dart';
|
import '../../l10n/tr.dart';
|
||||||
import '../setting_tile.dart';
|
import '../setting_tile.dart';
|
||||||
|
|
@ -90,7 +89,7 @@ class _FilesSettingsSectionState extends State<FilesSettingsSection> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// X-plore 비교 5-2 (사용자 결정 10/21): 시스템 폴더 보기 (Shizuku, 읽기 전용) - Android, 켤 때마다 경고
|
// X-plore 비교 5-2 (사용자 결정 10/21): 시스템 폴더 보기 (Shizuku, 읽기 전용) - Android, 켤 때마다 경고
|
||||||
if (shizukuSupported())
|
if (c.services.platform.isAndroid)
|
||||||
SettingTile(
|
SettingTile(
|
||||||
title: Text(tr('시스템 폴더 보기 (Shizuku · 읽기 전용)')),
|
title: Text(tr('시스템 폴더 보기 (Shizuku · 읽기 전용)')),
|
||||||
subtitle: Text(tr('Shizuku 앱이 있으면 Android/data · obb 처럼 막힌 폴더를 읽기만 합니다. 탐색기 위 탭 [시스템 (읽기 전용)]')),
|
subtitle: Text(tr('Shizuku 앱이 있으면 Android/data · obb 처럼 막힌 폴더를 읽기만 합니다. 탐색기 위 탭 [시스템 (읽기 전용)]')),
|
||||||
|
|
@ -110,7 +109,7 @@ class _FilesSettingsSectionState extends State<FilesSettingsSection> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// X-plore 비교 5-2 (관리자 결정 10/21): 루트 폴더 보기 (su, 읽기 전용) - Shizuku 와 같은 틀
|
// X-plore 비교 5-2 (관리자 결정 10/21): 루트 폴더 보기 (su, 읽기 전용) - Shizuku 와 같은 틀
|
||||||
if (rootSupported())
|
if (c.services.platform.isAndroid)
|
||||||
SettingTile(
|
SettingTile(
|
||||||
title: Text(tr('루트 폴더 보기 (su · 읽기 전용)')),
|
title: Text(tr('루트 폴더 보기 (su · 읽기 전용)')),
|
||||||
subtitle: Text(tr('루팅한 기기에서 su 로 시스템 · 다른 앱의 폴더를 읽기만 합니다. 탐색기 위 탭 [루트 (읽기 전용)]')),
|
subtitle: Text(tr('루팅한 기기에서 su 로 시스템 · 다른 앱의 폴더를 읽기만 합니다. 탐색기 위 탭 [루트 (읽기 전용)]')),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||||
|
import 'package:jj_mkvmaker/core/shizuku.dart';
|
||||||
|
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||||
|
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||||||
|
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||||
|
|
||||||
|
class _Back implements ShizukuBackend {
|
||||||
|
_Back(this.name);
|
||||||
|
final String name;
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation i) => super.noSuchMethod(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 약속 (M3 PrivilegedFs): 앱 조립이 privileged 를 넘기면 AppController 가 켤 때 건다 · 넘기지 않으면 지금 것을 그대로 둔다 (시험의 가짜를 덮지 않음) ·
|
||||||
|
/// ShizukuRemote 는 지금 건 뒤쪽 (시스템 · 루트) 을 쓴다
|
||||||
|
void main() {
|
||||||
|
final was = PrivilegedFs.current;
|
||||||
|
tearDown(() => PrivilegedFs.use(was));
|
||||||
|
|
||||||
|
test('privileged 를 넘기면 켤 때 걸고, 안 넘기면 그대로', () {
|
||||||
|
final a = PrivilegedFs(shizuku: _Back('a'), root: _Back('ar'), tempDir: () async => 'A');
|
||||||
|
PrivilegedFs.use(a);
|
||||||
|
AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||||
|
expect(identical(PrivilegedFs.current, a), isTrue, reason: '안 넘기면 그대로');
|
||||||
|
final b = PrivilegedFs(shizuku: _Back('b'), root: _Back('br'), tempDir: () async => 'B');
|
||||||
|
AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), privileged: b));
|
||||||
|
expect(identical(PrivilegedFs.current, b), isTrue, reason: '넘기면 건다');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('device: Shizuku 는 AndroidShizuku, 루트는 SuRoot (실제 Process.start), 임시는 넘긴 폴더', () async {
|
||||||
|
final d = PrivilegedFs.device(() async => 'T');
|
||||||
|
expect([d.shizuku is AndroidShizuku, d.root is SuRoot, await d.tempDir()], [true, true, 'T']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||||
import 'package:jj_mkvmaker/ui/settings_page.dart';
|
import 'package:jj_mkvmaker/ui/settings_page.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'support/test_explorer_device.dart';
|
import 'support/test_explorer_device.dart';
|
||||||
|
import 'package:jj_mkvmaker/services/platform_info.dart';
|
||||||
|
|
||||||
/// X-plore 비교 5-2 루트 폴더 보기 (su, 읽기 전용 - 관리자 결정 10/21: Shizuku 와 같은 틀, 처음 여는 곳 "/")
|
/// X-plore 비교 5-2 루트 폴더 보기 (su, 읽기 전용 - 관리자 결정 10/21: Shizuku 와 같은 틀, 처음 여는 곳 "/")
|
||||||
class _Proc implements Process {
|
class _Proc implements Process {
|
||||||
|
|
@ -56,25 +57,17 @@ void main() {
|
||||||
setUp(() => device = TestExplorerDevice());
|
setUp(() => device = TestExplorerDevice());
|
||||||
late Directory tmp;
|
late Directory tmp;
|
||||||
late _Su su;
|
late _Su su;
|
||||||
final wasStart = SuRoot.start;
|
final was = PrivilegedFs.current;
|
||||||
final wasRoot = ShizukuRemote.rootBackend;
|
|
||||||
final wasTemp = ShizukuRemote.tempDir;
|
|
||||||
final wasSupported = rootSupported;
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
tmp = Directory.systemTemp.createTempSync('jj_root_');
|
tmp = Directory.systemTemp.createTempSync('jj_root_');
|
||||||
su = _Su();
|
su = _Su();
|
||||||
SuRoot.start = su.start;
|
|
||||||
SuRoot.forgetGrant();
|
SuRoot.forgetGrant();
|
||||||
ShizukuRemote.rootBackend = SuRoot();
|
// 설계 M3: 실제 SuRoot 에 가짜 su 프로세스만 넘기고, PrivilegedFs 하나로 건다 (Shizuku 쪽은 그대로)
|
||||||
ShizukuRemote.tempDir = () async => tmp.path;
|
PrivilegedFs.use(PrivilegedFs(shizuku: was.shizuku, root: SuRoot(start: su.start), tempDir: () async => tmp.path));
|
||||||
rootSupported = () => true;
|
|
||||||
ExplorerPage.rootWarnedThisRun = false;
|
ExplorerPage.rootWarnedThisRun = false;
|
||||||
});
|
});
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
SuRoot.start = wasStart;
|
PrivilegedFs.use(was);
|
||||||
ShizukuRemote.rootBackend = wasRoot;
|
|
||||||
ShizukuRemote.tempDir = wasTemp;
|
|
||||||
rootSupported = wasSupported;
|
|
||||||
tmp.deleteSync(recursive: true);
|
tmp.deleteSync(recursive: true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -90,7 +83,7 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('su: 있는지 (묻지 않음) → 허락 (id -u = 0) · 목록 · 정보 · 내용 (끝 코드가 0 이 아니면 반쪽 파일을 지우고 오류) · 거절 · su 없음', () async {
|
test('su: 있는지 (묻지 않음) → 허락 (id -u = 0) · 목록 · 정보 · 내용 (끝 코드가 0 이 아니면 반쪽 파일을 지우고 오류) · 거절 · su 없음', () async {
|
||||||
final r = SuRoot();
|
final r = SuRoot(start: su.start); // 가짜 su 프로세스 (설계 M3 - 예전 static start 대신 넘김)
|
||||||
expect(await r.status(), (running: true, granted: false));
|
expect(await r.status(), (running: true, granted: false));
|
||||||
expect(su.calls, ['sh command -v su'], reason: '있는지만 볼 때는 허락 창을 띄우지 않음');
|
expect(su.calls, ['sh command -v su'], reason: '있는지만 볼 때는 허락 창을 띄우지 않음');
|
||||||
expect(await r.request(), isTrue);
|
expect(await r.request(), isTrue);
|
||||||
|
|
@ -127,7 +120,9 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
AppController ctl({bool on = true}) {
|
AppController ctl({bool on = true}) {
|
||||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
// 시스템 폴더 · 루트 탭은 Android 에서만 (설계 M3 - 예전 전역 함수 대신 PlatformInfo 주입)
|
||||||
|
final c = AppController(
|
||||||
|
PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), platform: PlatformInfo.android));
|
||||||
c.settings.explorer.style = 'xplore'; // E6: 이 시험은 X-plore 모양 (트리) 을 본다 - Windows 처음 값은 Total Commander
|
c.settings.explorer.style = 'xplore'; // E6: 이 시험은 X-plore 모양 (트리) 을 본다 - Windows 처음 값은 Total Commander
|
||||||
c.settings
|
c.settings
|
||||||
..tools.rootEnabled = on
|
..tools.rootEnabled = on
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||||
import 'package:jj_mkvmaker/ui/settings_page.dart';
|
import 'package:jj_mkvmaker/ui/settings_page.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'support/test_explorer_device.dart';
|
import 'support/test_explorer_device.dart';
|
||||||
|
import 'package:jj_mkvmaker/services/platform_info.dart';
|
||||||
|
|
||||||
/// X-plore 비교 5-2 시스템 폴더 보기 (Shizuku, 읽기 전용 · 처음 끔 · 경고 두 번 - 관리자 결정 10/21)
|
/// X-plore 비교 5-2 시스템 폴더 보기 (Shizuku, 읽기 전용 · 처음 끔 · 경고 두 번 - 관리자 결정 10/21)
|
||||||
class _Fake implements ShizukuBackend {
|
class _Fake implements ShizukuBackend {
|
||||||
|
|
@ -51,21 +52,16 @@ void main() {
|
||||||
setUp(() => device = TestExplorerDevice());
|
setUp(() => device = TestExplorerDevice());
|
||||||
late Directory tmp;
|
late Directory tmp;
|
||||||
late _Fake fake;
|
late _Fake fake;
|
||||||
final wasBackend = ShizukuRemote.backend;
|
final was = PrivilegedFs.current;
|
||||||
final wasTemp = ShizukuRemote.tempDir;
|
|
||||||
final wasSupported = shizukuSupported;
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
tmp = Directory.systemTemp.createTempSync('jj_shz_');
|
tmp = Directory.systemTemp.createTempSync('jj_shz_');
|
||||||
fake = _Fake();
|
fake = _Fake();
|
||||||
ShizukuRemote.backend = fake;
|
// 설계 M3: 가짜 Shizuku 뒤쪽 · 시험 임시 폴더를 PrivilegedFs 하나로 건다 (루트 쪽은 그대로)
|
||||||
ShizukuRemote.tempDir = () async => tmp.path;
|
PrivilegedFs.use(PrivilegedFs(shizuku: fake, root: was.root, tempDir: () async => tmp.path));
|
||||||
shizukuSupported = () => true;
|
|
||||||
ExplorerPage.shizukuWarnedThisRun = false;
|
ExplorerPage.shizukuWarnedThisRun = false;
|
||||||
});
|
});
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
ShizukuRemote.backend = wasBackend;
|
PrivilegedFs.use(was);
|
||||||
ShizukuRemote.tempDir = wasTemp;
|
|
||||||
shizukuSupported = wasSupported;
|
|
||||||
tmp.deleteSync(recursive: true);
|
tmp.deleteSync(recursive: true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -110,7 +106,9 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
AppController ctl({bool on = true}) {
|
AppController ctl({bool on = true}) {
|
||||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
// 시스템 폴더 · 루트 탭은 Android 에서만 (설계 M3 - 예전 전역 함수 대신 PlatformInfo 주입)
|
||||||
|
final c = AppController(
|
||||||
|
PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService(), platform: PlatformInfo.android));
|
||||||
c.settings.explorer.style = 'xplore'; // E6: 이 시험은 X-plore 모양 (트리) 을 본다 - Windows 처음 값은 Total Commander
|
c.settings.explorer.style = 'xplore'; // E6: 이 시험은 X-plore 모양 (트리) 을 본다 - Windows 처음 값은 Total Commander
|
||||||
c.settings
|
c.settings
|
||||||
..tools.shizukuEnabled = on
|
..tools.shizukuEnabled = on
|
||||||
|
|
|
||||||
|
|
@ -346,13 +346,9 @@ void main() {
|
||||||
addTearDown(t.view.reset);
|
addTearDown(t.view.reset);
|
||||||
final viewRoot = Directory(p.join(tmp.path, 'viewroot')); // where.viewRoot
|
final viewRoot = Directory(p.join(tmp.path, 'viewroot')); // where.viewRoot
|
||||||
final remote = _Remote({'/r/remote.txt': 'from nas'});
|
final remote = _Remote({'/r/remote.txt': 'from nas'});
|
||||||
final wasBackend = ShizukuRemote.backend, wasTemp = ShizukuRemote.tempDir;
|
final was = PrivilegedFs.current;
|
||||||
ShizukuRemote.backend = remote;
|
PrivilegedFs.use(PrivilegedFs(shizuku: remote, root: was.root, tempDir: () async => tmp.path));
|
||||||
ShizukuRemote.tempDir = () async => tmp.path;
|
addTearDown(() => PrivilegedFs.use(was));
|
||||||
addTearDown(() {
|
|
||||||
ShizukuRemote.backend = wasBackend;
|
|
||||||
ShizukuRemote.tempDir = wasTemp;
|
|
||||||
});
|
|
||||||
final album = Directory(p.join(tmp.path, 'album'));
|
final album = Directory(p.join(tmp.path, 'album'));
|
||||||
File(p.join(album.path, 'a.jpg'))
|
File(p.join(album.path, 'a.jpg'))
|
||||||
..createSync(recursive: true)
|
..createSync(recursive: true)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue