[controller] rediseño mayor de panel de control, sincronización de estados, y acciones.
This commit is contained in:
parent
1393a288c7
commit
5eac64be96
|
|
@ -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"/>
|
||||||
|
|
|
||||||
|
|
@ -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) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue