Monday, August 19, 2013

Android - WebView - JavaScript - onPageFinish() and stuff in between

OK... so let start by saying I'm working with some very talented people...

I've been digging in Android sources for days searching for the cause for this, I've even offered a bounty for 100 points which no one claimed.

I've seen many similar issues such as this, across StackOverFlow...

The main issue is that the loading of the page is completed, and then the onPageFinished event is called with a seemly random delay that can range from 0.1 - 40 sec, only after the <GATE-M>DEV_ACTION_COMPLETED</GATE-M> is printed to the log.

Here is the code snippet:

webView = (WebView) view.findViewById(R.id.WebView);

webView.setWebViewClient(new WebViewClient() {

    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        logDebug("Loading URL: " + url);
        super.onPageStarted(view, url, favicon);
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        return WrappingClass.this.shouldOverrideUrlLoading(view, url);
    }

    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
        logInfo("Injecting JavaScript to webview.");
        webView.loadUrl("full-js-here");
    }

    @Override
    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
        logError("error code:" + errorCode);
        super.onReceivedError(view, errorCode, description, failingUrl);
    }
});

WebSettings webSettings = webView.getSettings();
webSettings.setSavePassword(false);
webSettings.setSaveFormData(false);
webSettings.setJavaScriptEnabled(true);
webView.requestFocus(View.FOCUS_DOWN);
webView.loadUrl("url");

So the solution is quite a hack but it is wonderful... Check it out:

Somewhere in your class declare the following:
final class ObjectExtension {

    @JavascriptInterface
public void onLoad() { logInfo("onLoadCompleted"); WrappingClass.this.onLoadCompleted(); } }
public void onLoadCompleted() {
    webView.loadUrl("full-js-here");
}


And before the URL loading add the following:
webView.addJavascriptInterface(new ObjectExtension(), "webviewScriptAPI");
String fulljs = "javascript:(\n    function() { \n";
fulljs += "        window.onload = function() {\n";
fulljs += "            webviewScriptAPI.onLoad();\n";
fulljs += "        };\n";
fulljs += "    })()\n";
webView.loadUrl(fulljs);
webView.loadUrl("url");

This registers a callback for the onLoad event of the WebView window, which is loaded long time before the onPageFinished is called, because of that Android WebView issue.

So the trick is that we inject the onLoad callback before loading the url, later (next line) we load the url, once the onLoad callback is called, in our onLoad implementation we call to the Java API, which in its turn inject the Javascript into the loaded page, and sometime long after that the onPageFinished is called.

End of story...
---- UPDATE ----

It has been a very long while since that post... I've wrote SocialApp(link at the top Left), which is entirely a WebView application, a multi WebView applications, which runs Javascripts on all of the WebViews and monitor the beginning and ends of scripts, and runs Javascript on a WebViews in the background....

What I'm trying to say is, if you have any questions, I'm pretty sure I can answer them, so ask away...


---- UPDATE ----

It has been a very long while since I posted that update, I really hoped to release it as an open source, and was struggling with it for a long time, but as it was pretty much forced on me due to the architecture I've been decided to append this "CyborgWebView" to Cyborg, which is a license based SDK, You can find it here.

Also, if the post is not clear enough, and you prefer a sample project, let me know...







37 comments:

  1. Not working. It doesn't go to the onLoad() callback. Am I missing something here?

    ReplyDelete
  2. It can be that the page overrides the callback... Post some code?

    ReplyDelete
  3. webView=(WebView)findViewById(R.id.mobile_sso_wv);
    webViewUrl= "https://example.google.com";

    WebSettings webSettings=webView.getSettings();
    webSettings.setJavaScriptEnabled(true);
    webView.setWebViewClient(new LoginWebViewClient());
    webView.setWebChromeClient(new SignInWebChromeClient());
    webView.addJavascriptInterface(new LoginJsInterface(), "webviewScriptAPI");
    String fulljs = "javascript:(\n (function() {\n";
    fulljs += " window.onload = function({\n";
    fulljs += " webviewScriptAPI.onLoad();\n";
    fulljs += " };\n";
    fulljs += " })()\n";
    webView.loadUrl(fulljs);
    webView.loadUrl(webViewUrl);

    public void onLoadCompleted()
    {
    webView.loadUrl("javascript:document.getElementById('submit_button').style.backgroundColor='#3b3b3b'");

    webView.loadUrl("javascript:document.getElementById('submit_button').style.width='100%'");

    }


    In onLoadcompleted I have changed the submit button color and width.

    ReplyDelete
  4. Where is the LoginJsInterface object?

    ReplyDelete
  5. This is my Javascript interface class

    class LoginJsInterface
    {
    @JavascriptInterface
    public void onLoad()
    {
    Log.d("OnLoadCompleted----------------------->","Yes");
    onLoadCompleted();
    }
    }

    ReplyDelete
    Replies
    1. Are you targeting a real url?
      Because you would only get onLoad once a page has been loaded, if there was an error, you need to catch it in the onError received in your LoginWebViewClient

      Delete
    2. Yes I am targeting a real url. I have given dummy url in the above code. I have implemented onReceivedError() too. But no errors in that callback.

      Delete
  6. Where is the LoginJsInterface object?

    ReplyDelete
  7. See the first part of the code. I have added javascript object through the addJavascriptInterface() method.

    webView.addJavascriptInterface(new LoginJsInterface(), "webviewScriptAPI");

    ReplyDelete
    Replies
    1. Perhaps the page itself overrides the window.onLoad...
      Did you try another URL?

      Delete
    2. Dear Adam,

      Yes I have tried with different URLs but the result is same :(

      Delete
    3. I have tried in jelly bean 4.1.2 and gingerbread 2.3. Could you please send your .apk file or your application? Hope it helps to track the issue.

      Delete
    4. Sorry, but I don't have a sample project...

      Could you try removing the ChromeClient, and stick to the example...

      webView.setWebViewClient(...)
      webSettings.setSavePassword(false);
      webSettings.setSaveFormData(false);
      webSettings.setJavaScriptEnabled(true);
      webView.requestFocus(View.FOCUS_DOWN);

      because this works on so many devices, the only thing can cause this not to work is that something overrides the onLoad listener...

      Delete
    5. Hi Adam,

      Yes tried, unfortunately the result is same. I did not handle shouldOverrideUrlLoading(WebView webView, String url) method. I have returned false in that method. I have also tried what you have given in your example but no luck.

      Delete
    6. WOW... I really don't know why this does not work for you... is this a project you could share with me so I can take a look?

      Delete
    7. Could you please share your mail id ?

      Delete
    8. I found something weird yesterday. It went to JavaScript callback only on android 2.2 if I change the JS code like this otherwise it doesn't went to that callback.

      String fulljs="javascript:window.webviewScriptAPI.onLoad();";
      webView.loadUrl(fulljs);

      But JS never executing. In Other versions of OS it never goes to Javascript callback.

      Delete
    9. We have tested this yesterday and today, and I can say with certainty that this works...

      From the example you've sent me, I've noticed that the the Javascript is not processed... I'll look into it again, but I don't have time to do this until sometime next week...(Work stuff)

      In the meanwhile, try injecting some obvious JS, like window.location.href='yyy' and see that the url actually changes, this way you can know for sure that your script is running!!!

      Also, I think there was an extra '(' in the window.onload setting script in the example you've sent.

      How about you'l add that project to bitbucket, or github, and we can work on it together, and leave future reference for people to find?

      Delete
    10. Now I am trying new workaround for this use case. I will upload my project on github soon.

      Delete
    11. I can also tell you for sure that with Yahoo.com for example, they override the window.onload method, and then we use the onPageFinish callback

      Delete
    12. Dinesh,
      Try replacing the javascript loading line and the actual url call...

      webView.loadUrl(webViewUrl);
      webView.loadUrl(fulljs);

      Delete
  8. Don't know if you'll see this, but i have this working. The only problem is, when i input a form and the page loads the following result (the same URL, different content) onLoad does not seem to be called?

    Thanks for any help..

    ReplyDelete
    Replies
    1. Hi Daniel,

      What I've done is that I've merged the two calls from onLoad, and onPageFinish, into one delegation function, onLoadCompleted, and made sure (with a boolean) that the action it performs would only be called once.

      So, if the onLoad is not called... is the onPageFinished called?

      Delete
  9. Thank you, very much, this is working for me. I hardly found tutorial with passing parametars to javascript in oncreate method. Thank you, again

    ReplyDelete
    Replies
    1. Hi Ana,

      You are welcome, let me know if there is anything else...

      Adam.

      Delete
  10. i have maid a browser using web view , my problem is when the user open a link if he go out of the app or the activity , when he open it again it will open from the default link , i would like it to be like Google browser when u ever go back it will open for u the last link u was in it

    for ex if any way like



    webView.loadUrl("http://www.google.com");
    webView.getNewUrl;

    Url=NewUrl

    ReplyDelete
    Replies
    1. I'm not sure what do you mean... if you want to save the last state of your WebView you will have to store and load it, in the saveState and loadState methods respectively, in the your activity or fragment, or store the state to a sharedpreferences object.

      Delete
  11. Still not sure why onPageFinish take soooo long sometimes..
    Anyway - nice tip! Thank you for the post.

    ReplyDelete
    Replies
    1. p.s.
      If you run multiple WV and switch Activities before onPageFinish is called, than WV thread might be pushed down and execution will be delayed.
      Sometimes, the trigger for executing the WV on the background is when another WV onPageFinish method is executed.

      FYI...

      Delete
    2. Thank you for your reply, This issue was spotted when using a single webview within a single activity. it was one flow that repeated several times and not in a very rapid rate, that is way it took me so long to figure out, that something was actually wrong. After this long time that I've developed an entire infrastructure to overcome so many issues with WebViews I can tell you for sure that it is a bug, and that the workaround is not perfect but works.

      I've also noticed that history update is only called once you've left the page you were visiting, and that if you watch the onProgress in one of the clients, it stalls after reaching 86% or something like that...

      This is really a poor job done, also webview rendering and bg javascript consumes SO MUCH battery it is ridiculous.
      Let me know if I can be of any assistance, and check out SocialApp, it is in small part based on this solution.

      Delete
  12. I just want to thank you for this great solution :)
    But just one comment. This code:

    final class ObjectExtension {

    public void onLoad() {
    logInfo("onLoadCompleted");
    WrappingClass.this.onLoadCompleted();
    }

    }

    should include the @JavascriptInterface and look like this:

    final class ObjectExtension {

    @JavascriptInterface
    public void onLoad() {
    logInfo("onLoadCompleted");
    WrappingClass.this.onLoadCompleted();
    }

    }

    ReplyDelete
  13. i dont know if am too late but am having this same issue....lag before the execution of my javascript codes into my webview, please can i get the complete code for this fix....maybe a .zip please, VERY URGENT please

    ReplyDelete
    Replies
    1. Hi,
      I know this is not the same as a full working example, but it is enough to to compose your own example...

      I already have it all working in my Cyborg project, and unfortunately I don't have time to migrate it into a full working stand alone example, as my implementation uses a custom webview to provide default Javascript to java apis like logs and clicks, and to overcome other android webview issues, like battery consumption among other things...

      If you have a working sample, I might be able to help you out.

      Delete