diff --git a/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java b/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java index fab7b04..6bdb25d 100644 --- a/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java +++ b/apk/controller/app/src/main/java/org/iiab/controller/MainActivity.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.IntentFilter; import android.content.BroadcastReceiver; import android.content.ClipData; +import android.content.res.ColorStateList; import android.content.ClipboardManager; import android.content.ComponentName; import android.content.SharedPreferences; @@ -28,6 +29,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.util.Log; import android.view.View; +import android.graphics.Color; import android.view.MotionEvent; import android.widget.Button; import android.widget.CheckBox; @@ -63,747 +65,781 @@ import java.util.Locale; import java.util.Map; import java.text.SimpleDateFormat; import java.util.concurrent.Executor; +import java.net.HttpURLConnection; +import java.net.URL; public class MainActivity extends AppCompatActivity implements View.OnClickListener { - private static final String TAG = "IIAB-MainActivity"; - private static final String TERMUX_PERMISSION = "com.termux.permission.RUN_COMMAND"; - private Preferences prefs; - private EditText edittext_socks_addr; - private EditText edittext_socks_udp_addr; - private EditText edittext_socks_port; - private EditText edittext_socks_user; - private EditText edittext_socks_pass; - private EditText edittext_dns_ipv4; - private EditText edittext_dns_ipv6; - private CheckBox checkbox_udp_in_tcp; - private CheckBox checkbox_remote_dns; - private CheckBox checkbox_global; - private CheckBox checkbox_maintenance; - private TextView textview_maintenance_warning; - private CheckBox checkbox_ipv4; - private CheckBox checkbox_ipv6; - private Button button_apps; - private Button button_save; - private Button button_control; + private static final String TAG = "IIAB-MainActivity"; + private static final String TERMUX_PERMISSION = "com.termux.permission.RUN_COMMAND"; + private Preferences prefs; + private EditText edittext_socks_addr; + private EditText edittext_socks_udp_addr; + private EditText edittext_socks_port; + private EditText edittext_socks_user; + private EditText edittext_socks_pass; + private EditText edittext_dns_ipv4; + private EditText edittext_dns_ipv6; + private CheckBox checkbox_udp_in_tcp; + private CheckBox checkbox_remote_dns; + private CheckBox checkbox_global; + private CheckBox checkbox_maintenance; + private TextView textview_maintenance_warning; + private CheckBox checkbox_ipv4; + private CheckBox checkbox_ipv6; + private Button button_apps; + private Button button_save; + private Button button_control; private Button button_browse_content; - private Button watchdogControl; - private TextView connectionLog; - private LinearLayout logActions; - private LinearLayout configLayout; - private TextView configLabel; - private LinearLayout advancedConfig; - private TextView advConfigLabel; - private TextView logLabel; - private TextView logWarning; - private TextView logSizeText; - private ImageButton themeToggle; - private TextView versionFooter; - private ProgressBar logProgress; + private Button watchdogControl; + private TextView connectionLog; + private LinearLayout logActions; + private LinearLayout configLayout; + private TextView configLabel; + private LinearLayout advancedConfig; + private TextView advConfigLabel; + private TextView logLabel; + private TextView logWarning; + private TextView logSizeText; + private ImageButton themeToggle; + private TextView versionFooter; + private ProgressBar logProgress; - private ActivityResultLauncher vpnPermissionLauncher; - private ActivityResultLauncher requestPermissionsLauncher; - private ActivityResultLauncher batteryOptLauncher; - - private boolean isReadingLogs = false; - private final Handler sizeUpdateHandler = new Handler(); - private Runnable sizeUpdateRunnable; + private ActivityResultLauncher vpnPermissionLauncher; + private ActivityResultLauncher requestPermissionsLauncher; + private ActivityResultLauncher batteryOptLauncher; - private final BroadcastReceiver logReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (IIABWatchdog.ACTION_LOG_MESSAGE.equals(intent.getAction())) { - String message = intent.getStringExtra(IIABWatchdog.EXTRA_MESSAGE); - addToLog(message); - updateLogSizeUI(); - } - } - }; + private boolean isReadingLogs = false; + private final Handler sizeUpdateHandler = new Handler(); + private Runnable sizeUpdateRunnable; - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + // Variables for adaptive localhost server check + private final Handler serverCheckHandler = new Handler(android.os.Looper.getMainLooper()); + private Runnable serverCheckRunnable; + private static final int MIN_CHECK_INTERVAL = 5000; // 5 seconds floor + private static final int MAX_CHECK_INTERVAL = 60000; // 60 seconds ceiling + private int currentCheckInterval = MIN_CHECK_INTERVAL; - prefs = new Preferences(this); - setContentView(R.layout.main); + private final BroadcastReceiver logReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (IIABWatchdog.ACTION_LOG_MESSAGE.equals(intent.getAction())) { + String message = intent.getStringExtra(IIABWatchdog.EXTRA_MESSAGE); + addToLog(message); + updateLogSizeUI(); + } + } + }; - // 1. Initialize Result Launchers - vpnPermissionLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == RESULT_OK && prefs.getEnable()) { - connectVpn(); - } - checkBatteryOptimizations(); - } - ); + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); - batteryOptLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - Log.d(TAG, "Returned from the battery settings screen"); - checkBatteryOptimizations(); - } - ); + prefs = new Preferences(this); + setContentView(R.layout.main); - requestPermissionsLauncher = registerForActivityResult( - new ActivityResultContracts.RequestMultiplePermissions(), - result -> { - for (Map.Entry entry : result.entrySet()) { - if (entry.getKey().equals(TERMUX_PERMISSION)) { - addToLog(entry.getValue() ? "Termux permission granted" : "Termux permission denied"); - } else if (entry.getKey().equals(Manifest.permission.POST_NOTIFICATIONS)) { - addToLog(entry.getValue() ? "Notification permission granted" : "Notification permission denied"); - } - } - prepareVpn(); - } - ); + // 1. Initialize Result Launchers + vpnPermissionLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK && prefs.getEnable()) { + connectVpn(); + } + checkBatteryOptimizations(); + } + ); - // UI Bindings - edittext_socks_addr = findViewById(R.id.socks_addr); - edittext_socks_udp_addr = findViewById(R.id.socks_udp_addr); - edittext_socks_port = findViewById(R.id.socks_port); - edittext_socks_user = findViewById(R.id.socks_user); - edittext_socks_pass = findViewById(R.id.socks_pass); - edittext_dns_ipv4 = findViewById(R.id.dns_ipv4); - edittext_dns_ipv6 = findViewById(R.id.dns_ipv6); - checkbox_ipv4 = findViewById(R.id.ipv4); - checkbox_ipv6 = findViewById(R.id.ipv6); - checkbox_global = findViewById(R.id.global); - checkbox_udp_in_tcp = findViewById(R.id.udp_in_tcp); - checkbox_remote_dns = findViewById(R.id.remote_dns); - checkbox_maintenance = findViewById(R.id.checkbox_maintenance); - checkbox_maintenance.setOnClickListener(this); - textview_maintenance_warning = findViewById(R.id.maintenance_warning); - button_apps = findViewById(R.id.apps); - button_save = findViewById(R.id.save); - button_control = findViewById(R.id.control); - button_browse_content = findViewById(R.id.btnBrowseContent); - watchdogControl = findViewById(R.id.watchdog_control); - - logActions = findViewById(R.id.log_actions); - Button btnClearLog = findViewById(R.id.btn_clear_log); - Button btnCopyLog = findViewById(R.id.btn_copy_log); - connectionLog = findViewById(R.id.connection_log); - logProgress = findViewById(R.id.log_progress); - logWarning = findViewById(R.id.log_warning_text); - logSizeText = findViewById(R.id.log_size_text); - themeToggle = findViewById(R.id.theme_toggle); - versionFooter = findViewById(R.id.version_text); - configLayout = findViewById(R.id.config_layout); - configLabel = findViewById(R.id.config_label); - advancedConfig = findViewById(R.id.advanced_config); - advConfigLabel = findViewById(R.id.adv_config_label); - logLabel = findViewById(R.id.log_label); + batteryOptLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + Log.d(TAG, "Returned from the battery settings screen"); + checkBatteryOptimizations(); + } + ); - // Listeners - watchdogControl.setOnClickListener(this); - btnClearLog.setOnClickListener(this); - btnCopyLog.setOnClickListener(this); - themeToggle.setOnClickListener(v -> toggleTheme()); - configLabel.setOnClickListener(v -> handleConfigToggle()); - advConfigLabel.setOnClickListener(v -> toggleVisibility(advancedConfig, advConfigLabel, getString(R.string.advanced_settings_label))); - logLabel.setOnClickListener(v -> handleLogToggle()); - checkbox_udp_in_tcp.setOnClickListener(this); - checkbox_remote_dns.setOnClickListener(this); - checkbox_global.setOnClickListener(this); - button_apps.setOnClickListener(this); - button_save.setOnClickListener(this); - button_control.setOnClickListener(this); + requestPermissionsLauncher = registerForActivityResult( + new ActivityResultContracts.RequestMultiplePermissions(), + result -> { + for (Map.Entry entry : result.entrySet()) { + if (entry.getKey().equals(TERMUX_PERMISSION)) { + addToLog(entry.getValue() ? "Termux permission granted" : "Termux permission denied"); + } else if (entry.getKey().equals(Manifest.permission.POST_NOTIFICATIONS)) { + addToLog(entry.getValue() ? "Notification permission granted" : "Notification permission denied"); + } + } + prepareVpn(); + } + ); - // Logic to open the WebView (PortalActivity) - button_browse_content.setOnClickListener(v -> { - Intent intent = new Intent(MainActivity.this, PortalActivity.class); - startActivity(intent); - }); + // UI Bindings + edittext_socks_addr = findViewById(R.id.socks_addr); + edittext_socks_udp_addr = findViewById(R.id.socks_udp_addr); + edittext_socks_port = findViewById(R.id.socks_port); + edittext_socks_user = findViewById(R.id.socks_user); + edittext_socks_pass = findViewById(R.id.socks_pass); + edittext_dns_ipv4 = findViewById(R.id.dns_ipv4); + edittext_dns_ipv6 = findViewById(R.id.dns_ipv6); + checkbox_ipv4 = findViewById(R.id.ipv4); + checkbox_ipv6 = findViewById(R.id.ipv6); + checkbox_global = findViewById(R.id.global); + checkbox_udp_in_tcp = findViewById(R.id.udp_in_tcp); + checkbox_remote_dns = findViewById(R.id.remote_dns); + checkbox_maintenance = findViewById(R.id.checkbox_maintenance); + checkbox_maintenance.setOnClickListener(this); + textview_maintenance_warning = findViewById(R.id.maintenance_warning); + button_apps = findViewById(R.id.apps); + button_save = findViewById(R.id.save); + button_control = findViewById(R.id.control); + button_browse_content = findViewById(R.id.btnBrowseContent); + watchdogControl = findViewById(R.id.watchdog_control); - connectionLog.setMovementMethod(new ScrollingMovementMethod()); - connectionLog.setTextIsSelectable(true); - connectionLog.setOnTouchListener((v, event) -> { - v.getParent().requestDisallowInterceptTouchEvent(true); - if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) { - v.getParent().requestDisallowInterceptTouchEvent(false); - } - return false; - }); + logActions = findViewById(R.id.log_actions); + Button btnClearLog = findViewById(R.id.btn_clear_log); + Button btnCopyLog = findViewById(R.id.btn_copy_log); + connectionLog = findViewById(R.id.connection_log); + logProgress = findViewById(R.id.log_progress); + logWarning = findViewById(R.id.log_warning_text); + logSizeText = findViewById(R.id.log_size_text); + themeToggle = findViewById(R.id.theme_toggle); + versionFooter = findViewById(R.id.version_text); + configLayout = findViewById(R.id.config_layout); + configLabel = findViewById(R.id.config_label); + advancedConfig = findViewById(R.id.advanced_config); + advConfigLabel = findViewById(R.id.adv_config_label); + logLabel = findViewById(R.id.log_label); - applySavedTheme(); - setVersionFooter(); - updateUI(); - initiatePermissionChain(); - - addToLog(getString(R.string.app_started)); - - sizeUpdateRunnable = new Runnable() { - @Override - public void run() { - updateLogSizeUI(); - sizeUpdateHandler.postDelayed(this, 10000); - } - }; - } + // Listeners + watchdogControl.setOnClickListener(this); + btnClearLog.setOnClickListener(this); + btnCopyLog.setOnClickListener(this); + themeToggle.setOnClickListener(v -> toggleTheme()); + configLabel.setOnClickListener(v -> handleConfigToggle()); + advConfigLabel.setOnClickListener(v -> toggleVisibility(advancedConfig, advConfigLabel, getString(R.string.advanced_settings_label))); + logLabel.setOnClickListener(v -> handleLogToggle()); + checkbox_udp_in_tcp.setOnClickListener(this); + checkbox_remote_dns.setOnClickListener(this); + checkbox_global.setOnClickListener(this); + button_apps.setOnClickListener(this); + button_save.setOnClickListener(this); + button_control.setOnClickListener(this); - private void showBatterySnackbar() { - View rootView = findViewById(android.R.id.content); - Snackbar.make(rootView, R.string.battery_opt_denied, Snackbar.LENGTH_INDEFINITE) - .setAction(R.string.fix_action, v -> checkBatteryOptimizations()) - .show(); - } + // Logic to open the WebView (PortalActivity) + button_browse_content.setOnClickListener(v -> { + Intent intent = new Intent(MainActivity.this, PortalActivity.class); + startActivity(intent); + }); - private void initiatePermissionChain() { - List permissions = new ArrayList<>(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - permissions.add(Manifest.permission.POST_NOTIFICATIONS); - } - } - if (ContextCompat.checkSelfPermission(this, TERMUX_PERMISSION) != PackageManager.PERMISSION_GRANTED) { - permissions.add(TERMUX_PERMISSION); - } + connectionLog.setMovementMethod(new ScrollingMovementMethod()); + connectionLog.setTextIsSelectable(true); + connectionLog.setOnTouchListener((v, event) -> { + v.getParent().requestDisallowInterceptTouchEvent(true); + if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) { + v.getParent().requestDisallowInterceptTouchEvent(false); + } + return false; + }); - if (!permissions.isEmpty()) { - requestPermissionsLauncher.launch(permissions.toArray(new String[0])); - } else { - prepareVpn(); - } - } + applySavedTheme(); + setVersionFooter(); + updateUI(); + initiatePermissionChain(); - private void prepareVpn() { - Intent intent = VpnService.prepare(MainActivity.this); - if (intent != null) { - vpnPermissionLauncher.launch(intent); - } else { - if (prefs.getEnable()) connectVpn(); - checkBatteryOptimizations(); - } - } + addToLog(getString(R.string.app_started)); - private void handleLogToggle() { - boolean isOpening = connectionLog.getVisibility() == View.GONE; - if (isOpening) { - readBlackBoxLogs(); - startLogSizeUpdates(); - } else { - stopLogSizeUpdates(); - } - toggleVisibility(connectionLog, logLabel, getString(R.string.connection_log_label)); - logActions.setVisibility(connectionLog.getVisibility()); - if (logSizeText != null) logSizeText.setVisibility(connectionLog.getVisibility()); - } + sizeUpdateRunnable = new Runnable() { + @Override + public void run() { + updateLogSizeUI(); + sizeUpdateHandler.postDelayed(this, 10000); + } + }; - private void startLogSizeUpdates() { - sizeUpdateHandler.removeCallbacks(sizeUpdateRunnable); - sizeUpdateHandler.post(sizeUpdateRunnable); - } + serverCheckRunnable = new Runnable() { + @Override + public void run() { + checkServerStatus(); + serverCheckHandler.postDelayed(this, currentCheckInterval); + } + }; + serverCheckHandler.post(serverCheckRunnable); + } - private void stopLogSizeUpdates() { - sizeUpdateHandler.removeCallbacks(sizeUpdateRunnable); - } + private void showBatterySnackbar() { + View rootView = findViewById(android.R.id.content); + Snackbar.make(rootView, R.string.battery_opt_denied, Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.fix_action, v -> checkBatteryOptimizations()) + .show(); + } - private void updateLogSizeUI() { - if (logSizeText == null) return; - File logFile = new File(getFilesDir(), "watchdog_heartbeat_log.txt"); - long size = logFile.exists() ? logFile.length() : 0; - 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)); - } + private void initiatePermissionChain() { + List permissions = new ArrayList<>(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + permissions.add(Manifest.permission.POST_NOTIFICATIONS); + } + } + if (ContextCompat.checkSelfPermission(this, TERMUX_PERMISSION) != PackageManager.PERMISSION_GRANTED) { + permissions.add(TERMUX_PERMISSION); + } - private void connectVpn() { - Intent intent = new Intent(this, TProxyService.class); - startService(intent.setAction(TProxyService.ACTION_CONNECT)); - addToLog(getString(R.string.vpn_permission_granted)); - } + if (!permissions.isEmpty()) { + requestPermissionsLauncher.launch(permissions.toArray(new String[0])); + } else { + prepareVpn(); + } + } - private void readBlackBoxLogs() { - if (isReadingLogs) return; - isReadingLogs = true; - if (logProgress != null) logProgress.setVisibility(View.VISIBLE); + private void prepareVpn() { + Intent intent = VpnService.prepare(MainActivity.this); + if (intent != null) { + vpnPermissionLauncher.launch(intent); + } else { + if (prefs.getEnable()) connectVpn(); + checkBatteryOptimizations(); + } + } - 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"); - } + private void handleLogToggle() { + boolean isOpening = connectionLog.getVisibility() == View.GONE; + if (isOpening) { + readBlackBoxLogs(); + startLogSizeUpdates(); + } else { + stopLogSizeUpdates(); + } + toggleVisibility(connectionLog, logLabel, getString(R.string.connection_log_label)); + logActions.setVisibility(connectionLog.getVisibility()); + if (logSizeText != null) logSizeText.setVisibility(connectionLog.getVisibility()); + } - final String result = sb.toString(); - SharedPreferences internalPrefs = getSharedPreferences("IIAB_Internal", Context.MODE_PRIVATE); - final boolean isRapid = internalPrefs.getBoolean(IIABWatchdog.PREF_RAPID_GROWTH, false); + private void startLogSizeUpdates() { + sizeUpdateHandler.removeCallbacks(sizeUpdateRunnable); + sizeUpdateHandler.post(sizeUpdateRunnable); + } - 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 stopLogSizeUpdates() { + sizeUpdateHandler.removeCallbacks(sizeUpdateRunnable); + } - private void setVersionFooter() { - try { - PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); - String version = pInfo.versionName; - versionFooter.setText(version); - } catch (PackageManager.NameNotFoundException e) { - versionFooter.setText("v0.1.x"); - } - } + private void updateLogSizeUI() { + if (logSizeText == null) return; + File logFile = new File(getFilesDir(), "watchdog_heartbeat_log.txt"); + long size = logFile.exists() ? logFile.length() : 0; + 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)); + } - @Override - protected void onPause() { - super.onPause(); - stopLogSizeUpdates(); - } + private void connectVpn() { + Intent intent = new Intent(this, TProxyService.class); + startService(intent.setAction(TProxyService.ACTION_CONNECT)); + addToLog(getString(R.string.vpn_permission_granted)); + } - @Override - protected void onResume() { - super.onResume(); - // Check battery status whenever returning to the app - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - if (pm != null && !pm.isIgnoringBatteryOptimizations(getPackageName())) { - Log.d(TAG, "onResume: Battery still optimized, showing warning"); - showBatterySnackbar(); - } - } + private void readBlackBoxLogs() { + if (isReadingLogs) return; + isReadingLogs = true; + if (logProgress != null) logProgress.setVisibility(View.VISIBLE); - if (getIntent() != null && getIntent().getBooleanExtra(VpnRecoveryReceiver.EXTRA_RECOVERY, false)) { - addToLog(getString(R.string.recovery_pulse_received)); - Intent vpnIntent = new Intent(this, TProxyService.class); - startService(vpnIntent.setAction(TProxyService.ACTION_CONNECT)); - setIntent(null); - } - if (connectionLog != null && connectionLog.getVisibility() == View.VISIBLE) { - startLogSizeUpdates(); - } - } + new Thread(() -> { + File logFile = new File(getFilesDir(), "watchdog_heartbeat_log.txt"); + StringBuilder sb = new StringBuilder(); - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - setIntent(intent); - } + 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"); + } - private void checkBatteryOptimizations() { + 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() { + try { + PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0); + String version = pInfo.versionName; + versionFooter.setText(version); + } catch (PackageManager.NameNotFoundException e) { + versionFooter.setText("v0.1.x"); + } + } + + @Override + protected void onPause() { + super.onPause(); + stopLogSizeUpdates(); + serverCheckHandler.removeCallbacks(serverCheckRunnable); + } + + @Override + protected void onResume() { + super.onResume(); + // Check battery status whenever returning to the app 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); + Log.d(TAG, "onResume: Battery still optimized, showing warning"); + showBatterySnackbar(); + } + } - if (manufacturer.contains("oppo") || manufacturer.contains("realme") || manufacturer.contains("xiaomi")) { + if (getIntent() != null && getIntent().getBooleanExtra(VpnRecoveryReceiver.EXTRA_RECOVERY, false)) { + addToLog(getString(R.string.recovery_pulse_received)); + Intent vpnIntent = new Intent(this, TProxyService.class); + startService(vpnIntent.setAction(TProxyService.ACTION_CONNECT)); + setIntent(null); + } + if (connectionLog != null && connectionLog.getVisibility() == View.VISIBLE) { + startLogSizeUpdates(); + } + serverCheckHandler.removeCallbacks(serverCheckRunnable); + serverCheckHandler.post(serverCheckRunnable); + } - 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); - } + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + } - 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 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() { + SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE); + int currentMode = AppCompatDelegate.getDefaultNightMode(); + int nextMode = (currentMode == AppCompatDelegate.MODE_NIGHT_NO) ? AppCompatDelegate.MODE_NIGHT_YES : + (currentMode == AppCompatDelegate.MODE_NIGHT_YES) ? AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM : AppCompatDelegate.MODE_NIGHT_NO; + sharedPref.edit().putInt("ui_mode", nextMode).apply(); + AppCompatDelegate.setDefaultNightMode(nextMode); + updateThemeToggleButton(nextMode); + } + + private void applySavedTheme() { + SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE); + int savedMode = sharedPref.getInt("ui_mode", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + AppCompatDelegate.setDefaultNightMode(savedMode); + updateThemeToggleButton(savedMode); + } + + private void updateThemeToggleButton(int mode) { + if (mode == AppCompatDelegate.MODE_NIGHT_NO) themeToggle.setImageResource(R.drawable.ic_theme_dark); + else if (mode == AppCompatDelegate.MODE_NIGHT_YES) themeToggle.setImageResource(R.drawable.ic_theme_light); + else themeToggle.setImageResource(R.drawable.ic_theme_system); + } + + private void toggleVisibility(View view, TextView label, String text) { + boolean isGone = view.getVisibility() == View.GONE; + view.setVisibility(isGone ? View.VISIBLE : View.GONE); + label.setText((isGone ? "▼ " : "▶ ") + text); + } + + @Override + protected void onStart() { + super.onStart(); + IntentFilter filter = new IntentFilter(IIABWatchdog.ACTION_LOG_MESSAGE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerReceiver(logReceiver, filter, Context.RECEIVER_NOT_EXPORTED); + } else { + registerReceiver(logReceiver, filter); + } + } + + @Override + protected void onStop() { + super.onStop(); + try { unregisterReceiver(logReceiver); } catch (Exception e) {} + stopLogSizeUpdates(); + } + + @Override + public void onClick(View view) { + if (view == checkbox_global || view == checkbox_remote_dns || view == checkbox_maintenance) { + savePrefs(); + updateUI(); + } else if (view == button_apps) { + startActivity(new Intent(this, AppListActivity.class)); + } else if (view.getId() == R.id.save) { + savePrefs(); + Toast.makeText(this, R.string.saved_toast, Toast.LENGTH_SHORT).show(); + addToLog(getString(R.string.settings_saved)); + } else if (view.getId() == R.id.control) handleControlClick(); + else if (view.getId() == R.id.watchdog_control) handleWatchdogClick(); + else if (view.getId() == R.id.btn_clear_log) showResetLogConfirmation(); + else if (view.getId() == R.id.btn_copy_log) { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("IIAB Log", connectionLog.getText().toString()); + if (clipboard != null) { + clipboard.setPrimaryClip(clip); + Toast.makeText(this, R.string.log_copied_toast, Toast.LENGTH_SHORT).show(); + } + } + } + + private void showResetLogConfirmation() { + new AlertDialog.Builder(this) + .setTitle(R.string.log_reset_confirm_title) + .setMessage(R.string.log_reset_confirm_msg) + .setPositiveButton(R.string.reset_log, (dialog, which) -> resetLogFile()) + .setNegativeButton(R.string.cancel, null).show(); + } + + private void resetLogFile() { + File logFile = new File(getFilesDir(), "watchdog_heartbeat_log.txt"); + try (PrintWriter pw = new PrintWriter(logFile)) { + pw.print(""); + connectionLog.setText(""); + 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); + updateLogSizeUI(); + Toast.makeText(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(); + } + } + + private void handleWatchdogClick() { + toggleWatchdog(prefs.getWatchdogEnable()); + } + + private void toggleWatchdog(boolean stop) { + prefs.setWatchdogEnable(!stop); + Intent intent = new Intent(this, WatchdogService.class); + if (stop) { + stopService(intent); + addToLog(getString(R.string.watchdog_stopped)); + } 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)); + } + updateUI(); + } + + private void handleControlClick() { + if (prefs.getEnable()) showBiometricPrompt(); + else { + BiometricManager bm = BiometricManager.from(this); + 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) 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)); + toggleService(true); + } + }); + int auth = BiometricManager.Authenticators.BIOMETRIC_STRONG | 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()); + } + + // --- Secure Advanced Settings Menu --- + private void handleConfigToggle() { + if (configLayout.getVisibility() == View.GONE) { + BiometricManager bm = BiometricManager.from(this); + 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) getSystemService(Context.KEYGUARD_SERVICE); + + if (bm.canAuthenticate(auth) == BiometricManager.BIOMETRIC_SUCCESS || (km != null && km.isDeviceSecure())) { + showConfigBiometricPrompt(); + } else { + showEnrollmentDialog(); + } + } else { + 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) { + prefs.setEnable(!stop); + savePrefs(); + updateUI(); + Intent intent = new Intent(this, TProxyService.class); + startService(intent.setAction(stop ? TProxyService.ACTION_DISCONNECT : TProxyService.ACTION_CONNECT)); + addToLog(getString(stop ? R.string.vpn_stopping : R.string.vpn_starting)); + } + + private void updateUI() { + boolean vpnActive = prefs.getEnable(); + boolean watchdogActive = prefs.getWatchdogEnable(); + if (vpnActive) { + button_control.setText(R.string.control_disable); + button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on)); + } else { + button_control.setText(R.string.control_enable); + button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_off)); + } + if (watchdogActive) { + watchdogControl.setText(R.string.watchdog_disable); + watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_on)); + } else { + watchdogControl.setText(R.string.watchdog_enable); + watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_off)); + } + edittext_socks_addr.setText(prefs.getSocksAddress()); + edittext_socks_udp_addr.setText(prefs.getSocksUdpAddress()); + edittext_socks_port.setText(String.valueOf(prefs.getSocksPort())); + edittext_socks_user.setText(prefs.getSocksUsername()); + edittext_socks_pass.setText(prefs.getSocksPassword()); + edittext_dns_ipv4.setText(prefs.getDnsIpv4()); + edittext_dns_ipv6.setText(prefs.getDnsIpv6()); + checkbox_ipv4.setChecked(prefs.getIpv4()); + checkbox_ipv6.setChecked(prefs.getIpv6()); + checkbox_global.setChecked(prefs.getGlobal()); + checkbox_udp_in_tcp.setChecked(prefs.getUdpInTcp()); + checkbox_remote_dns.setChecked(prefs.getRemoteDns()); + checkbox_maintenance.setChecked(prefs.getMaintenanceMode()); + boolean editable = !vpnActive; + edittext_socks_addr.setEnabled(editable); + edittext_socks_port.setEnabled(editable); + button_save.setEnabled(editable); + + checkbox_maintenance.setEnabled(editable); + if (textview_maintenance_warning != null) { + textview_maintenance_warning.setVisibility(vpnActive ? View.VISIBLE : View.GONE); + } + } + + private void checkServerStatus() { + new Thread(() -> { + 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); + } catch (Exception e) { + isReachable = false; + } + + final boolean serverAlive = isReachable; + runOnUiThread(() -> updateUIColorsAndVisibility(serverAlive)); + }).start(); + } + + private void updateUIColorsAndVisibility(boolean isServerAlive) { + boolean isVpnActive = prefs.getEnable(); + + if (!isServerAlive) { + currentCheckInterval = MIN_CHECK_INTERVAL; + + button_control.setEnabled(false); // Disable ESPW click + button_browse_content.setVisibility(View.GONE); + if (isVpnActive) { + button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on_dim)); + } else { + button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_off_dim)); + } + } else { + currentCheckInterval = Math.min((int)(currentCheckInterval * 1.5), MAX_CHECK_INTERVAL); + + button_control.setEnabled(true); // Enable ESPW click + button_browse_content.setVisibility(View.VISIBLE); // Always visible if server is alive + + updateUI(); + if (isVpnActive) { + button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_explore_ready)); + } else { + button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_explore_disabled)); } } } - private void openBatterySettings(String manufacturer) { - boolean success = false; + private void savePrefs() { + prefs.setSocksAddress("127.0.0.1"); + prefs.setSocksPort(1080); + prefs.setSocksUdpAddress(""); + prefs.setSocksUsername(""); + prefs.setSocksPassword(""); + prefs.setIpv4(true); + prefs.setIpv6(true); + prefs.setUdpInTcp(false); + prefs.setRemoteDns(true); + prefs.setGlobal(true); - 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) {} - } + prefs.setDnsIpv4(edittext_dns_ipv4.getText().toString()); + prefs.setDnsIpv6(edittext_dns_ipv6.getText().toString()); + prefs.setMaintenanceMode(checkbox_maintenance.isChecked()); + } - 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 addToLog(String message) { + runOnUiThread(() -> { + SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); + String currentTime = sdf.format(new Date()); + String logEntry = "[" + currentTime + "] " + message + "\n"; + if (connectionLog != null) { + connectionLog.append(logEntry); + scrollToBottom(); + } + }); + } - private void toggleTheme() { - SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE); - int currentMode = AppCompatDelegate.getDefaultNightMode(); - int nextMode = (currentMode == AppCompatDelegate.MODE_NIGHT_NO) ? AppCompatDelegate.MODE_NIGHT_YES : - (currentMode == AppCompatDelegate.MODE_NIGHT_YES) ? AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM : AppCompatDelegate.MODE_NIGHT_NO; - sharedPref.edit().putInt("ui_mode", nextMode).apply(); - AppCompatDelegate.setDefaultNightMode(nextMode); - updateThemeToggleButton(nextMode); - } - - private void applySavedTheme() { - SharedPreferences sharedPref = getPreferences(Context.MODE_PRIVATE); - int savedMode = sharedPref.getInt("ui_mode", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); - AppCompatDelegate.setDefaultNightMode(savedMode); - updateThemeToggleButton(savedMode); - } - - private void updateThemeToggleButton(int mode) { - if (mode == AppCompatDelegate.MODE_NIGHT_NO) themeToggle.setImageResource(R.drawable.ic_theme_dark); - else if (mode == AppCompatDelegate.MODE_NIGHT_YES) themeToggle.setImageResource(R.drawable.ic_theme_light); - else themeToggle.setImageResource(R.drawable.ic_theme_system); - } - - private void toggleVisibility(View view, TextView label, String text) { - boolean isGone = view.getVisibility() == View.GONE; - view.setVisibility(isGone ? View.VISIBLE : View.GONE); - label.setText((isGone ? "▼ " : "▶ ") + text); - } - - @Override - protected void onStart() { - super.onStart(); - IntentFilter filter = new IntentFilter(IIABWatchdog.ACTION_LOG_MESSAGE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - registerReceiver(logReceiver, filter, Context.RECEIVER_NOT_EXPORTED); - } else { - registerReceiver(logReceiver, filter); - } - } - - @Override - protected void onStop() { - super.onStop(); - try { unregisterReceiver(logReceiver); } catch (Exception e) {} - stopLogSizeUpdates(); - } - - @Override - public void onClick(View view) { - if (view == checkbox_global || view == checkbox_remote_dns || view == checkbox_maintenance) { - savePrefs(); - updateUI(); - } else if (view == button_apps) { - startActivity(new Intent(this, AppListActivity.class)); - } else if (view.getId() == R.id.save) { - savePrefs(); - Toast.makeText(this, R.string.saved_toast, Toast.LENGTH_SHORT).show(); - addToLog(getString(R.string.settings_saved)); - } else if (view.getId() == R.id.control) handleControlClick(); - else if (view.getId() == R.id.watchdog_control) handleWatchdogClick(); - else if (view.getId() == R.id.btn_clear_log) showResetLogConfirmation(); - else if (view.getId() == R.id.btn_copy_log) { - ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("IIAB Log", connectionLog.getText().toString()); - if (clipboard != null) { - clipboard.setPrimaryClip(clip); - Toast.makeText(this, R.string.log_copied_toast, Toast.LENGTH_SHORT).show(); - } - } - } - - private void showResetLogConfirmation() { - new AlertDialog.Builder(this) - .setTitle(R.string.log_reset_confirm_title) - .setMessage(R.string.log_reset_confirm_msg) - .setPositiveButton(R.string.reset_log, (dialog, which) -> resetLogFile()) - .setNegativeButton(R.string.cancel, null).show(); - } - - private void resetLogFile() { - File logFile = new File(getFilesDir(), "watchdog_heartbeat_log.txt"); - try (PrintWriter pw = new PrintWriter(logFile)) { - pw.print(""); - connectionLog.setText(""); - 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); - updateLogSizeUI(); - Toast.makeText(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(); - } - } - - private void handleWatchdogClick() { -// if (prefs.getWatchdogEnable()) showWatchdogBiometricPrompt(); -// else toggleWatchdog(false); - toggleWatchdog(prefs.getWatchdogEnable()); - } - - private void toggleWatchdog(boolean stop) { - prefs.setWatchdogEnable(!stop); - Intent intent = new Intent(this, WatchdogService.class); - if (stop) { - stopService(intent); - addToLog(getString(R.string.watchdog_stopped)); - } 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)); - } - updateUI(); - } - - private void handleControlClick() { - if (prefs.getEnable()) showBiometricPrompt(); - else { - BiometricManager bm = BiometricManager.from(this); - 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) 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)); - toggleService(true); - } - }); - int auth = BiometricManager.Authenticators.BIOMETRIC_STRONG | 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()); - } - - // --- Secure Advanced Settings Menu --- - private void handleConfigToggle() { - if (configLayout.getVisibility() == View.GONE) { - // The menu is closed. We want to open it, so we check for security first. - BiometricManager bm = BiometricManager.from(this); - 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) getSystemService(Context.KEYGUARD_SERVICE); - - if (bm.canAuthenticate(auth) == BiometricManager.BIOMETRIC_SUCCESS || (km != null && km.isDeviceSecure())) { - showConfigBiometricPrompt(); - } else { - showEnrollmentDialog(); // Forces user to set a PIN if device has no security - } - } else { - // The menu is open. Just close it without asking for fingerprint. - 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); - // Auth successful! Open the menu. - 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; - } - - // Reusing your existing strings to avoid compilation errors - 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) { - prefs.setEnable(!stop); - savePrefs(); - updateUI(); - Intent intent = new Intent(this, TProxyService.class); - startService(intent.setAction(stop ? TProxyService.ACTION_DISCONNECT : TProxyService.ACTION_CONNECT)); - addToLog(getString(stop ? R.string.vpn_stopping : R.string.vpn_starting)); - } - - private void updateUI() { - boolean vpnActive = prefs.getEnable(); - boolean watchdogActive = prefs.getWatchdogEnable(); - if (vpnActive) { - button_control.setText(R.string.control_disable); - button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on)); - } else { - button_control.setText(R.string.control_enable); - button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_off)); - } - if (vpnActive) { - button_browse_content.setVisibility(View.VISIBLE); - } else { - button_browse_content.setVisibility(View.GONE); - } - if (watchdogActive) { - watchdogControl.setText(R.string.watchdog_disable); - watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_on)); - } else { - watchdogControl.setText(R.string.watchdog_enable); - watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_off)); - } - edittext_socks_addr.setText(prefs.getSocksAddress()); - edittext_socks_udp_addr.setText(prefs.getSocksUdpAddress()); - edittext_socks_port.setText(String.valueOf(prefs.getSocksPort())); - edittext_socks_user.setText(prefs.getSocksUsername()); - edittext_socks_pass.setText(prefs.getSocksPassword()); - edittext_dns_ipv4.setText(prefs.getDnsIpv4()); - edittext_dns_ipv6.setText(prefs.getDnsIpv6()); - checkbox_ipv4.setChecked(prefs.getIpv4()); - checkbox_ipv6.setChecked(prefs.getIpv6()); - checkbox_global.setChecked(prefs.getGlobal()); - checkbox_udp_in_tcp.setChecked(prefs.getUdpInTcp()); - checkbox_remote_dns.setChecked(prefs.getRemoteDns()); - checkbox_maintenance.setChecked(prefs.getMaintenanceMode()); - boolean editable = !vpnActive; - edittext_socks_addr.setEnabled(editable); - edittext_socks_port.setEnabled(editable); - button_save.setEnabled(editable); - - checkbox_maintenance.setEnabled(editable); - if (textview_maintenance_warning != null) { - textview_maintenance_warning.setVisibility(vpnActive ? View.VISIBLE : View.GONE); - } - } - - //DEFAULT VALUES ON ORIGINAL INTERFACE -// private void savePrefs() { -// prefs.setSocksAddress(edittext_socks_addr.getText().toString()); -// prefs.setSocksPort(Integer.parseInt(edittext_socks_port.getText().toString())); -// prefs.setSocksUdpAddress(edittext_socks_udp_addr.getText().toString()); -// prefs.setSocksUsername(edittext_socks_user.getText().toString()); -// prefs.setSocksPassword(edittext_socks_pass.getText().toString()); -// prefs.setDnsIpv4(edittext_dns_ipv4.getText().toString()); -// prefs.setDnsIpv6(edittext_dns_ipv6.getText().toString()); -// prefs.setIpv4(checkbox_ipv4.isChecked()); -// prefs.setIpv6(checkbox_ipv6.isChecked()); -// prefs.setGlobal(checkbox_global.isChecked()); -// prefs.setUdpInTcp(checkbox_udp_in_tcp.isChecked()); -// prefs.setRemoteDns(checkbox_remote_dns.isChecked()); -// prefs.setMaintenanceMode(checkbox_maintenance.isChecked()); -// } - - private void savePrefs() { - // 1. Hardcoded / Hidden Secure Values (Walled Garden defaults) - prefs.setSocksAddress("127.0.0.1"); - prefs.setSocksPort(1080); - prefs.setSocksUdpAddress(""); // Empty by default - prefs.setSocksUsername(""); - prefs.setSocksPassword(""); - prefs.setIpv4(true); - prefs.setIpv6(true); - prefs.setUdpInTcp(false); - prefs.setRemoteDns(true); - - // CRITICAL: Force Global to TRUE so the tunnel catches ALL system traffic - prefs.setGlobal(true); - - // 2. User Editable Values (The only things read from the UI) - prefs.setDnsIpv4(edittext_dns_ipv4.getText().toString()); - prefs.setDnsIpv6(edittext_dns_ipv6.getText().toString()); - prefs.setMaintenanceMode(checkbox_maintenance.isChecked()); - } - - private void addToLog(String message) { - runOnUiThread(() -> { - SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); - String currentTime = sdf.format(new Date()); - String logEntry = "[" + currentTime + "] " + message + "\n"; - if (connectionLog != null) { - connectionLog.append(logEntry); - scrollToBottom(); - } - }); - } - - private void scrollToBottom() { - if (connectionLog.getLayout() != null) { - int scroll = connectionLog.getLayout().getLineTop(connectionLog.getLineCount()) - connectionLog.getHeight(); - if (scroll > 0) connectionLog.scrollTo(0, scroll); - } - } + private void scrollToBottom() { + if (connectionLog.getLayout() != null) { + int scroll = connectionLog.getLayout().getLineTop(connectionLog.getLineCount()) - connectionLog.getHeight(); + if (scroll > 0) connectionLog.scrollTo(0, scroll); + } + } } diff --git a/apk/controller/app/src/main/java/org/iiab/controller/PortalActivity.java b/apk/controller/app/src/main/java/org/iiab/controller/PortalActivity.java index bdd65e8..0971bf5 100644 --- a/apk/controller/app/src/main/java/org/iiab/controller/PortalActivity.java +++ b/apk/controller/app/src/main/java/org/iiab/controller/PortalActivity.java @@ -85,8 +85,12 @@ public class PortalActivity extends AppCompatActivity { resetTimer.run(); }); + Preferences prefs = new Preferences(this); + boolean isVpnActive = prefs.getEnable(); + String targetUrl = isVpnActive ? "http://box/" : "http://localhost:8085/home"; + btnHome.setOnClickListener(v -> { - webView.loadUrl("http://box/"); + webView.loadUrl(targetUrl); resetTimer.run(); }); @@ -156,31 +160,35 @@ public class PortalActivity extends AppCompatActivity { webView.getSettings().setDomStorageEnabled(true); // 2. Port and Mirror logic - Preferences prefs = new Preferences(this); int tempPort = prefs.getSocksPort(); if (tempPort <= 0) tempPort = 1080; // Variable safe to read in lambda final int finalProxyPort = tempPort; - // 3. Proxy block - if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) { - ProxyConfig proxyConfig = new ProxyConfig.Builder() - .addProxyRule("socks5://127.0.0.1:" + finalProxyPort) - .build(); + // 3. Proxy block (ONLY IF VPN IS ACTIVE) + if (isVpnActive) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) { + ProxyConfig proxyConfig = new ProxyConfig.Builder() + .addProxyRule("socks5://127.0.0.1:" + finalProxyPort) + .build(); - Executor executor = ContextCompat.getMainExecutor(this); + Executor executor = ContextCompat.getMainExecutor(this); - ProxyController.getInstance().setProxyOverride(proxyConfig, executor, () -> { - Log.d(TAG, "Proxy configured on port: " + finalProxyPort); - // Load HTML only when proxy is ready - webView.loadUrl("http://box/"); - }); + ProxyController.getInstance().setProxyOverride(proxyConfig, executor, () -> { + Log.d(TAG, "Proxy configured on port: " + finalProxyPort); + // Load HTML only when proxy is ready + webView.loadUrl(targetUrl); + }); + } else { + // Fallback for older devices + Log.w(TAG, "Proxy Override not supported"); + webView.loadUrl(targetUrl); + } } else { - // Fallback for older devices - Log.w(TAG, "Proxy Override not supported"); + // VPN is OFF. Do NOT use proxy. Just load localhost directly. + webView.loadUrl(targetUrl); } - webView.loadUrl("http://box/"); } // 4. Cleanup (Important to not leave the proxy active) diff --git a/apk/controller/app/src/main/res/layout/main.xml b/apk/controller/app/src/main/res/layout/main.xml index c19e5ff..2745a9a 100644 --- a/apk/controller/app/src/main/res/layout/main.xml +++ b/apk/controller/app/src/main/res/layout/main.xml @@ -57,7 +57,7 @@ android:padding="16dp"> - +