Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 94 additions & 8 deletions controller/app/src/main/java/org/iiab/controller/DeployFragment.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,13 @@ public class DeployFragment extends Fragment {
private View ledInternet, ledDevMode, ledDcpr, ledPpk;
private TextView txtDcpr, txtPpk, btnRefreshModules;
private LinearLayout rolesContainer, discrepancyWarning;
private Button btnLaunchInstall, btnFastInstall, btnFastDelete, btnAdvancedReset;
private Button btnAdvancedBackup, btnAdvancedRestore, btnAdvancedForceStop;
private Button btnLaunchInstall, btnAdvancedReset;
private ProgressButton btnFastInstall, btnFastDelete;
private Button btnAdvancedForceStop;
private ProgressButton btnAdvancedBackup, btnAdvancedRestore;
private LinearLayout restoreLogPanel;
private TextView restoreLogText, restoreLogResult;
private androidx.core.widget.NestedScrollView restoreLogScroll;

// Backup Menu UI
private TextView txtSelectBackupTitle, txtBackupStatus;
Expand Down Expand Up @@ -137,6 +142,9 @@ public class DeployFragment extends Fragment {
private boolean isRestoring = false;
private boolean isDeleting = false;
private boolean isImporting = false;
private static final String[] IMPORT_SPINNER = {"\u28BF", "\u28FB", "\u28FD", "\u28FE", "\u28F7", "\u28EF", "\u28DF", "\u287F"};
private android.os.Handler importSpinnerHandler;
private int importSpinnerFrame = 0;
private PRootEngine prootEngine;

// Background Handlers
Expand Down Expand Up @@ -281,6 +289,14 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
btnAdvancedBackup = view.findViewById(R.id.btn_advanced_backup);
btnAdvancedRestore = view.findViewById(R.id.btn_advanced_restore);
btnAdvancedForceStop = view.findViewById(R.id.btn_advanced_force_stop);
restoreLogPanel = view.findViewById(R.id.restore_log_panel);
restoreLogText = view.findViewById(R.id.restore_log_text);
restoreLogResult = view.findViewById(R.id.restore_log_result);
restoreLogScroll = view.findViewById(R.id.restore_log_scroll);
View restoreLogClose = view.findViewById(R.id.restore_log_close);
if (restoreLogClose != null) {
restoreLogClose.setOnClickListener(vv -> { if (restoreLogPanel != null) restoreLogPanel.setVisibility(View.GONE); });
}
txtSelectBackupTitle = view.findViewById(R.id.txt_select_backup_title);
containerBackupList = view.findViewById(R.id.container_backup_list);
txtBackupStatus = view.findViewById(R.id.txt_backup_status);
Expand Down Expand Up @@ -1044,6 +1060,7 @@ private void bindInstallButtonLogic(MainActivity mainAct, File debianRootfs, Fil
mainAct.invalidateModuleStateTrust();
isDownloadingRootfs = true;
btnFastInstall.setAlpha(0.8f);
btnFastInstall.startProgress();
btnFastInstall.setTextSize(12f);

if (aria2Manager == null) aria2Manager = new Aria2Manager();
Expand Down Expand Up @@ -1183,6 +1200,7 @@ private void bindDeleteButtonLogic(MainActivity mainAct, File debianRootfs) {

mainAct.invalidateModuleStateTrust();
btnFastDelete.setEnabled(false);
btnFastDelete.startProgress();
Snackbar.make(getView(), R.string.install_status_deleting, Snackbar.LENGTH_SHORT).show();
new Thread(() -> {
enableSystemProtection();
Expand All @@ -1195,7 +1213,7 @@ private void bindDeleteButtonLogic(MainActivity mainAct, File debianRootfs) {
mainAct.runOnUiThread(() -> Snackbar.make(getView(), getString(R.string.install_error_delete, e.getMessage()), Snackbar.LENGTH_LONG).show());
} finally {
isDeleting = false;
mainAct.runOnUiThread(this::updateDynamicButtons);
mainAct.runOnUiThread(() -> { btnFastDelete.stopProgress(); updateDynamicButtons(); });
disableSystemProtection();
}
}).start();
Expand Down Expand Up @@ -1590,6 +1608,7 @@ private void finishInstallationSuccess() {
isDownloadingRootfs = false;
if (isAdded() && getActivity() != null) {
getActivity().runOnUiThread(() -> {
btnFastInstall.stopProgress();
btnFastInstall.setText(R.string.install_btn_reinstall);
btnFastInstall.setAlpha(1.0f);
updateDynamicButtons();
Expand All @@ -1605,6 +1624,7 @@ private void abortInstallation(String message) {
isDownloadingRootfs = false;
if (isAdded() && getActivity() != null) {
getActivity().runOnUiThread(() -> {
btnFastInstall.stopProgress();
btnFastInstall.setText(R.string.install_btn_install);
btnFastInstall.setAlpha(1.0f);
updateDynamicButtons();
Expand Down Expand Up @@ -1838,7 +1858,7 @@ private void bindBackupButtonLogic(MainActivity mainAct, File backupsDir, File i
.setMessage(getString(R.string.install_msg_backup_in_progress_body))
.setPositiveButton(getString(R.string.install_btn_force_stop_process), (dialog, which) -> {
isBackupInProgress = false;
btnAdvancedBackup.setText(getString(R.string.install_btn_backup));
btnAdvancedBackup.setText(getString(R.string.install_btn_backup)); btnAdvancedBackup.stopProgress();
Snackbar.make(getView(), getString(R.string.install_msg_backup_aborted), Snackbar.LENGTH_SHORT).show();
})
.setNegativeButton(getString(R.string.install_btn_let_finish), null)
Expand All @@ -1849,6 +1869,7 @@ private void bindBackupButtonLogic(MainActivity mainAct, File backupsDir, File i

isBackupInProgress = true;
btnAdvancedBackup.setText(getString(R.string.install_msg_compressing));
btnAdvancedBackup.startProgress();
Snackbar.make(v, getString(R.string.install_msg_creating_backup), Snackbar.LENGTH_LONG).show();

new Thread(() -> {
Expand Down Expand Up @@ -1917,14 +1938,14 @@ private void bindBackupButtonLogic(MainActivity mainAct, File backupsDir, File i
prefs.edit().putInt("backup_daily_id", currentId - 1).apply();
}
isBackupInProgress = false;
btnAdvancedBackup.setText(getString(R.string.install_btn_backup));
btnAdvancedBackup.setText(getString(R.string.install_btn_backup)); btnAdvancedBackup.stopProgress();
updateDynamicButtons();
disableSystemProtection();
});
} catch (Exception e) {
mainAct.runOnUiThread(() -> {
isBackupInProgress = false;
btnAdvancedBackup.setText(getString(R.string.install_btn_backup));
btnAdvancedBackup.setText(getString(R.string.install_btn_backup)); btnAdvancedBackup.stopProgress();
Snackbar.make(getView(), getString(R.string.install_msg_backup_error, e.getMessage()), Snackbar.LENGTH_LONG).show();
updateDynamicButtons();
disableSystemProtection();
Expand Down Expand Up @@ -2131,6 +2152,12 @@ private void refreshRestoreButtonLogic() {

btnAdvancedRestore.setEnabled(false);
btnAdvancedRestore.setText(getString(R.string.install_status_restoring));
btnAdvancedRestore.startProgress();
if (restoreLogPanel != null) {
restoreLogPanel.setVisibility(View.VISIBLE);
if (restoreLogText != null) restoreLogText.setText("");
if (restoreLogResult != null) restoreLogResult.setText("");
}
File iiabRootDir = new File(requireContext().getFilesDir(), "rootfs");
TarExtractor tarExtractor = new TarExtractor();

Expand All @@ -2144,6 +2171,8 @@ public void onComplete(String destDir) {
btnAdvancedRestore.setEnabled(true);
btnAdvancedRestore.setText(getString(R.string.install_btn_restore));
Snackbar.make(getView(), R.string.install_success_restore, Snackbar.LENGTH_LONG).show();
if (restoreLogResult != null) { restoreLogResult.setText("\u2713"); restoreLogResult.setTextColor(ContextCompat.getColor(requireContext(), R.color.status_success)); }
btnAdvancedRestore.stopProgress();
updateDynamicButtons();
});
}
Expand All @@ -2156,19 +2185,67 @@ public void onError(String error) {
btnAdvancedRestore.setEnabled(true);
btnAdvancedRestore.setText(getString(R.string.install_btn_restore));
Snackbar.make(getView(), getString(R.string.install_msg_restore_failed) + " " + error, Snackbar.LENGTH_LONG).show();
if (restoreLogResult != null) { restoreLogResult.setText("\u2717"); restoreLogResult.setTextColor(ContextCompat.getColor(requireContext(), R.color.status_warning)); }
btnAdvancedRestore.stopProgress();
updateDynamicButtons();
});
}

@Override
public void onProgress(String line) {
if (restoreLogText == null) return;
restoreLogText.append(line + "\n");
if (restoreLogScroll != null) {
restoreLogScroll.post(() -> restoreLogScroll.fullScroll(View.FOCUS_DOWN));
}
}
});
});
}
}

private void startImportSpinner() {
stopImportSpinner();
importSpinnerFrame = 0;
importSpinnerHandler = new android.os.Handler(android.os.Looper.getMainLooper());
final Runnable r = new Runnable() {
@Override public void run() {
if (btnImportBackup != null) {
String f = IMPORT_SPINNER[importSpinnerFrame++ % IMPORT_SPINNER.length];
btnImportBackup.setText(getString(R.string.install_msg_importing) + " " + f);
}
if (importSpinnerHandler != null) importSpinnerHandler.postDelayed(this, 90);
}
};
importSpinnerHandler.post(r);
}

private void stopImportSpinner() {
if (importSpinnerHandler != null) {
importSpinnerHandler.removeCallbacksAndMessages(null);
importSpinnerHandler = null;
}
}

/** Best-effort original filename from a SAF content:// URI (DISPLAY_NAME), or null. */
private String queryDisplayName(Uri uri) {
try (android.database.Cursor c = requireContext().getContentResolver()
.query(uri, new String[]{android.provider.OpenableColumns.DISPLAY_NAME}, null, null, null)) {
if (c != null && c.moveToFirst()) {
int idx = c.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME);
if (idx >= 0) return c.getString(idx);
}
} catch (Exception e) {
Log.w(TAG, "queryDisplayName failed: " + e.getMessage());
}
return null;
}

private void importBackupSafely(Uri sourceUri) {
isImporting = true;
updateDynamicButtons();
btnImportBackup.setEnabled(false);
btnImportBackup.setText(getString(R.string.install_msg_importing));
startImportSpinner();
Snackbar.make(getView(), getString(R.string.install_msg_importing), Snackbar.LENGTH_LONG).show();

new Thread(() -> {
Expand All @@ -2177,7 +2254,14 @@ private void importBackupSafely(Uri sourceUri) {
File backupsDir = new File(requireContext().getFilesDir(), "rootfs/backups");
if (!backupsDir.exists()) backupsDir.mkdirs();

String fileName = "imported_backup_" + System.currentTimeMillis() + ".tar.gz";
// Keep the imported file's EXACT name; disambiguate with -1/-2/... on collision.
String desiredName = queryDisplayName(sourceUri);
java.util.Set<String> existingNames = new java.util.HashSet<>();
File[] existingFiles = backupsDir.listFiles();
if (existingFiles != null) {
for (File f : existingFiles) existingNames.add(f.getName());
}
String fileName = org.iiab.controller.backup.domain.BackupNameResolver.resolve(desiredName, existingNames);
File destFile = new File(backupsDir, fileName);

InputStream is = requireContext().getContentResolver().openInputStream(sourceUri);
Expand All @@ -2194,6 +2278,7 @@ private void importBackupSafely(Uri sourceUri) {
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
isImporting = false;
stopImportSpinner();
btnImportBackup.setEnabled(true);
btnImportBackup.setText(getString(R.string.install_btn_import_backup));
selectedBackupFile = fileName;
Expand All @@ -2205,6 +2290,7 @@ private void importBackupSafely(Uri sourceUri) {
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
isImporting = false;
stopImportSpinner();
updateDynamicButtons();
btnImportBackup.setEnabled(true);
btnImportBackup.setText(getString(R.string.install_btn_import_backup));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public interface ExtractionListener {
void onComplete(String destDir);

void onError(String error);

/** A streamed line of extraction output (verbose tar). Default no-op. */
default void onProgress(String line) { }
}

public void startExtraction(Context context, String archivePath, String destDir, ExtractionListener listener) {
Expand Down Expand Up @@ -66,7 +69,7 @@ public void startExtraction(Context context, String archivePath, String destDir,
// 2. BUILD THE COMMAND
List<String> command = new ArrayList<>();
command.add(tarBinary);
command.add("-xf");
command.add("-xvf");

if (isGzip) {
// Tell tar to read the uncompressed raw bytes from standard input (stdin)
Expand All @@ -84,11 +87,19 @@ public void startExtraction(Context context, String archivePath, String destDir,
tarProcess = pb.start();

// 3. READ TAR OUTPUT (Prevents buffer blocking and logs errors)
final Handler uiHandler = new Handler(Looper.getMainLooper());
new Thread(() -> {
long[] lastEmit = {0L};
try (BufferedReader reader = new BufferedReader(new InputStreamReader(tarProcess.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
Log.d(TAG, "Tar Output: " + line);
long now = System.currentTimeMillis();
if (now - lastEmit[0] >= 50) {
lastEmit[0] = now;
final String l = line;
uiHandler.post(() -> listener.onProgress(l));
}
}
} catch (Exception ignored) {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* ============================================================================
* Name : BackupNameResolver.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : Pure rule for keeping an imported backup's exact filename, with
* -1/-2/-3 disambiguation on collision.
* ============================================================================
*/
package org.iiab.controller.backup.domain;

import java.util.Set;

/**
* Decides the on-disk filename for an imported backup. Keeps the original name
* EXACTLY; only if that name already exists does it insert {@code -1}, {@code -2}, ...
* before the extension. Compound extensions ({@code .tar.gz}, {@code .tar.xz}) are
* preserved. Pure domain logic (no Android, no I/O) so it is fully unit-testable.
*/
public final class BackupNameResolver {

private static final String FALLBACK = "backup.tar.gz";

private BackupNameResolver() { }

/**
* @param desired the original filename (e.g. from SAF DISPLAY_NAME); may be null/dirty
* @param existing the set of filenames already present in the backups dir
* @return the exact desired name if free, else the same name with -1/-2/... before the ext
*/
public static String resolve(String desired, Set<String> existing) {
String name = sanitize(desired);
if (existing == null || !existing.contains(name)) {
return name;
}
String base = baseName(name);
String ext = extension(name);
for (int i = 1; ; i++) {
String candidate = base + "-" + i + ext;
if (!existing.contains(candidate)) {
return candidate;
}
}
}

/** Strip any path, trim, and fall back to a safe default when empty. */
static String sanitize(String name) {
if (name == null) return FALLBACK;
String n = name.trim();
int sep = Math.max(n.lastIndexOf('/'), n.lastIndexOf('\\'));
if (sep >= 0) n = n.substring(sep + 1);
return n.isEmpty() ? FALLBACK : n;
}

/** Compound-extension aware: {@code .tar.gz}/{@code .tar.xz}, else the last {@code .ext}. */
static String extension(String name) {
String lower = name.toLowerCase();
if (lower.endsWith(".tar.gz") || lower.endsWith(".tar.xz")) {
return name.substring(name.length() - 7);
}
int dot = name.lastIndexOf('.');
return dot > 0 ? name.substring(dot) : "";
}

static String baseName(String name) {
return name.substring(0, name.length() - extension(name).length());
}
}
Loading
Loading