PSA: WebView URL-Handling Regression

About 40 minutes ago, a developer posted a question on Stack Overflow that made my heart sink:

I noticed that with the last update of Google System WebView, all the links in my WebViews are opened in the view itself.

While we generally applauded the move to decouple WebView from firmware updates, so security fixes can get rolled out quicker, that switch has its downsides. One is that bugs or other regressions in WebView behavior roll out to a lot of people before we as developers necessarily have time to adjust.

In this case, WebView (from Andoid System WebView 43.0.2357.121) now behaves as noted above:

  1. All links and redirected-to URLs open in the WebView by default

  2. If you return false from a shouldOverrideUrlLoading() in your WebViewClient, the URL will be loaded into the WebView

Before, both scenarios would trigger an ACTION_VIEW Intent on the URL, typically bringing up a Web browser.

This runs counter to what is documented for shouldOverrideUrlLoading() and past seven years’ of WebView behavior.

If your app is using a WebViewClient to keep all links within the WebView anyway, you should see no net change in behavior.

However, if your app relies on WebView opening up some URLs in a browser on its own, you have a problem.

Right now, though, we do not know if this is an accidental regression or a permanent change in behavior. I filed a bug report on it, but I have little hope that it will ever get addressed. I have had less luck with bug reports on the Chromium project than I have on the Android project, and that’s saying a lot.

Hence, my recommendation at the moment is:

  • Always attach a WebViewClient to your WebView

  • Always implement shouldOverrideUrlLoading() on the WebViewClient

  • Always return true to indicate that you are handling the event

  • Always do what your app needs to have done, whether that is loading the URL into the WebView or launching a browser on the URL (rather than returning false and relying on stock behavior)

Something like this static inner class appears to do the trick — create an instance and pass it to setWebViewClient() on your WebView:

private static class URLHandler extends WebViewClient {
  @Override
  public boolean shouldOverrideUrlLoading(WebView view, String url) {
    if (shouldKeepInWebView(url)) {
      view.loadUrl(url);
    }
    else {
      Intent i=new Intent(Intent.ACTION_VIEW, Uri.parse(url))
          .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

      view.getContext().startActivity(i);
    }

    return(true);
  }

  private boolean shouldKeepInWebView(String url) {
    return(true); // or false, or use regex, or whatever
  }
}

(where you would put your business logic in shouldKeepInWebView() to determine whether or not a given URL should stay in the WebView or launch a browser)

Unfortunately, since I do not see a getWebViewClient() counterpart to setWebViewClient(), we cannot use a chaining pattern here easily. If you already have a WebViewClient for other reasons, you would want to blend in something akin to the code shown above.

With luck, we will get more clarity on the situation in the coming days (weeks? months?).