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)
- 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
Wrap initResticRepo, showResticStats, and pruneResticSnapshots
coroutine bodies in try/finally to ensure button state is restored
even when the coroutine is cancelled mid-flight.
- 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!!
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.
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().
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
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.
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.
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.
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.
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.
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)
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
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.
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.
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
- 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)
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.
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.
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.
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.