Skip to content

Commit abd8635

Browse files
Merge pull request #35 from appdevforall/feat/portal-webview-slice
feat(portal): WebView clean-arch slice + two-finger map gestures
2 parents 3c614ee + 5370d81 commit abd8635

10 files changed

Lines changed: 323 additions & 68 deletions

File tree

‎controller/app/src/main/java/org/iiab/controller/PortalActivity.java‎

Lines changed: 64 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
1515

1616
import androidx.appcompat.app.AppCompatActivity;
1717
import androidx.core.content.ContextCompat;
18-
import androidx.webkit.ProxyConfig;
19-
import androidx.webkit.ProxyController;
20-
import androidx.webkit.WebViewFeature;
18+
import androidx.lifecycle.ViewModelProvider;
2119

2220
import java.util.concurrent.Executor;
2321

@@ -29,10 +27,33 @@
2927
import android.os.Handler;
3028
import android.os.Looper;
3129

30+
import org.iiab.controller.portal.data.WebViewProxyConfigurator;
31+
import org.iiab.controller.portal.domain.NavigationPolicy;
32+
import org.iiab.controller.portal.presentation.GestureWebView;
33+
import org.iiab.controller.portal.presentation.PortalViewModel;
34+
3235
public class PortalActivity extends AppCompatActivity {
3336
private static final String TAG = "IIAB-Portal";
34-
private WebView webView;
35-
private boolean isPageLoading = false;
37+
38+
/**
39+
* Injected after each page load. (1) logs touch-point counts the web content
40+
* receives (captured via onConsoleMessage -> logcat) so a lost multi-finger
41+
* gesture can be diagnosed; (2) best-effort enables MapLibre two-finger pitch
42+
* if a map instance is exposed on the page.
43+
*/
44+
private static final String TOUCH_PROBE_JS =
45+
"(function(){if(window.__iiabTouchProbe)return;window.__iiabTouchProbe=true;" +
46+
"['touchstart','touchmove'].forEach(function(t){document.addEventListener(t,function(e){" +
47+
"try{console.log('IIAB-TOUCH '+t+' touches='+(e.touches?e.touches.length:0));}catch(_){}}," +
48+
"{passive:true,capture:true});});" +
49+
"try{var m=window.map||window.__map||(window.maplibregl&&window.maplibregl.__map);" +
50+
"if(m&&m.touchPitch&&m.touchPitch.enable){m.touchPitch.enable();" +
51+
"if(m.touchZoomRotate&&m.touchZoomRotate.enable){m.touchZoomRotate.enable();}" +
52+
"console.log('IIAB-TOUCH pitch-enabled');}else{console.log('IIAB-TOUCH no-map-instance');}}" +
53+
"catch(err){console.log('IIAB-TOUCH pitch-error '+err);}})();";
54+
55+
private GestureWebView webView;
56+
private PortalViewModel vm;
3657
private android.webkit.ValueCallback<android.net.Uri[]> filePathCallback;
3758
private final static int FILECHOOSER_RESULTCODE = 100;
3859

@@ -41,8 +62,11 @@ protected void onCreate(Bundle savedInstanceState) {
4162
super.onCreate(savedInstanceState);
4263
setContentView(R.layout.activity_portal);
4364

65+
vm = new ViewModelProvider(this).get(PortalViewModel.class);
66+
4467
// 1. Basic WebView configuration
4568
webView = findViewById(R.id.myWebView);
69+
webView.setGestureLogging(BuildConfig.DEBUG);
4670

4771
LinearLayout bottomNav = findViewById(R.id.bottomNav);
4872
Button btnHandle = findViewById(R.id.btnHandle); // The new handle
@@ -55,39 +79,32 @@ protected void onCreate(Bundle savedInstanceState) {
5579
Button btnForward = findViewById(R.id.btnForward);
5680

5781
// --- PREPARE HIDDEN BAR ---
58-
// Wait for Android to draw the screen to determine bar height
59-
// and hide it exactly below the bottom edge.
6082
bottomNav.post(() -> {
6183
bottomNav.setTranslationY(bottomNav.getHeight()); // Move outside the screen
62-
bottomNav.setVisibility(View.VISIBLE); // Remove invisibility
84+
bottomNav.setVisibility(View.VISIBLE);
6385
});
6486

6587
// --- AUTO-HIDE TIMER ---
6688
Handler hideHandler = new Handler(Looper.getMainLooper());
6789

68-
// This is the hiding action packaged for later use
6990
Runnable hideRunnable = () -> {
7091
bottomNav.animate().translationY(bottomNav.getHeight()).setDuration(250);
7192
btnHandle.setVisibility(View.VISIBLE);
7293
btnHandle.animate().alpha(1f).setDuration(150);
7394
};
7495

75-
// --- Restart timer ---
7696
Runnable resetTimer = () -> {
7797
hideHandler.removeCallbacks(hideRunnable);
78-
hideHandler.postDelayed(hideRunnable, 5000); // Restarts new 5 sec
98+
hideHandler.postDelayed(hideRunnable, 5000);
7999
};
80100

81101
// --- HANDLE LOGIC (Show Bar) ---
82102
btnHandle.setOnClickListener(v -> {
83-
// 1. Animate entry
84103
btnHandle.animate().alpha(0f).setDuration(150).withEndAction(() -> btnHandle.setVisibility(View.GONE));
85104
bottomNav.animate().translationY(0).setDuration(250);
86-
87-
// 2. Starts countdown
88105
resetTimer.run();
89106
});
90-
// Button actions
107+
91108
btnBack.setOnClickListener(v -> {
92109
if (webView.canGoBack()) webView.goBack();
93110
resetTimer.run();
@@ -101,15 +118,8 @@ protected void onCreate(Bundle savedInstanceState) {
101118
Preferences prefs = new Preferences(this);
102119
boolean isVpnActive = prefs.getEnable();
103120

104-
String rawUrl = getIntent().getStringExtra("TARGET_URL");
105-
106-
// If for some strange reason the URL arrives empty, we use the security fallback
107-
if (rawUrl == null || rawUrl.isEmpty()) {
108-
rawUrl = "http://localhost:8085/home";
109-
}
110-
111-
// We are giving the URL secure global reach for all lambdas from now on
112-
final String finalTargetUrl = rawUrl;
121+
// Resolve the target URL once (domain), surviving rotation via the ViewModel.
122+
final String finalTargetUrl = vm.targetUrl(getIntent().getStringExtra("TARGET_URL"));
113123

114124
btnHome.setOnClickListener(v -> {
115125
webView.loadUrl(finalTargetUrl);
@@ -118,84 +128,78 @@ protected void onCreate(Bundle savedInstanceState) {
118128

119129
// Dual logic: Forced reload or Stop
120130
btnReload.setOnClickListener(v -> {
121-
if (isPageLoading) {
131+
if (vm.isLoading()) {
122132
webView.stopLoading();
123133
} else {
124-
// Disable cache temporarily
125134
webView.getSettings().setCacheMode(android.webkit.WebSettings.LOAD_NO_CACHE);
126-
// Force download from scratch
127135
webView.clearCache(true);
128136
webView.reload();
129137
}
130138
resetTimer.run();
131139
});
132140

133-
// --- NEW: DETECT LOADING TO CHANGE BUTTON TO 'X' ---
134141
webView.setWebViewClient(new WebViewClient() {
135142
@Override
136143
public boolean shouldOverrideUrlLoading(WebView view, android.webkit.WebResourceRequest request) {
137144
String url = request.getUrl().toString();
138145
String host = request.getUrl().getHost();
139146

140-
// Internal server link (Box)
141-
if (host != null && (host.equals("box") || host.equals("127.0.0.1") || host.equals("localhost"))) {
142-
return false; // Remains in our app and travels through the proxy
147+
// Internal server link stays in the WebView (and travels through the proxy).
148+
if (NavigationPolicy.isInternalHost(host)) {
149+
return false;
143150
}
144151

145-
// External link (Real Internet)
152+
// External link: hand to the system browser / appropriate app.
146153
try {
147-
// Tell Android to find the correct app to open this (Chrome, YouTube, etc.)
148-
Intent intent = new Intent(Intent.ACTION_VIEW, request.getUrl());
149-
startActivity(intent);
154+
startActivity(new Intent(Intent.ACTION_VIEW, request.getUrl()));
150155
} catch (Exception e) {
151156
Log.e(TAG, "No app installed to open: " + url);
152157
}
153-
154-
return true; // return true means: "WebView, I'll handle it, you ignore this click"
158+
return true;
155159
}
156160

157161
@Override
158162
public void onPageStarted(WebView view, String url, Bitmap favicon) {
159163
super.onPageStarted(view, url, favicon);
160-
isPageLoading = true;
164+
vm.setLoading(true);
161165
btnReload.setText("✕"); // Change to Stop
162166
}
163167

164168
@Override
165169
public void onPageFinished(WebView view, String url) {
166170
super.onPageFinished(view, url);
167-
isPageLoading = false;
171+
vm.setLoading(false);
168172
btnReload.setText("↻"); // Back to Reload
169-
170-
// Restore cache for normal browsing speed
171173
view.getSettings().setCacheMode(android.webkit.WebSettings.LOAD_DEFAULT);
174+
175+
// Touch diagnostics + best-effort MapLibre pitch enablement.
176+
view.evaluateJavascript(TOUCH_PROBE_JS, null);
172177
}
173178

174179
@Override
175180
public void onReceivedError(WebView view, android.webkit.WebResourceRequest request, android.webkit.WebResourceError error) {
176181
super.onReceivedError(view, request, error);
177-
178182
if (request.isForMainFrame()) {
179183
String customErrorHtml = "<html><body style='background-color:#1A1A1A;color:#FFFFFF;text-align:center;padding-top:50px;font-family:sans-serif;'>"
180184
+ "<h2>⚠️ Connection Failed</h2>"
181185
+ "<p>Unable to reach the secure environment.</p>"
182186
+ "<p style='color:#888;font-size:12px;'>Error: " + error.getDescription() + "</p>"
183187
+ "</body></html>";
184188
view.loadData(customErrorHtml, "text/html", "UTF-8");
185-
isPageLoading = false;
189+
vm.setLoading(false);
186190
btnReload.setText("↻");
187191
}
188192
}
189193
});
190194

191195
// --- MANUALLY CLOSE BAR LOGIC ---
192196
btnHideNav.setOnClickListener(v -> {
193-
hideHandler.removeCallbacks(hideRunnable); // Cancel the timer so it doesn't conflict
194-
hideRunnable.run(); // Execute hiding action immediately
197+
hideHandler.removeCallbacks(hideRunnable);
198+
hideRunnable.run();
195199
});
196200

197-
// <-- EXIT ACTION -->
198201
btnExit.setOnClickListener(v -> finish());
202+
199203
webView.getSettings().setJavaScriptEnabled(true);
200204
webView.getSettings().setDomStorageEnabled(true);
201205
webView.setWebChromeClient(new android.webkit.WebChromeClient() {
@@ -215,48 +219,43 @@ public boolean onShowFileChooser(WebView webView, android.webkit.ValueCallback<a
215219
}
216220
return true;
217221
}
222+
223+
@Override
224+
public boolean onConsoleMessage(android.webkit.ConsoleMessage consoleMessage) {
225+
// Surfaces in-page diagnostics (incl. IIAB-TOUCH probes) to logcat.
226+
Log.d(TAG, "WebConsole: " + consoleMessage.message());
227+
return true;
228+
}
218229
});
219230

220231
// Port and Mirror logic
221232
int tempPort = prefs.getSocksPort();
222233
if (tempPort <= 0) tempPort = 1080;
223-
224-
// We restored the secure variable for the port
225234
final int finalProxyPort = tempPort;
226235

227-
// 4. Proxy block (ONLY IF VPN IS ACTIVE)
236+
// Proxy block (ONLY IF VPN IS ACTIVE)
228237
if (isVpnActive) {
229-
if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
230-
ProxyConfig proxyConfig = new ProxyConfig.Builder()
231-
.addProxyRule("socks5://127.0.0.1:" + finalProxyPort)
232-
.build();
233-
238+
if (WebViewProxyConfigurator.isSupported()) {
234239
Executor executor = ContextCompat.getMainExecutor(this);
235-
236-
ProxyController.getInstance().setProxyOverride(proxyConfig, executor, () -> {
240+
WebViewProxyConfigurator.applySocks(finalProxyPort, executor, () -> {
237241
Log.d(TAG, "Proxy configured on port: " + finalProxyPort);
238-
// Load HTML only when proxy is ready
239-
webView.loadUrl(finalTargetUrl);
242+
webView.loadUrl(finalTargetUrl); // load only when proxy is ready
240243
});
241244
} else {
242-
// Fallback for older devices
243245
Log.w(TAG, "Proxy Override not supported");
244246
webView.loadUrl(finalTargetUrl);
245247
}
246248
} else {
247-
// VPN is OFF. Do NOT use proxy. Just load localhost directly.
249+
// VPN is OFF. Load localhost directly.
248250
webView.loadUrl(finalTargetUrl);
249251
}
250252
}
251253

252-
// Cleanup (Important to not leave the proxy active)
253254
@Override
254255
protected void onDestroy() {
255256
super.onDestroy();
256-
if (WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
257-
ProxyController.getInstance().clearProxyOverride(Runnable::run, () -> {
258-
Log.d(TAG, "WebView proxy released");
259-
});
257+
if (WebViewProxyConfigurator.isSupported()) {
258+
WebViewProxyConfigurator.clear(() -> Log.d(TAG, "WebView proxy released"));
260259
}
261260
}
262261

@@ -275,7 +274,6 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
275274
if (filePathCallback == null) return;
276275

277276
android.net.Uri[] results = null;
278-
279277
if (resultCode == RESULT_OK && data != null) {
280278
String dataString = data.getDataString();
281279
if (dataString != null) {
@@ -288,7 +286,6 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
288286
}
289287
}
290288
}
291-
292289
filePathCallback.onReceiveValue(results);
293290
filePathCallback = null;
294291
} else {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* ============================================================================
3+
* Name : WebViewProxyConfigurator.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : Wraps WebView SOCKS proxy override (VPN) behind a small adapter.
7+
* ============================================================================
8+
*/
9+
package org.iiab.controller.portal.data;
10+
11+
import androidx.webkit.ProxyConfig;
12+
import androidx.webkit.ProxyController;
13+
import androidx.webkit.WebViewFeature;
14+
15+
import java.util.concurrent.Executor;
16+
17+
/** Thin adapter over androidx ProxyController for the portal's SOCKS proxy. */
18+
public final class WebViewProxyConfigurator {
19+
20+
private WebViewProxyConfigurator() {}
21+
22+
public static boolean isSupported() {
23+
return WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE);
24+
}
25+
26+
/** Route WebView traffic through socks5://127.0.0.1:{port}; runs {@code onReady} when applied. */
27+
public static void applySocks(int port, Executor executor, Runnable onReady) {
28+
ProxyConfig config = new ProxyConfig.Builder()
29+
.addProxyRule("socks5://127.0.0.1:" + port)
30+
.build();
31+
ProxyController.getInstance().setProxyOverride(config, executor, onReady);
32+
}
33+
34+
/** Remove any proxy override (call on teardown). */
35+
public static void clear(Runnable onCleared) {
36+
ProxyController.getInstance().clearProxyOverride(Runnable::run, onCleared);
37+
}
38+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* ============================================================================
3+
* Name : NavigationPolicy.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : Decides whether a URL stays in the WebView (local IIAB server) or opens externally.
7+
* ============================================================================
8+
*/
9+
package org.iiab.controller.portal.domain;
10+
11+
/**
12+
* Pure navigation policy for the portal WebView. Hosts served by the on-device
13+
* IIAB server are kept inside the WebView; everything else is "external" and
14+
* should be handed to the system browser.
15+
*/
16+
public final class NavigationPolicy {
17+
18+
private NavigationPolicy() {}
19+
20+
/** True if {@code host} is served by the local IIAB server (stay in WebView). */
21+
public static boolean isInternalHost(String host) {
22+
if (host == null) return false;
23+
String h = host.trim().toLowerCase();
24+
return h.equals("box") || h.equals("localhost") || h.equals("127.0.0.1");
25+
}
26+
27+
/** Inverse of {@link #isInternalHost(String)} — should open in an external app. */
28+
public static boolean isExternalHost(String host) {
29+
return !isInternalHost(host);
30+
}
31+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* ============================================================================
3+
* Name : PortalUrlResolver.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : Resolves the portal target URL, falling back to the local home page.
7+
* ============================================================================
8+
*/
9+
package org.iiab.controller.portal.domain;
10+
11+
/** Resolves the URL the portal should open, with a safe local fallback. */
12+
public final class PortalUrlResolver {
13+
14+
/** Local IIAB home, used when no target URL is supplied. */
15+
public static final String DEFAULT_URL = "http://localhost:8085/home";
16+
17+
private PortalUrlResolver() {}
18+
19+
/** Returns {@code rawUrl} when usable, otherwise {@link #DEFAULT_URL}. */
20+
public static String resolve(String rawUrl) {
21+
if (rawUrl == null || rawUrl.trim().isEmpty()) {
22+
return DEFAULT_URL;
23+
}
24+
return rawUrl;
25+
}
26+
}

0 commit comments

Comments
 (0)