Opening a window.open in Android without killing the content of the main WebView

Preparation of the smaller problem

Problem: inside the webview we have a piece of code that calls window.open to make a popup containing some information. How does this information can be opened?

  • In the default browser for the device
  • Inside a new window in the same webview.

The first one is not feasible in our case: the URL would be a local url with file:// protocol pointing inside the application package itself and it wouldn’t be accessible from the outside world, or at least without implementing a webserver of some sort in our application.

The second approach seems more reasonable.

Now what are the requirements of this:

  1. The contents of the initial page in the webview are not lost
  2. The user can close and come back to the initial page

So to make sure nothing is going to influence what I am doing, I created a sample scenario which recreates the conditions in the app I am working:


<body>
    <div id="tokei"></div>
    <a>Press me</a>

    <div id="timestamp">This is the time when the user presses the button: <span id="stop"></span></div>

    <script>
    window.onload = function () {
      var tokei = document.getElementById('tokei');
      var stop = document.getElementById('stop');

      function printTime(element) {
        var time = new Date();
        var text = time.getHours() + ' : ' + time.getMinutes() + ' : ' + time.getSeconds();
        element.innerHTML = text;
      }
      setInterval(function () {
        printTime(tokei);
      }, 1000);
      var a = document.getElementsByTagName('a')[0];
      a.onclick = function () {
        printTime(stop);
        window.open('contents.html', '','height=700,width=620,resizable=yes,scrollbars=yes');
      };
    };
    </script>
  </body>

Making a simple app with a simple webview inside

To make sure that nothing is failing randomly better off to make first a simple web client, where we load for example google.com.

Inside our main layout:


<WebView
    android:id="@+id/webview"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
</WebView>

MainActivity.java:


private WebView webView;


@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    webView = (WebView) findViewById(R.id.webview);
    webView.setWebViewClient(new WebActivityClient(this));

    webView.getSettings().setSupportMultipleWindows(true);
    webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);

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

}

We add to the AndroidManifest.xml the permissions to access the internet:


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

In fact when I first made this sample app I forgot about adding the permissions. If the same would happen in a bigger app usually one would spend hours debugging the issue.

Now we add the sample html inside ‘assets/www’ folder and we set the loadUrl parameter to file:///android_asset/www/index.html/
Since JavaScript is not running, we need to add a permission for this:


webView.getSettings().setJavaScriptEnabled(true);

Now Javascript is running, and we can start playing with the opening of the popup.

First attempt to open the JavaScript popup:

Adding some settings to the webview, should apparently help. These settings are:

  • setSupportMultipleWindows(true) which allow the webview to have multiple windows
  • setJavaScriptCanOpenWindowsAutomatically(true) which will allow JavaScript to open windows automatically.

webView.getSettings().setSupportMultipleWindows(true);
webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);

Then we want to use custom management of windows, so we need to set WebChromeClient.


webView.setWebChromeClient(new WebChromeClient());

WebChromeClient exposes the method which is invoked when a new window is created: onCreateWindow.

This method is invoked when the application is trying to open a link. The return value will be set to ‘true’ if we are going to handle the opening, and false otherwise.

Now what we need to do is: create a new view and keep the existing one opened.

To make sure that everything is fine and kept I added a simple JavaScript clock and a time stamp of when the popup has been opened. If  the page should refresh then the time stamp would be lost.

Let’s start by just opening the popup.
Inside the onCreateWindow method, we are going to create a new WebView and then opening that view.

This is the snippet:


webView.setWebChromeClient(new WebChromeClient() {
    @Override
    public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
        webView.removeAllViews();
        WebView newView = new WebView(context);
        newView.setWebViewClient(new WebViewClient());
        // Create dynamically a new view
        newView.setLayoutParams(new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        
        webView.addView(newView);

        WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
        transport.setWebView(newView);
        resultMsg.sendToTarget();
        return true;
    }
});

The first step is to remove all the childViews which might be in the webView, and then create a new layout where to display the new view.

Then we need to inform the thread that we have a new view. To do this we use the class WebView.WebViewTransport which will allow to set the new webview as well as informing the target through the Message.

At this point the WebView is able to display the popup correctly, but we still have a problem: we can’t go back to the original webview.

Not killing the original webview

By default in Android we are able to go back to the previous page by pressing the back button and then executing the equivalent of history.goBack() in JavaScript world. This snippets looks like:


@Override
public void onBackPressed() {
    if (webView.canGoBack()) {
        webView.goBack();
    } else {
        super.onBackPressed();
    }
}

However: does this snippet help us in here? Not really: opening a different page and then going back would simply reload the old page, losing the content the user might have inserted.

So that’s not a good solution. Next thing to try is to intercept the page load, and open the new page inside a different activity. This way Android will take the user to another activity without killing the preexisting one.

Capturing the loading URL

Disclaimer: this can be not the best way to accomplish this task,
however it has been the only way I found to do so.

The idea: loading the new page inside of a different webview, and then reading the URL off it. At this point we can launch a different activity while destroying the original hidden webview.

In our layout we are going to add another invisible webview, by simply setting the layout to be 0px width and 0px height:


<WebView
    android:id="@+id/webview_hidden"
    android:layout_width="0px"
    android:layout_height="0px">
</WebView>

Now inside the onCreateWindow we are going to use this new view to load the content, and then we’ll use the method onPageStarted to capture the url and launch our new activity:


webView.setWebChromeClient(new WebChromeClient() {
    WebView newView = new WebView(context);
    @Override
    public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
        final WebView newView = (WebView) findViewById(R.id.webview_hidden);
        newView.setWebViewClient(new WebActivityClient(context) {
            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                Intent intent = new Intent(context, PopupActivity.class);
                intent.putExtra("URL", url);
                startActivity(intent);

                newView.destroy();
            }
        });

        WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
        transport.setWebView(newView);
        resultMsg.sendToTarget();
        return true;
    }
});

The new activity is called popup and it is composed by a simple webview.
The popup activity looks like this:


public class PopupActivity extends Activity {
    private WebView webView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_popup);

        Intent intent = getIntent();
        String url = intent.getStringExtra("URL");

        webView = (WebView) findViewById(R.id.webview);
        webView.loadUrl(url);
    }
}
  • Rahmad Andi Pasmi

    please send a sample project,

    • Maurizio_N

      All the code you need is in the post. If you need help with your app I am available for hire 😉

      • Safeer Qureshi

        what is WebActivityClient in this, or where is its definition/implementation?

        • Maurizio_N

          It is just the default Chrome client where you’re displaying the webview.

      • Angry President

        what is R.layout.activity_popup?

        • Maurizio_N

          It is the layout id for the new popup

          • Angry President

            Thanks. 🙂

  • Steffan Davies

    Thank you for posting this. I found it very useful, although I made a few modifications in my code.

    Rather than creating a new PopupActivity, I just reused the MainActivity with an argument:


    Intent intent = new Intent(context, MainActivity.class);
    intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
    intent.putExtra(ARG_URL, url);
    startActivity(intent);
    overridePendingTransition(0, 0);

    Handling it like this later on:


    String url = getIntent().getStringExtra(ARG_URL);
    webView.loadUrl(url != null ? url : "http://www.google.com");

    Note the Intent.FLAG_ACTIVITY_NO_ANIMATION which makes the transition less jarring.

  • Gopalan R C

    Hi, I’m getting the following exception

    Application attempted to call on a destroyed WebView
    java.lang.Throwable
    at org.chromium.android_webview.AwContents.isDestroyed(AwContents.java:1154)
    at org.chromium.android_webview.AwContents.getFavicon(AwContents.java:1336)
    at com.android.webview.chromium.WebViewChromium.getFavicon(WebViewChromium.java:968)
    at android.webkit.WebView.getFavicon(WebView.java:1332)
    at com.android.webview.chromium.WebViewContentsClientAdapter.onPageStarted(WebViewContentsClientAdapter.java:510)
    at org.chromium.android_webview.AwContentsClientCallbackHelper$MyHandler.handleMessage(AwContentsClientCallbackHelper.java:144)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:145)
    at android.app.ActivityThread.main(ActivityThread.java:5832)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1388)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1183)

    The link doesn’t work after the first time

  • Angry President

    I’m developing a Cordova app so I don’t have access to layout.xml. How can I implement this without altering layout.xml?

  • 0wn4g3r

    Hello dude can you give me full source ?

  • FuYun Liao

    If must use two activities, why not use JavascriptInterface directly

  • gilles gerlinger

    Maybe a stupid question, but in the first code snippet, there is an unresolved symbol: context. Where does it come from?

    • Maurizio_N

      It is the class of the caller. Normally you would use `this` but here I am passing the original class from the caller.
      Have a look at the documentation for the basic usage of webviews: https://developer.android.com/reference/android/webkit/WebView.html

      • gilles gerlinger

        Got it now. Working like a charm. Thanks a lot Maurizio for the fast and efficient support, really appreciated 🙂