1515
1616import androidx .appcompat .app .AppCompatActivity ;
1717import 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
2220import java .util .concurrent .Executor ;
2321
2927import android .os .Handler ;
3028import 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+
3235public 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 {
0 commit comments