From 2957783273bbe6c14c7671ecb3a8696d1141767f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E7=90=A5?= Date: Fri, 10 Apr 2020 19:13:50 +0800 Subject: [PATCH 01/14] =?UTF-8?q?=E4=BD=BF=E7=94=A8kotlin=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 + .../jsbridge/example/MainActivity.java | 4 - library/build.gradle | 6 +- .../main/assets/WebViewJavascriptBridge.js | 133 ++++++++++ library/src/main/assets/jsbridge | 3 - .../com/xiaomao/jsbridge/BridgeHandler.java | 7 - .../com/xiaomao/jsbridge/BridgeHandler.kt | 5 + .../java/com/xiaomao/jsbridge/BridgeUtil.java | 59 ----- .../java/com/xiaomao/jsbridge/BridgeUtil.kt | 12 + .../jsbridge/BridgeWebChromeClient.java | 201 --------------- .../com/xiaomao/jsbridge/BridgeWebView.java | 229 ------------------ .../com/xiaomao/jsbridge/BridgeWebView.kt | 36 +++ .../xiaomao/jsbridge/BridgeWebViewClient.java | 147 ----------- .../xiaomao/jsbridge/BridgeWebViewClient.kt | 185 ++++++++++++++ .../xiaomao/jsbridge/CallBackFunction.java | 7 - .../com/xiaomao/jsbridge/DefaultHandler.java | 12 - .../java/com/xiaomao/jsbridge/IWebView.kt | 13 + .../java/com/xiaomao/jsbridge/JsBridge.kt | 178 ++++++++++++++ .../main/java/com/xiaomao/jsbridge/Logger.kt | 32 +++ .../main/java/com/xiaomao/jsbridge/Message.kt | 31 +++ .../xiaomao/jsbridge/OnPageLoadListener.java | 5 - .../java/com/xiaomao/jsbridge/Request.java | 61 ----- .../java/com/xiaomao/jsbridge/Response.java | 50 ---- .../jsbridge/WebViewJavascriptBridge.java | 12 - .../jsbridge/WebViewJavascriptBridge.js | 137 ----------- 25 files changed, 630 insertions(+), 937 deletions(-) create mode 100644 library/src/main/assets/WebViewJavascriptBridge.js delete mode 100644 library/src/main/assets/jsbridge delete mode 100644 library/src/main/java/com/xiaomao/jsbridge/BridgeHandler.java create mode 100644 library/src/main/java/com/xiaomao/jsbridge/BridgeHandler.kt delete mode 100644 library/src/main/java/com/xiaomao/jsbridge/BridgeUtil.java create mode 100644 library/src/main/java/com/xiaomao/jsbridge/BridgeUtil.kt delete mode 100644 library/src/main/java/com/xiaomao/jsbridge/BridgeWebChromeClient.java delete mode 100644 library/src/main/java/com/xiaomao/jsbridge/BridgeWebView.java create mode 100644 library/src/main/java/com/xiaomao/jsbridge/BridgeWebView.kt delete mode 100644 library/src/main/java/com/xiaomao/jsbridge/BridgeWebViewClient.java create mode 100644 library/src/main/java/com/xiaomao/jsbridge/BridgeWebViewClient.kt delete mode 100644 library/src/main/java/com/xiaomao/jsbridge/CallBackFunction.java delete mode 100644 library/src/main/java/com/xiaomao/jsbridge/DefaultHandler.java create mode 100644 library/src/main/java/com/xiaomao/jsbridge/IWebView.kt create mode 100644 library/src/main/java/com/xiaomao/jsbridge/JsBridge.kt create mode 100644 library/src/main/java/com/xiaomao/jsbridge/Logger.kt create mode 100644 library/src/main/java/com/xiaomao/jsbridge/Message.kt delete mode 100644 library/src/main/java/com/xiaomao/jsbridge/OnPageLoadListener.java delete mode 100644 library/src/main/java/com/xiaomao/jsbridge/Request.java delete mode 100644 library/src/main/java/com/xiaomao/jsbridge/Response.java delete mode 100644 library/src/main/java/com/xiaomao/jsbridge/WebViewJavascriptBridge.java delete mode 100644 library/src/main/java/com/xiaomao/jsbridge/WebViewJavascriptBridge.js diff --git a/build.gradle b/build.gradle index 4ff022f..c60b441 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext.kotlin_version = '1.3.70' repositories { jcenter() google() } dependencies { classpath 'com.android.tools.build:gradle:3.6.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" //classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/example/src/main/java/com/xiaomao/jsbridge/example/MainActivity.java b/example/src/main/java/com/xiaomao/jsbridge/example/MainActivity.java index bb65650..3c25d11 100644 --- a/example/src/main/java/com/xiaomao/jsbridge/example/MainActivity.java +++ b/example/src/main/java/com/xiaomao/jsbridge/example/MainActivity.java @@ -12,11 +12,7 @@ import android.widget.Button; import com.google.gson.Gson; -import com.xiaomao.jsbridge.BridgeHandler; import com.xiaomao.jsbridge.BridgeWebView; -import com.xiaomao.jsbridge.CallBackFunction; -import com.xiaomao.jsbridge.DefaultHandler; -import com.xiaomao.jsbridge.OnPageLoadListener; public class MainActivity extends Activity implements OnClickListener { diff --git a/library/build.gradle b/library/build.gradle index 13d47d3..fff6ed4 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' //apply plugin: 'com.github.dcendents.android-maven' version = "1.0-alpha4" @@ -8,12 +9,11 @@ android { buildToolsVersion "29.0.3" defaultConfig { - minSdkVersion 14 + minSdkVersion 19 targetSdkVersion 29 versionCode 1 versionName version } - sourceSets.main.java.exclude 'com/xiaomao/jsbridge/WebViewJavascriptBridge.js' buildTypes { release { minifyEnabled false @@ -24,6 +24,7 @@ android { dependencies { implementation 'com.android.support:support-annotations:28.0.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } // build a jar with source files @@ -34,7 +35,6 @@ task sourcesJar(type: Jar) { task javadoc(type: Javadoc) { source = android.sourceSets.main.java.srcDirs - exclude 'com/xiaomao/jsbridge/WebViewJavascriptBridge.js' classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) } diff --git a/library/src/main/assets/WebViewJavascriptBridge.js b/library/src/main/assets/WebViewJavascriptBridge.js new file mode 100644 index 0000000..23b1d96 --- /dev/null +++ b/library/src/main/assets/WebViewJavascriptBridge.js @@ -0,0 +1,133 @@ +//notation: js file can only use this kind of comments +//since comments will cause error when use in webview.loadurl, +//comments will be remove by java use regexp +(function() { + if (window.WebViewJavascriptBridge) { + return; + } + + var receiveMessageQueue = []; + var messageHandlers = {}; + + var responseCallbacks = {}; + var uniqueId = 1; + + //set default messageHandler 初始化默认的消息线程 + function init(messageHandler) { + if (WebViewJavascriptBridge._messageHandler) { + throw new Error('WebViewJavascriptBridge.init called twice'); + } + WebViewJavascriptBridge._messageHandler = messageHandler; + var receivedMessages = receiveMessageQueue; + receiveMessageQueue = null; + for (var i = 0; i < receivedMessages.length; i++) { + _dispatchMessageFromNative(receivedMessages[i]); + } + } + + // 发送 + function send(data, responseCallback) { + _doSend('send', data, responseCallback); + } + + // 注册线程 往数组里面添加值 + function registerHandler(handlerName, handler) { + messageHandlers[handlerName] = handler; + } + // 调用线程 + function callHandler(handlerName, data, responseCallback) { + _doSend(handlerName, data, responseCallback); + } + + //sendMessage add message, 触发native处理 sendMessage + function _doSend(handlerName, message, responseCallback) { + var callbackId; + if(typeof responseCallback === 'string'){ + callbackId = responseCallback; + } else if (responseCallback) { + callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime(); + responseCallbacks[callbackId] = responseCallback; + }else{ + callbackId = ''; + } + try { + var fn = eval('window.android.' + handlerName); + } catch(e) { + console.log(e); + } + if (typeof fn === 'function'){ + var responseData = fn.call(this, JSON.stringify(message), callbackId); + if(responseData){ + console.log('response message: '+ responseData); + responseCallback = responseCallbacks[callbackId]; + if (!responseCallback) { + return; + } + responseCallback(responseData); + delete responseCallbacks[callbackId]; + } + } + } + + //提供给native使用, + function _dispatchMessageFromNative(messageJSON) { + setTimeout(function() { + var message = JSON.parse(messageJSON); + var responseCallback; + //java call finished, now need to call js callback function + if (message.responseId) { + responseCallback = responseCallbacks[message.responseId]; + if (!responseCallback) { + return; + } + responseCallback(message.responseData); + delete responseCallbacks[message.responseId]; + } else { + //直接发送 + if (message.callbackId) { + var callbackResponseId = message.callbackId; + responseCallback = function(responseData) { + _doSend('response', responseData, callbackResponseId); + }; + } + + var handler = WebViewJavascriptBridge._messageHandler; + if (message.handlerName) { + handler = messageHandlers[message.handlerName]; + } + //查找指定handler + try { + handler(message.data, responseCallback); + } catch (exception) { + if (typeof console != 'undefined') { + console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception); + } + } + } + }); + } + + //提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以 + function _handleMessageFromNative(messageJSON) { + console.log('handle message: '+ messageJSON); + if (receiveMessageQueue) { + receiveMessageQueue.push(messageJSON); + } + _dispatchMessageFromNative(messageJSON); + + } + + var WebViewJavascriptBridge = window.WebViewJavascriptBridge = { + init: init, + send: send, + registerHandler: registerHandler, + callHandler: callHandler, + _handleMessageFromNative: _handleMessageFromNative + }; + + var doc = document; + var readyEvent = doc.createEvent('Events'); + readyEvent.initEvent('WebViewJavascriptBridgeReady'); + readyEvent.bridge = WebViewJavascriptBridge; + doc.dispatchEvent(readyEvent); +})(); diff --git a/library/src/main/assets/jsbridge b/library/src/main/assets/jsbridge deleted file mode 100644 index e705cfd..0000000 --- a/library/src/main/assets/jsbridge +++ /dev/null @@ -1,3 +0,0 @@ -(function(){function h(b,a){if(a){var c="cb_"+n++ +"_"+(new Date).getTime();f[c]=a;b.id=c}prompt("gl://request/"+JSON.stringify(b))}function k(b){setTimeout(function(){var a=JSON.parse(b);var c=a.id?function(b){prompt("gl://response/"+JSON.stringify({id:a.id,data:b}))}:function(a){};var d=a.handlerName?l[a.handlerName]:e._requestHandler;if(void 0==d)console.log("WebViewJavascriptBridge: WARNING:no handler for ",a.handlerName);else try{d(a.data,c)}catch(p){console.error("WebViewJavascriptBridge: WARNING: javascript handler threw.", -p.stack)}})}if(!window.WebViewJavascriptBridge){var d=[],l={},f={},n=1;prompt("gl://pageLoaded/"+(0 Unit) +} \ No newline at end of file diff --git a/library/src/main/java/com/xiaomao/jsbridge/BridgeUtil.java b/library/src/main/java/com/xiaomao/jsbridge/BridgeUtil.java deleted file mode 100644 index 5fd013a..0000000 --- a/library/src/main/java/com/xiaomao/jsbridge/BridgeUtil.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.xiaomao.jsbridge; - -import android.content.Context; -import android.webkit.WebView; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; - -public class BridgeUtil { - public static final String toLoadJs = "jsbridge"; - final static String GL_SCHEMA = "gl://"; - final static String GL_RESPONSE = GL_SCHEMA + "response/"; - final static String GL_REQUEST = GL_SCHEMA + "request/"; - final static String GL_PAGE_LOADED = GL_SCHEMA + "pageLoaded/"; - final static String UNDERLINE_STR = "_"; - - final static String CALLBACK_ID_FORMAT = "JAVA_CB_%s"; - final static String JS_HANDLE_REQUEST_FROM_JAVA = "javascript:WebViewJavascriptBridge._handleRequestFromNative('%s');"; - final static String JS_HANDLE_RESPONSE_FROM_JAVA = "javascript:WebViewJavascriptBridge._handleResponseFromNative('%s');"; - - - public static void webViewLoadLocalJs(WebView view, String path) { - String jsContent = assetFile2Str(view.getContext(), path); - view.loadUrl("javascript:" + jsContent); - } - - public static String assetFile2Str(Context c, String urlStr) { - InputStream in = null; - try { - in = c.getAssets().open(urlStr); - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in)); - String line; - StringBuilder sb = new StringBuilder(); - do { - line = bufferedReader.readLine(); - if (line != null && !line.matches("^\\s*//.*")) { - sb.append(line); - } - } while (line != null); - - bufferedReader.close(); - in.close(); - - return sb.toString(); - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (in != null) { - try { - in.close(); - } catch (IOException e) { - } - } - } - return null; - } -} diff --git a/library/src/main/java/com/xiaomao/jsbridge/BridgeUtil.kt b/library/src/main/java/com/xiaomao/jsbridge/BridgeUtil.kt new file mode 100644 index 0000000..6988123 --- /dev/null +++ b/library/src/main/java/com/xiaomao/jsbridge/BridgeUtil.kt @@ -0,0 +1,12 @@ +package com.xiaomao.jsbridge + +object BridgeUtil { + const val JAVA_SCRIPT = "WebViewJavascriptBridge.js" + const val CALLBACK_ID_FORMAT = "JAVA_CB_%s" + const val JS_HANDLE_MESSAGE_FROM_JAVA = + "javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');" + const val JAVASCRIPT_STR = "javascript:%s" + const val UNDERLINE_STR = "_" + const val WebviewJavascriptBridge = + "(function(){if(window.WebViewJavascriptBridge){return}var receiveMessageQueue=[];var messageHandlers={};var responseCallbacks={};var uniqueId=1;function init(messageHandler){if(WebViewJavascriptBridge._messageHandler){throw new Error('WebViewJavascriptBridge.init called twice');}WebViewJavascriptBridge._messageHandler=messageHandler;var receivedMessages=receiveMessageQueue;receiveMessageQueue=null;for(var i=0;i callback) { - webChromeClient.getVisitedHistory(callback); - } - - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - @Override - public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) { - return webChromeClient.onShowFileChooser(webView, filePathCallback, fileChooserParams); - } - - @Override - public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { - if (interceptMessage((BridgeWebView) view, message)) { - result.confirm(); - return true; - } else { - return webChromeClient.onJsPrompt(view, url, message, defaultValue, result); - } - } - - private boolean interceptMessage(final BridgeWebView view, String message) { - if (message.startsWith(BridgeUtil.GL_REQUEST)) { - final Request request = Request.toObject(message.replace(BridgeUtil.GL_REQUEST, "")); - if (request != null) { - view.post(new Runnable() { - @Override - public void run() { - view.handleRequest(request); - } - }); - } - return true; - } else if (message.startsWith(BridgeUtil.GL_RESPONSE)) { - final Response response = Response.toObject(message.replace(BridgeUtil.GL_RESPONSE, "")); - if (response != null) { - view.post(new Runnable() { - @Override - public void run() { - view.handleResponse(response); - } - }); - } - return true; - } else if (message.startsWith(BridgeUtil.GL_PAGE_LOADED)) { - String result = message.replace(BridgeUtil.GL_PAGE_LOADED, ""); - view.onPageLoaded("ok".equals(result)); - return true; - } else { - return false; - } - } -} diff --git a/library/src/main/java/com/xiaomao/jsbridge/BridgeWebView.java b/library/src/main/java/com/xiaomao/jsbridge/BridgeWebView.java deleted file mode 100644 index c71611d..0000000 --- a/library/src/main/java/com/xiaomao/jsbridge/BridgeWebView.java +++ /dev/null @@ -1,229 +0,0 @@ -package com.xiaomao.jsbridge; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.os.Build; -import android.os.Looper; -import android.os.SystemClock; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.webkit.WebChromeClient; -import android.webkit.WebView; -import android.webkit.WebViewClient; - -import androidx.annotation.RequiresApi; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@SuppressLint("SetJavaScriptEnabled") -public class BridgeWebView extends WebView implements WebViewJavascriptBridge, OnPageLoadListener { - private OnPageLoadListener onPageLoadListener; - private Map responseCallbacks = new HashMap(); - private Map messageHandlers = new HashMap<>(); - private BridgeHandler defaultHandler = new DefaultHandler(); - - private List startupRequests = new ArrayList<>(); - - private BridgeWebViewClient bridgeWebViewClient; - private BridgeWebChromeClient bridgeWebChromeClient; - - private long uniqueId = 0; - - public void setOnPageLoadListener(OnPageLoadListener onPageLoadListener) { - this.onPageLoadListener = onPageLoadListener; - } - - public BridgeWebView(Context context, AttributeSet attrs) { - super(context, attrs); - init(); - } - - public BridgeWebView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(); - } - - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - public BridgeWebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - init(); - } - - @Deprecated - public BridgeWebView(Context context, AttributeSet attrs, int defStyleAttr, boolean privateBrowsing) { - super(context, attrs, defStyleAttr, privateBrowsing); - init(); - } - - public BridgeWebView(Context context) { - super(context); - init(); - } - - /** - * @param handler default handler,handle messages send by js without assigned handler name, - * if js message has handler name, it will be handled by named handlers registered by native - */ - public void setDefaultHandler(BridgeHandler handler) { - this.defaultHandler = handler; - } - - private void init() { - this.setVerticalScrollBarEnabled(false); - this.setHorizontalScrollBarEnabled(false); - this.getSettings().setJavaScriptEnabled(true); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - WebView.setWebContentsDebuggingEnabled(true); - } - bridgeWebViewClient = new BridgeWebViewClient(); - bridgeWebChromeClient = new BridgeWebChromeClient(); - super.setWebViewClient(bridgeWebViewClient); - super.setWebChromeClient(bridgeWebChromeClient); - } - - public void onPageLoaded(boolean isLoaded) { - if (onPageLoadListener != null) { - onPageLoadListener.onPageLoaded(isLoaded); - } - } - - @Override - public void setWebViewClient(WebViewClient client) { - bridgeWebViewClient.setWebViewClient(client); - } - - @Override - public void setWebChromeClient(WebChromeClient client) { - bridgeWebChromeClient.setWebChromeClient(client); - } - - - void sendStartupRequests() { - if (startupRequests != null) { - for (Request request : startupRequests) { - dispatchRequest(request); - } - startupRequests = null; - } - } - - @Override - public void send(String data) { - send(data, null); - } - - @Override - public void send(String data, CallBackFunction responseCallback) { - doSend(null, data, responseCallback); - } - - private void doSend(String handlerName, String data, CallBackFunction responseCallback) { - Request request = new Request(); - if (!TextUtils.isEmpty(data)) { - request.setData(data); - } - if (responseCallback != null) { - String callbackStr = String.format(BridgeUtil.CALLBACK_ID_FORMAT, ++uniqueId + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis())); - responseCallbacks.put(callbackStr, responseCallback); - request.setId(callbackStr); - } - if (!TextUtils.isEmpty(handlerName)) { - request.setHandlerName(handlerName); - } - if (startupRequests != null) { - startupRequests.add(request); - } else { - dispatchRequest(request); - } - } - - void dispatchRequest(Request request) { - String requestJson = request.toJson(); - //escape special characters for json string - requestJson = requestJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2"); - requestJson = requestJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\""); - String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_REQUEST_FROM_JAVA, requestJson); - if (Thread.currentThread() == Looper.getMainLooper().getThread()) { - this.loadUrl(javascriptCommand); - } - } - - void dispatchResponse(Response response) { - String responseJson = response.toJson(); - //escape special characters for json string - responseJson = responseJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2"); - responseJson = responseJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\""); - String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_RESPONSE_FROM_JAVA, responseJson); - if (Thread.currentThread() == Looper.getMainLooper().getThread()) { - this.loadUrl(javascriptCommand); - } - } - - void handleResponse(Response response) { - CallBackFunction function = responseCallbacks.get(response.getId()); - if (function != null) { - function.onCallBack(response.getData()); - responseCallbacks.remove(response.getId()); - } - } - - void handleRequest(final Request request) { - CallBackFunction responseFunction; - // if had callbackId - if (!TextUtils.isEmpty(request.getId())) { - responseFunction = new CallBackFunction() { - @Override - public void onCallBack(String data) { - Response response = new Response(); - response.setId(request.getId()); - response.setData(data); - dispatchResponse(response); - } - }; - } else { - responseFunction = new CallBackFunction() { - @Override - public void onCallBack(String data) { - // do nothing - } - }; - } - BridgeHandler handler; - if (!TextUtils.isEmpty(request.getHandlerName())) { - handler = messageHandlers.get(request.getHandlerName()); - } else { - handler = defaultHandler; - } - if (handler != null) { - handler.handler(request.getData(), responseFunction); - } - } - - /** - * register handler,so that javascript can call it - * - * @param handlerName - * @param handler - */ - public void registerHandler(String handlerName, BridgeHandler handler) { - if (handler != null) { - messageHandlers.put(handlerName, handler); - } - } - - /** - * call javascript registered handler - * - * @param handlerName - * @param data - * @param callBack - */ - public void callHandler(String handlerName, String data, CallBackFunction callBack) { - doSend(handlerName, data, callBack); - } - - -} diff --git a/library/src/main/java/com/xiaomao/jsbridge/BridgeWebView.kt b/library/src/main/java/com/xiaomao/jsbridge/BridgeWebView.kt new file mode 100644 index 0000000..06ba208 --- /dev/null +++ b/library/src/main/java/com/xiaomao/jsbridge/BridgeWebView.kt @@ -0,0 +1,36 @@ +package com.xiaomao.jsbridge + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.webkit.WebView +import android.webkit.WebViewClient + +@SuppressLint("SetJavaScriptEnabled") +class BridgeWebView : WebView, IWebView { + + val jsBridge: JsBridge = JsBridge(this) + private var bridgeWebViewClient: BridgeWebViewClient = BridgeWebViewClient(jsBridge) + + constructor(context: Context?) : super(context) + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + init { + super.setWebViewClient(bridgeWebViewClient) + } + + override fun setWebViewClient(webViewClient: WebViewClient) { + bridgeWebViewClient.webViewClient = webViewClient + } + + override fun destroy() { + super.destroy() + jsBridge.clean() + } + +} \ No newline at end of file diff --git a/library/src/main/java/com/xiaomao/jsbridge/BridgeWebViewClient.java b/library/src/main/java/com/xiaomao/jsbridge/BridgeWebViewClient.java deleted file mode 100644 index 8107fab..0000000 --- a/library/src/main/java/com/xiaomao/jsbridge/BridgeWebViewClient.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.xiaomao.jsbridge; - -import android.graphics.Bitmap; -import android.net.http.SslError; -import android.os.Build; -import android.os.Message; -import android.view.KeyEvent; -import android.webkit.ClientCertRequest; -import android.webkit.HttpAuthHandler; -import android.webkit.RenderProcessGoneDetail; -import android.webkit.SslErrorHandler; -import android.webkit.WebResourceError; -import android.webkit.WebResourceRequest; -import android.webkit.WebResourceResponse; -import android.webkit.WebView; -import android.webkit.WebViewClient; - -import androidx.annotation.RequiresApi; - - -class BridgeWebViewClient extends WebViewClient { - private WebViewClient webViewClient = new WebViewClient(); - - public void setWebViewClient(WebViewClient webViewClient) { - this.webViewClient = webViewClient; - } - - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - return webViewClient.shouldOverrideUrlLoading(view, url); - } - - @RequiresApi(api = Build.VERSION_CODES.N) - @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - return webViewClient.shouldOverrideUrlLoading(view, request); - } - - @Override - public void onPageStarted(WebView view, String url, Bitmap favicon) { - webViewClient.onPageStarted(view, url, favicon); - } - - @Override - public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { - return webViewClient.shouldOverrideKeyEvent(view, event); - } - - @Override - public void onPageFinished(WebView view, String url) { - BridgeUtil.webViewLoadLocalJs(view, BridgeUtil.toLoadJs); - ((BridgeWebView) view).sendStartupRequests(); - webViewClient.onPageFinished(view, url); - } - - @Override - public void onLoadResource(WebView view, String url) { - webViewClient.onLoadResource(view, url); - } - - @RequiresApi(api = Build.VERSION_CODES.M) - @Override - public void onPageCommitVisible(WebView view, String url) { - webViewClient.onPageCommitVisible(view, url); - } - - @Override - public WebResourceResponse shouldInterceptRequest(WebView view, String url) { - return webViewClient.shouldInterceptRequest(view, url); - } - - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - @Override - public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { - return webViewClient.shouldInterceptRequest(view, request); - } - - @Override - public void onTooManyRedirects(WebView view, Message cancelMsg, Message continueMsg) { - webViewClient.onTooManyRedirects(view, cancelMsg, continueMsg); - } - - @Override - public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { - webViewClient.onReceivedError(view, errorCode, description, failingUrl); - } - - @RequiresApi(api = Build.VERSION_CODES.M) - @Override - public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { - webViewClient.onReceivedError(view, request, error); - } - - @RequiresApi(api = Build.VERSION_CODES.M) - @Override - public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { - webViewClient.onReceivedHttpError(view, request, errorResponse); - } - - @Override - public void onFormResubmission(WebView view, Message dontResend, Message resend) { - webViewClient.onFormResubmission(view, dontResend, resend); - } - - @Override - public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { - webViewClient.doUpdateVisitedHistory(view, url, isReload); - } - - @Override - public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { - webViewClient.onReceivedSslError(view, handler, error); - } - - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - @Override - public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) { - webViewClient.onReceivedClientCertRequest(view, request); - } - - @Override - public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { - webViewClient.onReceivedHttpAuthRequest(view, handler, host, realm); - } - - @Override - public void onUnhandledKeyEvent(WebView view, KeyEvent event) { - webViewClient.onUnhandledKeyEvent(view, event); - } - - @Override - public void onScaleChanged(WebView view, float oldScale, float newScale) { - webViewClient.onScaleChanged(view, oldScale, newScale); - } - - @Override - public void onReceivedLoginRequest(WebView view, String realm, String account, String args) { - webViewClient.onReceivedLoginRequest(view, realm, account, args); - } - - @RequiresApi(api = Build.VERSION_CODES.O) - @Override - public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) { - return webViewClient.onRenderProcessGone(view, detail); - } -} \ No newline at end of file diff --git a/library/src/main/java/com/xiaomao/jsbridge/BridgeWebViewClient.kt b/library/src/main/java/com/xiaomao/jsbridge/BridgeWebViewClient.kt new file mode 100644 index 0000000..8b6e3d7 --- /dev/null +++ b/library/src/main/java/com/xiaomao/jsbridge/BridgeWebViewClient.kt @@ -0,0 +1,185 @@ +package com.xiaomao.jsbridge + +import android.graphics.Bitmap +import android.net.http.SslError +import android.os.Build +import android.os.Message +import android.view.KeyEvent +import android.webkit.* +import androidx.annotation.RequiresApi + +/** + * 如果要自定义WebViewClient必须要集成此类 + * Created by bruce on 10/28/15. + */ +internal class BridgeWebViewClient(private val jsBridge: JsBridge) : WebViewClient() { + internal var webViewClient: WebViewClient = WebViewClient() + + override fun shouldOverrideUrlLoading( + view: WebView?, + url: String? + ): Boolean { + return webViewClient.shouldOverrideUrlLoading(view, url) + } + + @RequiresApi(api = Build.VERSION_CODES.N) + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + return webViewClient.shouldOverrideUrlLoading(view, request) + } + + override fun onPageStarted( + view: WebView?, + url: String?, + favicon: Bitmap? + ) { + webViewClient.onPageStarted(view, url, favicon) + } + + override fun shouldOverrideKeyEvent( + view: WebView?, + event: KeyEvent? + ): Boolean { + return webViewClient.shouldOverrideKeyEvent(view, event) + } + + override fun onPageFinished(view: WebView, url: String?) { + webViewClient.onPageFinished(view, url) + jsBridge.register(view as IWebView) + } + + override fun onLoadResource(view: WebView?, url: String?) { + webViewClient.onLoadResource(view, url) + } + + @RequiresApi(api = Build.VERSION_CODES.M) + override fun onPageCommitVisible(view: WebView?, url: String?) { + webViewClient.onPageCommitVisible(view, url) + } + + override fun shouldInterceptRequest( + view: WebView?, + url: String? + ): WebResourceResponse? { + return webViewClient.shouldInterceptRequest(view, url) + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + override fun shouldInterceptRequest( + view: WebView?, + request: WebResourceRequest? + ): WebResourceResponse? { + return webViewClient.shouldInterceptRequest(view, request) + } + + override fun onTooManyRedirects( + view: WebView?, + cancelMsg: Message?, + continueMsg: Message? + ) { + webViewClient.onTooManyRedirects(view, cancelMsg, continueMsg) + } + + override fun onReceivedError( + view: WebView?, + errorCode: Int, + description: String?, + failingUrl: String? + ) { + webViewClient.onReceivedError(view, errorCode, description, failingUrl) + } + + @RequiresApi(api = Build.VERSION_CODES.M) + override fun onReceivedError( + view: WebView?, + request: WebResourceRequest?, + error: WebResourceError? + ) { + webViewClient.onReceivedError(view, request, error) + } + + @RequiresApi(api = Build.VERSION_CODES.M) + override fun onReceivedHttpError( + view: WebView?, + request: WebResourceRequest?, + errorResponse: WebResourceResponse? + ) { + webViewClient.onReceivedHttpError(view, request, errorResponse) + } + + override fun onFormResubmission( + view: WebView?, + dontResend: Message?, + resend: Message? + ) { + webViewClient.onFormResubmission(view, dontResend, resend) + } + + override fun doUpdateVisitedHistory( + view: WebView?, + url: String?, + isReload: Boolean + ) { + webViewClient.doUpdateVisitedHistory(view, url, isReload) + } + + override fun onReceivedSslError( + view: WebView?, + handler: SslErrorHandler?, + error: SslError? + ) { + webViewClient.onReceivedSslError(view, handler, error) + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + override fun onReceivedClientCertRequest( + view: WebView?, + request: ClientCertRequest? + ) { + webViewClient.onReceivedClientCertRequest(view, request) + } + + override fun onReceivedHttpAuthRequest( + view: WebView?, + handler: HttpAuthHandler?, + host: String?, + realm: String? + ) { + webViewClient.onReceivedHttpAuthRequest(view, handler, host, realm) + } + + override fun onUnhandledKeyEvent( + view: WebView?, + event: KeyEvent? + ) { + webViewClient.onUnhandledKeyEvent(view, event) + } + + override fun onScaleChanged( + view: WebView?, + oldScale: Float, + newScale: Float + ) { + webViewClient.onScaleChanged(view, oldScale, newScale) + } + + override fun onReceivedLoginRequest( + view: WebView?, + realm: String?, + account: String?, + args: String? + ) { + webViewClient.onReceivedLoginRequest(view, realm, account, args) + } + + @RequiresApi(api = Build.VERSION_CODES.O) + override fun onRenderProcessGone( + view: WebView?, + detail: RenderProcessGoneDetail? + ): Boolean { + return webViewClient.onRenderProcessGone(view, detail) + } + +} \ No newline at end of file diff --git a/library/src/main/java/com/xiaomao/jsbridge/CallBackFunction.java b/library/src/main/java/com/xiaomao/jsbridge/CallBackFunction.java deleted file mode 100644 index c01a92c..0000000 --- a/library/src/main/java/com/xiaomao/jsbridge/CallBackFunction.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.xiaomao.jsbridge; - -public interface CallBackFunction { - - void onCallBack(String data); - -} diff --git a/library/src/main/java/com/xiaomao/jsbridge/DefaultHandler.java b/library/src/main/java/com/xiaomao/jsbridge/DefaultHandler.java deleted file mode 100644 index 0771aed..0000000 --- a/library/src/main/java/com/xiaomao/jsbridge/DefaultHandler.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.xiaomao.jsbridge; - -public class DefaultHandler implements BridgeHandler{ - - @Override - public void handler(String data, CallBackFunction function) { - if(function != null){ - function.onCallBack("DefaultHandler response data."); - } - } - -} diff --git a/library/src/main/java/com/xiaomao/jsbridge/IWebView.kt b/library/src/main/java/com/xiaomao/jsbridge/IWebView.kt new file mode 100644 index 0000000..671ba21 --- /dev/null +++ b/library/src/main/java/com/xiaomao/jsbridge/IWebView.kt @@ -0,0 +1,13 @@ +package com.xiaomao.jsbridge + +import android.webkit.ValueCallback + +interface IWebView { + + fun loadUrl(url: String?) + + fun addJavascriptInterface(obj: Any?, interfaceName: String?) + + fun evaluateJavascript(var1: String?, callback: ValueCallback?) + +} \ No newline at end of file diff --git a/library/src/main/java/com/xiaomao/jsbridge/JsBridge.kt b/library/src/main/java/com/xiaomao/jsbridge/JsBridge.kt new file mode 100644 index 0000000..cab3ddb --- /dev/null +++ b/library/src/main/java/com/xiaomao/jsbridge/JsBridge.kt @@ -0,0 +1,178 @@ +package com.xiaomao.jsbridge + +import android.os.Handler +import android.os.Looper +import android.os.SystemClock +import android.util.ArrayMap +import android.webkit.JavascriptInterface +import java.net.URLEncoder +import java.util.* +import java.util.concurrent.atomic.AtomicLong +import kotlin.collections.HashMap + +private val EmptyResponseCallBack: (String) -> Unit = {} + + +class JsBridge(private val webView: IWebView) { + enum class State { + IDLE, RUN, DESTROY + } + + private var state: State = State.IDLE + + private val mUniqueId = AtomicLong(0) + + private val mMessageHandlers: MutableMap = HashMap() + + private val mCallbacks: MutableMap Unit> = ArrayMap() + + private var mRequests: MutableList = ArrayList() + + private val mainHandler = Handler(Looper.getMainLooper()) + + private fun checkState() { + require(state < State.DESTROY) { "JsBridge is destroyed." } + } + + /** + * register handler,so that javascript can call it + * 注册处理程序,以便javascript调用它 + * + * @param handlerName handlerName + * @param handler BridgeHandler + */ + @Synchronized + fun registerHandler(handlerName: String, handler: BridgeHandler) { + checkState() + // 添加至 Map + mMessageHandlers[handlerName] = handler + } + + + /** + * register handler,so that javascript can call it + * 注册处理程序,以便javascript调用它 + * + * @param handlers handlerName + */ + @Synchronized + fun registerHandler(handlers: Map) { + checkState() + mMessageHandlers.putAll(handlers) + } + + + /** + * unregister handler + * + * @param handlerName + */ + @Synchronized + fun unregisterHandler(handlerName: String) { + checkState() + mMessageHandlers.remove(handlerName) + } + + + @JavascriptInterface + @Synchronized + fun request(seqNo: String, handlerName: String, data: String) { + mMessageHandlers[handlerName]?.handle(data) { + dispatchMessage(JSResponse(seqNo, data)) + } + } + + @JavascriptInterface + @Synchronized + fun response(data: String, seqNo: String) { + Logger.d( + javaClass.simpleName, + "response->$data, seqNo: $seqNo ${Thread.currentThread().name}" + ) + mCallbacks.remove(seqNo)?.invoke(data) + } + + @Synchronized + fun register(view: IWebView) { + require(state == State.IDLE) { "JsBridge is registered." } + state = State.RUN + view.addJavascriptInterface(this, "jsbridge") + view.loadUrl( + String.format( + BridgeUtil.JAVASCRIPT_STR, + BridgeUtil.WebviewJavascriptBridge + ) + ) + for (request in mRequests) { + dispatchMessage(request) + } + } + + /** + * free memory + */ + @Synchronized + fun clean() { + state = State.DESTROY + mCallbacks.clear() + mMessageHandlers.clear() + mRequests.clear() + } + + /** + * 分发message 必须在主线程才分发成功 + * + * @param message Message + */ + private fun dispatchMessage(message: Message) { + var messageJson = message.toJson() + //escape special characters for json string 为json字符串转义特殊字符 + messageJson = messageJson.replace("(\\\\)([^utrn])".toRegex(), "\\\\\\\\$1$2") + messageJson = messageJson.replace("(?<=[^\\\\])(\")".toRegex(), "\\\\\"") + messageJson = messageJson.replace("(?<=[^\\\\])(\')".toRegex(), "\\\\\'") + messageJson = messageJson.replace("%7B".toRegex(), URLEncoder.encode("%7B")) + messageJson = messageJson.replace("%7D".toRegex(), URLEncoder.encode("%7D")) + messageJson = messageJson.replace("%22".toRegex(), URLEncoder.encode("%22")) + val javascriptCommand = + String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson) + Logger.d(javaClass.simpleName, "javascriptCommand->$javascriptCommand") + + // 必须要找主线程才会将数据传递出去 --- 划重点 + if (Thread.currentThread() === Looper.getMainLooper().thread) { + webView.evaluateJavascript(javascriptCommand, null) + } else { + mainHandler.post { webView.evaluateJavascript(javascriptCommand, null) } + } + } + + + /** + * 保存message到消息队列 + * + * @param handlerName handlerName + * @param data data + * @param responseCallback OnBridgeCallback + */ + @Synchronized + fun callHandler( + handlerName: String, + data: String, + responseCallback: (String) -> Unit = EmptyResponseCallBack + ) { + checkState() + require(handlerName.isNotEmpty()) { "Empty handler name." } + val sequenceId: String = String.format( + BridgeUtil.CALLBACK_ID_FORMAT, + "${mUniqueId.incrementAndGet()}" + BridgeUtil.UNDERLINE_STR + "${SystemClock.currentThreadTimeMillis()}" + ) + mCallbacks[sequenceId] = responseCallback + val request = JSRequest(sequenceId, handlerName, data) + //等待状态添加到消息集合否则分发消息 + if (state == State.IDLE) { + mRequests.add(request) + } else { + dispatchMessage(request) + } + } + +} \ No newline at end of file diff --git a/library/src/main/java/com/xiaomao/jsbridge/Logger.kt b/library/src/main/java/com/xiaomao/jsbridge/Logger.kt new file mode 100644 index 0000000..e6b2cc3 --- /dev/null +++ b/library/src/main/java/com/xiaomao/jsbridge/Logger.kt @@ -0,0 +1,32 @@ +package com.xiaomao.jsbridge + +import android.util.Log + +object Logger { + var debug: Boolean = false + + fun d(tag: String, message: String) { + if (debug) { + Log.d(tag, message) + } + } + + fun i(tag: String, message: String) { + if (debug) { + Log.i(tag, message) + } + } + + fun w(tag: String, message: String) { + if (debug) { + Log.w(tag, message) + } + } + + + fun e(tag: String, message: String) { + if (debug) { + Log.e(tag, message) + } + } +} \ No newline at end of file diff --git a/library/src/main/java/com/xiaomao/jsbridge/Message.kt b/library/src/main/java/com/xiaomao/jsbridge/Message.kt new file mode 100644 index 0000000..51b7ad2 --- /dev/null +++ b/library/src/main/java/com/xiaomao/jsbridge/Message.kt @@ -0,0 +1,31 @@ +package com.xiaomao.jsbridge + +import org.json.JSONObject + +interface Message { + fun toJson(): String +} + +data class JSRequest( + val seqNo: String, + val handlerName: String, + val data: String +) : Message { + override fun toJson(): String { + val jsonObject = JSONObject() + jsonObject.put("seqNo", seqNo) + jsonObject.put("handlerName", handlerName) + jsonObject.put("data", data) + return jsonObject.toString() + } +} + + +data class JSResponse(val seqNo: String, val data: String) : Message { + override fun toJson(): String { + val jsonObject = JSONObject() + jsonObject.put("seqNo", seqNo) + jsonObject.put("data", data) + return jsonObject.toString() + } +} \ No newline at end of file diff --git a/library/src/main/java/com/xiaomao/jsbridge/OnPageLoadListener.java b/library/src/main/java/com/xiaomao/jsbridge/OnPageLoadListener.java deleted file mode 100644 index 0d142e7..0000000 --- a/library/src/main/java/com/xiaomao/jsbridge/OnPageLoadListener.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.xiaomao.jsbridge; - -public interface OnPageLoadListener { - void onPageLoaded(boolean isLoaded); -} diff --git a/library/src/main/java/com/xiaomao/jsbridge/Request.java b/library/src/main/java/com/xiaomao/jsbridge/Request.java deleted file mode 100644 index 8ce965c..0000000 --- a/library/src/main/java/com/xiaomao/jsbridge/Request.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.xiaomao.jsbridge; - -import org.json.JSONException; -import org.json.JSONObject; - -public class Request { - private String id; - private String handlerName; - private String data; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getHandlerName() { - return handlerName; - } - - public void setHandlerName(String handlerName) { - this.handlerName = handlerName; - } - - public String getData() { - return data; - } - - public void setData(String data) { - this.data = data; - } - - public String toJson() { - try { - JSONObject jsonObject = new JSONObject(); - jsonObject.put("id", id); - jsonObject.put("handlerName", handlerName); - jsonObject.put("data", data); - return jsonObject.toString(); - } catch (JSONException e) { - e.printStackTrace(); - } - return null; - } - - public static Request toObject(String jsonStr) { - try { - Request request = new Request(); - JSONObject jsonObject = new JSONObject(jsonStr); - request.setId(jsonObject.has("id") ? jsonObject.getString("id") : ""); - request.setHandlerName(jsonObject.has("handlerName") ? jsonObject.getString("handlerName") : ""); - request.setData(jsonObject.has("data") ? jsonObject.getString("data") : ""); - return request; - } catch (JSONException e) { - e.printStackTrace(); - } - return null; - } -} diff --git a/library/src/main/java/com/xiaomao/jsbridge/Response.java b/library/src/main/java/com/xiaomao/jsbridge/Response.java deleted file mode 100644 index 23daf69..0000000 --- a/library/src/main/java/com/xiaomao/jsbridge/Response.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.xiaomao.jsbridge; - -import org.json.JSONException; -import org.json.JSONObject; - -public class Response { - private String id; - private String data; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getData() { - return data; - } - - public void setData(String data) { - this.data = data; - } - - public String toJson() { - try { - JSONObject jsonObject = new JSONObject(); - jsonObject.put("id", id); - jsonObject.put("data", data); - return jsonObject.toString(); - } catch (JSONException e) { - e.printStackTrace(); - } - return null; - } - - public static Response toObject(String jsonStr) { - try { - Response response = new Response(); - JSONObject jsonObject = new JSONObject(jsonStr); - response.setId(jsonObject.has("id") ? jsonObject.getString("id") : ""); - response.setData(jsonObject.has("data") ? jsonObject.getString("data") : ""); - return response; - } catch (JSONException e) { - e.printStackTrace(); - } - return null; - } -} diff --git a/library/src/main/java/com/xiaomao/jsbridge/WebViewJavascriptBridge.java b/library/src/main/java/com/xiaomao/jsbridge/WebViewJavascriptBridge.java deleted file mode 100644 index b3b52af..0000000 --- a/library/src/main/java/com/xiaomao/jsbridge/WebViewJavascriptBridge.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.xiaomao.jsbridge; - - -public interface WebViewJavascriptBridge { - void send(String data); - - void send(String data, CallBackFunction responseCallback); - - void registerHandler(String handlerName, BridgeHandler handler); - - void callHandler(String handlerName, String data, CallBackFunction callBack); -} diff --git a/library/src/main/java/com/xiaomao/jsbridge/WebViewJavascriptBridge.js b/library/src/main/java/com/xiaomao/jsbridge/WebViewJavascriptBridge.js deleted file mode 100644 index 7ed0fa7..0000000 --- a/library/src/main/java/com/xiaomao/jsbridge/WebViewJavascriptBridge.js +++ /dev/null @@ -1,137 +0,0 @@ -//use http://closure-compiler.appspot.com/home to generate jsbridge file -//notation: js file can only use this kind of comments -//since comments will cause error when use in webview.loadurl, -//comments will be remove by java use regexp -(function () { - if (window.WebViewJavascriptBridge) { - return; - } - - var receiveRequestQueue = []; - var requestHandlers = {}; - - var CUSTOM_PROTOCOL_SCHEME = 'gl://'; - var GL_REQUEST = CUSTOM_PROTOCOL_SCHEME + 'request/'; - var GL_RESPONSE = CUSTOM_PROTOCOL_SCHEME + 'response/'; - - var responseCallbacks = {}; - var uniqueId = 1; - - var state = window.location.host.length > 0 ? "ok" : "error"; - prompt(CUSTOM_PROTOCOL_SCHEME + 'pageLoaded/' + state); - - //set default requestHandler - function init(requestHandler) { - if (WebViewJavascriptBridge._requestHandler) { - throw new Error('WebViewJavascriptBridge.init called twice'); - } - WebViewJavascriptBridge._requestHandler = requestHandler; - var receivedRequests = receiveRequestQueue; - receiveRequestQueue = null; - for (var i = 0; i < receivedRequests.length; i++) { - _dispatchRequestFromNative(receivedRequests[i]); - } - } - - function send(data, responseCallback) { - _doSend({ - data: data - }, responseCallback); - } - - function registerHandler(handlerName, handler) { - requestHandlers[handlerName] = handler; - } - - function callHandler(handlerName, data, responseCallback) { - _doSend({ - handlerName: handlerName, - data: data - }, responseCallback); - } - - //sendRequest - function _doSend(request, responseCallback) { - if (responseCallback) { - var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime(); - responseCallbacks[callbackId] = responseCallback; - request.id = callbackId; - } - prompt(GL_REQUEST + JSON.stringify(request)); - } - - function _handleResponseFromNative(responseJson) { - setTimeout(function () { - var response = JSON.parse(responseJson); - var responseCallback = responseCallbacks[response.id]; - if (!responseCallback) { - return; - } - responseCallback(response.data); - delete responseCallbacks[response.id]; - }); - } - - //提供给native使用, - function _dispatchRequestFromNative(requestJSON) { - setTimeout(function () { - var request = JSON.parse(requestJSON); - var responseCallback; - if (request.id) { - responseCallback = function (responseData) { - var response = { - id: request.id, - data: responseData - }; - prompt(GL_RESPONSE + JSON.stringify(response)); - } - } else { - responseCallback = function (responseData) { - - } - } - //查找指定handler - var handler; - if (request.handlerName) { - handler = requestHandlers[request.handlerName]; - } else { - handler = WebViewJavascriptBridge._requestHandler; - } - if (handler == undefined) { - console.log("WebViewJavascriptBridge: WARNING:no handler for ", request.handlerName); - } else { - try { - handler(request.data, responseCallback); - } catch (exception) { - console.error('WebViewJavascriptBridge: WARNING: javascript handler threw.', exception.stack); - } - } - }); - } - - //提供给native调用,receiveRequestQueue 在会在页面加载完后赋值为null,所以 - function _handleRequestFromNative(requestJSON) { - //console.log(requestJSON); - if (receiveRequestQueue) { - receiveRequestQueue.push(requestJSON); - } else { - _dispatchRequestFromNative(requestJSON); - } - } - - var WebViewJavascriptBridge = window.WebViewJavascriptBridge = { - init: init, - send: send, - registerHandler: registerHandler, - callHandler: callHandler, - _handleRequestFromNative: _handleRequestFromNative, - _handleResponseFromNative: _handleResponseFromNative - }; - - var doc = document; - var readyEvent = doc.createEvent('Events'); - readyEvent.initEvent('WebViewJavascriptBridgeReady'); - readyEvent.bridge = WebViewJavascriptBridge; - doc.dispatchEvent(readyEvent); - -})(); From 7ee825327ff452836359a511c7d729804d9de395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E7=90=A5?= Date: Mon, 13 Apr 2020 18:46:09 +0800 Subject: [PATCH 02/14] =?UTF-8?q?=E4=BD=BF=E7=94=A8kotlin=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 + example/build.gradle | 6 +- example/src/main/AndroidManifest.xml | 13 +- example/src/main/assets/demo.html | 5 +- .../jsbridge/example/MainActivity.kt | 61 +++++ .../jsbridge/example/MainActivity.java | 139 ----------- example/src/main/res/layout/activity_main.xml | 4 +- library/bintray.gradle | 91 +++++++ library/build.gradle | 26 +- library/src/main/AndroidManifest.xml | 2 +- .../main/assets/WebViewJavascriptBridge.js | 138 +++++------ .../com/orientsec/jsbridge/BridgeHandler.kt | 7 + .../java/com/orientsec/jsbridge/BridgeUtil.kt | 11 + .../jsbridge/BridgeWebView.kt | 7 +- .../jsbridge/BridgeWebViewClient.kt | 2 +- .../jsbridge/IWebView.kt | 4 +- .../java/com/orientsec/jsbridge/JsBridge.kt | 232 ++++++++++++++++++ .../{xiaomao => orientsec}/jsbridge/Logger.kt | 2 +- .../java/com/orientsec/jsbridge/Message.kt | 7 + .../orientsec/jsbridge/OnPageLoadListener.kt | 5 + .../com/xiaomao/jsbridge/BridgeHandler.kt | 5 - .../java/com/xiaomao/jsbridge/BridgeUtil.kt | 12 - .../java/com/xiaomao/jsbridge/JsBridge.kt | 178 -------------- .../main/java/com/xiaomao/jsbridge/Message.kt | 31 --- peoject.properties | 18 ++ 25 files changed, 532 insertions(+), 481 deletions(-) create mode 100644 example/src/main/java/com/orientsec/jsbridge/example/MainActivity.kt delete mode 100644 example/src/main/java/com/xiaomao/jsbridge/example/MainActivity.java create mode 100644 library/bintray.gradle create mode 100644 library/src/main/java/com/orientsec/jsbridge/BridgeHandler.kt create mode 100644 library/src/main/java/com/orientsec/jsbridge/BridgeUtil.kt rename library/src/main/java/com/{xiaomao => orientsec}/jsbridge/BridgeWebView.kt (83%) rename library/src/main/java/com/{xiaomao => orientsec}/jsbridge/BridgeWebViewClient.kt (99%) rename library/src/main/java/com/{xiaomao => orientsec}/jsbridge/IWebView.kt (71%) create mode 100644 library/src/main/java/com/orientsec/jsbridge/JsBridge.kt rename library/src/main/java/com/{xiaomao => orientsec}/jsbridge/Logger.kt (94%) create mode 100644 library/src/main/java/com/orientsec/jsbridge/Message.kt create mode 100644 library/src/main/java/com/orientsec/jsbridge/OnPageLoadListener.kt delete mode 100644 library/src/main/java/com/xiaomao/jsbridge/BridgeHandler.kt delete mode 100644 library/src/main/java/com/xiaomao/jsbridge/BridgeUtil.kt delete mode 100644 library/src/main/java/com/xiaomao/jsbridge/JsBridge.kt delete mode 100644 library/src/main/java/com/xiaomao/jsbridge/Message.kt create mode 100644 peoject.properties diff --git a/build.gradle b/build.gradle index c60b441..fae62e9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,11 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. +Properties props = new Properties() +new File(rootDir, 'peoject.properties').withInputStream { props.load(it) } +props.each { + project.ext.set(it.key, it.value) +} + buildscript { ext.kotlin_version = '1.3.70' repositories { @@ -21,3 +27,4 @@ allprojects { google() } } + diff --git a/example/build.gradle b/example/build.gradle index ceb34f3..e71b375 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -1,12 +1,13 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' android { compileSdkVersion 29 buildToolsVersion "29.0.3" defaultConfig { - applicationId "com.xiaomao.jsbridge.example" - minSdkVersion 14 + applicationId "com.orientsec.jsbridge.example" + minSdkVersion 19 targetSdkVersion 29 versionCode 1 versionName "1.0" @@ -24,4 +25,5 @@ dependencies { implementation project(':library') implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'com.google.code.gson:gson:2.8.2' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml index 05a481e..6576e35 100644 --- a/example/src/main/AndroidManifest.xml +++ b/example/src/main/AndroidManifest.xml @@ -1,15 +1,16 @@ + package="com.orientsec.jsbridge.example"> - + + android:label="@string/app_name"> diff --git a/example/src/main/assets/demo.html b/example/src/main/assets/demo.html index c1dd3d1..64264c0 100644 --- a/example/src/main/assets/demo.html +++ b/example/src/main/assets/demo.html @@ -5,7 +5,7 @@ js调用java - +

@@ -103,7 +103,8 @@ var responseData = "Javascript Says Right back aka!"; responseCallback(responseData); }); - }) + }); + </script> </html> \ No newline at end of file diff --git a/example/src/main/java/com/orientsec/jsbridge/example/MainActivity.kt b/example/src/main/java/com/orientsec/jsbridge/example/MainActivity.kt new file mode 100644 index 0000000..556f9d1 --- /dev/null +++ b/example/src/main/java/com/orientsec/jsbridge/example/MainActivity.kt @@ -0,0 +1,61 @@ +package com.orientsec.jsbridge.example + +import android.app.Activity +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.Button +import android.widget.Toast +import com.google.gson.Gson +import com.orientsec.jsbridge.BridgeHandler +import com.orientsec.jsbridge.BridgeWebView +import com.orientsec.jsbridge.Logger + +class MainActivity : Activity(), View.OnClickListener { + private lateinit var webView: BridgeWebView + + data class Location( + val address: String + ) + + data class User( + val name: String, + val location: Location, + val testStr: String + ) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + Logger.debug = true + webView = findViewById(R.id.webView) + val settings = webView.settings + settings.javaScriptEnabled = true + val button = findViewById<Button>(R.id.button) + button.setOnClickListener(this) + webView.loadUrl("file:///android_asset/demo.html") + webView.jsBridge.registerHandler("submitFromWeb", object : BridgeHandler { + override fun handle(data: String, callback: (String) -> Unit) { + Log.i("MainActivity", "handler = submitFromWeb, data from web = $data") + callback("submitFromWeb exe, response data 中文 from Java") + } + }) + val location = + Location("SDU") + val user = User("大头鬼", location, "") + + webView.jsBridge.callHandler("functionInJs", Gson().toJson(user)) {} + + } + + override fun onClick(v: View) { + if (R.id.button == v.id) { + webView.jsBridge.callHandler( + "functionInJs", + "data from Java" + ) { + Toast.makeText(this, it, Toast.LENGTH_SHORT).show() + } + } + } +} \ No newline at end of file diff --git a/example/src/main/java/com/xiaomao/jsbridge/example/MainActivity.java b/example/src/main/java/com/xiaomao/jsbridge/example/MainActivity.java deleted file mode 100644 index 3c25d11..0000000 --- a/example/src/main/java/com/xiaomao/jsbridge/example/MainActivity.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.xiaomao.jsbridge.example; - -import android.app.Activity; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.util.Log; -import android.view.View; -import android.view.View.OnClickListener; -import android.webkit.ValueCallback; -import android.webkit.WebChromeClient; -import android.widget.Button; - -import com.google.gson.Gson; -import com.xiaomao.jsbridge.BridgeWebView; - -public class MainActivity extends Activity implements OnClickListener { - - private final String TAG = "MainActivity"; - - BridgeWebView webView; - - Button button; - - int RESULT_CODE = 0; - - ValueCallback<Uri> mUploadMessage; - - static class Location { - String address; - } - - static class User { - String name; - Location location; - String testStr; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - webView = findViewById(R.id.webView); - - button = findViewById(R.id.button); - - button.setOnClickListener(this); - webView.setDefaultHandler(new DefaultHandler()); - webView.setOnPageLoadListener(new OnPageLoadListener() { - @Override - public void onPageLoaded(boolean isLoaded) { - Log.i(TAG, "onPageLoaded:" + isLoaded); - } - }); - - webView.setWebChromeClient(new WebChromeClient() { - - @SuppressWarnings("unused") - public void openFileChooser(ValueCallback<Uri> uploadMsg, String AcceptType, String capture) { - this.openFileChooser(uploadMsg); - } - - @SuppressWarnings("unused") - public void openFileChooser(ValueCallback<Uri> uploadMsg, String AcceptType) { - this.openFileChooser(uploadMsg); - } - - public void openFileChooser(ValueCallback<Uri> uploadMsg) { - mUploadMessage = uploadMsg; - pickFile(); - } - - }); - - webView.loadUrl("file:///android_asset/demo.html"); - - webView.registerHandler("submitFromWeb", new BridgeHandler() { - - @Override - public void handler(String data, CallBackFunction function) { - Log.i(TAG, "handler = submitFromWeb, data from web = " + data); - function.onCallBack("submitFromWeb exe, response data 中文 from Java"); - } - - }); - - User user = new User(); - Location location = new Location(); - location.address = "SDU"; - user.location = location; - user.name = "大头鬼"; - - webView.callHandler("functionInJs", new Gson().toJson(user), new CallBackFunction() { - @Override - public void onCallBack(String data) { - - } - }); - - webView.send("hello"); - - } - - public void pickFile() { - Intent chooserIntent = new Intent(Intent.ACTION_GET_CONTENT); - chooserIntent.setType("image/*"); - startActivityForResult(chooserIntent, RESULT_CODE); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent intent) { - if (requestCode == RESULT_CODE) { - if (null == mUploadMessage) { - return; - } - Uri result = intent == null || resultCode != RESULT_OK ? null : intent.getData(); - mUploadMessage.onReceiveValue(result); - mUploadMessage = null; - } - } - - @Override - public void onClick(View v) { - if (button.equals(v)) { - webView.callHandler("functionInJs", "data from Java", new CallBackFunction() { - - @Override - public void onCallBack(String data) { - // TODO Auto-generated method stub - Log.i(TAG, "reponse data from js " + data); - } - - }); - } - - } - -} diff --git a/example/src/main/res/layout/activity_main.xml b/example/src/main/res/layout/activity_main.xml index f8f4f0d..549c9ac 100644 --- a/example/src/main/res/layout/activity_main.xml +++ b/example/src/main/res/layout/activity_main.xml @@ -13,10 +13,10 @@ /> <!-- webview 演示web调用Java --> - <com.xiaomao.jsbridge.BridgeWebView + <com.orientsec.jsbridge.BridgeWebView android:id="@+id/webView" android:layout_width="match_parent" android:layout_height="match_parent" > - </com.xiaomao.jsbridge.BridgeWebView> + </com.orientsec.jsbridge.BridgeWebView> </LinearLayout> diff --git a/library/bintray.gradle b/library/bintray.gradle new file mode 100644 index 0000000..9289300 --- /dev/null +++ b/library/bintray.gradle @@ -0,0 +1,91 @@ +//apply plugin: 'com.jfrog.bintray' +//apply plugin: "com.jfrog.artifactory" +apply plugin: 'maven-publish' + +task sourcesJar(type: Jar) { + from android.sourceSets.main.java.srcDirs + archiveClassifier.set('sources') +} + +task javadoc(type: Javadoc) { + options.addStringOption('Xdoclint:none', '-quiet') + options.charSet = 'UTF-8' + options.encoding = "UTF-8" + source = android.sourceSets.main.java.srcDirs + classpath += configurations.compile + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + archiveClassifier.set('javadoc') + from javadoc.destinationDir +} + +javadoc { + options { + encoding "UTF-8" + charSet 'UTF-8' + author true + version true + links "http://docs.oracle.com/javase/7/docs/api" + title "$project.name $PROJ_VERSION" + } +} + + +def pomConfig = { + licenses { + license { + name PROJ_LICENCE_NAME + url PROJ_LICENCE_URL + distribution PROJ_LICENCE_DIST + } + } + developers { + developer { + id DEVELOPER_ID + name DEVELOPER_NAME + email DEVELOPER_EMAIL + } + } +} + +publishing { + repositories { + maven { + url RELEASE_REPOSITORY_URL + credentials { + username NEXUS_USERNAME + password NEXUS_PASSWORD + } + } + } + publications { + mavenJava(MavenPublication) { + groupId PROJ_GROUP + artifactId PROJ_ARTIFACT + //artifact javadocJar + artifact sourcesJar + version = PROJ_VERSION + pom.withXml { + def root = asNode() + root.appendNode('description', PROJ_DESCRIPTION) + root.children().last() + pomConfig + + def dependenciesNode = root.appendNode('dependencies') + configurations.implementation.allDependencies.each { + if (it.group && it.name && it.version) { + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', it.group) + dependencyNode.appendNode('artifactId', it.name) + dependencyNode.appendNode('version', it.version) + } + } + } + } + } +} + +afterEvaluate { + publishing.publications.mavenJava.artifact(bundleReleaseAar) +} \ No newline at end of file diff --git a/library/build.gradle b/library/build.gradle index fff6ed4..50079d2 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -1,8 +1,6 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -//apply plugin: 'com.github.dcendents.android-maven' - -version = "1.0-alpha4" +apply from: 'bintray.gradle' android { compileSdkVersion 29 @@ -26,25 +24,3 @@ dependencies { implementation 'com.android.support:support-annotations:28.0.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } - -// build a jar with source files -task sourcesJar(type: Jar) { - from android.sourceSets.main.java.srcDirs - classifier = 'sources' -} - -task javadoc(type: Javadoc) { - source = android.sourceSets.main.java.srcDirs - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) -} - -// build a jar with javadoc -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir -} - -artifacts { - archives sourcesJar - archives javadocJar -} \ No newline at end of file diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml index 23c1924..f9b3d71 100644 --- a/library/src/main/AndroidManifest.xml +++ b/library/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ -<manifest package="com.xiaomao.jsbridge.library"> +<manifest package="com.orientsec.jsbridge.library"> </manifest> diff --git a/library/src/main/assets/WebViewJavascriptBridge.js b/library/src/main/assets/WebViewJavascriptBridge.js index 23b1d96..27ed4b0 100644 --- a/library/src/main/assets/WebViewJavascriptBridge.js +++ b/library/src/main/assets/WebViewJavascriptBridge.js @@ -1,10 +1,8 @@ -//notation: js file can only use this kind of comments -//since comments will cause error when use in webview.loadurl, -//comments will be remove by java use regexp -(function() { +(function () { if (window.WebViewJavascriptBridge) { return; } + callHandler("onPageLoad", window.location.host); var receiveMessageQueue = []; var messageHandlers = {}; @@ -21,108 +19,100 @@ var receivedMessages = receiveMessageQueue; receiveMessageQueue = null; for (var i = 0; i < receivedMessages.length; i++) { - _dispatchMessageFromNative(receivedMessages[i]); + var message = receivedMessages[i]; + _dispatchMessageFromNative(message.handlerName, message.data, message.callbackId); } } - // 发送 - function send(data, responseCallback) { - _doSend('send', data, responseCallback); - } - - // 注册线程 往数组里面添加值 + // 注册js handler function registerHandler(handlerName, handler) { messageHandlers[handlerName] = handler; } - // 调用线程 - function callHandler(handlerName, data, responseCallback) { - _doSend(handlerName, data, responseCallback); - } - //sendMessage add message, 触发native处理 sendMessage - function _doSend(handlerName, message, responseCallback) { + // 调用原生handler + function callHandler(handlerName, data, responseCallback) { var callbackId; - if(typeof responseCallback === 'string'){ - callbackId = responseCallback; - } else if (responseCallback) { + if (responseCallback) { callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime(); responseCallbacks[callbackId] = responseCallback; - }else{ - callbackId = ''; + } else { + callbackId = null; } + try { - var fn = eval('window.android.' + handlerName); - } catch(e) { - console.log(e); - } - if (typeof fn === 'function'){ - var responseData = fn.call(this, JSON.stringify(message), callbackId); - if(responseData){ - console.log('response message: '+ responseData); - responseCallback = responseCallbacks[callbackId]; - if (!responseCallback) { - return; - } - responseCallback(responseData); - delete responseCallbacks[callbackId]; - } - } + //调用原生JavascriptInterface + window.jsBridge.callHandler(handlerName, JSON.stringify(data), callbackId); + } catch (e) { + console.log(e); + } + } + + //向原生发送响应结果 + function _responseToNative(data, callbackId) { + try { + window.jsBridge.response(JSON.stringify(data), callbackId); + } catch (e) { + console.log(e); + } } - //提供给native使用, - function _dispatchMessageFromNative(messageJSON) { - setTimeout(function() { - var message = JSON.parse(messageJSON); + //提供给native使用 + function _responseFromNative(data, callbackId) { + setTimeout(function () { + var responseCallback = responseCallbacks[callbackId]; + if (!responseCallback) { + return; + } + responseCallback(data); + delete responseCallbacks[callbackId]; + }); + } + + function _dispatchMessageFromNative(handlerName, data, callbackId) { + setTimeout(function () { var responseCallback; - //java call finished, now need to call js callback function - if (message.responseId) { - responseCallback = responseCallbacks[message.responseId]; - if (!responseCallback) { - return; - } - responseCallback(message.responseData); - delete responseCallbacks[message.responseId]; + //直接发送 + if (callbackId) { + responseCallback = function (responseData) { + _responseToNative(responseData, callbackId); + }; + } + //查找指定handler + var handler; + if (handlerName) { + handler = messageHandlers[handlerName]; + } else { + handler = WebViewJavascriptBridge._requestHandler; + } + if (handler == undefined) { + console.log("WebViewJavascriptBridge: WARNING:no handler for ", request.handlerName); } else { - //直接发送 - if (message.callbackId) { - var callbackResponseId = message.callbackId; - responseCallback = function(responseData) { - _doSend('response', responseData, callbackResponseId); - }; - } - - var handler = WebViewJavascriptBridge._messageHandler; - if (message.handlerName) { - handler = messageHandlers[message.handlerName]; - } - //查找指定handler try { - handler(message.data, responseCallback); + handler(data, responseCallback); } catch (exception) { - if (typeof console != 'undefined') { - console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception); - } + console.error('WebViewJavascriptBridge: WARNING: javascript handler threw.', exception.stack); } } }); } //提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以 - function _handleMessageFromNative(messageJSON) { - console.log('handle message: '+ messageJSON); + function _handleMessageFromNative(handlerName, data, callbackId) { + console.log('handle message: ' + handlerName + ' callbackId:' + callbackId +" data:" + data); if (receiveMessageQueue) { - receiveMessageQueue.push(messageJSON); + var messageJson = {handlerName: handlerName, data: data, callbackId: callbackId}; + receiveMessageQueue.push(messageJson); } - _dispatchMessageFromNative(messageJSON); + _dispatchMessageFromNative(handlerName, data, callbackId); } var WebViewJavascriptBridge = window.WebViewJavascriptBridge = { init: init, - send: send, registerHandler: registerHandler, callHandler: callHandler, - _handleMessageFromNative: _handleMessageFromNative + _handleMessageFromNative: _handleMessageFromNative, + _responseFromNative: _responseFromNative }; var doc = document; @@ -130,4 +120,4 @@ readyEvent.initEvent('WebViewJavascriptBridgeReady'); readyEvent.bridge = WebViewJavascriptBridge; doc.dispatchEvent(readyEvent); -})(); +})(); \ No newline at end of file diff --git a/library/src/main/java/com/orientsec/jsbridge/BridgeHandler.kt b/library/src/main/java/com/orientsec/jsbridge/BridgeHandler.kt new file mode 100644 index 0000000..455011d --- /dev/null +++ b/library/src/main/java/com/orientsec/jsbridge/BridgeHandler.kt @@ -0,0 +1,7 @@ +package com.orientsec.jsbridge + +private val EmptyResponseCallBack: (String) -> Unit = {} + +interface BridgeHandler { + fun handle(data: String, callback: (String) -> Unit = EmptyResponseCallBack) +} \ No newline at end of file diff --git a/library/src/main/java/com/orientsec/jsbridge/BridgeUtil.kt b/library/src/main/java/com/orientsec/jsbridge/BridgeUtil.kt new file mode 100644 index 0000000..5800639 --- /dev/null +++ b/library/src/main/java/com/orientsec/jsbridge/BridgeUtil.kt @@ -0,0 +1,11 @@ +package com.orientsec.jsbridge + +object BridgeUtil { + const val JAVA_SCRIPT = "WebViewJavascriptBridge.js" + const val CALLBACK_ID_FORMAT = "JAVA_CB_%s" + const val JS_HANDLE_MESSAGE_FROM_JAVA = + "javascript:WebViewJavascriptBridge._handleMessageFromNative('%s','%s','%s');" + const val JS_RESPONSE_FROM_JAVA = + "javascript:WebViewJavascriptBridge._responseFromNative('%s','%s');" + const val UNDERLINE_STR = "_" +} diff --git a/library/src/main/java/com/xiaomao/jsbridge/BridgeWebView.kt b/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt similarity index 83% rename from library/src/main/java/com/xiaomao/jsbridge/BridgeWebView.kt rename to library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt index 06ba208..88c1de4 100644 --- a/library/src/main/java/com/xiaomao/jsbridge/BridgeWebView.kt +++ b/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt @@ -1,4 +1,4 @@ -package com.xiaomao.jsbridge +package com.orientsec.jsbridge import android.annotation.SuppressLint import android.content.Context @@ -22,6 +22,10 @@ class BridgeWebView : WebView, IWebView { init { super.setWebViewClient(bridgeWebViewClient) + addJavascriptInterface(jsBridge, "jsBridge") + if (Logger.debug) { + setWebContentsDebuggingEnabled(true) + } } override fun setWebViewClient(webViewClient: WebViewClient) { @@ -31,6 +35,7 @@ class BridgeWebView : WebView, IWebView { override fun destroy() { super.destroy() jsBridge.clean() + context } } \ No newline at end of file diff --git a/library/src/main/java/com/xiaomao/jsbridge/BridgeWebViewClient.kt b/library/src/main/java/com/orientsec/jsbridge/BridgeWebViewClient.kt similarity index 99% rename from library/src/main/java/com/xiaomao/jsbridge/BridgeWebViewClient.kt rename to library/src/main/java/com/orientsec/jsbridge/BridgeWebViewClient.kt index 8b6e3d7..d20f8b8 100644 --- a/library/src/main/java/com/xiaomao/jsbridge/BridgeWebViewClient.kt +++ b/library/src/main/java/com/orientsec/jsbridge/BridgeWebViewClient.kt @@ -1,4 +1,4 @@ -package com.xiaomao.jsbridge +package com.orientsec.jsbridge import android.graphics.Bitmap import android.net.http.SslError diff --git a/library/src/main/java/com/xiaomao/jsbridge/IWebView.kt b/library/src/main/java/com/orientsec/jsbridge/IWebView.kt similarity index 71% rename from library/src/main/java/com/xiaomao/jsbridge/IWebView.kt rename to library/src/main/java/com/orientsec/jsbridge/IWebView.kt index 671ba21..026bb58 100644 --- a/library/src/main/java/com/xiaomao/jsbridge/IWebView.kt +++ b/library/src/main/java/com/orientsec/jsbridge/IWebView.kt @@ -1,8 +1,10 @@ -package com.xiaomao.jsbridge +package com.orientsec.jsbridge +import android.content.Context import android.webkit.ValueCallback interface IWebView { + fun getContext(): Context? fun loadUrl(url: String?) diff --git a/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt b/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt new file mode 100644 index 0000000..262abcc --- /dev/null +++ b/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt @@ -0,0 +1,232 @@ +package com.orientsec.jsbridge + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.os.SystemClock +import android.webkit.JavascriptInterface +import java.net.URLEncoder +import java.util.* +import java.util.concurrent.atomic.AtomicLong +import kotlin.system.measureTimeMillis + +class JsBridge(private val webView: IWebView) { + + private val mUniqueId = AtomicLong(0) + + private val mMessageHandlers: MutableMap<String, BridgeHandler> = mutableMapOf() + + private val mCallbacks: MutableMap<String, (String) -> Unit> = mutableMapOf() + + private var mRequests: MutableList<JSRequest>? = ArrayList() + + private val mainHandler = Handler(Looper.getMainLooper()) + + var onPageLoadListener: OnPageLoadListener? = null + + /** + * register handler,so that javascript can call it + * 注册处理程序,以便javascript调用它 + * + * @param handlerName handlerName + * @param handler BridgeHandler + */ + fun registerHandler(handlerName: String, handler: BridgeHandler) { + // 添加至 Map<String, BridgeHandler> + mMessageHandlers[handlerName] = handler + } + + + /** + * register handler,so that javascript can call it + * 注册处理程序,以便javascript调用它 + * + * @param handlers handlerName + */ + fun registerHandler(handlers: Map<String, BridgeHandler>) { + mMessageHandlers.putAll(handlers) + } + + + /** + * unregister handler + * + * @param handlerName + */ + fun unregisterHandler(handlerName: String) { + mMessageHandlers.remove(handlerName) + } + + + @JavascriptInterface + fun callHandler(handlerName: String, data: String, callbackId: String?) { + val handler = mMessageHandlers[handlerName] ?: return + if (callbackId == null) { + handler.handle(data) + } else { + handler.handle(data) { + responseToJs(it, callbackId) + } + } + } + + @JavascriptInterface + fun response(data: String, callbackId: String) { + Logger.d( + javaClass.simpleName, + "response->$data, callbackId: $callbackId ${Thread.currentThread().name}" + ) + mCallbacks.remove(callbackId)?.invoke(data) + } + + fun register(view: IWebView) { + registerHandler("onPageLoad", object : BridgeHandler { + override fun handle(data: String, callback: (String) -> Unit) { + if (Thread.currentThread() === Looper.getMainLooper().thread) { + onPageLoadListener?.onPageLoaded(data.isNotEmpty()) + } else { + mainHandler.post { onPageLoadListener?.onPageLoaded(data.isNotEmpty()) } + } + } + }) + loadJs(view) + val requests = mRequests ?: return + for (request in requests) { + dispatchMessage(request.handlerName, request.data, request.callbackId) + } + mRequests = null + } + + /** + * free memory + */ + fun clean() { + mCallbacks.clear() + mMessageHandlers.clear() + mRequests?.clear() + } + + private fun responseToJs(data: String, callbackId: String) { + var messageJson = data + //escape special characters for json string 为json字符串转义特殊字符 + messageJson = messageJson.replace("(\\\\)([^utrn])".toRegex(), "\\\\\\\\$1$2") + messageJson = messageJson.replace("(?<=[^\\\\])(\")".toRegex(), "\\\\\"") + messageJson = messageJson.replace("(?<=[^\\\\])(\')".toRegex(), "\\\\\'") + messageJson = messageJson.replace("%7B".toRegex(), URLEncoder.encode("%7B")) + messageJson = messageJson.replace("%7D".toRegex(), URLEncoder.encode("%7D")) + messageJson = messageJson.replace("%22".toRegex(), URLEncoder.encode("%22")) + val javascriptCommand = + String.format( + BridgeUtil.JS_RESPONSE_FROM_JAVA, + messageJson, + callbackId + ) + Logger.d(javaClass.simpleName, "javascriptCommand->$javascriptCommand") + + // 必须要找主线程才会将数据传递出去 --- 划重点 + if (Thread.currentThread() === Looper.getMainLooper().thread) { + webView.evaluateJavascript(javascriptCommand, null) + } else { + mainHandler.post { webView.evaluateJavascript(javascriptCommand, null) } + } + } + + /** + * 分发message 必须在主线程才分发成功 + * + */ + private fun dispatchMessage(handlerName: String, data: String, callbackId: String?) { + var messageJson = data + //escape special characters for json string 为json字符串转义特殊字符 + messageJson = messageJson.replace("(\\\\)([^utrn])".toRegex(), "\\\\\\\\$1$2") + messageJson = messageJson.replace("(?<=[^\\\\])(\")".toRegex(), "\\\\\"") + messageJson = messageJson.replace("(?<=[^\\\\])(\')".toRegex(), "\\\\\'") + messageJson = messageJson.replace("%7B".toRegex(), URLEncoder.encode("%7B")) + messageJson = messageJson.replace("%7D".toRegex(), URLEncoder.encode("%7D")) + messageJson = messageJson.replace("%22".toRegex(), URLEncoder.encode("%22")) + val javascriptCommand = + String.format( + BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, + handlerName, + messageJson, + callbackId + ) + Logger.d(javaClass.simpleName, "javascriptCommand->$javascriptCommand") + + // 必须要找主线程才会将数据传递出去 --- 划重点 + if (Thread.currentThread() === Looper.getMainLooper().thread) { + webView.evaluateJavascript(javascriptCommand, null) + } else { + mainHandler.post { webView.evaluateJavascript(javascriptCommand, null) } + } + } + + + /** + * 保存message到消息队列 + * + * @param handlerName handlerName + * @param data data + * @param responseCallback OnBridgeCallback + */ + fun callHandler( + handlerName: String, + data: String, + responseCallback: ((String) -> Unit)? = null + ) { + require(handlerName.isNotEmpty()) { "Empty handler name." } + var callbackId: String? = null + if (responseCallback != null) { + callbackId = String.format( + BridgeUtil.CALLBACK_ID_FORMAT, + "${mUniqueId.incrementAndGet()}" + BridgeUtil.UNDERLINE_STR + "${SystemClock.currentThreadTimeMillis()}" + ) + mCallbacks[callbackId] = responseCallback + } + val request = mRequests + if (request != null) { + //等待状态添加到消息集合否则分发消息 + val jsRequest = JSRequest(callbackId, handlerName, data) + request.add(jsRequest) + } else { + dispatchMessage(handlerName, data, callbackId) + } + } + + /** + * 这里只是加载lib包中assets中的 WebViewJavascriptBridge.js + * @param view webview + */ + private fun loadJs(view: IWebView) { + val mill = measureTimeMillis { + val context = view.getContext() ?: return + val jsContent = assetFile2Str(context) + view.loadUrl("javascript:$jsContent") + } + Logger.i(javaClass.simpleName, "Load js bridge file cost:$mill ms") + } + + /** + * 解析assets文件夹里面的代码,去除注释,取可执行的代码 + * @param c context + * @return 可执行代码 + */ + private fun assetFile2Str(c: Context): String { + try { + c.assets.open(BridgeUtil.JAVA_SCRIPT) + .bufferedReader() + .use { + val sb = StringBuilder() + val regex = Regex("^\\s*//.*") + do { + val line = it.readLine() + if (line?.matches(regex) != true) sb.append(line) + } while (line != null) + return sb.toString() + } + } catch (e: Exception) { + e.printStackTrace() + } + return "" + } +} \ No newline at end of file diff --git a/library/src/main/java/com/xiaomao/jsbridge/Logger.kt b/library/src/main/java/com/orientsec/jsbridge/Logger.kt similarity index 94% rename from library/src/main/java/com/xiaomao/jsbridge/Logger.kt rename to library/src/main/java/com/orientsec/jsbridge/Logger.kt index e6b2cc3..0d1f9fc 100644 --- a/library/src/main/java/com/xiaomao/jsbridge/Logger.kt +++ b/library/src/main/java/com/orientsec/jsbridge/Logger.kt @@ -1,4 +1,4 @@ -package com.xiaomao.jsbridge +package com.orientsec.jsbridge import android.util.Log diff --git a/library/src/main/java/com/orientsec/jsbridge/Message.kt b/library/src/main/java/com/orientsec/jsbridge/Message.kt new file mode 100644 index 0000000..6ad4bb1 --- /dev/null +++ b/library/src/main/java/com/orientsec/jsbridge/Message.kt @@ -0,0 +1,7 @@ +package com.orientsec.jsbridge + +data class JSRequest( + val callbackId: String?, + val handlerName: String, + val data: String +) \ No newline at end of file diff --git a/library/src/main/java/com/orientsec/jsbridge/OnPageLoadListener.kt b/library/src/main/java/com/orientsec/jsbridge/OnPageLoadListener.kt new file mode 100644 index 0000000..a046d9e --- /dev/null +++ b/library/src/main/java/com/orientsec/jsbridge/OnPageLoadListener.kt @@ -0,0 +1,5 @@ +package com.orientsec.jsbridge + +interface OnPageLoadListener { + fun onPageLoaded(isLoaded: Boolean) +} \ No newline at end of file diff --git a/library/src/main/java/com/xiaomao/jsbridge/BridgeHandler.kt b/library/src/main/java/com/xiaomao/jsbridge/BridgeHandler.kt deleted file mode 100644 index a71a319..0000000 --- a/library/src/main/java/com/xiaomao/jsbridge/BridgeHandler.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.xiaomao.jsbridge - -interface BridgeHandler { - fun handle(data: String, callback: (String) -> Unit) -} \ No newline at end of file diff --git a/library/src/main/java/com/xiaomao/jsbridge/BridgeUtil.kt b/library/src/main/java/com/xiaomao/jsbridge/BridgeUtil.kt deleted file mode 100644 index 6988123..0000000 --- a/library/src/main/java/com/xiaomao/jsbridge/BridgeUtil.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.xiaomao.jsbridge - -object BridgeUtil { - const val JAVA_SCRIPT = "WebViewJavascriptBridge.js" - const val CALLBACK_ID_FORMAT = "JAVA_CB_%s" - const val JS_HANDLE_MESSAGE_FROM_JAVA = - "javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');" - const val JAVASCRIPT_STR = "javascript:%s" - const val UNDERLINE_STR = "_" - const val WebviewJavascriptBridge = - "(function(){if(window.WebViewJavascriptBridge){return}var receiveMessageQueue=[];var messageHandlers={};var responseCallbacks={};var uniqueId=1;function init(messageHandler){if(WebViewJavascriptBridge._messageHandler){throw new Error('WebViewJavascriptBridge.init called twice');}WebViewJavascriptBridge._messageHandler=messageHandler;var receivedMessages=receiveMessageQueue;receiveMessageQueue=null;for(var i=0;i<receivedMessages.length;i++){_dispatchMessageFromNative(receivedMessages[i])}}function send(data,responseCallback){_doSend('jsbridge','send',data,responseCallback)}function registerHandler(handlerName,handler){messageHandlers[handlerName]=handler}function callHandler(handlerName,data,responseCallback){_doSend('jsbridge',handlerName,data,responseCallback)}function callHandlerWithModule(moduleName,handlerName,data,responseCallback){_doSend(moduleName,handlerName,data,responseCallback)}function _doSend(moduleName,handlerName,message,responseCallback){var callbackId;if(typeof responseCallback==='string'){callbackId=responseCallback}else if(responseCallback){callbackId='cb_'+(uniqueId++)+'_'+new Date().getTime();responseCallbacks[callbackId]=responseCallback}else{callbackId=''}try{var evalStr1='window.'+moduleName+'.';if(moduleName=='jsbridge'&&handlerName!='response'){evalStr1+='handler'}else{evalStr1+=handlerName}var fn=eval(evalStr1)}catch(e){console.log(e)}if(typeof fn==='function'){var evalStr='window.'+moduleName;var fnwindow=eval(evalStr);var responseData;if(moduleName=='jsbridge'&&handlerName!='response'){responseData=fn.call(fnwindow,handlerName,JSON.stringify(message),callbackId)}else{responseData=fn.call(fnwindow,JSON.stringify(message),callbackId)}if(responseData){responseCallback=responseCallbacks[callbackId];if(!responseCallback){return}responseCallback(responseData);delete responseCallbacks[callbackId]}}}function _dispatchMessageFromNative(messageJSON){setTimeout(function(){var message=JSON.parse(messageJSON);var responseCallback;if(message.responseId){responseCallback=responseCallbacks[message.responseId];if(!responseCallback){return}responseCallback(message.responseData);delete responseCallbacks[message.responseId]}else{if(message.callbackId){var callbackResponseId=message.callbackId;responseCallback=function(responseData){_doSend('jsbridge','response',responseData,callbackResponseId)}}var handler=WebViewJavascriptBridge._messageHandler;if(message.handlerName){handler=messageHandlers[message.handlerName]}try{handler(message.data,responseCallback)}catch(exception){if(typeof console!='undefined'){console.log(\"WebViewJavascriptBridge: WARNING: javascript handler threw.\",message,exception)}}}})}function _handleMessageFromNative(messageJSON){if(receiveMessageQueue){receiveMessageQueue.push(messageJSON)}_dispatchMessageFromNative(messageJSON)}var WebViewJavascriptBridge=window.WebViewJavascriptBridge={init:init,send:send,registerHandler:registerHandler,callHandler:callHandler,callHandlerWithModule:callHandlerWithModule,_handleMessageFromNative:_handleMessageFromNative};console.log(\"start jsbridge ...\");var doc=document;var readyEvent=doc.createEvent('Events');readyEvent.initEvent('WebViewJavascriptBridgeReady');readyEvent.bridge=WebViewJavascriptBridge;doc.dispatchEvent(readyEvent);console.log(\"end jsbridge ...\")})();" -} diff --git a/library/src/main/java/com/xiaomao/jsbridge/JsBridge.kt b/library/src/main/java/com/xiaomao/jsbridge/JsBridge.kt deleted file mode 100644 index cab3ddb..0000000 --- a/library/src/main/java/com/xiaomao/jsbridge/JsBridge.kt +++ /dev/null @@ -1,178 +0,0 @@ -package com.xiaomao.jsbridge - -import android.os.Handler -import android.os.Looper -import android.os.SystemClock -import android.util.ArrayMap -import android.webkit.JavascriptInterface -import java.net.URLEncoder -import java.util.* -import java.util.concurrent.atomic.AtomicLong -import kotlin.collections.HashMap - -private val EmptyResponseCallBack: (String) -> Unit = {} - - -class JsBridge(private val webView: IWebView) { - enum class State { - IDLE, RUN, DESTROY - } - - private var state: State = State.IDLE - - private val mUniqueId = AtomicLong(0) - - private val mMessageHandlers: MutableMap<String, BridgeHandler> = HashMap() - - private val mCallbacks: MutableMap<String, (String) -> Unit> = ArrayMap() - - private var mRequests: MutableList<JSRequest> = ArrayList() - - private val mainHandler = Handler(Looper.getMainLooper()) - - private fun checkState() { - require(state < State.DESTROY) { "JsBridge is destroyed." } - } - - /** - * register handler,so that javascript can call it - * 注册处理程序,以便javascript调用它 - * - * @param handlerName handlerName - * @param handler BridgeHandler - */ - @Synchronized - fun registerHandler(handlerName: String, handler: BridgeHandler) { - checkState() - // 添加至 Map<String, BridgeHandler> - mMessageHandlers[handlerName] = handler - } - - - /** - * register handler,so that javascript can call it - * 注册处理程序,以便javascript调用它 - * - * @param handlers handlerName - */ - @Synchronized - fun registerHandler(handlers: Map<String, BridgeHandler>) { - checkState() - mMessageHandlers.putAll(handlers) - } - - - /** - * unregister handler - * - * @param handlerName - */ - @Synchronized - fun unregisterHandler(handlerName: String) { - checkState() - mMessageHandlers.remove(handlerName) - } - - - @JavascriptInterface - @Synchronized - fun request(seqNo: String, handlerName: String, data: String) { - mMessageHandlers[handlerName]?.handle(data) { - dispatchMessage(JSResponse(seqNo, data)) - } - } - - @JavascriptInterface - @Synchronized - fun response(data: String, seqNo: String) { - Logger.d( - javaClass.simpleName, - "response->$data, seqNo: $seqNo ${Thread.currentThread().name}" - ) - mCallbacks.remove(seqNo)?.invoke(data) - } - - @Synchronized - fun register(view: IWebView) { - require(state == State.IDLE) { "JsBridge is registered." } - state = State.RUN - view.addJavascriptInterface(this, "jsbridge") - view.loadUrl( - String.format( - BridgeUtil.JAVASCRIPT_STR, - BridgeUtil.WebviewJavascriptBridge - ) - ) - for (request in mRequests) { - dispatchMessage(request) - } - } - - /** - * free memory - */ - @Synchronized - fun clean() { - state = State.DESTROY - mCallbacks.clear() - mMessageHandlers.clear() - mRequests.clear() - } - - /** - * 分发message 必须在主线程才分发成功 - * - * @param message Message - */ - private fun dispatchMessage(message: Message) { - var messageJson = message.toJson() - //escape special characters for json string 为json字符串转义特殊字符 - messageJson = messageJson.replace("(\\\\)([^utrn])".toRegex(), "\\\\\\\\$1$2") - messageJson = messageJson.replace("(?<=[^\\\\])(\")".toRegex(), "\\\\\"") - messageJson = messageJson.replace("(?<=[^\\\\])(\')".toRegex(), "\\\\\'") - messageJson = messageJson.replace("%7B".toRegex(), URLEncoder.encode("%7B")) - messageJson = messageJson.replace("%7D".toRegex(), URLEncoder.encode("%7D")) - messageJson = messageJson.replace("%22".toRegex(), URLEncoder.encode("%22")) - val javascriptCommand = - String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson) - Logger.d(javaClass.simpleName, "javascriptCommand->$javascriptCommand") - - // 必须要找主线程才会将数据传递出去 --- 划重点 - if (Thread.currentThread() === Looper.getMainLooper().thread) { - webView.evaluateJavascript(javascriptCommand, null) - } else { - mainHandler.post { webView.evaluateJavascript(javascriptCommand, null) } - } - } - - - /** - * 保存message到消息队列 - * - * @param handlerName handlerName - * @param data data - * @param responseCallback OnBridgeCallback - */ - @Synchronized - fun callHandler( - handlerName: String, - data: String, - responseCallback: (String) -> Unit = EmptyResponseCallBack - ) { - checkState() - require(handlerName.isNotEmpty()) { "Empty handler name." } - val sequenceId: String = String.format( - BridgeUtil.CALLBACK_ID_FORMAT, - "${mUniqueId.incrementAndGet()}" + BridgeUtil.UNDERLINE_STR + "${SystemClock.currentThreadTimeMillis()}" - ) - mCallbacks[sequenceId] = responseCallback - val request = JSRequest(sequenceId, handlerName, data) - //等待状态添加到消息集合否则分发消息 - if (state == State.IDLE) { - mRequests.add(request) - } else { - dispatchMessage(request) - } - } - -} \ No newline at end of file diff --git a/library/src/main/java/com/xiaomao/jsbridge/Message.kt b/library/src/main/java/com/xiaomao/jsbridge/Message.kt deleted file mode 100644 index 51b7ad2..0000000 --- a/library/src/main/java/com/xiaomao/jsbridge/Message.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.xiaomao.jsbridge - -import org.json.JSONObject - -interface Message { - fun toJson(): String -} - -data class JSRequest( - val seqNo: String, - val handlerName: String, - val data: String -) : Message { - override fun toJson(): String { - val jsonObject = JSONObject() - jsonObject.put("seqNo", seqNo) - jsonObject.put("handlerName", handlerName) - jsonObject.put("data", data) - return jsonObject.toString() - } -} - - -data class JSResponse(val seqNo: String, val data: String) : Message { - override fun toJson(): String { - val jsonObject = JSONObject() - jsonObject.put("seqNo", seqNo) - jsonObject.put("data", data) - return jsonObject.toString() - } -} \ No newline at end of file diff --git a/peoject.properties b/peoject.properties new file mode 100644 index 0000000..0fc8e2e --- /dev/null +++ b/peoject.properties @@ -0,0 +1,18 @@ +# 库的简单描述 +PROJ_DESCRIPTION=JSBridge +PROJ_VERSION=1.0.2 +PROJ_GROUP=com.orientsec +PROJ_ARTIFACT=jsbridge +# 组织 +PROJ_ORGANIZATION="DFZQ" +PROJ_LICENCE_NAME="MIT License" +PROJ_LICENCE_URL="https://opensource.org/licenses/MIT" +PROJ_LICENCE_DIST="东方证券研发六部" +# 开发者的信息, 可以随意 +DEVELOPER_ID=orientsec +DEVELOPER_NAME=orientsec +DEVELOPER_EMAIL=meihu@orientsec.com.cn +RELEASE_REPOSITORY_URL=http://172.16.45.65:18080/nexus/content/repositories/releases/ +SNAPSHOT_REPOSITORY_URL=http://172.16.45.65:18080/nexus/content/repositories/snapshots/ +NEXUS_USERNAME=xxx +NEXUS_PASSWORD=xxx \ No newline at end of file From 7a49059de01e910b5573b2f074b9a785ef673b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E7=90=A5?= <meihu@orientsec.com.cn> Date: Tue, 14 Apr 2020 09:11:27 +0800 Subject: [PATCH 03/14] =?UTF-8?q?=E4=BC=98=E5=8C=96js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- library/src/main/assets/WebViewJavascriptBridge.js | 6 +++++- .../src/main/java/com/orientsec/jsbridge/JsBridge.kt | 12 +++++++++--- .../src/main/java/com/orientsec/jsbridge/Message.kt | 7 ------- 3 files changed, 14 insertions(+), 11 deletions(-) delete mode 100644 library/src/main/java/com/orientsec/jsbridge/Message.kt diff --git a/library/src/main/assets/WebViewJavascriptBridge.js b/library/src/main/assets/WebViewJavascriptBridge.js index 27ed4b0..89a8861 100644 --- a/library/src/main/assets/WebViewJavascriptBridge.js +++ b/library/src/main/assets/WebViewJavascriptBridge.js @@ -76,6 +76,10 @@ responseCallback = function (responseData) { _responseToNative(responseData, callbackId); }; + } else { + responseCallback = function (responseData) { + + } } //查找指定handler var handler; @@ -98,7 +102,7 @@ //提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以 function _handleMessageFromNative(handlerName, data, callbackId) { - console.log('handle message: ' + handlerName + ' callbackId:' + callbackId +" data:" + data); + console.log('handle message: ' + handlerName + ' callbackId:' + callbackId + " data:" + data); if (receiveMessageQueue) { var messageJson = {handlerName: handlerName, data: data, callbackId: callbackId}; receiveMessageQueue.push(messageJson); diff --git a/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt b/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt index 262abcc..9451c4a 100644 --- a/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt +++ b/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt @@ -135,7 +135,7 @@ class JsBridge(private val webView: IWebView) { * 分发message 必须在主线程才分发成功 * */ - private fun dispatchMessage(handlerName: String, data: String, callbackId: String?) { + private fun dispatchMessage(handlerName: String, data: String, callbackId: String) { var messageJson = data //escape special characters for json string 为json字符串转义特殊字符 messageJson = messageJson.replace("(\\\\)([^utrn])".toRegex(), "\\\\\\\\$1$2") @@ -175,7 +175,7 @@ class JsBridge(private val webView: IWebView) { responseCallback: ((String) -> Unit)? = null ) { require(handlerName.isNotEmpty()) { "Empty handler name." } - var callbackId: String? = null + var callbackId = "" if (responseCallback != null) { callbackId = String.format( BridgeUtil.CALLBACK_ID_FORMAT, @@ -229,4 +229,10 @@ class JsBridge(private val webView: IWebView) { } return "" } -} \ No newline at end of file +} + +data class JSRequest( + val callbackId: String, + val handlerName: String, + val data: String +) \ No newline at end of file diff --git a/library/src/main/java/com/orientsec/jsbridge/Message.kt b/library/src/main/java/com/orientsec/jsbridge/Message.kt deleted file mode 100644 index 6ad4bb1..0000000 --- a/library/src/main/java/com/orientsec/jsbridge/Message.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.orientsec.jsbridge - -data class JSRequest( - val callbackId: String?, - val handlerName: String, - val data: String -) \ No newline at end of file From 489e68c142b4d843e544f73e8b2ead89f131181c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E7=90=A5?= <meihu@orientsec.com.cn> Date: Tue, 14 Apr 2020 15:57:17 +0800 Subject: [PATCH 04/14] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jsbridge/example/MainActivity.kt | 12 +- .../main/assets/WebViewJavascriptBridge.js | 2 +- .../com/orientsec/jsbridge/BridgeHandler.kt | 9 + .../java/com/orientsec/jsbridge/BridgeUtil.kt | 11 - .../com/orientsec/jsbridge/BridgeWebView.kt | 38 ++- .../orientsec/jsbridge/BridgeWebViewClient.kt | 3 +- .../java/com/orientsec/jsbridge/IJSBridge.kt | 36 +++ .../java/com/orientsec/jsbridge/IWebView.kt | 1 + .../java/com/orientsec/jsbridge/JsBridge.kt | 226 +++++++++--------- .../java/com/orientsec/jsbridge/Logger.kt | 22 +- .../orientsec/jsbridge/OnPageLoadListener.kt | 7 + 11 files changed, 229 insertions(+), 138 deletions(-) delete mode 100644 library/src/main/java/com/orientsec/jsbridge/BridgeUtil.kt create mode 100644 library/src/main/java/com/orientsec/jsbridge/IJSBridge.kt diff --git a/example/src/main/java/com/orientsec/jsbridge/example/MainActivity.kt b/example/src/main/java/com/orientsec/jsbridge/example/MainActivity.kt index 556f9d1..40914bc 100644 --- a/example/src/main/java/com/orientsec/jsbridge/example/MainActivity.kt +++ b/example/src/main/java/com/orientsec/jsbridge/example/MainActivity.kt @@ -1,5 +1,6 @@ package com.orientsec.jsbridge.example +import android.annotation.SuppressLint import android.app.Activity import android.os.Bundle import android.util.Log @@ -9,7 +10,7 @@ import android.widget.Toast import com.google.gson.Gson import com.orientsec.jsbridge.BridgeHandler import com.orientsec.jsbridge.BridgeWebView -import com.orientsec.jsbridge.Logger +import com.orientsec.jsbridge.JsBridge class MainActivity : Activity(), View.OnClickListener { private lateinit var webView: BridgeWebView @@ -24,17 +25,18 @@ class MainActivity : Activity(), View.OnClickListener { val testStr: String ) + @SuppressLint("SetJavaScriptEnabled") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - Logger.debug = true + JsBridge.debug = true webView = findViewById(R.id.webView) val settings = webView.settings settings.javaScriptEnabled = true val button = findViewById<Button>(R.id.button) button.setOnClickListener(this) webView.loadUrl("file:///android_asset/demo.html") - webView.jsBridge.registerHandler("submitFromWeb", object : BridgeHandler { + webView.registerHandler("submitFromWeb", object : BridgeHandler { override fun handle(data: String, callback: (String) -> Unit) { Log.i("MainActivity", "handler = submitFromWeb, data from web = $data") callback("submitFromWeb exe, response data 中文 from Java") @@ -44,13 +46,13 @@ class MainActivity : Activity(), View.OnClickListener { Location("SDU") val user = User("大头鬼", location, "") - webView.jsBridge.callHandler("functionInJs", Gson().toJson(user)) {} + webView.callHandler("functionInJs", Gson().toJson(user)) {} } override fun onClick(v: View) { if (R.id.button == v.id) { - webView.jsBridge.callHandler( + webView.callHandler( "functionInJs", "data from Java" ) { diff --git a/library/src/main/assets/WebViewJavascriptBridge.js b/library/src/main/assets/WebViewJavascriptBridge.js index 89a8861..bf1ac67 100644 --- a/library/src/main/assets/WebViewJavascriptBridge.js +++ b/library/src/main/assets/WebViewJavascriptBridge.js @@ -89,7 +89,7 @@ handler = WebViewJavascriptBridge._requestHandler; } if (handler == undefined) { - console.log("WebViewJavascriptBridge: WARNING:no handler for ", request.handlerName); + console.log("WebViewJavascriptBridge: WARNING:no handler for ", handlerName); } else { try { handler(data, responseCallback); diff --git a/library/src/main/java/com/orientsec/jsbridge/BridgeHandler.kt b/library/src/main/java/com/orientsec/jsbridge/BridgeHandler.kt index 455011d..08279d4 100644 --- a/library/src/main/java/com/orientsec/jsbridge/BridgeHandler.kt +++ b/library/src/main/java/com/orientsec/jsbridge/BridgeHandler.kt @@ -1,7 +1,16 @@ package com.orientsec.jsbridge +//空回调函数。当js请求的回调id为null时,不需要回调js。 private val EmptyResponseCallBack: (String) -> Unit = {} +/** + * 原生指令handler。 + */ interface BridgeHandler { + /** + * 处理js请求。 + * @param data 请求数据。 + * @param callback 请求回调函数。 + */ fun handle(data: String, callback: (String) -> Unit = EmptyResponseCallBack) } \ No newline at end of file diff --git a/library/src/main/java/com/orientsec/jsbridge/BridgeUtil.kt b/library/src/main/java/com/orientsec/jsbridge/BridgeUtil.kt deleted file mode 100644 index 5800639..0000000 --- a/library/src/main/java/com/orientsec/jsbridge/BridgeUtil.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.orientsec.jsbridge - -object BridgeUtil { - const val JAVA_SCRIPT = "WebViewJavascriptBridge.js" - const val CALLBACK_ID_FORMAT = "JAVA_CB_%s" - const val JS_HANDLE_MESSAGE_FROM_JAVA = - "javascript:WebViewJavascriptBridge._handleMessageFromNative('%s','%s','%s');" - const val JS_RESPONSE_FROM_JAVA = - "javascript:WebViewJavascriptBridge._responseFromNative('%s','%s');" - const val UNDERLINE_STR = "_" -} diff --git a/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt b/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt index 88c1de4..d119d12 100644 --- a/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt +++ b/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt @@ -7,9 +7,9 @@ import android.webkit.WebView import android.webkit.WebViewClient @SuppressLint("SetJavaScriptEnabled") -class BridgeWebView : WebView, IWebView { - - val jsBridge: JsBridge = JsBridge(this) +class BridgeWebView : WebView, IWebView, IJSBridge, OnPageLoadListener { + var mOnPageLoadListener: OnPageLoadListener = this + private val jsBridge: JsBridge = JsBridge(this) private var bridgeWebViewClient: BridgeWebViewClient = BridgeWebViewClient(jsBridge) constructor(context: Context?) : super(context) @@ -23,7 +23,7 @@ class BridgeWebView : WebView, IWebView { init { super.setWebViewClient(bridgeWebViewClient) addJavascriptInterface(jsBridge, "jsBridge") - if (Logger.debug) { + if (JsBridge.debug) { setWebContentsDebuggingEnabled(true) } } @@ -38,4 +38,34 @@ class BridgeWebView : WebView, IWebView { context } + override fun registerHandler(handlerName: String, handler: BridgeHandler) { + jsBridge.registerHandler(handlerName, handler) + } + + override fun registerHandler(handlers: Map<String, BridgeHandler>) { + jsBridge.registerHandler(handlers) + } + + override fun unregisterHandler(handlerName: String) { + jsBridge.unregisterHandler(handlerName) + } + + override fun callHandler( + handlerName: String, + data: String, + responseCallback: ((String) -> Unit)? + ) { + jsBridge.callHandler(handlerName, data, responseCallback) + } + + override fun onPageLoaded(isLoaded: Boolean) { + + } + + fun setOnPageLoadListener(onPageLoadListener: OnPageLoadListener) { + this.mOnPageLoadListener = onPageLoadListener + } + + override val onPageLoadListener: OnPageLoadListener + get() = mOnPageLoadListener } \ No newline at end of file diff --git a/library/src/main/java/com/orientsec/jsbridge/BridgeWebViewClient.kt b/library/src/main/java/com/orientsec/jsbridge/BridgeWebViewClient.kt index d20f8b8..793c580 100644 --- a/library/src/main/java/com/orientsec/jsbridge/BridgeWebViewClient.kt +++ b/library/src/main/java/com/orientsec/jsbridge/BridgeWebViewClient.kt @@ -9,8 +9,7 @@ import android.webkit.* import androidx.annotation.RequiresApi /** - * 如果要自定义WebViewClient必须要集成此类 - * Created by bruce on 10/28/15. + * 如果要自定义WebViewClient必须要集成此类。 */ internal class BridgeWebViewClient(private val jsBridge: JsBridge) : WebViewClient() { internal var webViewClient: WebViewClient = WebViewClient() diff --git a/library/src/main/java/com/orientsec/jsbridge/IJSBridge.kt b/library/src/main/java/com/orientsec/jsbridge/IJSBridge.kt new file mode 100644 index 0000000..b09c6df --- /dev/null +++ b/library/src/main/java/com/orientsec/jsbridge/IJSBridge.kt @@ -0,0 +1,36 @@ +package com.orientsec.jsbridge + +interface IJSBridge { + /** + * Register handler, so that javascript can call it. + * 注册处理程序,以便javascript调用它。 + * + * @param handlerName HandlerName. + * @param handler BridgeHandler. + */ + fun registerHandler(handlerName: String, handler: BridgeHandler) + + /** + * Register handler,so that javascript can call it. + * 注册处理程序,以便javascript调用它。 + * + * @param handlers HandlerName. + */ + fun registerHandler(handlers: Map<String, BridgeHandler>) + + /** + * Unregister handler. + * + * @param handlerName HandlerName. + */ + fun unregisterHandler(handlerName: String) + + /** + * 调用Js handler。 + * + * @param handlerName HandlerName. + * @param data Request data. + * @param responseCallback OnBridgeCallback. + */ + fun callHandler(handlerName: String, data: String, responseCallback: ((String) -> Unit)? = null) +} \ No newline at end of file diff --git a/library/src/main/java/com/orientsec/jsbridge/IWebView.kt b/library/src/main/java/com/orientsec/jsbridge/IWebView.kt index 026bb58..a2e351a 100644 --- a/library/src/main/java/com/orientsec/jsbridge/IWebView.kt +++ b/library/src/main/java/com/orientsec/jsbridge/IWebView.kt @@ -12,4 +12,5 @@ interface IWebView { fun evaluateJavascript(var1: String?, callback: ValueCallback<String>?) + val onPageLoadListener: OnPageLoadListener } \ No newline at end of file diff --git a/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt b/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt index 9451c4a..4c58d44 100644 --- a/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt +++ b/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt @@ -5,24 +5,34 @@ import android.os.Handler import android.os.Looper import android.os.SystemClock import android.webkit.JavascriptInterface -import java.net.URLEncoder +import androidx.annotation.MainThread import java.util.* +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicLong import kotlin.system.measureTimeMillis -class JsBridge(private val webView: IWebView) { +class JsBridge(private val webView: IWebView) : IJSBridge, + Loggable by Logger(JsBridge::class.java.simpleName) { + + enum class State { + IDLE, READY, DESTROY + } + + private var state: State = State.IDLE private val mUniqueId = AtomicLong(0) - private val mMessageHandlers: MutableMap<String, BridgeHandler> = mutableMapOf() + private val mMessageHandlers: MutableMap<String, BridgeHandler> = ConcurrentHashMap() - private val mCallbacks: MutableMap<String, (String) -> Unit> = mutableMapOf() + private val mCallbacks: MutableMap<String, (String) -> Unit> = ConcurrentHashMap() - private var mRequests: MutableList<JSRequest>? = ArrayList() + private val mRequests: MutableList<String> = ArrayList() private val mainHandler = Handler(Looper.getMainLooper()) - var onPageLoadListener: OnPageLoadListener? = null + private fun checkState() { + require(state < State.DESTROY) { "JsBridge is destroyed." } + } /** * register handler,so that javascript can call it @@ -31,7 +41,9 @@ class JsBridge(private val webView: IWebView) { * @param handlerName handlerName * @param handler BridgeHandler */ - fun registerHandler(handlerName: String, handler: BridgeHandler) { + @MainThread + override fun registerHandler(handlerName: String, handler: BridgeHandler) { + checkState() // 添加至 Map<String, BridgeHandler> mMessageHandlers[handlerName] = handler } @@ -43,7 +55,9 @@ class JsBridge(private val webView: IWebView) { * * @param handlers handlerName */ - fun registerHandler(handlers: Map<String, BridgeHandler>) { + @MainThread + override fun registerHandler(handlers: Map<String, BridgeHandler>) { + checkState() mMessageHandlers.putAll(handlers) } @@ -53,149 +67,104 @@ class JsBridge(private val webView: IWebView) { * * @param handlerName */ - fun unregisterHandler(handlerName: String) { + @MainThread + override fun unregisterHandler(handlerName: String) { + checkState() mMessageHandlers.remove(handlerName) } @JavascriptInterface fun callHandler(handlerName: String, data: String, callbackId: String?) { - val handler = mMessageHandlers[handlerName] ?: return + info( + "callJavaHandler->$handlerName, $data, callbackId: " + + "$callbackId [${Thread.currentThread().name}]" + ) + val handler = mMessageHandlers[handlerName] + if (handler == null) { + warn("no handler for:$handlerName") + return + } if (callbackId == null) { handler.handle(data) } else { handler.handle(data) { - responseToJs(it, callbackId) + dispatchMessage(String.format(JS_RESPONSE_FROM_JAVA, it, callbackId)) } } } @JavascriptInterface fun response(data: String, callbackId: String) { - Logger.d( - javaClass.simpleName, - "response->$data, callbackId: $callbackId ${Thread.currentThread().name}" - ) + info("responseFromJs->$data, callbackId: $callbackId [${Thread.currentThread().name}]") mCallbacks.remove(callbackId)?.invoke(data) } + @MainThread fun register(view: IWebView) { + require(state == State.IDLE) { "JsBridge is already registered." } + state = State.READY registerHandler("onPageLoad", object : BridgeHandler { override fun handle(data: String, callback: (String) -> Unit) { - if (Thread.currentThread() === Looper.getMainLooper().thread) { - onPageLoadListener?.onPageLoaded(data.isNotEmpty()) - } else { - mainHandler.post { onPageLoadListener?.onPageLoaded(data.isNotEmpty()) } - } + runOnUiThread { view.onPageLoadListener.onPageLoaded(data.isNotEmpty()) } } }) loadJs(view) - val requests = mRequests ?: return - for (request in requests) { - dispatchMessage(request.handlerName, request.data, request.callbackId) + if (mRequests.isNotEmpty()) { + dispatchMessage(mRequests) + mRequests.clear() } - mRequests = null } /** - * free memory + * Free memory. */ + @MainThread fun clean() { - mCallbacks.clear() - mMessageHandlers.clear() - mRequests?.clear() - } - - private fun responseToJs(data: String, callbackId: String) { - var messageJson = data - //escape special characters for json string 为json字符串转义特殊字符 - messageJson = messageJson.replace("(\\\\)([^utrn])".toRegex(), "\\\\\\\\$1$2") - messageJson = messageJson.replace("(?<=[^\\\\])(\")".toRegex(), "\\\\\"") - messageJson = messageJson.replace("(?<=[^\\\\])(\')".toRegex(), "\\\\\'") - messageJson = messageJson.replace("%7B".toRegex(), URLEncoder.encode("%7B")) - messageJson = messageJson.replace("%7D".toRegex(), URLEncoder.encode("%7D")) - messageJson = messageJson.replace("%22".toRegex(), URLEncoder.encode("%22")) - val javascriptCommand = - String.format( - BridgeUtil.JS_RESPONSE_FROM_JAVA, - messageJson, - callbackId - ) - Logger.d(javaClass.simpleName, "javascriptCommand->$javascriptCommand") - - // 必须要找主线程才会将数据传递出去 --- 划重点 - if (Thread.currentThread() === Looper.getMainLooper().thread) { - webView.evaluateJavascript(javascriptCommand, null) - } else { - mainHandler.post { webView.evaluateJavascript(javascriptCommand, null) } - } - } - - /** - * 分发message 必须在主线程才分发成功 - * - */ - private fun dispatchMessage(handlerName: String, data: String, callbackId: String) { - var messageJson = data - //escape special characters for json string 为json字符串转义特殊字符 - messageJson = messageJson.replace("(\\\\)([^utrn])".toRegex(), "\\\\\\\\$1$2") - messageJson = messageJson.replace("(?<=[^\\\\])(\")".toRegex(), "\\\\\"") - messageJson = messageJson.replace("(?<=[^\\\\])(\')".toRegex(), "\\\\\'") - messageJson = messageJson.replace("%7B".toRegex(), URLEncoder.encode("%7B")) - messageJson = messageJson.replace("%7D".toRegex(), URLEncoder.encode("%7D")) - messageJson = messageJson.replace("%22".toRegex(), URLEncoder.encode("%22")) - val javascriptCommand = - String.format( - BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, - handlerName, - messageJson, - callbackId - ) - Logger.d(javaClass.simpleName, "javascriptCommand->$javascriptCommand") - - // 必须要找主线程才会将数据传递出去 --- 划重点 - if (Thread.currentThread() === Looper.getMainLooper().thread) { - webView.evaluateJavascript(javascriptCommand, null) - } else { - mainHandler.post { webView.evaluateJavascript(javascriptCommand, null) } + if (state < State.DESTROY) { + state = State.DESTROY + mCallbacks.clear() + mMessageHandlers.clear() + mRequests.clear() } } - /** - * 保存message到消息队列 + * 调用Js handler。 * - * @param handlerName handlerName - * @param data data - * @param responseCallback OnBridgeCallback + * @param handlerName HandlerName. + * @param data Request data. + * @param responseCallback OnBridgeCallback. */ - fun callHandler( + @MainThread + override fun callHandler( handlerName: String, data: String, - responseCallback: ((String) -> Unit)? = null + responseCallback: ((String) -> Unit)? ) { + checkState() require(handlerName.isNotEmpty()) { "Empty handler name." } var callbackId = "" if (responseCallback != null) { callbackId = String.format( - BridgeUtil.CALLBACK_ID_FORMAT, - "${mUniqueId.incrementAndGet()}" + BridgeUtil.UNDERLINE_STR + "${SystemClock.currentThreadTimeMillis()}" + CALLBACK_ID_FORMAT, + mUniqueId.incrementAndGet(), + SystemClock.currentThreadTimeMillis() ) mCallbacks[callbackId] = responseCallback } - val request = mRequests - if (request != null) { + val jsCommand = String.format(JS_HANDLE_MESSAGE_FROM_JAVA, handlerName, data, callbackId) + if (state == State.IDLE) { //等待状态添加到消息集合否则分发消息 - val jsRequest = JSRequest(callbackId, handlerName, data) - request.add(jsRequest) + mRequests.add(jsCommand) } else { - dispatchMessage(handlerName, data, callbackId) + dispatchMessage(jsCommand) } } /** - * 这里只是加载lib包中assets中的 WebViewJavascriptBridge.js - * @param view webview + * 这里只是加载lib包中assets中的 WebViewJavascriptBridge.js。 + * @param view WebView. */ private fun loadJs(view: IWebView) { val mill = measureTimeMillis { @@ -203,17 +172,17 @@ class JsBridge(private val webView: IWebView) { val jsContent = assetFile2Str(context) view.loadUrl("javascript:$jsContent") } - Logger.i(javaClass.simpleName, "Load js bridge file cost:$mill ms") + info("load js bridge file cost:$mill ms") } /** - * 解析assets文件夹里面的代码,去除注释,取可执行的代码 - * @param c context - * @return 可执行代码 + * 解析assets文件夹里面的代码,去除注释,取可执行的代码。 + * @param c Context. + * @return 可执行代码。 */ private fun assetFile2Str(c: Context): String { try { - c.assets.open(BridgeUtil.JAVA_SCRIPT) + c.assets.open(JAVA_SCRIPT) .bufferedReader() .use { val sb = StringBuilder() @@ -229,10 +198,49 @@ class JsBridge(private val webView: IWebView) { } return "" } -} -data class JSRequest( - val callbackId: String, - val handlerName: String, - val data: String -) \ No newline at end of file + private fun dispatchMessage(jsCommand: String) { + runOnUiThread { + info("callJs->$jsCommand") + webView.evaluateJavascript(jsCommand, null) + } + } + + private fun dispatchMessage(jsCommands: List<String>) { + runOnUiThread { + jsCommands.forEach { + info("callJs->$it") + webView.evaluateJavascript(it, null) + } + } + } + + private fun runOnUiThread(runnable: () -> Unit) { + // 必须要找主线程才会将数据传递出去 --- 划重点 + if (Thread.currentThread() === Looper.getMainLooper().thread) { + if (state == State.READY) { + runnable() + } else { + warn("JsBridge is destroyed, stop dispatch to js") + } + } else { + mainHandler.post { + if (state == State.READY) { + runnable() + } else { + warn("JsBridge is destroyed, stop dispatch to js") + } + } + } + } + + companion object { + const val JAVA_SCRIPT = "WebViewJavascriptBridge.js" + const val CALLBACK_ID_FORMAT = "JAVA_CB_%s_%s" + const val JS_HANDLE_MESSAGE_FROM_JAVA = + "javascript:WebViewJavascriptBridge._handleMessageFromNative('%s','%s','%s');" + const val JS_RESPONSE_FROM_JAVA = + "javascript:WebViewJavascriptBridge._responseFromNative('%s','%s');" + var debug: Boolean = false + } +} \ No newline at end of file diff --git a/library/src/main/java/com/orientsec/jsbridge/Logger.kt b/library/src/main/java/com/orientsec/jsbridge/Logger.kt index 0d1f9fc..21fec58 100644 --- a/library/src/main/java/com/orientsec/jsbridge/Logger.kt +++ b/library/src/main/java/com/orientsec/jsbridge/Logger.kt @@ -1,30 +1,40 @@ package com.orientsec.jsbridge import android.util.Log +import com.orientsec.jsbridge.JsBridge.Companion.debug -object Logger { - var debug: Boolean = false +interface Loggable { + fun debug(message: String) - fun d(tag: String, message: String) { + fun info(message: String) + + fun warn(message: String) + + fun error(message: String) +} + +class Logger(private val tag: String) : Loggable { + + override fun debug(message: String) { if (debug) { Log.d(tag, message) } } - fun i(tag: String, message: String) { + override fun info(message: String) { if (debug) { Log.i(tag, message) } } - fun w(tag: String, message: String) { + override fun warn(message: String) { if (debug) { Log.w(tag, message) } } - fun e(tag: String, message: String) { + override fun error(message: String) { if (debug) { Log.e(tag, message) } diff --git a/library/src/main/java/com/orientsec/jsbridge/OnPageLoadListener.kt b/library/src/main/java/com/orientsec/jsbridge/OnPageLoadListener.kt index a046d9e..8ac7505 100644 --- a/library/src/main/java/com/orientsec/jsbridge/OnPageLoadListener.kt +++ b/library/src/main/java/com/orientsec/jsbridge/OnPageLoadListener.kt @@ -1,5 +1,12 @@ package com.orientsec.jsbridge +/** + * 页面加载回调。 + */ interface OnPageLoadListener { + /** + * 页面加载回调。 + * @param isLoaded 页面是否加载成功。 + */ fun onPageLoaded(isLoaded: Boolean) } \ No newline at end of file From 2297d5b91c3eb35bad92815c25c6d6514abde1f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E7=90=A5?= <meihu@orientsec.com.cn> Date: Mon, 20 Apr 2020 17:50:08 +0800 Subject: [PATCH 05/14] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dwebview5.0-6.0=E4=B9=8B?= =?UTF-8?q?=E9=97=B4=E4=B8=8Eandroidx=E7=9A=84=E5=85=BC=E5=AE=B9=E6=80=A7?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=9B=20=E4=BF=AE=E5=A4=8D=E5=A4=9A?= =?UTF-8?q?=E6=AC=A1=E8=B0=83=E7=94=A8onPageFinished=E6=8A=A5=E9=94=99?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++-- .../com/orientsec/jsbridge/BridgeWebView.kt | 22 +++++++++++++++---- .../java/com/orientsec/jsbridge/JsBridge.kt | 2 +- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index fae62e9..881254e 100644 --- a/build.gradle +++ b/build.gradle @@ -7,13 +7,13 @@ props.each { } buildscript { - ext.kotlin_version = '1.3.70' + ext.kotlin_version = '1.3.71' repositories { jcenter() google() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.2' + classpath 'com.android.tools.build:gradle:3.6.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" //classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' // NOTE: Do not place your application dependencies here; they belong diff --git a/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt b/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt index d119d12..590931b 100644 --- a/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt +++ b/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt @@ -2,20 +2,23 @@ package com.orientsec.jsbridge import android.annotation.SuppressLint import android.content.Context +import android.content.res.Configuration +import android.os.Build import android.util.AttributeSet import android.webkit.WebView import android.webkit.WebViewClient + @SuppressLint("SetJavaScriptEnabled") class BridgeWebView : WebView, IWebView, IJSBridge, OnPageLoadListener { var mOnPageLoadListener: OnPageLoadListener = this private val jsBridge: JsBridge = JsBridge(this) private var bridgeWebViewClient: BridgeWebViewClient = BridgeWebViewClient(jsBridge) - constructor(context: Context?) : super(context) - constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, + constructor(context: Context) : super(getFixedContext(context)) + constructor(context: Context, attrs: AttributeSet?) : super(getFixedContext(context), attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + getFixedContext(context), attrs, defStyleAttr ) @@ -68,4 +71,15 @@ class BridgeWebView : WebView, IWebView, IJSBridge, OnPageLoadListener { override val onPageLoadListener: OnPageLoadListener get() = mOnPageLoadListener + + companion object { + private fun getFixedContext(context: Context): Context { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP + && Build.VERSION.SDK_INT <= Build.VERSION_CODES.M + ) { + return context.createConfigurationContext(Configuration()) + } + return context + } + } } \ No newline at end of file diff --git a/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt b/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt index 4c58d44..d88b65a 100644 --- a/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt +++ b/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt @@ -102,7 +102,7 @@ class JsBridge(private val webView: IWebView) : IJSBridge, @MainThread fun register(view: IWebView) { - require(state == State.IDLE) { "JsBridge is already registered." } + if (state != State.IDLE) return state = State.READY registerHandler("onPageLoad", object : BridgeHandler { override fun handle(data: String, callback: (String) -> Unit) { From d61d2775b0ac6687162ac1db7a67415d5fe8f95d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E7=90=A5?= <meihu@orientsec.com.cn> Date: Fri, 22 Oct 2021 14:15:11 +0800 Subject: [PATCH 06/14] =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=EF=BC=9B=E4=BF=AE=E5=A4=8D=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E9=87=8D=E8=BD=BDbridge=E5=A4=B1=E6=95=88=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 8 +- example/build.gradle | 12 +-- example/src/main/AndroidManifest.xml | 1 + gradle/wrapper/gradle-wrapper.properties | 2 +- library/bintray.gradle | 88 ++++++------------- library/build.gradle | 6 +- .../com/orientsec/jsbridge/BridgeWebView.kt | 4 +- .../java/com/orientsec/jsbridge/IWebView.kt | 6 +- .../java/com/orientsec/jsbridge/JsBridge.kt | 24 ++--- 9 files changed, 59 insertions(+), 92 deletions(-) diff --git a/build.gradle b/build.gradle index 881254e..e2b7fa2 100644 --- a/build.gradle +++ b/build.gradle @@ -7,13 +7,13 @@ props.each { } buildscript { - ext.kotlin_version = '1.3.71' + ext.kotlin_version = '1.5.30' repositories { - jcenter() + maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' } google() } dependencies { - classpath 'com.android.tools.build:gradle:3.6.3' + classpath 'com.android.tools.build:gradle:7.0.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" //classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' // NOTE: Do not place your application dependencies here; they belong @@ -23,7 +23,7 @@ buildscript { allprojects { repositories { - jcenter() + maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' } google() } } diff --git a/example/build.gradle b/example/build.gradle index e71b375..ccd1dfb 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -2,15 +2,15 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { - compileSdkVersion 29 - buildToolsVersion "29.0.3" + compileSdkVersion 31 + buildToolsVersion '31.0.0' defaultConfig { applicationId "com.orientsec.jsbridge.example" minSdkVersion 19 - targetSdkVersion 29 + targetSdkVersion 31 versionCode 1 - versionName "1.0" + versionName '1.0' } buildTypes { release { @@ -23,7 +23,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation project(':library') - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'com.google.code.gson:gson:2.8.2' + implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'com.google.code.gson:gson:2.8.6' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml index 6576e35..3054658 100644 --- a/example/src/main/AndroidManifest.xml +++ b/example/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ android:theme="@style/AppTheme"> <activity android:name=".MainActivity" + android:exported="true" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e7cd316..aab1cb2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/library/bintray.gradle b/library/bintray.gradle index 9289300..e0fffa2 100644 --- a/library/bintray.gradle +++ b/library/bintray.gradle @@ -2,36 +2,6 @@ //apply plugin: "com.jfrog.artifactory" apply plugin: 'maven-publish' -task sourcesJar(type: Jar) { - from android.sourceSets.main.java.srcDirs - archiveClassifier.set('sources') -} - -task javadoc(type: Javadoc) { - options.addStringOption('Xdoclint:none', '-quiet') - options.charSet = 'UTF-8' - options.encoding = "UTF-8" - source = android.sourceSets.main.java.srcDirs - classpath += configurations.compile - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) -} - -task javadocJar(type: Jar, dependsOn: javadoc) { - archiveClassifier.set('javadoc') - from javadoc.destinationDir -} - -javadoc { - options { - encoding "UTF-8" - charSet 'UTF-8' - author true - version true - links "http://docs.oracle.com/javase/7/docs/api" - title "$project.name $PROJ_VERSION" - } -} - def pomConfig = { licenses { @@ -50,42 +20,34 @@ def pomConfig = { } } -publishing { - repositories { - maven { - url RELEASE_REPOSITORY_URL - credentials { - username NEXUS_USERNAME - password NEXUS_PASSWORD - } - } - } - publications { - mavenJava(MavenPublication) { - groupId PROJ_GROUP - artifactId PROJ_ARTIFACT - //artifact javadocJar - artifact sourcesJar - version = PROJ_VERSION - pom.withXml { - def root = asNode() - root.appendNode('description', PROJ_DESCRIPTION) - root.children().last() + pomConfig - - def dependenciesNode = root.appendNode('dependencies') - configurations.implementation.allDependencies.each { - if (it.group && it.name && it.version) { - def dependencyNode = dependenciesNode.appendNode('dependency') - dependencyNode.appendNode('groupId', it.group) - dependencyNode.appendNode('artifactId', it.name) - dependencyNode.appendNode('version', it.version) +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + // The following applies a component to this publication + // which results in publishing an app bundle. + groupId = PROJ_GROUP + artifactId = project.name + version = PROJ_VERSION + + artifact(bundleReleaseAar) + pom.withXml { + def root = asNode() + root.appendNode('description', PROJ_DESCRIPTION) + root.children().last() + pomConfig + + def dependenciesNode = root.appendNode('dependencies') + configurations.api.allDependencies.each { + if (it.group && it.name && it.version) { + def dependencyNode = dependenciesNode.appendNode('dependency') + dependencyNode.appendNode('groupId', it.group) + dependencyNode.appendNode('artifactId', it.name) + dependencyNode.appendNode('version', it.version) + } } } + } } } -} - -afterEvaluate { - publishing.publications.mavenJava.artifact(bundleReleaseAar) } \ No newline at end of file diff --git a/library/build.gradle b/library/build.gradle index 50079d2..675b52b 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -3,12 +3,12 @@ apply plugin: 'kotlin-android' apply from: 'bintray.gradle' android { - compileSdkVersion 29 - buildToolsVersion "29.0.3" + compileSdkVersion 31 + buildToolsVersion '31.0.0' defaultConfig { minSdkVersion 19 - targetSdkVersion 29 + targetSdkVersion 31 versionCode 1 versionName version } diff --git a/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt b/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt index 590931b..5b0b0d9 100644 --- a/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt +++ b/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt @@ -11,9 +11,9 @@ import android.webkit.WebViewClient @SuppressLint("SetJavaScriptEnabled") class BridgeWebView : WebView, IWebView, IJSBridge, OnPageLoadListener { - var mOnPageLoadListener: OnPageLoadListener = this + private var mOnPageLoadListener: OnPageLoadListener = this private val jsBridge: JsBridge = JsBridge(this) - private var bridgeWebViewClient: BridgeWebViewClient = BridgeWebViewClient(jsBridge) + private val bridgeWebViewClient: BridgeWebViewClient = BridgeWebViewClient(jsBridge) constructor(context: Context) : super(getFixedContext(context)) constructor(context: Context, attrs: AttributeSet?) : super(getFixedContext(context), attrs) diff --git a/library/src/main/java/com/orientsec/jsbridge/IWebView.kt b/library/src/main/java/com/orientsec/jsbridge/IWebView.kt index a2e351a..1dd98c9 100644 --- a/library/src/main/java/com/orientsec/jsbridge/IWebView.kt +++ b/library/src/main/java/com/orientsec/jsbridge/IWebView.kt @@ -6,11 +6,11 @@ import android.webkit.ValueCallback interface IWebView { fun getContext(): Context? - fun loadUrl(url: String?) + fun loadUrl(url: String) - fun addJavascriptInterface(obj: Any?, interfaceName: String?) + fun addJavascriptInterface(obj: Any, interfaceName: String) - fun evaluateJavascript(var1: String?, callback: ValueCallback<String>?) + fun evaluateJavascript(var1: String, callback: ValueCallback<String>?) val onPageLoadListener: OnPageLoadListener } \ No newline at end of file diff --git a/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt b/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt index d88b65a..e0a3ad4 100644 --- a/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt +++ b/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt @@ -102,17 +102,21 @@ class JsBridge(private val webView: IWebView) : IJSBridge, @MainThread fun register(view: IWebView) { - if (state != State.IDLE) return - state = State.READY - registerHandler("onPageLoad", object : BridgeHandler { - override fun handle(data: String, callback: (String) -> Unit) { - runOnUiThread { view.onPageLoadListener.onPageLoaded(data.isNotEmpty()) } + if (state != State.IDLE) { + state = State.READY + registerHandler("onPageLoad", object : BridgeHandler { + override fun handle(data: String, callback: (String) -> Unit) { + runOnUiThread { view.onPageLoadListener.onPageLoaded(data.isNotEmpty()) } + } + }) + } + //修复reload后bridge失效的问题。 + if (state == State.READY) { + loadJs(view) + if (mRequests.isNotEmpty()) { + dispatchMessage(mRequests) + mRequests.clear() } - }) - loadJs(view) - if (mRequests.isNotEmpty()) { - dispatchMessage(mRequests) - mRequests.clear() } } From daef2bb6dce0c17f44b8d7613b23a779acfb2ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E7=90=A5?= <meihu@orientsec.com.cn> Date: Fri, 22 Oct 2021 15:04:57 +0800 Subject: [PATCH 07/14] =?UTF-8?q?=E5=A2=9E=E5=8A=A0jdk=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/jitpack.yml | 2 ++ library/jitpack.yml | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 example/jitpack.yml create mode 100644 library/jitpack.yml diff --git a/example/jitpack.yml b/example/jitpack.yml new file mode 100644 index 0000000..46c8529 --- /dev/null +++ b/example/jitpack.yml @@ -0,0 +1,2 @@ +jdk: + - openjdk11 \ No newline at end of file diff --git a/library/jitpack.yml b/library/jitpack.yml new file mode 100644 index 0000000..46c8529 --- /dev/null +++ b/library/jitpack.yml @@ -0,0 +1,2 @@ +jdk: + - openjdk11 \ No newline at end of file From af3c11c3c58be4e52c3634b26c6dd3dd6dcf924b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E7=90=A5?= <meihu@orientsec.com.cn> Date: Fri, 22 Oct 2021 15:51:49 +0800 Subject: [PATCH 08/14] =?UTF-8?q?=E5=A2=9E=E5=8A=A0jdk=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/jitpack.yml => jitpack.yml | 0 library/jitpack.yml | 2 -- 2 files changed, 2 deletions(-) rename example/jitpack.yml => jitpack.yml (100%) delete mode 100644 library/jitpack.yml diff --git a/example/jitpack.yml b/jitpack.yml similarity index 100% rename from example/jitpack.yml rename to jitpack.yml diff --git a/library/jitpack.yml b/library/jitpack.yml deleted file mode 100644 index 46c8529..0000000 --- a/library/jitpack.yml +++ /dev/null @@ -1,2 +0,0 @@ -jdk: - - openjdk11 \ No newline at end of file From fb77c701396d40bf5b490d08496244336accef44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E7=90=A5?= <meihu@orientsec.com.cn> Date: Fri, 22 Oct 2021 16:46:02 +0800 Subject: [PATCH 09/14] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- library/src/main/java/com/orientsec/jsbridge/JsBridge.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt b/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt index e0a3ad4..b8ac361 100644 --- a/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt +++ b/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt @@ -102,7 +102,7 @@ class JsBridge(private val webView: IWebView) : IJSBridge, @MainThread fun register(view: IWebView) { - if (state != State.IDLE) { + if (state == State.IDLE) { state = State.READY registerHandler("onPageLoad", object : BridgeHandler { override fun handle(data: String, callback: (String) -> Unit) { From 0a458ff527b4be1b0a2d93806ad36a724afbbb3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=85=E7=90=A5?= <meihu@orientsec.com.cn> Date: Thu, 18 Aug 2022 13:46:43 +0800 Subject: [PATCH 10/14] =?UTF-8?q?=E9=87=8D=E6=9E=84=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 8 +- android/.gitignore | 7 + build.gradle => android/build.gradle | 4 +- {example => android/example}/.gitignore | 0 {example => android/example}/build.gradle | 8 +- .../example}/proguard-rules.pro | 0 .../example}/src/main/AndroidManifest.xml | 7 +- .../jsbridge/example/MainActivity.kt | 72 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../example}/src/main/res/values/strings.xml | 0 .../example}/src/main/res/values/styles.xml | 0 .../gradle.properties | 0 .../gradle}/wrapper/gradle-wrapper.jar | Bin .../gradle}/wrapper/gradle-wrapper.properties | 2 +- gradlew => android/gradlew | 0 gradlew.bat => android/gradlew.bat | 0 {library => android/library}/.gitignore | 0 {library => android/library}/bintray.gradle | 2 +- {library => android/library}/build.gradle | 11 +- .../library}/proguard-rules.pro | 0 .../library}/src/main/AndroidManifest.xml | 0 .../src/main/assets/jsbridge/index.min.js | 1 + .../com/orientsec/jsbridge/BridgeHandler.kt | 36 + .../com/orientsec/jsbridge/BridgeWebView.kt | 81 + .../orientsec/jsbridge/BridgeWebViewClient.kt | 31 +- .../java/com/orientsec/jsbridge/JsBridge.kt | 14 +- .../orientsec/jsbridge/JsBridgeDelegate.kt | 217 + .../java/com/orientsec/jsbridge/Logger.kt | 22 +- .../com/orientsec/jsbridge/PageListener.kt | 16 + .../main/java/com/orientsec/jsbridge/Utils.kt | 14 + .../peoject.properties | 2 +- settings.gradle => android/settings.gradle | 0 example/src/main/assets/demo.html | 110 - .../jsbridge/example/MainActivity.kt | 63 - javascrpit/demo/.eslintignore | 5 + javascrpit/demo/.eslintrc.js | 41 + javascrpit/demo/.gitingore | 2 + javascrpit/demo/.vscode/launch.json | 16 + javascrpit/demo/.vscode/settings.json | 5 + javascrpit/demo/.vscode/tasks.json | 13 + javascrpit/demo/dist/index.html | 15 + javascrpit/demo/dist/index.js | 200 + javascrpit/demo/package-lock.json | 5144 +++++++++++++++++ javascrpit/demo/package.json | 38 + javascrpit/demo/resources/index.html | 15 + javascrpit/demo/src/compat.tsx | 39 + javascrpit/demo/src/index.tsx | 55 + javascrpit/demo/src/native.tsx | 24 + javascrpit/demo/tsconfig.json | 105 + javascrpit/demo/webpack.config.js | 38 + javascrpit/lib/.eslintignore | 5 + javascrpit/lib/.eslintrc.js | 41 + javascrpit/lib/.gitingore | 2 + javascrpit/lib/.npmingore | 4 + javascrpit/lib/.vscode/settings.json | 5 + javascrpit/lib/dist/index.min.js | 1 + javascrpit/lib/package-lock.json | 3452 +++++++++++ javascrpit/lib/package.json | 34 + javascrpit/lib/src/index.ts | 6 + javascrpit/lib/src/jsBridge.ts | 215 + javascrpit/lib/src/jsBridgeCompat.ts | 64 + javascrpit/lib/tsconfig.json | 95 + javascrpit/lib/typing/index.d.ts | 3 + javascrpit/lib/typing/jsBridge.d.ts | 40 + javascrpit/lib/typing/jsBridgeCompat.d.ts | 21 + javascrpit/lib/webpack.config.js | 38 + jitpack.yml | 2 - .../main/assets/WebViewJavascriptBridge.js | 127 - .../com/orientsec/jsbridge/BridgeHandler.kt | 16 - .../com/orientsec/jsbridge/BridgeWebView.kt | 85 - .../java/com/orientsec/jsbridge/IWebView.kt | 16 - .../java/com/orientsec/jsbridge/JsBridge.kt | 250 - .../orientsec/jsbridge/OnPageLoadListener.kt | 12 - 76 files changed, 10295 insertions(+), 722 deletions(-) create mode 100644 android/.gitignore rename build.gradle => android/build.gradle (90%) rename {example => android/example}/.gitignore (100%) rename {example => android/example}/build.gradle (86%) rename {example => android/example}/proguard-rules.pro (100%) rename {example => android/example}/src/main/AndroidManifest.xml (79%) create mode 100644 android/example/src/main/java/com/orientsec/jsbridge/example/MainActivity.kt rename {example => android/example}/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename {example => android/example}/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename {example => android/example}/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename {example => android/example}/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {example => android/example}/src/main/res/values/strings.xml (100%) rename {example => android/example}/src/main/res/values/styles.xml (100%) rename gradle.properties => android/gradle.properties (100%) rename {gradle => android/gradle}/wrapper/gradle-wrapper.jar (100%) rename {gradle => android/gradle}/wrapper/gradle-wrapper.properties (93%) rename gradlew => android/gradlew (100%) rename gradlew.bat => android/gradlew.bat (100%) rename {library => android/library}/.gitignore (100%) rename {library => android/library}/bintray.gradle (97%) rename {library => android/library}/build.gradle (75%) rename {library => android/library}/proguard-rules.pro (100%) rename {library => android/library}/src/main/AndroidManifest.xml (100%) create mode 100644 android/library/src/main/assets/jsbridge/index.min.js create mode 100644 android/library/src/main/java/com/orientsec/jsbridge/BridgeHandler.kt create mode 100644 android/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt rename {library => android/library}/src/main/java/com/orientsec/jsbridge/BridgeWebViewClient.kt (84%) rename library/src/main/java/com/orientsec/jsbridge/IJSBridge.kt => android/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt (76%) create mode 100644 android/library/src/main/java/com/orientsec/jsbridge/JsBridgeDelegate.kt rename {library => android/library}/src/main/java/com/orientsec/jsbridge/Logger.kt (51%) create mode 100644 android/library/src/main/java/com/orientsec/jsbridge/PageListener.kt create mode 100644 android/library/src/main/java/com/orientsec/jsbridge/Utils.kt rename peoject.properties => android/peoject.properties (96%) rename settings.gradle => android/settings.gradle (100%) delete mode 100644 example/src/main/assets/demo.html delete mode 100644 example/src/main/java/com/orientsec/jsbridge/example/MainActivity.kt create mode 100644 javascrpit/demo/.eslintignore create mode 100644 javascrpit/demo/.eslintrc.js create mode 100644 javascrpit/demo/.gitingore create mode 100644 javascrpit/demo/.vscode/launch.json create mode 100644 javascrpit/demo/.vscode/settings.json create mode 100644 javascrpit/demo/.vscode/tasks.json create mode 100644 javascrpit/demo/dist/index.html create mode 100644 javascrpit/demo/dist/index.js create mode 100644 javascrpit/demo/package-lock.json create mode 100644 javascrpit/demo/package.json create mode 100644 javascrpit/demo/resources/index.html create mode 100644 javascrpit/demo/src/compat.tsx create mode 100644 javascrpit/demo/src/index.tsx create mode 100644 javascrpit/demo/src/native.tsx create mode 100644 javascrpit/demo/tsconfig.json create mode 100644 javascrpit/demo/webpack.config.js create mode 100644 javascrpit/lib/.eslintignore create mode 100644 javascrpit/lib/.eslintrc.js create mode 100644 javascrpit/lib/.gitingore create mode 100644 javascrpit/lib/.npmingore create mode 100644 javascrpit/lib/.vscode/settings.json create mode 100644 javascrpit/lib/dist/index.min.js create mode 100644 javascrpit/lib/package-lock.json create mode 100644 javascrpit/lib/package.json create mode 100644 javascrpit/lib/src/index.ts create mode 100644 javascrpit/lib/src/jsBridge.ts create mode 100644 javascrpit/lib/src/jsBridgeCompat.ts create mode 100644 javascrpit/lib/tsconfig.json create mode 100644 javascrpit/lib/typing/index.d.ts create mode 100644 javascrpit/lib/typing/jsBridge.d.ts create mode 100644 javascrpit/lib/typing/jsBridgeCompat.d.ts create mode 100644 javascrpit/lib/webpack.config.js delete mode 100644 jitpack.yml delete mode 100644 library/src/main/assets/WebViewJavascriptBridge.js delete mode 100644 library/src/main/java/com/orientsec/jsbridge/BridgeHandler.kt delete mode 100644 library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt delete mode 100644 library/src/main/java/com/orientsec/jsbridge/IWebView.kt delete mode 100644 library/src/main/java/com/orientsec/jsbridge/JsBridge.kt delete mode 100644 library/src/main/java/com/orientsec/jsbridge/OnPageLoadListener.kt diff --git a/.gitignore b/.gitignore index 144609d..36f2735 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1 @@ -*.iml -.gradle -/gradle/wrapper/*.zip -/local.properties -.DS_Store -/build -.idea +/javascrpit/demo/node_modules/ diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..144609d --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,7 @@ +*.iml +.gradle +/gradle/wrapper/*.zip +/local.properties +.DS_Store +/build +.idea diff --git a/build.gradle b/android/build.gradle similarity index 90% rename from build.gradle rename to android/build.gradle index e2b7fa2..efa5490 100644 --- a/build.gradle +++ b/android/build.gradle @@ -7,13 +7,13 @@ props.each { } buildscript { - ext.kotlin_version = '1.5.30' + ext.kotlin_version = '1.7.10' repositories { maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' } google() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.2' + classpath 'com.android.tools.build:gradle:7.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" //classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' // NOTE: Do not place your application dependencies here; they belong diff --git a/example/.gitignore b/android/example/.gitignore similarity index 100% rename from example/.gitignore rename to android/example/.gitignore diff --git a/example/build.gradle b/android/example/build.gradle similarity index 86% rename from example/build.gradle rename to android/example/build.gradle index ccd1dfb..22e50d8 100644 --- a/example/build.gradle +++ b/android/example/build.gradle @@ -2,13 +2,13 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { - compileSdkVersion 31 - buildToolsVersion '31.0.0' + compileSdkVersion 33 + buildToolsVersion '33.0.0' defaultConfig { applicationId "com.orientsec.jsbridge.example" - minSdkVersion 19 - targetSdkVersion 31 + minSdkVersion 23 + targetSdkVersion 33 versionCode 1 versionName '1.0' } diff --git a/example/proguard-rules.pro b/android/example/proguard-rules.pro similarity index 100% rename from example/proguard-rules.pro rename to android/example/proguard-rules.pro diff --git a/example/src/main/AndroidManifest.xml b/android/example/src/main/AndroidManifest.xml similarity index 79% rename from example/src/main/AndroidManifest.xml rename to android/example/src/main/AndroidManifest.xml index 3054658..b497422 100644 --- a/example/src/main/AndroidManifest.xml +++ b/android/example/src/main/AndroidManifest.xml @@ -4,14 +4,13 @@ <uses-permission android:name="android.permission.INTERNET" /> <application - android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" + android:usesCleartextTraffic="true"> <activity android:name=".MainActivity" - android:exported="true" - android:label="@string/app_name"> + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> diff --git a/android/example/src/main/java/com/orientsec/jsbridge/example/MainActivity.kt b/android/example/src/main/java/com/orientsec/jsbridge/example/MainActivity.kt new file mode 100644 index 0000000..6bda7dd --- /dev/null +++ b/android/example/src/main/java/com/orientsec/jsbridge/example/MainActivity.kt @@ -0,0 +1,72 @@ +package com.orientsec.jsbridge.example + +import android.annotation.SuppressLint +import android.app.Activity +import android.os.Bundle +import android.util.Log +import android.view.View +import android.webkit.JsResult +import android.webkit.WebChromeClient +import android.webkit.WebView +import android.widget.Button +import android.widget.Toast +import com.orientsec.jsbridge.* + +class MainActivity : Activity(), View.OnClickListener { + private lateinit var webView: BridgeWebView + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + WebView.setWebContentsDebuggingEnabled(true) + debug = true + webView = findViewById(R.id.webView) + val settings = webView.settings + settings.javaScriptEnabled = true + val button = findViewById<Button>(R.id.button) + button.setOnClickListener(this) + webView.loadUrl("http:///192.168.106.129:8080") + webView.registerHandler("hello") { data, callback -> + Log.i("MainActivity", "handler = hello, data from web = $data") + callback.onResult("Hello! Welcome to visit native!") + } + webView.webChromeClient = object : WebChromeClient() { + override fun onJsAlert( + view: WebView?, + url: String?, + message: String?, + result: JsResult? + ): Boolean { + return super.onJsAlert(view, url, message, result) + } + } + } + + override fun onClick(v: View) { + if (R.id.button == v.id) { + webView.callHandler( + "hello", + "hello \\ from \\Java \\kotlin", + object : BridgeCallback { + override fun onResult(data: String) { + Toast.makeText( + this@MainActivity, + "Response from js: $data", + Toast.LENGTH_LONG + ).show() + } + + override fun onError(code: Int, info: String) { + Toast.makeText( + this@MainActivity, + "Error from js: $code, $info", + Toast.LENGTH_LONG + ).show() + + } + + }) + } + } +} \ No newline at end of file diff --git a/example/src/main/res/mipmap-hdpi/ic_launcher.png b/android/example/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from example/src/main/res/mipmap-hdpi/ic_launcher.png rename to android/example/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/example/src/main/res/mipmap-mdpi/ic_launcher.png b/android/example/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from example/src/main/res/mipmap-mdpi/ic_launcher.png rename to android/example/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/example/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/example/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from example/src/main/res/mipmap-xhdpi/ic_launcher.png rename to android/example/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/example/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/example/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from example/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to android/example/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/example/src/main/res/values/strings.xml b/android/example/src/main/res/values/strings.xml similarity index 100% rename from example/src/main/res/values/strings.xml rename to android/example/src/main/res/values/strings.xml diff --git a/example/src/main/res/values/styles.xml b/android/example/src/main/res/values/styles.xml similarity index 100% rename from example/src/main/res/values/styles.xml rename to android/example/src/main/res/values/styles.xml diff --git a/gradle.properties b/android/gradle.properties similarity index 100% rename from gradle.properties rename to android/gradle.properties diff --git a/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from gradle/wrapper/gradle-wrapper.jar rename to android/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties similarity index 93% rename from gradle/wrapper/gradle-wrapper.properties rename to android/gradle/wrapper/gradle-wrapper.properties index aab1cb2..334f0f3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip diff --git a/gradlew b/android/gradlew similarity index 100% rename from gradlew rename to android/gradlew diff --git a/gradlew.bat b/android/gradlew.bat similarity index 100% rename from gradlew.bat rename to android/gradlew.bat diff --git a/library/.gitignore b/android/library/.gitignore similarity index 100% rename from library/.gitignore rename to android/library/.gitignore diff --git a/library/bintray.gradle b/android/library/bintray.gradle similarity index 97% rename from library/bintray.gradle rename to android/library/bintray.gradle index e0fffa2..dbdc574 100644 --- a/library/bintray.gradle +++ b/android/library/bintray.gradle @@ -27,7 +27,7 @@ afterEvaluate { // The following applies a component to this publication // which results in publishing an app bundle. groupId = PROJ_GROUP - artifactId = project.name + artifactId = PROJ_ARTIFACT version = PROJ_VERSION artifact(bundleReleaseAar) diff --git a/library/build.gradle b/android/library/build.gradle similarity index 75% rename from library/build.gradle rename to android/library/build.gradle index 675b52b..dd9d238 100644 --- a/library/build.gradle +++ b/android/library/build.gradle @@ -3,14 +3,12 @@ apply plugin: 'kotlin-android' apply from: 'bintray.gradle' android { - compileSdkVersion 31 - buildToolsVersion '31.0.0' + compileSdkVersion 33 + buildToolsVersion '33.0.0' defaultConfig { - minSdkVersion 19 - targetSdkVersion 31 - versionCode 1 - versionName version + minSdkVersion 21 + targetSdkVersion 33 } buildTypes { release { @@ -23,4 +21,5 @@ android { dependencies { implementation 'com.android.support:support-annotations:28.0.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "androidx.webkit:webkit:1.4.0" } diff --git a/library/proguard-rules.pro b/android/library/proguard-rules.pro similarity index 100% rename from library/proguard-rules.pro rename to android/library/proguard-rules.pro diff --git a/library/src/main/AndroidManifest.xml b/android/library/src/main/AndroidManifest.xml similarity index 100% rename from library/src/main/AndroidManifest.xml rename to android/library/src/main/AndroidManifest.xml diff --git a/android/library/src/main/assets/jsbridge/index.min.js b/android/library/src/main/assets/jsbridge/index.min.js new file mode 100644 index 0000000..0efef8b --- /dev/null +++ b/android/library/src/main/assets/jsbridge/index.min.js @@ -0,0 +1 @@ +!function(e,r){"object"==typeof exports&&"object"==typeof module?module.exports=r():"function"==typeof define&&define.amd?define([],r):"object"==typeof exports?exports.JsBridge=r():e.JsBridge=r()}(self,(()=>(()=>{"use strict";var e={};class r{constructor(e,r,n){this.type=e,this.data=r,this.callbackId=n}}class n{constructor(){this.receiveMessageQueue=[],this.messageHandlers=new Map,this.responseCallbacks=new Map,this.uniqueId=1}static getInstance(){if(!window.jsBridge){console.log("JsBridge: new Javascript bridge."),window.jsBridge=new n;const e=new Event("WebViewJavascriptBridgeReady");window.dispatchEvent(e)}return window.jsBridge}init(e){var r;if(this.defaultHandler)throw new Error("WebViewJavascriptBridge.init called twice.");this.defaultHandler=e;const n=null!==(r=this.receiveMessageQueue)&&void 0!==r?r:[];this.receiveMessageQueue=void 0;for(let e=0;e<n.length;e++){const r=n[e];this.handleNativeMessage(r.type,r.data,r.callbackId)}}registerHandler(e,r){this.messageHandlers.set(e,r)}callHandler(e,r,n){let s="";if(n&&(s="cb_"+this.uniqueId+++"_"+(new Date).getTime(),this.responseCallbacks.set(s,n)),console.info(`JsBridge: call native handler:${e}, callbackId:${s}, data:${r}.`),null==window.nativeBridge)return console.error("JsBridge: Native bridge is undefined"),void(null==n||n.onError(-1,"Native bridge is undefined."));try{window.nativeBridge.request(e,JSON.stringify(r),s)}catch(e){console.error("JsBridge: excute native JavascriptInterface error.",e),this.responseCallbacks.delete(s),null==n||n.onError(-2,"Fail to execute native JavascriptInterface.")}}response(e,r,n,s){console.info(`JsBridge: receive native response, callbackId:${s}, code:${e}, info:${r}, data:${n}.`),setTimeout((()=>{const i=this.responseCallbacks.get(s);i?(this.responseCallbacks.delete(s),0==e?i.onResult(decodeURI(n)):i.onError(e,r)):console.warn(`JsBridge: callback:${s} not found.`)}))}handleNativeMessage(e,r,n){setTimeout((()=>{var s;let i;i=null==n||""===n.trim()?this.emptyCallback(e):this.nativeCallback(e,n);const t=null!==(s=this.messageHandlers.get(e))&&void 0!==s?s:this.defaultHandler;if(null==t)return console.warn(`JsBridge: none js handler for [${e}].`),void i.onError(-1,`Handler:${e} not found.`);try{const n=decodeURI(r);console.info(`JsBridge: invoke js handler:${e}, data:${n}.`),t.handle(n,i)}catch(r){i.onError(-2,`Uncaught exception in js handler:${e}.`),console.error(`JsBridge: invoke js handler:${e} error.`,r)}}))}request(e,n,s){if(console.info(`JsBridge: receive native request:${e}, callbackId:${s}, data:${n}.`),this.receiveMessageQueue){const i=new r(e,n,s);this.receiveMessageQueue.push(i)}else this.handleNativeMessage(e,n,s)}emptyCallback(e){const r=r=>{console.warn(`JsBridge: none callback for [${e}], result:${r}.`)};return r.onError=(r,n)=>{console.warn(`JsBridge: none callback for [${e}], onError:[${r}, ${n}].`)},r.onResult=r=>{console.warn(`JsBridge: none callback for [${e}], onResult:${r}.`)},r}nativeCallback(e,r){const n=(n,s,i)=>{if(null!=window.nativeBridge)if(null!=window.nativeBridge.response)try{window.nativeBridge.response(n,s,JSON.stringify(i),r)}catch(r){console.error(`JsBridge: ${e} response to native error.`,r)}else console.error("JsBridge: native bridge.response() is undefined.");else console.error("JsBridge: native bridge is undefined.")},s=e=>{n(0,"OK",e)};return s.onError=(e,r)=>{n(e,r)},s.onResult=e=>{n(0,"OK",e)},s}}class s{constructor(){this.jsBridge=n.getInstance()}static getInstance(){if(null==window.WebViewJavascriptBridge){window.WebViewJavascriptBridge=new s;const e=new Event("WebViewJavascriptBridgeReady");e.bridge=window.WebViewJavascriptBridge,document.dispatchEvent(e)}return window.WebViewJavascriptBridge}init(e){this.jsBridge.init({handle(r,n){e(r,n)}})}registerHandler(e,r){this.jsBridge.registerHandler(e,{handle(e,n){r(e,n)}})}callHandler(e,r,n){n?this.jsBridge.callHandler(e,r,{onError:(e,r)=>{n(r)},onResult:e=>{n(e)}}):this.jsBridge.callHandler(e,r)}}return s.getInstance(),e=e.default})())); \ No newline at end of file diff --git a/android/library/src/main/java/com/orientsec/jsbridge/BridgeHandler.kt b/android/library/src/main/java/com/orientsec/jsbridge/BridgeHandler.kt new file mode 100644 index 0000000..5a5052f --- /dev/null +++ b/android/library/src/main/java/com/orientsec/jsbridge/BridgeHandler.kt @@ -0,0 +1,36 @@ +package com.orientsec.jsbridge + +/** + * 原生指令handler。 + */ +fun interface BridgeHandler { + /** + * 处理js请求。 + * @param data 请求数据。 + * @param callback 请求回调函数。 + */ + fun handle(data: String, callback: BridgeCallback) +} + +interface BridgeCallback { + /** + * 成功回调。 + */ + fun onResult(data: String) + + /** + * 失败回调。 + */ + fun onError(code: Int, info: String) +} + +//空回调函数。当js请求的回调id为null时,不需要回调js。 +object EmptyBridgeCallback : BridgeCallback { + override fun onResult(data: String) { + BridgeLogger.info("None js callback, onResult:$data") + } + + override fun onError(code: Int, info: String) { + BridgeLogger.info("None js callback, onError:[$code, $info]") + } +} \ No newline at end of file diff --git a/android/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt b/android/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt new file mode 100644 index 0000000..b8e731a --- /dev/null +++ b/android/library/src/main/java/com/orientsec/jsbridge/BridgeWebView.kt @@ -0,0 +1,81 @@ +package com.orientsec.jsbridge + +import android.content.Context +import android.util.AttributeSet +import android.webkit.WebView +import android.webkit.WebViewClient +import kotlin.system.measureTimeMillis + + +class BridgeWebView : WebView, JsBridge { + private val jsBridge: JsBridgeDelegate = JsBridgeDelegate(this) + private val bridgeWebViewClient: BridgeWebViewClient = BridgeWebViewClient() + + constructor(context: Context) : super(context.fixedContext()) + constructor(context: Context, attrs: AttributeSet?) : super(context.fixedContext(), attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context.fixedContext(), + attrs, + defStyleAttr + ) + + init { + super.setWebViewClient(bridgeWebViewClient) + addJavascriptInterface(jsBridge, "nativeBridge") + if (debug) { + setWebContentsDebuggingEnabled(true) + } + } + + override fun setWebViewClient(webViewClient: WebViewClient) { + bridgeWebViewClient.webViewClient = webViewClient + } + + override fun registerHandler(handlerName: String, handler: BridgeHandler) { + jsBridge.registerHandler(handlerName, handler) + } + + override fun registerHandler(handlers: Map<String, BridgeHandler>) { + jsBridge.registerHandler(handlers) + } + + override fun unregisterHandler(handlerName: String) { + jsBridge.unregisterHandler(handlerName) + } + + override fun callHandler( + handlerName: String, + data: String, + responseCallback: BridgeCallback? + ) { + jsBridge.callHandler(handlerName, data, responseCallback) + } + + + /** + * 这里只是加载lib包中assets中的 index.min.js。 + */ + internal fun loadJs() { + jsBridge.onLoadStart() + val mill = measureTimeMillis { + try { + val js = context.assets.open("jsbridge/index.min.js") + .bufferedReader() + .use { + val sb = StringBuilder() + do { + val line = it.readLine() + sb.append(line) + } while (line != null) + sb.toString() + } + loadUrl("javascript:$js") + } catch (e: Exception) { + BridgeLogger.error("Js bridge script load failed.", e) + } + + } + BridgeLogger.info("load js bridge script in:$mill ms") + jsBridge.onLoadFinished() + } +} \ No newline at end of file diff --git a/library/src/main/java/com/orientsec/jsbridge/BridgeWebViewClient.kt b/android/library/src/main/java/com/orientsec/jsbridge/BridgeWebViewClient.kt similarity index 84% rename from library/src/main/java/com/orientsec/jsbridge/BridgeWebViewClient.kt rename to android/library/src/main/java/com/orientsec/jsbridge/BridgeWebViewClient.kt index 793c580..c6fab48 100644 --- a/library/src/main/java/com/orientsec/jsbridge/BridgeWebViewClient.kt +++ b/android/library/src/main/java/com/orientsec/jsbridge/BridgeWebViewClient.kt @@ -11,9 +11,13 @@ import androidx.annotation.RequiresApi /** * 如果要自定义WebViewClient必须要集成此类。 */ -internal class BridgeWebViewClient(private val jsBridge: JsBridge) : WebViewClient() { +internal class BridgeWebViewClient : WebViewClient() { internal var webViewClient: WebViewClient = WebViewClient() + @Deprecated( + "Deprecated in Java", + ReplaceWith("shouldOverrideUrlLoading(WebView, WebResourceRequest)") + ) override fun shouldOverrideUrlLoading( view: WebView?, url: String? @@ -46,7 +50,7 @@ internal class BridgeWebViewClient(private val jsBridge: JsBridge) : WebViewClie override fun onPageFinished(view: WebView, url: String?) { webViewClient.onPageFinished(view, url) - jsBridge.register(view as IWebView) + (view as BridgeWebView).loadJs() } override fun onLoadResource(view: WebView?, url: String?) { @@ -58,6 +62,10 @@ internal class BridgeWebViewClient(private val jsBridge: JsBridge) : WebViewClie webViewClient.onPageCommitVisible(view, url) } + @Deprecated( + "Deprecated in Java", + ReplaceWith("shouldInterceptRequest(WebView, WebResourceRequest)") + ) override fun shouldInterceptRequest( view: WebView?, url: String? @@ -65,7 +73,6 @@ internal class BridgeWebViewClient(private val jsBridge: JsBridge) : WebViewClie return webViewClient.shouldInterceptRequest(view, url) } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) override fun shouldInterceptRequest( view: WebView?, request: WebResourceRequest? @@ -73,6 +80,10 @@ internal class BridgeWebViewClient(private val jsBridge: JsBridge) : WebViewClie return webViewClient.shouldInterceptRequest(view, request) } + @Deprecated( + "Deprecated in Java", + ReplaceWith("") + ) override fun onTooManyRedirects( view: WebView?, cancelMsg: Message?, @@ -81,6 +92,10 @@ internal class BridgeWebViewClient(private val jsBridge: JsBridge) : WebViewClie webViewClient.onTooManyRedirects(view, cancelMsg, continueMsg) } + @Deprecated( + "Deprecated in Java", + ReplaceWith("onReceivedError(WebView, WebResourceRequest, WebResourceError)") + ) override fun onReceivedError( view: WebView?, errorCode: Int, @@ -132,7 +147,6 @@ internal class BridgeWebViewClient(private val jsBridge: JsBridge) : WebViewClie webViewClient.onReceivedSslError(view, handler, error) } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) override fun onReceivedClientCertRequest( view: WebView?, request: ClientCertRequest? @@ -181,4 +195,13 @@ internal class BridgeWebViewClient(private val jsBridge: JsBridge) : WebViewClie return webViewClient.onRenderProcessGone(view, detail) } + @RequiresApi(Build.VERSION_CODES.O_MR1) + override fun onSafeBrowsingHit( + view: WebView?, + request: WebResourceRequest?, + threatType: Int, + callback: SafeBrowsingResponse? + ) { + webViewClient.onSafeBrowsingHit(view, request, threatType, callback) + } } \ No newline at end of file diff --git a/library/src/main/java/com/orientsec/jsbridge/IJSBridge.kt b/android/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt similarity index 76% rename from library/src/main/java/com/orientsec/jsbridge/IJSBridge.kt rename to android/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt index b09c6df..e8867c8 100644 --- a/library/src/main/java/com/orientsec/jsbridge/IJSBridge.kt +++ b/android/library/src/main/java/com/orientsec/jsbridge/JsBridge.kt @@ -1,9 +1,9 @@ package com.orientsec.jsbridge -interface IJSBridge { +interface JsBridge { /** * Register handler, so that javascript can call it. - * 注册处理程序,以便javascript调用它。 + * 注册处理程序,以便javascript调用。 * * @param handlerName HandlerName. * @param handler BridgeHandler. @@ -12,7 +12,7 @@ interface IJSBridge { /** * Register handler,so that javascript can call it. - * 注册处理程序,以便javascript调用它。 + * 注册处理程序,以便javascript调用。 * * @param handlers HandlerName. */ @@ -30,7 +30,9 @@ interface IJSBridge { * * @param handlerName HandlerName. * @param data Request data. - * @param responseCallback OnBridgeCallback. + * @param responseCallback BridgeCallback. */ - fun callHandler(handlerName: String, data: String, responseCallback: ((String) -> Unit)? = null) -} \ No newline at end of file + fun callHandler(handlerName: String, data: String, responseCallback: BridgeCallback? = null) +} + +var debug: Boolean = false \ No newline at end of file diff --git a/android/library/src/main/java/com/orientsec/jsbridge/JsBridgeDelegate.kt b/android/library/src/main/java/com/orientsec/jsbridge/JsBridgeDelegate.kt new file mode 100644 index 0000000..b26d5b6 --- /dev/null +++ b/android/library/src/main/java/com/orientsec/jsbridge/JsBridgeDelegate.kt @@ -0,0 +1,217 @@ +package com.orientsec.jsbridge + +import android.os.Handler +import android.os.Looper +import android.os.SystemClock +import android.webkit.JavascriptInterface +import androidx.annotation.MainThread +import java.net.URLEncoder +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +internal class JsBridgeDelegate(private val webView: BridgeWebView) : JsBridge, PageListener, + Loggable by BridgeLogger { + + /** + * Native callback id. Auto increament. + */ + private val mUniqueId = AtomicLong(0) + + /** + * Native handlers for js. + */ + private val mMessageHandlers: MutableMap<String, BridgeHandler> = + ConcurrentHashMap<String, BridgeHandler>() + + /** + * Native callbacks to receive js result. + */ + private val mCallbacks: MutableMap<String, BridgeCallback> = ConcurrentHashMap() + + /** + * Cached native request before page loaded. + */ + private var mRequests: MutableList<String>? = ArrayList() + + private val mainHandler = Handler(Looper.getMainLooper()) + + /** + * register handler,so that javascript can call it + * 注册处理程序,以便javascript调用它 + * + * @param handlerName handlerName + * @param handler BridgeHandler + */ + @MainThread + override fun registerHandler(handlerName: String, handler: BridgeHandler) { + // 添加至 Map<String, BridgeHandler> + mMessageHandlers[handlerName] = handler + } + + + /** + * register handler,so that javascript can call it + * 注册处理程序,以便javascript调用它 + * + * @param handlers handlerName + */ + @MainThread + override fun registerHandler(handlers: Map<String, BridgeHandler>) { + mMessageHandlers.putAll(handlers) + } + + + /** + * unregister handler + * + * @param handlerName + */ + @MainThread + override fun unregisterHandler(handlerName: String) { + mMessageHandlers.remove(handlerName) + } + + + @JavascriptInterface + fun request(handlerName: String, data: String, callbackId: String?) { + info("requestFromJs->$handlerName, $data, callbackId: $callbackId") + val callback = callbackOfId(callbackId) + val handler = mMessageHandlers[handlerName] + if (handler == null) { + warn("No handler for [$handlerName].") + callback.onError(-1, "Handler for [$handlerName] not found.") + } else { + handler.handle(data, callback) + } + } + + private fun callbackOfId(callbackId: String?): BridgeCallback { + return if (callbackId.isNullOrEmpty()) EmptyBridgeCallback + else DispatchCallback(callbackId, this) + } + + @JavascriptInterface + fun response(code: Int, info: String, data: String, callbackId: String) { + info("responseFromJs->code[$code], info[$info], data[$data], callbackId[$callbackId]") + mCallbacks.remove(callbackId)?.apply { + if (code == 0) onResult(data) + else onError(code, info) + } + } + + /** + * 调用Js handler。 + * + * @param handlerName HandlerName. + * @param data Request data. + * @param responseCallback BridgeCallback. + */ + @MainThread + override fun callHandler( + handlerName: String, + data: String, + responseCallback: BridgeCallback? + ) { + var callbackId = "" + if (responseCallback != null) { + callbackId = callbackId(mUniqueId.incrementAndGet()) + mCallbacks[callbackId] = responseCallback + } + val requestScript = requestScript(handlerName, data, callbackId) + val requests = mRequests + if (requests != null) { + //等待状态添加到消息集合否则分发消息 + requests.add(requestScript) + } else { + callJs(requestScript) + } + } + + internal fun callJs(script: String) { + runOnUiThread { + info("callJs->$script") + webView.evaluateJavascript(script, null) + } + } + + private fun callJs(scripts: List<String>) { + runOnUiThread { + scripts.forEach { + info("callJs->$it") + webView.evaluateJavascript(it, null) + } + } + } + + private fun runOnUiThread(runnable: () -> Unit) { + // 必须要找主线程才会将数据传递出去 --- 划重点 + if (Thread.currentThread() === Looper.getMainLooper().thread) { + runnable() + } else { + mainHandler.post { + runnable() + } + } + } + + override fun onLoadStart() { + } + + override fun onLoadFinished() { + val requests = mRequests + if (requests != null) { + callJs(requests) + mRequests = null + } + } + + private fun requestScript(handlerName: String, data: String, callbackId: String): String { + val formattedData = data.replace("\\", "\\\\") + return String.format(JS_REQUEST_FROM_NATIVE, handlerName, formattedData, callbackId) + } + + private fun callbackId(uniqueId: Long): String { + return String.format( + CALLBACK_ID_FORMAT, + uniqueId, + SystemClock.currentThreadTimeMillis() + ) + } + + companion object { + private const val CALLBACK_ID_FORMAT = "JAVA_CB_%s_%s" + private const val JS_REQUEST_FROM_NATIVE = + "javascript:jsBridge.request('%s','%s','%s');" + internal const val JS_RESPONSE_FROM_NATIVE = + "javascript:jsBridge.response('%d','%s','%s','%s');" + } +} + +internal class DispatchCallback( + private val callbackId: String, + private val jsBridgeDelegate: JsBridgeDelegate +) : BridgeCallback { + override fun onResult(data: String) { + jsBridgeDelegate.callJs(responseScript(0, "OK", data, callbackId)) + } + + override fun onError(code: Int, info: String) { + jsBridgeDelegate.callJs(responseScript(code, info, "", callbackId)) + } + + private fun responseScript( + code: Int, + info: String, + data: String, + callbackId: String + ): String { + val formattedData = data.replace("\\", "\\\\") + return String.format( + JsBridgeDelegate.JS_RESPONSE_FROM_NATIVE, + code, + info, + formattedData, + callbackId + ) + } +} \ No newline at end of file diff --git a/library/src/main/java/com/orientsec/jsbridge/Logger.kt b/android/library/src/main/java/com/orientsec/jsbridge/Logger.kt similarity index 51% rename from library/src/main/java/com/orientsec/jsbridge/Logger.kt rename to android/library/src/main/java/com/orientsec/jsbridge/Logger.kt index 21fec58..9bf343e 100644 --- a/library/src/main/java/com/orientsec/jsbridge/Logger.kt +++ b/android/library/src/main/java/com/orientsec/jsbridge/Logger.kt @@ -1,7 +1,6 @@ package com.orientsec.jsbridge import android.util.Log -import com.orientsec.jsbridge.JsBridge.Companion.debug interface Loggable { fun debug(message: String) @@ -11,32 +10,45 @@ interface Loggable { fun warn(message: String) fun error(message: String) + + fun error(message: String, throwable: Throwable) } +internal val BridgeLogger = Logger("JsBridge") + class Logger(private val tag: String) : Loggable { + private fun String.fillUp(): String { + return "$this [Thread: ${Thread.currentThread().name}]" + } override fun debug(message: String) { if (debug) { - Log.d(tag, message) + Log.d(tag, message.fillUp()) } } override fun info(message: String) { if (debug) { - Log.i(tag, message) + Log.i(tag, message.fillUp()) } } override fun warn(message: String) { if (debug) { - Log.w(tag, message) + Log.w(tag, message.fillUp()) } } override fun error(message: String) { if (debug) { - Log.e(tag, message) + Log.e(tag, message.fillUp()) + } + } + + override fun error(message: String, throwable: Throwable) { + if (debug) { + Log.e(tag, message.fillUp(), throwable) } } } \ No newline at end of file diff --git a/android/library/src/main/java/com/orientsec/jsbridge/PageListener.kt b/android/library/src/main/java/com/orientsec/jsbridge/PageListener.kt new file mode 100644 index 0000000..0d11a63 --- /dev/null +++ b/android/library/src/main/java/com/orientsec/jsbridge/PageListener.kt @@ -0,0 +1,16 @@ +package com.orientsec.jsbridge + +/** + * 页面加载回调。 + */ +interface PageListener { + /** + * 开始加载。 + */ + fun onLoadStart() + + /** + * 页面加载回调。 + */ + fun onLoadFinished() +} \ No newline at end of file diff --git a/android/library/src/main/java/com/orientsec/jsbridge/Utils.kt b/android/library/src/main/java/com/orientsec/jsbridge/Utils.kt new file mode 100644 index 0000000..c03109c --- /dev/null +++ b/android/library/src/main/java/com/orientsec/jsbridge/Utils.kt @@ -0,0 +1,14 @@ +package com.orientsec.jsbridge + +import android.content.Context +import android.content.res.Configuration +import android.os.Build +import android.webkit.WebView +import kotlin.system.measureTimeMillis + +internal fun Context.fixedContext(): Context { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { + return createConfigurationContext(Configuration()) + } + return this +} \ No newline at end of file diff --git a/peoject.properties b/android/peoject.properties similarity index 96% rename from peoject.properties rename to android/peoject.properties index 0fc8e2e..55ad5e3 100644 --- a/peoject.properties +++ b/android/peoject.properties @@ -1,6 +1,6 @@ # 库的简单描述 PROJ_DESCRIPTION=JSBridge -PROJ_VERSION=1.0.2 +PROJ_VERSION=2.0.0 PROJ_GROUP=com.orientsec PROJ_ARTIFACT=jsbridge # 组织 diff --git a/settings.gradle b/android/settings.gradle similarity index 100% rename from settings.gradle rename to android/settings.gradle diff --git a/example/src/main/assets/demo.html b/example/src/main/assets/demo.html deleted file mode 100644 index 64264c0..0000000 --- a/example/src/main/assets/demo.html +++ /dev/null @@ -1,110 +0,0 @@ -<html> - <head> - <meta content="text/html; charset=utf-8" http-equiv="content-type"> - <title> - js调用java - </title> - </head> - - <body> - <p> - <xmp id="show"> - -

-

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

- - - - \ No newline at end of file diff --git a/example/src/main/java/com/orientsec/jsbridge/example/MainActivity.kt b/example/src/main/java/com/orientsec/jsbridge/example/MainActivity.kt deleted file mode 100644 index 40914bc..0000000 --- a/example/src/main/java/com/orientsec/jsbridge/example/MainActivity.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.orientsec.jsbridge.example - -import android.annotation.SuppressLint -import android.app.Activity -import android.os.Bundle -import android.util.Log -import android.view.View -import android.widget.Button -import android.widget.Toast -import com.google.gson.Gson -import com.orientsec.jsbridge.BridgeHandler -import com.orientsec.jsbridge.BridgeWebView -import com.orientsec.jsbridge.JsBridge - -class MainActivity : Activity(), View.OnClickListener { - private lateinit var webView: BridgeWebView - - data class Location( - val address: String - ) - - data class User( - val name: String, - val location: Location, - val testStr: String - ) - - @SuppressLint("SetJavaScriptEnabled") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - JsBridge.debug = true - webView = findViewById(R.id.webView) - val settings = webView.settings - settings.javaScriptEnabled = true - val button = findViewById