Android not loading your CSS?

How a two year old mistake only came to light today.

Hi Medium. First post, go easy on me.

Splitting out the WebView from the core Android OS was a good decision. It gave Google the ability to fight fragmentation and to roll out fixes for security issues outside of doing full OS updates. On the other hand it presented the developer with a moving goalpost.

A small part of the Zendesk SDK uses a WebView to display articles from our users’ Help Centers. We allow our users to customise their content by supplying a CSS file. After a recent update of the Android system WebView, the customisation stopped working.

Now that the blurb is out of the way it’s time for the TL;DR of the rest of the post.

The WebView stopped loading external CSS from the assets directory.

I wrote this post once and then threw it out after reevaluating the issue with Occam’s razor. This seemed to be such a fundamental issue, but I could find nothing on the Android or Chromium issue trackers, nor on stack overflow.

Then I saw the mistake:

When the fail is strong, you need double the facepalm.

We use the loadDataWithBaseURL() method to get the HTML into the WebView. It looks something like this:

mArticleContentWebView.loadDataWithBaseURL(
baseUrl,
html,
TYPE_TEXT_HTML,
UTF_8_ENCODING_TYPE,
null
);

I then noticed the code which generated the html variable. It looked like this:

String html = "..." +
"<LINK href=’%s’ type=’%s’ rel=’%s’/>" +
"...";
html = String.format(
html, CSS_FILE, TYPE_TEXT_HTML, "stylesheet", ...);

The issue was simple. We were sharing the TYPE_TEXT_HTML MIME type, meaning that the WebView’s content was text/html, and the CSS’ content was text/html. Simply changing the CSS’ type to text/css fixed the rendering immediately. Android’s WebView had been working for two years by ignoring our mistake. Interestingly, no warnings were raised in the logs. Many tables were flipped but we were happy to have solved the issue.

Before finding the simple solution, we experimented with a different approach. We decided to try to load the CSS from the assets directory and then inline it in the HTML. This also worked well, although we had to be careful to do the I/O in a background thread. The following code outlines the alternate approach. In this code, renderArticleBody(css) is now what eventually sets the WebView’s data with loadDataWithBaseURL().

private static final String TOKEN_BOUNDARY = "\\A";
/**
* Reads CSS from src/main/assets/help_center_article_style.css
*
* @return The CSS as a string, or the empty String if
* none was found.
*/
@WorkerThread
private String getCssAsString() {

String css = StringUtils.EMPTY_STRING;
Scanner cssScanner = null;

try {

InputStream cssInputStream =
getResources().getAssets().open(CSS_FILE);
       cssScanner = new Scanner(
cssInputStream,
UTF_8_ENCODING_TYPE
).useDelimiter(TOKEN_BOUNDARY);

if (cssScanner.hasNext()) {
css = cssScanner.next();
}

} catch (IOException e) {
Logger.e(LOG_TAG, "Failed to load CSS.", e);
} finally {
if (cssScanner != null) {
cssScanner.close();
}
}

return css;
}
AsyncTask.execute(new Runnable() {
@Override
public void run() {
final String css = getCssAsString();
runOnUiThread(new Runnable() {
@Override
public void run() {
renderArticleBody(css);
}
});
}
});

Using the alternate approach is more complex than simply linking to the stylesheet, but it does have one interesting benefit. In Lollipop, Google changed the default behaviour of the WebView to disallow the loading of content from mixed sources. The simple solution loads the CSS using a file:// URI which falls under the mixed-content umbrella because our baseUrl is https://. To allow the WebView to use CSS from a file URI we need to do this, which isn’t desirable:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
articleContentWebView.getSettings()
.setMixedContentMode(
WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
);
}

At the end of the day we have two solutions for the same issue, both of which have their pros and cons. The real lesson here was that if nobody else seems to be having the same problem, then it’s probably a better idea to go through your code again before blaming a WebView update.