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);
    }
}