From af7655b572b1936e3e6e2537bab99c6ad990eb19 Mon Sep 17 00:00:00 2001 From: Ark74 Date: Thu, 2 Apr 2026 15:56:34 -0600 Subject: [PATCH] =?UTF-8?q?[controller]=20redise=C3=B1o=20y=20actualizaci?= =?UTF-8?q?=C3=B3n=20en=20interacci=C3=B3n=20de=20los=20botones=20atado=20?= =?UTF-8?q?a=20eventos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/iiab/controller/MainActivity.java | 220 +++++++++--------- .../org/iiab/controller/ProgressButton.java | 147 ++++++++++++ .../app/src/main/res/layout/main.xml | 9 +- .../app/src/main/res/values/attrs.xml | 6 + .../app/src/main/res/values/integers.xml | 4 + 5 files changed, 270 insertions(+), 116 deletions(-) create mode 100644 apk/controller/app/src/main/java/org/iiab/controller/ProgressButton.java create mode 100644 apk/controller/app/src/main/res/values/integers.xml diff --git a/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java b/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java index 2d6fc35..530968e 100644 --- a/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java +++ b/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java @@ -112,12 +112,16 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe // Cassette Deck UI private LinearLayout deckContainer; - private Button btnServerControl; + private ProgressButton btnServerControl; private ObjectAnimator fusionAnimator; private android.animation.ObjectAnimator exploreAnimator; private boolean isServerAlive = false; private boolean isNegotiating = false; private boolean isProxyDegraded = false; + private Boolean targetServerState = null; + private String serverTransitionText = ""; + private final Handler timeoutHandler = new Handler(android.os.Looper.getMainLooper()); + private Runnable timeoutRunnable; private String currentTargetUrl = null; private long pulseStartTime = 0; @@ -271,14 +275,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe // Listeners watchdogControl.setOnClickListener(v -> { boolean willBeEnabled = !prefs.getWatchdogEnable(); - if (willBeEnabled) { - BiometricHelper.prompt(MainActivity.this, - getString(R.string.unlock_watchdog_title), - getString(R.string.unlock_watchdog_subtitle), - () -> setWatchdogState(true)); - } else { - setWatchdogState(false); - } + setWatchdogState(willBeEnabled); }); btnClearLog.setOnClickListener(this); btnCopyLog.setOnClickListener(this); @@ -294,30 +291,42 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe button_save.setOnClickListener(this); btnServerControl.setOnClickListener(v -> { - // We're simplifying the action for now to test - if (btnServerControl.getText().toString().contains("Launch")) { - btnServerControl.setText("Starting..."); - btnServerControl.setAlpha(0.7f); // Efecto visual de "Cargando" + // Ignore clicks if we are already waiting for a state change + if (targetServerState != null) return; + + // Freeze the transition text and define the TARGET state + serverTransitionText = !isServerAlive ? "Booting..." : "Shutting down..."; + targetServerState = !isServerAlive; + + // Lock the UI and start infinite animation + updateUIColorsAndVisibility(); + btnServerControl.startProgress(); + + // Set a hard timeout (45 seconds) as a safety net + timeoutRunnable = () -> { + if (targetServerState != null) { + targetServerState = null; // Abort transition + btnServerControl.stopProgress(); + updateUIColorsAndVisibility(); + addToLog("Warning: Server state transition timed out."); + } + }; + timeoutHandler.postDelayed(timeoutRunnable, 45000); + + // Execute the corresponding script command + if (!isServerAlive) { startTermuxEnvironmentVisible("--start"); } else { - // TODO: We'll add the VPN biometric validation here later - btnServerControl.setText("Stopping..."); - btnServerControl.setAlpha(0.7f); startTermuxEnvironmentVisible("--stop"); - // Automatically turn off the Watchdog if we take down the server. - if (prefs.getWatchdogEnable()) setWatchdogState(false); + // Turn off Watchdog gracefully when stopping the server manually + if (prefs.getWatchdogEnable()) { + setWatchdogState(false); + } } }); // Logic to open the WebView (PortalActivity) -// button_browse_content.setOnClickListener(v -> { -// Intent intent = new Intent(MainActivity.this, PortalActivity.class); -// // We tell the Portal exactly where to go -// String urlToLoad = prefs.getEnable() ? "http://box/home" : "http://localhost:8085/home"; -// intent.putExtra("TARGET_URL", urlToLoad); -// startActivity(intent); -// }); button_browse_content.setOnClickListener(v -> { if (currentTargetUrl != null) { Intent intent = new Intent(MainActivity.this, PortalActivity.class); @@ -442,7 +451,13 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe // We evaluate the results isNegotiating = false; isServerAlive = boxAlive || localAlive; - isProxyDegraded = !boxAlive && localAlive; // Tunnel on, but proxy dead + + // If VPN is ON but box/proxy is dead, the tunnel is degraded (Orange). + if (prefs.getEnable()) { + isProxyDegraded = !boxAlive; + } else { + isProxyDegraded = false; + } if (boxAlive) { currentTargetUrl = "http://box/home"; @@ -690,7 +705,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe } updateUI(); - updateUIColorsAndVisibility(isServerAlive); + updateUIColorsAndVisibility(); } private void handleControlClick() { @@ -795,13 +810,20 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe if (vpnOn) { // The passive radar must also use the proxy to test the tunnel. boxAlive = pingUrl("http://box/home", true); - isProxyDegraded = !boxAlive && localAlive; + isProxyDegraded = !boxAlive; } else { isProxyDegraded = false; } isServerAlive = localAlive || boxAlive; + // STATE MACHINE: Has the target state been reached? + if (targetServerState != null && isServerAlive == targetServerState) { + targetServerState = null; // Transition complete! + timeoutHandler.removeCallbacks(timeoutRunnable); // Cancel safety net + runOnUiThread(() -> btnServerControl.stopProgress()); // Unlock button + } + if (vpnOn && boxAlive) { currentTargetUrl = "http://box/home"; } else if (localAlive) { @@ -818,120 +840,90 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe boolean isVpnActive = prefs.getEnable(); boolean isWatchdogOn = prefs.getWatchdogEnable(); - // Draw island + // Draw island (Tunnel LED colors) if (dashboardManager != null) { dashboardManager.setTunnelState(isVpnActive, isProxyDegraded); } - // Draw main button - if (isVpnActive) { - button_control.setText(R.string.control_disable); - button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, isServerAlive ? R.color.btn_vpn_on : R.color.btn_vpn_on_dim)); + // Draw main VPN button (ESPW) + if (!isServerAlive) { + // Lock and dim the VPN button if there is no server to connect to + button_control.setEnabled(false); + if (isVpnActive) { + button_control.setText(R.string.control_disable); + button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on_dim)); + } else { + button_control.setText(R.string.control_enable); + button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_off_dim)); + } } else { - button_control.setText(R.string.control_enable); - button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, isServerAlive ? R.color.btn_vpn_off : R.color.btn_vpn_off_dim)); + // Unlock if server is alive + button_control.setEnabled(true); + if (isVpnActive) { + button_control.setText(R.string.control_disable); + button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on)); + } else { + button_control.setText(R.string.control_enable); + button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_off)); + } } - // 3. Draw Explore Content button + // Draw Explore Content button + // Ensure it is ALWAYS visible, never GONE + button_browse_content.setVisibility(View.VISIBLE); + if (!isServerAlive) { - // State 1: Stopped + // State 1: Stopped (Greyed out) stopExplorePulse(); button_browse_content.setEnabled(false); button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_explore_disabled)); button_browse_content.setAlpha(1.0f); - button_browse_content.setTextColor(Color.parseColor("#888888")); // Texto grisΓ‘ceo apagado + button_browse_content.setTextColor(Color.parseColor("#888888")); } else if (isNegotiating) { - // State 2: Negotiating + // State 3: Negotiating button_browse_content.setEnabled(true); button_browse_content.setTextColor(Color.WHITE); - // (El latido ya maneja la opacidad al 100%) } else { + // State: Alive stopExplorePulse(); button_browse_content.setEnabled(true); button_browse_content.setTextColor(Color.WHITE); button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_explore_ready)); if (isVpnActive && !isProxyDegraded) { - // State 5: All good - button_browse_content.setAlpha(1.0f); - } else { - // State 2: local or state 4 - button_browse_content.setAlpha(0.6f); - } - } - - // FUSION LOGIC - Watchdog - btnServerControl.setAlpha(1.0f); - if (isServerAlive) { - btnServerControl.setText("πŸ›‘ Stop Server"); - if (isWatchdogOn) { - deckContainer.setBackgroundColor(Color.parseColor("#44FF9800")); - btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_on)); - watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_on)); - } else { - if (fusionAnimator == null || !fusionAnimator.isRunning()) deckContainer.setBackgroundColor(Color.TRANSPARENT); - btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_danger)); - watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_off)); - } - } else { - deckContainer.setBackgroundColor(Color.TRANSPARENT); - btnServerControl.setText("πŸš€ Launch Server"); - btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_success)); - watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, isWatchdogOn ? R.color.btn_watchdog_on : R.color.btn_watchdog_off)); - } - } - - private void updateUIColorsAndVisibility(boolean isServerAlive) { - boolean isVpnActive = prefs.getEnable(); - - if (!isServerAlive) { - button_control.setEnabled(false); // Disable ESPW click - button_browse_content.setVisibility(View.GONE); - stopExplorePulse(); // We stop the animation if the server dies - - if (isVpnActive) { - button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on_dim)); - } else { - button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_off_dim)); - } - } else { - button_control.setEnabled(true); // Enable ESPW click - button_browse_content.setVisibility(View.VISIBLE); - - // Heart rate and diluted color control - if (isVpnActive) { + button_browse_content.setAlpha(1.0f); // 100% Perfect state startExplorePulse(); } else { - stopExplorePulse(); + button_browse_content.setAlpha(0.6f); // Watered down fallback state } } - // --- THE FUSION LOGIC (CASSETTE DECK) --- - boolean isWatchdogOn = prefs.getWatchdogEnable(); - btnServerControl.setAlpha(1.0f); // Reset opacity - if (isServerAlive) { - btnServerControl.setText("πŸ›‘ Stop Server"); - - if (isWatchdogOn) { - // FUSION: Server alive + Watchdog active - // DELETED the stopFusionPulse() from here so the animation can live! - deckContainer.setBackgroundColor(Color.parseColor("#44FF9800")); // Bright border/background - btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_on)); - watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_on)); - } else { - // Sever alive, without Watchdog - if (fusionAnimator == null || !fusionAnimator.isRunning()) { - deckContainer.setBackgroundColor(Color.TRANSPARENT); - } - btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_danger)); // Red - watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_off)); - } + // FUSION LOGIC (Watchdog & Server Control) + if (targetServerState != null) { + // STATE: COOL-OFF (Locked) + btnServerControl.setAlpha(0.6f); + btnServerControl.setText(serverTransitionText); + btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_explore_disabled)); } else { - // Server offline - deckContainer.setBackgroundColor(Color.TRANSPARENT); - btnServerControl.setText("πŸš€ Launch Server"); - btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_success)); // Verde - watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, isWatchdogOn ? R.color.btn_watchdog_on : R.color.btn_watchdog_off)); + // STATE: NORMAL (Unlocked) + btnServerControl.setAlpha(1.0f); + if (isServerAlive) { + btnServerControl.setText("πŸ›‘ Stop Server"); + if (isWatchdogOn) { + deckContainer.setBackgroundColor(Color.parseColor("#44FF9800")); + btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_on)); + watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_on)); + } else { + if (fusionAnimator == null || !fusionAnimator.isRunning()) deckContainer.setBackgroundColor(Color.TRANSPARENT); + btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_danger)); + watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_off)); + } + } else { + deckContainer.setBackgroundColor(Color.TRANSPARENT); + btnServerControl.setText("πŸš€ Launch Server"); + btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_success)); + watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, isWatchdogOn ? R.color.btn_watchdog_on : R.color.btn_watchdog_off)); + } } } diff --git a/apk/controller/app/src/main/java/org/iiab/controller/ProgressButton.java b/apk/controller/app/src/main/java/org/iiab/controller/ProgressButton.java new file mode 100644 index 0000000..3a1bda7 --- /dev/null +++ b/apk/controller/app/src/main/java/org/iiab/controller/ProgressButton.java @@ -0,0 +1,147 @@ +package org.iiab.controller; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Path; +import android.util.AttributeSet; + +import androidx.core.content.ContextCompat; +import androidx.appcompat.widget.AppCompatButton; + +public class ProgressButton extends AppCompatButton { + + private Paint progressPaint; + private Paint progressBackgroundPaint; + + private int progressColor; + private int progressBackgroundColor; + private Path clipPath; + private RectF rectF; + private float cornerRadius; + private int progressHeight; + + // Animation variables + private float currentProgress = 0f; + private boolean isRunning = false; + private ValueAnimator animator; + + public ProgressButton(Context context) { + super(context); + init(context, null); + } + + public ProgressButton(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + public ProgressButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs); + } + + private void init(Context context, AttributeSet attrs) { + if (attrs != null) { + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ProgressButton, 0, 0); + try { + progressColor = a.getColor(R.styleable.ProgressButton_progressButtonColor, ContextCompat.getColor(context, R.color.btn_danger)); + progressBackgroundColor = a.getColor(R.styleable.ProgressButton_progressButtonBackgroundColor, Color.parseColor("#44888888")); + progressHeight = a.getDimensionPixelSize(R.styleable.ProgressButton_progressButtonHeight, (int) (6 * getResources().getDisplayMetrics().density)); + // Note: We no longer read 'duration' from XML because the animation is infinite. + } finally { + a.recycle(); + } + } else { + // Safe defaults + progressColor = ContextCompat.getColor(context, R.color.btn_danger); + progressBackgroundColor = Color.parseColor("#44888888"); + progressHeight = (int) (6 * getResources().getDisplayMetrics().density); + } + + progressPaint = new Paint(); + progressPaint.setColor(progressColor); + progressPaint.setStyle(Paint.Style.FILL); + + progressBackgroundPaint = new Paint(); + progressBackgroundPaint.setColor(progressBackgroundColor); + progressBackgroundPaint.setStyle(Paint.Style.FILL); + + // Initialize clipping path variables + clipPath = new Path(); + rectF = new RectF(); + cornerRadius = 8 * getResources().getDisplayMetrics().density; + } + + @Override + protected void onDraw(Canvas canvas) { + // Draw the background and text first + super.onDraw(canvas); + + // Draw the progress bar constrained by the button's rounded corners + if (progressHeight > 0 && isRunning) { + int buttonWidth = getWidth(); + int buttonHeight = getHeight(); + + // Calculate width based on the current animated float (0.0f to 1.0f) + int progressWidth = (int) (buttonWidth * currentProgress); + + // 1. Prepare the rounded mask (matches the button's bounds) + rectF.set(0, 0, buttonWidth, buttonHeight); + clipPath.reset(); + clipPath.addRoundRect(rectF, cornerRadius, cornerRadius, Path.Direction.CW); + + // 2. Save canvas state and apply the mask + canvas.save(); + canvas.clipPath(clipPath); + + // 3. Draw the tracks + canvas.drawRect(0, buttonHeight - progressHeight, buttonWidth, buttonHeight, progressBackgroundPaint); + canvas.drawRect(0, buttonHeight - progressHeight, progressWidth, buttonHeight, progressPaint); + + // 4. Restore canvas + canvas.restore(); + } + } + + /** + * Starts an infinite cyclic animation (fills and empties the bar). + * Disables the button to prevent spam clicks. + */ + public void startProgress() { + if (isRunning) return; + isRunning = true; + setEnabled(false); // Lock the button immediately + + // Create an animator that goes from 0.0 to 1.0 (empty to full) + animator = ValueAnimator.ofFloat(0f, 1f); + animator.setDuration(1200); // 1.2 seconds per sweep + animator.setRepeatMode(ValueAnimator.REVERSE); // Fill up, then empty down + animator.setRepeatCount(ValueAnimator.INFINITE); // Never stop until commanded + + animator.addUpdateListener(animation -> { + currentProgress = (float) animation.getAnimatedValue(); + invalidate(); // Force redraw on every frame + }); + + animator.start(); + } + + /** + * Stops the animation, clears the bar, and unlocks the button. + * To be called by the Controller when the backend confirms the state change. + */ + public void stopProgress() { + if (animator != null && animator.isRunning()) { + animator.cancel(); + } + isRunning = false; + setEnabled(true); // Unlock button + currentProgress = 0f; // Reset width + invalidate(); // Clear the bar visually + } +} \ No newline at end of file diff --git a/apk/controller/app/src/main/res/layout/main.xml b/apk/controller/app/src/main/res/layout/main.xml index cc23211..7ae5401 100644 --- a/apk/controller/app/src/main/res/layout/main.xml +++ b/apk/controller/app/src/main/res/layout/main.xml @@ -343,7 +343,7 @@ android:baselineAligned="false" android:weightSum="2"> -