diff --git a/apk/controller/app/build.gradle b/apk/controller/app/build.gradle
index fa313b6..0f47488 100644
--- a/apk/controller/app/build.gradle
+++ b/apk/controller/app/build.gradle
@@ -9,8 +9,8 @@ android {
applicationId "org.iiab.controller"
minSdkVersion 24
targetSdkVersion 34
- versionCode 28
- versionName "v0.1.32beta"
+ versionCode 31
+ versionName "v0.2.1beta"
setProperty("archivesBaseName", "$applicationId-$versionName")
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
diff --git a/apk/controller/app/release/output-metadata.json b/apk/controller/app/release/output-metadata.json
index cff0147..602f183 100644
--- a/apk/controller/app/release/output-metadata.json
+++ b/apk/controller/app/release/output-metadata.json
@@ -11,9 +11,9 @@
"type": "SINGLE",
"filters": [],
"attributes": [],
- "versionCode": 28,
- "versionName": "v0.1.32beta",
- "outputFile": "org.iiab.controller-v0.1.32beta-release.apk"
+ "versionCode": 29,
+ "versionName": "v0.1.33beta",
+ "outputFile": "org.iiab.controller-v0.1.33beta-release.apk"
}
],
"elementType": "File",
@@ -22,14 +22,14 @@
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
- "baselineProfiles/1/org.iiab.controller-v0.1.32beta-release.dm"
+ "baselineProfiles/1/org.iiab.controller-v0.1.33beta-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
- "baselineProfiles/0/org.iiab.controller-v0.1.32beta-release.dm"
+ "baselineProfiles/0/org.iiab.controller-v0.1.33beta-release.dm"
]
}
],
diff --git a/apk/controller/app/src/main/AndroidManifest.xml b/apk/controller/app/src/main/AndroidManifest.xml
index 72845c8..6b91b64 100644
--- a/apk/controller/app/src/main/AndroidManifest.xml
+++ b/apk/controller/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
+ android:exported="true"
+ android:configChanges="orientation|screenSize|keyboardHidden|screenLayout">
@@ -93,4 +95,7 @@
+
+
+
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/AppListActivity.java b/apk/controller/app/src/main/java/org/iiab/controller/AppListActivity.java
index 0df17d4..c6f55a3 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/AppListActivity.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/AppListActivity.java
@@ -2,7 +2,9 @@
============================================================================
Name : AppListActivity.java
Author : hev
+ Contributors: IIAB Project
Copyright : Copyright (c) 2025 xyz
+ Copyright (c) 2026 IIAB Project
Description : App List Activity
============================================================================
*/
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/BatteryUtils.java b/apk/controller/app/src/main/java/org/iiab/controller/BatteryUtils.java
index 7deded9..1b900cf 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/BatteryUtils.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/BatteryUtils.java
@@ -1,3 +1,11 @@
+/*
+ * ============================================================================
+ * Name : BatteryUtils.java
+ * Author : IIAB Project
+ * Copyright : Copyright (c) 2026 IIAB Project
+ * Description : Manage battery permissions
+ * ============================================================================
+ */
package org.iiab.controller;
import android.app.Activity;
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/BiometricHelper.java b/apk/controller/app/src/main/java/org/iiab/controller/BiometricHelper.java
index af35270..87be6a3 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/BiometricHelper.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/BiometricHelper.java
@@ -1,3 +1,11 @@
+/*
+ * ============================================================================
+ * Name : BiometricHelper.java
+ * Author : IIAB Project
+ * Copyright : Copyright (c) 2026 IIAB Project
+ * Description : Biometrics helper
+ * ============================================================================
+ */
package org.iiab.controller;
import android.content.Context;
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/DashboardFragment.java b/apk/controller/app/src/main/java/org/iiab/controller/DashboardFragment.java
new file mode 100644
index 0000000..b7a10d9
--- /dev/null
+++ b/apk/controller/app/src/main/java/org/iiab/controller/DashboardFragment.java
@@ -0,0 +1,498 @@
+/*
+ * ============================================================================
+ * Name : DashboardFragment.java
+ * Author : IIAB Project
+ * Copyright : Copyright (c) 2026 IIAB Project
+ * Description : Initial dasboard status activity
+ * ============================================================================
+ */
+package org.iiab.controller;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.graphics.Color;
+import android.os.BatteryManager;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.Html;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.File;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+public class DashboardFragment extends Fragment {
+
+ private TextView txtDeviceName;
+ private TextView txtWifiIp, txtHotspotIp, txtUptime, txtBattery, badgeStatus, txtStorage, txtRam, txtSwap, txtTermuxState;
+ private ProgressBar progStorage, progRam, progSwap;
+ private View ledTermuxState;
+ private LinearLayout modulesContainer;
+
+ private final Handler refreshHandler = new Handler(Looper.getMainLooper());
+ private Runnable refreshRunnable;
+
+ // List of modules to scan (Endpoint, Display Name)
+ private final Object[][] TARGET_MODULES = {
+ {"books", R.string.dash_books},
+ {"kiwix", R.string.dash_kiwix},
+ {"kolibri", R.string.dash_kolibri},
+ {"maps", R.string.dash_maps},
+ {"matomo", R.string.dash_matomo},
+ {"dashboard", R.string.dash_system}
+ };
+
+ public enum SystemState {
+ ONLINE, OFFLINE, DEBIAN_ONLY, INSTALLER, TERMUX_ONLY, NONE
+ }
+
+ private SystemState currentSystemState = SystemState.NONE;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_dashboard, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ // Bindings
+ txtDeviceName = view.findViewById(R.id.dash_text_device_name);
+ txtWifiIp = view.findViewById(R.id.dash_text_wifi_ip);
+ txtHotspotIp = view.findViewById(R.id.dash_text_hotspot_ip);
+ txtUptime = view.findViewById(R.id.dash_text_uptime);
+ txtBattery = view.findViewById(R.id.dash_text_battery);
+ badgeStatus = view.findViewById(R.id.dash_badge_status);
+
+ txtStorage = view.findViewById(R.id.dash_text_storage);
+ txtRam = view.findViewById(R.id.dash_text_ram);
+ txtSwap = view.findViewById(R.id.dash_text_swap);
+ progStorage = view.findViewById(R.id.dash_progress_storage);
+ progRam = view.findViewById(R.id.dash_progress_ram);
+ progSwap = view.findViewById(R.id.dash_progress_swap);
+
+ ledTermuxState = view.findViewById(R.id.led_termux_state);
+ txtTermuxState = view.findViewById(R.id.text_termux_state);
+ modulesContainer = view.findViewById(R.id.modules_container);
+
+ // Generate module views dynamically
+ createModuleViews();
+
+ // Configure refresh timer (every 5 seconds)
+ refreshRunnable = new Runnable() {
+ @Override
+ public void run() {
+ updateSystemStats();
+ checkServerAndModules();
+ refreshHandler.postDelayed(this, 5000);
+ }
+ };
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ refreshHandler.post(refreshRunnable);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ refreshHandler.removeCallbacks(refreshRunnable);
+ }
+
+ private void updateSystemStats() {
+ txtDeviceName.setText(getDeviceName());
+
+ // --- 0. CALCULATE SERVER UPTIME ---
+ long uptimeMillis = android.os.SystemClock.elapsedRealtime();
+ long minutes = (uptimeMillis / (1000 * 60)) % 60;
+ long hours = (uptimeMillis / (1000 * 60 * 60)) % 24;
+ long days = (uptimeMillis / (1000 * 60 * 60 * 24));
+
+ // Format: "Uptime: 2d 14h 05m" (Omit days if 0)
+ String timeStr = (days > 0) ?
+ String.format(Locale.US, "%dd %02dh %02dm", days, hours, minutes) :
+ String.format(Locale.US, "%02dh %02dm", hours, minutes);
+
+ txtUptime.setText(Html.fromHtml(getString(R.string.dash_uptime_format, timeStr), Html.FROM_HTML_MODE_LEGACY));
+
+ txtWifiIp.setText(Html.fromHtml(getString(R.string.dash_wifi_format, getWifiIp()), Html.FROM_HTML_MODE_LEGACY));
+ txtHotspotIp.setText(Html.fromHtml(getString(R.string.dash_hotspot_format, getHotspotIp()), Html.FROM_HTML_MODE_LEGACY));
+
+ int batteryLevel = getBatteryPercentage();
+ if (batteryLevel >= 0) {
+ txtBattery.setText(Html.fromHtml(getString(R.string.dash_battery_format, batteryLevel), Html.FROM_HTML_MODE_LEGACY));
+ } else {
+ txtBattery.setText(Html.fromHtml(getString(R.string.dash_battery_no_value), Html.FROM_HTML_MODE_LEGACY));
+ }
+
+ // --- 1. GET REAL RAM AND SWAP FROM LINUX ---
+ long memTotal = 0, memAvailable = 0, swapTotal = 0, swapFree = 0;
+ try (BufferedReader br = new BufferedReader(new FileReader("/proc/meminfo"))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ if (line.startsWith("MemTotal:")) memTotal = parseMemLine(line);
+ else if (line.startsWith("MemAvailable:")) memAvailable = parseMemLine(line);
+ // If phone is old and doesn't have "MemAvailable", use "MemFree"
+ else if (memAvailable == 0 && line.startsWith("MemFree:")) memAvailable = parseMemLine(line);
+ else if (line.startsWith("SwapTotal:")) swapTotal = parseMemLine(line);
+ else if (line.startsWith("SwapFree:")) swapFree = parseMemLine(line);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ // Convert the values from kB to GB (1 GB = 1048576 kB)
+ double memTotalGb = memTotal / 1048576.0;
+ double memUsedGb = (memTotal - memAvailable) / 1048576.0;
+ int memProgress = memTotal > 0 ? (int) (((memTotal - memAvailable) * 100) / memTotal) : 0;
+
+ double swapTotalGb = swapTotal / 1048576.0;
+ double swapUsedGb = (swapTotal - swapFree) / 1048576.0;
+ int swapProgress = swapTotal > 0 ? (int) (((swapTotal - swapFree) * 100) / swapTotal) : 0;
+
+ // --- UPDATE UI (TEXT AND BARS) ---
+ txtRam.setText(String.format(Locale.US, "%.2f GB / %.2f GB", memUsedGb, memTotalGb));
+ progRam.setProgress(memProgress);
+
+ if (swapTotal > 0) {
+ txtSwap.setText(String.format(Locale.US, "%.2f GB / %.2f GB", swapUsedGb, swapTotalGb));
+ progSwap.setProgress(swapProgress);
+ } else {
+ // If the device does not use Swap
+ txtSwap.setText("-- / --");
+ progSwap.setProgress(0);
+ }
+
+ // 2. Get Internal Storage
+ File path = android.os.Environment.getDataDirectory();
+ long totalSpace = path.getTotalSpace() / (1024 * 1024 * 1024); // To GB
+ long freeSpace = path.getFreeSpace() / (1024 * 1024 * 1024);
+ long usedSpace = totalSpace - freeSpace;
+
+ txtStorage.setText(usedSpace + " GB / " + totalSpace + " GB");
+ progStorage.setProgress(totalSpace > 0 ? (int) ((usedSpace * 100) / totalSpace) : 0);
+ }
+
+ private void createModuleViews() {
+ modulesContainer.removeAllViews();
+
+ // Set 3 lines
+ for (int row = 0; row < 2; row++) {
+ LinearLayout rowLayout = new LinearLayout(requireContext());
+ rowLayout.setOrientation(LinearLayout.HORIZONTAL);
+ rowLayout.setLayoutParams(new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ rowLayout.setBaselineAligned(false);
+ rowLayout.setWeightSum(3f);
+ rowLayout.setPadding(0, 0, 0, 16);
+
+ // Create 3 columns per row
+ for (int col = 0; col < 3; col++) {
+ int index = (row * 3) + col;
+ if (index >= TARGET_MODULES.length) break;
+
+ // Grid
+ LinearLayout cell = new LinearLayout(requireContext());
+ cell.setOrientation(LinearLayout.HORIZONTAL);
+ cell.setBackgroundResource(R.drawable.rounded_button);
+ cell.setBackgroundTintList(android.content.res.ColorStateList.valueOf(
+ androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_module_bg)));
+ cell.setPadding(16, 24, 16, 24);
+ cell.setGravity(android.view.Gravity.CENTER);
+
+ LinearLayout.LayoutParams cellParams = new LinearLayout.LayoutParams(
+ 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
+
+ // Leave small margins between the cards so they don't stick together.
+ int margin = 8;
+ if (col == 0) cellParams.setMargins(0, 0, margin, 0); // Left
+ else if (col == 1) cellParams.setMargins(margin/2, 0, margin/2, 0); // Center
+ else cellParams.setMargins(margin, 0, 0, 0); // Right
+
+ cell.setLayoutParams(cellParams);
+
+ // Small LED
+ View led = new View(requireContext());
+ led.setLayoutParams(new LinearLayout.LayoutParams(20, 20));
+ led.setBackgroundResource(R.drawable.led_off);
+ led.setId(View.generateViewId());
+
+ // Module name
+ TextView name = new TextView(requireContext());
+ LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ textParams.setMargins(12, 0, 0, 0);
+ name.setLayoutParams(textParams);
+ name.setText(getString((Integer) TARGET_MODULES[index][1]));
+ name.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_module_text));
+ name.setTextSize(11f);
+ name.setSingleLine(true);
+
+ cell.addView(led);
+ cell.addView(name);
+ cell.setTag(TARGET_MODULES[index][0]);
+
+ rowLayout.addView(cell);
+ }
+ modulesContainer.addView(rowLayout);
+ }
+ }
+
+ private void checkServerAndModules() {
+ new Thread(() -> {
+ // 1. Ping the network once
+ boolean isMainServerAlive = pingUrl("http://localhost:8085/home");
+
+ // 2. Ask the State Machine for the definitive truth
+ currentSystemState = evaluateSystemState(isMainServerAlive);
+
+ // 3. Update the UI on the main thread
+ requireActivity().runOnUiThread(() -> {
+
+ // Configure the Top Traffic Light (Server Status)
+ if (currentSystemState == SystemState.ONLINE) {
+ badgeStatus.setText(R.string.dash_online);
+ badgeStatus.setBackgroundTintList(android.content.res.ColorStateList.valueOf(
+ androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_status_online)));
+ } else {
+ badgeStatus.setText(R.string.dash_offline);
+ badgeStatus.setBackgroundTintList(android.content.res.ColorStateList.valueOf(
+ androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_text_secondary)));
+ }
+
+ // Configure the Bottom LED and Suggestion Message
+ switch (currentSystemState) {
+ case ONLINE:
+ ledTermuxState.setBackgroundResource(R.drawable.led_on_green);
+ txtTermuxState.setText(getString(R.string.dash_state_online));
+ txtTermuxState.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_text_primary));
+ break;
+ case OFFLINE:
+ ledTermuxState.setBackgroundResource(R.drawable.led_off);
+ txtTermuxState.setText(getString(R.string.dash_state_offline));
+ txtTermuxState.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_text_secondary));
+ break;
+ case DEBIAN_ONLY:
+ ledTermuxState.setBackgroundResource(R.drawable.led_off);
+ txtTermuxState.setText(getString(R.string.dash_state_debian_only));
+ txtTermuxState.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_text_primary));
+ break;
+ case INSTALLER:
+ ledTermuxState.setBackgroundResource(R.drawable.led_off);
+ txtTermuxState.setText(getString(R.string.dash_state_installer));
+ txtTermuxState.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_text_primary));
+ break;
+ case TERMUX_ONLY:
+ ledTermuxState.setBackgroundResource(R.drawable.led_off);
+ txtTermuxState.setText(getString(R.string.dash_state_termux_only));
+ txtTermuxState.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_warning));
+ break;
+ case NONE:
+ ledTermuxState.setBackgroundResource(R.drawable.led_off);
+ txtTermuxState.setText(getString(R.string.dash_state_none));
+ txtTermuxState.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.dash_warning));
+ break;
+ }
+ });
+
+ // 4. Scan individual modules (Only if the system is ONLINE)
+ for (int r = 0; r < modulesContainer.getChildCount(); r++) {
+ LinearLayout row = (LinearLayout) modulesContainer.getChildAt(r);
+
+ for (int c = 0; c < row.getChildCount(); c++) {
+ LinearLayout card = (LinearLayout) row.getChildAt(c);
+ String endpoint = (String) card.getTag();
+ if (endpoint == null) continue;
+
+ View led = card.getChildAt(0);
+
+ // Module ON = (System is ONLINE) AND (URL responds)
+ boolean isModuleAlive = (currentSystemState == SystemState.ONLINE) && pingUrl("http://localhost:8085/" + endpoint);
+
+ requireActivity().runOnUiThread(() -> {
+ led.setBackgroundResource(isModuleAlive ? R.drawable.led_on_green : R.drawable.led_off);
+ });
+ }
+ }
+ }).start();
+ }
+
+ private boolean pingUrl(String urlStr) {
+ try {
+ URL url = new URL(urlStr);
+ HttpURLConnection 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;
+ }
+ }
+
+ // Extracts the numbers (in kB) from the lines of /proc/meminfo
+ private long parseMemLine(String line) {
+ try {
+ String[] parts = line.split("\\s+");
+ return Long.parseLong(parts[1]);
+ } catch (Exception e) {
+ return 0;
+ }
+ }
+ // --- METHODS FOR OBTAINING IPs ---
+ private String getWifiIp() {
+ return getIpByInterface("wlan0");
+ }
+
+ private String getHotspotIp() {
+ String[] hotspotInterfaces = {"ap0", "wlan1", "swlan0"};
+ for (String iface : hotspotInterfaces) {
+ String ip = getIpByInterface(iface);
+ if (!ip.equals("--")) return ip;
+ }
+ return "--";
+ }
+
+ private String getIpByInterface(String interfaceName) {
+ try {
+ List interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
+ for (NetworkInterface intf : interfaces) {
+ if (intf.getName().equalsIgnoreCase(interfaceName)) {
+ List addrs = Collections.list(intf.getInetAddresses());
+ for (InetAddress addr : addrs) {
+ if (!addr.isLoopbackAddress() && addr instanceof Inet4Address) {
+ return addr.getHostAddress();
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return "--";
+ }
+
+ private int getBatteryPercentage() {
+ try {
+ IntentFilter iFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+ Intent batteryStatus = requireContext().registerReceiver(null, iFilter);
+ if (batteryStatus != null) {
+ int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+ int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
+ return (int) ((level / (float) scale) * 100);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return -1;
+ }
+
+ // --- METHODS FOR OBTAINING THE DEVICE NAME ---
+ private String getDeviceName() {
+ String manufacturer = android.os.Build.MANUFACTURER;
+ String model = android.os.Build.MODEL;
+
+ if (model.toLowerCase().startsWith(manufacturer.toLowerCase())) {
+ return capitalize(model);
+ } else {
+ return capitalize(manufacturer) + " " + model;
+ }
+ }
+
+ private String capitalize(String s) {
+ if (s == null || s.length() == 0) return "";
+ char first = s.charAt(0);
+ if (Character.isUpperCase(first)) {
+ return s;
+ } else {
+ return Character.toUpperCase(first) + s.substring(1);
+ }
+ }
+
+ // The 5 possible system states
+ // --- MASTER STATE EVALUATOR ---
+ private SystemState evaluateSystemState(boolean isNginxAlive) {
+ // 1. Does the Nginx server respond? (The network doesn't lie)
+ if (isNginxAlive) {
+ return SystemState.ONLINE;
+ }
+
+ // 2. Does Termux physically exist on the Android device?
+ boolean isTermuxInstalled = false;
+ try {
+ requireContext().getPackageManager().getPackageInfo("com.termux", 0);
+ isTermuxInstalled = true;
+ } catch (PackageManager.NameNotFoundException e) {
+ isTermuxInstalled = false;
+ }
+
+ File stateDir = new File(Environment.getExternalStorageDirectory(), ".iiab_state");
+
+ // Ghost Handling: If Termux is uninstalled, but garbage remains, delete it.
+ if (!isTermuxInstalled) {
+ if (stateDir.exists()) {
+ deleteRecursive(stateDir);
+ }
+ return SystemState.NONE;
+ }
+
+ // 3. Is IIAB fully compiled/restored and ready?
+ File flagIiabReady = new File(stateDir, "flag_iiab_ready");
+ if (flagIiabReady.exists()) {
+ return SystemState.OFFLINE; // The real offline state
+ }
+
+ // 4. Is the base Debian OS installed, but NO IIAB yet? (The Virgin Debian Trap)
+ File flagSystem = new File(stateDir, "flag_system_installed");
+ if (flagSystem.exists()) {
+ return SystemState.DEBIAN_ONLY;
+ }
+
+ // 5. Is only the installer ready?
+ File flagInstaller = new File(stateDir, "flag_installer_present");
+ if (flagInstaller.exists()) {
+ return SystemState.INSTALLER;
+ }
+
+ // 6. Only the raw base app is present.
+ return SystemState.TERMUX_ONLY;
+ }
+
+ // Helper method to recursively delete the .iiab_state folder if Termux was uninstalled
+ private void deleteRecursive(File fileOrDirectory) {
+ if (fileOrDirectory.isDirectory()) {
+ File[] children = fileOrDirectory.listFiles();
+ if (children != null) {
+ for (File child : children) {
+ deleteRecursive(child);
+ }
+ }
+ }
+ fileOrDirectory.delete();
+ }
+}
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/DashboardManager.java b/apk/controller/app/src/main/java/org/iiab/controller/DashboardManager.java
index a34828f..083eb9a 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/DashboardManager.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/DashboardManager.java
@@ -1,3 +1,11 @@
+/*
+ * ============================================================================
+ * Name : DashboardManager.java
+ * Author : IIAB Project
+ * Copyright : Copyright (c) 2026 IIAB Project
+ * Description : Initial dasboard status helper
+ * ============================================================================
+ */
package org.iiab.controller;
import android.app.Activity;
@@ -10,33 +18,37 @@ 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
+ // Memory variables to avoid freezing the screen
+ private boolean lastTunnelState = false;
+ private boolean lastDegradedState = false;
+ private boolean isFirstRun = true;
+
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);
@@ -44,9 +56,9 @@ public class DashboardManager {
}
private void setupListeners(DashboardActionCallback callback) {
- // Single tap opens Settings directly (No wrench icons needed!)
+ // Single tap opens Settings directly
dashWifi.setOnClickListener(v -> activity.startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS)));
-
+
dashHotspot.setOnClickListener(v -> {
try {
Intent intent = new Intent(Intent.ACTION_MAIN);
@@ -71,6 +83,15 @@ public class DashboardManager {
// The Magic Morphing Animation!
public void setTunnelState(boolean isTunnelActive, boolean isDegraded) {
+ // ANTI-FREEZE SHIELD!
+ // If the state is exactly the same as 3 seconds ago, abort to avoid blocking the UI
+ if (!isFirstRun && lastTunnelState == isTunnelActive && lastDegradedState == isDegraded) {
+ return;
+ }
+ isFirstRun = false;
+ lastTunnelState = isTunnelActive;
+ lastDegradedState = isDegraded;
+
// Tells Android to smoothly animate any layout changes we make next
TransitionManager.beginDelayedTransition((ViewGroup) dashboardContainer.getParent(), new AutoTransition().setDuration(300));
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/DeployFragment.java b/apk/controller/app/src/main/java/org/iiab/controller/DeployFragment.java
new file mode 100644
index 0000000..55dda15
--- /dev/null
+++ b/apk/controller/app/src/main/java/org/iiab/controller/DeployFragment.java
@@ -0,0 +1,26 @@
+/*
+ * ============================================================================
+ * Name : DeployFragment.java
+ * Author : IIAB Project
+ * Copyright : Copyright (c) 2026 IIAB Project
+ * Description : Installation / deployment view
+ * ============================================================================
+ */
+package org.iiab.controller;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+public class DeployFragment extends Fragment {
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_deploy, container, false);
+ }
+}
\ No newline at end of file
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/IIABWatchdog.java b/apk/controller/app/src/main/java/org/iiab/controller/IIABWatchdog.java
index c0adcb4..dfc1e12 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/IIABWatchdog.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/IIABWatchdog.java
@@ -1,3 +1,11 @@
+/*
+ * ============================================================================
+ * Name : IIABWatchdog.java
+ * Author : IIAB Project
+ * Copyright : Copyright (c) 2026 IIAB Project
+ * Description : Watchdog activity
+ * ============================================================================
+ */
package org.iiab.controller;
import android.app.PendingIntent;
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/LogManager.java b/apk/controller/app/src/main/java/org/iiab/controller/LogManager.java
index 4c48512..2dd235a 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/LogManager.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/LogManager.java
@@ -1,3 +1,11 @@
+/*
+ * ============================================================================
+ * Name : LogManager.java
+ * Author : IIAB Project
+ * Copyright : Copyright (c) 2026 IIAB Project
+ * Description : Watchdog log manager
+ * ============================================================================
+ */
package org.iiab.controller;
import android.content.Context;
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 2f7d1a2..533a366 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
@@ -2,6 +2,9 @@
============================================================================
Name : MainActivity.java
Author : hev
+ Contributors: IIAB Project
+ Copyright : Copyright (c) 2025 hev
+ Copyright (c) 2026 IIAB Project
Copyright : Copyright (c) 2023 xyz
Description : Main Activity
============================================================================
@@ -27,6 +30,7 @@ import android.content.ComponentName;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.graphics.Color;
@@ -55,6 +59,9 @@ import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;
import com.google.android.material.snackbar.Snackbar;
+import com.google.android.material.tabs.TabLayout;
+import com.google.android.material.tabs.TabLayoutMediator;
+import androidx.viewpager2.widget.ViewPager2;
import java.io.BufferedReader;
import java.io.File;
@@ -76,50 +83,25 @@ import java.net.InetSocketAddress;
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 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;
+ public Preferences prefs;
private ImageButton themeToggle;
private ImageButton btnSettings;
- private TextView versionFooter;
- private ProgressBar logProgress;
+ private android.widget.ImageView headerIcon;
- // Cassette Deck UI
- private LinearLayout deckContainer;
- private ProgressButton btnServerControl;
- private ObjectAnimator fusionAnimator;
- private android.animation.ObjectAnimator exploreAnimator;
- private boolean isServerAlive = false;
- private boolean isNegotiating = false;
- private boolean isProxyDegraded = false;
- private Boolean targetServerState = null;
- private String serverTransitionText = "";
+ // Tabs UI
+ private TabLayout tabLayout;
+ private ViewPager2 viewPager;
+ private TextView versionFooter;
+ public boolean isServerAlive = false;
+ public boolean isNegotiating = false;
+ public boolean isProxyDegraded = false;
+ public Boolean targetServerState = null;
+ public String serverTransitionText = "";
+ public UsageFragment usageFragment;
+
+ public void setUsageFragment(UsageFragment fragment) {
+ this.usageFragment = fragment;
+ }
private final Handler timeoutHandler = new Handler(android.os.Looper.getMainLooper());
private Runnable timeoutRunnable;
private boolean isWifiActive = false;
@@ -127,13 +109,11 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private String currentTargetUrl = null;
private long pulseStartTime = 0;
- private DashboardManager dashboardManager;
-
private ActivityResultLauncher vpnPermissionLauncher;
private ActivityResultLauncher requestPermissionsLauncher;
private ActivityResultLauncher batteryOptLauncher;
- private boolean isReadingLogs = false;
+ public boolean isReadingLogs = false;
private final Handler sizeUpdateHandler = new Handler();
private Runnable sizeUpdateRunnable;
@@ -150,10 +130,9 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
if (IIABWatchdog.ACTION_LOG_MESSAGE.equals(action)) {
String message = intent.getStringExtra(IIABWatchdog.EXTRA_MESSAGE);
addToLog(message);
- updateLogSizeUI();
+ if (usageFragment != null) usageFragment.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;
@@ -169,13 +148,13 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
// Wait exactly until the wave hits 1.0f alpha, then lock it!
new Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
- finalizeEntryPulse();
+ if (usageFragment != null) usageFragment.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();
+ if (usageFragment != null) usageFragment.finalizeExitPulse();
}, 1500);
}
}
@@ -186,8 +165,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
super.onCreate(savedInstanceState);
// Intercept launch and redirect to Setup Wizard if first time
- SharedPreferences internalPrefs = getSharedPreferences("IIAB_Internal", Context.MODE_PRIVATE);
- if (!internalPrefs.getBoolean("setup_complete", false)) {
+ SharedPreferences internalPrefs = getSharedPreferences(getString(R.string.pref_file_internal), Context.MODE_PRIVATE);
+ if (!internalPrefs.getBoolean(getString(R.string.pref_key_setup_complete), false)) {
startActivity(new Intent(this, SetupActivity.class));
finish();
return;
@@ -196,6 +175,24 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
prefs = new Preferences(this);
setContentView(R.layout.main);
+ // --- START TABS & VIEWPAGER ---
+ tabLayout = findViewById(R.id.tab_layout);
+ viewPager = findViewById(R.id.view_pager);
+
+ MainPagerAdapter pagerAdapter = new MainPagerAdapter(this);
+ viewPager.setAdapter(pagerAdapter);
+
+ new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
+ switch (position) {
+ case 0: tab.setText(R.string.tab_status); break;
+ case 1: tab.setText(R.string.tab_usage); break;
+ case 2: tab.setText(R.string.tab_deploy); break;
+ }
+ }).attach();
+ versionFooter = findViewById(R.id.version_text);
+ setVersionFooter();
+ viewPager.setCurrentItem(1, false);
+
// 1. Initialize Result Launchers
vpnPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
@@ -229,118 +226,14 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
}
);
- // 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);
btnSettings = findViewById(R.id.btn_settings);
- 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);
-
- deckContainer = findViewById(R.id.deck_container);
- btnServerControl = findViewById(R.id.btn_server_control);
+ headerIcon = findViewById(R.id.header_icon);
ImageButton btnShareQr = findViewById(R.id.btn_share_qr);
- dashboardManager = new DashboardManager(this, findViewById(android.R.id.content), () -> {
- handleControlClick();
- });
-
// Listeners
- watchdogControl.setOnClickListener(v -> {
- boolean willBeEnabled = !prefs.getWatchdogEnable();
- setWatchdogState(willBeEnabled);
- });
- btnClearLog.setOnClickListener(this);
- btnCopyLog.setOnClickListener(this);
themeToggle.setOnClickListener(v -> toggleTheme());
btnSettings.setOnClickListener(v -> startActivity(new Intent(MainActivity.this, SetupActivity.class)));
- 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);
-
- btnServerControl.setOnClickListener(v -> {
- // Ignore clicks if we are already waiting for a state change
- if (targetServerState != null) return;
-
- // Freeze the transition text and define the TARGET state
- serverTransitionText = !isServerAlive ? getString(R.string.server_booting) : getString(R.string.server_shutting_down);
- targetServerState = !isServerAlive;
-
- // Lock the UI and start infinite animation
- updateUIColorsAndVisibility();
- btnServerControl.startProgress();
-
- // Set a hard timeout (45 seconds) as a safety net
- timeoutRunnable = () -> {
- if (targetServerState != null) {
- targetServerState = null; // Abort transition
- btnServerControl.stopProgress();
- updateUIColorsAndVisibility();
- addToLog(getString(R.string.server_timeout_warning));
- }
- };
- timeoutHandler.postDelayed(timeoutRunnable, getResources().getInteger(R.integer.server_cool_off_duration_ms));
-
- // Execute the corresponding script command
- if (!isServerAlive) {
- startTermuxEnvironmentVisible("--start");
- } else {
- startTermuxEnvironmentVisible("--stop");
-
- // Turn off Watchdog gracefully when stopping the server manually
- if (prefs.getWatchdogEnable()) {
- setWatchdogState(false);
- }
- }
- });
-
- // Logic to open the WebView (PortalActivity)
- button_browse_content.setOnClickListener(v -> {
- if (!isServerAlive) {
- Snackbar.make(v, R.string.qr_error_no_server, Snackbar.LENGTH_LONG).show();
- return;
- }
- if (currentTargetUrl != null) {
- Intent intent = new Intent(MainActivity.this, PortalActivity.class);
- intent.putExtra("TARGET_URL", currentTargetUrl);
- startActivity(intent);
- }
- });
// --- QR Share Button Logic ---
btnShareQr.setOnClickListener(v -> {
@@ -359,18 +252,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
startActivity(new Intent(MainActivity.this, QrActivity.class));
});
- 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;
- });
-
applySavedTheme();
- setVersionFooter();
updateUI();
addToLog(getString(R.string.app_started));
@@ -378,7 +260,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
sizeUpdateRunnable = new Runnable() {
@Override
public void run() {
- updateLogSizeUI();
+ if (usageFragment != null && usageFragment.isAdded()) usageFragment.updateLogSizeUI();
sizeUpdateHandler.postDelayed(this, 10000);
}
};
@@ -447,7 +329,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
private void runNegotiationSequence() {
isNegotiating = true;
runOnUiThread(() -> {
- startExplorePulse(); // The orange button starts to beat.
+ if (usageFragment != null) usageFragment.startExplorePulse(); // The orange button starts to beat.
updateUIColorsAndVisibility(); // We forced an immediate visual update
});
@@ -504,66 +386,21 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
}
}
- private void handleLogToggle() {
- boolean isOpening = connectionLog.getVisibility() == View.GONE;
- if (isOpening) {
- 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();
- } else {
- stopLogSizeUpdates();
- }
- toggleVisibility(connectionLog, logLabel, getString(R.string.connection_log_label));
- logActions.setVisibility(connectionLog.getVisibility());
- if (logSizeText != null) logSizeText.setVisibility(connectionLog.getVisibility());
- }
-
- private void startLogSizeUpdates() {
+ public void startLogSizeUpdates() {
sizeUpdateHandler.removeCallbacks(sizeUpdateRunnable);
sizeUpdateHandler.post(sizeUpdateRunnable);
}
- private void stopLogSizeUpdates() {
+ public void stopLogSizeUpdates() {
sizeUpdateHandler.removeCallbacks(sizeUpdateRunnable);
}
- private void updateLogSizeUI() {
- if (logSizeText == null) return;
- // The LogManager class does the calculation
- String sizeStr = LogManager.getFormattedSize(this);
- logSizeText.setText(getString(R.string.log_size_format, sizeStr));
- }
-
private void connectVpn() {
Intent intent = new Intent(this, TProxyService.class);
startService(intent.setAction(TProxyService.ACTION_CONNECT));
addToLog(getString(R.string.vpn_permission_granted));
}
- private void setVersionFooter() {
- try {
- PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
- String version = pInfo.versionName;
- versionFooter.setText(version);
- } catch (PackageManager.NameNotFoundException e) {
- versionFooter.setText(R.string.default_version);
- }
- }
-
@Override
protected void onPause() {
super.onPause();
@@ -574,6 +411,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
@Override
protected void onResume() {
super.onResume();
+ // Check permissions status
+ updateHeaderIconsOpacity();
// Check battery status whenever returning to the app
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
@@ -590,7 +429,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
startService(vpnIntent.setAction(TProxyService.ACTION_CONNECT));
setIntent(null);
}
- if (connectionLog != null && connectionLog.getVisibility() == View.VISIBLE) {
+ if (usageFragment != null && usageFragment.isLogVisible()) {
startLogSizeUpdates();
}
serverCheckHandler.removeCallbacks(serverCheckRunnable);
@@ -626,11 +465,6 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
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(String.format(getString(isGone ? R.string.label_separator_down : R.string.label_separator_up), text));
- }
@Override
protected void onStart() {
@@ -652,55 +486,10 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
@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();
- }
- }
+ // Delegated
}
- 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() {
- LogManager.clearLogs(this, new LogManager.LogClearCallback() {
- @Override
- public void onSuccess() {
- connectionLog.setText("");
- addToLog(getString(R.string.log_reset_user));
- if (logWarning != null) logWarning.setVisibility(View.GONE);
- updateLogSizeUI();
- Toast.makeText(MainActivity.this, R.string.log_cleared_toast, 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() {
+ public void handleWatchdogClick() {
setWatchdogState(!prefs.getWatchdogEnable());
}
@@ -709,9 +498,10 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
Intent intent = new Intent(this, WatchdogService.class);
if (enable) {
+ forceTermuxToForeground();
intent.setAction(WatchdogService.ACTION_START);
addToLog(getString(R.string.watchdog_started));
- if (isServerAlive) startFusionPulse();
+ if (isServerAlive && usageFragment != null) usageFragment.startFusionPulse();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent);
@@ -720,7 +510,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
}
} else {
addToLog(getString(R.string.watchdog_stopped));
- startExitPulse();
+ if (usageFragment != null) usageFragment.startExitPulse();
stopService(intent);
}
@@ -728,7 +518,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
updateUIColorsAndVisibility();
}
- private void handleControlClick() {
+ public void handleControlClick() {
if (!isServerAlive) {
Snackbar.make(findViewById(android.R.id.content), R.string.qr_error_no_server, Snackbar.LENGTH_LONG).show();
return;
@@ -751,19 +541,47 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
}
}
- // --- Secure Advanced Settings Menu ---
- private void handleConfigToggle() {
- if (configLayout.getVisibility() == View.GONE) {
- if (BiometricHelper.isDeviceSecure(this)) {
- BiometricHelper.prompt(this,
- getString(R.string.auth_required_title),
- getString(R.string.auth_required_subtitle),
- () -> toggleVisibility(configLayout, configLabel, getString(R.string.advanced_settings_label)));
- } else {
- BiometricHelper.showEnrollmentDialog(this);
+ public void handleBrowseContentClick(View v) {
+ if (!isServerAlive) {
+ Snackbar.make(v, R.string.qr_error_no_server, Snackbar.LENGTH_LONG).show();
+ return;
+ }
+ if (currentTargetUrl != null) {
+ Intent intent = new Intent(this, PortalActivity.class);
+ intent.putExtra("TARGET_URL", currentTargetUrl);
+ startActivity(intent);
+ }
+ }
+ public void handleServerLaunchClick(View v) {
+ // Set a hard timeout as a safety net
+ timeoutRunnable = () -> {
+ if (targetServerState != null) {
+ targetServerState = null; // Abort transition
+ if (usageFragment != null) runOnUiThread(() -> usageFragment.stopBtnProgress());
+ updateUIColorsAndVisibility();
+ addToLog(getString(R.string.server_timeout_warning));
}
+ };
+ timeoutHandler.postDelayed(timeoutRunnable, getResources().getInteger(R.integer.server_cool_off_duration_ms));
+
+ // Execute the corresponding script command
+ if (!isServerAlive) {
+ startTermuxEnvironmentVisible("--start");
+
+ // Fallback for Oppo/Xiaomi
+ new Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
+ if (targetServerState != null && !isServerAlive) {
+ Snackbar.make(v, R.string.termux_stuck_warning, Snackbar.LENGTH_LONG).show();
+ }
+ }, getResources().getInteger(R.integer.server_snackbar_delay_ms));
+
} else {
- toggleVisibility(configLayout, configLabel, getString(R.string.advanced_settings_label));
+ startTermuxEnvironmentVisible("--stop");
+
+ // Turn off Watchdog gracefully when stopping the server manually
+ if (prefs.getWatchdogEnable()) {
+ setWatchdogState(false);
+ }
}
}
@@ -781,45 +599,9 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
}
}
- private void updateUI() {
- boolean vpnActive = prefs.getEnable();
- boolean watchdogActive = prefs.getWatchdogEnable();
-
- if (dashboardManager != null) dashboardManager.setTunnelState(vpnActive, isProxyDegraded);
-
- 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);
- } else {
- watchdogControl.setText(R.string.watchdog_enable);
- }
- 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);
+ public void updateUI() {
+ if (usageFragment != null) {
+ usageFragment.updateUI();
}
}
@@ -845,7 +627,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
if (targetServerState != null && isServerAlive == targetServerState) {
targetServerState = null; // Transition complete!
timeoutHandler.removeCallbacks(timeoutRunnable); // Cancel safety net
- runOnUiThread(() -> btnServerControl.stopProgress()); // Unlock button
+ if (usageFragment != null) runOnUiThread(() -> usageFragment.stopBtnProgress());
}
if (vpnOn && boxAlive) {
@@ -860,163 +642,10 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
}).start();
}
- private void updateUIColorsAndVisibility() {
- boolean isVpnActive = prefs.getEnable();
- boolean isWatchdogOn = prefs.getWatchdogEnable();
-
- // Draw island (Tunnel LED colors)
- if (dashboardManager != null) {
- dashboardManager.setTunnelState(isVpnActive, isProxyDegraded);
+ public void updateUIColorsAndVisibility() {
+ if (usageFragment != null) {
+ usageFragment.updateUIColorsAndVisibility();
}
-
- // Draw main VPN button (ESPW)
- if (!isServerAlive) {
- // Lock and dim the VPN button if there is no server to connect to
- if (isVpnActive) {
- button_control.setText(R.string.control_disable);
- button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on_dim));
- } else {
- button_control.setText(R.string.control_enable);
- button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_off_dim));
- }
- } else {
- // Unlock if server is alive
- button_control.setEnabled(true);
- if (isVpnActive) {
- button_control.setText(R.string.control_disable);
- button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_on));
- } else {
- button_control.setText(R.string.control_enable);
- button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_off));
- }
- }
-
- // Draw Explore Content button
- // Ensure it is ALWAYS visible, never GONE
- button_browse_content.setVisibility(View.VISIBLE);
-
- if (!isServerAlive) {
- // State 1: Stopped (Greyed out)
- stopExplorePulse();
- button_browse_content.setEnabled(true);
- 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"));
- } else if (isNegotiating) {
- // State 3: Negotiating
- button_browse_content.setEnabled(true);
- button_browse_content.setTextColor(Color.WHITE);
- } else {
- // State: Alive
- stopExplorePulse();
- button_browse_content.setEnabled(true);
- button_browse_content.setTextColor(Color.WHITE);
- button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_explore_ready));
-
- if (isVpnActive && !isProxyDegraded) {
- button_browse_content.setAlpha(1.0f); // 100% Perfect state
- startExplorePulse();
- } else {
- button_browse_content.setAlpha(0.6f); // Watered down fallback state
- }
- }
-
- // FUSION LOGIC (Watchdog & Server Control)
- if (targetServerState != null) {
- // STATE: COOL-OFF (Locked)
- btnServerControl.setAlpha(0.6f);
- btnServerControl.setText(serverTransitionText);
- btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_explore_disabled));
- } else {
- // STATE: NORMAL (Unlocked)
- btnServerControl.setAlpha(1.0f);
- if (isServerAlive) {
- btnServerControl.setText(R.string.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(R.string.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 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) {
@@ -1075,43 +704,100 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
this.isWifiActive = isWifiOn;
this.isHotspotActive = isHotspotOn;
- // Let the Dashboard handle the LEDs!
- if (dashboardManager != null) dashboardManager.updateConnectivityLeds(isWifiOn, isHotspotOn);
- }
-
- 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);
-
- prefs.setDnsIpv4(edittext_dns_ipv4.getText().toString());
- prefs.setDnsIpv6(edittext_dns_ipv6.getText().toString());
- prefs.setMaintenanceMode(checkbox_maintenance.isChecked());
+ if (usageFragment != null) {
+ runOnUiThread(() -> usageFragment.updateConnectivityLeds(this.isWifiActive, this.isHotspotActive));
+ }
}
- 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();
+ public void savePrefs() {
+ if (usageFragment != null) {
+ usageFragment.savePrefsFromUI();
+ }
+ }
+
+ public void addToLog(String message) {
+ if (usageFragment != null) {
+ usageFragment.addToLog(message);
+ }
+ }
+
+ private void setVersionFooter() {
+ try {
+ PackageInfo pInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
+ String version = pInfo.versionName;
+
+ String footerText = getString(R.string.version_footer_format, version);
+
+ versionFooter.setText(footerText);
+ } catch (PackageManager.NameNotFoundException e) {
+ versionFooter.setText(getString(R.string.version_footer_fallback));
+ }
+ }
+ private void forceTermuxToForeground() {
+ try {
+ Intent intent = getPackageManager().getLaunchIntentForPackage("com.termux");
+ if (intent != null) {
+ // Bring existing activity to the foreground
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ startActivity(intent);
+ addToLog(getString(R.string.force_termux_foreground));
}
- });
+ } catch (Exception e) {
+ addToLog(getString(R.string.termux_invocation_error, e.getMessage()));
+ }
}
- private void scrollToBottom() {
- if (connectionLog.getLayout() != null) {
- int scroll = connectionLog.getLayout().getLineTop(connectionLog.getLineCount()) - connectionLog.getHeight();
- if (scroll > 0) connectionLog.scrollTo(0, scroll);
+ // --- PERMISSION CHECKERS FOR UI OPACITY ---
+
+ private void updateHeaderIconsOpacity() {
+ boolean hasAllControllerPerms = hasNotifPermission() && hasTermuxPermission() && hasBatteryPermission() && hasStoragePermission();
+ boolean hasTermuxStorage = hasTermuxStoragePermission();
+
+ // If any vital permission is missing, dim the icons to 40% opacity (0.4f)
+ boolean allPerfect = hasAllControllerPerms && hasTermuxStorage;
+ float targetAlpha = allPerfect ? 1.0f : 0.4f;
+
+ if (btnSettings != null) btnSettings.setAlpha(targetAlpha);
+ if (headerIcon != null) headerIcon.setAlpha(targetAlpha);
+ }
+
+ private boolean hasNotifPermission() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ return ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED;
+ }
+ return true;
+ }
+
+ private boolean hasTermuxPermission() {
+ return ContextCompat.checkSelfPermission(this, TERMUX_PERMISSION) == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private boolean hasBatteryPermission() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ return pm != null && pm.isIgnoringBatteryOptimizations(getPackageName());
+ }
+ return true;
+ }
+
+ private boolean hasTermuxStoragePermission() {
+ try {
+ int result = getPackageManager().checkPermission(Manifest.permission.READ_EXTERNAL_STORAGE, "com.termux");
+ if (result == PackageManager.PERMISSION_GRANTED) return true;
+
+ // Fallback: If Android denies the package query, check if the directory actually exists
+ File stateDir = new File(android.os.Environment.getExternalStorageDirectory(), ".iiab_state");
+ return stateDir.exists();
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ private boolean hasStoragePermission() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ return Environment.isExternalStorageManager();
+ } else {
+ return ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
}
}
}
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/MainPagerAdapter.java b/apk/controller/app/src/main/java/org/iiab/controller/MainPagerAdapter.java
new file mode 100644
index 0000000..d12b587
--- /dev/null
+++ b/apk/controller/app/src/main/java/org/iiab/controller/MainPagerAdapter.java
@@ -0,0 +1,41 @@
+/*
+ * ============================================================================
+ * Name : MainPagerAdapter.java
+ * Author : IIAB Project
+ * Copyright : Copyright (c) 2026 IIAB Project
+ * Description : Main Pager Adapter
+ * ============================================================================
+ */
+package org.iiab.controller;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+
+public class MainPagerAdapter extends FragmentStateAdapter {
+
+ public MainPagerAdapter(@NonNull FragmentActivity fragmentActivity) {
+ super(fragmentActivity);
+ }
+
+ @NonNull
+ @Override
+ public Fragment createFragment(int position) {
+ switch (position) {
+ case 0:
+ return new DashboardFragment();
+ case 1:
+ return new UsageFragment();
+ case 2:
+ return new DeployFragment();
+ default:
+ return new DashboardFragment();
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return 3;
+ }
+}
\ No newline at end of file
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 98738ad..69ef097 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
@@ -1,3 +1,11 @@
+/*
+ * ============================================================================
+ * Name : PortalActivity.java
+ * Author : IIAB Project
+ * Copyright : Copyright (c) 2026 IIAB Project
+ * Description : Webview portal activity
+ * ============================================================================
+ */
package org.iiab.controller;
import android.os.Bundle;
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/Preferences.java b/apk/controller/app/src/main/java/org/iiab/controller/Preferences.java
index 4c26273..7bdb287 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/Preferences.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/Preferences.java
@@ -2,7 +2,9 @@
============================================================================
Name : Preferences.java
Author : hev
+ Contributors: IIAB Project
Copyright : Copyright (c) 2023 xyz
+ Copyright (c) 2026 IIAB Project
Description : Preferences
============================================================================
*/
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/ProgressButton.java b/apk/controller/app/src/main/java/org/iiab/controller/ProgressButton.java
index 3a1bda7..b30b72f 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/ProgressButton.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/ProgressButton.java
@@ -1,3 +1,11 @@
+/*
+ * ============================================================================
+ * Name : ProgressButton.java
+ * Author : IIAB Project
+ * Copyright : Copyright (c) 2026 IIAB Project
+ * Description : Button animation helper
+ * ============================================================================
+ */
package org.iiab.controller;
import android.animation.ValueAnimator;
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/QrActivity.java b/apk/controller/app/src/main/java/org/iiab/controller/QrActivity.java
index f203afd..2674028 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/QrActivity.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/QrActivity.java
@@ -1,3 +1,11 @@
+/*
+ * ============================================================================
+ * Name : QrActivity.java
+ * Author : IIAB Project
+ * Copyright : Copyright (c) 2026 IIAB Project
+ * Description : QR share content helper
+ * ============================================================================
+ */
package org.iiab.controller;
import android.graphics.Bitmap;
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/ServiceReceiver.java b/apk/controller/app/src/main/java/org/iiab/controller/ServiceReceiver.java
index a81e052..8cafd15 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/ServiceReceiver.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/ServiceReceiver.java
@@ -2,7 +2,9 @@
============================================================================
Name : ServiceReceiver.java
Author : hev
+ Contributors: IIAB Project
Copyright : Copyright (c) 2023 xyz
+ Copyright (c) 2026 IIAB Project
Description : ServiceReceiver
============================================================================
*/
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/SetupActivity.java b/apk/controller/app/src/main/java/org/iiab/controller/SetupActivity.java
index e43b722..eba99a4 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/SetupActivity.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/SetupActivity.java
@@ -1,3 +1,11 @@
+/*
+ * ============================================================================
+ * Name : SetupActivity.java
+ * Author : IIAB Project
+ * Copyright : Copyright (c) 2026 IIAB Project
+ * Description : Setup permission table helper
+ * ============================================================================
+ */
package org.iiab.controller;
import android.Manifest;
@@ -9,6 +17,7 @@ import android.net.Uri;
import android.net.VpnService;
import android.os.Build;
import android.os.Bundle;
+import android.os.Environment;
import android.os.PowerManager;
import android.provider.Settings;
import android.view.View;
@@ -28,13 +37,15 @@ public class SetupActivity extends AppCompatActivity {
private static final String TERMUX_PERMISSION = "com.termux.permission.RUN_COMMAND";
- private SwitchCompat switchNotif, switchTermux, switchVpn, switchBattery;
+ private SwitchCompat switchNotif, switchTermux, switchStorage, switchVpn, switchBattery;
private Button btnContinue;
private Button btnManageAll;
private Button btnTermuxOverlay;
+ private Button btnTermuxStorage;
private Button btnManageTermux;
private ActivityResultLauncher requestPermissionLauncher;
+ private ActivityResultLauncher storageLauncher;
private ActivityResultLauncher vpnLauncher;
private ActivityResultLauncher batteryLauncher;
@@ -48,11 +59,13 @@ public class SetupActivity extends AppCompatActivity {
switchNotif = findViewById(R.id.switch_perm_notifications);
switchTermux = findViewById(R.id.switch_perm_termux);
+ switchStorage = findViewById(R.id.switch_perm_storage);
switchVpn = findViewById(R.id.switch_perm_vpn);
switchBattery = findViewById(R.id.switch_perm_battery);
btnContinue = findViewById(R.id.btn_setup_continue);
btnManageAll = findViewById(R.id.btn_manage_all);
btnTermuxOverlay = findViewById(R.id.btn_termux_overlay);
+ btnTermuxStorage = findViewById(R.id.btn_termux_storage);
btnManageTermux = findViewById(R.id.btn_manage_termux);
// Hide Notification switch if Android < 13
@@ -65,9 +78,11 @@ public class SetupActivity extends AppCompatActivity {
checkAllPermissions();
btnContinue.setOnClickListener(v -> {
+ // Tell bash that permissions are handled
+ writeTermuxPermissionFlags();
// Save flag so we don't show this screen again
- SharedPreferences prefs = getSharedPreferences("IIAB_Internal", Context.MODE_PRIVATE);
- prefs.edit().putBoolean("setup_complete", true).apply();
+ SharedPreferences prefs = getSharedPreferences(getString(R.string.pref_file_internal), Context.MODE_PRIVATE);
+ prefs.edit().putBoolean(getString(R.string.pref_key_setup_complete), true).apply();
finish();
});
@@ -79,6 +94,11 @@ public class SetupActivity extends AppCompatActivity {
isGranted -> checkAllPermissions()
);
+ storageLauncher = registerForActivityResult(
+ new ActivityResultContracts.StartActivityForResult(),
+ result -> checkAllPermissions()
+ );
+
vpnLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> checkAllPermissions()
@@ -115,6 +135,30 @@ public class SetupActivity extends AppCompatActivity {
switchTermux.setChecked(false);
});
+ switchStorage.setOnClickListener(v -> {
+ if (hasStoragePermission()) {
+ handleRevokeAttempt(v);
+ return;
+ }
+ if (switchStorage.isChecked()) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ try {
+ Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
+ intent.addCategory("android.intent.category.DEFAULT");
+ intent.setData(Uri.parse(String.format("package:%s", getApplicationContext().getPackageName())));
+ storageLauncher.launch(intent);
+ } catch (Exception e) {
+ Intent intent = new Intent();
+ intent.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
+ storageLauncher.launch(intent);
+ }
+ } else {
+ requestPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
+ }
+ }
+ switchStorage.setChecked(false); // Force visual state back until system confirms
+ });
+
switchVpn.setOnClickListener(v -> {
if (hasVpnPermission()) {
handleRevokeAttempt(v);
@@ -158,6 +202,18 @@ public class SetupActivity extends AppCompatActivity {
}
});
+ // Direct access to Termux settings to grant Files/Storage permission
+ btnTermuxStorage.setOnClickListener(v -> {
+ try {
+ Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.setData(Uri.parse("package:com.termux"));
+ startActivity(intent);
+ // Toast.makeText(this, "Please go to Permissions and allow Storage/Files", Toast.LENGTH_LONG).show();
+ } catch (Exception e) {
+ Snackbar.make(v, R.string.termux_not_installed_error, Snackbar.LENGTH_LONG).show();
+ }
+ });
+
// Direct access to Controller settings (Reuses the method from Phase 1)
btnManageTermux.setOnClickListener(v -> {
try {
@@ -202,15 +258,17 @@ public class SetupActivity extends AppCompatActivity {
private void checkAllPermissions() {
boolean notif = hasNotifPermission();
boolean termux = hasTermuxPermission();
+ boolean storage = hasStoragePermission();
boolean vpn = hasVpnPermission();
boolean battery = hasBatteryPermission();
switchNotif.setChecked(notif);
switchTermux.setChecked(termux);
+ switchStorage.setChecked(storage);
switchVpn.setChecked(vpn);
switchBattery.setChecked(battery);
- boolean allGranted = termux && vpn && battery;
+ boolean allGranted = termux && storage && vpn && battery;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
allGranted = allGranted && notif;
}
@@ -242,4 +300,25 @@ public class SetupActivity extends AppCompatActivity {
}
return true;
}
+
+ private boolean hasStoragePermission() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ return Environment.isExternalStorageManager();
+ } else {
+ return ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
+ }
+ }
+
+ private void writeTermuxPermissionFlags() {
+ java.io.File stateDir = new java.io.File(android.os.Environment.getExternalStorageDirectory(), ".iiab_state");
+ if (!stateDir.exists()) {
+ stateDir.mkdirs();
+ }
+ try {
+ new java.io.File(stateDir, "flag_perm_battery").createNewFile();
+ new java.io.File(stateDir, "flag_perm_overlay").createNewFile();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
}
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/TProxyService.java b/apk/controller/app/src/main/java/org/iiab/controller/TProxyService.java
index 8e08804..cd28058 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/TProxyService.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/TProxyService.java
@@ -2,7 +2,9 @@
============================================================================
Name : TProxyService.java
Author : hev
+ Contributors: IIAB Project
Copyright : Copyright (c) 2024 xyz
+ Copyright (c) 2026 IIAB Project
Description : TProxy Service with integrated Watchdog
============================================================================
*/
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/TermuxCallbackReceiver.java b/apk/controller/app/src/main/java/org/iiab/controller/TermuxCallbackReceiver.java
index 7fe1392..0e86549 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/TermuxCallbackReceiver.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/TermuxCallbackReceiver.java
@@ -1,3 +1,11 @@
+/*
+ * ============================================================================
+ * Name : TermuxCallbackReceiver.java
+ * Author : IIAB Project
+ * Copyright : Copyright (c) 2026 IIAB Project
+ * Description : Termux callback helper
+ * ============================================================================
+ */
package org.iiab.controller;
import android.content.BroadcastReceiver;
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/UsageFragment.java b/apk/controller/app/src/main/java/org/iiab/controller/UsageFragment.java
new file mode 100644
index 0000000..69d9220
--- /dev/null
+++ b/apk/controller/app/src/main/java/org/iiab/controller/UsageFragment.java
@@ -0,0 +1,490 @@
+/*
+ * ============================================================================
+ * Name : UsageFragment.java
+ * Author : IIAB Project
+ * Copyright : Copyright (c) 2026 IIAB Project
+ * Description : Usage Fragment Activity
+ * ============================================================================
+ */
+package org.iiab.controller;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.method.ScrollingMovementMethod;
+import android.view.MotionEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+import androidx.appcompat.app.AlertDialog;
+import androidx.core.content.ContextCompat;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import com.google.android.material.snackbar.Snackbar;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public class UsageFragment extends Fragment implements View.OnClickListener {
+
+ private MainActivity mainActivity;
+ // INTERFACE VARS
+ private EditText edittext_socks_addr, edittext_socks_udp_addr, edittext_socks_port, edittext_socks_user, edittext_socks_pass, edittext_dns_ipv4, edittext_dns_ipv6;
+ private CheckBox checkbox_udp_in_tcp, checkbox_remote_dns, checkbox_global, checkbox_maintenance, checkbox_ipv4, checkbox_ipv6;
+ private TextView textview_maintenance_warning, configLabel, advConfigLabel, logLabel, logWarning, logSizeText, connectionLog;
+ private Button button_apps, button_save, button_control, button_browse_content, watchdogControl, btnClearLog, btnCopyLog;
+ private LinearLayout logActions, configLayout, advancedConfig, deckContainer;
+ private ProgressBar logProgress;
+ private ProgressButton btnServerControl;
+
+ private ObjectAnimator fusionAnimator;
+ private ObjectAnimator exploreAnimator;
+ private DashboardManager dashboardManager;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ if (context instanceof MainActivity) {
+ mainActivity = (MainActivity) context;
+ mainActivity.setUsageFragment(this);
+ }
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_usage, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ // UI Bindings
+ edittext_socks_addr = view.findViewById(R.id.socks_addr);
+ edittext_socks_udp_addr = view.findViewById(R.id.socks_udp_addr);
+ edittext_socks_port = view.findViewById(R.id.socks_port);
+ edittext_socks_user = view.findViewById(R.id.socks_user);
+ edittext_socks_pass = view.findViewById(R.id.socks_pass);
+ edittext_dns_ipv4 = view.findViewById(R.id.dns_ipv4);
+ edittext_dns_ipv6 = view.findViewById(R.id.dns_ipv6);
+ checkbox_ipv4 = view.findViewById(R.id.ipv4);
+ checkbox_ipv6 = view.findViewById(R.id.ipv6);
+ checkbox_global = view.findViewById(R.id.global);
+ checkbox_udp_in_tcp = view.findViewById(R.id.udp_in_tcp);
+ checkbox_remote_dns = view.findViewById(R.id.remote_dns);
+ checkbox_maintenance = view.findViewById(R.id.checkbox_maintenance);
+ textview_maintenance_warning = view.findViewById(R.id.maintenance_warning);
+ button_apps = view.findViewById(R.id.apps);
+ button_save = view.findViewById(R.id.save);
+ button_control = view.findViewById(R.id.control);
+ button_browse_content = view.findViewById(R.id.btnBrowseContent);
+ watchdogControl = view.findViewById(R.id.watchdog_control);
+
+ logActions = view.findViewById(R.id.log_actions);
+ btnClearLog = view.findViewById(R.id.btn_clear_log);
+ btnCopyLog = view.findViewById(R.id.btn_copy_log);
+ connectionLog = view.findViewById(R.id.connection_log);
+ logProgress = view.findViewById(R.id.log_progress);
+ logWarning = view.findViewById(R.id.log_warning_text);
+ logSizeText = view.findViewById(R.id.log_size_text);
+ configLayout = view.findViewById(R.id.config_layout);
+ configLabel = view.findViewById(R.id.config_label);
+ advancedConfig = view.findViewById(R.id.advanced_config);
+ advConfigLabel = view.findViewById(R.id.adv_config_label);
+ logLabel = view.findViewById(R.id.log_label);
+
+ deckContainer = view.findViewById(R.id.deck_container);
+ btnServerControl = view.findViewById(R.id.btn_server_control);
+
+ dashboardManager = new DashboardManager(requireActivity(), view, () -> {
+ mainActivity.handleControlClick();
+ });
+
+ // Listeners
+ watchdogControl.setOnClickListener(v -> mainActivity.handleWatchdogClick());
+ button_control.setOnClickListener(v -> mainActivity.handleControlClick());
+ button_browse_content.setOnClickListener(v -> mainActivity.handleBrowseContentClick(v));
+ btnClearLog.setOnClickListener(this);
+ btnCopyLog.setOnClickListener(this);
+ 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);
+ checkbox_maintenance.setOnClickListener(this);
+ button_apps.setOnClickListener(this);
+ button_save.setOnClickListener(this);
+
+ btnServerControl.setOnClickListener(v -> {
+ if (mainActivity.targetServerState != null) return;
+
+ mainActivity.serverTransitionText = !mainActivity.isServerAlive ? getString(R.string.server_booting) : getString(R.string.server_shutting_down);
+ mainActivity.targetServerState = !mainActivity.isServerAlive;
+
+ updateUIColorsAndVisibility();
+ btnServerControl.startProgress();
+
+ mainActivity.handleServerLaunchClick(v);
+ });
+
+ 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;
+ });
+
+ updateUI();
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == checkbox_global || v == checkbox_remote_dns || v == checkbox_maintenance) {
+ mainActivity.savePrefs();
+ updateUI();
+ } else if (v == button_apps) {
+ startActivity(new Intent(requireContext(), AppListActivity.class));
+ } else if (v.getId() == R.id.save) {
+ mainActivity.savePrefs();
+ Toast.makeText(requireContext(), R.string.saved_toast, Toast.LENGTH_SHORT).show();
+ addToLog(getString(R.string.settings_saved));
+ } else if (v.getId() == R.id.btn_clear_log) {
+ showResetLogConfirmation();
+ } else if (v.getId() == R.id.btn_copy_log) {
+ ClipboardManager clipboard = (ClipboardManager) requireContext().getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("IIAB Log", connectionLog.getText().toString());
+ if (clipboard != null) {
+ clipboard.setPrimaryClip(clip);
+ Toast.makeText(requireContext(), R.string.log_copied_toast, Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ public void updateUI() {
+ if (button_control == null) return;
+
+ boolean vpnActive = mainActivity.prefs.getEnable();
+ boolean watchdogActive = mainActivity.prefs.getWatchdogEnable();
+
+ if (dashboardManager != null) dashboardManager.setTunnelState(vpnActive, mainActivity.isProxyDegraded);
+
+ if (vpnActive) {
+ button_control.setText(R.string.control_disable);
+ button_control.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_vpn_on));
+ } else {
+ button_control.setText(R.string.control_enable);
+ button_control.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_vpn_off));
+ }
+ if (watchdogActive) {
+ watchdogControl.setText(R.string.watchdog_disable);
+ } else {
+ watchdogControl.setText(R.string.watchdog_enable);
+ }
+ edittext_socks_addr.setText(mainActivity.prefs.getSocksAddress());
+ edittext_socks_udp_addr.setText(mainActivity.prefs.getSocksUdpAddress());
+ edittext_socks_port.setText(String.valueOf(mainActivity.prefs.getSocksPort()));
+ edittext_socks_user.setText(mainActivity.prefs.getSocksUsername());
+ edittext_socks_pass.setText(mainActivity.prefs.getSocksPassword());
+ edittext_dns_ipv4.setText(mainActivity.prefs.getDnsIpv4());
+ edittext_dns_ipv6.setText(mainActivity.prefs.getDnsIpv6());
+ checkbox_ipv4.setChecked(mainActivity.prefs.getIpv4());
+ checkbox_ipv6.setChecked(mainActivity.prefs.getIpv6());
+ checkbox_global.setChecked(mainActivity.prefs.getGlobal());
+ checkbox_udp_in_tcp.setChecked(mainActivity.prefs.getUdpInTcp());
+ checkbox_remote_dns.setChecked(mainActivity.prefs.getRemoteDns());
+ checkbox_maintenance.setChecked(mainActivity.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);
+ }
+ }
+
+ public void updateUIColorsAndVisibility() {
+ if (!isAdded() || getContext() == null) {
+ return;
+ }
+ if (button_control == null) return;
+
+ boolean isVpnActive = mainActivity.prefs.getEnable();
+ boolean isWatchdogOn = mainActivity.prefs.getWatchdogEnable();
+
+ if (dashboardManager != null) {
+ dashboardManager.setTunnelState(isVpnActive, mainActivity.isProxyDegraded);
+ }
+
+ // Main VPN Button
+ if (!mainActivity.isServerAlive) {
+ if (isVpnActive) {
+ button_control.setText(R.string.control_disable);
+ button_control.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_vpn_on_dim));
+ } else {
+ button_control.setText(R.string.control_enable);
+ button_control.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_vpn_off_dim));
+ }
+ } else {
+ button_control.setEnabled(true);
+ if (isVpnActive) {
+ button_control.setText(R.string.control_disable);
+ button_control.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_vpn_on));
+ } else {
+ button_control.setText(R.string.control_enable);
+ button_control.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_vpn_off));
+ }
+ }
+
+ // Explore Button
+ button_browse_content.setVisibility(View.VISIBLE);
+ if (!mainActivity.isServerAlive) {
+ stopExplorePulse();
+ button_browse_content.setEnabled(true);
+ button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_explore_disabled));
+ button_browse_content.setAlpha(1.0f);
+ button_browse_content.setTextColor(Color.parseColor("#888888"));
+ } else if (mainActivity.isNegotiating) {
+ button_browse_content.setEnabled(true);
+ button_browse_content.setTextColor(Color.WHITE);
+ } else {
+ stopExplorePulse();
+ button_browse_content.setEnabled(true);
+ button_browse_content.setTextColor(Color.WHITE);
+ button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_explore_ready));
+
+ if (isVpnActive && !mainActivity.isProxyDegraded) {
+ button_browse_content.setAlpha(1.0f);
+ startExplorePulse();
+ } else {
+ button_browse_content.setAlpha(0.6f);
+ }
+ }
+
+ // Server Control Logic
+ if (mainActivity.targetServerState != null) {
+ btnServerControl.setAlpha(0.6f);
+ btnServerControl.setText(mainActivity.serverTransitionText);
+ btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_explore_disabled));
+ } else {
+ btnServerControl.setAlpha(1.0f);
+ if (mainActivity.isServerAlive) {
+ btnServerControl.setText(R.string.stop_server);
+ if (isWatchdogOn) {
+ deckContainer.setBackgroundColor(Color.parseColor("#44FF9800"));
+ btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_watchdog_on));
+ watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_watchdog_on));
+ } else {
+ if (fusionAnimator == null || !fusionAnimator.isRunning()) deckContainer.setBackgroundColor(Color.TRANSPARENT);
+ btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_danger));
+ watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_watchdog_off));
+ }
+ } else {
+ deckContainer.setBackgroundColor(Color.TRANSPARENT);
+ btnServerControl.setText(R.string.launch_server);
+ btnServerControl.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_success));
+ watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), isWatchdogOn ? R.color.btn_watchdog_on : R.color.btn_watchdog_off));
+ }
+ }
+ }
+
+ public void stopBtnProgress() {
+ btnServerControl.stopProgress();
+ }
+
+ public void startFusionPulse() {
+ deckContainer.setBackgroundColor(Color.parseColor("#44FF9800"));
+ if (fusionAnimator != null && fusionAnimator.isRunning()) fusionAnimator.cancel();
+ fusionAnimator = ObjectAnimator.ofFloat(deckContainer, "alpha", 1f, 0.4f);
+ fusionAnimator.setDuration(600);
+ fusionAnimator.setRepeatCount(ObjectAnimator.INFINITE);
+ fusionAnimator.setRepeatMode(ObjectAnimator.REVERSE);
+ fusionAnimator.start();
+ }
+
+ public void startExitPulse() {
+ if (fusionAnimator != null && fusionAnimator.isRunning()) fusionAnimator.cancel();
+ fusionAnimator = ObjectAnimator.ofFloat(deckContainer, "alpha", deckContainer.getAlpha(), 0.3f);
+ fusionAnimator.setDuration(800);
+ fusionAnimator.setRepeatCount(ObjectAnimator.INFINITE);
+ fusionAnimator.setRepeatMode(ObjectAnimator.REVERSE);
+ fusionAnimator.start();
+ }
+
+ public void startExplorePulse() {
+ button_browse_content.setAlpha(1.0f);
+ button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_explore_ready));
+ if (exploreAnimator == null) {
+ PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat(View.SCALE_X, 1.0f, 1.03f);
+ PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.0f, 1.03f);
+ exploreAnimator = ObjectAnimator.ofPropertyValuesHolder(button_browse_content, scaleX, scaleY);
+ exploreAnimator.setDuration(800);
+ exploreAnimator.setRepeatCount(ObjectAnimator.INFINITE);
+ exploreAnimator.setRepeatMode(ObjectAnimator.REVERSE);
+ }
+ if (!exploreAnimator.isRunning()) exploreAnimator.start();
+ }
+
+ public void stopExplorePulse() {
+ if (exploreAnimator != null && exploreAnimator.isRunning()) exploreAnimator.cancel();
+ button_browse_content.setScaleX(1.0f);
+ button_browse_content.setScaleY(1.0f);
+ button_browse_content.setBackgroundTintList(ContextCompat.getColorStateList(requireContext(), R.color.btn_explore_ready));
+ button_browse_content.setAlpha(0.6f);
+ }
+
+ public void finalizeEntryPulse() {
+ if (fusionAnimator != null) fusionAnimator.cancel();
+ deckContainer.setAlpha(1f);
+ }
+
+ public void finalizeExitPulse() {
+ if (fusionAnimator != null) fusionAnimator.cancel();
+ deckContainer.animate().alpha(1f).setDuration(300).withEndAction(() -> deckContainer.setBackgroundColor(Color.TRANSPARENT)).start();
+ }
+
+ public void addToLog(String message) {
+ requireActivity().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 != null && connectionLog.getLayout() != null) {
+ int scroll = connectionLog.getLayout().getLineTop(connectionLog.getLineCount()) - connectionLog.getHeight();
+ if (scroll > 0) connectionLog.scrollTo(0, scroll);
+ }
+ }
+
+ public void updateLogSizeUI() {
+ if (logSizeText == null) return;
+ String sizeStr = LogManager.getFormattedSize(requireContext());
+ logSizeText.setText(getString(R.string.log_size_format, sizeStr));
+ }
+
+ public void updateConnectivityLeds(boolean wifiOn, boolean hotspotOn) {
+ if (dashboardManager != null) {
+ dashboardManager.updateConnectivityLeds(wifiOn, hotspotOn);
+ }
+ }
+
+ public boolean isLogVisible() {
+ return connectionLog != null && connectionLog.getVisibility() == View.VISIBLE;
+ }
+
+ private void handleLogToggle() {
+ boolean isOpening = connectionLog.getVisibility() == View.GONE;
+ if (isOpening) {
+ if (mainActivity.isReadingLogs) return;
+ mainActivity.isReadingLogs = true;
+ if (logProgress != null) logProgress.setVisibility(View.VISIBLE);
+
+ LogManager.readLogsAsync(requireContext(), (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();
+ mainActivity.isReadingLogs = false;
+ });
+ mainActivity.startLogSizeUpdates();
+ } else {
+ mainActivity.stopLogSizeUpdates();
+ }
+ toggleVisibility(connectionLog, logLabel, getString(R.string.connection_log_label));
+ logActions.setVisibility(connectionLog.getVisibility());
+ if (logSizeText != null) logSizeText.setVisibility(connectionLog.getVisibility());
+ }
+
+ private void handleConfigToggle() {
+ if (configLayout.getVisibility() == View.GONE) {
+ if (BiometricHelper.isDeviceSecure(requireContext())) {
+ BiometricHelper.prompt((androidx.appcompat.app.AppCompatActivity) requireActivity(),
+ getString(R.string.auth_required_title),
+ getString(R.string.auth_required_subtitle),
+ () -> toggleVisibility(configLayout, configLabel, getString(R.string.advanced_settings_label)));
+ } else {
+ BiometricHelper.showEnrollmentDialog(requireContext());
+ }
+ } else {
+ toggleVisibility(configLayout, configLabel, getString(R.string.advanced_settings_label));
+ }
+ }
+
+ private void toggleVisibility(View view, TextView label, String text) {
+ boolean isGone = view.getVisibility() == View.GONE;
+ view.setVisibility(isGone ? View.VISIBLE : View.GONE);
+ label.setText(String.format(getString(isGone ? R.string.label_separator_down : R.string.label_separator_up), text));
+ }
+
+ private void showResetLogConfirmation() {
+ new AlertDialog.Builder(requireContext())
+ .setTitle(R.string.log_reset_confirm_title)
+ .setMessage(R.string.log_reset_confirm_msg)
+ .setPositiveButton(R.string.reset_log, (dialog, which) -> {
+ LogManager.clearLogs(requireContext(), new LogManager.LogClearCallback() {
+ @Override
+ public void onSuccess() {
+ connectionLog.setText("");
+ addToLog(getString(R.string.log_reset_user));
+ if (logWarning != null) logWarning.setVisibility(View.GONE);
+ updateLogSizeUI();
+ Toast.makeText(requireContext(), R.string.log_cleared_toast, Toast.LENGTH_SHORT).show();
+ }
+ @Override
+ public void onError(String message) {
+ Toast.makeText(requireContext(), getString(R.string.failed_reset_log, message), Toast.LENGTH_SHORT).show();
+ }
+ });
+ })
+ .setNegativeButton(R.string.cancel, null).show();
+ }
+
+ public void savePrefsFromUI() {
+ mainActivity.prefs.setSocksAddress("127.0.0.1");
+ mainActivity.prefs.setSocksPort(1080);
+ mainActivity.prefs.setSocksUdpAddress("");
+ mainActivity.prefs.setSocksUsername("");
+ mainActivity.prefs.setSocksPassword("");
+ mainActivity.prefs.setIpv4(true);
+ mainActivity.prefs.setIpv6(true);
+ mainActivity.prefs.setUdpInTcp(false);
+ mainActivity.prefs.setRemoteDns(true);
+ mainActivity.prefs.setGlobal(true);
+
+ mainActivity.prefs.setDnsIpv4(edittext_dns_ipv4.getText().toString());
+ mainActivity.prefs.setDnsIpv6(edittext_dns_ipv6.getText().toString());
+ mainActivity.prefs.setMaintenanceMode(checkbox_maintenance.isChecked());
+ }
+}
\ No newline at end of file
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/VpnRecoveryReceiver.java b/apk/controller/app/src/main/java/org/iiab/controller/VpnRecoveryReceiver.java
index 85f47f2..024918e 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/VpnRecoveryReceiver.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/VpnRecoveryReceiver.java
@@ -1,3 +1,11 @@
+/*
+ * ============================================================================
+ * Name : VpnRecoveryReceiver
+ * Author : IIAB Project
+ * Copyright : Copyright (c) 2026 IIAB Project
+ * Description : Button Tunnel helper
+ * ============================================================================
+ */
package org.iiab.controller;
import android.app.Notification;
diff --git a/apk/controller/app/src/main/java/org/iiab/controller/WatchdogService.java b/apk/controller/app/src/main/java/org/iiab/controller/WatchdogService.java
index 9b79487..01bd95a 100644
--- a/apk/controller/app/src/main/java/org/iiab/controller/WatchdogService.java
+++ b/apk/controller/app/src/main/java/org/iiab/controller/WatchdogService.java
@@ -1,3 +1,11 @@
+/*
+ * ============================================================================
+ * Name : WatchdogService.java
+ * Author : IIAB Project
+ * Copyright : Copyright (c) 2026 IIAB Project
+ * Description : Watchdog service helper
+ * ============================================================================
+ */
package org.iiab.controller;
import android.app.AlarmManager;
diff --git a/apk/controller/app/src/main/res/drawable/ic_wip_construction.xml b/apk/controller/app/src/main/res/drawable/ic_wip_construction.xml
new file mode 100644
index 0000000..070e00a
--- /dev/null
+++ b/apk/controller/app/src/main/res/drawable/ic_wip_construction.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/apk/controller/app/src/main/res/drawable/rounded_progress_bar.xml b/apk/controller/app/src/main/res/drawable/rounded_progress_bar.xml
new file mode 100644
index 0000000..5495172
--- /dev/null
+++ b/apk/controller/app/src/main/res/drawable/rounded_progress_bar.xml
@@ -0,0 +1,19 @@
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/apk/controller/app/src/main/res/layout/activity_setup.xml b/apk/controller/app/src/main/res/layout/activity_setup.xml
index 92946ad..9296bae 100644
--- a/apk/controller/app/src/main/res/layout/activity_setup.xml
+++ b/apk/controller/app/src/main/res/layout/activity_setup.xml
@@ -5,8 +5,6 @@
android:layout_height="match_parent"
android:background="?android:attr/windowBackground">
-
-
+
+
+
+