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
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ private InstallProgressRepository() {
/** Marks the upcoming posts as belonging to the scratch-reset pipeline. */
public void beginReset() { currentOp = InstallState.Op.RESET; }

/** ADFA-5011: marks the upcoming posts as belonging to a dash-node REST-core rebuild. */
public void beginRebuild() { currentOp = InstallState.Op.REBUILD; }

/** The operation the current state belongs to (INSTALL when idle). */
public InstallState.Op currentOp() { return current().op; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ public final class InstallService extends Service {
public static final String ACTION_CANCEL = "org.iiab.controller.INSTALL_CANCEL";
// Per-module install queue (ADFA-4476 slice 3): distinct from the rootfs ACTION_START.
public static final String ACTION_START_MODULES = "org.iiab.controller.INSTALL_START_MODULES";
/** ADFA-5011: rebuild the dash-node REST core in place (no rootfs rebuild). Reuses this service's
* guard/foreground/status-window so the op can't be killed mid-rebuild. */
public static final String ACTION_REBUILD_DASHBOARD = "org.iiab.controller.REBUILD_DASHBOARD";

// Broadcast of per-line provisioning output (best-effort in-app log).
public static final String ACTION_INSTALL_LOG = "org.iiab.controller.INSTALL_LOG";
Expand Down Expand Up @@ -142,6 +145,23 @@ public int onStartCommand(Intent intent, int flags, int startId) {
doCancel();
return START_NOT_STICKY;
}
if (ACTION_REBUILD_DASHBOARD.equals(action)) {
if (started) return START_NOT_STICKY;
started = true;
rebuildMode = true;
org.iiab.controller.InstallGuard.begin(this); // exclusive: no concurrent proot op
iiabRootDir = new File(getFilesDir(), "rootfs");
debianRootfs = new File(iiabRootDir, "installed-rootfs/iiab");
if (prootEngine == null) prootEngine = new PRootEngine();
startForeground(NOTIFICATION_ID, buildNotification(getString(R.string.k2go_dash_rebuilding)));
acquireHardwareLocks();
// ADFA-5011: tag posts as REBUILD so SetupProgressActivity treats this as a blocking rebuild
// session (stays on the animation, no premature "nothing to do → redirect").
InstallProgressRepository.get().beginRebuild();
InstallProgressRepository.get().postProvisioning(getString(R.string.k2go_dash_rebuilding));
new Thread(this::runDashboardRebuild, "dash-rebuild-service").start();
return START_NOT_STICKY;
}
boolean isModules = ACTION_START_MODULES.equals(action);
if (!ACTION_START.equals(action) && !isModules) {
return START_NOT_STICKY;
Expand Down Expand Up @@ -806,6 +826,26 @@ private void persistClearQueue() {
.putString("pending_modules", "").putBoolean("is_batch_installing", false).apply();
}

// ---------------------------------------------------------------- dashboard rebuild (ADFA-5011)

/** True while this service is running a dash-node rebuild (skips install-only analytics/finish). */
private boolean rebuildMode = false;

/** Drive DashboardRebuildRunner (pdsm stop -> preflight -> rebuild -> pdsm start) on the service's
* guarded, foreground lifecycle. Terminal states reuse finishSuccess()/fail() so the guard, the
* status window and teardown behave exactly like an install. */
private void runDashboardRebuild() {
new org.iiab.controller.redesign.DashboardRebuildRunner(this, prootEngine, debianRootfs.getAbsolutePath())
.start(new org.iiab.controller.redesign.DashboardRebuildRunner.Callback() {
@Override public void onLog(String line) { log(line); }
@Override public void onPreflight(org.iiab.controller.redesign.DashboardRebuildRunner.PreflightResult r) {
log("[rebuild] preflight ok=" + r.ok + " installed=" + r.installed + " available=" + r.available);
}
@Override public void onDone() { log("[rebuild] complete"); finishSuccess(); }
@Override public void onError(String reason) { log("[rebuild] error: " + reason); fail(reason); }
});
}

// ---------------------------------------------------------------- terminal

private void finishSuccess() {
Expand All @@ -814,7 +854,7 @@ private void finishSuccess() {
// ADFA-4811: clear the install guard BEFORE publishing SUCCESS, so the UI observer can
// start the server for this session (handleServerLaunchClick refuses while the guard is set).
org.iiab.controller.InstallGuard.end(this);
if (!resetMode && !moduleMode) {
if (!resetMode && !moduleMode && !rebuildMode) {
// ADFA-4466 Phase 1: operational analytics (no-op unless the operator opted in).
org.iiab.controller.analytics.AnalyticsClient.with(this)
.logInstallCompleted(tier != null ? tier.name() : null, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ public final class InstallState {

public enum Phase { IDLE, DOWNLOADING, EXTRACTING, PROVISIONING, SUCCESS, FAILED }

/** Which long-running operation this state belongs to (ADFA-4476). */
public enum Op { INSTALL, RESET }
/** Which long-running operation this state belongs to (ADFA-4476). ADFA-5011 adds REBUILD
* (dash-node REST-core rebuild) so the progress screen can tell a rebuild apart from an install
* and stay put (blocking) until it reaches SUCCESS/FAILED. */
public enum Op { INSTALL, RESET, REBUILD }

public final Phase phase;
public final Op op;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* ============================================================================
* Name : DashboardDetailFragment.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : ADFA-5011. Detail card for the dash-node REST core, mirroring the module detail
* (Play Store style): 16:9 image, title + subtitle, meta chips (live version / REST API
* / Runs offline / System core), a description of what the dashboard IS, a "What it
* includes" block and the license. The dashboard is core (not installable/removable),
* so the single action is "Rebuild" — routed through the shared DashboardRebuild gate.
* ============================================================================
*/
package org.iiab.controller.redesign;

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

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;

import org.iiab.controller.R;
import org.iiab.controller.util.AppExecutors;

public class DashboardDetailFragment extends Fragment {

private final Handler main = new Handler(Looper.getMainLooper());
private ViewGroup chips; // FlowLayout in XML — typed as ViewGroup so it wraps chips to 2 lines

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle s) {
View root = inflater.inflate(R.layout.fragment_k2go_module_detail, container, false);

TextView back = root.findViewById(R.id.k2go_moddet_back);
back.setText("‹ " + getString(R.string.k2go_mod_back));
back.setOnClickListener(v -> requireActivity().getOnBackPressedDispatcher().onBackPressed());

((ImageView) root.findViewById(R.id.k2go_moddet_image)).setImageResource(R.drawable.k2go_module_placeholder);
((TextView) root.findViewById(R.id.k2go_moddet_title)).setText(R.string.k2go_dash_detail_title);
((TextView) root.findViewById(R.id.k2go_moddet_sub)).setText(R.string.k2go_dash_detail_sub);
((TextView) root.findViewById(R.id.k2go_moddet_desc)).setText(R.string.k2go_dash_detail_desc);

// Meta chips: version (filled in live), then the fixed system-core descriptors.
chips = root.findViewById(R.id.k2go_moddet_chips);
chips.addView(chip(getString(R.string.k2go_dash_chip_rest), R.color.k2go_teal));
chips.addView(chip(getString(R.string.k2go_mod_runs_offline), R.color.k2go_leaf));
chips.addView(chip(getString(R.string.k2go_dash_chip_core), R.color.k2go_teal));
fetchVersionChip();

((TextView) root.findViewById(R.id.k2go_moddet_includes_body)).setText(R.string.k2go_dash_includes);
((TextView) root.findViewById(R.id.k2go_moddet_license))
.setText(getString(R.string.k2go_mod_license_fmt, getString(R.string.k2go_dash_license)));

// Core service — the only action is Rebuild (no schedule/install). Reuse the primary button as
// "Rebuild"; hide the secondary "Install now".
Button rebuild = root.findViewById(R.id.k2go_moddet_schedule);
rebuild.setText(R.string.k2go_dash_rebuild);
rebuild.setOnClickListener(v -> DashboardRebuild.confirmAndStart(this, root));
root.findViewById(R.id.k2go_moddet_install_now).setVisibility(View.GONE);

return root;
}

/** Read the installed version from the rootfs package.json on disk (authoritative, always present;
* no network/proot) and, if found, prepend a "v<version>" chip. */
private void fetchVersionChip() {
final Context ctx = requireContext().getApplicationContext();
AppExecutors.get().io().execute(() -> {
final String ver = DashboardVersion.installed(ctx);
main.post(() -> {
if (isAdded() && chips != null && ver != null) {
chips.addView(chip("v" + ver, R.color.k2go_teal), 0);
}
});
});
}

/** Small outlined pill for the meta-chip row (matches ModuleDetailFragment). */
private TextView chip(String text, int colorRes) {
float d = getResources().getDisplayMetrics().density;
TextView t = new TextView(requireContext());
t.setText(text);
t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_LabelMedium);
int color = ContextCompat.getColor(requireContext(), colorRes);
t.setTextColor(color);
android.graphics.drawable.GradientDrawable bg = new android.graphics.drawable.GradientDrawable();
bg.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
bg.setColor(android.graphics.Color.TRANSPARENT);
bg.setCornerRadius(11 * d);
bg.setStroke(Math.max(1, Math.round(1.4f * d)), color);
t.setBackground(bg);
int hp = Math.round(10 * d), vp = Math.round(5 * d);
t.setPadding(hp, vp, hp, vp);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
lp.rightMargin = Math.round(8 * d);
t.setLayoutParams(lp);
return t;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* ============================================================================
* Name : DashboardRebuild.java
* Author : AppDevForAll
* Copyright : Copyright (c) 2026 AppDevForAll
* Description : ADFA-5011. Single entry point for starting a dash-node REST-core rebuild, shared by
* the Module-management hub row and the dashboard detail card so both offer the exact
* same gated flow: busy check (EnvironmentLock) -> internet check -> confirm dialog ->
* start the guarded InstallService rebuild + open the progress screen (which now stays
* put until the rebuild reaches SUCCESS/FAILED).
* ============================================================================
*/
package org.iiab.controller.redesign;

import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;

import com.google.android.material.dialog.MaterialAlertDialogBuilder;

import org.iiab.controller.R;
import org.iiab.controller.install.presentation.InstallService;
import org.iiab.controller.util.Snackbars;

public final class DashboardRebuild {
private DashboardRebuild() {}

/** Gate then confirm then start. {@code anchor} is where a "busy"/"no internet" snackbar shows. */
public static void confirmAndStart(@NonNull Fragment host, @NonNull View anchor) {
Context ctx = host.requireContext();
if (org.iiab.controller.env.EnvironmentLock.isHeld(ctx)) {
Snackbars.make(anchor, R.string.k2go_install_busy).show();
return;
}
if (!hasInternet(ctx)) {
Snackbars.make(anchor, R.string.k2go_dash_needs_internet).show();
return;
}
new MaterialAlertDialogBuilder(ctx)
.setTitle(R.string.k2go_dash_rebuild_confirm_title)
.setMessage(R.string.k2go_dash_rebuild_confirm_msg)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.k2go_dash_rebuild, (d, w) -> start(host))
.show();
}

/** Kick the foreground rebuild service and open the guarded progress screen (flagged as a rebuild
* so it stays on the animation and blocks leaving until the rebuild finishes). */
private static void start(@NonNull Fragment host) {
Context ctx = host.requireContext();
Intent svc = new Intent(ctx, InstallService.class)
.setAction(InstallService.ACTION_REBUILD_DASHBOARD);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) ctx.startForegroundService(svc);
else ctx.startService(svc);
host.startActivity(new Intent(ctx, SetupProgressActivity.class)
.putExtra(SetupProgressActivity.EXTRA_REBUILD, true));
}

/** True when the device reports an internet-capable active network. Unknown -> true (let the
* preflight decide), matching the previous inline check in ModuleHubFragment. */
public static boolean hasInternet(@NonNull Context ctx) {
android.net.ConnectivityManager cm = (android.net.ConnectivityManager)
ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null) return true;
android.net.Network n = cm.getActiveNetwork();
if (n == null) return false;
android.net.NetworkCapabilities caps = cm.getNetworkCapabilities(n);
return caps != null && caps.hasCapability(android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET);
}
}
Loading