Injecting Javascript bridge in WebView
Asked Answered
P

5

10

I want to pull some stuff from a webpage in Android. I know there are libraries to parse HTML, but I thought maybe I could cheat a little bit.

Here's what I'm doing..

  1. Programmatically create a WebView using the application context so it doesn't have to be displayed in the UI.
  2. Load the web page
  3. Attach the JS Interface
  4. Inject some Javascript to interact with the host application

Here's some code...

    public void getLatestVersion(){
        Log.e("Testing", "getLatestVersion called...");
        WebView webview = new WebView(context.getApplicationContext());
        webview.loadUrl("https://example.com");
        webview.addJavascriptInterface(new jsInterface(), "Droid");
        webview.loadUrl("javascript: window.onload=function(){ Droid.showToast('testing!'); }");
    }

    class jsInterface{
        @JavascriptInterface
        public void showToast(String message){
            Log.e("Testing", message);
            Toast.makeText(context, message, Toast.LENGTH_LONG).show();
        }
    }

Since the WebView is not visible in the UI, it's hard to tell which part is breaking. All I know is that the first Log called is called, but the Log and Toast from the JavascriptInterface are never shown.

Is what I'm trying to do even possible? If so, what am I doing wrong? If not, why not?

EDIT

Stuck the view in the UI for testing, apparently the second call to loadUrl is not working. No matter what Javascript I try to inject, it doesn't work.

EDIT 2

I feel dumb for forgetting to enable Javascript, but it's still not working.. I've added the following lines..

    WebSettings webSettings = webview.getSettings();
    webSettings.setJavaScriptEnabled(true);
    webview.loadUrl("javascript: alert('farts0');");

    webview.loadUrl("https://example.com");
    setContentView(webview);

    String js = "document.body.innerHTML = '<p>test<p>';";
    if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        webview.evaluateJavascript(js, null);
    }else{
        webview.loadUrl("javascript: "+js);
    }

EDIT 3

Thanks for everyone's suggestions, you've been helpful but so far it's still not working so unless someone provides working code in the next hour Nainal will get half the bounty. If so I'm not sure if I'll be allowed to place another bounty on it as the problem is still unresolved.

Here's my complete code so far after taking into account suggestions on this page and trying several settings from the manual that I don't really understand.

import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    WebView webView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        webView = new WebView(getApplicationContext());


        if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN)
            webView.getSettings().setAllowFileAccessFromFileURLs(true);

        if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN)
            webView.getSettings().setAllowUniversalAccessFromFileURLs(true);

        webView.getSettings().setDomStorageEnabled(true);
        webView.getSettings().setJavaScriptEnabled(true);
        try {
            webView.setWebContentsDebuggingEnabled(true);
        }catch(Exception e){}
        webView.setWebChromeClient(new WebChromeClient());
        webView.setWebViewClient(new WebViewClient() {
            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                webView.setVisibility(View.GONE);

            }
            @Override
            public void onPageFinished(final WebView view, String url) {
                Log.e("checking", "MYmsg");
                Log.e("content-url", webView.getSettings().getAllowContentAccess()?"y":"n");
                webView.loadUrl("javascript: void window.CallToAnAndroidFunction.setVisible(document.getElementsByTagName('body')[0].innerHTML);");



            }
        });
        webView.setVisibility(View.INVISIBLE);
        webView.addJavascriptInterface(new myJavaScriptInterface(), "CallToAnAndroidFunction");
        webView.loadUrl("http://example.com");
    }
    public class myJavaScriptInterface {
        @JavascriptInterface
        public void setVisible(final String aThing) {
            Handler handler = new Handler();
            Runnable runnable = new Runnable() {
                @Override
                public void run() {

                    MainActivity.this.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            webView.setVisibility(View.VISIBLE);
                            Toast.makeText(MainActivity.this, "Reached JS: "+aThing, Toast.LENGTH_LONG).show();

                        }
                    });


                }
            };handler.postDelayed(runnable,2000);

        }}



}

Edit 4

Started a new bounty and increased the reward to 100pts. Nainal got the last bounty for being the most helpful, not for solving the problem.

Poucher answered 5/8, 2016 at 19:13 Comment(4)
The post was edited too many times, it's hard to follow the "problem". What is the current problem that you have after all these improvements?Cloe
7 other people understood the question just fine.. see the accepted answer for more info.Poucher
I only wanted to catch-up and help, since you didn't have an accepted answer at the time of my writing and the information was scrambled all over the place. Hope you also understand the solution and what was the root cause.Cloe
Thank you. The solution was simple. I didnt add internet permission. Android isnt my main bag. Noob mistake :)Poucher
A
3

Here is a cleaned up version, minimizing unneeded code. This runs on API level 18 and 23 emulators (and my 6.0.1 phone). The webView is never added to the view hierarchy. The toast shows the HTML pulled from the site anyway. Compiled against API 25 using Java 8.

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        webView = new WebView(getApplicationContext());

        webView.getSettings().setJavaScriptEnabled(true);
        webView.setWebChromeClient(new WebChromeClient());

        webView.setWebViewClient(new WebViewClient() {
            @Override
            public void onPageFinished(final WebView view, String url) {
                webView.loadUrl("javascript: void AndroidHook.showToast(document.getElementsByTagName('body')[0].innerHTML);");
            }
        });

        webView.addJavascriptInterface(new JSInterface(), "AndroidHook");
        webView.loadUrl("http://example.com");
    }

    public class JSInterface {
        @JavascriptInterface
        public void showToast(final String html) {

            MainActivity.this.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(MainActivity.this, "Reached JS: " + html, Toast.LENGTH_LONG).show();
                }
            });
        }
    }
}

Here's the layout.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="16dp"
    android:paddingLeft="16dp"
    android:paddingRight="16dp"
    android:paddingTop="16dp"
    tools:context="com.foo.jsinjectiontest.MainActivity">

</RelativeLayout>

And finally the manifest.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.foo.jsinjectiontest">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

    <uses-permission android:name="android.permission.INTERNET"/>

</manifest>
Appellate answered 29/12, 2016 at 21:47 Comment(6)
i'm really stoked to try this out when i get home. thank you so much. will report back by end of day.Poucher
you know what.... i don't think I ever included internet permissions in the manifest. that may have been the issue the whole time. i might have to increase the bounty before I give it to you if this works..Poucher
Turns out it isn't relevant to this use, but I found this fascinating. You sometimes want to add a call to void(0); to get the results of injected JavaScript to show. See these two links: #27523540 #1292442Appellate
Did it work? Glad I found this question. I want to try this in a project myself.Appellate
sorry i haven't had a chance to try it yet, on vacation for new years but will give it a shot tomorrow if i get home early enough, else it will be a nice way to ease back into coding when i go back to work tuesday morning.Poucher
You are he man, @Hod! That worked beautifully! Thank you so much for your help.Poucher
E
5

Please try this, it is calling the javascript function and showing toast message also.

public class Main3Activity extends AppCompatActivity {
     WebView webView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main3);
        webView = new WebView(getApplicationContext());
        webView.getSettings().setJavaScriptEnabled(true);

        webView.setWebViewClient(new WebViewClient() {
            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                webView.setVisibility(View.GONE);

            }
            @Override
            public void onPageFinished(final WebView view, String url) {
                Log.e("checking", "MYmsg");
                webView.loadUrl("javascript:(function() { " +
                        "document.body.innerHTML = '<p>test<p>';" + "})()");
                webView.loadUrl("javascript: window.CallToAnAndroidFunction.setVisible()");



            }
        });
        webView.setVisibility(View.INVISIBLE);
        webView.addJavascriptInterface(new myJavaScriptInterface(), "CallToAnAndroidFunction");
        webView.loadUrl("https://example.com");
    }
    public class myJavaScriptInterface {
        @JavascriptInterface
        public void setVisible() {

            Handler handler = new Handler();
            Runnable runnable = new Runnable() {
                @Override
                public void run() {

                    Main3Activity.this.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            webView.setVisibility(View.VISIBLE);
                            Log.e("Testing", "no");
                            Toast.makeText(Main3Activity.this, "Reached JS", Toast.LENGTH_LONG).show();

                        }
                    });


                }
            };handler.postDelayed(runnable,2000);

        }}
}

It will not show webView in the UI, as webview is not defined in xml layout.

Extrajudicial answered 22/12, 2016 at 8:53 Comment(7)
ah, that makes sense to put it in a new thread. looking forward to trying this. thanks!Poucher
it seems like it's working because the JS interface is being called but it doesn't seem to be able to interact with the DOM... I tried doing this, but got an empty string: webView.loadUrl("javascript: window.CallToAnAndroidFunction.setVisible(document.getElementsByTagName('body')[0].innerHTML);");Poucher
you have to enable the DOM storage as-- webView.getSettings().setDomStorageEnabled(true);Extrajudicial
issue is still unresolved but i already lost my 50 rep and you're going to get half of it if i don't award you the full bounty, so i just gave it to you. my understanding is that setDomStorageEnabled has to do with indexeddb and websql but i tried it anyway and it still didn't work. plus everyone else sort of gave the same suggestions as you and you're the only one who provided a full example. thank you for your help.Poucher
I've added an additional bounty, you have a chance to win 100 more points on top of the 50 you got from the last bounty if you can provide any further suggestions.Poucher
So you're saying you don't get the html from getElementsByTagName? What are you testing on? The toast is showing the html for me.Appellate
Your code is working fine, i am getting the html in toast. Surely you are accessing a webpage so it is must to include internet permission, so you are right that you have forgot to include it. I think that was the issue.Extrajudicial
A
3

Here is a cleaned up version, minimizing unneeded code. This runs on API level 18 and 23 emulators (and my 6.0.1 phone). The webView is never added to the view hierarchy. The toast shows the HTML pulled from the site anyway. Compiled against API 25 using Java 8.

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        webView = new WebView(getApplicationContext());

        webView.getSettings().setJavaScriptEnabled(true);
        webView.setWebChromeClient(new WebChromeClient());

        webView.setWebViewClient(new WebViewClient() {
            @Override
            public void onPageFinished(final WebView view, String url) {
                webView.loadUrl("javascript: void AndroidHook.showToast(document.getElementsByTagName('body')[0].innerHTML);");
            }
        });

        webView.addJavascriptInterface(new JSInterface(), "AndroidHook");
        webView.loadUrl("http://example.com");
    }

    public class JSInterface {
        @JavascriptInterface
        public void showToast(final String html) {

            MainActivity.this.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(MainActivity.this, "Reached JS: " + html, Toast.LENGTH_LONG).show();
                }
            });
        }
    }
}

Here's the layout.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="16dp"
    android:paddingLeft="16dp"
    android:paddingRight="16dp"
    android:paddingTop="16dp"
    tools:context="com.foo.jsinjectiontest.MainActivity">

</RelativeLayout>

And finally the manifest.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.foo.jsinjectiontest">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

    <uses-permission android:name="android.permission.INTERNET"/>

</manifest>
Appellate answered 29/12, 2016 at 21:47 Comment(6)
i'm really stoked to try this out when i get home. thank you so much. will report back by end of day.Poucher
you know what.... i don't think I ever included internet permissions in the manifest. that may have been the issue the whole time. i might have to increase the bounty before I give it to you if this works..Poucher
Turns out it isn't relevant to this use, but I found this fascinating. You sometimes want to add a call to void(0); to get the results of injected JavaScript to show. See these two links: #27523540 #1292442Appellate
Did it work? Glad I found this question. I want to try this in a project myself.Appellate
sorry i haven't had a chance to try it yet, on vacation for new years but will give it a shot tomorrow if i get home early enough, else it will be a nice way to ease back into coding when i go back to work tuesday morning.Poucher
You are he man, @Hod! That worked beautifully! Thank you so much for your help.Poucher
R
2

A couple of things that pop out to me.

  1. The JavaScript interface should be attached BEFORE loading any URLs.

  2. The second loadURL window.onload might be assigned AFTER the original URL has loaded. It would make more sense to call setWebViewClient() and call Droid.showToast('testing!'); from inside the onPageFinished method.

  3. The @JavascriptInterface doesn't run on the main UI thread which will stop your toasts from running.

  4. The issue with your second edit's innerHTML code not working is related to point 2. You're making your calls in a synchronous single block, whereas it should be AFTER the page dom has loaded AKA onPageFinished()

Rear answered 25/12, 2016 at 20:3 Comment(5)
i've tried a few different ways but it does not seem like javascript can access the dom on the page being injected to. the html of the page appears blank to the javascript. why is that?Poucher
Do you mind sending a github gist of your current activity (along with other classes which you think might be relevant)? I should be able to solve this fairly easily once I can see the full picture of what you're doing. You should also use the chrome://inspect/#devices tool on your desktop to debug the issue and even make Droid.showToast('test') calls from it!.Rear
I've added my full activity to the question (SO generally frowns upon providing code from offsite-sources like github). I've also increased and extended the bounty to 100pts if you've got any further suggestions they would be helpful.Poucher
I used your code on a new project and it seems to be working fine for me. I optimized it a bit further and provided a couple of helpful comments. Found a couple of minor issues: the log & toast was delayed by 2 seconds, there was a minor memory leak with the webviews, and a couple redundant runnables on the main thread. I also added a way to visually inspect the webview to ensure that it's working as expected Here's the updated class - gist.github.com/anonymous/13d7cbe3d318d4c7d64a24e139026be5Rear
The android.permission.INTERNET permission isn't required for the WebView to function. It would just give a generic error message stating that it's unable to connect to example.comRear
T
2

webview.addJavascriptInterface(new jsInterface(), "Droid"); have to come before webview.loadUrl("https://example.com");

Then use Webview Listener. onFinish() method.. then inject your webview.loadUrl("javascript: window.onload=function(){ Droid.showToast('testing!'); }"); in onFinish method

i already do tons of webview injection modifying web.. i thinks its should work..

EDIT
use chrome://inspect/#devices to inspect your app when webview is load

Thorr answered 27/12, 2016 at 7:37 Comment(2)
i've tried a few different ways but it does not seem like javascript can access the dom on the page being injected to. the html of the page appears blank to the javascript. why is that?Poucher
Thank you for chrome://inspect/#devices that is a handy tool. I've increased the bounty to 100 points if you'e got any further suggestions. Thank you.Poucher
F
0

How about use onProgressChanged() in WebChromeClient?

I've changed some code from Edit 3 to like this,

webView.setWebChromeClient(new WebChromeClient(){
        @Override
        public void onProgressChanged(WebView view, int newProgress) {
            super.onProgressChanged(view, newProgress);
            if(newProgress == 100){
                webView.loadUrl("javascript: void window.CallToAnAndroidFunction.setVisible(document.getElementsByTagName('body')[0].innerHTML);");
            }
        }
    });

The change is that you invoke javascript when progress==100 instead of onPageFinished()

Familiarity answered 2/1, 2017 at 9:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.