[controller] rediseño mayor de panel de control, sincronización de estados, y acciones.

This commit is contained in:
Luis Guzmán 2026-04-02 02:45:18 -06:00
parent 1393a288c7
commit 5eac64be96
15 changed files with 1030 additions and 306 deletions

View File

@ -73,6 +73,8 @@
</application> </application>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

View File

@ -0,0 +1,84 @@
package org.iiab.controller;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.PowerManager;
import android.provider.Settings;
import androidx.activity.result.ActivityResultLauncher;
import androidx.appcompat.app.AlertDialog;
public class BatteryUtils {
// Previously at MainActivity
public static void checkAndPromptOptimizations(Activity activity, ActivityResultLauncher<Intent> launcher) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PowerManager pm = (PowerManager) activity.getSystemService(Context.POWER_SERVICE);
if (pm != null && !pm.isIgnoringBatteryOptimizations(activity.getPackageName())) {
String manufacturer = Build.MANUFACTURER.toLowerCase();
String message = activity.getString(R.string.battery_opt_msg);
if (manufacturer.contains("oppo") || manufacturer.contains("realme") || manufacturer.contains("xiaomi")) {
if (manufacturer.contains("oppo") || manufacturer.contains("realme")) {
message += activity.getString(R.string.battery_opt_oppo_extra);
} else if (manufacturer.contains("xiaomi")) {
message += activity.getString(R.string.battery_opt_xiaomi_extra);
}
new AlertDialog.Builder(activity)
.setTitle(R.string.battery_opt_title)
.setMessage(message)
.setPositiveButton(R.string.go_to_settings, (dialog, which) -> openBatterySettings(activity, manufacturer))
.setNegativeButton(R.string.cancel, null)
.show();
} else {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + activity.getPackageName()));
launcher.launch(intent);
}
}
}
}
private static void openBatterySettings(Activity activity, String manufacturer) {
boolean success = false;
String packageName = activity.getPackageName();
if (manufacturer.contains("oppo") || manufacturer.contains("realme")) {
try {
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.coloros.safecenter", "com.coloros.safecenter.permission.startup.StartupAppListActivity"));
activity.startActivity(intent);
success = true;
} catch (Exception e) {
try {
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.coloros.oppoguardelf", "com.coloros.oppoguardelf.Permission.BackgroundAllowAppListActivity"));
activity.startActivity(intent);
success = true;
} catch (Exception e2) {}
}
} else if (manufacturer.contains("xiaomi")) {
try {
Intent intent = new Intent("miui.intent.action.APP_BATTERY_SAVER_SETTINGS");
intent.setComponent(new ComponentName("com.miui.powerkeeper", "com.miui.powerkeeper.ui.HiddenAppsConfigActivity"));
intent.putExtra("package_name", packageName);
intent.putExtra("package_label", activity.getString(R.string.app_name));
activity.startActivity(intent);
success = true;
} catch (Exception e) {}
}
if (!success) {
try {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + packageName));
activity.startActivity(intent);
} catch (Exception ex) {}
}
}
}

View File

@ -0,0 +1,70 @@
package org.iiab.controller;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;
import java.util.concurrent.Executor;
public class BiometricHelper {
// This is the "phone line" that tells MainActivity the user succeeded
public interface AuthCallback {
void onSuccess();
}
public static boolean isDeviceSecure(Context context) {
BiometricManager bm = BiometricManager.from(context);
int auth = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
auth = BiometricManager.Authenticators.BIOMETRIC_WEAK | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
}
android.app.KeyguardManager km = (android.app.KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
return bm.canAuthenticate(auth) == BiometricManager.BIOMETRIC_SUCCESS || (km != null && km.isDeviceSecure());
}
public static void showEnrollmentDialog(Context context) {
new AlertDialog.Builder(context)
.setTitle(R.string.security_required_title)
.setMessage(R.string.security_required_msg)
.setPositiveButton(R.string.go_to_settings, (dialog, which) -> {
Intent intent = new Intent(Settings.ACTION_SECURITY_SETTINGS);
context.startActivity(intent);
})
.setNegativeButton(R.string.cancel, null)
.show();
}
public static void prompt(AppCompatActivity activity, String title, String subtitle, AuthCallback callback) {
Executor executor = ContextCompat.getMainExecutor(activity);
BiometricPrompt biometricPrompt = new BiometricPrompt(activity, executor, new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
// Call back to MainActivity!
callback.onSuccess();
}
});
int auth = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
auth = BiometricManager.Authenticators.BIOMETRIC_WEAK | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
}
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setAllowedAuthenticators(auth)
.build();
biometricPrompt.authenticate(promptInfo);
}
}

View File

@ -0,0 +1,99 @@
package org.iiab.controller;
import android.app.Activity;
import android.content.Intent;
import android.provider.Settings;
import android.transition.AutoTransition;
import android.transition.TransitionManager;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
public class DashboardManager {
private final Activity activity;
private final LinearLayout dashboardContainer;
private final View dashWifi, dashHotspot, dashTunnel;
private final View ledWifi, ledHotspot, ledTunnel;
private final View standaloneEspwButton;
private final View standaloneEspwDescription;
// We pass a Callback so the Dashboard can tell MainActivity to start/stop the VPN
public interface DashboardActionCallback {
void onToggleEspwRequested();
}
public DashboardManager(Activity activity, View rootView, DashboardActionCallback callback) {
this.activity = activity;
// Bind all the views
dashboardContainer = (LinearLayout) rootView.findViewById(R.id.dashboard_container);
dashWifi = rootView.findViewById(R.id.dash_wifi);
dashHotspot = rootView.findViewById(R.id.dash_hotspot);
dashTunnel = rootView.findViewById(R.id.dash_tunnel);
ledWifi = rootView.findViewById(R.id.led_wifi);
ledHotspot = rootView.findViewById(R.id.led_hotspot);
ledTunnel = rootView.findViewById(R.id.led_tunnel);
standaloneEspwButton = rootView.findViewById(R.id.control);
standaloneEspwDescription = rootView.findViewById(R.id.control_description);
setupListeners(callback);
}
private void setupListeners(DashboardActionCallback callback) {
// Single tap opens Settings directly (No wrench icons needed!)
dashWifi.setOnClickListener(v -> activity.startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS)));
dashHotspot.setOnClickListener(v -> {
try {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setClassName("com.android.settings", "com.android.settings.TetherSettings");
activity.startActivity(intent);
} catch (Exception e) {
activity.startActivity(new Intent(Settings.ACTION_WIRELESS_SETTINGS));
}
});
// The Tunnel/ESPW toggle logic
View.OnClickListener toggleEspw = v -> callback.onToggleEspwRequested();
standaloneEspwButton.setOnClickListener(toggleEspw);
dashTunnel.setOnClickListener(toggleEspw);
}
// Updates the LED graphics based on actual OS connectivity states
public void updateConnectivityLeds(boolean isWifiOn, boolean isHotspotOn) {
ledWifi.setBackgroundResource(isWifiOn ? R.drawable.led_on_green : R.drawable.led_off);
ledHotspot.setBackgroundResource(isHotspotOn ? R.drawable.led_on_green : R.drawable.led_off);
}
// The Magic Morphing Animation!
public void setTunnelState(boolean isTunnelActive, boolean isDegraded) {
// Tells Android to smoothly animate any layout changes we make next
TransitionManager.beginDelayedTransition((ViewGroup) dashboardContainer.getParent(), new AutoTransition().setDuration(300));
if (isTunnelActive) {
// Morph into 33% / 33% / 33% Dashboard mode
standaloneEspwButton.setVisibility(View.GONE);
standaloneEspwDescription.setVisibility(View.GONE);
dashTunnel.setVisibility(View.VISIBLE);
ledTunnel.setBackgroundResource(isDegraded ? R.drawable.led_on_orange : R.drawable.led_on_green);
// Force recalculate
dashboardContainer.setWeightSum(3f);
} else {
// Morph back into 50% / 50% mode
dashTunnel.setVisibility(View.GONE);
standaloneEspwButton.setVisibility(View.VISIBLE);
standaloneEspwDescription.setVisibility(View.VISIBLE);
// The LED turns off implicitly since the whole dash_tunnel hides, but we can enforce it:
ledTunnel.setBackgroundResource(R.drawable.led_off);
// Force recalculate
dashboardContainer.setWeightSum(2f);
}
// Force recalculate
dashboardContainer.requestLayout();
}
}

View File

@ -0,0 +1,84 @@
package org.iiab.controller;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
public class LogManager {
private static final String LOG_FILE_NAME = "watchdog_heartbeat_log.txt";
// Callbacks to communicate with MainActivity
public interface LogReadCallback {
void onResult(String logContent, boolean isRapidGrowth);
}
public interface LogClearCallback {
void onSuccess();
void onError(String message);
}
// Read the file in the background and return the result to the main thread
public static void readLogsAsync(Context context, LogReadCallback callback) {
new Thread(() -> {
File logFile = new File(context.getFilesDir(), LOG_FILE_NAME);
StringBuilder sb = new StringBuilder();
if (!logFile.exists()) {
sb.append(context.getString(R.string.no_blackbox_found)).append("\n");
} else {
sb.append(context.getString(R.string.loading_history)).append("\n");
try (BufferedReader br = new BufferedReader(new FileReader(logFile))) {
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
} catch (IOException e) {
sb.append(context.getString(R.string.error_reading_history, e.getMessage())).append("\n");
}
sb.append(context.getString(R.string.end_of_history)).append("\n");
}
SharedPreferences internalPrefs = context.getSharedPreferences("IIAB_Internal", Context.MODE_PRIVATE);
boolean isRapid = internalPrefs.getBoolean(IIABWatchdog.PREF_RAPID_GROWTH, false);
String result = sb.toString();
// We return the call on the main UI thread
new Handler(Looper.getMainLooper()).post(() -> callback.onResult(result, isRapid));
}).start();
}
// Delete the file securely
public static void clearLogs(Context context, LogClearCallback callback) {
File logFile = new File(context.getFilesDir(), LOG_FILE_NAME);
try (PrintWriter pw = new PrintWriter(logFile)) {
pw.print("");
context.getSharedPreferences("IIAB_Internal", Context.MODE_PRIVATE)
.edit().putBoolean(IIABWatchdog.PREF_RAPID_GROWTH, false).apply();
callback.onSuccess();
} catch (IOException e) {
callback.onError(e.getMessage());
}
}
// Calculate the file size
public static String getFormattedSize(Context context) {
File logFile = new File(context.getFilesDir(), LOG_FILE_NAME);
long size = logFile.exists() ? logFile.length() : 0;
if (size < 1024) {
return size + " B";
} else if (size < 1024 * 1024) {
return String.format(Locale.getDefault(), "%.1f KB", size / 1024.0);
} else {
return String.format(Locale.getDefault(), "%.2f MB", size / (1024.0 * 1024.0));
}
}
}

View File

@ -45,7 +45,10 @@ import android.text.method.ScrollingMovementMethod;
import android.os.Build; import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.PowerManager; import android.os.PowerManager;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.provider.Settings; import android.provider.Settings;
import android.net.wifi.WifiManager;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.biometric.BiometricManager; import androidx.biometric.BiometricManager;
@ -67,6 +70,8 @@ import java.text.SimpleDateFormat;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.net.Proxy;
import java.net.InetSocketAddress;
public class MainActivity extends AppCompatActivity implements View.OnClickListener { public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private static final String TAG = "IIAB-MainActivity"; private static final String TAG = "IIAB-MainActivity";
@ -105,6 +110,19 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private TextView versionFooter; private TextView versionFooter;
private ProgressBar logProgress; private ProgressBar logProgress;
// Cassette Deck UI
private LinearLayout deckContainer;
private Button btnServerControl;
private ObjectAnimator fusionAnimator;
private android.animation.ObjectAnimator exploreAnimator;
private boolean isServerAlive = false;
private boolean isNegotiating = false;
private boolean isProxyDegraded = false;
private String currentTargetUrl = null;
private long pulseStartTime = 0;
private DashboardManager dashboardManager;
private ActivityResultLauncher<Intent> vpnPermissionLauncher; private ActivityResultLauncher<Intent> vpnPermissionLauncher;
private ActivityResultLauncher<String[]> requestPermissionsLauncher; private ActivityResultLauncher<String[]> requestPermissionsLauncher;
private ActivityResultLauncher<Intent> batteryOptLauncher; private ActivityResultLauncher<Intent> batteryOptLauncher;
@ -116,18 +134,44 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
// Variables for adaptive localhost server check // Variables for adaptive localhost server check
private final Handler serverCheckHandler = new Handler(android.os.Looper.getMainLooper()); private final Handler serverCheckHandler = new Handler(android.os.Looper.getMainLooper());
private Runnable serverCheckRunnable; private Runnable serverCheckRunnable;
private static final int MIN_CHECK_INTERVAL = 5000; // 5 seconds floor private static final int CHECK_INTERVAL_MS = 3000;
private static final int MAX_CHECK_INTERVAL = 60000; // 60 seconds ceiling
private int currentCheckInterval = MIN_CHECK_INTERVAL;
private final BroadcastReceiver logReceiver = new BroadcastReceiver() { private final BroadcastReceiver logReceiver = new BroadcastReceiver() {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if (IIABWatchdog.ACTION_LOG_MESSAGE.equals(intent.getAction())) { String action = intent.getAction();
if (IIABWatchdog.ACTION_LOG_MESSAGE.equals(action)) {
String message = intent.getStringExtra(IIABWatchdog.EXTRA_MESSAGE); String message = intent.getStringExtra(IIABWatchdog.EXTRA_MESSAGE);
addToLog(message); addToLog(message);
updateLogSizeUI(); updateLogSizeUI();
} }
else if (WatchdogService.ACTION_STATE_STARTED.equals(action)) {
// Calculate where we are in the 1200ms cycle (600ms down + 600ms up)
long elapsed = System.currentTimeMillis() - pulseStartTime;
long fullCycle = 1200;
// Find out how many milliseconds are left to finish the current wave
long remainder = elapsed % fullCycle;
long timeToNextCycleEnd = fullCycle - remainder;
// If the remaining time is too fast (< 1 second), add one more full cycle
// so the user actually has time to see the system notification drop down gracefully.
if (timeToNextCycleEnd < 1000) {
timeToNextCycleEnd += fullCycle;
}
// Wait exactly until the wave hits 1.0f alpha, then lock it!
new Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
finalizeEntryPulse();
}, timeToNextCycleEnd);
}
else if (WatchdogService.ACTION_STATE_STOPPED.equals(action)) {
// Service is down! Give it a 1.5 second visual margin, then stop the exit pulse.
new Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
finalizeExitPulse();
}, 1500);
}
} }
}; };
@ -153,7 +197,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
if (result.getResultCode() == RESULT_OK && prefs.getEnable()) { if (result.getResultCode() == RESULT_OK && prefs.getEnable()) {
connectVpn(); connectVpn();
} }
checkBatteryOptimizations(); BatteryUtils.checkAndPromptOptimizations(MainActivity.this, batteryOptLauncher);
} }
); );
@ -161,7 +205,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
new ActivityResultContracts.StartActivityForResult(), new ActivityResultContracts.StartActivityForResult(),
result -> { result -> {
Log.d(TAG, "Returned from the battery settings screen"); Log.d(TAG, "Returned from the battery settings screen");
checkBatteryOptimizations(); BatteryUtils.checkAndPromptOptimizations(MainActivity.this, batteryOptLauncher);
} }
); );
@ -217,8 +261,25 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
advConfigLabel = findViewById(R.id.adv_config_label); advConfigLabel = findViewById(R.id.adv_config_label);
logLabel = findViewById(R.id.log_label); logLabel = findViewById(R.id.log_label);
deckContainer = findViewById(R.id.deck_container);
btnServerControl = findViewById(R.id.btn_server_control);
dashboardManager = new DashboardManager(this, findViewById(android.R.id.content), () -> {
handleControlClick();
});
// Listeners // Listeners
watchdogControl.setOnClickListener(this); 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);
}
});
btnClearLog.setOnClickListener(this); btnClearLog.setOnClickListener(this);
btnCopyLog.setOnClickListener(this); btnCopyLog.setOnClickListener(this);
themeToggle.setOnClickListener(v -> toggleTheme()); themeToggle.setOnClickListener(v -> toggleTheme());
@ -231,12 +292,38 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
checkbox_global.setOnClickListener(this); checkbox_global.setOnClickListener(this);
button_apps.setOnClickListener(this); button_apps.setOnClickListener(this);
button_save.setOnClickListener(this); button_save.setOnClickListener(this);
button_control.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"
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);
}
});
// Logic to open the WebView (PortalActivity) // 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 -> { button_browse_content.setOnClickListener(v -> {
if (currentTargetUrl != null) {
Intent intent = new Intent(MainActivity.this, PortalActivity.class); Intent intent = new Intent(MainActivity.this, PortalActivity.class);
intent.putExtra("TARGET_URL", currentTargetUrl);
startActivity(intent); startActivity(intent);
}
}); });
connectionLog.setMovementMethod(new ScrollingMovementMethod()); connectionLog.setMovementMethod(new ScrollingMovementMethod());
@ -267,7 +354,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
@Override @Override
public void run() { public void run() {
checkServerStatus(); checkServerStatus();
serverCheckHandler.postDelayed(this, currentCheckInterval); updateConnectivityStatus(); // Check Wi-Fi & Hotspot states
serverCheckHandler.postDelayed(this, CHECK_INTERVAL_MS);
} }
}; };
serverCheckHandler.post(serverCheckRunnable); serverCheckHandler.post(serverCheckRunnable);
@ -276,7 +364,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private void showBatterySnackbar() { private void showBatterySnackbar() {
View rootView = findViewById(android.R.id.content); View rootView = findViewById(android.R.id.content);
Snackbar.make(rootView, R.string.battery_opt_denied, Snackbar.LENGTH_INDEFINITE) Snackbar.make(rootView, R.string.battery_opt_denied, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.fix_action, v -> checkBatteryOptimizations()) .setAction(R.string.fix_action, v -> BatteryUtils.checkAndPromptOptimizations(MainActivity.this, batteryOptLauncher))
.show(); .show();
} }
@ -298,20 +386,104 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
} }
} }
private boolean pingUrl(String urlStr, boolean useProxy) {
try {
URL url = new URL(urlStr);
HttpURLConnection conn;
if (useProxy) {
// We routed the request directly to the app's SOCKS proxy
int socksPort = prefs.getSocksPort(); // generally 1080
Proxy proxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("127.0.0.1", socksPort));
conn = (HttpURLConnection) url.openConnection(proxy);
} else {
// Normal request (for localhost)
conn = (HttpURLConnection) url.openConnection();
}
conn.setUseCaches(false);
conn.setConnectTimeout(1500);
conn.setReadTimeout(1500);
conn.setRequestMethod("GET");
return (conn.getResponseCode() >= 200 && conn.getResponseCode() < 400);
} catch (Exception e) {
return false;
}
}
private void runNegotiationSequence() {
isNegotiating = true;
runOnUiThread(() -> {
startExplorePulse(); // The orange button starts to beat.
updateUIColorsAndVisibility(); // We forced an immediate visual update
});
new Thread(() -> {
boolean boxAlive = false;
// Attempt 1 (0 seconds)
boxAlive = pingUrl("http://box/home", true);
// Attempt 2 (At 2 seconds)
if (!boxAlive) {
try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
boxAlive = pingUrl("http://box/home", true);
}
// Attempt 3 (At 3 seconds)
if (!boxAlive) {
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
boxAlive = pingUrl("http://box/home", true);
}
// We validate if localhost serves as a fallback.
boolean localAlive = pingUrl("http://localhost:8085/home", false);
// We evaluate the results
isNegotiating = false;
isServerAlive = boxAlive || localAlive;
isProxyDegraded = !boxAlive && localAlive; // Tunnel on, but proxy dead
if (boxAlive) {
currentTargetUrl = "http://box/home";
} else if (localAlive) {
currentTargetUrl = "http://localhost:8085/home";
} else {
currentTargetUrl = null;
}
runOnUiThread(this::updateUIColorsAndVisibility);
}).start();
}
private void prepareVpn() { private void prepareVpn() {
Intent intent = VpnService.prepare(MainActivity.this); Intent intent = VpnService.prepare(MainActivity.this);
if (intent != null) { if (intent != null) {
vpnPermissionLauncher.launch(intent); vpnPermissionLauncher.launch(intent);
} else { } else {
if (prefs.getEnable()) connectVpn(); if (prefs.getEnable()) connectVpn();
checkBatteryOptimizations(); BatteryUtils.checkAndPromptOptimizations(MainActivity.this, batteryOptLauncher);
} }
} }
private void handleLogToggle() { private void handleLogToggle() {
boolean isOpening = connectionLog.getVisibility() == View.GONE; boolean isOpening = connectionLog.getVisibility() == View.GONE;
if (isOpening) { if (isOpening) {
readBlackBoxLogs(); if (isReadingLogs) return;
isReadingLogs = true;
if (logProgress != null) logProgress.setVisibility(View.VISIBLE);
// We delegate the reading to the Manager
LogManager.readLogsAsync(this, (logContent, isRapidGrowth) -> {
if (connectionLog != null) {
connectionLog.setText(logContent);
scrollToBottom();
}
if (logProgress != null) logProgress.setVisibility(View.GONE);
if (logWarning != null) logWarning.setVisibility(isRapidGrowth ? View.VISIBLE : View.GONE);
updateLogSizeUI();
isReadingLogs = false;
});
startLogSizeUpdates(); startLogSizeUpdates();
} else { } else {
stopLogSizeUpdates(); stopLogSizeUpdates();
@ -332,16 +504,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private void updateLogSizeUI() { private void updateLogSizeUI() {
if (logSizeText == null) return; if (logSizeText == null) return;
File logFile = new File(getFilesDir(), "watchdog_heartbeat_log.txt"); // The LogManager class does the calculation
long size = logFile.exists() ? logFile.length() : 0; String sizeStr = LogManager.getFormattedSize(this);
String sizeStr;
if (size < 1024) {
sizeStr = size + " B";
} else if (size < 1024 * 1024) {
sizeStr = String.format(Locale.getDefault(), "%.1f KB", size / 1024.0);
} else {
sizeStr = String.format(Locale.getDefault(), "%.2f MB", size / (1024.0 * 1024.0));
}
logSizeText.setText(getString(R.string.log_size_format, sizeStr)); logSizeText.setText(getString(R.string.log_size_format, sizeStr));
} }
@ -351,51 +515,6 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
addToLog(getString(R.string.vpn_permission_granted)); addToLog(getString(R.string.vpn_permission_granted));
} }
private void readBlackBoxLogs() {
if (isReadingLogs) return;
isReadingLogs = true;
if (logProgress != null) logProgress.setVisibility(View.VISIBLE);
new Thread(() -> {
File logFile = new File(getFilesDir(), "watchdog_heartbeat_log.txt");
StringBuilder sb = new StringBuilder();
if (!logFile.exists()) {
sb.append(getString(R.string.no_blackbox_found)).append("\n");
} else {
sb.append(getString(R.string.loading_history)).append("\n");
try (BufferedReader br = new BufferedReader(new FileReader(logFile))) {
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append("\n");
}
} catch (IOException e) {
sb.append(getString(R.string.error_reading_history, e.getMessage())).append("\n");
}
sb.append(getString(R.string.end_of_history)).append("\n");
}
final String result = sb.toString();
SharedPreferences internalPrefs = getSharedPreferences("IIAB_Internal", Context.MODE_PRIVATE);
final boolean isRapid = internalPrefs.getBoolean(IIABWatchdog.PREF_RAPID_GROWTH, false);
runOnUiThread(() -> {
if (connectionLog != null) {
connectionLog.setText(result);
scrollToBottom();
}
if (logProgress != null) {
logProgress.setVisibility(View.GONE);
}
if (logWarning != null) {
logWarning.setVisibility(isRapid ? View.VISIBLE : View.GONE);
}
updateLogSizeUI();
isReadingLogs = false;
});
}).start();
}
private void setVersionFooter() { private void setVersionFooter() {
try { try {
PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
@ -424,6 +543,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
showBatterySnackbar(); showBatterySnackbar();
} }
} }
updateConnectivityStatus(); // Force instant UI refresh when returning to app
if (getIntent() != null && getIntent().getBooleanExtra(VpnRecoveryReceiver.EXTRA_RECOVERY, false)) { if (getIntent() != null && getIntent().getBooleanExtra(VpnRecoveryReceiver.EXTRA_RECOVERY, false)) {
addToLog(getString(R.string.recovery_pulse_received)); addToLog(getString(R.string.recovery_pulse_received));
@ -444,73 +564,6 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
setIntent(intent); setIntent(intent);
} }
private void checkBatteryOptimizations() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
if (pm != null && !pm.isIgnoringBatteryOptimizations(getPackageName())) {
String manufacturer = Build.MANUFACTURER.toLowerCase();
String message = getString(R.string.battery_opt_msg);
if (manufacturer.contains("oppo") || manufacturer.contains("realme") || manufacturer.contains("xiaomi")) {
if (manufacturer.contains("oppo") || manufacturer.contains("realme")) {
message += getString(R.string.battery_opt_oppo_extra);
} else if (manufacturer.contains("xiaomi")) {
message += getString(R.string.battery_opt_xiaomi_extra);
}
new AlertDialog.Builder(this)
.setTitle(R.string.battery_opt_title)
.setMessage(message)
.setPositiveButton(R.string.go_to_settings, (dialog, which) -> openBatterySettings(manufacturer))
.setNegativeButton(R.string.cancel, null)
.show();
} else {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + getPackageName()));
batteryOptLauncher.launch(intent);
}
}
}
}
private void openBatterySettings(String manufacturer) {
boolean success = false;
if (manufacturer.contains("oppo") || manufacturer.contains("realme")) {
try {
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.coloros.safecenter", "com.coloros.safecenter.permission.startup.StartupAppListActivity"));
startActivity(intent);
success = true;
} catch (Exception e) {
try {
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.coloros.oppoguardelf", "com.coloros.oppoguardelf.Permission.BackgroundAllowAppListActivity"));
startActivity(intent);
success = true;
} catch (Exception e2) {}
}
} else if (manufacturer.contains("xiaomi")) {
try {
Intent intent = new Intent("miui.intent.action.APP_BATTERY_SAVER_SETTINGS");
intent.setComponent(new ComponentName("com.miui.powerkeeper", "com.miui.powerkeeper.ui.HiddenAppsConfigActivity"));
intent.putExtra("package_name", getPackageName());
intent.putExtra("package_label", getString(R.string.app_name));
startActivity(intent);
success = true;
} catch (Exception e) {}
}
if (!success) {
try {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
} catch (Exception ex) {}
}
}
private void toggleTheme() { private void toggleTheme() {
SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE); SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE);
int currentMode = AppCompatDelegate.getDefaultNightMode(); int currentMode = AppCompatDelegate.getDefaultNightMode();
@ -543,7 +596,11 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
IntentFilter filter = new IntentFilter(IIABWatchdog.ACTION_LOG_MESSAGE); IntentFilter filter = new IntentFilter();
filter.addAction(IIABWatchdog.ACTION_LOG_MESSAGE);
filter.addAction(WatchdogService.ACTION_STATE_STARTED);
filter.addAction(WatchdogService.ACTION_STATE_STOPPED);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(logReceiver, filter, Context.RECEIVER_NOT_EXPORTED); registerReceiver(logReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else { } else {
@ -591,145 +648,106 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
} }
private void resetLogFile() { private void resetLogFile() {
File logFile = new File(getFilesDir(), "watchdog_heartbeat_log.txt"); LogManager.clearLogs(this, new LogManager.LogClearCallback() {
try (PrintWriter pw = new PrintWriter(logFile)) { @Override
pw.print(""); public void onSuccess() {
connectionLog.setText(""); connectionLog.setText("");
addToLog(getString(R.string.log_reset_user)); addToLog(getString(R.string.log_reset_user));
getSharedPreferences("IIAB_Internal", Context.MODE_PRIVATE).edit().putBoolean(IIABWatchdog.PREF_RAPID_GROWTH, false).apply();
if (logWarning != null) logWarning.setVisibility(View.GONE); if (logWarning != null) logWarning.setVisibility(View.GONE);
updateLogSizeUI(); updateLogSizeUI();
Toast.makeText(this, R.string.log_cleared_toast, Toast.LENGTH_SHORT).show(); Toast.makeText(MainActivity.this, R.string.log_cleared_toast, Toast.LENGTH_SHORT).show();
} catch (IOException e) {
Toast.makeText(this, getString(R.string.failed_reset_log, e.getMessage()), Toast.LENGTH_SHORT).show();
} }
@Override
public void onError(String message) {
Toast.makeText(MainActivity.this, getString(R.string.failed_reset_log, message), Toast.LENGTH_SHORT).show();
}
});
} }
private void handleWatchdogClick() { private void handleWatchdogClick() {
toggleWatchdog(prefs.getWatchdogEnable()); setWatchdogState(!prefs.getWatchdogEnable());
} }
private void toggleWatchdog(boolean stop) { private void setWatchdogState(boolean enable) {
prefs.setWatchdogEnable(!stop); prefs.setWatchdogEnable(enable);
Intent intent = new Intent(this, WatchdogService.class); Intent intent = new Intent(this, WatchdogService.class);
if (stop) {
stopService(intent); if (enable) {
addToLog(getString(R.string.watchdog_stopped)); intent.setAction(WatchdogService.ACTION_START);
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) startForegroundService(intent.setAction(WatchdogService.ACTION_START));
else startService(intent.setAction(WatchdogService.ACTION_START));
addToLog(getString(R.string.watchdog_started)); addToLog(getString(R.string.watchdog_started));
if (isServerAlive) startFusionPulse();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent);
} else {
startService(intent);
} }
} else {
addToLog(getString(R.string.watchdog_stopped));
startExitPulse();
stopService(intent);
}
updateUI(); updateUI();
updateUIColorsAndVisibility(isServerAlive);
} }
private void handleControlClick() { private void handleControlClick() {
if (prefs.getEnable()) showBiometricPrompt(); if (prefs.getEnable()) {
else { BiometricHelper.prompt(this,
BiometricManager bm = BiometricManager.from(this); getString(R.string.auth_required_title),
int auth = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL; getString(R.string.auth_required_subtitle),
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) auth = BiometricManager.Authenticators.BIOMETRIC_WEAK | BiometricManager.Authenticators.DEVICE_CREDENTIAL; () -> {
android.app.KeyguardManager km = (android.app.KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
if (bm.canAuthenticate(auth) == BiometricManager.BIOMETRIC_SUCCESS || (km != null && km.isDeviceSecure())) {
addToLog(getString(R.string.user_initiated_conn));
toggleService(false);
} else showEnrollmentDialog();
}
}
private void showEnrollmentDialog() {
new AlertDialog.Builder(this)
.setTitle(R.string.security_required_title)
.setMessage(R.string.security_required_msg)
.setPositiveButton(R.string.go_to_settings, (dialog, which) -> {
Intent intent = new Intent(Settings.ACTION_SECURITY_SETTINGS);
startActivity(intent);
})
.setNegativeButton(R.string.cancel, null)
.show();
}
private void showBiometricPrompt() {
Executor ex = ContextCompat.getMainExecutor(this);
BiometricPrompt bp = new BiometricPrompt(this, ex, new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
addToLog(getString(R.string.auth_success_disconnect)); addToLog(getString(R.string.auth_success_disconnect));
toggleService(true); toggleService(true);
}
}); });
int auth = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL; } else {
bp.authenticate(new BiometricPrompt.PromptInfo.Builder().setTitle(getString(R.string.auth_required_title)).setSubtitle(getString(R.string.auth_required_subtitle)).setAllowedAuthenticators(auth).build()); if (BiometricHelper.isDeviceSecure(this)) {
addToLog(getString(R.string.user_initiated_conn));
toggleService(false);
} else {
BiometricHelper.showEnrollmentDialog(this);
}
}
} }
// --- Secure Advanced Settings Menu --- // --- Secure Advanced Settings Menu ---
private void handleConfigToggle() { private void handleConfigToggle() {
if (configLayout.getVisibility() == View.GONE) { if (configLayout.getVisibility() == View.GONE) {
BiometricManager bm = BiometricManager.from(this); if (BiometricHelper.isDeviceSecure(this)) {
int auth = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL; BiometricHelper.prompt(this,
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { getString(R.string.auth_required_title),
auth = BiometricManager.Authenticators.BIOMETRIC_WEAK | BiometricManager.Authenticators.DEVICE_CREDENTIAL; getString(R.string.auth_required_subtitle),
} () -> toggleVisibility(configLayout, configLabel, getString(R.string.advanced_settings_label)));
android.app.KeyguardManager km = (android.app.KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
if (bm.canAuthenticate(auth) == BiometricManager.BIOMETRIC_SUCCESS || (km != null && km.isDeviceSecure())) {
showConfigBiometricPrompt();
} else { } else {
showEnrollmentDialog(); BiometricHelper.showEnrollmentDialog(this);
} }
} else { } else {
toggleVisibility(configLayout, configLabel, getString(R.string.advanced_settings_label)); toggleVisibility(configLayout, configLabel, getString(R.string.advanced_settings_label));
} }
} }
private void showConfigBiometricPrompt() {
Executor ex = ContextCompat.getMainExecutor(this);
BiometricPrompt bp = new BiometricPrompt(this, ex, new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
toggleVisibility(configLayout, configLabel, getString(R.string.advanced_settings_label));
}
});
int auth = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
auth = BiometricManager.Authenticators.BIOMETRIC_WEAK | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
}
bp.authenticate(new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.auth_required_title))
.setSubtitle(getString(R.string.auth_required_subtitle))
.setAllowedAuthenticators(auth)
.build());
}
private void showWatchdogBiometricPrompt() {
Executor ex = ContextCompat.getMainExecutor(this);
BiometricPrompt bp = new BiometricPrompt(this, new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
toggleWatchdog(true);
}
});
int auth = BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
bp.authenticate(new BiometricPrompt.PromptInfo.Builder().setTitle(getString(R.string.unlock_watchdog_title)).setSubtitle(getString(R.string.unlock_watchdog_subtitle)).setAllowedAuthenticators(auth).build());
}
private void toggleService(boolean stop) { private void toggleService(boolean stop) {
prefs.setEnable(!stop); prefs.setEnable(!stop);
savePrefs(); savePrefs();
updateUI();
Intent intent = new Intent(this, TProxyService.class); Intent intent = new Intent(this, TProxyService.class);
startService(intent.setAction(stop ? TProxyService.ACTION_DISCONNECT : TProxyService.ACTION_CONNECT)); startService(intent.setAction(stop ? TProxyService.ACTION_DISCONNECT : TProxyService.ACTION_CONNECT));
addToLog(getString(stop ? R.string.vpn_stopping : R.string.vpn_starting)); addToLog(getString(stop ? R.string.vpn_stopping : R.string.vpn_starting));
if (!stop) {
runNegotiationSequence();
} else {
updateUIColorsAndVisibility();
}
} }
private void updateUI() { private void updateUI() {
boolean vpnActive = prefs.getEnable(); boolean vpnActive = prefs.getEnable();
boolean watchdogActive = prefs.getWatchdogEnable(); boolean watchdogActive = prefs.getWatchdogEnable();
if (dashboardManager != null) dashboardManager.setTunnelState(vpnActive, isProxyDegraded);
if (vpnActive) { if (vpnActive) {
button_control.setText(R.string.control_disable); button_control.setText(R.string.control_disable);
button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on)); button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on));
@ -739,10 +757,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
} }
if (watchdogActive) { if (watchdogActive) {
watchdogControl.setText(R.string.watchdog_disable); watchdogControl.setText(R.string.watchdog_disable);
watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_on));
} else { } else {
watchdogControl.setText(R.string.watchdog_enable); watchdogControl.setText(R.string.watchdog_enable);
watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_off));
} }
edittext_socks_addr.setText(prefs.getSocksAddress()); edittext_socks_addr.setText(prefs.getSocksAddress());
edittext_socks_udp_addr.setText(prefs.getSocksUdpAddress()); edittext_socks_udp_addr.setText(prefs.getSocksUdpAddress());
@ -769,52 +785,274 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
} }
private void checkServerStatus() { private void checkServerStatus() {
new Thread(() -> { if (isNegotiating) return;
boolean isReachable = false;
try {
URL url = new URL("http://localhost:8085/home");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(1500);
connection.setReadTimeout(1500);
connection.setRequestMethod("GET");
int responseCode = connection.getResponseCode();
isReachable = (responseCode >= 200 && responseCode < 400); new Thread(() -> {
} catch (Exception e) { boolean localAlive = pingUrl("http://localhost:8085/home", false);
isReachable = false; boolean vpnOn = prefs.getEnable();
boolean boxAlive = false;
if (vpnOn) {
// The passive radar must also use the proxy to test the tunnel.
boxAlive = pingUrl("http://box/home", true);
isProxyDegraded = !boxAlive && localAlive;
} else {
isProxyDegraded = false;
} }
final boolean serverAlive = isReachable; isServerAlive = localAlive || boxAlive;
runOnUiThread(() -> updateUIColorsAndVisibility(serverAlive));
if (vpnOn && boxAlive) {
currentTargetUrl = "http://box/home";
} else if (localAlive) {
currentTargetUrl = "http://localhost:8085/home";
} else {
currentTargetUrl = null;
}
runOnUiThread(this::updateUIColorsAndVisibility);
}).start(); }).start();
} }
private void updateUIColorsAndVisibility() {
boolean isVpnActive = prefs.getEnable();
boolean isWatchdogOn = prefs.getWatchdogEnable();
// Draw island
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));
} 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));
}
// 3. Draw Explore Content button
if (!isServerAlive) {
// State 1: Stopped
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
} else if (isNegotiating) {
// State 2: Negotiating
button_browse_content.setEnabled(true);
button_browse_content.setTextColor(Color.WHITE);
// (El latido ya maneja la opacidad al 100%)
} else {
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) { private void updateUIColorsAndVisibility(boolean isServerAlive) {
boolean isVpnActive = prefs.getEnable(); boolean isVpnActive = prefs.getEnable();
if (!isServerAlive) { if (!isServerAlive) {
currentCheckInterval = MIN_CHECK_INTERVAL;
button_control.setEnabled(false); // Disable ESPW click button_control.setEnabled(false); // Disable ESPW click
button_browse_content.setVisibility(View.GONE); button_browse_content.setVisibility(View.GONE);
stopExplorePulse(); // We stop the animation if the server dies
if (isVpnActive) { if (isVpnActive) {
button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on_dim)); button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on_dim));
} else { } else {
button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_off_dim)); button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_off_dim));
} }
} else { } else {
currentCheckInterval = Math.min((int)(currentCheckInterval * 1.5), MAX_CHECK_INTERVAL);
button_control.setEnabled(true); // Enable ESPW click button_control.setEnabled(true); // Enable ESPW click
button_browse_content.setVisibility(View.VISIBLE); // Always visible if server is alive button_browse_content.setVisibility(View.VISIBLE);
updateUI(); // Heart rate and diluted color control
if (isVpnActive) { if (isVpnActive) {
button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_explore_ready)); startExplorePulse();
} else { } else {
button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_explore_disabled)); stopExplorePulse();
} }
} }
// --- 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));
}
} 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));
}
}
private void startFusionPulse() {
deckContainer.setBackgroundColor(Color.parseColor("#44FF9800"));
if (fusionAnimator != null && fusionAnimator.isRunning()) fusionAnimator.cancel();
pulseStartTime = System.currentTimeMillis();
// Pulses infinitely until the Service broadcast stops it
fusionAnimator = ObjectAnimator.ofFloat(deckContainer, "alpha", 1f, 0.4f);
fusionAnimator.setDuration(600);
fusionAnimator.setRepeatCount(ObjectAnimator.INFINITE);
fusionAnimator.setRepeatMode(ObjectAnimator.REVERSE);
fusionAnimator.start();
}
private void startExitPulse() {
if (fusionAnimator != null && fusionAnimator.isRunning()) fusionAnimator.cancel();
// A slower, deliberate pulse infinitely until the Service + 1.5s delay stops it
fusionAnimator = ObjectAnimator.ofFloat(deckContainer, "alpha", deckContainer.getAlpha(), 0.3f);
fusionAnimator.setDuration(800);
fusionAnimator.setRepeatCount(ObjectAnimator.INFINITE);
fusionAnimator.setRepeatMode(ObjectAnimator.REVERSE);
fusionAnimator.start();
}
private void startExplorePulse() {
button_browse_content.setAlpha(1.0f); // 100% Bright Orange
button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_explore_ready));
if (exploreAnimator == null) {
android.animation.PropertyValuesHolder scaleX = android.animation.PropertyValuesHolder.ofFloat(View.SCALE_X, 1.0f, 1.03f);
android.animation.PropertyValuesHolder scaleY = android.animation.PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.0f, 1.03f);
exploreAnimator = android.animation.ObjectAnimator.ofPropertyValuesHolder(button_browse_content, scaleX, scaleY);
exploreAnimator.setDuration(800); // 800ms per heartbeat
exploreAnimator.setRepeatCount(android.animation.ObjectAnimator.INFINITE);
exploreAnimator.setRepeatMode(android.animation.ObjectAnimator.REVERSE);
}
if (!exploreAnimator.isRunning()) exploreAnimator.start();
}
private void stopExplorePulse() {
if (exploreAnimator != null && exploreAnimator.isRunning()) {
exploreAnimator.cancel();
}
// Restore to normal size
button_browse_content.setScaleX(1.0f);
button_browse_content.setScaleY(1.0f);
// Diluted orange (ready, but waiting for the tunnel)
button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_explore_ready));
button_browse_content.setAlpha(0.6f);
}
private void finalizeEntryPulse() {
if (fusionAnimator != null) fusionAnimator.cancel();
deckContainer.setAlpha(1f); // Lock solid instantly
}
private void finalizeExitPulse() {
if (fusionAnimator != null) fusionAnimator.cancel();
deckContainer.animate()
.alpha(1f)
.setDuration(300)
.withEndAction(() -> {
deckContainer.setBackgroundColor(Color.TRANSPARENT);
})
.start();
}
private void startTermuxEnvironmentVisible(String actionFlag) {
Intent intent = new Intent();
intent.setClassName("com.termux", "com.termux.app.RunCommandService");
intent.setAction("com.termux.RUN_COMMAND");
intent.putExtra("com.termux.RUN_COMMAND_PATH", "/data/data/com.termux/files/usr/bin/iiab-termux");
intent.putExtra("com.termux.RUN_COMMAND_ARGUMENTS", new String[]{actionFlag});
intent.putExtra("com.termux.RUN_COMMAND_WORKDIR", "/data/data/com.termux/files/home");
intent.putExtra("com.termux.RUN_COMMAND_BACKGROUND", false);
intent.putExtra("com.termux.RUN_COMMAND_SESSION_ACTION", "0");
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent);
} else {
startService(intent);
}
addToLog("Sent to Termux: " + actionFlag);
} catch (Exception e) {
addToLog("CRITICAL: Failed Termux Intent: " + e.getMessage());
}
}
private void updateConnectivityStatus() {
WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
boolean isWifiOn = wifiManager != null && wifiManager.isWifiEnabled();
boolean isHotspotOn = false;
try {
// 1. Try standard reflection (Works on older Androids)
java.lang.reflect.Method method = wifiManager.getClass().getDeclaredMethod("isWifiApEnabled");
method.setAccessible(true);
isHotspotOn = (Boolean) method.invoke(wifiManager);
} catch (Throwable e) {
// 2. Fallback for Android 10+: Check physical network interfaces
try {
java.util.Enumeration<java.net.NetworkInterface> interfaces = java.net.NetworkInterface.getNetworkInterfaces();
while (interfaces != null && interfaces.hasMoreElements()) {
java.net.NetworkInterface iface = interfaces.nextElement();
String name = iface.getName();
if ((name.startsWith("ap") || name.startsWith("swlan")) && iface.isUp()) {
isHotspotOn = true;
break;
}
}
} catch (Exception ex) {}
}
// Let the Dashboard handle the LEDs!
if (dashboardManager != null) dashboardManager.updateConnectivityLeds(isWifiOn, isHotspotOn);
} }
private void savePrefs() { private void savePrefs() {

View File

@ -87,10 +87,19 @@ public class PortalActivity extends AppCompatActivity {
Preferences prefs = new Preferences(this); Preferences prefs = new Preferences(this);
boolean isVpnActive = prefs.getEnable(); boolean isVpnActive = prefs.getEnable();
String targetUrl = isVpnActive ? "http://box/" : "http://localhost:8085/home";
String rawUrl = getIntent().getStringExtra("TARGET_URL");
// If for some strange reason the URL arrives empty, we use the security fallback
if (rawUrl == null || rawUrl.isEmpty()) {
rawUrl = "http://localhost:8085/home";
}
// 1. Damos alcance global seguro a la URL para todos los lambdas de aquí en adelante
final String finalTargetUrl = rawUrl;
btnHome.setOnClickListener(v -> { btnHome.setOnClickListener(v -> {
webView.loadUrl(targetUrl); webView.loadUrl(finalTargetUrl); // Usamos la variable final
resetTimer.run(); resetTimer.run();
}); });
@ -146,6 +155,22 @@ public class PortalActivity extends AppCompatActivity {
// Restore cache for normal browsing speed // Restore cache for normal browsing speed
view.getSettings().setCacheMode(android.webkit.WebSettings.LOAD_DEFAULT); view.getSettings().setCacheMode(android.webkit.WebSettings.LOAD_DEFAULT);
} }
@Override
public void onReceivedError(WebView view, android.webkit.WebResourceRequest request, android.webkit.WebResourceError error) {
super.onReceivedError(view, request, error);
if (request.isForMainFrame()) {
String customErrorHtml = "<html><body style='background-color:#1A1A1A;color:#FFFFFF;text-align:center;padding-top:50px;font-family:sans-serif;'>"
+ "<h2>⚠️ Connection Failed</h2>"
+ "<p>Unable to reach the secure environment.</p>"
+ "<p style='color:#888;font-size:12px;'>Error: " + error.getDescription() + "</p>"
+ "</body></html>";
view.loadData(customErrorHtml, "text/html", "UTF-8");
isPageLoading = false;
btnReload.setText("");
}
}
}); });
// --- MANUALLY CLOSE BAR LOGIC --- // --- MANUALLY CLOSE BAR LOGIC ---
@ -163,10 +188,10 @@ public class PortalActivity extends AppCompatActivity {
int tempPort = prefs.getSocksPort(); int tempPort = prefs.getSocksPort();
if (tempPort <= 0) tempPort = 1080; if (tempPort <= 0) tempPort = 1080;
// Variable safe to read in lambda // 3. Restauramos la variable segura para el puerto
final int finalProxyPort = tempPort; final int finalProxyPort = tempPort;
// 3. Proxy block (ONLY IF VPN IS ACTIVE) // 4. Proxy block (ONLY IF VPN IS ACTIVE)
if (isVpnActive) { if (isVpnActive) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) { if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
ProxyConfig proxyConfig = new ProxyConfig.Builder() ProxyConfig proxyConfig = new ProxyConfig.Builder()
@ -178,16 +203,16 @@ public class PortalActivity extends AppCompatActivity {
ProxyController.getInstance().setProxyOverride(proxyConfig, executor, () -> { ProxyController.getInstance().setProxyOverride(proxyConfig, executor, () -> {
Log.d(TAG, "Proxy configured on port: " + finalProxyPort); Log.d(TAG, "Proxy configured on port: " + finalProxyPort);
// Load HTML only when proxy is ready // Load HTML only when proxy is ready
webView.loadUrl(targetUrl); webView.loadUrl(finalTargetUrl);
}); });
} else { } else {
// Fallback for older devices // Fallback for older devices
Log.w(TAG, "Proxy Override not supported"); Log.w(TAG, "Proxy Override not supported");
webView.loadUrl(targetUrl); webView.loadUrl(finalTargetUrl);
} }
} else { } else {
// VPN is OFF. Do NOT use proxy. Just load localhost directly. // VPN is OFF. Do NOT use proxy. Just load localhost directly.
webView.loadUrl(targetUrl); webView.loadUrl(finalTargetUrl);
} }
} }

View File

@ -21,7 +21,8 @@ public class WatchdogService extends Service {
public static final String ACTION_START = "org.iiab.controller.WATCHDOG_START"; public static final String ACTION_START = "org.iiab.controller.WATCHDOG_START";
public static final String ACTION_STOP = "org.iiab.controller.WATCHDOG_STOP"; public static final String ACTION_STOP = "org.iiab.controller.WATCHDOG_STOP";
public static final String ACTION_HEARTBEAT = "org.iiab.controller.HEARTBEAT"; public static final String ACTION_HEARTBEAT = "org.iiab.controller.HEARTBEAT";
public static final String ACTION_STATE_STARTED = "org.iiab.controller.WATCHDOG_STARTED";
public static final String ACTION_STATE_STOPPED = "org.iiab.controller.WATCHDOG_STOPPED";
private static final int HEARTBEAT_INTERVAL_MS = 20 * 1000; private static final int HEARTBEAT_INTERVAL_MS = 20 * 1000;
@Override @Override
@ -36,12 +37,8 @@ public class WatchdogService extends Service {
String action = intent.getAction(); String action = intent.getAction();
if (ACTION_START.equals(action)) { if (ACTION_START.equals(action)) {
startWatchdog(); startWatchdog();
} else if (ACTION_STOP.equals(action)) {
stopWatchdog();
return START_NOT_STICKY;
} else if (ACTION_HEARTBEAT.equals(action)) { } else if (ACTION_HEARTBEAT.equals(action)) {
IIABWatchdog.performHeartbeat(this); IIABWatchdog.performHeartbeat(this);
// CRITICAL: Reschedule for the next pulse to create an infinite loop
scheduleHeartbeat(); scheduleHeartbeat();
} }
} }
@ -58,15 +55,26 @@ public class WatchdogService extends Service {
IIABWatchdog.logSessionStart(this); IIABWatchdog.logSessionStart(this);
scheduleHeartbeat(); scheduleHeartbeat();
Intent startIntent = new Intent(ACTION_STATE_STARTED);
startIntent.setPackage(getPackageName());
sendBroadcast(startIntent);
} }
private void stopWatchdog() { @Override
public void onDestroy() {
// 1. Avisamos inmediatamente a la UI que nos estamos apagando
Intent stopIntent = new Intent(ACTION_STATE_STOPPED);
stopIntent.setPackage(getPackageName());
sendBroadcast(stopIntent);
// 2. Limpiamos la basura
cancelHeartbeat(); cancelHeartbeat();
IIABWatchdog.logSessionStop(this); IIABWatchdog.logSessionStop(this);
stopForeground(true); stopForeground(true);
stopSelf();
}
super.onDestroy();
}
private PendingIntent getHeartbeatPendingIntent() { private PendingIntent getHeartbeatPendingIntent() {
Intent intent = new Intent(this, WatchdogService.class); Intent intent = new Intent(this, WatchdogService.class);
intent.setAction(ACTION_HEARTBEAT); intent.setAction(ACTION_HEARTBEAT);
@ -101,12 +109,6 @@ public class WatchdogService extends Service {
} }
} }
@Override
public void onDestroy() {
stopWatchdog();
super.onDestroy();
}
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
return null; return null;

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#333333" /> <stroke android:width="1dp" android:color="#222222" />
</shape>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#00FF00" /> <stroke android:width="2dp" android:color="#4400FF00" />
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#FF9800" />
<stroke android:width="2dp" android:color="#44FF9800" />
</shape>

View File

@ -68,8 +68,88 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="16dp"> android:padding="16dp">
<!-- HR below Watchdog --> <LinearLayout
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="@color/divider_color" android:layout_marginBottom="20dp"/> android:id="@+id/dashboard_container"
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="horizontal"
android:layout_marginBottom="16dp"
android:background="@drawable/rounded_button"
android:backgroundTint="#1A1A1A"
android:paddingHorizontal="8dp"
android:gravity="center_vertical"
android:baselineAligned="false">
<LinearLayout
android:id="@+id/dash_wifi"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground"
android:padding="12dp">
<View
android:layout_width="10dp"
android:id="@+id/led_wifi"
android:layout_height="10dp"
android:layout_marginEnd="8dp"
android:background="@drawable/led_off" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Wi-Fi"
android:textColor="#FFFFFF"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/dash_hotspot"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground"
android:padding="12dp">
<View
android:id="@+id/led_hotspot"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginEnd="8dp"
android:background="@drawable/led_off" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hotspot"
android:textColor="#FFFFFF"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/dash_tunnel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground"
android:padding="12dp"
android:visibility="gone">
<View
android:id="@+id/led_tunnel"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginEnd="8dp"
android:background="@drawable/led_on_green" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tunnel"
android:textColor="#FFFFFF"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
<!-- VPN Control Section --> <!-- VPN Control Section -->
<Button <Button
@ -85,6 +165,7 @@
android:textAllCaps="false"/> android:textAllCaps="false"/>
<TextView <TextView
android:id="@+id/control_description"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/vpn_description" android:text="@string/vpn_description"
@ -109,7 +190,7 @@
android:backgroundTint="@color/btn_explore_disabled" android:backgroundTint="@color/btn_explore_disabled"
android:textAllCaps="false" android:textAllCaps="false"
android:elevation="4dp" android:elevation="4dp"
android:visibility="gone" /> android:enabled="false" />
<TextView <TextView
android:id="@+id/config_label" android:id="@+id/config_label"
@ -250,19 +331,45 @@
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="@color/divider_color" android:layout_marginBottom="16dp"/> <View android:layout_width="match_parent" android:layout_height="1dp" android:background="@color/divider_color" android:layout_marginBottom="16dp"/>
<!-- Watchdog Control Section --> <!-- Watchdog Control Section -->
<LinearLayout
android:id="@+id/deck_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginBottom="8dp"
android:background="#00000000"
android:padding="3dp"
android:orientation="horizontal"
android:baselineAligned="false"
android:weightSum="2">
<Button
android:id="@+id/btn_server_control"
android:layout_width="0dp"
android:layout_height="80dp"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:text="Launch Server"
android:textSize="15sp"
android:textStyle="bold"
android:textColor="#FFFFFF"
android:background="@drawable/rounded_button"
android:backgroundTint="@color/btn_success"
android:textAllCaps="false"/>
<Button <Button
android:id="@+id/watchdog_control" android:id="@+id/watchdog_control"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="90dp" android:layout_height="80dp"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:text="@string/watchdog_enable" android:text="@string/watchdog_enable"
android:textSize="20sp" android:textSize="15sp"
android:textStyle="bold" android:textStyle="bold"
android:textColor="#FFFFFF" android:textColor="#FFFFFF"
android:background="@drawable/rounded_button" android:background="@drawable/rounded_button"
android:backgroundTint="@color/btn_watchdog_off" android:backgroundTint="@color/btn_watchdog_off"
android:layout_marginBottom="8dp"
android:textAllCaps="false"/> android:textAllCaps="false"/>
</LinearLayout>
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -25,8 +25,8 @@
<string name="control_enable">Activar Safe Pocket Web</string> <string name="control_enable">Activar Safe Pocket Web</string>
<string name="control_disable">Desactivar Safe Pocket Web</string> <string name="control_disable">Desactivar Safe Pocket Web</string>
<string name="vpn_description">Habilite URLs amigables. Bloquee las amenazas.</string> <string name="vpn_description">Habilite URLs amigables. Bloquee las amenazas.</string>
<string name="watchdog_enable">Activar Watchdog Maestro</string> <string name="watchdog_enable">Activar\nWatchdog Maestro</string>
<string name="watchdog_disable">Desactivar Watchdog Maestro</string> <string name="watchdog_disable">Desactivar\nWatchdog Maestro</string>
<string name="log_reset_confirm_title">¿Reiniciar historial de log?</string> <string name="log_reset_confirm_title">¿Reiniciar historial de log?</string>
<string name="log_reset_confirm_msg">Esto borrará permanentemente todos los logs de conexión guardados. Esta acción no se puede deshacer.</string> <string name="log_reset_confirm_msg">Esto borrará permanentemente todos los logs de conexión guardados. Esta acción no se puede deshacer.</string>
<string name="log_warning_rapid_growth">El archivo de log está creciendo demasiado rápido, verifique si algo está fallando</string> <string name="log_warning_rapid_growth">El archivo de log está creciendo demasiado rápido, verifique si algo está fallando</string>

View File

@ -25,8 +25,8 @@
<string name="control_enable">Enable Safe Pocket Web</string> <string name="control_enable">Enable Safe Pocket Web</string>
<string name="control_disable">Disable Safe Pocket Web</string> <string name="control_disable">Disable Safe Pocket Web</string>
<string name="vpn_description">Enable friendly URLs. Lock out the threats.</string> <string name="vpn_description">Enable friendly URLs. Lock out the threats.</string>
<string name="watchdog_enable">Enable Master Watchdog</string> <string name="watchdog_enable">Enable\nMaster Watchdog</string>
<string name="watchdog_disable">Disable Master Watchdog</string> <string name="watchdog_disable">Disable\nMaster Watchdog</string>
<string name="log_reset_confirm_title">Reset Log History?</string> <string name="log_reset_confirm_title">Reset Log History?</string>
<string name="log_reset_confirm_msg">This will permanently delete all stored connection logs. This action cannot be undone.</string> <string name="log_reset_confirm_msg">This will permanently delete all stored connection logs. This action cannot be undone.</string>
<string name="log_warning_rapid_growth">The logging file is growing too rapidly, you might want to check if something is failing</string> <string name="log_warning_rapid_growth">The logging file is growing too rapidly, you might want to check if something is failing</string>

View File

@ -25,8 +25,8 @@
<string name="control_enable">Enable Safe Pocket Web</string> <string name="control_enable">Enable Safe Pocket Web</string>
<string name="control_disable">Disable Safe Pocket Web</string> <string name="control_disable">Disable Safe Pocket Web</string>
<string name="vpn_description">Enable friendly URLs. Lock out the threats.</string> <string name="vpn_description">Enable friendly URLs. Lock out the threats.</string>
<string name="watchdog_enable">Enable Master Watchdog</string> <string name="watchdog_enable">Enable\nMaster Watchdog</string>
<string name="watchdog_disable">Disable Master Watchdog</string> <string name="watchdog_disable">Disable\nMaster Watchdog</string>
<string name="log_reset_confirm_title">Reset Log History?</string> <string name="log_reset_confirm_title">Reset Log History?</string>
<string name="log_reset_confirm_msg">This will permanently delete all stored connection logs. This action cannot be undone.</string> <string name="log_reset_confirm_msg">This will permanently delete all stored connection logs. This action cannot be undone.</string>
<string name="log_warning_rapid_growth">The logging file is growing too rapidly, you might want to check if something is failing</string> <string name="log_warning_rapid_growth">The logging file is growing too rapidly, you might want to check if something is failing</string>