From 6bb00b5b4d67098b1ff3a044450113b214423507 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 22 Jul 2026 18:55:51 +0530 Subject: [PATCH 1/4] feat: add WebView Plugin API Add cordova-plugin-acode-webview allowing plugins to create and manage isolated WebView instances with fullscreen, window, panel, and hidden modes. Features: - Create independent WebView instances via acode.webview.create() - Load URLs, HTML content, and execute JavaScript - Bidirectional messaging between plugin and WebView - Lifecycle management: show, hide, reload, destroy - Lifecycle events: pageFinished, titleChanged, closed --- package-lock.json | 11 + package.json | 4 +- src/index.d.ts | 38 +- src/lib/acode.js | 2 + src/lib/webview.js | 155 +++++++ src/plugins/webview/package.json | 11 + src/plugins/webview/plugin.xml | 26 ++ .../com/foxdebug/webview/WebViewActivity.java | 198 ++++++++ .../com/foxdebug/webview/WebViewInstance.java | 423 ++++++++++++++++++ .../com/foxdebug/webview/WebViewPlugin.java | 266 +++++++++++ src/plugins/webview/www/webview.js | 88 ++++ 11 files changed, 1220 insertions(+), 2 deletions(-) create mode 100644 src/lib/webview.js create mode 100644 src/plugins/webview/package.json create mode 100644 src/plugins/webview/plugin.xml create mode 100644 src/plugins/webview/src/android/com/foxdebug/webview/WebViewActivity.java create mode 100644 src/plugins/webview/src/android/com/foxdebug/webview/WebViewInstance.java create mode 100644 src/plugins/webview/src/android/com/foxdebug/webview/WebViewPlugin.java create mode 100644 src/plugins/webview/www/webview.js diff --git a/package-lock.json b/package-lock.json index 50540a937..3af595deb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,6 +111,7 @@ "com.foxdebug.acode.rk.plugin.plugincontext": "file:src/plugins/pluginContext", "cordova-android": "^15.0.0", "cordova-clipboard": "^1.3.0", + "cordova-plugin-acode-webview": "file:src/plugins/webview", "cordova-plugin-advanced-http": "file:src/plugins/cordova-plugin-advanced-http", "cordova-plugin-browser": "file:src/plugins/browser", "cordova-plugin-buildinfo": "file:src/plugins/cordova-plugin-buildinfo", @@ -8242,6 +8243,10 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/cordova-plugin-acode-webview": { + "resolved": "src/plugins/webview", + "link": true + }, "node_modules/cordova-plugin-advanced-http": { "resolved": "src/plugins/cordova-plugin-advanced-http", "link": true @@ -18565,6 +18570,12 @@ "version": "0.0.1", "dev": true, "license": "Apache-2.0" + }, + "src/plugins/webview": { + "name": "cordova-plugin-acode-webview", + "version": "1.0.0", + "dev": true, + "license": "Apache-2.0" } } } diff --git a/package.json b/package.json index 39fc2f685..e7de0b9f3 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "com.foxdebug.acode.rk.customtabs": {}, "cordova-plugin-system": {}, "cordova-plugin-crashhandler": {}, - "cordova-plugin-advanced-http": {} + "cordova-plugin-advanced-http": {}, + "cordova-plugin-acode-webview": {} }, "platforms": [ "android" @@ -84,6 +85,7 @@ "com.foxdebug.acode.rk.plugin.plugincontext": "file:src/plugins/pluginContext", "cordova-android": "^15.0.0", "cordova-clipboard": "^1.3.0", + "cordova-plugin-acode-webview": "file:src/plugins/webview", "cordova-plugin-advanced-http": "file:src/plugins/cordova-plugin-advanced-http", "cordova-plugin-browser": "file:src/plugins/browser", "cordova-plugin-buildinfo": "file:src/plugins/cordova-plugin-buildinfo", diff --git a/src/index.d.ts b/src/index.d.ts index 7f6cbaeb2..2b0c2b1e1 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -8,7 +8,10 @@ declare const PLUGIN_DIR: string; declare const KEYBINDING_FILE: string; declare const ANDROID_SDK_INT: number; declare const DOES_SUPPORT_THEME: boolean; -declare const acode: object; +declare const acode: { + webview: AcodeWebViewAPI; + [key: string]: unknown; +}; interface Window { ASSETS_DIRECTORY: string; @@ -107,3 +110,36 @@ interface AcodeFile { declare global { var Executor: Executor | undefined; } + +interface WebViewOptions { + title?: string; + mode?: "fullscreen" | "window" | "panel" | "hidden"; + width?: number; + height?: number; + x?: number; + y?: number; + allowNavigation?: boolean; + allowDownloads?: boolean; + visible?: boolean; +} + +interface AcodeWebView { + readonly id: string; + readonly options: WebViewOptions; + loadURL(url: string): Promise; + loadHTML(html: string): Promise; + evaluate(js: string): Promise; + onMessage(callback: (message: unknown) => void): void; + offMessage(callback: (message: unknown) => void): void; + on(event: string, callback: (event: string, data?: unknown) => void): void; + off(event: string, callback: (event: string, data?: unknown) => void): void; + postMessage(message: unknown): Promise; + show(): Promise; + hide(): Promise; + reload(): Promise; + destroy(): Promise; +} + +interface AcodeWebViewAPI { + create(options?: WebViewOptions): Promise; +} diff --git a/src/lib/acode.js b/src/lib/acode.js index 81c82f08a..341e2d7de 100644 --- a/src/lib/acode.js +++ b/src/lib/acode.js @@ -73,6 +73,7 @@ import encodings, { decode, encode } from "utils/encodings"; import helpers from "utils/helpers"; import KeyboardEvent from "utils/keyboardEvent"; import Url from "utils/Url"; +import webview from "./webview"; import config from "./config"; class Acode { @@ -396,6 +397,7 @@ class Acode { this.define("selectionMenu", selectionMenu); this.define("sidebarApps", sidebarAppsModule); this.define("terminal", terminalModule); + this.define("webview", webview); this.define("codemirror", codemirrorModule); this.define("@codemirror/autocomplete", cmAutocomplete); this.define("@codemirror/commands", cmCommands); diff --git a/src/lib/webview.js b/src/lib/webview.js new file mode 100644 index 000000000..b237fe38d --- /dev/null +++ b/src/lib/webview.js @@ -0,0 +1,155 @@ +import nativeBridge from "../plugins/webview/www/webview"; + +let initialized = false; +const instances = new Map(); +const eventCallbacks = new Map(); + +function ensureInit() { + if (!initialized) { + nativeBridge.setMessageCallback((payload) => { + const { id, message, event, data } = payload; + + if (event) { + const callbacks = eventCallbacks.get(id); + if (callbacks) { + callbacks.forEach((cb) => { + try { + cb(event, data); + } catch (e) { + console.error("WebView event callback error:", e); + } + }); + } + return; + } + + if (message !== undefined) { + const instance = instances.get(id); + if (instance && instance._messageCallbacks) { + let parsed = message; + try { + parsed = JSON.parse(message); + } catch (_) {} + instance._messageCallbacks.forEach((cb) => { + try { + cb(parsed); + } catch (e) { + console.error("WebView message callback error:", e); + } + }); + } + } + }); + initialized = true; + } +} + +class WebView { + constructor(id, options = {}) { + this.id = id; + this.options = options; + this._messageCallbacks = []; + this._eventCallbacks = []; + this._destroyed = false; + + instances.set(id, this); + eventCallbacks.set(id, this._eventCallbacks); + } + + async loadURL(url) { + this._checkDestroyed(); + await nativeBridge.loadURL(this.id, url); + } + + async loadHTML(html) { + this._checkDestroyed(); + await nativeBridge.loadHTML(this.id, html); + } + + async evaluate(js) { + this._checkDestroyed(); + return await nativeBridge.evaluate(this.id, js); + } + + onMessage(callback) { + this._checkDestroyed(); + if (typeof callback === "function") { + this._messageCallbacks.push(callback); + } + } + + offMessage(callback) { + this._messageCallbacks = this._messageCallbacks.filter((cb) => cb !== callback); + } + + on(event, callback) { + this._checkDestroyed(); + if (typeof callback === "function") { + this._eventCallbacks.push({ event, callback }); + } + } + + off(event, callback) { + this._eventCallbacks = this._eventCallbacks.filter( + (cb) => !(cb.event === event && cb.callback === callback) + ); + } + + async postMessage(message) { + this._checkDestroyed(); + await nativeBridge.postMessage(this.id, message); + } + + async show() { + this._checkDestroyed(); + await nativeBridge.show(this.id); + } + + async hide() { + this._checkDestroyed(); + await nativeBridge.hide(this.id); + } + + async reload() { + this._checkDestroyed(); + await nativeBridge.reload(this.id); + } + + async destroy() { + this._checkDestroyed(); + this._destroyed = true; + await nativeBridge.destroy(this.id); + instances.delete(this.id); + eventCallbacks.delete(this.id); + this._messageCallbacks = []; + this._eventCallbacks = []; + } + + _checkDestroyed() { + if (this._destroyed) { + throw new Error("WebView has been destroyed"); + } + } +} + +const webviewAPI = { + async create(options = {}) { + ensureInit(); + + const id = await nativeBridge.create({ + title: options.title || "", + mode: options.mode || "hidden", + width: options.width || 0, + height: options.height || 0, + x: options.x || 0, + y: options.y || 0, + allowNavigation: options.allowNavigation !== false, + allowDownloads: options.allowDownloads === true, + visible: options.visible !== false, + }); + + return new WebView(id, options); + }, +}; + +export default webviewAPI; diff --git a/src/plugins/webview/package.json b/src/plugins/webview/package.json new file mode 100644 index 000000000..673be361c --- /dev/null +++ b/src/plugins/webview/package.json @@ -0,0 +1,11 @@ +{ + "name": "cordova-plugin-acode-webview", + "version": "1.0.0", + "description": "Acode WebView Plugin API", + "main": "", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "Apache-2.0" +} diff --git a/src/plugins/webview/plugin.xml b/src/plugins/webview/plugin.xml new file mode 100644 index 000000000..c7a6ff773 --- /dev/null +++ b/src/plugins/webview/plugin.xml @@ -0,0 +1,26 @@ + + + cordova-plugin-acode-webview + Acode WebView Plugin API - Create and manage isolated WebView instances + Apache 2.0 + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/webview/src/android/com/foxdebug/webview/WebViewActivity.java b/src/plugins/webview/src/android/com/foxdebug/webview/WebViewActivity.java new file mode 100644 index 000000000..7c9c337f8 --- /dev/null +++ b/src/plugins/webview/src/android/com/foxdebug/webview/WebViewActivity.java @@ -0,0 +1,198 @@ +package com.foxdebug.webview; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.Insets; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowInsetsController; +import android.webkit.JavascriptInterface; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.graphics.drawable.GradientDrawable; + +public class WebViewActivity extends Activity { + + private static final String TAG = "WebViewActivity"; + private static WebViewPlugin plugin; + + private WebView webView; + private String webviewId; + private String title; + private boolean allowNavigation; + private boolean allowDownloads; + + public static void setPlugin(WebViewPlugin p) { + plugin = p; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Intent intent = getIntent(); + webviewId = intent.getStringExtra("webviewId"); + title = intent.getStringExtra("title"); + allowNavigation = intent.getBooleanExtra("allowNavigation", true); + allowDownloads = intent.getBooleanExtra("allowDownloads", false); + + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )); + + TextView titleBar = new TextView(this); + titleBar.setText(title != null ? title : "WebView"); + titleBar.setTextColor(Color.WHITE); + titleBar.setPadding(dpToPx(16), dpToPx(8), dpToPx(16), dpToPx(8)); + titleBar.setTextSize(16); + titleBar.setBackgroundColor(Color.argb(255, 33, 33, 33)); + + GradientDrawable closeBg = new GradientDrawable(); + closeBg.setColor(Color.argb(255, 80, 80, 80)); + closeBg.setCornerRadius(dpToPx(4)); + + TextView closeButton = new TextView(this); + closeButton.setText("✕"); + closeButton.setTextColor(Color.WHITE); + closeButton.setTextSize(18); + closeButton.setPadding(dpToPx(12), dpToPx(4), dpToPx(12), dpToPx(4)); + closeButton.setBackground(closeBg); + closeButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); + + LinearLayout titleLayout = new LinearLayout(this); + titleLayout.setOrientation(LinearLayout.HORIZONTAL); + titleLayout.setBackgroundColor(Color.argb(255, 33, 33, 33)); + titleLayout.setPadding(dpToPx(12), 0, dpToPx(12), 0); + + LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams( + 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1 + ); + titleLayout.addView(titleBar, titleParams); + titleLayout.addView(closeButton, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + )); + + webView = new WebView(this); + webView.setWebViewClient(new FullscreenWebViewClient()); + webView.addJavascriptInterface(new JsBridge(), "AcodeWebViewNative"); + + WebSettings settings = webView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setDomStorageEnabled(true); + settings.setAllowContentAccess(true); + settings.setDisplayZoomControls(false); + settings.setLoadWithOverviewMode(true); + settings.setUseWideViewPort(true); + settings.setAllowFileAccess(true); + + String bridgeJs = + "(function() {" + + " window.webview = window.webview || {};" + + " window.webview._callbacks = [];" + + " window.webview.onMessage = function(cb) { window.webview._callbacks.push(cb); };" + + " window.webview.postMessage = function(msg) {" + + " var data = typeof msg === 'string' ? msg : JSON.stringify(msg);" + + " window.AcodeWebViewNative.postMessage(data);" + + " };" + + " window.webview.offMessage = function(cb) {" + + " window.webview._callbacks = window.webview._callbacks.filter(function(c) { return c !== cb; });" + + " };" + + "})();"; + + webView.evaluateJavascript(bridgeJs, null); + + FrameLayout webViewContainer = new FrameLayout(this); + webViewContainer.setBackgroundColor(Color.WHITE); + webViewContainer.addView(webView, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )); + + layout.addView(titleLayout); + layout.addView(webViewContainer, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + 0, 1 + )); + + setContentView(layout); + + if (Build.VERSION.SDK_INT >= 30) { + getWindow().setDecorFitsSystemWindows(false); + } + + WebViewInstance instance = plugin != null ? plugin.getInstance(webviewId) : null; + if (instance != null) { + instance.createWebView(this); + webView = instance.getWebView(); + webViewContainer.removeAllViews(); + webViewContainer.addView(webView, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )); + } + } + + @Override + public void onBackPressed() { + if (webView != null && webView.canGoBack()) { + webView.goBack(); + } else { + finish(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (plugin != null) { + plugin.sendEventToCordova(webviewId, "closed", null); + } + } + + private int dpToPx(int dp) { + return (int) (dp * getResources().getDisplayMetrics().density); + } + + private class FullscreenWebViewClient extends WebViewClient { + @Override + public boolean shouldOverrideUrlLoading(WebView view, android.webkit.WebResourceRequest request) { + if (!allowNavigation) { + return true; + } + return false; + } + } + + private class JsBridge { + @JavascriptInterface + public void postMessage(String message) { + if (plugin != null) { + plugin.sendMessageToCordova(webviewId, message); + } + } + + @JavascriptInterface + public String getWebViewId() { + return webviewId; + } + } +} diff --git a/src/plugins/webview/src/android/com/foxdebug/webview/WebViewInstance.java b/src/plugins/webview/src/android/com/foxdebug/webview/WebViewInstance.java new file mode 100644 index 000000000..6844c6b13 --- /dev/null +++ b/src/plugins/webview/src/android/com/foxdebug/webview/WebViewInstance.java @@ -0,0 +1,423 @@ +package com.foxdebug.webview; + +import android.app.Activity; +import android.content.DialogInterface; +import android.graphics.Color; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.webkit.DownloadListener; +import android.webkit.JavascriptInterface; +import android.webkit.URLUtil; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceRequest; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.app.AlertDialog; +import android.app.DownloadManager; +import android.content.Context; +import android.net.Uri; +import android.os.Environment; +import android.widget.FrameLayout; +import android.widget.Toast; +import org.apache.cordova.CallbackContext; +import org.json.JSONException; +import org.json.JSONObject; + +public class WebViewInstance { + + private static final String TAG = "WebViewInstance"; + + final String id; + final String mode; + final String title; + final int width; + final int height; + final int x; + final int y; + final boolean allowNavigation; + final boolean allowDownloads; + final boolean initiallyVisible; + final WebViewPlugin plugin; + + private WebView webView; + private FrameLayout container; + private boolean isDestroyed = false; + + WebViewInstance( + String id, String mode, String title, + int width, int height, int x, int y, + boolean allowNavigation, boolean allowDownloads, + boolean initiallyVisible, + Activity activity, + WebViewPlugin plugin + ) { + this.id = id; + this.mode = mode; + this.title = title; + this.width = width; + this.height = height; + this.x = x; + this.y = y; + this.allowNavigation = allowNavigation; + this.allowDownloads = allowDownloads; + this.initiallyVisible = initiallyVisible; + this.plugin = plugin; + } + + public WebView getWebView() { + return webView; + } + + void createWebView(Activity activity) { + webView = new WebView(activity); + + WebSettings settings = webView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setDomStorageEnabled(true); + settings.setAllowContentAccess(true); + settings.setDisplayZoomControls(false); + settings.setLoadWithOverviewMode(true); + settings.setUseWideViewPort(true); + settings.setAllowFileAccess(true); + settings.setAllowFileAccessFromFileURLs(true); + settings.setAllowUniversalAccessFromFileURLs(true); + + webView.setWebViewClient(new InstanceWebViewClient()); + webView.setWebChromeClient(new InstanceWebChromeClient()); + webView.setFocusable(true); + webView.setFocusableInTouchMode(true); + + webView.addJavascriptInterface(new JsBridge(), "AcodeWebViewNative"); + + if (allowDownloads) { + webView.setDownloadListener(new InstanceDownloadListener(activity)); + } + + String bridgeJs = + "(function() {" + + " window.webview = window.webview || {};" + + " window.webview._callbacks = [];" + + " window.webview.onMessage = function(cb) { window.webview._callbacks.push(cb); };" + + " window.webview.postMessage = function(msg) {" + + " var data = typeof msg === 'string' ? msg : JSON.stringify(msg);" + + " window.AcodeWebViewNative.postMessage(data);" + + " };" + + " window.webview.offMessage = function(cb) {" + + " window.webview._callbacks = window.webview._callbacks.filter(function(c) { return c !== cb; });" + + " };" + + "})();"; + + webView.evaluateJavascript(bridgeJs, null); + } + + void attachToActivity(Activity activity) { + container = new FrameLayout(activity); + + FrameLayout.LayoutParams webViewParams = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ); + + container.setBackgroundColor(Color.argb(180, 0, 0, 0)); + + if (mode.equals("window")) { + int w = width > 0 ? width : ViewGroup.LayoutParams.WRAP_CONTENT; + int h = height > 0 ? height : ViewGroup.LayoutParams.WRAP_CONTENT; + + webViewParams = new FrameLayout.LayoutParams( + w > 0 ? dpToPx(activity, w) : ViewGroup.LayoutParams.MATCH_PARENT, + h > 0 ? dpToPx(activity, h) : ViewGroup.LayoutParams.MATCH_PARENT + ); + webViewParams.gravity = Gravity.CENTER; + webViewParams.setMargins(dpToPx(activity, 16), dpToPx(activity, 48), dpToPx(activity, 16), dpToPx(activity, 48)); + + container.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + hideInner(); + } + }); + } else if (mode.equals("panel")) { + webViewParams = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + height > 0 ? dpToPx(activity, height) : (int)(getScreenHeight(activity) * 0.4) + ); + webViewParams.gravity = Gravity.BOTTOM; + } + + container.addView(webView, webViewParams); + + ViewGroup rootView = activity.findViewById(android.R.id.content); + if (rootView instanceof FrameLayout) { + rootView.addView(container, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )); + } + } + + void loadURL(String url) { + if (isDestroyed || webView == null) return; + final String safeUrl = (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://")) + ? url : "http://" + url; + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + webView.loadUrl(safeUrl); + } + }); + } + + void loadHTML(String html) { + if (isDestroyed || webView == null) return; + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + webView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null); + } + }); + } + + void evaluate(String js, final CallbackContext callbackContext) { + if (isDestroyed || webView == null) { + callbackContext.error("WebView destroyed"); + return; + } + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + webView.evaluateJavascript(js, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + if (value != null && value.startsWith("\"") && value.endsWith("\"")) { + value = value.substring(1, value.length() - 1) + .replace("\\\"", "\"") + .replace("\\\\", "\\"); + } + callbackContext.success(value); + } + }); + } + }); + } + + void postMessage(String message, CallbackContext callbackContext) { + if (isDestroyed || webView == null) { + callbackContext.error("WebView destroyed"); + return; + } + String escaped = message + .replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\n", "\\n") + .replace("\r", "\\r"); + + String js = "if(window.webview&&window.webview._callbacks){" + + "var msg=" + safeParseJSON(message) + ";" + + "window.webview._callbacks.forEach(function(cb){try{cb(msg)}catch(e){console.error(e)}});" + + "}"; + + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + webView.evaluateJavascript(js, null); + callbackContext.success(); + } + }); + } + + private String safeParseJSON(String message) { + try { + new JSONObject(message); + return message; + } catch (JSONException e1) { + try { + new org.json.JSONArray(message); + return message; + } catch (JSONException e2) { + return "\"" + message.replace("\"", "\\\"").replace("\n", "\\n") + "\""; + } + } + } + + void show(final CallbackContext callbackContext) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (isDestroyed || container == null) { + if (callbackContext != null) callbackContext.error("Cannot show"); + return; + } + container.setVisibility(View.VISIBLE); + if (callbackContext != null) callbackContext.success(); + } + }); + } + + void hide(final CallbackContext callbackContext) { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (isDestroyed || container == null) { + if (callbackContext != null) callbackContext.error("Cannot hide"); + return; + } + container.setVisibility(View.GONE); + if (callbackContext != null) callbackContext.success(); + } + }); + } + + private void hideInner() { + if (container != null) { + container.setVisibility(View.GONE); + } + } + + void reload(CallbackContext callbackContext) { + if (isDestroyed || webView == null) { + callbackContext.error("WebView destroyed"); + return; + } + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + webView.reload(); + callbackContext.success(); + } + }); + } + + void destroy() { + isDestroyed = true; + + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + if (container != null && container.getParent() != null) { + ((ViewGroup) container.getParent()).removeView(container); + } + if (webView != null) { + webView.removeJavascriptInterface("AcodeWebViewNative"); + webView.setWebViewClient(null); + webView.setWebChromeClient(null); + webView.loadUrl("about:blank"); + webView.destroy(); + } + webView = null; + container = null; + } + }); + } + + void onPageFinished() { + try { + JSONObject data = new JSONObject(); + data.put("url", webView.getUrl()); + data.put("title", webView.getTitle()); + plugin.sendEventToCordova(id, "pageFinished", data); + } catch (Exception e) { + Log.e(TAG, "onPageFinished error", e); + } + } + + private static int dpToPx(Context context, int dp) { + return (int) (dp * context.getResources().getDisplayMetrics().density); + } + + private static int getScreenHeight(Activity activity) { + return activity.getResources().getDisplayMetrics().heightPixels; + } + + private class InstanceWebViewClient extends WebViewClient { + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + if (!allowNavigation) { + return true; + } + return false; + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + WebViewInstance.this.onPageFinished(); + } + } + + private class InstanceWebChromeClient extends WebChromeClient { + @Override + public void onReceivedTitle(WebView view, String pageTitle) { + super.onReceivedTitle(view, pageTitle); + try { + JSONObject data = new JSONObject(); + data.put("title", pageTitle); + plugin.sendEventToCordova(id, "titleChanged", data); + } catch (Exception e) { + Log.e(TAG, "onReceivedTitle error", e); + } + } + } + + private class InstanceDownloadListener implements DownloadListener { + private final Context context; + + InstanceDownloadListener(Context context) { + this.context = context; + } + + @Override + public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimeType, long contentLength) { + final String fileName = URLUtil.guessFileName(url, contentDisposition, mimeType); + + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + new AlertDialog.Builder(context) + .setTitle("Download file") + .setMessage("Do you want to download \"" + fileName + "\"?") + .setPositiveButton("Yes", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url)); + request.setMimeType(mimeType); + request.addRequestHeader("User-Agent", userAgent); + request.setDescription("Downloading file..."); + request.setTitle(fileName); + request.allowScanningByMediaScanner(); + request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName); + + DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); + dm.enqueue(request); + Toast.makeText(context, "Download started...", Toast.LENGTH_SHORT).show(); + } + }) + .setNegativeButton("Cancel", null) + .show(); + } + }); + } + } + + public class JsBridge { + @JavascriptInterface + public void postMessage(String message) { + plugin.sendMessageToCordova(id, message); + } + + @JavascriptInterface + public String getWebViewId() { + return id; + } + } +} diff --git a/src/plugins/webview/src/android/com/foxdebug/webview/WebViewPlugin.java b/src/plugins/webview/src/android/com/foxdebug/webview/WebViewPlugin.java new file mode 100644 index 000000000..e060d007f --- /dev/null +++ b/src/plugins/webview/src/android/com/foxdebug/webview/WebViewPlugin.java @@ -0,0 +1,266 @@ +package com.foxdebug.webview; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Color; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.webkit.JavascriptInterface; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceRequest; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.FrameLayout; +import java.util.HashMap; +import java.util.UUID; +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class WebViewPlugin extends CordovaPlugin { + + private static final String TAG = "AcodeWebView"; + private static WebViewPlugin instance; + + private final HashMap instances = new HashMap<>(); + private CallbackContext messageCallback; + + @Override + protected void pluginInitialize() { + instance = this; + } + + public static WebViewPlugin getInstance() { + return instance; + } + + @Override + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + try { + switch (action) { + case "setMessageCallback": + this.messageCallback = callbackContext; + PluginResult keepResult = new PluginResult(PluginResult.Status.NO_RESULT); + keepResult.setKeepCallback(true); + callbackContext.sendPluginResult(keepResult); + return true; + case "create": + create(args.getJSONObject(0), callbackContext); + return true; + case "loadURL": + loadURL(args.getString(0), args.getString(1), callbackContext); + return true; + case "loadHTML": + loadHTML(args.getString(0), args.getString(1), callbackContext); + return true; + case "evaluate": + evaluate(args.getString(0), args.getString(1), callbackContext); + return true; + case "postMessage": + postMessage(args.getString(0), args.getString(1), callbackContext); + return true; + case "show": + show(args.getString(0), callbackContext); + return true; + case "hide": + hide(args.getString(0), callbackContext); + return true; + case "reload": + reload(args.getString(0), callbackContext); + return true; + case "destroy": + destroy(args.getString(0), callbackContext); + return true; + default: + callbackContext.error("Unknown action: " + action); + return true; + } + } catch (Exception e) { + Log.e(TAG, "Error: " + action, e); + callbackContext.error(e.getMessage()); + } + return true; + } + + private String generateId() { + return "wv_" + UUID.randomUUID().toString().replace("-", "").substring(0, 12); + } + + private void create(JSONObject options, final CallbackContext callbackContext) throws JSONException { + final String id = generateId(); + final String mode = options.optString("mode", "hidden"); + final String title = options.optString("title", "WebView"); + final int width = options.optInt("width", 0); + final int height = options.optInt("height", 0); + final int x = options.optInt("x", 0); + final int y = options.optInt("y", 0); + final boolean allowNavigation = options.optBoolean("allowNavigation", true); + final boolean allowDownloads = options.optBoolean("allowDownloads", false); + final boolean visible = options.optBoolean("visible", true); + + final String effectiveMode = visible ? mode : "hidden"; + + cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + try { + WebViewInstance instance = new WebViewInstance( + id, effectiveMode, title, + width, height, x, y, + allowNavigation, allowDownloads, + visible, cordova.getActivity(), + WebViewPlugin.this + ); + + if (effectiveMode.equals("fullscreen")) { + instances.put(id, instance); + launchFullscreenActivity(id, title, allowNavigation, allowDownloads); + callbackContext.success(id); + return; + } + + instance.createWebView(cordova.getActivity()); + + if (effectiveMode.equals("window") || effectiveMode.equals("panel")) { + instance.attachToActivity(cordova.getActivity()); + } + + instances.put(id, instance); + callbackContext.success(id); + } catch (Exception e) { + Log.e(TAG, "Create error: " + e.getMessage(), e); + callbackContext.error(e.getMessage()); + } + } + }); + } + + private void launchFullscreenActivity( + String id, String title, boolean allowNavigation, boolean allowDownloads + ) { + Intent intent = new Intent(cordova.getActivity(), WebViewActivity.class); + intent.putExtra("webviewId", id); + intent.putExtra("title", title); + intent.putExtra("allowNavigation", allowNavigation); + intent.putExtra("allowDownloads", allowDownloads); + cordova.getActivity().startActivity(intent); + } + + public WebViewInstance getInstance(String id) { + return instances.get(id); + } + + private void loadURL(String id, String url, CallbackContext callbackContext) { + WebViewInstance instance = getInstance(id); + if (instance == null) { callbackContext.error("WebView not found: " + id); return; } + instance.loadURL(url); + callbackContext.success(); + } + + private void loadHTML(String id, String html, CallbackContext callbackContext) { + WebViewInstance instance = getInstance(id); + if (instance == null) { callbackContext.error("WebView not found: " + id); return; } + instance.loadHTML(html); + callbackContext.success(); + } + + private void evaluate(String id, String js, CallbackContext callbackContext) { + WebViewInstance instance = getInstance(id); + if (instance == null) { callbackContext.error("WebView not found: " + id); return; } + instance.evaluate(js, callbackContext); + } + + private void postMessage(String id, String message, CallbackContext callbackContext) { + WebViewInstance instance = getInstance(id); + if (instance == null) { callbackContext.error("WebView not found: " + id); return; } + instance.postMessage(message, callbackContext); + } + + private void show(String id, CallbackContext callbackContext) { + WebViewInstance instance = getInstance(id); + if (instance == null) { callbackContext.error("WebView not found: " + id); return; } + instance.show(callbackContext); + } + + private void hide(String id, CallbackContext callbackContext) { + WebViewInstance instance = getInstance(id); + if (instance == null) { callbackContext.error("WebView not found: " + id); return; } + instance.hide(callbackContext); + } + + private void reload(String id, CallbackContext callbackContext) { + WebViewInstance instance = getInstance(id); + if (instance == null) { callbackContext.error("WebView not found: " + id); return; } + instance.reload(callbackContext); + } + + private void destroy(String id, CallbackContext callbackContext) { + final WebViewInstance instance = instances.remove(id); + if (instance == null) { callbackContext.error("WebView not found: " + id); return; } + + cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + instance.destroy(); + callbackContext.success(); + } + }); + } + + public void sendMessageToCordova(String id, String message) { + if (messageCallback == null) return; + + try { + JSONObject payload = new JSONObject(); + payload.put("id", id); + payload.put("message", message); + + PluginResult result = new PluginResult(PluginResult.Status.OK, payload); + result.setKeepCallback(true); + messageCallback.sendPluginResult(result); + } catch (JSONException e) { + Log.e(TAG, "Error building message payload", e); + } + } + + public void sendEventToCordova(String id, String event, JSONObject data) { + if (messageCallback == null) return; + + try { + JSONObject payload = new JSONObject(); + payload.put("id", id); + payload.put("event", event); + if (data != null) { + payload.put("data", data); + } + + PluginResult result = new PluginResult(PluginResult.Status.OK, payload); + result.setKeepCallback(true); + messageCallback.sendPluginResult(result); + } catch (JSONException e) { + Log.e(TAG, "Error building event payload", e); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + for (WebViewInstance instance : instances.values()) { + try { instance.destroy(); } catch (Exception ignored) {} + } + instances.clear(); + instance = null; + } +} diff --git a/src/plugins/webview/www/webview.js b/src/plugins/webview/www/webview.js new file mode 100644 index 000000000..b1935dddf --- /dev/null +++ b/src/plugins/webview/www/webview.js @@ -0,0 +1,88 @@ +const SERVICE = "AcodeWebView"; + +let messageCallback = null; + +function setMessageCallback(callback) { + messageCallback = callback; + cordova.exec( + (payload) => { + if (messageCallback) { + messageCallback(payload); + } + }, + (error) => { + console.error("WebView message callback error:", error); + }, + SERVICE, + "setMessageCallback", + [] + ); +} + +function create(options) { + return new Promise((resolve, reject) => { + cordova.exec(resolve, reject, SERVICE, "create", [options || {}]); + }); +} + +function loadURL(id, url) { + return new Promise((resolve, reject) => { + cordova.exec(resolve, reject, SERVICE, "loadURL", [id, url]); + }); +} + +function loadHTML(id, html) { + return new Promise((resolve, reject) => { + cordova.exec(resolve, reject, SERVICE, "loadHTML", [id, html]); + }); +} + +function evaluate(id, js) { + return new Promise((resolve, reject) => { + cordova.exec(resolve, reject, SERVICE, "evaluate", [id, js]); + }); +} + +function postMessage(id, message) { + return new Promise((resolve, reject) => { + const payload = typeof message === "string" ? message : JSON.stringify(message); + cordova.exec(resolve, reject, SERVICE, "postMessage", [id, payload]); + }); +} + +function show(id) { + return new Promise((resolve, reject) => { + cordova.exec(resolve, reject, SERVICE, "show", [id]); + }); +} + +function hide(id) { + return new Promise((resolve, reject) => { + cordova.exec(resolve, reject, SERVICE, "hide", [id]); + }); +} + +function reload(id) { + return new Promise((resolve, reject) => { + cordova.exec(resolve, reject, SERVICE, "reload", [id]); + }); +} + +function destroy(id) { + return new Promise((resolve, reject) => { + cordova.exec(resolve, reject, SERVICE, "destroy", [id]); + }); +} + +export default { + setMessageCallback, + create, + loadURL, + loadHTML, + evaluate, + postMessage, + show, + hide, + reload, + destroy, +}; From fdbca224b49949e0bfe2dcfd38170f0dc3be1188 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 22 Jul 2026 19:00:34 +0530 Subject: [PATCH 2/4] format --- src/lib/acode.js | 2 +- src/lib/webview.js | 280 +++++++++++++++++++++++---------------------- 2 files changed, 142 insertions(+), 140 deletions(-) diff --git a/src/lib/acode.js b/src/lib/acode.js index 341e2d7de..3e0ecf694 100644 --- a/src/lib/acode.js +++ b/src/lib/acode.js @@ -73,8 +73,8 @@ import encodings, { decode, encode } from "utils/encodings"; import helpers from "utils/helpers"; import KeyboardEvent from "utils/keyboardEvent"; import Url from "utils/Url"; -import webview from "./webview"; import config from "./config"; +import webview from "./webview"; class Acode { #modules = {}; diff --git a/src/lib/webview.js b/src/lib/webview.js index b237fe38d..70965c27b 100644 --- a/src/lib/webview.js +++ b/src/lib/webview.js @@ -5,151 +5,153 @@ const instances = new Map(); const eventCallbacks = new Map(); function ensureInit() { - if (!initialized) { - nativeBridge.setMessageCallback((payload) => { - const { id, message, event, data } = payload; - - if (event) { - const callbacks = eventCallbacks.get(id); - if (callbacks) { - callbacks.forEach((cb) => { - try { - cb(event, data); - } catch (e) { - console.error("WebView event callback error:", e); - } - }); - } - return; - } - - if (message !== undefined) { - const instance = instances.get(id); - if (instance && instance._messageCallbacks) { - let parsed = message; - try { - parsed = JSON.parse(message); - } catch (_) {} - instance._messageCallbacks.forEach((cb) => { - try { - cb(parsed); - } catch (e) { - console.error("WebView message callback error:", e); - } - }); - } - } - }); - initialized = true; - } + if (!initialized) { + nativeBridge.setMessageCallback((payload) => { + const { id, message, event, data } = payload; + + if (event) { + const callbacks = eventCallbacks.get(id); + if (callbacks) { + callbacks.forEach((cb) => { + try { + cb(event, data); + } catch (e) { + console.error("WebView event callback error:", e); + } + }); + } + return; + } + + if (message !== undefined) { + const instance = instances.get(id); + if (instance && instance._messageCallbacks) { + let parsed = message; + try { + parsed = JSON.parse(message); + } catch (_) {} + instance._messageCallbacks.forEach((cb) => { + try { + cb(parsed); + } catch (e) { + console.error("WebView message callback error:", e); + } + }); + } + } + }); + initialized = true; + } } class WebView { - constructor(id, options = {}) { - this.id = id; - this.options = options; - this._messageCallbacks = []; - this._eventCallbacks = []; - this._destroyed = false; - - instances.set(id, this); - eventCallbacks.set(id, this._eventCallbacks); - } - - async loadURL(url) { - this._checkDestroyed(); - await nativeBridge.loadURL(this.id, url); - } - - async loadHTML(html) { - this._checkDestroyed(); - await nativeBridge.loadHTML(this.id, html); - } - - async evaluate(js) { - this._checkDestroyed(); - return await nativeBridge.evaluate(this.id, js); - } - - onMessage(callback) { - this._checkDestroyed(); - if (typeof callback === "function") { - this._messageCallbacks.push(callback); - } - } - - offMessage(callback) { - this._messageCallbacks = this._messageCallbacks.filter((cb) => cb !== callback); - } - - on(event, callback) { - this._checkDestroyed(); - if (typeof callback === "function") { - this._eventCallbacks.push({ event, callback }); - } - } - - off(event, callback) { - this._eventCallbacks = this._eventCallbacks.filter( - (cb) => !(cb.event === event && cb.callback === callback) - ); - } - - async postMessage(message) { - this._checkDestroyed(); - await nativeBridge.postMessage(this.id, message); - } - - async show() { - this._checkDestroyed(); - await nativeBridge.show(this.id); - } - - async hide() { - this._checkDestroyed(); - await nativeBridge.hide(this.id); - } - - async reload() { - this._checkDestroyed(); - await nativeBridge.reload(this.id); - } - - async destroy() { - this._checkDestroyed(); - this._destroyed = true; - await nativeBridge.destroy(this.id); - instances.delete(this.id); - eventCallbacks.delete(this.id); - this._messageCallbacks = []; - this._eventCallbacks = []; - } - - _checkDestroyed() { - if (this._destroyed) { - throw new Error("WebView has been destroyed"); - } - } + constructor(id, options = {}) { + this.id = id; + this.options = options; + this._messageCallbacks = []; + this._eventCallbacks = []; + this._destroyed = false; + + instances.set(id, this); + eventCallbacks.set(id, this._eventCallbacks); + } + + async loadURL(url) { + this._checkDestroyed(); + await nativeBridge.loadURL(this.id, url); + } + + async loadHTML(html) { + this._checkDestroyed(); + await nativeBridge.loadHTML(this.id, html); + } + + async evaluate(js) { + this._checkDestroyed(); + return await nativeBridge.evaluate(this.id, js); + } + + onMessage(callback) { + this._checkDestroyed(); + if (typeof callback === "function") { + this._messageCallbacks.push(callback); + } + } + + offMessage(callback) { + this._messageCallbacks = this._messageCallbacks.filter( + (cb) => cb !== callback, + ); + } + + on(event, callback) { + this._checkDestroyed(); + if (typeof callback === "function") { + this._eventCallbacks.push({ event, callback }); + } + } + + off(event, callback) { + this._eventCallbacks = this._eventCallbacks.filter( + (cb) => !(cb.event === event && cb.callback === callback), + ); + } + + async postMessage(message) { + this._checkDestroyed(); + await nativeBridge.postMessage(this.id, message); + } + + async show() { + this._checkDestroyed(); + await nativeBridge.show(this.id); + } + + async hide() { + this._checkDestroyed(); + await nativeBridge.hide(this.id); + } + + async reload() { + this._checkDestroyed(); + await nativeBridge.reload(this.id); + } + + async destroy() { + this._checkDestroyed(); + this._destroyed = true; + await nativeBridge.destroy(this.id); + instances.delete(this.id); + eventCallbacks.delete(this.id); + this._messageCallbacks = []; + this._eventCallbacks = []; + } + + _checkDestroyed() { + if (this._destroyed) { + throw new Error("WebView has been destroyed"); + } + } } const webviewAPI = { - async create(options = {}) { - ensureInit(); - - const id = await nativeBridge.create({ - title: options.title || "", - mode: options.mode || "hidden", - width: options.width || 0, - height: options.height || 0, - x: options.x || 0, - y: options.y || 0, - allowNavigation: options.allowNavigation !== false, - allowDownloads: options.allowDownloads === true, - visible: options.visible !== false, - }); - - return new WebView(id, options); - }, + async create(options = {}) { + ensureInit(); + + const id = await nativeBridge.create({ + title: options.title || "", + mode: options.mode || "hidden", + width: options.width || 0, + height: options.height || 0, + x: options.x || 0, + y: options.y || 0, + allowNavigation: options.allowNavigation !== false, + allowDownloads: options.allowDownloads === true, + visible: options.visible !== false, + }); + + return new WebView(id, options); + }, }; export default webviewAPI; From d58b26c1dcb47d26fdb019e9a859b73041fcb6fa Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 22 Jul 2026 19:03:30 +0530 Subject: [PATCH 3/4] fix: remove title bar and close button from fullscreen WebView Fullscreen mode is now a clean full-screen WebView with no chrome. Back button handles navigation and closing. --- .../com/foxdebug/webview/WebViewActivity.java | 157 +++++------------- .../com/foxdebug/webview/WebViewPlugin.java | 8 +- 2 files changed, 42 insertions(+), 123 deletions(-) diff --git a/src/plugins/webview/src/android/com/foxdebug/webview/WebViewActivity.java b/src/plugins/webview/src/android/com/foxdebug/webview/WebViewActivity.java index 7c9c337f8..00ee0f07b 100644 --- a/src/plugins/webview/src/android/com/foxdebug/webview/WebViewActivity.java +++ b/src/plugins/webview/src/android/com/foxdebug/webview/WebViewActivity.java @@ -2,35 +2,22 @@ import android.app.Activity; import android.content.Intent; -import android.graphics.Color; -import android.graphics.Insets; import android.os.Build; import android.os.Bundle; -import android.util.Log; -import android.view.View; import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowInsets; -import android.view.WindowInsetsController; import android.webkit.JavascriptInterface; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.FrameLayout; -import android.widget.LinearLayout; -import android.widget.TextView; -import android.graphics.drawable.GradientDrawable; public class WebViewActivity extends Activity { - private static final String TAG = "WebViewActivity"; private static WebViewPlugin plugin; private WebView webView; private String webviewId; - private String title; private boolean allowNavigation; - private boolean allowDownloads; public static void setPlugin(WebViewPlugin p) { plugin = p; @@ -42,113 +29,56 @@ public void onCreate(Bundle savedInstanceState) { Intent intent = getIntent(); webviewId = intent.getStringExtra("webviewId"); - title = intent.getStringExtra("title"); allowNavigation = intent.getBooleanExtra("allowNavigation", true); - allowDownloads = intent.getBooleanExtra("allowDownloads", false); - LinearLayout layout = new LinearLayout(this); - layout.setOrientation(LinearLayout.VERTICAL); - layout.setLayoutParams(new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - )); + WebViewInstance instance = plugin != null ? plugin.getInstance(webviewId) : null; - TextView titleBar = new TextView(this); - titleBar.setText(title != null ? title : "WebView"); - titleBar.setTextColor(Color.WHITE); - titleBar.setPadding(dpToPx(16), dpToPx(8), dpToPx(16), dpToPx(8)); - titleBar.setTextSize(16); - titleBar.setBackgroundColor(Color.argb(255, 33, 33, 33)); - - GradientDrawable closeBg = new GradientDrawable(); - closeBg.setColor(Color.argb(255, 80, 80, 80)); - closeBg.setCornerRadius(dpToPx(4)); - - TextView closeButton = new TextView(this); - closeButton.setText("✕"); - closeButton.setTextColor(Color.WHITE); - closeButton.setTextSize(18); - closeButton.setPadding(dpToPx(12), dpToPx(4), dpToPx(12), dpToPx(4)); - closeButton.setBackground(closeBg); - closeButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - finish(); - } - }); - - LinearLayout titleLayout = new LinearLayout(this); - titleLayout.setOrientation(LinearLayout.HORIZONTAL); - titleLayout.setBackgroundColor(Color.argb(255, 33, 33, 33)); - titleLayout.setPadding(dpToPx(12), 0, dpToPx(12), 0); - - LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams( - 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1 - ); - titleLayout.addView(titleBar, titleParams); - titleLayout.addView(closeButton, new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - )); + if (instance != null && instance.getWebView() == null) { + instance.createWebView(this); + webView = instance.getWebView(); + } else if (instance != null) { + webView = instance.getWebView(); + } else { + webView = new WebView(this); + webView.addJavascriptInterface(new JsBridge(), "AcodeWebViewNative"); + + WebSettings settings = webView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setDomStorageEnabled(true); + settings.setAllowContentAccess(true); + settings.setDisplayZoomControls(false); + settings.setLoadWithOverviewMode(true); + settings.setUseWideViewPort(true); + settings.setAllowFileAccess(true); + + webView.setWebViewClient(new FullscreenWebViewClient()); + + String bridgeJs = + "(function() {" + + " window.webview = window.webview || {};" + + " window.webview._callbacks = [];" + + " window.webview.onMessage = function(cb) { window.webview._callbacks.push(cb); };" + + " window.webview.postMessage = function(msg) {" + + " var data = typeof msg === 'string' ? msg : JSON.stringify(msg);" + + " window.AcodeWebViewNative.postMessage(data);" + + " };" + + " window.webview.offMessage = function(cb) {" + + " window.webview._callbacks = window.webview._callbacks.filter(function(c) { return c !== cb; });" + + " };" + + "})();"; + webView.evaluateJavascript(bridgeJs, null); + } - webView = new WebView(this); - webView.setWebViewClient(new FullscreenWebViewClient()); - webView.addJavascriptInterface(new JsBridge(), "AcodeWebViewNative"); - - WebSettings settings = webView.getSettings(); - settings.setJavaScriptEnabled(true); - settings.setDomStorageEnabled(true); - settings.setAllowContentAccess(true); - settings.setDisplayZoomControls(false); - settings.setLoadWithOverviewMode(true); - settings.setUseWideViewPort(true); - settings.setAllowFileAccess(true); - - String bridgeJs = - "(function() {" + - " window.webview = window.webview || {};" + - " window.webview._callbacks = [];" + - " window.webview.onMessage = function(cb) { window.webview._callbacks.push(cb); };" + - " window.webview.postMessage = function(msg) {" + - " var data = typeof msg === 'string' ? msg : JSON.stringify(msg);" + - " window.AcodeWebViewNative.postMessage(data);" + - " };" + - " window.webview.offMessage = function(cb) {" + - " window.webview._callbacks = window.webview._callbacks.filter(function(c) { return c !== cb; });" + - " };" + - "})();"; - - webView.evaluateJavascript(bridgeJs, null); - - FrameLayout webViewContainer = new FrameLayout(this); - webViewContainer.setBackgroundColor(Color.WHITE); - webViewContainer.addView(webView, new FrameLayout.LayoutParams( + FrameLayout container = new FrameLayout(this); + container.addView(webView, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT )); - - layout.addView(titleLayout); - layout.addView(webViewContainer, new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - 0, 1 - )); - - setContentView(layout); + setContentView(container); if (Build.VERSION.SDK_INT >= 30) { getWindow().setDecorFitsSystemWindows(false); } - - WebViewInstance instance = plugin != null ? plugin.getInstance(webviewId) : null; - if (instance != null) { - instance.createWebView(this); - webView = instance.getWebView(); - webViewContainer.removeAllViews(); - webViewContainer.addView(webView, new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - )); - } } @Override @@ -168,17 +98,10 @@ protected void onDestroy() { } } - private int dpToPx(int dp) { - return (int) (dp * getResources().getDisplayMetrics().density); - } - private class FullscreenWebViewClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, android.webkit.WebResourceRequest request) { - if (!allowNavigation) { - return true; - } - return false; + return !allowNavigation; } } diff --git a/src/plugins/webview/src/android/com/foxdebug/webview/WebViewPlugin.java b/src/plugins/webview/src/android/com/foxdebug/webview/WebViewPlugin.java index e060d007f..17b7e51aa 100644 --- a/src/plugins/webview/src/android/com/foxdebug/webview/WebViewPlugin.java +++ b/src/plugins/webview/src/android/com/foxdebug/webview/WebViewPlugin.java @@ -126,7 +126,7 @@ public void run() { if (effectiveMode.equals("fullscreen")) { instances.put(id, instance); - launchFullscreenActivity(id, title, allowNavigation, allowDownloads); + launchFullscreenActivity(id, allowNavigation); callbackContext.success(id); return; } @@ -147,14 +147,10 @@ public void run() { }); } - private void launchFullscreenActivity( - String id, String title, boolean allowNavigation, boolean allowDownloads - ) { + private void launchFullscreenActivity(String id, boolean allowNavigation) { Intent intent = new Intent(cordova.getActivity(), WebViewActivity.class); intent.putExtra("webviewId", id); - intent.putExtra("title", title); intent.putExtra("allowNavigation", allowNavigation); - intent.putExtra("allowDownloads", allowDownloads); cordova.getActivity().startActivity(intent); } From f30398a48081c7a1115f0160983780f6ef15b4c4 Mon Sep 17 00:00:00 2001 From: Rohit Kushwaha Date: Wed, 22 Jul 2026 19:10:12 +0530 Subject: [PATCH 4/4] fix: lifecycle events, hidden show, fullscreen disconnect, and security - Fix event dispatch: filter callbacks by event name instead of calling {event,callback} objects as functions (webview.js) - Make hidden WebViews showable: attachToActivity() on first show() - Remove universal file access (setAllowFileAccessFromFileURLs, setAllowUniversalAccessFromFileURLs) for untrusted content - Clean up fullscreen path: remove throwaway WebView in Activity, register instance cleanup in onDestroy - Add deprecated shouldOverrideUrlLoading(String) for compat - Use Handler(Looper.getMainLooper()) instead of View.post() so hidden WebViews can process loadURL/evaluate without a window - Remove unused constructor params (title, x, y, initiallyVisible) --- src/lib/webview.js | 284 +++++++++--------- .../com/foxdebug/webview/WebViewActivity.java | 68 +---- .../com/foxdebug/webview/WebViewInstance.java | 102 +++---- .../com/foxdebug/webview/WebViewPlugin.java | 22 +- 4 files changed, 203 insertions(+), 273 deletions(-) diff --git a/src/lib/webview.js b/src/lib/webview.js index 70965c27b..ae96cb662 100644 --- a/src/lib/webview.js +++ b/src/lib/webview.js @@ -5,153 +5,155 @@ const instances = new Map(); const eventCallbacks = new Map(); function ensureInit() { - if (!initialized) { - nativeBridge.setMessageCallback((payload) => { - const { id, message, event, data } = payload; - - if (event) { - const callbacks = eventCallbacks.get(id); - if (callbacks) { - callbacks.forEach((cb) => { - try { - cb(event, data); - } catch (e) { - console.error("WebView event callback error:", e); - } - }); - } - return; - } - - if (message !== undefined) { - const instance = instances.get(id); - if (instance && instance._messageCallbacks) { - let parsed = message; - try { - parsed = JSON.parse(message); - } catch (_) {} - instance._messageCallbacks.forEach((cb) => { - try { - cb(parsed); - } catch (e) { - console.error("WebView message callback error:", e); - } - }); - } - } - }); - initialized = true; - } + if (!initialized) { + nativeBridge.setMessageCallback((payload) => { + const { id, message, event, data } = payload; + + if (event) { + const callbacks = eventCallbacks.get(id); + if (callbacks) { + callbacks.forEach((entry) => { + if (entry.event === event) { + try { + entry.callback(event, data); + } catch (e) { + console.error("WebView event callback error:", e); + } + } + }); + } + return; + } + + if (message !== undefined) { + const instance = instances.get(id); + if (instance && instance._messageCallbacks) { + let parsed = message; + try { + parsed = JSON.parse(message); + } catch (_) {} + instance._messageCallbacks.forEach((cb) => { + try { + cb(parsed); + } catch (e) { + console.error("WebView message callback error:", e); + } + }); + } + } + }); + initialized = true; + } } class WebView { - constructor(id, options = {}) { - this.id = id; - this.options = options; - this._messageCallbacks = []; - this._eventCallbacks = []; - this._destroyed = false; - - instances.set(id, this); - eventCallbacks.set(id, this._eventCallbacks); - } - - async loadURL(url) { - this._checkDestroyed(); - await nativeBridge.loadURL(this.id, url); - } - - async loadHTML(html) { - this._checkDestroyed(); - await nativeBridge.loadHTML(this.id, html); - } - - async evaluate(js) { - this._checkDestroyed(); - return await nativeBridge.evaluate(this.id, js); - } - - onMessage(callback) { - this._checkDestroyed(); - if (typeof callback === "function") { - this._messageCallbacks.push(callback); - } - } - - offMessage(callback) { - this._messageCallbacks = this._messageCallbacks.filter( - (cb) => cb !== callback, - ); - } - - on(event, callback) { - this._checkDestroyed(); - if (typeof callback === "function") { - this._eventCallbacks.push({ event, callback }); - } - } - - off(event, callback) { - this._eventCallbacks = this._eventCallbacks.filter( - (cb) => !(cb.event === event && cb.callback === callback), - ); - } - - async postMessage(message) { - this._checkDestroyed(); - await nativeBridge.postMessage(this.id, message); - } - - async show() { - this._checkDestroyed(); - await nativeBridge.show(this.id); - } - - async hide() { - this._checkDestroyed(); - await nativeBridge.hide(this.id); - } - - async reload() { - this._checkDestroyed(); - await nativeBridge.reload(this.id); - } - - async destroy() { - this._checkDestroyed(); - this._destroyed = true; - await nativeBridge.destroy(this.id); - instances.delete(this.id); - eventCallbacks.delete(this.id); - this._messageCallbacks = []; - this._eventCallbacks = []; - } - - _checkDestroyed() { - if (this._destroyed) { - throw new Error("WebView has been destroyed"); - } - } + constructor(id, options = {}) { + this.id = id; + this.options = options; + this._messageCallbacks = []; + this._eventCallbacks = []; + this._destroyed = false; + + instances.set(id, this); + eventCallbacks.set(id, this._eventCallbacks); + } + + async loadURL(url) { + this._checkDestroyed(); + await nativeBridge.loadURL(this.id, url); + } + + async loadHTML(html) { + this._checkDestroyed(); + await nativeBridge.loadHTML(this.id, html); + } + + async evaluate(js) { + this._checkDestroyed(); + return await nativeBridge.evaluate(this.id, js); + } + + onMessage(callback) { + this._checkDestroyed(); + if (typeof callback === "function") { + this._messageCallbacks.push(callback); + } + } + + offMessage(callback) { + this._messageCallbacks = this._messageCallbacks.filter( + (cb) => cb !== callback, + ); + } + + on(event, callback) { + this._checkDestroyed(); + if (typeof callback === "function") { + this._eventCallbacks.push({ event, callback }); + } + } + + off(event, callback) { + this._eventCallbacks = this._eventCallbacks.filter( + (entry) => !(entry.event === event && entry.callback === callback), + ); + } + + async postMessage(message) { + this._checkDestroyed(); + await nativeBridge.postMessage(this.id, message); + } + + async show() { + this._checkDestroyed(); + await nativeBridge.show(this.id); + } + + async hide() { + this._checkDestroyed(); + await nativeBridge.hide(this.id); + } + + async reload() { + this._checkDestroyed(); + await nativeBridge.reload(this.id); + } + + async destroy() { + this._checkDestroyed(); + this._destroyed = true; + await nativeBridge.destroy(this.id); + instances.delete(this.id); + eventCallbacks.delete(this.id); + this._messageCallbacks = []; + this._eventCallbacks = []; + } + + _checkDestroyed() { + if (this._destroyed) { + throw new Error("WebView has been destroyed"); + } + } } const webviewAPI = { - async create(options = {}) { - ensureInit(); - - const id = await nativeBridge.create({ - title: options.title || "", - mode: options.mode || "hidden", - width: options.width || 0, - height: options.height || 0, - x: options.x || 0, - y: options.y || 0, - allowNavigation: options.allowNavigation !== false, - allowDownloads: options.allowDownloads === true, - visible: options.visible !== false, - }); - - return new WebView(id, options); - }, + async create(options = {}) { + ensureInit(); + + const id = await nativeBridge.create({ + title: options.title || "", + mode: options.mode || "hidden", + width: options.width || 0, + height: options.height || 0, + x: options.x || 0, + y: options.y || 0, + allowNavigation: options.allowNavigation !== false, + allowDownloads: options.allowDownloads === true, + visible: options.visible !== false, + }); + + return new WebView(id, options); + }, }; export default webviewAPI; diff --git a/src/plugins/webview/src/android/com/foxdebug/webview/WebViewActivity.java b/src/plugins/webview/src/android/com/foxdebug/webview/WebViewActivity.java index 00ee0f07b..54b7304c6 100644 --- a/src/plugins/webview/src/android/com/foxdebug/webview/WebViewActivity.java +++ b/src/plugins/webview/src/android/com/foxdebug/webview/WebViewActivity.java @@ -5,10 +5,7 @@ import android.os.Build; import android.os.Bundle; import android.view.ViewGroup; -import android.webkit.JavascriptInterface; -import android.webkit.WebSettings; import android.webkit.WebView; -import android.webkit.WebViewClient; import android.widget.FrameLayout; public class WebViewActivity extends Activity { @@ -17,7 +14,6 @@ public class WebViewActivity extends Activity { private WebView webView; private String webviewId; - private boolean allowNavigation; public static void setPlugin(WebViewPlugin p) { plugin = p; @@ -29,46 +25,16 @@ public void onCreate(Bundle savedInstanceState) { Intent intent = getIntent(); webviewId = intent.getStringExtra("webviewId"); - allowNavigation = intent.getBooleanExtra("allowNavigation", true); WebViewInstance instance = plugin != null ? plugin.getInstance(webviewId) : null; - - if (instance != null && instance.getWebView() == null) { - instance.createWebView(this); - webView = instance.getWebView(); - } else if (instance != null) { - webView = instance.getWebView(); - } else { - webView = new WebView(this); - webView.addJavascriptInterface(new JsBridge(), "AcodeWebViewNative"); - - WebSettings settings = webView.getSettings(); - settings.setJavaScriptEnabled(true); - settings.setDomStorageEnabled(true); - settings.setAllowContentAccess(true); - settings.setDisplayZoomControls(false); - settings.setLoadWithOverviewMode(true); - settings.setUseWideViewPort(true); - settings.setAllowFileAccess(true); - - webView.setWebViewClient(new FullscreenWebViewClient()); - - String bridgeJs = - "(function() {" + - " window.webview = window.webview || {};" + - " window.webview._callbacks = [];" + - " window.webview.onMessage = function(cb) { window.webview._callbacks.push(cb); };" + - " window.webview.postMessage = function(msg) {" + - " var data = typeof msg === 'string' ? msg : JSON.stringify(msg);" + - " window.AcodeWebViewNative.postMessage(data);" + - " };" + - " window.webview.offMessage = function(cb) {" + - " window.webview._callbacks = window.webview._callbacks.filter(function(c) { return c !== cb; });" + - " };" + - "})();"; - webView.evaluateJavascript(bridgeJs, null); + if (instance == null) { + finish(); + return; } + instance.createWebView(this); + webView = instance.getWebView(); + FrameLayout container = new FrameLayout(this); container.addView(webView, new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, @@ -95,27 +61,7 @@ protected void onDestroy() { super.onDestroy(); if (plugin != null) { plugin.sendEventToCordova(webviewId, "closed", null); - } - } - - private class FullscreenWebViewClient extends WebViewClient { - @Override - public boolean shouldOverrideUrlLoading(WebView view, android.webkit.WebResourceRequest request) { - return !allowNavigation; - } - } - - private class JsBridge { - @JavascriptInterface - public void postMessage(String message) { - if (plugin != null) { - plugin.sendMessageToCordova(webviewId, message); - } - } - - @JavascriptInterface - public String getWebViewId() { - return webviewId; + plugin.removeInstance(webviewId); } } } diff --git a/src/plugins/webview/src/android/com/foxdebug/webview/WebViewInstance.java b/src/plugins/webview/src/android/com/foxdebug/webview/WebViewInstance.java index 6844c6b13..592ce2fdf 100644 --- a/src/plugins/webview/src/android/com/foxdebug/webview/WebViewInstance.java +++ b/src/plugins/webview/src/android/com/foxdebug/webview/WebViewInstance.java @@ -3,15 +3,12 @@ import android.app.Activity; import android.content.DialogInterface; import android.graphics.Color; -import android.os.Build; import android.os.Handler; import android.os.Looper; import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; import android.webkit.DownloadListener; import android.webkit.JavascriptInterface; import android.webkit.URLUtil; @@ -38,38 +35,32 @@ public class WebViewInstance { final String id; final String mode; - final String title; final int width; final int height; - final int x; - final int y; final boolean allowNavigation; final boolean allowDownloads; - final boolean initiallyVisible; final WebViewPlugin plugin; private WebView webView; private FrameLayout container; + private Activity activity; private boolean isDestroyed = false; + private boolean isAttached = false; WebViewInstance( - String id, String mode, String title, - int width, int height, int x, int y, + String id, String mode, + int width, int height, boolean allowNavigation, boolean allowDownloads, - boolean initiallyVisible, Activity activity, WebViewPlugin plugin ) { this.id = id; this.mode = mode; - this.title = title; this.width = width; this.height = height; - this.x = x; - this.y = y; this.allowNavigation = allowNavigation; this.allowDownloads = allowDownloads; - this.initiallyVisible = initiallyVisible; + this.activity = activity; this.plugin = plugin; } @@ -78,6 +69,7 @@ public WebView getWebView() { } void createWebView(Activity activity) { + this.activity = activity; webView = new WebView(activity); WebSettings settings = webView.getSettings(); @@ -87,9 +79,7 @@ void createWebView(Activity activity) { settings.setDisplayZoomControls(false); settings.setLoadWithOverviewMode(true); settings.setUseWideViewPort(true); - settings.setAllowFileAccess(true); - settings.setAllowFileAccessFromFileURLs(true); - settings.setAllowUniversalAccessFromFileURLs(true); + settings.setAllowFileAccess(false); webView.setWebViewClient(new InstanceWebViewClient()); webView.setWebChromeClient(new InstanceWebChromeClient()); @@ -119,34 +109,29 @@ void createWebView(Activity activity) { webView.evaluateJavascript(bridgeJs, null); } - void attachToActivity(Activity activity) { - container = new FrameLayout(activity); - - FrameLayout.LayoutParams webViewParams = new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ); + void attachToActivity() { + if (isAttached || isDestroyed || webView == null || activity == null) return; + container = new FrameLayout(activity); container.setBackgroundColor(Color.argb(180, 0, 0, 0)); + FrameLayout.LayoutParams webViewParams; if (mode.equals("window")) { - int w = width > 0 ? width : ViewGroup.LayoutParams.WRAP_CONTENT; - int h = height > 0 ? height : ViewGroup.LayoutParams.WRAP_CONTENT; - - webViewParams = new FrameLayout.LayoutParams( - w > 0 ? dpToPx(activity, w) : ViewGroup.LayoutParams.MATCH_PARENT, - h > 0 ? dpToPx(activity, h) : ViewGroup.LayoutParams.MATCH_PARENT - ); + int w = width > 0 ? dpToPx(activity, width) : ViewGroup.LayoutParams.MATCH_PARENT; + int h = height > 0 ? dpToPx(activity, height) : ViewGroup.LayoutParams.MATCH_PARENT; + webViewParams = new FrameLayout.LayoutParams(w, h); webViewParams.gravity = Gravity.CENTER; - webViewParams.setMargins(dpToPx(activity, 16), dpToPx(activity, 48), dpToPx(activity, 16), dpToPx(activity, 48)); - + webViewParams.setMargins( + dpToPx(activity, 16), dpToPx(activity, 48), + dpToPx(activity, 16), dpToPx(activity, 48) + ); container.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - hideInner(); + container.setVisibility(View.GONE); } }); - } else if (mode.equals("panel")) { + } else { webViewParams = new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, height > 0 ? dpToPx(activity, height) : (int)(getScreenHeight(activity) * 0.4) @@ -154,6 +139,9 @@ public void onClick(View v) { webViewParams.gravity = Gravity.BOTTOM; } + if (webView.getParent() != null) { + ((ViewGroup) webView.getParent()).removeView(webView); + } container.addView(webView, webViewParams); ViewGroup rootView = activity.findViewById(android.R.id.content); @@ -163,6 +151,8 @@ public void onClick(View v) { ViewGroup.LayoutParams.MATCH_PARENT )); } + + isAttached = true; } void loadURL(String url) { @@ -215,11 +205,6 @@ void postMessage(String message, CallbackContext callbackContext) { callbackContext.error("WebView destroyed"); return; } - String escaped = message - .replace("\\", "\\\\") - .replace("'", "\\'") - .replace("\n", "\\n") - .replace("\r", "\\r"); String js = "if(window.webview&&window.webview._callbacks){" + "var msg=" + safeParseJSON(message) + ";" + @@ -253,12 +238,19 @@ void show(final CallbackContext callbackContext) { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { - if (isDestroyed || container == null) { - if (callbackContext != null) callbackContext.error("Cannot show"); + if (isDestroyed) { + if (callbackContext != null) callbackContext.error("WebView destroyed"); return; } - container.setVisibility(View.VISIBLE); - if (callbackContext != null) callbackContext.success(); + if (!isAttached) { + attachToActivity(); + } + if (container != null) { + container.setVisibility(View.VISIBLE); + if (callbackContext != null) callbackContext.success(); + } else { + if (callbackContext != null) callbackContext.error("Cannot show"); + } } }); } @@ -277,12 +269,6 @@ public void run() { }); } - private void hideInner() { - if (container != null) { - container.setVisibility(View.GONE); - } - } - void reload(CallbackContext callbackContext) { if (isDestroyed || webView == null) { callbackContext.error("WebView destroyed"); @@ -315,6 +301,7 @@ public void run() { } webView = null; container = null; + isAttached = false; } }); } @@ -339,12 +326,14 @@ private static int getScreenHeight(Activity activity) { } private class InstanceWebViewClient extends WebViewClient { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + return !allowNavigation; + } + @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - if (!allowNavigation) { - return true; - } - return false; + return !allowNavigation; } @Override @@ -414,10 +403,5 @@ public class JsBridge { public void postMessage(String message) { plugin.sendMessageToCordova(id, message); } - - @JavascriptInterface - public String getWebViewId() { - return id; - } } } diff --git a/src/plugins/webview/src/android/com/foxdebug/webview/WebViewPlugin.java b/src/plugins/webview/src/android/com/foxdebug/webview/WebViewPlugin.java index 17b7e51aa..22104bcfa 100644 --- a/src/plugins/webview/src/android/com/foxdebug/webview/WebViewPlugin.java +++ b/src/plugins/webview/src/android/com/foxdebug/webview/WebViewPlugin.java @@ -101,30 +101,24 @@ private String generateId() { private void create(JSONObject options, final CallbackContext callbackContext) throws JSONException { final String id = generateId(); final String mode = options.optString("mode", "hidden"); - final String title = options.optString("title", "WebView"); final int width = options.optInt("width", 0); final int height = options.optInt("height", 0); - final int x = options.optInt("x", 0); - final int y = options.optInt("y", 0); final boolean allowNavigation = options.optBoolean("allowNavigation", true); final boolean allowDownloads = options.optBoolean("allowDownloads", false); - final boolean visible = options.optBoolean("visible", true); - - final String effectiveMode = visible ? mode : "hidden"; cordova.getActivity().runOnUiThread(new Runnable() { @Override public void run() { try { WebViewInstance instance = new WebViewInstance( - id, effectiveMode, title, - width, height, x, y, + id, mode, + width, height, allowNavigation, allowDownloads, - visible, cordova.getActivity(), + cordova.getActivity(), WebViewPlugin.this ); - if (effectiveMode.equals("fullscreen")) { + if (mode.equals("fullscreen")) { instances.put(id, instance); launchFullscreenActivity(id, allowNavigation); callbackContext.success(id); @@ -133,8 +127,8 @@ public void run() { instance.createWebView(cordova.getActivity()); - if (effectiveMode.equals("window") || effectiveMode.equals("panel")) { - instance.attachToActivity(cordova.getActivity()); + if (mode.equals("window") || mode.equals("panel")) { + instance.attachToActivity(); } instances.put(id, instance); @@ -158,6 +152,10 @@ public WebViewInstance getInstance(String id) { return instances.get(id); } + public void removeInstance(String id) { + instances.remove(id); + } + private void loadURL(String id, String url, CallbackContext callbackContext) { WebViewInstance instance = getInstance(id); if (instance == null) { callbackContext.error("WebView not found: " + id); return; }