I am using a WebView to execute some JS code in my app. This works fine so far on my KitKat (targetSdk) device, and also on a 2.2 (minSdk) emulator.
On a 4.3 Nexus 7 I encountered an error though. It seems as the JS object doesn't get injected.
Uncaught ReferenceError: android is not defined at null:1
Following a stripped down version of my code:
public void getData(Context ctx) {
WebView webView = new WebView(ctx);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webView.addJavascriptInterface(new AndroidInterface(), "android");
webView.loadUrl("javascript:console.log(android.getInput());");
}
public class AndroidInterface {
#JavascriptInterface
public String getInput() {
return "FOO";
}
}
The JS itself gets executed fine, so doing console.log('FOO'); prints FOO just as expected. But as soon as I try to access the interface I get the ReferenceError. Any ideas?
Thanks in advance!
Could be a number of things. You can try the following to narrow it down:
insert a sleep (1sec is more than enough) between the add and the loadUrl. It's unlikely that the API is racey but it's easy to check.
load a page before calling loadUrl("javascript:..."). Call something like loadUrl("about:blank") or loadData("<p>foo</p>", ...); and sleep for a bit.
Calling loadUrl("javascript:...") internally evaluates the JavaScript in the current page, it doesn't perform a 'real' navigation. It might be that the interface that you're adding is not being picked up by the context in which your loadUrl("javascript:...") call is executed (especially that the 'starting' page the WebView loads after being constructed is a bit special) and that performing a navigation makes the WebView "pick up" the newly added interface.
Found a workaround, would still be interesting to know why it doesn't work on 4.3.
So instead of using loadUrl, I surrounded the JS by a piece of HTML and used loadData.
webView.loadData("<!DOCTYPE html><html><head><script type=\"text/javascript\">function go(){console.log(android.getInput());}</script></head><body onload=\"javascript:go()\"></body></html", "text/html", "UTF-8");
Related
I am currently trying to port an existing Android application that we have to an IOS app. The issue here is that a large portion of our Android app was made using a lot of webviews with custom Javascript code called from Android.
If anyone is not familiar with it, the code for android would go something like this:
mWebView.addJavascriptInterface(WebInterface(this), "Android");
// more settings or whatever
mWebView.loadUrl("yourUrl.html.whatever");
Then the interface to the Javascript code reads like this:
public class WebInterface {
Context mContext;
public WebInterface(Context c) { this.mContext = c;}
#JavascriptInterface
public String returnMessage() {
Toast.makeText(mContext, "This is being called from the interface", Toast.LENGTH_SHORT).show();
return "This is being called from the interface";
}
}
At this point any time we want to access the code for the returnMessage() method inside of the webview we simply call:
var getContentsFromIntefrace = Android.returnMessage()
// getcontentsFromInterface is now "This is being called from the interface"
These targeted calls(Android.returnMessage()) allow me to call the interface code only when I need to do so, in contrast, if I was to use Android API´s evaluate javascript methods I would have to watch for the code executing all over the place e.g if my evaluatejavascript Android API method sets a variable to something it will do so in any other part of the code where said variable exist(please do correct me if I am wrong, it seems fuzzy at first but it is an issue thus far)
===== The issue I am having with IOS
The above is merely an explanation of what I need to do, to show what I am attempting on IOS I will demonstrate the code:
In IOS if I want to se functions that call on swift code inside my controller I have to set it in JS as:
function testButton() {
webkit.messageHandlers.callbackHandlerTestSend.postMessage({action: "testButtonAction", data: {name: "E.A.P"}});
}
The above sends a named message('callbackHandlerTestSend') with the key value pairs of action and data which can be parsed on the appropriate swift delegate as:
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if (message.name == "callbackHandlerTestSend") {
guard let body = message.body as? [String:Any] else {
print("Unable to do connection")
return
}
for(key, value) in body {
print("\(key) : \(value)")
if(key == "action"){
if(value as! String == "testButtonAction") {
intCounter += 1
print("The testButtonAction message action has been called")
webView.evaluateJavaScript("receiverP.innerHTML = 'It works \(intCounter)'", completionHandler: nil)
webView.evaluateJavaScript("receiverFunction('Message test')", completionHandler: nil)
// FIXME: evaluate JS might call on any other instance of receiverP, this is an issue
}
}
}
}// body of if
}// end of delegate
This is currently the only way in which I have been able to send Swift code back to the webview, by injecting the Javascript code with webView.evaluateJavascript()
My problem is that I need to capture everything with the whole:
webkit.messageHandlers.callbackHandlerTestSend.postMessage({action: "testButtonAction", data: {name: "E.A.P"}});
And it becomes bothersome to continue to do it as well as to have to individually parse each call for the proper parameters to be set while at the same time trying to not inject javascript code without messing something else up. So calling javascript and sending javascript code seems extremely cumbersome at this point and I would like to know if there is something I could do for the code to look more like the Android counterpart.
Any advice on what cool external libs out there I could use or in how could I modify the code to this would be greatly appreciated, I have been trying to find something in the IOS docs but have not found anything that I could use.
-- Edit:
At this time I am seriously considering an alternative such as React Native since it seems as the fastest route to develop an application that requires heavy usage of web like content as well as having limited knowledge of IOS in contrast to Android. I would still want to know if there is a viable solution to the above.
When ever I make a css class change, the changes don't always appear. This appears to happen when I have a touch event that adds something like a down class name to a button. The button doesn't update, and neither does anything else on the page. It's very inconstant in when it works. I've also noticed that sometimes my elements appear white with no content or anything. This is very frustrating!
Note:
There's a better solution as of Android 4.4+. It's a drop-in WebView replacement called CrossWalk. It uses the latest Chromium-kit and it's fantastic. You can read up about it here: crosswalk-project.org
Also, it appears that since Android 4.4, the invalidate() solution is no longer necessary and you can get away with using some of the other safer answer. I would only use this invalidate() approach as a last-ditch effort.
I'm answering my own question to hopefully help people out with the same issues as me.
I've tried several methods to making things better, even the all notorious -webkit-transform: translate3d(0,0,0); Even that didn't work all too well.
Let me share with you what did work.
First, use the most recent API. I'm using API 15. In your AndroidManifest.xml, make sure to enable hardware acceleration. If your version of API does not support this, then move on to the next bit.
If your version does support it, you can enable it by modifying your manifest:
<application
...
android:hardwareAccelerated="true">
Also, make sure that your manifest has the minimum supported API to the one that you are using. Since I'm using API 15, this is what my manifest has:
<uses-sdk
android:minSdkVersion="15"
android:targetSdkVersion="15" />
(Update: you should now modify that values in your build.gradle)
Now, in your primary CSS file for what will be presented in a WebView, add something like this:
body div {
-webkit-transform: translate3d(0,0,0);
}
Add onto the body div bit with any other element types you have; you can probably exclude images, ul, li, etc. The reason for applying this CSS style to everything is just by trial and error, and I found that brute-applying it to everything appears to work the best. With a larger DOM tree, you may need to be more-specific. I'm not sure what the specification would be, however.
When you instantiate your WebView, there are some settings you'll want to set:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
super.loadUrl("file:///android_asset/www/index.html");
appView.getSettings().setRenderPriority(RenderPriority.HIGH);
appView.getSettings()
.setPluginState(WebSettings.PluginState.ON_DEMAND);
}
Second to last, but crucial bit: I was reading through the source code for the WebView class and found this little tiny comment about force redrawing. There is a static final boolean, that when set to true will force the view to always redraw. I'm not huge on Java syntax, but I don't think you can directly change a static final attribute of a class. So what I ended up doing was I extended the class like so:
import org.apache.cordova.CordovaWebView;
import android.content.Context;
import android.graphics.Canvas;
public class MyWebView extends CordovaWebView {
public static final String TAG = "MyWebView";
public MyWebView(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Warning! This will cause the WebView to continuously be redrawn
// and will drain the devices battery while the view is displayed!
invalidate();
}
}
Keep in mind, I'm using Cordova/PhoneGap, so I had to extend from the CordovaWebView. If you see in the onDraw method, there is a call to invalidate. This will cause the view to redraw constantly. I highly recommend adding in logic to only redraw when you need it, however.
There is one final step, if you are using Cordova. You have to tell PhoneGap to use your new WebView class instead of their own WebView class. In your MainActivity class, add this:
public void init(){
CordovaWebView webView = new MyWebView(MainActivity.this);
super.init(webView, new CordovaWebViewClient(this, webView), new CordovaChromeClient(this, webView));
}
That's it! Try and run your app and see if everything is much smoother. Before doing all of these changes, the pages would appear white, no CSS changes would be applied until after tapping on the screen again, animations were super choppy or not even noticeable. I should mention that the animations are still choppy, but far much less choppy than before.
If anyone has anything to add to this, just comment under. I'm always open for optimizations; and I'm fully aware there may be better ways to do what I have just recommended.
If my above solution did not work for you, could you describe your specific situation and what results you are seeing with Androids WebView?
Lastly, I have enabled this answer as a "community wiki", so anyone and everyone, feel free to make adjustments.
Thanks!
Edit:
With the most latest PhoneGap, you'll need to have your init() method look more like this:
public void init(){
CordovaWebView webView = new MyWebView(MainActivity.this);
super.init(webView, new IceCreamCordovaWebViewClient(this, webView), new CordovaChromeClient(this, webView));
}
I implemented kyle's solution and it solved the problem. Howewer I noticed a huge battery drain on android 4.0.4 when the app was open. Also after the change I had users complaining that the swiftKey keyboard was not working with my app anymore.
Every change in my app are triggered by a user action so I came up with a modified version that only trigger invalidate() after a touchEvent:
Handler handler = new Handler();
public boolean onTouchEvent (MotionEvent event){
super.onTouchEvent(event);
handler.postDelayed(triggerInvalidate, 60);
handler.postDelayed(triggerInvalidate, 300);
return true;
}
private Runnable triggerInvalidate=new Runnable(){
public void run(){
invalidate();
}
};
Never did any programming with Java so there might be a better solution to do this.
re: the redraw problem, you can force a redraw by reading a property from the element
so say you do this:
$('#myElement').addClass('foo'); // youre not seeing this take effect
if you do this afterwards:
$('#myElement').width();
it will force a redraw.
This way you can be selective instead of redrawing the whole page all the time, which is expensive
For me this issue was only happening on Samsung devices. I was able to fix it by disabling Hardware Acceleration for WebViews:
webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
Hope it helps.
As pointed out above and elsewhere - overriding View.onDraw() and calling View.invalidate will make for an unhappy battery / app performance will drop. You can also do a manual invalidate call ever x ms like so
/**
* Due to bug in 4.2.2 sometimes the webView will not draw its contents after the data has loaded.
* Triggers redraw. Does not work in webView's onPageFinished callback for some reason
*/
private void forceWebViewRedraw()
{
mWebView.post(new Runnable() {
#Override
public void run()
{
mWebView.invalidate();
if(!isFinishing())
mWebView.postDelayed(this, 1000);
}
});
}
I tried putting an invalidate call in WebViewClient.onPageLoaded() but this does not seem to work. While my solution could be better its simple and it works for me (im just showing a twitter login)
Please also see my answer at WebView fails to render until touched Android 4.2.2 , The idea is to export #Olivier's function to JavaScript so you can trigger invalidates from JS in sensible parts ... My god, yet another more hack !! :)
I had this problem because onPageStared and onPageFinished I show and hide a loading animation.
Since was injecting JS do my menus and stuff from the original website and noticed that injecting JS on onPageFinished force the webview to draw contents. Here is a example of what you can add at the end of onPageFinished
view.loadUrl("javascript:(function() { var select = document.getElementsByClassName('something')[0]\r\n" +
" if(select)" +
" select.style.display = 'none';})()");
Just clear the cache of webView in oncreate() and it works for me.
webView.clearCache(true);
You should enable hardware by modifying your manifest:
<application
...
android:hardwareAccelerated="true">
And in your activity onResume()
webView.postDelayed(() -> {
if (!isFinishing() && webView != null) {
webView.setLayerType(View.LAYER_TYPE_NONE, null);
}
}, 1000);
And in your onPause()
webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
I have an application working on the following envirnoment:
Monotouch 2.1
MonoDevelop 2.4.2
MacOS 10.6.8
iOS SDK 4.3
The same application, migrated to iOS 5.0/5.1 and Monotouch 5.2.10 with MonoDevelop 2.8.8.4 under, has the following problem: When i click a button to navigate in a UIWebView do not work.
Code is that:(obviously it's same in Monotouch 2.1)
public void ScrollToTop (object sender, EventArgs evt)
{
webView.EvaluateJavascript("window.scrollTo(0,0)");
}
What could i do?
SOLVED (with Jonathan.Peppers help):
public void ScrollToTop (object sender, EventArgs evt)
{
if((UIDevice.CurrentDevice.CheckVersion(5, 0)){
System.Drawing.PointF p = new System.Drawing.PointF(0, 0);
webView.ScrollView.SetContentOffset(p,true);
}
else{
webView.EvaluateJavascript("window.scrollTo(0,0)");
}
}
i have made in that way because on 4.3 webView.ScrollView.SetContentOffset make application crash.
Try this:
string error = webView.EvaluateJavascript("try { window.scrollTo(0,0); } catch (e) { return e.message; }");
(double check my code)
It should get you a better error message. Tell us the value of the string returned.
I suspect the issue isn't MonoTouch, but perhaps iOS5? Safari had drastic changes in iOS5, new javascript engine, etc.
Note that UIWebView is not re-entrant. If the way of loading the (or just the timing of) page changes between iOS release then this may become a problem.
To avoid this just make sure you're not calling back into any UIWebView instance until the loading is completed, e.g. LoadFinished or LoadError is called.
If this does not help then please create a small, self-contained, test case that duplicate the issue and attach it to a bug report at http://bugzilla.xamarin.com
Since the way you call javascript on a WebView is through loadUrl("javascript: ... "); The keyboard cannot stay open.
The loadUrl() method calls loadUrlImpl() , which calls a method called clearHelpers() which then calls clearTextEntry(), which then calls hideSoftKeyboard() and then we become oh so lonely as the keyboard goes away.
As far as I can see all of those are private and cannot be overridden.
Has anyone found a workaround for this? Is there a way to force the keyboard to stay open or to call the javascript directly without going through loadUrl()?
Is there anyway to override the WebView in a way to prevent (the private method) clearTextEntry() from being called?
Update
KitKat added a public method for invoking javascript directly: evaluateJavascript()
For older apis, you could try a solution like below, but if I had to do this again I'd look at just building an compatibility method that on KitKat uses the above method and on older devices, uses reflection to drill down to a inner private method: BrowserFrame.stringByEvaluatingJavaScriptFromString()
Then you could call javascript directly without having to deal with loadUrl and adding "javascript: " to the script.
Old Answer
As requested by Alok Kulkarni, I'll give a rough overview of a possible workaround I thought of for this. I haven't actually tried it but in theory it should work. This code is going to be rough and is just to serve as an example.
Instead of sending the calls down through loadUrl(), you queue your javascript calls and then have javascript pull them down. Some thing like:
private final Object LOCK = new Object();
private StringBuilder mPendingJS;
public void execJS(String js) {
synchronized(LOCK) {
if (mPendingJS == null) {
mPendingJS = new StringBuilder();
mPendingJS.append("javascript: ");
}
mPendingJS
.append(js)
.append("; ");
}
}
Instead of calling loadUrl() call that method. (For making this simple I used a synchronized block, but this might be better suited to a different route. Since javascript runs on its own thread, this will need to be thread safe in some way or another).
Then your WebView would have an interface like this:
public class JSInterface {
public String getPendingJS() {
synchronized(LOCK) {
String pendingCommands = mPendingJS.toString();
mPendingJS.setLength(0);
mPendingJS.append("javascript: ");
return pendingCommands;
}
}
}
That returns a String with the pending commands and clears them so they don't get returned again.
You would add it to the WebView like this:
mWebView.addJavascriptInterface(new JSInterface(), "JSInterface");
Then in your javascript you would set some interval in which to flush the pending commands. On each interval it would call JSInterface.getPendingJS() which would return a String of all of the pending commands and then you could execute them.
You could further improve this by adding a check in the execJS method to see if a EditText field exists in the WebView and is in focus. If there is one, then you would use this queueing method, but if there wasn't one in focus then you could just call loadUrl() like normal. That way it only uses this workaround when it actually needs to.
Regarding older APIs (pre 19), I used a similar method to the excepted answer, but slightly different.
First, I keep track of if the keyboard is displayed by using javascript in the webview roughly like so:
document.addEventListener( "focus", function(e){
var el = e.target;
reportKeyboardDisplayedToJava( isInputElement( el ) );
}, true);
document.addEventListener( "blur", function(e){
reportKeyboardDisplayedToJava( false );
}, true);
If the keyboard is displayed, and a js injection is attempted by the Android Java layer – I “defer” that injection. I add it to a string list, allow the user to finish up their input, and then upon the keyboard disappearing, I detect that and execute the backlog of injections.
I could implement cottonBallPaws's idea to use the internals of WebView with reflection, and got it to work for my 4.2 device. There are gracious fallbacks for Android versions older than KitKat.
The code is written in Xamarin, but it should be easily adaptable to native Java code.
/// <summary>
/// Executes a JavaScript on an Android WebView. This method offers fallbacks for older
/// Android versions, to avoid closing of the soft keyboard when executing JavaScript.
/// </summary>
/// <param name="webView">The WebView to run the JavaScript.</param>
/// <param name="script">The JavaScript code.</param>
private static void ExecuteJavaScript(Android.Webkit.WebView webView, string script)
{
if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Kitkat)
{
// Best way for Android level 19 and above
webView.EvaluateJavascript(script, null);
}
else
{
try
{
// Try to do with reflection
CompatExecuteJavaScript(webView, script);
}
catch (Exception)
{
// Fallback to old way, which closes any open soft keyboard
webView.LoadUrl("javascript:" + script);
}
}
}
private static void CompatExecuteJavaScript(Android.Webkit.WebView androidWebView, string script)
{
Java.Lang.Class webViewClass = Java.Lang.Class.FromType(typeof(Android.Webkit.WebView));
Java.Lang.Reflect.Field providerField = webViewClass.GetDeclaredField("mProvider");
providerField.Accessible = true;
Java.Lang.Object webViewProvider = providerField.Get(androidWebView);
Java.Lang.Reflect.Field webViewCoreField = webViewProvider.Class.GetDeclaredField("mWebViewCore");
webViewCoreField.Accessible = true;
Java.Lang.Object mWebViewCore = webViewCoreField.Get(webViewProvider);
Java.Lang.Reflect.Method sendMessageMethod = mWebViewCore.Class.GetDeclaredMethod(
"sendMessage", Java.Lang.Class.FromType(typeof(Message)));
sendMessageMethod.Accessible = true;
Java.Lang.String javaScript = new Java.Lang.String(script);
Message javaScriptCodeMsg = Message.Obtain(null, 194, javaScript);
sendMessageMethod.Invoke(mWebViewCore, javaScriptCodeMsg);
}
I want to create a plugin for phone which pass and returns the value between javascript and android.
Can anybody suggest any ideas on how to do this?
Actually, this is not very difficult. Here, I will show you how to call native code from javascript within the page and vice-versa:
Calling native code from within web view:
When creating the web view add javascript interface (basically java class whose methods will be exposed to be called via javascript in the web view.
JavaScriptInterface jsInterface = new JavaScriptInterface(this);
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(jsInterface, "JSInterface");
The definition of the javascript interface class itself (this is exemplary class I took from another answer of mine and opens video in native intent)
public class JavaScriptInterface {
private Activity activity;
public JavaScriptInterface(Activity activiy) {
this.activity = activiy;
}
public void startVideo(String videoAddress){
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(videoAddress), "video/3gpp"); // The Mime type can actually be determined from the file
activity.startActivity(intent);
}
}
Now if you want to call this code form the HTML of the page you provide the following method:
<script>
function playVideo(video){
window.JSInterface.startVideo(video);
}
</script>
Easy isn't it?
Calling javascript code from native code:
This is also simple suppose in the code of the HTML loaded in WebView you have javascript function defined:
<script>
function function(){
//... do something
}
</script>
Then you call this function through the WebView in the native code like that:
webView.loadUrl("javascript:function()");
Here's a tutorial for creating a PhoneGap Plugin. Also the instructions for the ChildBrowser plugin are especially good.