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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,25 @@
* Name : BackupRestoreFragment.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : ADFA-4952. Backup & restore intro — a Clone-style screen: title + description +
* "what do you want to do?" with two cards (Back up / Restore). Each card opens its own
* dedicated job screen (BackupJobFragment) with the working Lottie + status. Hosted in
* SetupLibraryActivity (which owns the ServerController + coordinates via EnvironmentLock).
* Description : ADFA-4952 / 4961. Backup & restore bifurcation — title + description + "what do you
* want to do?" with two icon cards (Back up / Restore). Tapping a card IS the Start: it
* opens BackupJobFragment, which auto-opens the file picker. ADFA-4961: after an op
* finishes and its screen taps Finish, this bifurcation shows the module-index-style
* "returning to Home" countdown with a Cancel (armed via armReturnCountdown()). Hosted in
* SetupLibraryActivity.
* ============================================================================
*/
package org.iiab.controller.redesign;

import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

Expand All @@ -28,6 +35,19 @@

public class BackupRestoreFragment extends Fragment {

// ADFA-4961: one-shot flag set by BackupJobFragment's Finish, consumed here to run the "returning
// to Home" countdown. Process-scoped: the bifurcation is recreated when the job screen pops.
private static boolean sReturnPending = false;
public static void armReturnCountdown() { sReturnPending = true; }

// ADFA-4961: like the module index, wait for the environment to actually be live before redirecting.
private static final long READY_POLL_MS = 2000L;
private static final long READY_TIMEOUT_MS = 45000L;
private final Handler main = new Handler(Looper.getMainLooper());
private Runnable readyPoll;
private boolean returnCancelled = false;
private long returnStartMs = 0L;

private int px(int dp) { return Math.round(dp * getResources().getDisplayMetrics().density); }

@Nullable
Expand Down Expand Up @@ -63,11 +83,18 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
clp.topMargin = px(10);
col.addView(cards, clp);
cards.addView(card(R.string.k2go_br_backup_title, R.string.k2go_br_backup_sub,
cards.addView(card(R.drawable.ic_archive, R.string.k2go_br_backup_title, R.string.k2go_br_backup_sub,
v -> open(BackupJobFragment.MODE_BACKUP)));
cards.addView(card(R.string.k2go_br_restore_title, R.string.k2go_br_restore_sub,
cards.addView(card(R.drawable.ic_unarchive, R.string.k2go_br_restore_title, R.string.k2go_br_restore_sub,
v -> open(BackupJobFragment.MODE_RESTORE)));

// ADFA-4961: returning from a finished op → index-style "returning to Home" countdown + Cancel.
if (sReturnPending) {
sReturnPending = false;
col.addView(buildReturnBar());
startReturnToHome();
}

return scroll;
}

Expand All @@ -77,6 +104,79 @@ private void open(String jobMode) {
}
}

private View buildReturnBar() {
LinearLayout bar = new LinearLayout(requireContext());
bar.setOrientation(LinearLayout.HORIZONTAL);
bar.setGravity(Gravity.CENTER_VERTICAL);
bar.setBackgroundResource(R.drawable.k2go_info_bg);
bar.setPadding(px(14), px(12), px(14), px(12));
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lp.topMargin = px(16);
bar.setLayoutParams(lp);

TextView msg = new TextView(requireContext());
msg.setText(getString(R.string.k2go_bj_returning));
msg.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium);
// ADFA-4961: set the color AFTER the appearance — setTextAppearance() overrides textColor, which
// was leaving this line the theme's light default (invisible on the pale info background).
msg.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_teal));
bar.addView(msg, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));

TextView cancel = new TextView(requireContext());
cancel.setText(getString(R.string.cancel));
cancel.setTypeface(cancel.getTypeface(), android.graphics.Typeface.BOLD);
cancel.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_teal));
cancel.setPadding(px(12), px(4), px(4), px(4));
cancel.setOnClickListener(v -> {
returnCancelled = true;
if (readyPoll != null) main.removeCallbacks(readyPoll);
bar.setVisibility(View.GONE);
});
bar.addView(cancel);
return bar;
}

/**
* ADFA-4961: mirror the module index — boot the environment (startEnvironment, never the toggle)
* and WAIT for RestReadiness.apiReady() before redirecting, so we never drop the user on a Home that
* is still starting. Cancel keeps them here; a READY_TIMEOUT_MS fallback still goes Home so the user
* is never trapped (Home's boot gate then owns the rest).
*/
private void startReturnToHome() {
returnCancelled = false;
returnStartMs = android.os.SystemClock.elapsedRealtime();
if (getActivity() instanceof SetupLibraryActivity) {
org.iiab.controller.ServerController sc = ((SetupLibraryActivity) getActivity()).server();
if (sc != null) sc.startEnvironment();
}
readyPoll = new Runnable() {
@Override public void run() {
if (returnCancelled || !isAdded()) return;
org.iiab.controller.util.AppExecutors.get().io().execute(() -> {
final boolean up = RestReadiness.apiReady();
main.post(() -> {
if (returnCancelled || !isAdded()) return;
if (up || android.os.SystemClock.elapsedRealtime() - returnStartMs > READY_TIMEOUT_MS) {
goHome(); // live (or timed out → Home's boot gate takes over)
} else {
main.postDelayed(readyPoll, READY_POLL_MS);
}
});
});
}
};
main.postDelayed(readyPoll, READY_POLL_MS);
}

private void goHome() {
if (!isAdded()) return;
startActivity(new Intent(requireContext(), LibraryActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP)
.putExtra(LibraryActivity.EXTRA_TAB, R.id.nav_library));
requireActivity().finish();
}

private TextView heading(String text, boolean bold, int appearance, int colorRes, int topDp) {
TextView t = new TextView(requireContext());
t.setText(text);
Expand All @@ -90,9 +190,10 @@ private TextView heading(String text, boolean bold, int appearance, int colorRes
return t;
}

private View card(int titleRes, int subRes, View.OnClickListener onClick) {
private View card(int iconRes, int titleRes, int subRes, View.OnClickListener onClick) {
LinearLayout row = new LinearLayout(requireContext());
row.setOrientation(LinearLayout.VERTICAL);
row.setOrientation(LinearLayout.HORIZONTAL);
row.setGravity(Gravity.CENTER_VERTICAL);
row.setBackgroundResource(R.drawable.k2go_card_bg);
row.setPadding(px(16), px(16), px(16), px(16));
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
Expand All @@ -102,12 +203,23 @@ private View card(int titleRes, int subRes, View.OnClickListener onClick) {
row.setClickable(true);
row.setOnClickListener(onClick);

ImageView icon = new ImageView(requireContext());
icon.setImageResource(iconRes);
icon.setColorFilter(ContextCompat.getColor(requireContext(), R.color.k2go_teal));
LinearLayout.LayoutParams ilp = new LinearLayout.LayoutParams(px(28), px(28));
ilp.rightMargin = px(14);
row.addView(icon, ilp);

LinearLayout textCol = new LinearLayout(requireContext());
textCol.setOrientation(LinearLayout.VERTICAL);
row.addView(textCol, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));

TextView t = new TextView(requireContext());
t.setText(getString(titleRes));
t.setTypeface(t.getTypeface(), android.graphics.Typeface.BOLD);
t.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_ink));
t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_TitleMedium);
row.addView(t);
textCol.addView(t);

TextView sub = new TextView(requireContext());
sub.setText(getString(subRes));
Expand All @@ -116,7 +228,12 @@ private View card(int titleRes, int subRes, View.OnClickListener onClick) {
LinearLayout.LayoutParams slp = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
slp.topMargin = px(4);
row.addView(sub, slp);
textCol.addView(sub, slp);
return row;
}

@Override public void onDestroyView() {
if (readyPoll != null) main.removeCallbacks(readyPoll);
super.onDestroyView();
}
}
8 changes: 8 additions & 0 deletions controller/app/src/main/res/drawable/ic_archive.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- Material Symbols "archive" (box) — ADFA-4961: Back up card icon. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path android:fillColor="#FF000000"
android:pathData="M20.54,5.23l-1.39,-1.68C18.88,3.21 18.47,3 18,3H6c-0.47,0 -0.88,0.21 -1.16,0.55L3.46,5.23C3.17,5.57 3,6.02 3,6.5V19c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V6.5c0,-0.48 -0.17,-0.93 -0.46,-1.27zM12,17.5L6.5,12H10v-2h4v2h3.5L12,17.5zM5.12,5l0.81,-1h12l0.94,1H5.12z"/>
</vector>
8 changes: 8 additions & 0 deletions controller/app/src/main/res/drawable/ic_unarchive.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- Material Symbols "unarchive" (box with an up arrow) — ADFA-4961: Restore card icon. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path android:fillColor="#FF000000"
android:pathData="M20.55,5.22l-1.39,-1.68C18.88,3.21 18.47,3 18,3H6c-0.47,0 -0.88,0.21 -1.15,0.55L3.46,5.22C3.17,5.57 3,6.01 3,6.5V19c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V6.5c0,-0.49 -0.17,-0.93 -0.45,-1.28zM12,9.5l5.5,5.5H14v2h-4v-2H6.5L12,9.5zM5.12,5l0.82,-1h12l0.93,1H5.12z"/>
</vector>
Loading
Loading