diff --git a/README.md b/README.md index d2c14e1..c13b165 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 0000000..712b80f --- /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 8cc0946..4f540b3 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 a32c2c3..88fc46a 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 500ecf2..f595cc0 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 6f93f09..eac870e 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 cfbdfee..2073bfd 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 0000000..cfd99bd --- /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 0000000..d499ffa --- /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