Skip to content

Instantly share code, notes, and snippets.

@pabloko
Created December 8, 2018 13:20
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pabloko/cda14ee68f2031612af19612174dd974 to your computer and use it in GitHub Desktop.
Save pabloko/cda14ee68f2031612af19612174dd974 to your computer and use it in GitHub Desktop.
Lets have any JSON serializable type and methods to callback on Android WebView's JSInterface
//How it works: all the script relies on a tiny javascript stub injected to webView's document that uses Proxy api and json serialicing stuff. Calls to our internal objects are proxified to a single method "__mm_handle_method", that using reflection find the target method and populate arguments exchanged in json, plus a custom interface for methods passed as argument.
package es.pabloko.webviewbridge;
import android.app.Activity;
import android.content.Context;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class MainActivity extends AppCompatActivity {
private String JSINTERFACE="Android";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setContentView(R.layout.activity_main);
//WebView webView = findViewById(R.id.web);
WebView webView = new WebView(this);
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new JSInterface(this, webView, JSINTERFACE), JSINTERFACE+"_stub");
String html = "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><script>"
+ "var __mm_cb={};function __mm_guid(){function _(){return Math.floor(65536*(1+Math.random())).toString(16).substring(1)}return\"__mm_method_\"+_()+_()+\"-\"+_()+\"-\"+_()+\"-\"+_()+\"-\"+_()+_()+_()}function __mm_callback(){var _=Array.from(arguments),n=_.shift();__mm_cb[n]&&__mm_cb[n].apply(null,_)}var __mm_method_handler="+JSINTERFACE+"_stub.__mm_method_handler.bind("+JSINTERFACE+"_stub),"+JSINTERFACE+"=new Proxy("+JSINTERFACE+"_stub,{get:function(_,r){return function(){for(var _=0;_<arguments.length;_++)if(\"[object Function]\"==={}.toString.call(arguments[_])){var n=__mm_guid();\"\"!=arguments[_].name&&(n=\"__mm_method_\"+arguments[_].name),__mm_cb[n]=arguments[_],arguments[_]=n}var m=__mm_method_handler(r,JSON.stringify(arguments));return JSON.parse(m).value}}});"
+ "</script></head><body>Android WebView JavascriptInterface Test</body></html>";
webView.loadData(html, "text/html", "UTF-8");
WebView.setWebContentsDebuggingEnabled(true);
setContentView(webView);
}
public class JSCallback {
private Context caller;
private WebView wb;
private String guid;
public JSCallback(Context caller, WebView wb, String guid) {
this.caller = caller;
this.wb=wb;
this.guid=guid;
}
public void call(Object... o) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("try{");
String separator = "";
if (guid.startsWith("__mm_method_")) {
stringBuilder.append("__mm_callback");
stringBuilder.append("(");
stringBuilder.append("'" + guid + "'");
separator = ",";
} else {
stringBuilder.append(guid);
stringBuilder.append("(");
}
for (Object param : o) {
stringBuilder.append(separator);
separator = ",";
if (param instanceof String) {
stringBuilder.append("'");
stringBuilder.append(((String) param).replace("'", "\'"));
stringBuilder.append("'");
} else
stringBuilder.append(param);
}
stringBuilder.append(")}catch(error){console.error(error.message);}");
final String call = stringBuilder.toString();
((Activity)caller).runOnUiThread(new Runnable() {
@Override
public void run() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
wb.evaluateJavascript(call, null);
} else {
wb.loadUrl("javascript:" + call);
}
}
});
}
}
public class JSInterface {
private Context caller;
private WebView wb;
private String jsinterfacename;
public JSInterface(Context caller, WebView wb, String jsc) {
this.wb = wb;
this.jsinterfacename = jsc;
this.caller = caller;
}
@JavascriptInterface
public String __mm_method_handler(String method, String data) {
Method[] methods = this.getClass().getMethods();
for (Method meth : methods) {
if (meth.getName().equals(method)) {
try {
JSONObject obj = new JSONObject(data);
Object[] params = new Object[obj.length()];
for (int uc=0; uc<obj.length();uc++) {
params[uc] = obj.get(uc + "");
if (params[uc].toString().startsWith("__mm_method_")) {
params[uc] = new JSCallback(caller, wb, params[uc].toString());
}
}
Object ret = meth.invoke(this, params);
JSONObject jsr = new JSONObject();
jsr.put("value", ret);
return jsr.toString();
} catch (JSONException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return "";
}
}
return "";
}
//PUBLIC METHODS HERE
public int kkk (String s, int s1, int s2, final JSCallback uc) {
Log.e("JSInterface",(s1+s2)+" = goof kkk "+s);
uc.call("AAAAAAAAAAAAAAAAAA");
final int[] ix = {1};
new Thread(new Runnable() {
public void run(){
while (ix[0] <20) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
uc.call("AAAAAAAAAAAAAAAAAA" + ix[0]);
ix[0]++;
}
}
}).start();
return 9;
}
//sample console output for this call:
/*
Android.kkk("ayy",1,2,function(a){console.log("bbbaaaaaaaaaaabbbbb"+a)})
9
(logcat) >>> E/JSInterface: 3 = goof kkk ayy
VM30:1 bbbaaaaaaaaaaabbbbbAAAAAAAAAAAAAAAAAA
VM30:1 bbbaaaaaaaaaaabbbbbAAAAAAAAAAAAAAAAAA1 (+1.000s)
VM30:1 bbbaaaaaaaaaaabbbbbAAAAAAAAAAAAAAAAAA2 (+1.000s)
VM30:1 bbbaaaaaaaaaaabbbbbAAAAAAAAAAAAAAAAAA3 (+1.000s)
...
*/
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment