Commit Graph

75 Commits

Author SHA1 Message Date
sakuradairong
7e98e0f78e refactor: replace staging sync with REST bridge and add streaming backup
Phase 1: REST bridge
- New ResticRestBridge (NanoHTTPD) implementing restic REST protocol
- New RestBridgeRunner lifecycle manager (withBridge pattern)
- ResticEnvResolver: buildLocalEnv/buildBridgeEnv split
- ResticWrapper: syncManager → bridgeRunner, propagate cacheDir
- 5 sub-modules (Backup/Restore/SnapshotOps/Maintenance/RepoInit):
  unified local/bridge branching
- Delete RemoteSyncManager.kt (231 lines removed)
- Clean RemoteTransport companion (only create() remains)
- Remove getTempRepoDir, cleanup(), onSyncProgress/ByteSyncProgress

Phase 2: Streaming backup
- New StreamingBackup with FIFO orchestration
- ResticBackup.backupStdin() with --stdin mode
- ResticCommandRunner.runResticWithStdin() (API 24 compat)
- BackupFragment: space detection + auto-switch to streaming

Code quality fixes:
- OOM protection: stream blob bodies to temp files
- API compat: manual stdin piping (no redirectInput API 26)
- Build: 0 errors, 0 warnings, lint 0 errors, all 53 tests pass
V1.4Debug
2026-06-05 14:11:52 +08:00
sakuradairong
922a8f0381 feat: cumulative snapshots, per-app data exclusion, clickable rows
Round 4:
- Clickable app rows: tapping anywhere on row toggles selection
- Per-app data exclusion: exclude data backup per app
- Backup path display on backup screen

Round 5:
- Cumulative snapshots: every restic snapshot contains all apps ever backed up
- Automatic merge of historical snapshot apps with current selection
- Removed incremental/full dialog — no user choice needed, always safe
- Legacy metadata preserved via buildAppDetailsJson(legacyApps)
2026-06-04 22:59:11 +08:00
sakuradairong
5fcf261025 chore: bump version to 1.3 v1.3 2026-06-04 22:57:39 +08:00
sakuradairong
14b914252e chore: bump version to 1.2
Round 3 deep review fixes:
- F1: onDestroyView cleanup ordering (lifecycleScope before super)
- F2: try/catch guards on unprotected coroutines
- F3: CancellationException rethrow in catch blocks
- F4: Extract formatSize and resticJson to shared utilities
- F5: Resource management (stream close, waitFor timeout, AppInfo.label val)
- F6: SharedFlow buffer 4→16
v1.2
2026-06-04 21:47:03 +08:00
sakuradairong
c01428b866 fix: resolve Lint API level errors
- BackupFragment: use ContextCompat.startForegroundService (API 24+)
- ResticCommandRunner: replace readAllBytes() with compat impl (API <33)
- themes: move windowLayoutInDisplayCutoutMode to values-v27
2026-06-04 21:29:44 +08:00
sakuradairong
51fe8e22c0 chore: remove embedded kmboxnet repo, add to gitignore 2026-06-04 21:21:22 +08:00
sakuradairong
f5dd61a83b refactor: Result → AppResult, DomainTypes, cleanup, and other improvements
- Replace kotlin.Result with AppResult across all transports and operations
- Introduce DomainTypes (PackageName, AppInfo) for type safety
- Add AppError sealed hierarchy for structured error handling
- Add SELinuxUtil for SELinux context restoration
- Add values-sw600dp and dimens.xml for tablet layout support
- Sync progress UI refactoring in BackupFragment/RestoreFragment
- BinaryResolver per-binary cache fix
2026-06-04 21:21:17 +08:00
sakuradairong
40f03e5bad fix: add try/finally for loading state on cancellation
Wrap initResticRepo, showResticStats, and pruneResticSnapshots
coroutine bodies in try/finally to ensure button state is restored
even when the coroutine is cancelled mid-flight.
2026-06-04 21:20:27 +08:00
sakuradairong
45f7af00b8 fix: eliminate redundant null assertions
- RemoteTransport.kt: localFiles[relPath] ?: continue instead of !!
- RestoreFragment.kt: selectedSnapshot/resticConfig ?: return@launch
- BackupFragment.kt: extract val summary = resticSummary before usage
- ResticCommandRunner.kt: var line: String instead of val l = line!!
2026-06-04 21:19:54 +08:00
sakuradairong
7ef0b2c9da refactor: replace launch(Dispatchers.IO) with launch { withContext(IO) }
Idiomatic coroutine pattern: launch on the default dispatcher,
wrap blocking IO work in withContext(Dispatchers.IO). This ensures
the coroutine body starts on the correct dispatcher and only
the blocking work switches to IO.
2026-06-04 21:19:04 +08:00
sakuradairong
6fa15af565 fix: use runBlocking in onCleared() instead of cancelled viewModelScope
viewModelScope is already cancelled when onCleared() runs, so
viewModelScope.launch(Dispatchers.IO) never executes ResticWrapper.cleanup().
Replace with runBlocking(Dispatchers.IO) to ensure cleanup actually runs.

Add fallback cleanup in ConfigFragment.onDestroyView().
2026-06-04 21:18:24 +08:00
sakuradairong
6cdad04905 feat: improve backup quality from Android-DataBackup analysis
Phase 1 — Core Backup Quality:
- Add archive tar structure validation after compression
- Exclude cache/.ota/lib/code_cache/no_backup from data backup
- Add keystore detection with user warning
- Enhance SSAID parsing (XML attribute) and restore via settings put secure

Phase 2 — App Data Model + Multi-user:
- Add DataSizes data class, enrich AppInfo with userId/keystore/iconPath
- Add icon backup from snapshot cache or APK
- Multi-user enumeration and per-user scanning (pm list users)
- User profile selector in backup/restore UI

Phase 3 — UI + Service:
- Add sort (A-Z, size), filter (system apps toggle), select all/deselect all
- New BackupService foreground service to prevent process death
- AndroidManifest: FOREGROUND_SERVICE + POST_NOTIFICATIONS permissions

Phase 4 — Polish:
- Add LogUtil with file rotation (7-day retention, filesDir/logs/)
- Wire file logging into backup/restore entry/exit points
v1.0.0
2026-06-02 00:54:30 +08:00
sakuradairong
5cbd21577b fix: correct TransferProgress/ByteProgress field names in snapshot progress 2026-06-01 23:29:46 +08:00
sakuradairong
1bae01de72 perf: add sync progress to restic snapshot loading 2026-06-01 23:14:17 +08:00
sakuradairong
e710c36ee2 fix: BinaryResolver per-binary cache (was sharing incorrect cross-binary state) 2026-06-01 23:03:31 +08:00
sakuradairong
c1bbef4eef feat: bundle zstd and tar binaries in jniLibs for reliable data backup
Download statically compiled ARM64 zstd (1.3MB) and tar (1.1MB)
binaries from YAWAsau/backup_script, placed in jniLibs as
libzstd_bin.so and libtar_bin.so. BinaryResolver extracts them to
filesDir/bin/ with execute permissions.

backupUserData auto-detects:
1. Bundled zstd → use absolute path
2. System zstd → use PATH lookup
3. No zstd → fall back to gzip (tar -czf)

This eliminates the 'command not found' (exit=127) issue when nsenter
+ FLAG_MOUNT_MASTER switches to a namespace with different PATH.
2026-06-01 23:01:17 +08:00
sakuradairong
4c4542e059 fix: auto-detect zstd availability, fall back to gzip when missing
With nsenter + FLAG_MOUNT_MASTER, test -d now correctly finds
/data/data/<pkg>. But zstd binary is absent on this device (previous
backups never reached tar|zstd because dirs were always empty).
Auto-detect zstd before creating archive; fall back to tar -czf (gzip)
when unavailable.
2026-06-01 22:55:08 +08:00
sakuradairong
ef78ab8bec debug: log tar stderr in each fallback step to identify actual error 2026-06-01 22:49:42 +08:00
sakuradairong
a38a483c70 fix: configure libsu with FLAG_MOUNT_MASTER and nsenter global namespace initializer
DataBackup (XayahSuSuSu) approach: configure the default libsu builder
with:
- Shell.FLAG_MOUNT_MASTER (requests su --mount-master from Magisk)
- GlobalNamespaceInitializer: runs 'nsenter --mount=/proc/1/ns/mnt sh'
  at shell init time, switching to init's global mount namespace
  where ALL /data/data/ directories are visible.

This replaces the previous 4-tier fallback hacks with a proper
shell-level fix at the libsu configuration layer.
2026-06-01 22:42:51 +08:00
sakuradairong
d0bfef41c8 fix: replace su -Z with magiskpolicy SELinux relax in backupUserData fallback 2026-06-01 22:33:16 +08:00
sakuradairong
0bde3b0a75 fix: try su -Z u:r:magisk:s0 for SELinux context switch
Magisk 30+ adds -Z flag to switch SELinux context. On this device,
the app's su runs as u:r:untrusted_app:s0 which cannot access other
apps' /data/data/. Switching to u:r:magisk:s0 lifts this restriction.
v1.2-debug
2026-06-01 21:57:11 +08:00
sakuradairong
d2ea9f532f fix: add su -mm fallback for Magisk isolated mount namespace
On some Magisk/Zygisk configurations, the app's su session stays
in an isolated mount namespace AND SELinux context that blocks even
/proc/1/root access. Add su -mm (Magisk mount namespace master) as
a fourth fallback attempt — it forkes a new su session in the global
mount namespace where all /data/data/ directories are accessible.
2026-06-01 21:45:39 +08:00
sakuradairong
ac0fd8b063 fix: access app data via /proc/1/root for isolated mount namespace
On some Magisk/Zygisk configurations, su from an app process stays
in the app's mount namespace where /data/data/<other-pkg> is not
visible (exit 1, 'No such file or directory').

Fix: three-tier fallback in backupUserData:
1. test -d on direct paths (standard)
2. tar directly on direct paths (covers test -d edge cases)
3. cd /proc/1/root && tar (crosses mount namespace boundary to init's
   global namespace, where all /data/data/ directories are visible)
2026-06-01 21:35:45 +08:00
sakuradairong
fde6d05b83 fix: bypass test -d quoting issue, use raw pkg paths with tar fallback
libsu's shell wrapper mishandles single-quoted path segments from
shellEscape(), causing test -d /data/data/'tv.danmaku.bili' to return
exit 1 even though the directory exists. Fix:

- Use raw (unescaped) package name for path checking commands
- Fallback: if test -d fails, try tar directly (which works even when
  test -d doesn't) to detect and back up existing data directories
- Restructure to avoid double tar execution in fallback path
2026-06-01 21:29:16 +08:00
sakuradairong
2ff096ee8a debug: add ls-d fallback when test -d fails in backupUserData 2026-06-01 21:21:35 +08:00
sakuradairong
eae7f4b369 debug: log backupUserData test -d exit codes to diagnose missing data archive 2026-06-01 21:18:19 +08:00
sakuradairong
420d960ce1 fix: restoreData logging catches empty filter result
The ?: operator only triggers on null, not empty list.
Replace with explicit null check + filter + empty check,
logging actual filenames when no _data.tar is found.
2026-06-01 21:02:10 +08:00
sakuradairong
88e81e956c debug: add logging to restoreData/restoreObb for silent failure diagnosis
restoreData and restoreObb currently log nothing on failure and return
Unit, making it impossible to diagnose why data archives aren't being
extracted. Add Log.d/w/e calls for every step: file discovery, archive
safety check, extraction command, and result.
2026-06-01 20:46:55 +08:00
sakuradairong
c12f7a9c81 chore: cleanup restic_tmp directory after sync 2026-06-01 20:27:59 +08:00
sakuradairong
a43638698c fix: set TMPDIR for restic pack files on Android
Android has no /tmp directory, causing restic backup to fail with:
  Fatal: unable to save snapshot: open /tmp/restic-temp-pack-...: no such file or directory

- Add TMPDIR env var pointing to a writable app cache subdirectory
- Ensure the tmp directory is created before restic runs
2026-06-01 20:27:30 +08:00
sakuradairong
12fe29f841 fix: preserve restic error in final status, revert binary copy
- Restic error message was overwritten by '备份完成!' before user
  could see it — now included in final status as '── Restic 错误 ──'
- Revert copy-to-filesDir: SELinux blocks exec from app private dir
- Use native library directory directly (Android extracts with exec perms)
2026-06-01 20:23:37 +08:00
sakuradairong
b7addcca6b fix: revert ResticBinary to native lib dir, use direct exec path
The copy-to-filesDir approach fails on Android 10+ because SELinux
blocks execution from app private data directories even with
setExecutable(true). Revert to using the native library directory
directly — Android PackageManager extracts native libs with proper
execution permissions there.

Also: the copy change also reset ResticBinary.cacheInit=false, meaning
the cached "Permission denied" path would be re-picked up. Full revert
ensures the next prepare() call uses the correct executable path.
2026-06-01 20:20:51 +08:00
sakuradairong
98d4029fd4 fix: reload config onResume so Restic settings take effect without restart
BackupFragment and RestoreFragment read config only in onViewCreated(),
so config changes saved in ConfigFragment (e.g. enabling Restic) were
picked up only after app restart (when fragments are recreated). Add
onResume() to both fragments to re-read config from file.
2026-06-01 20:14:10 +08:00
sakuradairong
07366f744f fix: copy restic binary to writable dir before execution
Native library directory may be mounted noexec on Android 10+,
preventing ProcessBuilder from executing librestic.so directly.
Copy to app's filesDir/restic_bin/librestic and set executable
permission before use.
2026-06-01 20:12:33 +08:00
sakuradairong
88e18f4c57 chore: gitignore restic test repo containing encryption keys 2026-06-01 17:44:53 +08:00
sakuradairong
d8992c3931 chore: gitignore memory files from agent harness
memory:/ paths resolve to literal files in the project directory.
Add gitignore pattern to prevent accidental commits.
2026-06-01 17:39:00 +08:00
sakuradairong
c7e9d8b5f1 chore: remove memory files from git tracking (internal harness paths) 2026-06-01 17:38:07 +08:00
sakuradairong
3e1f8a5937 fix: conditionally sign release APK when keystore exists
release.keystore is gitignored (regenerate locally), so CI checks out
without it. Make signingConfig and signing reference conditional on
file existence, allowing CI to produce an unsigned APK while local
builds still get a signed release.
2026-06-01 17:38:02 +08:00
sakuradairong
80b84f6cff refactor: migrate to libsu root shell with unified exec interface
Replace custom persistent su process management (Mutex-guarded
process, InputStream/OutputStreamReader) with libsu's Shell.cmd.

RootShell.kt changes:
- Remove: process lifecycle, mutex, stdin/stdout/stderr management,
  execBinary/execStreaming, sentinel health-check pattern
- Add: libsu Shell.getShell()/Shell.cmd() with structured timeout,
  single public exec() method returning ShellResult
- Keep: shellEscape() extension, ensureSession(), ShellResult data class

Caller changes:
- WifiManager.kt: add return-value checks for mkdir, chown, chmod;
  mark best-effort wifi reload as intentionally unchecked

Supporting:
- app/build.gradle: add libsu 6.0.0 dependency
- AGENTS.md / CLAUDE.md: update GitNexus index stats
2026-06-01 17:27:26 +08:00
sakuradairong
0b017e853b fix: restore review fixes — snapshot dialog, staging finally, permission try-catch 2026-06-01 16:53:43 +08:00
sakuradairong
2351cce99e fix: incremental backup progress — add message_type check, skip count in sync progress 2026-06-01 16:45:16 +08:00
sakuradairong
f1e7141ad5 chore: add signing config and release keystore v1.1 2026-06-01 16:30:10 +08:00
sakuradairong
7e0e250ac2 chore: bump version to 1.1 2026-06-01 16:27:55 +08:00
sakuradairong
792204f6fa fix: API 24 compatible process timeout loop for runResticStreaming 2026-06-01 16:11:02 +08:00
sakuradairong
13e33995cc chore: update AGENTS.md and CLAUDE.md metadata 2026-06-01 16:05:46 +08:00
sakuradairong
5079bc423e docs: update README with current architecture and design details 2026-06-01 16:00:38 +08:00
sakuradairong
f60ab1de7d fix: waitFor timeout, cache path cleanup, repo pre-check, cloud WiFi restore 2026-06-01 16:00:01 +08:00
sakuradairong
4e525f5ce0 fix: SMB signing configurable, WebDAV 50MB limit, i18n cleanup 2026-06-01 15:45:45 +08:00
sakuradairong
b8d06774ae fix: disable lint QueryAllPackagesPermission check for CI build 2026-06-01 14:55:27 +08:00
sakuradairong
06229482a1 docs: 添加用户偏好(中文回复)到 CLAUDE.md 2026-06-01 14:48:12 +08:00