[WebView] implementación de custom webview; manejo de proxy
página de inicio local (index) botones personalizados: recargar página recarga el cache (maps) administración de URLs externos se abren fuera otras funciones
This commit is contained in:
parent
e784d515dc
commit
055116675d
|
|
@ -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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
<application android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/Theme.IIABController">
|
||||
android:theme="@style/Theme.IIABController"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<!-- VPN Service (Network Layer) -->
|
||||
<service android:name=".TProxyService" android:process=":native"
|
||||
|
|
@ -66,6 +67,9 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".AppListActivity" android:label="@string/app_name"/>
|
||||
<activity
|
||||
android:name=".PortalActivity"
|
||||
android:theme="@style/Theme.AppCompat.NoActionBar" />
|
||||
</application>
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Safe Pocket Web</title>
|
||||
<style>
|
||||
/* Reseteo básico y fondo */
|
||||
body {
|
||||
margin: 0; padding: 0;
|
||||
background-color: #F4F7F6;
|
||||
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex; flex-direction: column;
|
||||
justify-content: center; align-items: center;
|
||||
}
|
||||
|
||||
/* Contenedor de botones */
|
||||
.menu-container {
|
||||
width: 80%; display: flex;
|
||||
flex-direction: column; gap: 20px;
|
||||
}
|
||||
|
||||
/* Estilo de los botones */
|
||||
.btn {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 18px; border-radius: 14px; text-decoration: none;
|
||||
font-size: 20px; font-weight: bold;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
|
||||
transition: transform 0.1s, box-shadow 0.1s;
|
||||
}
|
||||
.btn:active { transform: scale(0.96); box-shadow: 0 2px 5px rgba(0,0,0,0.2); }
|
||||
|
||||
/* Colores */
|
||||
.btn-books { background-color: #00BCD4; color: #FFFFFF; }
|
||||
.btn-kiwix { background-color: #FF9800; color: #FFFFFF; }
|
||||
.btn-kolibri { background-color: #FFD54F; color: #333333; }
|
||||
.btn-maps { background-color: #4CAF50; color: #FFFFFF; }
|
||||
.btn-matomo { background-color: #1976D2; color: #FFFFFF; }
|
||||
|
||||
.btn span { margin-right: 12px; font-size: 26px; }
|
||||
|
||||
/* --- NUEVO: ESTILOS DEL SPINNER Y OVERLAY --- */
|
||||
#loadingOverlay {
|
||||
display: none; /* Oculto por defecto */
|
||||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background-color: rgba(244, 247, 246, 0.85); /* Fondo difuminado */
|
||||
z-index: 9999; /* Por encima de todo */
|
||||
flex-direction: column; justify-content: center; align-items: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 50px; height: 50px;
|
||||
border: 6px solid #E0E0E0;
|
||||
border-top: 6px solid #1976D2; /* Azul elegante */
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 15px; font-size: 18px; font-weight: bold; color: #333;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="loadingOverlay">
|
||||
<div class="spinner"></div>
|
||||
<div class="loading-text" id="loadingText">Cargando...</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-container">
|
||||
<a href="http://box/books" class="btn btn-books"><span>📖</span> Books</a>
|
||||
<a href="http://box/kiwix" class="btn btn-kiwix"><span>🥝</span> Kiwix</a>
|
||||
<a href="http://box/kolibri" class="btn btn-kolibri"><span>📚</span> Kolibri</a>
|
||||
<a href="http://box/maps" class="btn btn-maps"><span>🗺️</span> Maps</a>
|
||||
<a href="http://box/matomo" class="btn btn-matomo"><span>📊</span> Matomo</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const overlay = document.getElementById('loadingOverlay');
|
||||
const textLabel = document.getElementById('loadingText');
|
||||
const buttons = document.querySelectorAll('.btn');
|
||||
|
||||
// 1. Mostrar el spinner al hacer clic
|
||||
buttons.forEach(btn => {
|
||||
btn.addEventListener('click', function(e) {
|
||||
// Extraemos el texto del botón (ej. "Kolibri") para hacerlo personalizado
|
||||
const appName = this.innerText.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]|\uD83C[\uDF00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/g, '').trim();
|
||||
textLabel.innerText = 'Abriendo ' + appName + '...';
|
||||
|
||||
// Mostramos la pantalla de carga
|
||||
overlay.style.display = 'flex';
|
||||
});
|
||||
});
|
||||
|
||||
// 2. Ocultar el spinner si el usuario regresa con el botón "Atrás"
|
||||
window.addEventListener('pageshow', function(event) {
|
||||
overlay.style.display = 'none';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
window.i18n = {
|
||||
"title": "Choose Content",
|
||||
"maps": "🗺️ Maps",
|
||||
"kolibri": "📚 Kolibri",
|
||||
"kiwix": "📖 Kiwix",
|
||||
"books": "📕 Books"
|
||||
};
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
window.i18n = {
|
||||
"title": "Elige el Contenido",
|
||||
"maps": "🗺️ Mapas",
|
||||
"kolibri": "📚 Kolibri",
|
||||
"kiwix": "📖 Kiwix",
|
||||
"books": "📕 Libros"
|
||||
};
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/myWebView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnHandle"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:background="#AA000000"
|
||||
android:text="⌃"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="18sp"
|
||||
android:padding="0dp"
|
||||
android:stateListAnimator="@null" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bottomNav"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="#EEF2F2F2"
|
||||
android:orientation="vertical"
|
||||
android:visibility="invisible"
|
||||
android:elevation="8dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnHideNav"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="30dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:text="⌄"
|
||||
android:textSize="20sp"
|
||||
android:textColor="#555555"
|
||||
android:stateListAnimator="@null" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<Button android:id="@+id/btnBack" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="◀" android:textSize="20sp" style="?android:attr/buttonBarButtonStyle"/>
|
||||
<Button android:id="@+id/btnHome" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="🏠" android:textSize="20sp" style="?android:attr/buttonBarButtonStyle"/>
|
||||
<Button android:id="@+id/btnReload" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="↻" android:textSize="20sp" style="?android:attr/buttonBarButtonStyle"/>
|
||||
<Button android:id="@+id/btnForward" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="▶" android:textSize="20sp" style="?android:attr/buttonBarButtonStyle"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
|
|
@ -108,6 +108,14 @@
|
|||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="12dp"/>
|
||||
|
||||
<!-- Local WebView -->
|
||||
<Button
|
||||
android:id="@+id/btnBrowseContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Browse Content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/apps"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
Loading…
Reference in New Issue