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"> - -