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.

I wrestled a bear once.
  • 22,983
  • 19
  • 69
  • 116

5 Answers5

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.

Nainal
  • 1,728
  • 14
  • 27
  • ah, that makes sense to put it in a new thread. looking forward to trying this. thanks! – I wrestled a bear once. Dec 22 '16 at 13:14
  • 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);");` – I wrestled a bear once. Dec 27 '16 at 13:52
  • you have to enable the DOM storage as-- webView.getSettings().setDomStorageEnabled(true); – Nainal Dec 28 '16 at 04:04
  • 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. – I wrestled a bear once. Dec 29 '16 at 16:00
  • 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. – I wrestled a bear once. Dec 29 '16 at 16:08
  • 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. – Hod Dec 29 '16 at 19:59
  • 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. – Nainal Dec 30 '16 at 04:37
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>
Hod
  • 2,236
  • 1
  • 14
  • 22
  • i'm really stoked to try this out when i get home. thank you so much. will report back by end of day. – I wrestled a bear once. Dec 29 '16 at 21:52
  • 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.. – I wrestled a bear once. Dec 29 '16 at 21:54
  • 2
    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: http://stackoverflow.com/questions/27523040/javascript-injection-into-webview?rq=1 http://stackoverflow.com/questions/1291942/what-does-javascriptvoid0-mean – Hod Dec 29 '16 at 22:00
  • Did it work? Glad I found this question. I want to try this in a project myself. – Hod Dec 31 '16 at 00:54
  • 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. – I wrestled a bear once. Jan 02 '17 at 05:10
  • You are he man, @Hod! That worked beautifully! Thank you so much for your help. – I wrestled a bear once. Jan 03 '17 at 13:36
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()

Passer by
  • 562
  • 9
  • 14
  • 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? – I wrestled a bear once. Dec 27 '16 at 14:05
  • 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!. – Passer by Dec 27 '16 at 22:26
  • 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. – I wrestled a bear once. Dec 29 '16 at 16:10
  • 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 - https://gist.github.com/anonymous/13d7cbe3d318d4c7d64a24e139026be5 – Passer by Dec 31 '16 at 19:47
  • 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.com – Passer by Dec 31 '16 at 19:51
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

ZeroOne
  • 8,996
  • 4
  • 27
  • 45
  • 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? – I wrestled a bear once. Dec 27 '16 at 14:05
  • 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. – I wrestled a bear once. Dec 29 '16 at 16:11
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()

Choim
  • 372
  • 1
  • 10