[
  {
    "path": ".github/workflows/android.yml",
    "content": "name: Android CI\n\non: [push]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v2\n    - name: set up JDK 1.8\n      uses: actions/setup-java@v1\n      with:\n        java-version: 1.8\n    - name: Build with Gradle\n      \n"
  },
  {
    "path": ".gitignore",
    "content": "#built application files\n*.apk\n*.ap_\n\n\n# files for the dex VM\n*.dex\n\n\n# Java class files\n*.class\n\n\n# generated files\nbin/\ngen/\n\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n\n# Windows thumbnail db\nThumbs.db\n\n\n# OSX files\n.DS_Store\n\n\n# Eclipse project files\n.classpath\n.project\n\n\n# Android Studio\n.idea\n#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.\n.gradle\nbuild/\n\n\n# Signing files\n.signing/\n\n\n# User-specific configurations\n.idea/libraries/\n.idea/workspace.xml\n.idea/tasks.xml\n.idea/.name\n.idea/compiler.xml\n.idea/copyright/profiles_settings.xml\n.idea/encodings.xml\n.idea/misc.xml\n.idea/modules.xml\n.idea/scopes/scope_settings.xml\n.idea/vcs.xml\n*.iml\n"
  },
  {
    "path": "README.md",
    "content": "# JsBridge\n\n-----\n\nInspired and modified from [this](https://github.com/jacin1/JsBridge) and WeChat jsBridge file, with some bug fixes and feature enhancements.\n\nThis project makes a bridge between Java and JavaScript.\n\nIt provides a safe and convenient way to call Java code from JavaScript and call JavaScript code from Java.\n\n## How JsBridge Works\n![JsBridge](./JsBridgeWork.png)\n\n## Demo\n![JsBridge Demo](https://raw.githubusercontent.com/lzyzsd/JsBridge/master/JsBridge.gif)\n\n## Usage\n\n## JitPack.io\n\nI strongly recommend [JitPack.io](https://jitpack.io)\n\n```groovy\nrepositories {\n    // ...\n    maven { url \"https://jitpack.io\" }\n}\n\ndependencies {\n    compile 'com.github.lzyzsd:jsbridge:1.0.4'\n}\n```\n\n## Use it in Java\n\nAdd `com.github.lzyzsd.jsbridge.BridgeWebView` to your layout, it is inherited from WebView.\n\n### Register a Java handler function so that JavaScript can call\n\n```java\n\n    webView.registerHandler(\"submitFromWeb\", new BridgeHandler() {\n        @Override\n        public void handler(String data, CallBackFunction function) {\n            Log.i(TAG, \"handler = submitFromWeb, data from web = \" + data);\n            function.onCallBack(\"submitFromWeb exe, response data from Java\");\n        }\n    });\n\n```\n\nJavaScript can call this Java handler method \"submitFromWeb\" through:\n\n```javascript\n\n    WebViewJavascriptBridge.callHandler(\n        'submitFromWeb'\n        , {'param': str1}\n        , function(responseData) {\n            document.getElementById(\"show\").innerHTML = \"send get responseData from java, data = \" + responseData\n        }\n    );\n\n```\n\nYou can set a default handler in Java, so that JavaScript can send messages to Java without an assigned handlerName\n\n```java\n\n    webView.setDefaultHandler(new DefaultHandler());\n\n```\n\n```javascript\n\n    window.WebViewJavascriptBridge.doSend(\n        data\n        , function(responseData) {\n            document.getElementById(\"show\").innerHTML = \"responseData from java, data = \" + responseData\n        }\n    );\n\n```\n\n### Register a JavaScript handler function so that Java can call\n\n```javascript\n\n    WebViewJavascriptBridge.registerHandler(\"functionInJs\", function(data, responseCallback) {\n        document.getElementById(\"show\").innerHTML = (\"data from Java: = \" + data);\n        var responseData = \"Javascript Says Right back aka!\";\n        responseCallback(responseData);\n    });\n\n```\n\nJava can call this JavaScript handler function \"functionInJs\" through:\n\n```java\n\n    webView.callHandler(\"functionInJs\", new Gson().toJson(user), new CallBackFunction() {\n        @Override\n        public void onCallBack(String data) {\n\n        }\n    });\n\n```\nYou can also define a default handler using the init method, so that Java can send messages to JavaScript without an assigned handlerName\n\nFor example:\n\n```javascript\n\n    window.WebViewJavascriptBridge.init(function(message, responseCallback) {\n        console.log('JS got a message', message);\n        var data = {\n            'Javascript Responds': 'Wee!'\n        };\n        console.log('JS responding with', data);\n        responseCallback(data);\n    });\n\n```\n\n```java\n    webView.send(\"hello\");\n```\n\nwill print 'JS got a message hello' and 'JS responding with' in webview console.\n\n### Persistent Callbacks (New Feature)\n\nBy default, callbacks are deleted after first use. However, you can now use persistent callbacks that can be reused multiple times:\n\n#### Java Side\n\n```java\n// Use persistent callback that won't be deleted after first use\nwebView.callHandlerPersistent(\"functionInJs\", data, new OnBridgeCallback() {\n    @Override\n    public void onCallBack(String data) {\n        // This callback can be called multiple times\n        Log.d(TAG, \"Persistent callback called: \" + data);\n    }\n});\n```\n\n#### JavaScript Side\n\n```javascript\n// Use persistent callback\nWebViewJavascriptBridge.callHandlerPersistent(\"javaHandler\", data, function(response) {\n    // This callback can be reused multiple times\n    console.log(\"Persistent callback response: \" + response);\n});\n\n// Register and manually manage persistent callbacks\nvar callbackId = \"my_persistent_callback\";\nWebViewJavascriptBridge.registerPersistentCallback(callbackId, function(data) {\n    console.log(\"Persistent callback called: \" + data);\n});\n\n// Remove persistent callback when no longer needed\nWebViewJavascriptBridge.removePersistentCallback(callbackId);\n```\n\nThis 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.\n\n### Switch to CustomWebView\n* activity_main.xml\n```xml\n    <com.github.lzyzsd.jsbridge.example.CustomWebView\n        android:id=\"@+id/webView\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" >\n     </com.github.lzyzsd.jsbridge.example.CustomWebView>\n```\n* MainActivity.java\nChange BridgeWebView class to CustomWebView:\n```java\n    CustomWebView webView = (CustomWebView) findViewById(R.id.webView);\n    \n```\n\n## Notice\n\nThis library will inject a WebViewJavascriptBridge Object to the window object.\nYou can listen to the `WebViewJavascriptBridgeReady` event to ensure `window.WebViewJavascriptBridge` exists, as the below code shows:\n\n```javascript\n\n    if (window.WebViewJavascriptBridge) {\n        //do your work here\n    } else {\n        document.addEventListener(\n            'WebViewJavascriptBridgeReady'\n            , function() {\n                //do your work here\n            },\n            false\n        );\n    }\n\n```\n\nOr put all JsBridge function call into `window.WVJBCallbacks` array if `window.WebViewJavascriptBridge` is undefined, this task queue will be flushed when `WebViewJavascriptBridgeReady` event triggered.\n\nCopy and paste setupWebViewJavascriptBridge into your JS:\n\n```javascript\nfunction setupWebViewJavascriptBridge(callback) {\n\tif (window.WebViewJavascriptBridge) {\n        return callback(WebViewJavascriptBridge);\n    }\n\tif (window.WVJBCallbacks) {\n        return window.WVJBCallbacks.push(callback);\n    }\n\twindow.WVJBCallbacks = [callback];\n}\n```\n\nCall `setupWebViewJavascriptBridge` and then use the bridge to register handlers or call Java handlers:\n\n```javascript\nsetupWebViewJavascriptBridge(function(bridge) {\n\tbridge.registerHandler('JS Echo', function(data, responseCallback) {\n\t\tconsole.log(\"JS Echo called with:\", data);\n\t\tresponseCallback(data);\n    });\n\tbridge.callHandler('ObjC Echo', {'key':'value'}, function(responseData) {\n\t\tconsole.log(\"JS received response:\", responseData);\n\t});\n});\n```\n\nIt's the same as [WebViewJavascriptBridge](https://github.com/marcuswestin/WebViewJavascriptBridge), which makes it easier for you to define the same behavior across different platforms between Android and iOS, while writing concise code.\n\n## License\n\nThis project is licensed under the terms of the MIT license.\n"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    repositories {\n\n        jcenter()\n        google()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:4.1.3'\n        classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3'\n        // NOTE: Do not place your application dependencies here; they belong\n        // in the individual module build.gradle files\n    }\n}\n\nallprojects {\n    repositories {\n\n        jcenter()\n        maven {\n            url \"https://maven.google.com\"\n        }\n        google()\n    }\n    tasks.withType(Javadoc) { // 这一段是为了消除gbk的错�?\n        options{\n            encoding \"UTF-8\"\n            charSet 'UTF-8'\n            links \"http://docs.oracle.com/javase/7/docs/api\"\n        }\n    }\n}\n"
  },
  {
    "path": "example/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "example/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n    compileSdkVersion 28\n//    buildToolsVersion \"25.0.3\"\n\n    defaultConfig {\n        applicationId \"com.github.lzyzsd.jsbridge.example\"\n        minSdkVersion 17\n        targetSdkVersion 28\n        versionCode 1\n        versionName \"1.0\"\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n}\n\ndependencies {\n    implementation fileTree(dir: 'libs', include: ['*.jar'])\n    implementation project(':library')\n    implementation 'androidx.appcompat:appcompat:1.0.2'\n    implementation 'com.google.code.gson:gson:2.8.5'\n}\n"
  },
  {
    "path": "example/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /Users/bruce/sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n"
  },
  {
    "path": "example/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          package=\"com.github.lzyzsd.jsbridge.example\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n\n    <application android:allowBackup=\"true\"\n                 android:label=\"@string/app_name\"\n                 android:icon=\"@mipmap/ic_launcher\"\n                 android:theme=\"@style/AppTheme\">\n        <activity\n            android:name=\".MainActivity\"\n            android:label=\"@string/app_name\" >\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "example/src/main/assets/demo.html",
    "content": "<html>\n<head>\n    <meta content=\"text/html; charset=utf-8\" http-equiv=\"content-type\">\n    <title>\n        js调用java\n    </title>\n</head>\n\n<body>\n<p>\n    <xmp id=\"show\">\n    </xmp>\n</p>\n<p>\n    <xmp id=\"init\">\n    </xmp>\n</p>\n<p>\n    <input type=\"text\" id=\"text1\" value=\"用户名(username)\"/>\n</p>\n<p>\n    <input type=\"text\" id=\"text2\" value=\"password\"/>\n</p>\n<p>\n    <input type=\"button\" id=\"enter\" value=\"发消息给Native\" onclick=\"testClick();\"\n    />\n</p>\n<p>\n    <input type=\"button\" id=\"enter1\" value=\"调用Native方法\" onclick=\"testClick1();\"\n    />\n</p>\n<p>\n    <input type=\"button\" id=\"enter2\" value=\"显示html\" onclick=\"testDiv();\"/>\n</p>\n<p>\n    <input type=\"file\" value=\"打开文件\"/>\n</p>\n</body>\n<script>\n        function testDiv() {\n            document.getElementById(\"show\").innerHTML = document.getElementsByTagName(\"html\")[0].innerHTML;\n        }\n\n        function testClick() {\n            var str1 = document.getElementById(\"text1\").value;\n            var str2 = document.getElementById(\"text2\").value;\n\n            //send message to native\n            var data = {id: 1, content: \"这是一个图片 <img src=\\\"a.png\\\"/> test\\r\\nhahaha\"};\n            WebViewJavascriptBridge.doSend(\n                data\n                , function(responseData) {\n                    document.getElementById(\"show\").innerHTML = \"repsonseData from java, data = \" + responseData\n                }\n            );\n\n        }\n\n        function testClick1() {\n            var str1 = document.getElementById(\"text1\").value;\n            var str2 = document.getElementById(\"text2\").value;\n\n            //call native method\n            window.WebViewJavascriptBridge.callHandler(\n                'submitFromWeb'\n                , {'param': '中文测试'}\n                , function(responseData) {\n                    document.getElementById(\"show\").innerHTML = \"send get responseData from java, data = \" + responseData\n                }\n            );\n        }\n\n        function bridgeLog(logContent) {\n            document.getElementById(\"show\").innerHTML = logContent;\n        }\n\n        function connectWebViewJavascriptBridge(callback) {\n            if (window.WebViewJavascriptBridge && WebViewJavascriptBridge.inited) {\n                callback(WebViewJavascriptBridge)\n            } else {\n                document.addEventListener(\n                    'WebViewJavascriptBridgeReady'\n                    , function() {\n                        callback(WebViewJavascriptBridge)\n                    },\n                    false\n                );\n            }\n        }\n\n        connectWebViewJavascriptBridge(function(bridge) {\n            bridge.init(function(message, responseCallback) {\n                console.log('JS got a message', message);\n                var data = {\n                    'Javascript Responds': '测试中文!'\n                };\n\n                if (responseCallback) {\n                    console.log('JS responding with', data);\n                    responseCallback(data);\n                }\n            });\n\n            bridge.registerHandler(\"functionInJs\", function(data, responseCallback) {\n                document.getElementById(\"show\").innerHTML = (\"data from Java: = \" + data + Date.now());\n                if (responseCallback) {\n                    var responseData = \"Javascript Says Right back aka!\";\n                    responseCallback(responseData);\n                }\n            });\n        })\n\n</script>\n\n</html>\n\n"
  },
  {
    "path": "example/src/main/assets/persistent_callback_demo.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta content=\"text/html; charset=utf-8\" http-equiv=\"content-type\">\n    <title>Persistent Callback Demo</title>\n</head>\n\n<body>\n<h2>Persistent Callback Demo</h2>\n<p>This demo shows how to use persistent callbacks that can be reused multiple times.</p>\n\n<div id=\"log\" style=\"border: 1px solid #ccc; padding: 10px; height: 300px; overflow-y: scroll; margin: 10px 0;\"></div>\n\n<p>\n    <input type=\"button\" id=\"testPersistent\" value=\"Test Persistent Callback\" onclick=\"testPersistentCallback();\" />\n</p>\n<p>\n    <input type=\"button\" id=\"reuseCached\" value=\"Reuse Cached Callback\" onclick=\"reuseCachedCallback();\" />\n</p>\n<p>\n    <input type=\"button\" id=\"testNormal\" value=\"Test Normal Callback\" onclick=\"testNormalCallback();\" />\n</p>\n<p>\n    <input type=\"button\" id=\"clearLog\" value=\"Clear Log\" onclick=\"clearLog();\" />\n</p>\n\n<script>\n    var cachedCallback = null;\n    var callCount = 0;\n\n    function log(message) {\n        var logDiv = document.getElementById('log');\n        var timestamp = new Date().toLocaleTimeString();\n        logDiv.innerHTML += '[' + timestamp + '] ' + message + '<br>';\n        logDiv.scrollTop = logDiv.scrollHeight;\n        console.log(message);\n    }\n\n    function clearLog() {\n        document.getElementById('log').innerHTML = '';\n    }\n\n    function connectWebViewJavascriptBridge(callback) {\n        if (window.WebViewJavascriptBridge && WebViewJavascriptBridge.inited) {\n            callback(WebViewJavascriptBridge);\n        } else {\n            document.addEventListener(\n                'WebViewJavascriptBridgeReady',\n                function() {\n                    callback(WebViewJavascriptBridge);\n                },\n                false\n            );\n        }\n    }\n\n    connectWebViewJavascriptBridge(function(bridge) {\n        bridge.init(function(message, responseCallback) {\n            log('Bridge initialized');\n        });\n\n        // Register handler for persistent callback testing\n        bridge.registerHandler(\"persistentCallbackTest\", function(data, responseCallback) {\n            log('Handler called with data: ' + data);\n            \n            // Cache the callback for reuse\n            cachedCallback = responseCallback;\n            \n            // Respond immediately\n            if (responseCallback) {\n                responseCallback(\"Initial response from handler\");\n                log('Sent initial response');\n            }\n        });\n\n        log('Bridge ready and handlers registered');\n    });\n\n    function testPersistentCallback() {\n        log('Testing persistent callback...');\n        \n        // Use the new persistent callback method\n        WebViewJavascriptBridge.callHandlerPersistent('persistentCallbackTest', 'test data for persistent callback', function(response) {\n            callCount++;\n            log('Received response #' + callCount + ': ' + response);\n        });\n    }\n\n    function reuseCachedCallback() {\n        if (cachedCallback) {\n            try {\n                callCount++;\n                cachedCallback(\"Reused callback response #\" + callCount);\n                log('Successfully reused cached callback #' + callCount);\n            } catch (error) {\n                log('Error reusing cached callback: ' + error.message);\n            }\n        } else {\n            log('No cached callback available. Call \"Test Persistent Callback\" first.');\n        }\n    }\n\n    function testNormalCallback() {\n        log('Testing normal callback...');\n        \n        // Use normal callback (should be deleted after first use)\n        WebViewJavascriptBridge.callHandler('persistentCallbackTest', 'test data for normal callback', function(response) {\n            log('Received normal callback response: ' + response);\n        });\n    }\n</script>\n\n</body>\n</html>"
  },
  {
    "path": "example/src/main/java/com/github/lzyzsd/jsbridge/example/CustomWebView.java",
    "content": "package com.github.lzyzsd.jsbridge.example;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.os.Build;\nimport android.util.AttributeSet;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\n\nimport com.github.lzyzsd.jsbridge.BridgeHandler;\nimport com.github.lzyzsd.jsbridge.BridgeHelper;\nimport com.github.lzyzsd.jsbridge.IWebView;\nimport com.github.lzyzsd.jsbridge.OnBridgeCallback;\nimport com.github.lzyzsd.jsbridge.WebViewJavascriptBridge;\nimport com.google.gson.Gson;\n\nimport java.util.Map;\n\n/**\n * 采用BridgeHelper集成JsBridge功能示例.定制WebView,可只添加实际需要的JsBridge接口.\n *\n * @author ZhengAn\n * @date 2019-07-07\n */\n@SuppressLint(\"SetJavaScriptEnabled\")\npublic class CustomWebView extends WebView implements WebViewJavascriptBridge, IWebView {\n\n    private BridgeHelper bridgeHelper;\n\n    private Gson mGson;\n\n    public CustomWebView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init();\n    }\n\n    public CustomWebView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n        init();\n    }\n\n    public CustomWebView(Context context) {\n        super(context);\n        init();\n    }\n\n    private void init() {\n        this.setVerticalScrollBarEnabled(false);\n        this.setHorizontalScrollBarEnabled(false);\n        this.getSettings().setJavaScriptEnabled(true);\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {\n            WebView.setWebContentsDebuggingEnabled(true);\n        }\n\n        bridgeHelper = new BridgeHelper(this);\n        this.setWebViewClient(new WebViewClient() {\n            @Override\n            public void onPageFinished(WebView webView, String s) {\n                bridgeHelper.onPageFinished();\n            }\n\n            @Override\n            public boolean shouldOverrideUrlLoading(WebView webView, String s) {\n                return bridgeHelper.shouldOverrideUrlLoading(s);\n            }\n        });\n    }\n\n    /**\n     * @param handler default handler,handle messages send by js without assigned handler name,\n     *                if js message has handler name, it will be handled by named handlers registered by native\n     */\n    public void setDefaultHandler(BridgeHandler handler) {\n        bridgeHelper.setDefaultHandler(handler);\n    }\n\n    /**\n     * register handler,so that javascript can call it\n     * 注册处理程序,以便javascript调用它\n     *\n     * @param handlerName handlerName\n     * @param handler     BridgeHandler\n     */\n    public void registerHandler(String handlerName, BridgeHandler handler) {\n        bridgeHelper.registerHandler(handlerName, handler);\n    }\n\n    /**\n     * unregister handler\n     *\n     * @param handlerName\n     */\n    public void unregisterHandler(String handlerName) {\n        bridgeHelper.unregisterHandler(handlerName);\n    }\n\n    /**\n     * call javascript registered handler\n     * 调用javascript处理程序注册\n     *\n     * @param handlerName handlerName\n     * @param data        data\n     * @param callBack    CallBackFunction\n     */\n    public void callHandler(String handlerName, String data, OnBridgeCallback callBack) {\n        bridgeHelper.callHandler(handlerName, data, callBack);\n    }\n\n    @Override\n    public void sendToWeb(String data) {\n        sendToWeb(data, (OnBridgeCallback) null);\n    }\n\n    @Override\n    public void sendToWeb(String data, OnBridgeCallback responseCallback) {\n        bridgeHelper.sendToWeb(data, responseCallback);\n    }\n\n    @Override\n    public void sendToWeb(String function, Object... values) {\n        bridgeHelper.sendToWeb(function, values);\n    }\n\n    @Override\n    public void responseFromWeb(String data, String callbackId) {\n        bridgeHelper.responseFromWeb(data,callbackId);\n    }\n\n    public void setGson(Gson gson){\n        this.mGson = gson;\n    }\n\n    public Map<String, OnBridgeCallback> getCallbacks() {\n        return bridgeHelper.getCallbacks();\n    }\n\n    @Override\n    public WebView getWebView() {\n        return this;\n    }\n}\n"
  },
  {
    "path": "example/src/main/java/com/github/lzyzsd/jsbridge/example/MainActivity.java",
    "content": "package com.github.lzyzsd.jsbridge.example;\n\nimport android.app.Activity;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.os.Bundle;\nimport android.util.Log;\nimport android.view.View;\nimport android.view.View.OnClickListener;\nimport android.webkit.ValueCallback;\nimport android.webkit.WebChromeClient;\nimport android.webkit.WebView;\nimport android.widget.Button;\n\nimport com.github.lzyzsd.jsbridge.BridgeWebView;\nimport com.github.lzyzsd.jsbridge.OnBridgeCallback;\nimport com.google.gson.Gson;\n\npublic class MainActivity extends Activity implements OnClickListener {\n\n\tprivate final String TAG = \"MainActivity\";\n\n\tBridgeWebView webView;\n\n\tButton button;\n\n\tint RESULT_CODE = 0;\n\n\tValueCallback<Uri> mUploadMessage;\n\n\tValueCallback<Uri[]> mUploadMessageArray;\n\n    static class Location {\n        String address;\n    }\n\n    static class User {\n        String name;\n        Location location;\n        String testStr;\n    }\n\n\t@Override\n\tprotected void onCreate(Bundle savedInstanceState) {\n\t\tsuper.onCreate(savedInstanceState);\n\t\tsetContentView(R.layout.activity_main);\n\n        webView = (BridgeWebView) findViewById(R.id.webView);\n\n\t\tbutton = (Button) findViewById(R.id.button);\n\n\t\tbutton.setOnClickListener(this);\n\n\n\t\twebView.setWebChromeClient(new WebChromeClient() {\n\n\t\t\t@SuppressWarnings(\"unused\")\n\t\t\tpublic void openFileChooser(ValueCallback<Uri> uploadMsg, String AcceptType, String capture) {\n\t\t\t\tthis.openFileChooser(uploadMsg);\n\t\t\t}\n\n\t\t\t@SuppressWarnings(\"unused\")\n\t\t\tpublic void openFileChooser(ValueCallback<Uri> uploadMsg, String AcceptType) {\n\t\t\t\tthis.openFileChooser(uploadMsg);\n\t\t\t}\n\n\t\t\tpublic void openFileChooser(ValueCallback<Uri> uploadMsg) {\n\t\t\t\tmUploadMessage = uploadMsg;\n\t\t\t\tpickFile();\n\t\t\t}\n\n\t\t\t@Override\n\t\t\tpublic boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {\n\t\t\t\tmUploadMessageArray = filePathCallback;\n\t\t\t\tpickFile();\n\t\t\t\treturn true;\n\t\t\t}\n\t\t});\n\n\t\twebView.addJavascriptInterface(new MainJavascriptInterface(webView.getCallbacks(), webView.getPersistentCallbacks(), webView), \"WebViewJavascriptBridge\");\n\t\twebView.setGson(new Gson());\n\t\twebView.loadUrl(\"file:///android_asset/demo.html\");\n        User user = new User();\n        Location location = new Location();\n        location.address = \"SDU\";\n        user.location = location;\n        user.name = \"大头鬼\";\n\n        webView.callHandler(\"functionInJs\", new Gson().toJson(user), new OnBridgeCallback() {\n            @Override\n            public void onCallBack(String data) {\n\t\t\t\tLog.d(TAG, \"onCallBack: \" + data);\n            }\n        });\n\n        webView.sendToWeb(\"hello\");\n\n\t}\n\n\tpublic void pickFile() {\n\t\tIntent chooserIntent = new Intent(Intent.ACTION_GET_CONTENT);\n\t\tchooserIntent.setType(\"image/*\");\n\t\tstartActivityForResult(chooserIntent, RESULT_CODE);\n\t}\n\n\t@Override\n\tprotected void onActivityResult(int requestCode, int resultCode, Intent intent) {\n\t\tif (requestCode == RESULT_CODE) {\n\t\t\tif (null == mUploadMessage && null == mUploadMessageArray){\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif(null!= mUploadMessage && null == mUploadMessageArray){\n\t\t\t\tUri result = intent == null || resultCode != RESULT_OK ? null : intent.getData();\n\t\t\t\tmUploadMessage.onReceiveValue(result);\n\t\t\t\tmUploadMessage = null;\n\t\t\t}\n\n\t\t\tif(null == mUploadMessage && null != mUploadMessageArray){\n\t\t\t\tUri result = intent == null || resultCode != RESULT_OK ? null : intent.getData();\n\t\t\t\tif (result != null) {\n\t\t\t\t\tmUploadMessageArray.onReceiveValue(new Uri[]{result});\n\t\t\t\t}\n\t\t\t\tmUploadMessageArray = null;\n\t\t\t}\n\n\t\t}\n\t}\n\n\t@Override\n\tpublic void onClick(View v) {\n\t\tif (button.equals(v)) {\n            webView.callHandler(\"functionInJs\", \"data from Java\", new OnBridgeCallback() {\n\n\t\t\t\t@Override\n\t\t\t\tpublic void onCallBack(String data) {\n\t\t\t\t\t// TODO Auto-generated method stub\n\t\t\t\t\tLog.i(TAG, \"reponse data from js \" + data);\n\t\t\t\t}\n\n\t\t\t});\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "example/src/main/java/com/github/lzyzsd/jsbridge/example/MainJavascriptInterface.java",
    "content": "package com.github.lzyzsd.jsbridge.example;\n\nimport android.util.Log;\nimport android.webkit.JavascriptInterface;\n\nimport com.github.lzyzsd.jsbridge.BridgeWebView;\nimport com.github.lzyzsd.jsbridge.OnBridgeCallback;\nimport com.github.lzyzsd.jsbridge.WebViewJavascriptBridge;\n\nimport java.util.Map;\n\n/**\n * Created on 2019/7/10.\n * Author: bigwang\n * Description:\n */\npublic class MainJavascriptInterface extends BridgeWebView.BaseJavascriptInterface {\n\n    //WebJSbridge\n    private WebViewJavascriptBridge mWebView;\n\n    public MainJavascriptInterface(Map<String, OnBridgeCallback> callbacks, WebViewJavascriptBridge webView) {\n        super(callbacks);\n        mWebView = webView;\n    }\n\n    public MainJavascriptInterface(Map<String, OnBridgeCallback> callbacks, Map<String, OnBridgeCallback> persistentCallbacks, WebViewJavascriptBridge webView) {\n        super(callbacks, persistentCallbacks);\n        mWebView = webView;\n    }\n\n    public MainJavascriptInterface(Map<String, OnBridgeCallback> callbacks) {\n        super(callbacks);\n    }\n\n    @Override\n    public String send(String data) {\n        return \"it is default response\";\n    }\n\n\n    @JavascriptInterface\n    public void submitFromWeb(String data, String callbackId) {\n        Log.d(\"MainJavascriptInterface\", data + \", callbackId: \" + callbackId + \" \" + Thread.currentThread().getName());\n        mWebView.responseFromWeb(\"submitFromWeb response\", callbackId);\n    }\n}\n"
  },
  {
    "path": "example/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\" >\n\n    <!-- button 演示Java调用web -->\n    <Button \n        android:id=\"@+id/button\"\n        android:layout_width=\"match_parent\"\n        android:text=\"@string/button_name\"\n        android:layout_height=\"48dp\"\n        />\n    \n    <!-- webview 演示web调用Java -->\n    <com.github.lzyzsd.jsbridge.BridgeWebView\n        android:id=\"@+id/webView\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\" >\n     </com.github.lzyzsd.jsbridge.BridgeWebView>\n\n</LinearLayout>\n"
  },
  {
    "path": "example/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">JsBridge</string>\n    <string name=\"button_name\">Java调用Web</string>\n</resources>\n"
  },
  {
    "path": "example/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <!-- Customize your theme here. -->\n    </style>\n\n</resources>\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Wed Mar 24 16:24:00 CST 2021\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.5-bin.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "## Project-wide Gradle settings.\n#\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n#\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\n# Default value: -Xmx1024m -XX:MaxPermSize=256m\n# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8\n#\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n#Thu Jul 27 13:52:48 CST 2017\n#systemProp.https.proxyPort=1080\n#systemProp.http.proxyHost=127.0.0.1\n#systemProp.https.proxyHost=127.0.0.1\n#systemProp.http.proxyPort=1080\nandroid.enableJetifier=true\nandroid.useAndroidX=true"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\nesac\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched.\nif $cygwin ; then\n    [ -n \"$JAVA_HOME\" ] && JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\nfi\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >&-\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >&-\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windowz variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\ngoto execute\r\n\r\n:4NT_args\r\n@rem Get arguments from the 4NT Shell from JP Software\r\nset CMD_LINE_ARGS=%$\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "library/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "library/build.gradle",
    "content": "apply plugin: 'com.android.library'\n\nversion = \"1.0.0\"\n\nandroid {\n    compileSdkVersion 28\n//    buildToolsVersion \"25.0.3\"\n\n    defaultConfig {\n        minSdkVersion 17\n        targetSdkVersion 28\n        versionCode 1\n        versionName version\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n    lintOptions { // 消除lint警告\n        abortOnError false\n        checkReleaseBuilds false\n    }\n}\n\ndependencies {\n    implementation 'androidx.appcompat:appcompat:1.0.2'\n    implementation 'com.google.code.gson:gson:2.8.5'\n    \n    // Test dependencies\n    testImplementation 'junit:junit:4.13.2'\n    testImplementation 'org.mockito:mockito-core:3.12.4'\n    androidTestImplementation 'androidx.test.ext:junit:1.1.3'\n    androidTestImplementation 'androidx.test:core:1.4.0'\n    androidTestImplementation 'androidx.test:runner:1.4.0'\n    androidTestImplementation 'org.mockito:mockito-android:3.12.4'\n}\n\n\ndef siteUrl = 'https://github.com/lzyzsd/JsBridge'\ndef gitUrl = 'https://github.com/lzyzsd/JsBridge.git'\n// apply plugin: 'com.github.dcendents.android-maven'\ngroup = \"com.github.lzyzsd.jsbridge\"\ntask sourcesJar(type: Jar) {\n    from android.sourceSets.main.java.srcDirs\n    classifier = 'sources'\n}\ntask javadoc(type: Javadoc) {\n    source = android.sourceSets.main.java.srcDirs\n    classpath += project.files(android.getBootClasspath().join(File.pathSeparator))\n}\ntask javadocJar(type: Jar, dependsOn: javadoc) {\n    classifier = 'javadoc'\n    from javadoc.destinationDir\n}\nartifacts {\n    archives javadocJar\n    archives sourcesJar\n}\nProperties properties = new Properties()\nproperties.load(project.rootProject.file('local.properties').newDataInputStream())\n"
  },
  {
    "path": "library/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /Users/bruce/sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n"
  },
  {
    "path": "library/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          package=\"com.github.lzyzsd.library\">\n\n    <application android:allowBackup=\"true\"\n                 android:label=\"@string/app_name\"\n        >\n\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "library/src/main/assets/WebViewJavascriptBridge.js",
    "content": "//notation: js file can only use this kind of comments\n//since comments will cause error when use in webview.loadurl,\n//comments will be remove by java use regexp\n(function() {\n    if (window.WebViewJavascriptBridge && window.WebViewJavascriptBridge.inited) {\n        return;\n    }\n\n    var receiveMessageQueue = [];\n    var messageHandlers = {};\n    var sendMessageQueue = [];\n\n    var responseCallbacks = {};\n    var persistentCallbacks = {};\n    var uniqueId = 1;\n\n    var lastCallTime = 0;\n    var stoId = null;\n    var FETCH_QUEUE_INTERVAL = 20;\n    var messagingIframe;\n    var CUSTOM_PROTOCOL_SCHEME = \"yy\";\n    var QUEUE_HAS_MESSAGE = \"__QUEUE_MESSAGE__\";\n\n    // 创建消息index队列iframe\n    function _createQueueReadyIframe() {\n        messagingIframe = document.createElement('iframe');\n        messagingIframe.style.display = 'none';\n        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;\n        document.documentElement.appendChild(messagingIframe);\n    }\n    //创建消息体队列iframe\n    function _createQueueReadyIframe4biz() {\n        bizMessagingIframe = document.createElement('iframe');\n        bizMessagingIframe.style.display = 'none';\n        document.documentElement.appendChild(bizMessagingIframe);\n    }\n    //set default messageHandler  初始化默认的消息线程\n    function init(messageHandler) {\n        if (WebViewJavascriptBridge._messageHandler) {\n            throw new Error('WebViewJavascriptBridge.init called twice');\n        }\n        _createQueueReadyIframe();\n        _createQueueReadyIframe4biz();\n        WebViewJavascriptBridge._messageHandler = messageHandler;\n        var receivedMessages = receiveMessageQueue;\n        receiveMessageQueue = null;\n        for (var i = 0; i < receivedMessages.length; i++) {\n            _dispatchMessageFromNative(receivedMessages[i]);\n        }\n        WebViewJavascriptBridge.inited = true;\n    }\n\n    // 发送\n    function send(data, responseCallback) {\n        _doSend('send', data, responseCallback);\n    }\n\n    // 注册线程 往数组里面添加值\n    function registerHandler(handlerName, handler) {\n        messageHandlers[handlerName] = handler;\n    }\n\n    function removeHandler(handlerName, handler) {\n        delete messageHandlers[handlerName];\n    }\n\n    // Register a persistent callback that won't be deleted after first use\n    function registerPersistentCallback(callbackId, callback) {\n        persistentCallbacks[callbackId] = callback;\n        responseCallbacks[callbackId] = callback;\n    }\n\n    // Remove a persistent callback\n    function removePersistentCallback(callbackId) {\n        delete persistentCallbacks[callbackId];\n        delete responseCallbacks[callbackId];\n    }\n\n    // 调用线程\n    function callHandler(handlerName, data, responseCallback, persistent) {\n        // 如果方法不需要参数，只有回调函数，简化JS中的调用\n        if (arguments.length == 2 && typeof data == 'function') {\n\t\t\tresponseCallback = data;\n\t\t\tdata = null;\n\t\t}\n        _doSend(handlerName, data, responseCallback, persistent);\n    }\n\n    // Call handler with persistent callback that can be reused\n    function callHandlerPersistent(handlerName, data, responseCallback) {\n        if (arguments.length == 2 && typeof data == 'function') {\n\t\t\tresponseCallback = data;\n\t\t\tdata = null;\n\t\t}\n        _doSend(handlerName, data, responseCallback, true);\n    }\n\n    //sendMessage add message, 触发native处理 sendMessage\n    function _doSend(handlerName, message, responseCallback, persistent) {\n        var callbackId;\n        if(typeof responseCallback === 'string'){\n            callbackId = responseCallback;\n        } else if (responseCallback) {\n            callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();\n            responseCallbacks[callbackId] = responseCallback;\n            if (persistent) {\n                persistentCallbacks[callbackId] = responseCallback;\n            }\n            message.callbackId = callbackId;\n        }else{\n            callbackId = '';\n        }\n        try {\n             var fn = eval('WebViewJavascriptBridge.' + handlerName);\n         } catch(e) {\n             console.log(e);\n         }\n         if (typeof fn === 'function'){\n             var responseData = fn.call(WebViewJavascriptBridge, JSON.stringify(message), callbackId);\n             if(responseData){\n                 responseCallback = responseCallbacks[callbackId];\n                 if (!responseCallback) {\n                     return;\n                  }\n                 responseCallback(responseData);\n                 // Only delete if it's not a persistent callback\n                 if (!persistentCallbacks[callbackId]) {\n                     delete responseCallbacks[callbackId];\n                 }\n             }\n         }\n\n        sendMessageQueue.push(message);\n        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;\n    }\n\n    // 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容\n    function _fetchQueue() {\n        // 空数组直接返回\n        if (sendMessageQueue.length === 0) {\n          return;\n        }\n\n        // _fetchQueue 的调用间隔过短，延迟调用\n        if (new Date().getTime() - lastCallTime < FETCH_QUEUE_INTERVAL) {\n          if (!stoId) {\n            stoId = setTimeout(_fetchQueue, FETCH_QUEUE_INTERVAL);\n          }\n          return;\n        }\n\n        lastCallTime = new Date().getTime();\n        stoId = null;\n        var messageQueueString = JSON.stringify(sendMessageQueue);\n        sendMessageQueue = [];\n        //android can't read directly the return data, so we can reload iframe src to communicate with java\n        bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);\n    }\n\n    //提供给native使用,\n    function _dispatchMessageFromNative(messageJSON) {\n        setTimeout(function() {\n            var message = JSON.parse(messageJSON);\n            var responseCallback;\n            //java call finished, now need to call js callback function\n            if (message.responseId) {\n                responseCallback = responseCallbacks[message.responseId];\n                if (!responseCallback) {\n                    return;\n                }\n                responseCallback(message.responseData);\n                // Only delete if it's not a persistent callback\n                if (!persistentCallbacks[message.responseId]) {\n                    delete responseCallbacks[message.responseId];\n                }\n            } else {\n                //直接发送\n                if (message.callbackId) {\n                    var callbackResponseId = message.callbackId;\n                    responseCallback = function(responseData) {\n                        _doSend('response', responseData, callbackResponseId);\n                    };\n                }\n\n                var handler = WebViewJavascriptBridge._messageHandler;\n                if (message.handlerName) {\n                    handler = messageHandlers[message.handlerName];\n                }\n                //查找指定handler\n                try {\n                    handler(message.data, responseCallback);\n                } catch (exception) {\n                    if (typeof console != 'undefined') {\n                        console.log(\"WebViewJavascriptBridge: WARNING: javascript handler threw.\", message, exception);\n                    }\n                }\n            }\n        });\n    }\n\n    //提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以\n    function _handleMessageFromNative(messageJSON) {\n        if (receiveMessageQueue) {\n            receiveMessageQueue.push(messageJSON);\n        }\n        _dispatchMessageFromNative(messageJSON);\n\n    }\n\n    WebViewJavascriptBridge.init = init;\n    WebViewJavascriptBridge.doSend = send;\n    WebViewJavascriptBridge.registerHandler = registerHandler;\n    WebViewJavascriptBridge.removeHandler = removeHandler;\n    WebViewJavascriptBridge.callHandler = callHandler;\n    WebViewJavascriptBridge.callHandlerPersistent = callHandlerPersistent;\n    WebViewJavascriptBridge.registerPersistentCallback = registerPersistentCallback;\n    WebViewJavascriptBridge.removePersistentCallback = removePersistentCallback;\n    WebViewJavascriptBridge._handleMessageFromNative = _handleMessageFromNative;\n    WebViewJavascriptBridge._fetchQueue = _fetchQueue;\n\n    var readyEvent = document.createEvent('Events');\n    var jobs = window.WVJBCallbacks || [];\n    readyEvent.initEvent('WebViewJavascriptBridgeReady');\n    readyEvent.bridge = WebViewJavascriptBridge;\n    window.WVJBCallbacks = [];\n    jobs.forEach(function (job) {\n        job(WebViewJavascriptBridge)\n    });\n    document.dispatchEvent(readyEvent);\n})();\n"
  },
  {
    "path": "library/src/main/java/com/github/lzyzsd/jsbridge/BridgeHandler.java",
    "content": "package com.github.lzyzsd.jsbridge;\n\npublic interface BridgeHandler {\n    void handler(String data, OnBridgeCallback callBackFunction);\n}\n"
  },
  {
    "path": "library/src/main/java/com/github/lzyzsd/jsbridge/BridgeHelper.java",
    "content": "package com.github.lzyzsd.jsbridge;\n\nimport android.os.Looper;\nimport android.os.SystemClock;\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport com.google.gson.Gson;\n\nimport org.json.JSONObject;\n\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLDecoder;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n/**\n * JsBridge辅助类,帮助集成JsBridge功能.\n *\n * @author ZhengAn\n * @date 2019-06-30\n */\npublic class BridgeHelper implements WebViewJavascriptBridge {\n\n    private static final String TAG = \"BridgeHelper\";\n\n    private static final String BRIDGE_JS = \"WebViewJavascriptBridge.js\";\n    private Map<String, OnBridgeCallback> responseCallbacks = new HashMap<>();\n    private Map<String, BridgeHandler> messageHandlers = new HashMap<>();\n    private BridgeHandler defaultHandler = new DefaultHandler();\n\n    private List<Message> startupMessage = new ArrayList<>();\n\n    private List<Message> getStartupMessage() {\n        return startupMessage;\n    }\n\n    private void setStartupMessage(List<Message> startupMessage) {\n        this.startupMessage = startupMessage;\n    }\n\n    private long uniqueId = 0;\n\n    private IWebView webView;\n\n    public BridgeHelper(IWebView webView) {\n        this.webView = webView;\n    }\n\n    /**\n     * @param handler default handler,handle messages send by js without assigned handler name,\n     *                if js message has handler name, it will be handled by named handlers registered by native\n     */\n    public void setDefaultHandler(BridgeHandler handler) {\n        this.defaultHandler = handler;\n    }\n\n    /**\n     * 获取到CallBackFunction data执行调用并且从数据集移除\n     *\n     * @param url\n     */\n    private void handlerReturnData(String url) {\n        String functionName = BridgeUtil.getFunctionFromReturnUrl(url);\n        OnBridgeCallback f = responseCallbacks.get(functionName);\n        String data = BridgeUtil.getDataFromReturnUrl(url);\n        if (f != null) {\n            f.onCallBack(data);\n            responseCallbacks.remove(functionName);\n        }\n    }\n\n    /**\n     * 保存message到消息队列\n     *\n     * @param handlerName      handlerName\n     * @param data             data\n     * @param responseCallback CallBackFunction\n     */\n    private void doSend(String handlerName, String data, OnBridgeCallback responseCallback) {\n        Message m = new Message();\n        if (!TextUtils.isEmpty(data)) {\n            m.setData(data);\n        }\n        if (responseCallback != null) {\n            String callbackStr = String.format(BridgeUtil.CALLBACK_ID_FORMAT, ++uniqueId + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis()));\n            responseCallbacks.put(callbackStr, responseCallback);\n            m.setCallbackId(callbackStr);\n        }\n        if (!TextUtils.isEmpty(handlerName)) {\n            m.setHandlerName(handlerName);\n        }\n        queueMessage(m);\n    }\n\n    /**\n     * list<message> != null 添加到消息集合否则分发消息\n     *\n     * @param m Message\n     */\n    private void queueMessage(Message m) {\n        if (startupMessage != null) {\n            startupMessage.add(m);\n        } else {\n            dispatchMessage(m);\n        }\n    }\n\n    /**\n     * 分发message 必须在主线程才分发成功\n     *\n     * @param m Message\n     */\n    private void dispatchMessage(Message m) {\n        String messageJson = m.toJson();\n        //escape special characters for json string  为json字符串转义特殊字符\n        //系统原生API做Json转义替换手动转义，解决js解析数据格式报错\n        messageJson = JSONObject.quote(messageJson);\n        String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);\n        // 必须要找主线程才会将数据传递出去 --- 划重点\n        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {\n            this.loadUrl(javascriptCommand);\n        }\n    }\n\n    /**\n     * 刷新消息队列\n     */\n    private void flushMessageQueue() {\n        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {\n            loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new OnBridgeCallback() {\n\n                @Override\n                public void onCallBack(String data) {\n                    // deserializeMessage 反序列化消息\n                    List<Message> list = null;\n                    try {\n                        list = Message.toArrayList(data);\n                    } catch (Exception e) {\n                        Log.w(TAG, e);\n                        return;\n                    }\n                    if (list == null || list.isEmpty()) {\n                        return;\n                    }\n                    for (int i = 0; i < list.size(); i++) {\n                        Message m = list.get(i);\n                        String responseId = m.getResponseId();\n                        // 是否是response  CallBackFunction\n                        if (!TextUtils.isEmpty(responseId)) {\n                            OnBridgeCallback function = responseCallbacks.get(responseId);\n                            String responseData = m.getResponseData();\n                            function.onCallBack(responseData);\n                            responseCallbacks.remove(responseId);\n                        } else {\n                            OnBridgeCallback responseFunction = null;\n                            // if had callbackId 如果有回调Id\n                            final String callbackId = m.getCallbackId();\n                            if (!TextUtils.isEmpty(callbackId)) {\n                                responseFunction = new OnBridgeCallback() {\n                                    @Override\n                                    public void onCallBack(String data) {\n                                        Message responseMsg = new Message();\n                                        responseMsg.setResponseId(callbackId);\n                                        responseMsg.setResponseData(data);\n                                        queueMessage(responseMsg);\n                                    }\n                                };\n                            } else {\n                                responseFunction = new OnBridgeCallback() {\n                                    @Override\n                                    public void onCallBack(String data) {\n                                        // do nothing\n                                    }\n                                };\n                            }\n                            // BridgeHandler执行\n                            BridgeHandler handler;\n                            if (!TextUtils.isEmpty(m.getHandlerName())) {\n                                handler = messageHandlers.get(m.getHandlerName());\n                            } else {\n                                handler = defaultHandler;\n                            }\n                            if (handler != null) {\n                                handler.handler(m.getData(), responseFunction);\n                            }\n                        }\n                    }\n                }\n            });\n        }\n    }\n\n    private void loadUrl(String jsUrl, OnBridgeCallback returnCallback) {\n        this.loadUrl(jsUrl);\n        // 添加至 Map<String, CallBackFunction>\n        responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);\n    }\n\n    private void loadUrl(String jsUrl) {\n        webView.loadUrl(jsUrl);\n    }\n\n    /**\n     * register handler,so that javascript can call it\n     * 注册处理程序,以便javascript调用它\n     *\n     * @param handlerName handlerName\n     * @param handler     BridgeHandler\n     */\n    public void registerHandler(String handlerName, BridgeHandler handler) {\n        if (handler != null) {\n            // 添加至 Map<String, BridgeHandler>\n            messageHandlers.put(handlerName, handler);\n        }\n    }\n\n    /**\n     * unregister handler\n     *\n     * @param handlerName\n     */\n    public void unregisterHandler(String handlerName) {\n        if (handlerName != null) {\n            messageHandlers.remove(handlerName);\n        }\n    }\n\n    /**\n     * call javascript registered handler\n     * 调用javascript处理程序注册\n     *\n     * @param handlerName handlerName\n     * @param data        data\n     * @param callBack    CallBackFunction\n     */\n    public void callHandler(String handlerName, String data, OnBridgeCallback callBack) {\n        doSend(handlerName, data, callBack);\n    }\n\n    public void onPageFinished() {\n        webViewLoadLocalJs();\n\n        if (getStartupMessage() != null) {\n            for (Message m : getStartupMessage()) {\n                dispatchMessage(m);\n            }\n            setStartupMessage(null);\n        }\n    }\n\n    private void webViewLoadLocalJs() {\n        String jsContent = BridgeUtil.assetFile2Str(webView.getContext(), BridgeHelper.BRIDGE_JS);\n        loadUrl(\"javascript:\" + jsContent);\n    }\n\n    public boolean shouldOverrideUrlLoading(String url) {\n        try {\n            // decode 之前，处理 % 和 +\n            String replacedUrl = url.replaceAll(\"%(?![0-9a-fA-F]{2})\", \"%25\").replaceAll(\"\\\\+\", \"%2B\");\n            url = URLDecoder.decode(replacedUrl, \"UTF-8\");\n        } catch (UnsupportedEncodingException e) {\n            Log.w(TAG, e);\n        }\n\n        if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回数据\n            handlerReturnData(url);\n            return true;\n        } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //\n            flushMessageQueue();\n            return true;\n        }\n        return false;\n    }\n\n    @Override\n    public void sendToWeb(String data) {\n        sendToWeb(data, (OnBridgeCallback) null);\n    }\n\n    @Override\n    public void sendToWeb(String data, OnBridgeCallback responseCallback) {\n        doSend(null, data, responseCallback);\n    }\n\n    @Override\n    public void sendToWeb(String function, Object... values) {\n        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {\n            String jsCommand = String.format(function, values);\n            jsCommand = String.format(BridgeUtil.JAVASCRIPT_STR, jsCommand);\n            loadUrl(jsCommand);\n        }\n    }\n\n    public void sendResponse(Object data, String callbackId) {\n        if (!TextUtils.isEmpty(callbackId)) {\n            final Message response = new Message();\n            response.responseId = callbackId;\n            response.responseData = data instanceof String ? (String) data : new Gson().toJson(data);\n            if (Thread.currentThread() == Looper.getMainLooper().getThread()){\n                dispatchMessage(response);\n            }else {\n                webView.getWebView().post(new Runnable() {\n                    @Override\n                    public void run() {\n                        dispatchMessage(response);\n                    }\n                });\n            }\n        }\n    }\n\n    @Override\n    public void responseFromWeb(String data, String callbackId) {\n        sendResponse(data,callbackId);\n    }\n\n    public Map<String, OnBridgeCallback> getCallbacks() {\n        return responseCallbacks;\n    }\n\n}\n"
  },
  {
    "path": "library/src/main/java/com/github/lzyzsd/jsbridge/BridgeUtil.java",
    "content": "package com.github.lzyzsd.jsbridge;\n\nimport android.content.Context;\nimport android.webkit.WebView;\n\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\n\n\npublic class BridgeUtil {\n\tfinal static String YY_OVERRIDE_SCHEMA = \"yy://\";\n\tfinal static String YY_RETURN_DATA = YY_OVERRIDE_SCHEMA + \"return/\";//格式为   yy://return/{function}/returncontent\n\tfinal static String YY_FETCH_QUEUE = YY_RETURN_DATA + \"_fetchQueue/\";\n\tfinal static String EMPTY_STR = \"\";\n\tfinal static String UNDERLINE_STR = \"_\";\n\tfinal static String SPLIT_MARK = \"/\";\n\t\n\tfinal static String CALLBACK_ID_FORMAT = \"JAVA_CB_%s\";\n\tfinal static String JS_HANDLE_MESSAGE_FROM_JAVA = \"javascript:WebViewJavascriptBridge._handleMessageFromNative(%s);\";\n\tfinal static String JS_FETCH_QUEUE_FROM_JAVA = \"javascript:WebViewJavascriptBridge._fetchQueue();\";\n//\tpublic final static String JAVASCRIPT_STR = \"javascript:\";\n\n\n\tpublic static final String JAVA_SCRIPT = \"WebViewJavascriptBridge.js\";\n\tpublic final static String JAVASCRIPT_STR = \"javascript:%s\";\n\n\t/**\n\t * js 文件将注入为第一个script引用\n\t * @param view WebView\n\t * @param url url\n\t */\n\tpublic static void webViewLoadJs(WebView view, String url){\n\t\tString js = \"var newscript = document.createElement(\\\"script\\\");\";\n\t\tjs += \"newscript.src=\\\"\" + url + \"\\\";\";\n\t\tjs += \"document.scripts[0].parentNode.insertBefore(newscript,document.scripts[0]);\";\n\t\tview.loadUrl(\"javascript:\" + js);\n\t}\n\n\t/**\n\t * 这里只是加载lib包中assets中的 WebViewJavascriptBridge.js\n\t * @param view webview\n\t * @param path 路径\n\t */\n    public static void webViewLoadLocalJs(WebView view, String path){\n        String jsContent = assetFile2Str(view.getContext(), path);\n\t\tview.loadUrl(\"javascript:\" + jsContent);\n    }\n\n\t/**\n\t * 解析assets文件夹里面的代码,去除注释,取可执行的代码\n\t * @param c context\n\t * @param urlStr 路径\n\t * @return 可执行代码\n\t */\n\tpublic static String assetFile2Str(Context c, String urlStr){\n\t\tInputStream in = null;\n\t\ttry{\n\t\t\tin = c.getAssets().open(urlStr);\n            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));\n            String line = null;\n            StringBuilder sb = new StringBuilder();\n            do {\n                line = bufferedReader.readLine();\n                if (line != null && !line.matches(\"^\\\\s*\\\\/\\\\/.*\")) { // 去除注释\n                    sb.append(line);\n                }\n            } while (line != null);\n\n            bufferedReader.close();\n            in.close();\n \n            return sb.toString();\n\t\t} catch (Exception e) {\n\t\t\te.printStackTrace();\n\t\t} finally {\n\t\t\tif(in != null) {\n\t\t\t\ttry {\n\t\t\t\t\tin.close();\n\t\t\t\t} catch (IOException e) {\n\t\t\t\t\te.printStackTrace();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn null;\n\t}\n\n\tpublic static String getFunctionFromReturnUrl(String url) {\n\t\treturn \"\";\n\t}\n\n\tpublic static String getDataFromReturnUrl(String url) {\n\t\treturn \"\";\n\t}\n\n\tpublic static String parseFunctionName(String jsUrl) {\n\t\treturn \"\";\n\t}\n}\n"
  },
  {
    "path": "library/src/main/java/com/github/lzyzsd/jsbridge/BridgeWebView.java",
    "content": "package com.github.lzyzsd.jsbridge;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.os.Build;\nimport android.os.Looper;\nimport android.os.SystemClock;\nimport androidx.collection.ArrayMap;\nimport android.text.TextUtils;\nimport android.util.AttributeSet;\nimport android.webkit.WebView;\nimport android.util.Log;\nimport android.webkit.JavascriptInterface;\nimport android.webkit.WebSettings;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\n\nimport com.github.lzyzsd.library.BuildConfig;\nimport com.google.gson.Gson;\n\n\nimport org.json.JSONObject;\n\nimport java.net.URLEncoder;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\n\n@SuppressLint(\"SetJavaScriptEnabled\")\npublic class BridgeWebView extends WebView implements WebViewJavascriptBridge, BridgeWebViewClient.OnLoadJSListener {\n\n\tprivate final int URL_MAX_CHARACTER_NUM=2097152;\n    private Map<String, OnBridgeCallback> mCallbacks = new ArrayMap<>();\n    private Map<String, OnBridgeCallback> mPersistentCallbacks = new ArrayMap<>();\n\n    private List<Object> mMessages = new ArrayList<>();\n\n    private BridgeWebViewClient mClient;\n\n    private long mUniqueId = 0;\n\n    private boolean mJSLoaded = false;\n\n    private Gson mGson;\n\n    public BridgeWebView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        init();\n    }\n\n    public BridgeWebView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n        init();\n    }\n\n    public BridgeWebView(Context context) {\n        super(context);\n        init();\n    }\n\n    private void init() {\n        clearCache(true);\n        getSettings().setUseWideViewPort(true);\n//\t\twebView.getSettings().setLoadWithOverviewMode(true);\n        getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);\n        getSettings().setJavaScriptEnabled(true);\n//        mContent.setLayerType(View.LAYER_TYPE_SOFTWARE, null);\n        getSettings().setJavaScriptCanOpenWindowsAutomatically(true);\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && BuildConfig.DEBUG) {\n            WebView.setWebContentsDebuggingEnabled(true);\n        }\n        mClient = new BridgeWebViewClient(this);\n        super.setWebViewClient(mClient);\n    }\n\n    public void setGson(Gson gson) {\n        mGson = gson;\n    }\n\n    public boolean isJSLoaded() {\n        return mJSLoaded;\n    }\n\n    public Map<String, OnBridgeCallback> getCallbacks() {\n        return mCallbacks;\n    }\n\n    public Map<String, OnBridgeCallback> getPersistentCallbacks() {\n        return mPersistentCallbacks;\n    }\n\n    @Override\n    public void setWebViewClient(WebViewClient client) {\n        mClient.setWebViewClient(client);\n    }\n\n    @Override\n    public void onLoadStart() {\n        mJSLoaded = false;\n    }\n\n    @Override\n    public void onLoadFinished() {\n        mJSLoaded = true;\n        if (mMessages != null) {\n            for (Object message : mMessages) {\n                dispatchMessage(message);\n            }\n            mMessages = null;\n        }\n    }\n\n    @Override\n    public void sendToWeb(String data) {\n        sendToWeb(data, (OnBridgeCallback) null);\n    }\n\n    @Override\n    public void sendToWeb(String data, OnBridgeCallback responseCallback) {\n        doSend(null, data, responseCallback);\n    }\n\n    /**\n     * call javascript registered handler\n     * 调用javascript处理程序注册\n     *\n     * @param handlerName handlerName\n     * @param data        data\n     * @param callBack    OnBridgeCallback\n     */\n    public void callHandler(String handlerName, String data, OnBridgeCallback callBack) {\n        doSend(handlerName, data, callBack);\n    }\n\n    /**\n     * call javascript registered handler with persistent callback\n     * 调用javascript处理程序注册，使用持久回调\n     *\n     * @param handlerName handlerName\n     * @param data        data\n     * @param callBack    OnBridgeCallback (will be persistent and reusable)\n     */\n    public void callHandlerPersistent(String handlerName, String data, OnBridgeCallback callBack) {\n        doSendPersistent(handlerName, data, callBack);\n    }\n\n\n    @Override\n    public void sendToWeb(String function, Object... values) {\n        // 必须要找主线程才会将数据传递出去 --- 划重点\n        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {\n            String jsCommand = String.format(function, values);\n            jsCommand = String.format(BridgeUtil.JAVASCRIPT_STR, jsCommand);\n            loadUrl(jsCommand);\n        }\n    }\n\n    @Override\n    public void responseFromWeb(String data, String callbackId) {\n        sendResponse(data,callbackId);\n    }\n\n    /**\n     * 保存message到消息队列\n     *\n     * @param handlerName      handlerName\n     * @param data             data\n     * @param responseCallback OnBridgeCallback\n     */\n    private void doSend(String handlerName, Object data, OnBridgeCallback responseCallback) {\n        if (!(data instanceof String) && mGson == null){\n            return;\n        }\n        JSRequest request = new JSRequest();\n        if (data != null) {\n            request.data = data instanceof String ? (String) data : mGson.toJson(data);\n        }\n        if (responseCallback != null) {\n            String callbackId = String.format(BridgeUtil.CALLBACK_ID_FORMAT, (++mUniqueId) + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis()));\n            mCallbacks.put(callbackId, responseCallback);\n            request.callbackId = callbackId;\n        }\n        if (!TextUtils.isEmpty(handlerName)) {\n            request.handlerName = handlerName;\n        }\n        queueMessage(request);\n    }\n\n    /**\n     * 保存message到消息队列，使用持久回调\n     *\n     * @param handlerName      handlerName\n     * @param data             data\n     * @param responseCallback OnBridgeCallback (persistent)\n     */\n    private void doSendPersistent(String handlerName, Object data, OnBridgeCallback responseCallback) {\n        if (!(data instanceof String) && mGson == null){\n            return;\n        }\n        JSRequest request = new JSRequest();\n        if (data != null) {\n            request.data = data instanceof String ? (String) data : mGson.toJson(data);\n        }\n        if (responseCallback != null) {\n            String callbackId = String.format(BridgeUtil.CALLBACK_ID_FORMAT, (++mUniqueId) + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis()));\n            mCallbacks.put(callbackId, responseCallback);\n            mPersistentCallbacks.put(callbackId, responseCallback);\n            request.callbackId = callbackId;\n        }\n        if (!TextUtils.isEmpty(handlerName)) {\n            request.handlerName = handlerName;\n        }\n        queueMessage(request);\n    }\n\n    /**\n     * list<message> != null 添加到消息集合否则分发消息\n     *\n     * @param message Message\n     */\n    private void queueMessage(Object message) {\n        if (mMessages != null) {\n            mMessages.add(message);\n        } else {\n            dispatchMessage(message);\n        }\n    }\n\n    /**\n     * 分发message 必须在主线程才分发成功\n     *\n     * @param message Message\n     */\n    private void dispatchMessage(Object message) {\n        if (mGson == null){\n            return;\n        }\n        String messageJson = mGson.toJson(message);\n        //escape special characters for json string  为json字符串转义特殊字符\n\n\t\t  // 系统原生 API 做 Json转义，没必要自己正则替换，而且替换不一定完整\n        messageJson = JSONObject.quote(messageJson);\n        String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);\n        // 必须要找主线程才会将数据传递出去 --- 划重点\n        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {\n\t\t\tif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT&&javascriptCommand.length()>=URL_MAX_CHARACTER_NUM) {\n\t\t\t\tthis.evaluateJavascript(javascriptCommand,null);\n\t\t\t}else {\n\t\t\t\tthis.loadUrl(javascriptCommand);\n\t\t\t}\n        }\n    }\n\n    public void sendResponse(Object data, String callbackId) {\n        if (!(data instanceof String) && mGson == null){\n            return;\n        }\n        if (!TextUtils.isEmpty(callbackId)) {\n            final JSResponse response = new JSResponse();\n            response.responseId = callbackId;\n            response.responseData = data instanceof String ? (String) data : mGson.toJson(data);\n            if (Thread.currentThread() == Looper.getMainLooper().getThread()){\n                dispatchMessage(response);\n            }else {\n                post(new Runnable() {\n                    @Override\n                    public void run() {\n                        dispatchMessage(response);\n                    }\n                });\n            }\n        }\n    }\n\n    @Override\n    public void destroy() {\n        super.destroy();\n        mCallbacks.clear();\n        mPersistentCallbacks.clear();\n    }\n\n    public static abstract class BaseJavascriptInterface {\n\n        private Map<String, OnBridgeCallback> mCallbacks;\n        private Map<String, OnBridgeCallback> mPersistentCallbacks;\n\n        public BaseJavascriptInterface(Map<String, OnBridgeCallback> callbacks) {\n            mCallbacks = callbacks;\n        }\n\n        public BaseJavascriptInterface(Map<String, OnBridgeCallback> callbacks, Map<String, OnBridgeCallback> persistentCallbacks) {\n            mCallbacks = callbacks;\n            mPersistentCallbacks = persistentCallbacks;\n        }\n\n        @JavascriptInterface\n        public String send(String data, String callbackId) {\n            Log.d(\"BaseJavascriptInterface\", data + \", callbackId: \" + callbackId + \" \" + Thread.currentThread().getName());\n            return send(data);\n        }\n\n        @JavascriptInterface\n        public void response(String data, String responseId) {\n            Log.d(\"BaseJavascriptInterface\", data + \", responseId: \" + responseId + \" \" + Thread.currentThread().getName());\n            if (!TextUtils.isEmpty(responseId)) {\n                OnBridgeCallback function = mCallbacks.get(responseId);\n                if (function != null) {\n                    function.onCallBack(data);\n                    // Only remove if it's not a persistent callback\n                    if (mPersistentCallbacks == null || !mPersistentCallbacks.containsKey(responseId)) {\n                        mCallbacks.remove(responseId);\n                    }\n                }\n            }\n        }\n\n        public abstract String send(String data);\n    }\n\n}\n"
  },
  {
    "path": "library/src/main/java/com/github/lzyzsd/jsbridge/BridgeWebViewClient.java",
    "content": "package com.github.lzyzsd.jsbridge;\n\nimport android.graphics.Bitmap;\nimport android.net.http.SslError;\nimport android.os.Build;\nimport androidx.annotation.Nullable;\n\nimport android.util.Log;\nimport android.view.KeyEvent;\nimport android.webkit.ClientCertRequest;\nimport android.webkit.HttpAuthHandler;\nimport android.webkit.RenderProcessGoneDetail;\nimport android.webkit.SafeBrowsingResponse;\nimport android.webkit.SslErrorHandler;\nimport android.webkit.WebResourceError;\nimport android.webkit.WebResourceRequest;\nimport android.webkit.WebResourceResponse;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\n\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLDecoder;\n\n/**\n * 如果要自定义WebViewClient必须要集成此类\n * Created by bruce on 10/28/15.\n */\nclass BridgeWebViewClient extends WebViewClient {\n\n    private static final String TAG = \"BridgeWebViewClient\";\n    private OnLoadJSListener mListener;\n\n    private WebViewClient mClient;\n\n    public BridgeWebViewClient(OnLoadJSListener listener) {\n        mListener = listener;\n    }\n\n    public void setWebViewClient(WebViewClient client) {\n        mClient = client;\n    }\n\n    @Override\n    public boolean shouldOverrideUrlLoading(WebView view, String url) {\n        if (mClient != null) {\n            return mClient.shouldOverrideUrlLoading(view, url);\n        }\n        return interceptUrl(url) ? true : super.shouldOverrideUrlLoading(view, url);\n    }\n\n    private boolean interceptUrl(String url) {\n        try {\n            url = URLDecoder.decode(url, \"UTF-8\");\n        } catch (UnsupportedEncodingException e) {\n            e.printStackTrace();\n        }\n        Log.i(TAG, \"shouldOverrideUrlLoading, url = \" + url);\n        if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回数据\n            return true;\n        } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //\n            return true;\n        }\n        return false;\n    }\n\n    @Override\n    public void onPageStarted(WebView view, String url, Bitmap favicon) {\n        if (mClient != null) {\n            mClient.onPageStarted(view, url, favicon);\n        } else {\n            super.onPageStarted(view, url, favicon);\n        }\n\n    }\n\n    @Override\n    public void onPageFinished(WebView view, String url) {\n        if (mClient != null) {\n            mClient.onPageFinished(view, url);\n        } else {\n            super.onPageFinished(view, url);\n        }\n        mListener.onLoadStart();\n        BridgeUtil.webViewLoadLocalJs(view, BridgeUtil.JAVA_SCRIPT);\n        mListener.onLoadFinished();\n    }\n\n    @Override\n    public void onLoadResource(WebView view, String url) {\n        if (mClient != null) {\n            mClient.onLoadResource(view, url);\n        } else {\n            super.onLoadResource(view, url);\n        }\n    }\n\n    @Override\n    public void onPageCommitVisible(WebView view, String url) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && mClient != null) {\n            mClient.onPageCommitVisible(view, url);\n        } else {\n            super.onPageCommitVisible(view, url);\n        }\n    }\n\n    @Nullable\n    @Override\n    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {\n        if (mClient != null) {\n            return mClient.shouldInterceptRequest(view, url);\n        }\n        return super.shouldInterceptRequest(view, url);\n    }\n\n    @Nullable\n    @Override\n    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mClient != null) {\n            return mClient.shouldInterceptRequest(view, request);\n        }\n        return super.shouldInterceptRequest(view, request);\n    }\n\n    @Override\n    public void onTooManyRedirects(WebView view, android.os.Message cancelMsg, android.os.Message continueMsg) {\n        if (mClient != null) {\n            mClient.onTooManyRedirects(view, cancelMsg, continueMsg);\n        } else {\n            super.onTooManyRedirects(view, cancelMsg, continueMsg);\n        }\n    }\n\n    @Override\n    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {\n        if (mClient != null) {\n            mClient.onReceivedError(view, errorCode, description, failingUrl);\n        } else {\n            super.onReceivedError(view, errorCode, description, failingUrl);\n        }\n    }\n\n    @Override\n    public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && mClient != null) {\n            mClient.onReceivedError(view, request, error);\n        } else {\n            super.onReceivedError(view, request, error);\n        }\n    }\n\n    @Override\n    public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && mClient != null) {\n            mClient.onReceivedHttpError(view, request, errorResponse);\n        } else {\n            super.onReceivedHttpError(view, request, errorResponse);\n        }\n\n    }\n\n    @Override\n    public void onFormResubmission(WebView view, android.os.Message dontResend, android.os.Message resend) {\n        if (mClient != null) {\n            mClient.onFormResubmission(view, dontResend, resend);\n        } else {\n            super.onFormResubmission(view, dontResend, resend);\n        }\n\n    }\n\n    @Override\n    public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {\n        if (mClient != null) {\n            mClient.doUpdateVisitedHistory(view, url, isReload);\n        } else {\n            super.doUpdateVisitedHistory(view, url, isReload);\n        }\n    }\n\n    @Override\n    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {\n        if (mClient != null) {\n            mClient.onReceivedSslError(view, handler, error);\n        } else {\n            super.onReceivedSslError(view, handler, error);\n        }\n    }\n\n    @Override\n    public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && mClient != null) {\n            mClient.onReceivedClientCertRequest(view, request);\n        } else {\n            super.onReceivedClientCertRequest(view, request);\n        }\n    }\n\n    @Override\n    public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) {\n        if (mClient != null) {\n            mClient.onReceivedHttpAuthRequest(view, handler, host, realm);\n        } else {\n            super.onReceivedHttpAuthRequest(view, handler, host, realm);\n        }\n    }\n\n    @Override\n    public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {\n        if (mClient != null) {\n            return mClient.shouldOverrideKeyEvent(view, event);\n        }\n        return super.shouldOverrideKeyEvent(view, event);\n    }\n\n    @Override\n    public void onUnhandledKeyEvent(WebView view, KeyEvent event) {\n        if (mClient != null) {\n            mClient.onUnhandledKeyEvent(view, event);\n        } else {\n            super.onUnhandledKeyEvent(view, event);\n        }\n    }\n\n    @Override\n    public void onScaleChanged(WebView view, float oldScale, float newScale) {\n        if (mClient != null) {\n            mClient.onScaleChanged(view, oldScale, newScale);\n        } else {\n            super.onScaleChanged(view, oldScale, newScale);\n        }\n    }\n\n    @Override\n    public void onReceivedLoginRequest(WebView view, String realm, @Nullable String account, String args) {\n        if (mClient != null) {\n            mClient.onReceivedLoginRequest(view, realm, account, args);\n        } else {\n            super.onReceivedLoginRequest(view, realm, account, args);\n        }\n    }\n\n    @Override\n    public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mClient != null){\n            return mClient.onRenderProcessGone(view, detail);\n        }\n        return super.onRenderProcessGone(view, detail);\n    }\n\n    @Override\n    public void onSafeBrowsingHit(WebView view, WebResourceRequest request, int threatType, SafeBrowsingResponse callback) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 && mClient != null){\n            mClient.onSafeBrowsingHit(view, request, threatType, callback);\n        }else {\n            super.onSafeBrowsingHit(view, request, threatType, callback);\n        }\n    }\n\n\n    public interface OnLoadJSListener {\n\n        void onLoadStart();\n\n        void onLoadFinished();\n\n    }\n}"
  },
  {
    "path": "library/src/main/java/com/github/lzyzsd/jsbridge/DefaultHandler.java",
    "content": "package com.github.lzyzsd.jsbridge;\n\npublic class DefaultHandler implements BridgeHandler {\n    @Override\n    public void handler(String data, OnBridgeCallback callBackFunction) {\n\n    }\n}\n"
  },
  {
    "path": "library/src/main/java/com/github/lzyzsd/jsbridge/IWebView.java",
    "content": "package com.github.lzyzsd.jsbridge;\n\nimport android.content.Context;\nimport android.webkit.WebView;\n\n/**\n * WebView功能接口.\n *\n * @author ZhengAn\n * @date 2019-07-01\n */\npublic interface IWebView {\n\n    Context getContext();\n\n    void loadUrl(String url);\n\n    /**\n     * 获取当前Webview\n     */\n    WebView getWebView();\n}\n"
  },
  {
    "path": "library/src/main/java/com/github/lzyzsd/jsbridge/JSRequest.java",
    "content": "package com.github.lzyzsd.jsbridge;\n\n/**\n * Created on 2019/7/10.\n * Author: bigwang\n * Description:\n */\nclass JSRequest {\n\n    public String callbackId;\n\n    public String data;\n\n    public String handlerName;\n}\n"
  },
  {
    "path": "library/src/main/java/com/github/lzyzsd/jsbridge/JSResponse.java",
    "content": "package com.github.lzyzsd.jsbridge;\n\n/**\n * Created on 2019/7/10.\n * Author: bigwang\n * Description:\n */\nclass JSResponse {\n\n    public String responseId;\n\n    public String responseData;\n}\n"
  },
  {
    "path": "library/src/main/java/com/github/lzyzsd/jsbridge/Message.java",
    "content": "package com.github.lzyzsd.jsbridge;\n\nimport com.google.gson.Gson;\nimport com.google.gson.reflect.TypeToken;\n\nimport java.util.List;\n\npublic class Message {\n    public String responseId;\n    public String responseData;\n    public String callbackId;\n    public String data;\n    public String handlerName;\n\n    public static List<Message> toArrayList(String data) {\n        return new Gson().fromJson(data, new TypeToken<List<Message>>(){}.getType());\n    }\n\n    public String getResponseId() {\n        return responseId;\n    }\n\n    public void setResponseId(String responseId) {\n        this.responseId = responseId;\n    }\n\n    public String getResponseData() {\n        return responseData;\n    }\n\n    public void setResponseData(String responseData) {\n        this.responseData = responseData;\n    }\n\n    public String getCallbackId() {\n        return callbackId;\n    }\n\n    public void setData(String data) {\n        this.data = data;\n    }\n\n    public String getData() {\n        return data;\n    }\n\n    public void setCallbackId(String callbackStr) {\n        this.callbackId = callbackStr;\n    }\n\n    public void setHandlerName(String handlerName) {\n        this.handlerName = handlerName;\n    }\n\n    public String getHandlerName() {\n        return handlerName;\n    }\n\n    public String toJson() {\n        return new Gson().toJson(this);\n    }\n}\n"
  },
  {
    "path": "library/src/main/java/com/github/lzyzsd/jsbridge/OnBridgeCallback.java",
    "content": "package com.github.lzyzsd.jsbridge;\n\npublic interface OnBridgeCallback {\n\t\n\tvoid onCallBack(String data);\n\n}\n"
  },
  {
    "path": "library/src/main/java/com/github/lzyzsd/jsbridge/WebViewJavascriptBridge.java",
    "content": "package com.github.lzyzsd.jsbridge;\n\n\npublic interface WebViewJavascriptBridge {\n\t\n\tvoid sendToWeb(String data);\n\n\tvoid sendToWeb(String data, OnBridgeCallback responseCallback);\n\n\tvoid sendToWeb(String function, Object... values);\n\n\t/**\n\t * 处理从js返回的数据\n\t * @param data 数据\n\t * @param callbackId jsCallbackId\n\t */\n\tvoid responseFromWeb(String data,String callbackId);\n\n}\n"
  },
  {
    "path": "library/src/test/assets/test_persistent_callback.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"utf-8\">\n    <title>Persistent Callback Test</title>\n</head>\n<body>\n    <div id=\"test-results\"></div>\n    \n    <script>\n        // Test script for persistent callback functionality\n        var testResults = [];\n        var persistentCallbackCount = 0;\n        var cachedCallback = null;\n\n        function logResult(message) {\n            testResults.push(message);\n            document.getElementById('test-results').innerHTML = testResults.join('<br>');\n            console.log(message);\n        }\n\n        function connectWebViewJavascriptBridge(callback) {\n            if (window.WebViewJavascriptBridge && WebViewJavascriptBridge.inited) {\n                callback(WebViewJavascriptBridge);\n            } else {\n                document.addEventListener(\n                    'WebViewJavascriptBridgeReady',\n                    function() {\n                        callback(WebViewJavascriptBridge);\n                    },\n                    false\n                );\n            }\n        }\n\n        connectWebViewJavascriptBridge(function(bridge) {\n            bridge.init(function(message, responseCallback) {\n                logResult('Bridge initialized');\n            });\n\n            // Test persistent callback functionality\n            bridge.registerHandler(\"testPersistentCallback\", function(data, responseCallback) {\n                logResult('Handler called with data: ' + data);\n                \n                // Cache the callback for reuse\n                cachedCallback = responseCallback;\n                \n                // Respond immediately\n                if (responseCallback) {\n                    responseCallback(\"First response\");\n                    logResult('Sent first response');\n                }\n            });\n\n            // Function to test reusing the cached callback\n            window.testReuseCachedCallback = function() {\n                if (cachedCallback) {\n                    try {\n                        cachedCallback(\"Reused callback response \" + (++persistentCallbackCount));\n                        logResult('Successfully reused cached callback #' + persistentCallbackCount);\n                        return true;\n                    } catch (error) {\n                        logResult('Error reusing cached callback: ' + error.message);\n                        return false;\n                    }\n                } else {\n                    logResult('No cached callback available');\n                    return false;\n                }\n            };\n\n            // Test normal callback behavior\n            bridge.registerHandler(\"testNormalCallback\", function(data, responseCallback) {\n                logResult('Normal handler called with data: ' + data);\n                if (responseCallback) {\n                    responseCallback(\"Normal response\");\n                    logResult('Sent normal response');\n                }\n            });\n\n            logResult('Test handlers registered');\n        });\n\n        // Function to run all tests\n        window.runPersistentCallbackTests = function() {\n            logResult('Starting persistent callback tests...');\n            \n            // Test 1: Call handler and cache callback\n            WebViewJavascriptBridge.callHandler('testPersistentCallback', 'test data', function(response) {\n                logResult('Received response: ' + response);\n            });\n            \n            // Test 2: Try to reuse cached callback multiple times\n            setTimeout(function() {\n                for (var i = 0; i < 3; i++) {\n                    setTimeout(function() {\n                        testReuseCachedCallback();\n                    }, i * 100);\n                }\n            }, 500);\n        };\n    </script>\n</body>\n</html>"
  },
  {
    "path": "library/src/test/java/com/github/lzyzsd/jsbridge/PersistentCallbackTest.java",
    "content": "package com.github.lzyzsd.jsbridge;\n\nimport android.content.Context;\nimport android.webkit.WebView;\nimport androidx.test.core.app.ApplicationProvider;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\n\nimport com.google.gson.Gson;\n\nimport org.junit.Before;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport org.mockito.Mock;\nimport org.mockito.MockitoAnnotations;\n\nimport static org.junit.Assert.*;\nimport static org.mockito.Mockito.*;\n\n/**\n * Test class for persistent callback functionality\n * Tests the fix for issue #280 - persistent callbacks should be reusable\n */\n@RunWith(AndroidJUnit4.class)\npublic class PersistentCallbackTest {\n\n    private BridgeWebView bridgeWebView;\n    private Context context;\n\n    @Mock\n    private OnBridgeCallback mockCallback;\n\n    @Before\n    public void setUp() {\n        MockitoAnnotations.initMocks(this);\n        context = ApplicationProvider.getApplicationContext();\n        bridgeWebView = new BridgeWebView(context);\n        bridgeWebView.setGson(new Gson());\n    }\n\n    @Test\n    public void testPersistentCallbackReuse() {\n        // This test verifies that persistent callbacks can be reused multiple times\n        // without being deleted after first use\n        \n        final int[] callCount = {0};\n        \n        // Create a callback that should be persistent\n        OnBridgeCallback persistentCallback = new OnBridgeCallback() {\n            @Override\n            public void onCallBack(String data) {\n                callCount[0]++;\n            }\n        };\n\n        // Use the persistent callback method\n        bridgeWebView.callHandlerPersistent(\"testHandler\", \"testData\", persistentCallback);\n        \n        // Get the callback ID that was generated\n        String callbackId = null;\n        for (String id : bridgeWebView.getCallbacks().keySet()) {\n            if (bridgeWebView.getPersistentCallbacks().containsKey(id)) {\n                callbackId = id;\n                break;\n            }\n        }\n        \n        assertNotNull(\"Persistent callback should be stored\", callbackId);\n\n        // Simulate multiple responses from JavaScript\n        // This should work without the callback being deleted\n        bridgeWebView.sendResponse(\"response1\", callbackId);\n        bridgeWebView.sendResponse(\"response2\", callbackId);\n        bridgeWebView.sendResponse(\"response3\", callbackId);\n\n        // Verify the callback was called multiple times\n        assertEquals(\"Persistent callback should be called 3 times\", 3, callCount[0]);\n        \n        // Verify the callback is still available after multiple uses\n        assertTrue(\"Persistent callback should still exist after multiple uses\", \n                   bridgeWebView.getCallbacks().containsKey(callbackId));\n        assertTrue(\"Persistent callback should be marked as persistent\", \n                   bridgeWebView.getPersistentCallbacks().containsKey(callbackId));\n    }\n\n    @Test\n    public void testNormalCallbackBehavior() {\n        // This test verifies that normal (non-persistent) callbacks still work as before\n        // and are deleted after first use\n        \n        final int[] callCount = {0};\n        \n        OnBridgeCallback normalCallback = new OnBridgeCallback() {\n            @Override\n            public void onCallBack(String data) {\n                callCount[0]++;\n            }\n        };\n        \n        // Call handler with normal callback - should be deleted after first use\n        bridgeWebView.callHandler(\"testHandler\", \"testData\", normalCallback);\n        \n        // The callback should be stored initially\n        assertFalse(\"Normal callbacks should be stored initially\", \n                    bridgeWebView.getCallbacks().isEmpty());\n        \n        // Get the callback ID that was generated\n        String callbackId = null;\n        for (String id : bridgeWebView.getCallbacks().keySet()) {\n            callbackId = id;\n            break;\n        }\n        \n        assertNotNull(\"Normal callback should be stored\", callbackId);\n        \n        // Verify it's not marked as persistent\n        assertFalse(\"Normal callback should not be marked as persistent\", \n                    bridgeWebView.getPersistentCallbacks().containsKey(callbackId));\n        \n        // Simulate response from JavaScript\n        bridgeWebView.sendResponse(\"response\", callbackId);\n        \n        // Verify the callback was called\n        assertEquals(\"Normal callback should be called once\", 1, callCount[0]);\n        \n        // Try to call it again - should not work since it should be deleted\n        bridgeWebView.sendResponse(\"response2\", callbackId);\n        \n        // Verify the callback was not called again\n        assertEquals(\"Normal callback should not be called again after deletion\", 1, callCount[0]);\n    }\n\n    @Test\n    public void testCallbackIdGeneration() {\n        // Test that callback IDs are generated correctly\n        OnBridgeCallback callback1 = mock(OnBridgeCallback.class);\n        OnBridgeCallback callback2 = mock(OnBridgeCallback.class);\n        \n        bridgeWebView.callHandler(\"handler1\", \"data1\", callback1);\n        int size1 = bridgeWebView.getCallbacks().size();\n        \n        bridgeWebView.callHandler(\"handler2\", \"data2\", callback2);\n        int size2 = bridgeWebView.getCallbacks().size();\n        \n        assertEquals(\"Each callback should be stored with unique ID\", size1 + 1, size2);\n    }\n}"
  },
  {
    "path": "settings.gradle",
    "content": "include ':example', ':library'\n"
  }
]