[controller] rediseño y actualización en interacción de los botones atado a eventos

This commit is contained in:
Luis Guzmán 2026-04-02 15:56:34 -06:00
parent 5eac64be96
commit af7655b572
5 changed files with 270 additions and 116 deletions

View File

@ -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));
}
}
}

View File

@ -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
}
}

View File

@ -343,7 +343,7 @@
android:baselineAligned="false"
android:weightSum="2">
<Button
<org.iiab.controller.ProgressButton
android:id="@+id/btn_server_control"
android:layout_width="0dp"
android:layout_height="80dp"
@ -355,7 +355,12 @@
android:textColor="#FFFFFF"
android:background="@drawable/rounded_button"
android:backgroundTint="@color/btn_success"
android:textAllCaps="false"/>
android:textAllCaps="false"
app:progressButtonHeight="6dp"
app:progressButtonDuration="@integer/server_cool_off_duration_ms"
app:progressButtonColor="#FF9800" />
<Button
android:id="@+id/watchdog_control"
android:layout_width="0dp"

View File

@ -2,4 +2,10 @@
<resources>
<attr name="sectionBackground" format="color" />
<attr name="sectionHeaderBackground" format="color" />
<declare-styleable name="ProgressButton">
<attr name="progressButtonColor" format="color" />
<attr name="progressButtonBackgroundColor" format="color" />
<attr name="progressButtonHeight" format="dimension" />
<attr name="progressButtonDuration" format="integer" />
</declare-styleable>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="server_cool_off_duration_ms">15000</integer>
</resources>