From a4a18ce6c1e45d2e342d1bad164cdf1e17210f4d Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 13 Jun 2025 06:05:20 +0000 Subject: [PATCH] Fix issue #280: Add support for persistent callbacks - Add persistent callback functionality to prevent callbacks from being deleted after first use - Implement callHandlerPersistent() method for Java side - Implement callHandlerPersistent() method for JavaScript side - Add registerPersistentCallback() and removePersistentCallback() methods - Update BaseJavascriptInterface to support persistent callbacks - Add comprehensive tests for persistent callback functionality - Add demo HTML page showing persistent callback usage - Update README with documentation for new persistent callback feature This allows callbacks to be cached and reused multiple times, fixing the issue where Android side couldn't reuse cached callbacks from JavaScript handlers. --- README.md | 38 +++++ .../main/assets/persistent_callback_demo.html | 114 ++++++++++++++ .../lzyzsd/jsbridge/example/MainActivity.java | 2 +- .../example/MainJavascriptInterface.java | 5 + library/build.gradle | 8 + .../main/assets/WebViewJavascriptBridge.js | 45 +++++- .../github/lzyzsd/jsbridge/BridgeWebView.java | 57 ++++++- .../test/assets/test_persistent_callback.html | 104 +++++++++++++ .../jsbridge/PersistentCallbackTest.java | 146 ++++++++++++++++++ 9 files changed, 512 insertions(+), 7 deletions(-) create mode 100644 example/src/main/assets/persistent_callback_demo.html create mode 100644 library/src/test/assets/test_persistent_callback.html create mode 100644 library/src/test/java/com/github/lzyzsd/jsbridge/PersistentCallbackTest.java diff --git a/README.md b/README.md index d2c14e17..c13b1650 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,44 @@ For example: will print 'JS got a message hello' and 'JS responding with' in webview console. +### Persistent Callbacks (New Feature) + +By default, callbacks are deleted after first use. However, you can now use persistent callbacks that can be reused multiple times: + +#### Java Side + +```java +// Use persistent callback that won't be deleted after first use +webView.callHandlerPersistent("functionInJs", data, new OnBridgeCallback() { + @Override + public void onCallBack(String data) { + // This callback can be called multiple times + Log.d(TAG, "Persistent callback called: " + data); + } +}); +``` + +#### JavaScript Side + +```javascript +// Use persistent callback +WebViewJavascriptBridge.callHandlerPersistent("javaHandler", data, function(response) { + // This callback can be reused multiple times + console.log("Persistent callback response: " + response); +}); + +// Register and manually manage persistent callbacks +var callbackId = "my_persistent_callback"; +WebViewJavascriptBridge.registerPersistentCallback(callbackId, function(data) { + console.log("Persistent callback called: " + data); +}); + +// Remove persistent callback when no longer needed +WebViewJavascriptBridge.removePersistentCallback(callbackId); +``` + +This feature is useful when you need to maintain a long-term communication channel between Java and JavaScript, such as for real-time updates or event notifications. + ### Switch to CustomWebView * activity_main.xml ```xml diff --git a/example/src/main/assets/persistent_callback_demo.html b/example/src/main/assets/persistent_callback_demo.html new file mode 100644 index 00000000..712b80fa --- /dev/null +++ b/example/src/main/assets/persistent_callback_demo.html @@ -0,0 +1,114 @@ + + + + + Persistent Callback Demo + + + +

Persistent Callback Demo

+

This demo shows how to use persistent callbacks that can be reused multiple times.

+ +
+ +

+ +

+

+ +

+

+ +

+

+ +

+ + + + + \ No newline at end of file diff --git a/example/src/main/java/com/github/lzyzsd/jsbridge/example/MainActivity.java b/example/src/main/java/com/github/lzyzsd/jsbridge/example/MainActivity.java index 8cc0946d..4f540b37 100644 --- a/example/src/main/java/com/github/lzyzsd/jsbridge/example/MainActivity.java +++ b/example/src/main/java/com/github/lzyzsd/jsbridge/example/MainActivity.java @@ -77,7 +77,7 @@ public boolean onShowFileChooser(WebView webView, ValueCallback filePathC } }); - webView.addJavascriptInterface(new MainJavascriptInterface(webView.getCallbacks(), webView), "WebViewJavascriptBridge"); + webView.addJavascriptInterface(new MainJavascriptInterface(webView.getCallbacks(), webView.getPersistentCallbacks(), webView), "WebViewJavascriptBridge"); webView.setGson(new Gson()); webView.loadUrl("file:///android_asset/demo.html"); User user = new User(); diff --git a/example/src/main/java/com/github/lzyzsd/jsbridge/example/MainJavascriptInterface.java b/example/src/main/java/com/github/lzyzsd/jsbridge/example/MainJavascriptInterface.java index a32c2c3a..88fc46ae 100644 --- a/example/src/main/java/com/github/lzyzsd/jsbridge/example/MainJavascriptInterface.java +++ b/example/src/main/java/com/github/lzyzsd/jsbridge/example/MainJavascriptInterface.java @@ -24,6 +24,11 @@ public MainJavascriptInterface(Map callbacks, WebViewJ mWebView = webView; } + public MainJavascriptInterface(Map callbacks, Map persistentCallbacks, WebViewJavascriptBridge webView) { + super(callbacks, persistentCallbacks); + mWebView = webView; + } + public MainJavascriptInterface(Map callbacks) { super(callbacks); } diff --git a/library/build.gradle b/library/build.gradle index 500ecf28..f595cc06 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -27,6 +27,14 @@ android { dependencies { implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'com.google.code.gson:gson:2.8.5' + + // Test dependencies + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:3.12.4' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test:core:1.4.0' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'org.mockito:mockito-android:3.12.4' } diff --git a/library/src/main/assets/WebViewJavascriptBridge.js b/library/src/main/assets/WebViewJavascriptBridge.js index 6f93f09c..eac870eb 100644 --- a/library/src/main/assets/WebViewJavascriptBridge.js +++ b/library/src/main/assets/WebViewJavascriptBridge.js @@ -11,6 +11,7 @@ var sendMessageQueue = []; var responseCallbacks = {}; + var persistentCallbacks = {}; var uniqueId = 1; var lastCallTime = 0; @@ -63,24 +64,48 @@ delete messageHandlers[handlerName]; } + // Register a persistent callback that won't be deleted after first use + function registerPersistentCallback(callbackId, callback) { + persistentCallbacks[callbackId] = callback; + responseCallbacks[callbackId] = callback; + } + + // Remove a persistent callback + function removePersistentCallback(callbackId) { + delete persistentCallbacks[callbackId]; + delete responseCallbacks[callbackId]; + } + // 调用线程 - function callHandler(handlerName, data, responseCallback) { + function callHandler(handlerName, data, responseCallback, persistent) { // 如果方法不需要参数,只有回调函数,简化JS中的调用 if (arguments.length == 2 && typeof data == 'function') { responseCallback = data; data = null; } - _doSend(handlerName, data, responseCallback); + _doSend(handlerName, data, responseCallback, persistent); + } + + // Call handler with persistent callback that can be reused + function callHandlerPersistent(handlerName, data, responseCallback) { + if (arguments.length == 2 && typeof data == 'function') { + responseCallback = data; + data = null; + } + _doSend(handlerName, data, responseCallback, true); } //sendMessage add message, 触发native处理 sendMessage - function _doSend(handlerName, message, responseCallback) { + function _doSend(handlerName, message, responseCallback, persistent) { var callbackId; if(typeof responseCallback === 'string'){ callbackId = responseCallback; } else if (responseCallback) { callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime(); responseCallbacks[callbackId] = responseCallback; + if (persistent) { + persistentCallbacks[callbackId] = responseCallback; + } message.callbackId = callbackId; }else{ callbackId = ''; @@ -98,7 +123,10 @@ return; } responseCallback(responseData); - delete responseCallbacks[callbackId]; + // Only delete if it's not a persistent callback + if (!persistentCallbacks[callbackId]) { + delete responseCallbacks[callbackId]; + } } } @@ -141,7 +169,10 @@ return; } responseCallback(message.responseData); - delete responseCallbacks[message.responseId]; + // Only delete if it's not a persistent callback + if (!persistentCallbacks[message.responseId]) { + delete responseCallbacks[message.responseId]; + } } else { //直接发送 if (message.callbackId) { @@ -179,7 +210,11 @@ WebViewJavascriptBridge.init = init; WebViewJavascriptBridge.doSend = send; WebViewJavascriptBridge.registerHandler = registerHandler; + WebViewJavascriptBridge.removeHandler = removeHandler; WebViewJavascriptBridge.callHandler = callHandler; + WebViewJavascriptBridge.callHandlerPersistent = callHandlerPersistent; + WebViewJavascriptBridge.registerPersistentCallback = registerPersistentCallback; + WebViewJavascriptBridge.removePersistentCallback = removePersistentCallback; WebViewJavascriptBridge._handleMessageFromNative = _handleMessageFromNative; WebViewJavascriptBridge._fetchQueue = _fetchQueue; diff --git a/library/src/main/java/com/github/lzyzsd/jsbridge/BridgeWebView.java b/library/src/main/java/com/github/lzyzsd/jsbridge/BridgeWebView.java index cfbdfeef..2073bfd7 100644 --- a/library/src/main/java/com/github/lzyzsd/jsbridge/BridgeWebView.java +++ b/library/src/main/java/com/github/lzyzsd/jsbridge/BridgeWebView.java @@ -33,6 +33,7 @@ public class BridgeWebView extends WebView implements WebViewJavascriptBridge, B private final int URL_MAX_CHARACTER_NUM=2097152; private Map mCallbacks = new ArrayMap<>(); + private Map mPersistentCallbacks = new ArrayMap<>(); private List mMessages = new ArrayList<>(); @@ -86,6 +87,10 @@ public Map getCallbacks() { return mCallbacks; } + public Map getPersistentCallbacks() { + return mPersistentCallbacks; + } + @Override public void setWebViewClient(WebViewClient client) { mClient.setWebViewClient(client); @@ -129,6 +134,18 @@ public void callHandler(String handlerName, String data, OnBridgeCallback callBa doSend(handlerName, data, callBack); } + /** + * call javascript registered handler with persistent callback + * 调用javascript处理程序注册,使用持久回调 + * + * @param handlerName handlerName + * @param data data + * @param callBack OnBridgeCallback (will be persistent and reusable) + */ + public void callHandlerPersistent(String handlerName, String data, OnBridgeCallback callBack) { + doSendPersistent(handlerName, data, callBack); + } + @Override public void sendToWeb(String function, Object... values) { @@ -171,6 +188,33 @@ private void doSend(String handlerName, Object data, OnBridgeCallback responseCa queueMessage(request); } + /** + * 保存message到消息队列,使用持久回调 + * + * @param handlerName handlerName + * @param data data + * @param responseCallback OnBridgeCallback (persistent) + */ + private void doSendPersistent(String handlerName, Object data, OnBridgeCallback responseCallback) { + if (!(data instanceof String) && mGson == null){ + return; + } + JSRequest request = new JSRequest(); + if (data != null) { + request.data = data instanceof String ? (String) data : mGson.toJson(data); + } + if (responseCallback != null) { + String callbackId = String.format(BridgeUtil.CALLBACK_ID_FORMAT, (++mUniqueId) + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis())); + mCallbacks.put(callbackId, responseCallback); + mPersistentCallbacks.put(callbackId, responseCallback); + request.callbackId = callbackId; + } + if (!TextUtils.isEmpty(handlerName)) { + request.handlerName = handlerName; + } + queueMessage(request); + } + /** * list != null 添加到消息集合否则分发消息 * @@ -234,16 +278,23 @@ public void run() { public void destroy() { super.destroy(); mCallbacks.clear(); + mPersistentCallbacks.clear(); } public static abstract class BaseJavascriptInterface { private Map mCallbacks; + private Map mPersistentCallbacks; public BaseJavascriptInterface(Map callbacks) { mCallbacks = callbacks; } + public BaseJavascriptInterface(Map callbacks, Map persistentCallbacks) { + mCallbacks = callbacks; + mPersistentCallbacks = persistentCallbacks; + } + @JavascriptInterface public String send(String data, String callbackId) { Log.d("BaseJavascriptInterface", data + ", callbackId: " + callbackId + " " + Thread.currentThread().getName()); @@ -254,9 +305,13 @@ public String send(String data, String callbackId) { public void response(String data, String responseId) { Log.d("BaseJavascriptInterface", data + ", responseId: " + responseId + " " + Thread.currentThread().getName()); if (!TextUtils.isEmpty(responseId)) { - OnBridgeCallback function = mCallbacks.remove(responseId); + OnBridgeCallback function = mCallbacks.get(responseId); if (function != null) { function.onCallBack(data); + // Only remove if it's not a persistent callback + if (mPersistentCallbacks == null || !mPersistentCallbacks.containsKey(responseId)) { + mCallbacks.remove(responseId); + } } } } diff --git a/library/src/test/assets/test_persistent_callback.html b/library/src/test/assets/test_persistent_callback.html new file mode 100644 index 00000000..cfd99bdc --- /dev/null +++ b/library/src/test/assets/test_persistent_callback.html @@ -0,0 +1,104 @@ + + + + + Persistent Callback Test + + +
+ + + + \ No newline at end of file diff --git a/library/src/test/java/com/github/lzyzsd/jsbridge/PersistentCallbackTest.java b/library/src/test/java/com/github/lzyzsd/jsbridge/PersistentCallbackTest.java new file mode 100644 index 00000000..d499ffab --- /dev/null +++ b/library/src/test/java/com/github/lzyzsd/jsbridge/PersistentCallbackTest.java @@ -0,0 +1,146 @@ +package com.github.lzyzsd.jsbridge; + +import android.content.Context; +import android.webkit.WebView; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.google.gson.Gson; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Test class for persistent callback functionality + * Tests the fix for issue #280 - persistent callbacks should be reusable + */ +@RunWith(AndroidJUnit4.class) +public class PersistentCallbackTest { + + private BridgeWebView bridgeWebView; + private Context context; + + @Mock + private OnBridgeCallback mockCallback; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + context = ApplicationProvider.getApplicationContext(); + bridgeWebView = new BridgeWebView(context); + bridgeWebView.setGson(new Gson()); + } + + @Test + public void testPersistentCallbackReuse() { + // This test verifies that persistent callbacks can be reused multiple times + // without being deleted after first use + + final int[] callCount = {0}; + + // Create a callback that should be persistent + OnBridgeCallback persistentCallback = new OnBridgeCallback() { + @Override + public void onCallBack(String data) { + callCount[0]++; + } + }; + + // Use the persistent callback method + bridgeWebView.callHandlerPersistent("testHandler", "testData", persistentCallback); + + // Get the callback ID that was generated + String callbackId = null; + for (String id : bridgeWebView.getCallbacks().keySet()) { + if (bridgeWebView.getPersistentCallbacks().containsKey(id)) { + callbackId = id; + break; + } + } + + assertNotNull("Persistent callback should be stored", callbackId); + + // Simulate multiple responses from JavaScript + // This should work without the callback being deleted + bridgeWebView.sendResponse("response1", callbackId); + bridgeWebView.sendResponse("response2", callbackId); + bridgeWebView.sendResponse("response3", callbackId); + + // Verify the callback was called multiple times + assertEquals("Persistent callback should be called 3 times", 3, callCount[0]); + + // Verify the callback is still available after multiple uses + assertTrue("Persistent callback should still exist after multiple uses", + bridgeWebView.getCallbacks().containsKey(callbackId)); + assertTrue("Persistent callback should be marked as persistent", + bridgeWebView.getPersistentCallbacks().containsKey(callbackId)); + } + + @Test + public void testNormalCallbackBehavior() { + // This test verifies that normal (non-persistent) callbacks still work as before + // and are deleted after first use + + final int[] callCount = {0}; + + OnBridgeCallback normalCallback = new OnBridgeCallback() { + @Override + public void onCallBack(String data) { + callCount[0]++; + } + }; + + // Call handler with normal callback - should be deleted after first use + bridgeWebView.callHandler("testHandler", "testData", normalCallback); + + // The callback should be stored initially + assertFalse("Normal callbacks should be stored initially", + bridgeWebView.getCallbacks().isEmpty()); + + // Get the callback ID that was generated + String callbackId = null; + for (String id : bridgeWebView.getCallbacks().keySet()) { + callbackId = id; + break; + } + + assertNotNull("Normal callback should be stored", callbackId); + + // Verify it's not marked as persistent + assertFalse("Normal callback should not be marked as persistent", + bridgeWebView.getPersistentCallbacks().containsKey(callbackId)); + + // Simulate response from JavaScript + bridgeWebView.sendResponse("response", callbackId); + + // Verify the callback was called + assertEquals("Normal callback should be called once", 1, callCount[0]); + + // Try to call it again - should not work since it should be deleted + bridgeWebView.sendResponse("response2", callbackId); + + // Verify the callback was not called again + assertEquals("Normal callback should not be called again after deletion", 1, callCount[0]); + } + + @Test + public void testCallbackIdGeneration() { + // Test that callback IDs are generated correctly + OnBridgeCallback callback1 = mock(OnBridgeCallback.class); + OnBridgeCallback callback2 = mock(OnBridgeCallback.class); + + bridgeWebView.callHandler("handler1", "data1", callback1); + int size1 = bridgeWebView.getCallbacks().size(); + + bridgeWebView.callHandler("handler2", "data2", callback2); + int size2 = bridgeWebView.getCallbacks().size(); + + assertEquals("Each callback should be stored with unique ID", size1 + 1, size2); + } +} \ No newline at end of file