package triaina.webview; import android.content.Context; import android.graphics.Bitmap; import android.net.Uri; import android.net.http.SslError; import android.os.Build; import android.os.Handler; import android.os.Message; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; import android.view.ViewGroup; import android.view.ViewParent; import android.webkit.HttpAuthHandler; import android.webkit.SslErrorHandler; import android.webkit.WebView; import android.webkit.WebViewClient; import org.json.JSONObject; import java.net.URLEncoder; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import triaina.commons.exception.SecurityRuntimeException; import triaina.commons.json.JSONConverter; import triaina.commons.utils.JSONObjectUtils; import triaina.commons.utils.UriUtils; import triaina.webview.config.BridgeObjectConfig; import triaina.webview.config.DomainConfig; import triaina.webview.entity.Error; import triaina.webview.entity.Params; import triaina.webview.entity.Result; import triaina.webview.exception.SkipDomainCheckRuntimeException; public class WebViewBridge extends WebView { public static final float VERSION = 1.2F; public static final double COMPATIBLE_VERSION = Math.floor(VERSION); private static final String TAG = WebViewBridge.class.getSimpleName(); private static final String JAVASCRIPT_INTERFACE_NAME = "DeviceBridge"; private Handler mHandler; private DomainConfig mDomainConfig; public static interface SecurityRuntimeExceptionResolver { public void resolve(SecurityRuntimeException e); } private SecurityRuntimeExceptionResolver mSecurityRuntimeExceptionResolver; private DeviceBridgeProxy mDeviceBridgeProxy; private WebViewBridgeHelper mHelper = new WebViewBridgeHelper(); private AtomicInteger mSeq = new AtomicInteger(); private Map<String, Callback<?>> callbacks = new ConcurrentHashMap<String, Callback<?>>(); private boolean mIsDestroyed; private boolean mNoPause; public WebViewBridge(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public WebViewBridge(Context context, AttributeSet attrs) { super(context, attrs); } public WebViewBridge(Context context) { super(context); } public void setWebViewClient(WebViewClient client) { super.setWebViewClient(new WebViewClientProxy(client, mDomainConfig, mSecurityRuntimeExceptionResolver)); } public void setSecurityRuntimeExceptionResolver(SecurityRuntimeExceptionResolver resolver){ mSecurityRuntimeExceptionResolver = resolver; } public void setDomainConfig(DomainConfig domainConfig) { mDomainConfig = domainConfig; } public DomainConfig getDomainConfig() { return mDomainConfig; } public void addBridgeObjectConfig(Object bridgeObject, BridgeObjectConfig config) { addBridgeObjectConfig(bridgeObject, config, new Handler(getContext().getMainLooper())); } public void addBridgeObjectConfig(Object bridgeObject, BridgeObjectConfig config, Handler handler) { mHandler = handler; if (mDeviceBridgeProxy == null) { mDeviceBridgeProxy = new DeviceBridgeProxy(this, mHandler); addJavascriptInterface(mDeviceBridgeProxy, JAVASCRIPT_INTERFACE_NAME); } mDeviceBridgeProxy.addBridgeObjectConfig(bridgeObject, config); } public void setNoPause(boolean noPause) { mNoPause = noPause; } public void onPause() { if (!mNoPause) super.onPause(); } /** * for unit test * * @return */ public DeviceBridgeProxy getDeviceBridgeProxy() { return mDeviceBridgeProxy; } public BridgeObjectConfig getBridgeConfigSet() { return mDeviceBridgeProxy.getBridgeConfigSet(); } public String call(String dest, Params params) { if (mIsDestroyed) return null; return notifyToWebInternal(null, dest, "params", params); } public String call(String dest, Params params, Callback<?> callback) { if (mIsDestroyed) return null; String id = mSeq.incrementAndGet() + ""; String js = notifyToWebInternal(id, dest, "params", params); if (js != null) callbacks.put(id, callback); return js; } public Callback<?> getCallback(String id) { return callbacks.get(id); } public void removeCallback(String id) { callbacks.remove(id); } public String returnToWeb(String id, String dest, Result result) { if (mIsDestroyed || TextUtils.isEmpty(id)) return null; return notifyToWebInternal(id, dest, "result", result); } public String returnToWeb(String id, String dest, Error error) { if (mIsDestroyed || TextUtils.isEmpty(id)) { return null; } return notifyToWebInternal(id, dest, "error", error); } private String notifyToWebInternal(String id, String dest, String container, Object data) { try { JSONObject json = new JSONObject(); JSONObjectUtils.put(json, "bridge", VERSION + ""); JSONObjectUtils.put(json, "id", id); if (dest != null) JSONObjectUtils.put(json, "dest", dest); JSONObject jsonData = JSONConverter.toJSON(data); JSONObjectUtils.put(json, container, jsonData); String s = json.toString(); Log.d(TAG, "Notify to Web with " + s); if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){ s = s.replace("\\", "\\\\"); // for Kitkat(19) inner browser(Chrome) } String js = mHelper.makeJavaScript("WebBridge.notifyToWeb", URLEncoder.encode(s, "UTF-8").replace("+", "%20")); loadUrl(js); return js;// for test } catch (Exception exp) { Log.e(TAG, exp.getMessage() + "", exp); } return null; } public void resume() { mDeviceBridgeProxy.resume(); } public void pause() { mDeviceBridgeProxy.pause(); } @Override public void destroy() { if (mIsDestroyed) return; // workaround for some device try { detachFromParent(); mDeviceBridgeProxy.destroy(); super.destroy(); } catch (Exception exp) { Log.w(TAG, exp.getMessage() + "", exp); } finally { mIsDestroyed = true; } } private void detachFromParent() { ViewParent parent = this.getParent(); if (parent != null && parent instanceof ViewGroup) { //sanity check ((ViewGroup) parent).removeAllViews(); } } public static class WebViewClientProxy extends WebViewClient { private WebViewClient mWebViewClient; private DomainConfig mDomainConfig; private SecurityRuntimeExceptionResolver mSecurityRuntimeExceptionResolver; public WebViewClientProxy(WebViewClient webViewClient, DomainConfig domainConfig) { this(webViewClient, domainConfig, null); } public WebViewClientProxy(WebViewClient webViewClient, DomainConfig domainConfig, SecurityRuntimeExceptionResolver resolver) { mWebViewClient = webViewClient; mDomainConfig = domainConfig; mSecurityRuntimeExceptionResolver = resolver; } public boolean shouldOverrideUrlLoading(WebView view, String url) { return mWebViewClient.shouldOverrideUrlLoading(view, url); } public void onPageStarted(WebView view, String url, Bitmap favicon) { try { // XXX ugly mWebViewClient.onPageStarted(view, url, favicon); } catch (SkipDomainCheckRuntimeException exp) { return; } Uri uri = Uri.parse(url); String[] domains = mDomainConfig.getDomains(); for (String domain : domains) { if (UriUtils.compareDomain(uri, domain)) return; } SecurityRuntimeException exception = new SecurityRuntimeException("cannot load " + url, url); if(mSecurityRuntimeExceptionResolver != null){ mSecurityRuntimeExceptionResolver.resolve(exception); } else { throw exception; } } public void onPageFinished(WebView view, String url) { mWebViewClient.onPageFinished(view, url); } public void onLoadResource(WebView view, String url) { mWebViewClient.onLoadResource(view, url); } @SuppressWarnings("deprecation") public void onTooManyRedirects(WebView view, Message cancelMsg, Message continueMsg) { mWebViewClient.onTooManyRedirects(view, cancelMsg, continueMsg); } public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { mWebViewClient.onReceivedError(view, errorCode, description, failingUrl); } public boolean equals(Object o) { return mWebViewClient.equals(o); } public void onFormResubmission(WebView view, Message dontResend, Message resend) { mWebViewClient.onFormResubmission(view, dontResend, resend); } public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { mWebViewClient.doUpdateVisitedHistory(view, url, isReload); } public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { mWebViewClient.onReceivedSslError(view, handler, error); } public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm) { mWebViewClient.onReceivedHttpAuthRequest(view, handler, host, realm); } public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { return mWebViewClient.shouldOverrideKeyEvent(view, event); } public int hashCode() { return mWebViewClient.hashCode(); } public void onUnhandledKeyEvent(WebView view, KeyEvent event) { mWebViewClient.onUnhandledKeyEvent(view, event); } public void onScaleChanged(WebView view, float oldScale, float newScale) { mWebViewClient.onScaleChanged(view, oldScale, newScale); } public String toString() { return mWebViewClient.toString(); } } }