43

It has been asked many times before, I browsed through everything, no clear answers yet.

Question simplified: Is it possible to inject local Javascript file (from asset or storage) to remote webpage loaded in an Android Web-View? I know that it is possible to inject such files to local Webpages (Assets HTML) loaded in a Web-View.

Why do I need this to work? : To make browsing experience faster, by avoiding downloading of bigger files such as Js and CSS files every time. I want to avoid Web-View Caching.

sumit
  • 723
  • 1
  • 7
  • 16

4 Answers4

60

There is a way to 'force' the injection of your local Javascript files from local assets (e.g., assets/js/script.js), and to circumvent the 'Not allowed to load local resource : file:///android_assets/js/script.js ...' issue.

It is similar to what described in another thread (Android webview, loading javascript file in assets folder), with additional BASE64 encoding/decoding for representing your Javascript file as a printable string.

I am using an Android 4.4.2, API level 19 Virtual Device.

Here are some code snippets:

[assets/js/script.js]:

    'use strict';

    function test() {
       // ... do something
    }

    // more Javascript

[MainActivity.java]:

    ...

    WebView myWebView = (WebView) findViewById(R.id.webView);
    WebSettings webSettings = myWebView.getSettings();

    webSettings.setJavaScriptEnabled(true);
    webSettings.setAllowUniversalAccessFromFileURLs(true);
    myWebView.setWebViewClient(new WebViewClient() {
       @Override
       public boolean shouldOverrideUrlLoading(WebView view, String url) {
          return false;
       }

       @Override
       public void onPageFinished(WebView view, String url) {
          super.onPageFinished(view, url);

          injectScriptFile(view, "js/script.js"); // see below ...

          // test if the script was loaded
          view.loadUrl("javascript:setTimeout(test(), 500)");
       }

       private void injectScriptFile(WebView view, String scriptFile) {
          InputStream input;
          try {
             input = getAssets().open(scriptFile);
             byte[] buffer = new byte[input.available()];
             input.read(buffer);
             input.close();

             // String-ify the script byte-array using BASE64 encoding !!!
             String encoded = Base64.encodeToString(buffer, Base64.NO_WRAP);
             view.loadUrl("javascript:(function() {" +
                          "var parent = document.getElementsByTagName('head').item(0);" +
                          "var script = document.createElement('script');" +
                          "script.type = 'text/javascript';" +
             // Tell the browser to BASE64-decode the string into your script !!!
                          "script.innerHTML = window.atob('" + encoded + "');" +
                          "parent.appendChild(script)" +
                          "})()");
          } catch (IOException e) {
             // TODO Auto-generated catch block
             e.printStackTrace();
          }
       }
    });

    myWebView.loadUrl("http://www.example.com");

    ...
Community
  • 1
  • 1
Raffaele N.
  • 966
  • 6
  • 4
  • Thanks a lot, that worked. However it works only for android version upto 4.3. For Android 4.4, it says I have to run the loading thing (loadUrl) inside the UI thread only and when I do that using RunOnUIThread() it does nothing, I mean it loads nothing. Do you know what can be the problem? – sumit Feb 17 '14 at 10:13
  • @sumit In Android 4.4 I use `Runnable` objects as follows: first, I define a `Handler` in class `MainActivity ... { final Handler handler = new Handler(); ...`. Second, in my method I use a `Runnable` object, e.g. `void loadWebPage() { final Runnable r = new Runnable() { public void run() { WebView myWebView = ... } }; handler.post(r) }`. I hope it helps. – Raffaele N. Feb 18 '14 at 02:00
  • 6
    I was doing the same but it didnt work. The reason is, in Kitkat version it is not possible to inject js file locally using view.loadUrl(). I had to use evaluateJavaScript() method and it worked. To inject file CSS files one can use view.loadUrl(), but for js, evaluateJavaScript() has to be used. Welcome to ChromeView :) – sumit Feb 18 '14 at 06:09
  • 1
    This is an awesome answer! I only have one problem with it, it doesn't work with the asset you want to inject is in UTF-8 and outside of the ANSI character range. I was able to fix it by changing the decode to this: "script.innerHTML = decodeURIComponent(escape(window.atob('" + encoded + "')));" + – Jon Hargett Aug 27 '14 at 20:11
  • Genius genius!!! you saved my day bro! I have posted a question with 200 bounty. If you post answer under that I can accept your answer. Here is the link: https://stackoverflow.com/questions/51570906/how-to-add-javascript-function-in-webview-and-call-it-later-from-html – Zohab Ali Jul 30 '18 at 16:39
  • Worked for me in a modified way. I changed `setTimeout(test(), 500)` to `setTimeout(test, 0)`. First, you don't want to call the `test` function _immediately_, you want `setTimeout` to call it (though it works both ways!). Second, why wait 500ms; You can just queue the function call to run immediately when ready. – Eilon Jun 15 '20 at 21:57
  • This worked for me with some minor changes and with `setTimeout` too. Thanks! – user7637745 Jan 29 '22 at 21:50
11

loadUrl will work only in old version use evaluateJavascript

webview.evaluateJavascript("(function() { document.getElementsByName('username')[0].value='USERNAME';document.getElementsByName('password')[0].value='PASSWORD'; "+
"return { var1: \"variable1\", var2: \"variable2\" }; })();", new ValueCallback<String>() {
                @Override
                public void onReceiveValue(String s) {
                    Log.d("LogName", s); // Prints: {"var1":"variable1","var2":"variable2"}
                }
            });
rogerdpack
  • 62,887
  • 36
  • 269
  • 388
Rinkesh
  • 3,150
  • 28
  • 32
8

Yes, you could use shouldInterceptRequest() to intercept remote url loading and return local stored content.

WebView webview = (WebView) findViewById(R.id.webview);

webview.setWebViewClient(new WebViewClient() {
    @Override
    public WebResourceResponse shouldInterceptRequest (final WebView view, String url) {
       if (url.equals("script_url_to_load_local")) {
           return new WebResourceResponse("text/javascript", "UTF-8", new FileInputStream("local_url")));
       } else {
           return super.shouldInterceptRequest(view, url);
       }
    }
});
cyberflohr
  • 789
  • 5
  • 10
  • 1
    This is a better solution, as you can run Javascript more consistently. – dasmikko May 20 '16 at 03:55
  • 1
    Note that `shouldInterceptRequest(WebView, String)` has been superseded by `shouldInterceptRequest(WebView, WebResourceRequest)`. This solution still works (except that you have to check `request.getUrl()` and remember that its type is `Uri`). Also note that if you use `WebView.loadData()` without a base URL, you cannot use relative paths in ` – corwin.amber Jun 07 '19 at 14:29
2

Be careful using evaluateJavascript: if there is a syntax error or exception thrown in your javascript it will call your onReceiveValue with a null. The most common way to support both SDK 19 as well as lower seems to be like this:Fill form in WebView with Javascript

Also if you get terribly desperate for some kind of browser functionality (in my case, never could figure out how to get DRM to work well) you could use a bookmarklet within normal chrome, which works only if you type the bookmark name into the omnibox but does work and does inject javascript.

Also be aware that with the default WebView you can't use javascript alerts to test anything, they don't show. Also be aware that "video" by default (like html <video> tags) doesn't "really work" by default and also DRM video doesn't work by default, they're all configure options :\

rogerdpack
  • 62,887
  • 36
  • 269
  • 388