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
+
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apk/controller/app/src/main/res/layout/main.xml b/apk/controller/app/src/main/res/layout/main.xml
index 3817cc0..c85b803 100644
--- a/apk/controller/app/src/main/res/layout/main.xml
+++ b/apk/controller/app/src/main/res/layout/main.xml
@@ -108,6 +108,14 @@
android:layout_marginTop="8dp"
android:layout_marginBottom="12dp"/>
+
+
+