diff --git a/apk/controller/app/build.gradle b/apk/controller/app/build.gradle index 8f4082d..ac283ed 100644 --- a/apk/controller/app/build.gradle +++ b/apk/controller/app/build.gradle @@ -78,4 +78,5 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'androidx.biometric:biometric:1.1.0' implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.webkit:webkit:1.12.0' } diff --git a/apk/controller/app/src/main/AndroidManifest.xml b/apk/controller/app/src/main/AndroidManifest.xml index 9cd9938..392c937 100644 --- a/apk/controller/app/src/main/AndroidManifest.xml +++ b/apk/controller/app/src/main/AndroidManifest.xml @@ -11,7 +11,8 @@ + android:theme="@style/Theme.IIABController" + android:usesCleartextTraffic="true"> + diff --git a/apk/controller/app/src/main/assets/index.html b/apk/controller/app/src/main/assets/index.html new file mode 100644 index 0000000..706af1b --- /dev/null +++ b/apk/controller/app/src/main/assets/index.html @@ -0,0 +1,108 @@ + + + + + + Safe Pocket Web + + + + +
+
+
Cargando...
+
+ + + + + + \ No newline at end of file diff --git a/apk/controller/app/src/main/assets/js/app.js b/apk/controller/app/src/main/assets/js/app.js new file mode 100644 index 0000000..a1521ab --- /dev/null +++ b/apk/controller/app/src/main/assets/js/app.js @@ -0,0 +1,43 @@ +document.addEventListener("DOMContentLoaded", () => { + + // 1. Detect device language + let userLang = (navigator.language || navigator.userLanguage).substring(0, 2).toLowerCase(); + + // Function to detect the user's language + const applyTranslations = () => { + if (!window.i18n) return; // If something went wrong, we return + + // Search for all elements with the data-i18n attribute + const elements = document.querySelectorAll("[data-i18n]"); + + elements.forEach(el => { + const key = el.getAttribute("data-i18n"); + // Si la clave existe en el diccionario, reemplaza el texto + if (window.i18n[key]) { + el.innerText = window.i18n[key]; + } + }); + }; + + // Function to load the .js file of the language + const loadScript = (langCode, isFallback = false) => { + const script = document.createElement("script"); + script.src = `lang/${langCode}.js`; + + // If the file exists, we apply the translations + script.onload = () => applyTranslations(); + + // If the file does NOT exist + script.onerror = () => { + if (!isFallback) { + console.log(`Idioma ${langCode} no encontrado. Cargando inglés...`); + loadScript("en", true); // Intentamos con el idioma por defecto + } + }; + + document.head.appendChild(script); + }; + + // 2. Start the script loading + loadScript(userLang); +}); diff --git a/apk/controller/app/src/main/assets/lang/en.js b/apk/controller/app/src/main/assets/lang/en.js new file mode 100644 index 0000000..3e6602e --- /dev/null +++ b/apk/controller/app/src/main/assets/lang/en.js @@ -0,0 +1,7 @@ +window.i18n = { + "title": "Choose Content", + "maps": "🗺️ Maps", + "kolibri": "📚 Kolibri", + "kiwix": "📖 Kiwix", + "books": "📕 Books" +}; diff --git a/apk/controller/app/src/main/assets/lang/es.js b/apk/controller/app/src/main/assets/lang/es.js new file mode 100644 index 0000000..bc7b492 --- /dev/null +++ b/apk/controller/app/src/main/assets/lang/es.js @@ -0,0 +1,7 @@ +window.i18n = { + "title": "Elige el Contenido", + "maps": "🗺️ Mapas", + "kolibri": "📚 Kolibri", + "kiwix": "📖 Kiwix", + "books": "📕 Libros" +}; diff --git a/apk/controller/app/src/main/assets/style.css b/apk/controller/app/src/main/assets/style.css new file mode 100644 index 0000000..e69de29 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 ba079e8..40dd13a 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 @@ -83,7 +83,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe 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; @@ -138,7 +138,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe batteryOptLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { - Log.d(TAG, "Regresamos de la pantalla de ajustes de batería"); + Log.d(TAG, "Returned from the battery settings screen"); checkBatteryOptimizations(); } ); @@ -173,6 +173,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe 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); @@ -205,6 +206,12 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe button_save.setOnClickListener(this); button_control.setOnClickListener(this); + // Logic to open the WebView (PortalActivity) + button_browse_content.setOnClickListener(v -> { + Intent intent = new Intent(MainActivity.this, PortalActivity.class); + startActivity(intent); + }); + connectionLog.setMovementMethod(new ScrollingMovementMethod()); connectionLog.setTextIsSelectable(true); connectionLog.setOnTouchListener((v, event) -> { @@ -373,12 +380,11 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe @Override protected void onResume() { super.onResume(); - // Comprobamos batería siempre que volvemos a la app + // Check battery status whenever returning to the app if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); if (pm != null && !pm.isIgnoringBatteryOptimizations(getPackageName())) { - // Si no está ignorado, mostramos el aviso (o Snackbar si ya se lanzó el diálogo) - Log.d(TAG, "onResume: Batería aún optimizada, mostrando aviso"); + Log.d(TAG, "onResume: Battery still optimized, showing warning"); showBatterySnackbar(); } } @@ -625,7 +631,7 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe private void showWatchdogBiometricPrompt() { Executor ex = ContextCompat.getMainExecutor(this); - BiometricPrompt bp = new BiometricPrompt(this, ex, new BiometricPrompt.AuthenticationCallback() { + BiometricPrompt bp = new BiometricPrompt(this, new BiometricPrompt.AuthenticationCallback() { @Override public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { super.onAuthenticationSucceeded(result); @@ -655,6 +661,11 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe button_control.setText(R.string.control_enable); button_control.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_vpn_off)); } + if (vpnActive) { + button_browse_content.setVisibility(View.VISIBLE); + } else { + button_browse_content.setVisibility(View.GONE); + } if (watchdogActive) { watchdogControl.setText(R.string.watchdog_disable); watchdogControl.setBackgroundTintList(ContextCompat.getColorStateList(this, R.color.btn_watchdog_on)); 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 new file mode 100644 index 0000000..d3a29b1 --- /dev/null +++ b/apk/controller/app/src/main/java/org/iiab/controller/PortalActivity.java @@ -0,0 +1,191 @@ +package org.iiab.controller; + +import android.os.Bundle; +import android.util.Log; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; +import androidx.webkit.ProxyConfig; +import androidx.webkit.ProxyController; +import androidx.webkit.WebViewFeature; +import java.util.concurrent.Executor; +import android.graphics.Bitmap; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; + +public class PortalActivity extends AppCompatActivity { + private static final String TAG = "IIAB-Portal"; + private WebView webView; + private boolean isPageLoading = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_portal); + + // 1. Basic WebView configuration + webView = findViewById(R.id.myWebView); + + LinearLayout bottomNav = findViewById(R.id.bottomNav); + Button btnHandle = findViewById(R.id.btnHandle); // The new handle + Button btnHideNav = findViewById(R.id.btnHideNav); // Button to close + + Button btnBack = findViewById(R.id.btnBack); + Button btnHome = findViewById(R.id.btnHome); + Button btnReload = findViewById(R.id.btnReload); + Button btnForward = findViewById(R.id.btnForward); + + // Button actions + btnBack.setOnClickListener(v -> { if (webView.canGoBack()) webView.goBack(); }); + btnForward.setOnClickListener(v -> { if (webView.canGoForward()) webView.goForward(); }); + btnHome.setOnClickListener(v -> webView.loadUrl("file:///android_asset/index.html")); + + // Dual logic: Forced reload or Stop + btnReload.setOnClickListener(v -> { + if (isPageLoading) { + webView.stopLoading(); + } else { + // 1. Disable cache temporarily + webView.getSettings().setCacheMode(android.webkit.WebSettings.LOAD_NO_CACHE); + // 2. Force download from scratch + webView.reload(); + } + }); + + // --- NEW: DETECT LOADING TO CHANGE BUTTON TO 'X' --- + webView.setWebViewClient(new WebViewClient() { + @Override + public boolean shouldOverrideUrlLoading(WebView view, android.webkit.WebResourceRequest request) { + String url = request.getUrl().toString(); + String host = request.getUrl().getHost(); + + // 1. Local main menu + if (url.startsWith("file://")) { + return false; // return false means: "WebView, handle it yourself" + } + + // 2. Internal server link (Box) + if (host != null && (host.equals("box") || host.equals("127.0.0.1") || host.equals("localhost"))) { + return false; // Remains in our app and travels through the proxy + } + + // 3. External link (Real Internet) + try { + // Tell Android to find the correct app to open this (Chrome, YouTube, etc.) + Intent intent = new Intent(Intent.ACTION_VIEW, request.getUrl()); + startActivity(intent); + } catch (Exception e) { + Log.e(TAG, "No app installed to open: " + url); + } + + return true; // return true means: "WebView, I'll handle it, you ignore this click" + } + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + super.onPageStarted(view, url, favicon); + isPageLoading = true; + btnReload.setText("✕"); // Change to Stop + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + isPageLoading = false; + btnReload.setText("↻"); // Back to Reload + + // Restore cache for normal browsing speed + view.getSettings().setCacheMode(android.webkit.WebSettings.LOAD_DEFAULT); + } + }); + + // --- PREPARE HIDDEN BAR --- + // Wait for Android to draw the screen to determine bar height + // and hide it exactly below the bottom edge. + bottomNav.post(() -> { + bottomNav.setTranslationY(bottomNav.getHeight()); // Move outside the screen + bottomNav.setVisibility(View.VISIBLE); // Remove invisibility + }); + +// --- TEMPORIZADOR PARA AUTO-OCULTAR --- + Handler hideHandler = new Handler(Looper.getMainLooper()); + + // Esta es la acción de ocultar empaquetada para usarla luego + Runnable hideRunnable = () -> { + bottomNav.animate().translationY(bottomNav.getHeight()).setDuration(250); + btnHandle.setVisibility(View.VISIBLE); + btnHandle.animate().alpha(1f).setDuration(150); + }; + + // --- LÓGICA DEL TIRADOR (Mostrar Barra) --- + btnHandle.setOnClickListener(v -> { + // 1. Animamos la entrada + btnHandle.animate().alpha(0f).setDuration(150).withEndAction(() -> btnHandle.setVisibility(View.GONE)); + bottomNav.animate().translationY(0).setDuration(250); + + // 2. Iniciamos la cuenta regresiva de 5 segundos (5000 ms) + hideHandler.removeCallbacks(hideRunnable); // Cancelamos si había una cuenta anterior + hideHandler.postDelayed(hideRunnable, 5000); // <- Cambia el 5000 por 7000 si prefieres 7 segundos + }); + + // --- LÓGICA DE CERRAR BARRA MANUALMENTE --- + btnHideNav.setOnClickListener(v -> { + hideHandler.removeCallbacks(hideRunnable); // Cancelamos el temporizador para que no choque + hideRunnable.run(); // Ejecutamos la acción de ocultar inmediatamente + }); + + webView.getSettings().setJavaScriptEnabled(true); + webView.getSettings().setDomStorageEnabled(true); + + // 2. Port and Mirror logic + Preferences prefs = new Preferences(this); + int tempPort = prefs.getSocksPort(); + if (tempPort <= 0) tempPort = 1080; + + // Variable safe to read in lambda + final int finalProxyPort = tempPort; + + // 3. Proxy block + if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) { + ProxyConfig proxyConfig = new ProxyConfig.Builder() + .addProxyRule("socks5://127.0.0.1:" + finalProxyPort) + .build(); + + Executor executor = ContextCompat.getMainExecutor(this); + + ProxyController.getInstance().setProxyOverride(proxyConfig, executor, () -> { + Log.d(TAG, "Proxy configured on port: " + finalProxyPort); + // Load HTML only when proxy is ready + webView.loadUrl("file:///android_asset/index.html"); + }); + } else { + // Fallback for older devices + Log.w(TAG, "Proxy Override not supported"); + } + webView.loadUrl("file:///android_asset/index.html"); + } + + // 4. Cleanup (Important to not leave the proxy active) + @Override + protected void onDestroy() { + super.onDestroy(); + if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) { + ProxyController.getInstance().clearProxyOverride(Runnable::run, () -> { + Log.d(TAG, "WebView proxy released"); + }); + } + } + + @Override + public void onBackPressed() { + if (webView.canGoBack()) { + webView.goBack(); + } else { + super.onBackPressed(); + } + } +} diff --git a/apk/controller/app/src/main/res/layout/activity_portal.xml b/apk/controller/app/src/main/res/layout/activity_portal.xml new file mode 100644 index 0000000..5405afd --- /dev/null +++ b/apk/controller/app/src/main/res/layout/activity_portal.xml @@ -0,0 +1,56 @@ + + + + + +