I have an app that heavily uses the Android WebView to display my custom HTML content. The latest Android update (4.4/Kit-Kat/SDK-19) featured a redesigned WebView.
One of my users with a Nexus 5 reported a problem where some links cause the app to crash. I ran in the 4.4 emulator and debug into my WebViewClient's shouldOverrideUrlLoading() method. On all previously tested Android versions (2.2-4.3) the url String passed into the method had my custom url with "/" characters in it. In 4.4 the exact same link now has "\" characters in their place.
This doesn't make any sense to me. I load the HTML exactly the same, so somehow the new WebView converted all my slashes into backslashes.
Why does the new WebView do this?
Changes in URL handling are a known issue. Please see the migration guide for more detail.
The behaviour in this particular case will depend on what your base URL's scheme is, from what you're describing I'm guessing your base URL's scheme is "http(s)://" in which case the Chromium WebView performs URL normalization.
You might want to consider using the URI class to handle the discrepancy between the Classic and Chromium WebViews in this case.
I did more debugging and discovered I actually have the question reversed. Turns out the older versions of WebView did conversions of the URL, not the new one.
I load HTML with a format similar to this into a WebView:
link
I use the double back slashes as delimiters and parse the data later when the link is clicked. In older versions of WebView it converted my double backslash characters into forward slashes. It had been so long since I was in that code, I forgot I adjusted my code to use forward slashes rather than the backslashes in the original HTML.
The new version of WebView leaves my custom URL intact, giving me the exact same string as my original HTML. So turns out the old WebView is the problem not the new one.
The new WebView applies additional restrictions when requesting resources and resolving links that use a custom URL scheme. For example, if you implement callbacks such as shouldOverrideUrlLoading() or shouldInterceptRequest(), then WebView invokes them only for valid URLs.
If you are using a custom URL scheme or a base URL and notice that your app is receiving fewer calls to these callbacks or failing to load resources on Android 4.4, ensure that the requests specify valid URLs that conform to RFC 3986.
For example, the new WebView may not call your shouldOverrideUrlLoading() method for links like this:
Show Profile
The result of the user clicking such a link can vary:
If you loaded the page by calling loadData() or loadDataWithBaseURL() with an invalid or null base URL, then you will not receive the shouldOverrideUrlLoading() callback for this type of link on the page.
Note: When you use loadDataWithBaseURL() and the base URL is invalid or set null, all links in the content you are loading must be absolute.
If you loaded the page by calling loadUrl() or provided a valid base URL with loadDataWithBaseURL(), then you will receive the shouldOverrideUrlLoading() callback for this type of link on the page, but the URL you receive will be absolute, relative to the current page. For example, the URL you receive will be "http://www.example.com/showProfile" instead of just "showProfile".
Instead of using a simple string in a link as shown above, you can use a custom scheme such as the following:
Show Profile
You can then handle this URL in your shouldOverrideUrlLoading() method like this:
// The URL scheme should be non-hierarchical (no trailing slashes)
private static final String APP_SCHEME = "example-app:";
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith(APP_SCHEME)) {
urlData = URLDecoder.decode(url.substring(APP_SCHEME.length()), "UTF-8");
respondToData(urlData);
return true;
}
return false;
}
If you can't alter the HTML then you may be able to use loadDataWithBaseURL() and set a base URL consisting of a custom scheme and a valid host, such as "example-app:///". For example:
webView.loadDataWithBaseURL("example-app://example.co.uk/", HTML_DATA,
null, "UTF-8", null);
The valid host name should conform to RFC 3986 and it's important to include the trailing slash at the end, otherwise, any requests from the loaded page may be dropped.
to avoid webview below 4.4 convert backslash to forward slash, I just escape my url, then in Java code, use URI.decode to get the real url.That works for me.
Related
Background: I have a working application which populates webview data by reading the response from an HttpURLConnection, running some processing on the markup, and then feeding the complete string to WebView.loadDataWithBaseUrl to display the page. This approach works, but has the performance drawback of eliminating concurrent processing of the partially loaded document which happens by default when loading a page normally in any browser or webview.
Goal: I'd like to change my application to a streaming model instead of a two-step serial process. In short, I'm investigating whether I can move from this:
Open the response stream from the server.
Block and read everything from the server until end of document.
Hand the whole thing to the webview at once.
to this:
Open the response stream from the server.
Read any available data and hand it immediately to the webview.
Repeat 2 until end of document.
For an additional wrinkle, the webview pages use both http:// and file:///android_asset requests, which work fine in my current setup. My main difficulty preserving access to both of these. Sidenote: I'm supporting API v14+.
Here's a simplified code summary of my current attempt.
// -- Setup --
WebView webview = makeWebView(); // WebSettings, JS on, view setup, etc
String url = "http://mywebsite.com";
InputStream is = getResponseStreamFromNetwork(url);
// -- Current data injection code --
String completeMarkup = readEverythingAtOnce(is);
webview.loadDataWithBaseUrl(url, completeMarkup, "text/html", "UTF-8", url);
// -- Ideal future data injection code --
webview.loadDataWithBaseUrl(url, "", "text/html", "UTF-8", url);
String line = null;
while ((line = is.readLine()) != null) {
appendToWebView(webview, line);
}
finishWebView(webview);
// -- The hard bits --
void appendToWebView(WebView webview, String line) {
// Inject a partial markup string to the end of the webview's content.
webview.loadUrl("javascript:document.write('" + line.replace("'", "\\'") + "\\n');");
}
void finishWebView(WebView webview) {
// We're done injecting data. Tell the webview we're finished if needed.
webview.loadUrl("javascript:document.write('\\n\\n');");
webview.loadUrl("javascript:document.close();");
}
As written, this successfully streams markup to the webview via document.write, and the page is properly displayed, but my file:///android_asset data requests all get denied access. Changing the base URL of loadDataWithBaseUrl to file:///android_asset fixes those but breaks access to http:// requests via XmlHttpRequest. I assume that this is by-design same origin policy specifically for content introduced via document.write. If that's the case, is there an alternative method for injecting this content piece by piece?
In short, is there a way to reimplement appendToWebView and finishWebView or change something elsewhere to stream data into the webview while avoiding this access problem?
So yes, mixing http(s) and file:// scheme is going to break due to the same origin policy.
This should work instead:
host your entire contents under a 'virtual' http(s) URL (ideally one that you own, or use androidplatform.net),
use WebViewClient.shouldInterceptRequest to intercept the requests to the 'virtual' URL and provide either the network InputStream or an InputStream associated with the asset.
Edit: The relevant bit of code the WebView uses to handle file://android_asset URLs is here.
I have a WebView where I am loading a javascript that loads some content. That content is an html with an iframe.
It seems any click within iframe is not triggering calls to WebViewClient#shouldOverrideUrlLoading(WebView view, String url); The webview has set both WebViewClient and WebChromeClient.
A work-around I can see of is to call getSettings().setSupportMultipleWindows(true) on the WebView and then within onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) assume the call was made within iframe and use some inserted javascript in order to get the iframe data. But that seems ugly to me.
So, the question: how do you detect a click within iframe loaded within the webview?
Thanks!
I think I have found what is the problem: I broke the Same Origin Policy. A colleague that is doing web development pointed me in the right direction.
The whole document (containing the iFrame) has a different origin (combination of scheme, host name and port number) than containing iframe. As long as those frames (containing document and inner iframe) have different origins, they cannot communicate. That's why I am not getting calls to shouldOverrideUrlLoading.
EDIT: It seems there is a way to overpass above security policy, but that comes with accepted security risk: Add the following header in the response that contains the iframe: "Access-Control-Allow-Origin:*"
I have an app with a previously-existing, web-based registration process that I am trying to use inside a WebView. I need to add some style tags to the html in order to hide some elements for better displaying the content inside my app. I can get it to work on initial load, but I cannot figure out how to do it from one page to the next inside the WebView. Here is what I have working:
On initial load of the site, I am getting the raw html and appending "<style>MY STYLES HERE</style>" to the string before calling
wv.loadDataWithBaseURL(url, rawHtml, null, "UTF-8", url);
This works perfectly, but if a user clicks a link on the page and it loads another page into the WebView, then this code does not get called and the style tag is lost.
I assume I need to override "shouldOverrideUrlLoading" in the WebViewClient, but I don't know how to intercept the html from here. I thought I would try something like:
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
String rawHtml = getRawHtml(url) + "<style>...</style>";
wv.loadDataWithBaseURL(url, rawHtml, null, "UTF-8", url);
}
But this obviously sends it into an endless loop of intercepting the load to start a new load.
I have also tried overriding onPageFinished and doing:
wv.loadUrl("javascript:(function() { ... })()");
which works, except that it waits until the entire page is loaded before executing. This causes the page to appear loaded with all of the UI elements in tact, and then all of the ones I am trying to hide suddenly disappear. My ultimate goal is to enhance the look and feel of the site on a mobile device, so this is not an option.
Is there something else I can do in "shouldOverrideUrlLoading" to inject style tags? Or if not, what else can I try?
I've run into this problem, and depending on the number of redirects, etc, we have not been able to make the injected JavaScript available all the time.
At minimum, you should use the wv.loadUrl("javascript:(function() { ... })()"); approach, but call it in both onPageStarted() and onPageFinished().
Depending on the complexity of your pages, you might need to inject the JavaScript in onLoadResource() as well.
I'm using the loadUrl() method in an activity's onCreate() method to load content from an external web server into a webview (I'm building an adroid app).
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
super.loadUrl("http://server-name/path/to/my/server/app/");
}
On the server I check the ACCEPT HTTP header to decide which data format I will deliver to a client.
Now the problem is: the loadUrl() method seems to always send the ACCEPT header value "application/xml, ...", which will cause my server to deliver data not as nicely layouted html (this is what I want for the webview), but rather as pure xml (which is good for e.g. some web service client).
So what I want to do is to set the ACCEPT HTTP header to "text/html", but the loadUrl() method won't allow me to do that.
I checked out the source code for cordova/android and found, that the loadUrl() method (in CordovaWebView.java) finally calls the (android sdk) WebView's loadUrl() method, which is overloaded to take some additional http headers. But I can't access that loadUrl method through normal cordova use.
So I edited the CordovaWebView's loadUrlNow() method, which calls the WebView's loadUrl() method and hardcoded an extra header:
void loadUrlNow(String url) {
...
Map<String, String> extraHeaders = new HashMap<String, String>();
extraHeaders.put("ACCEPT", "text/html");
super.loadUrl(url, extraHeaders);
}
then compiled it, replaced the original cordova.jar with my own and rebuild my android app. But if I now start the app and check the ACCEPT HTTP header on server side, the ACCEPT header didn't change. It's value is the very same than before hardcoding the extra header; it seems that I can't change that header.
So, is there any way to set the ACCEPT http header when calling the loadUrl() method?
Okay, I found the answer by myself. I found out, that I can't override headers sent by the WebView; if I do, they get overridden by the WebView's default values as the ADT documentation says here (see method loadUrl, parameter additionalHttpHeaders):
http://developer.android.com/reference/android/webkit/WebView.html#loadUrl(java.lang.String, java.util.Map)
Note that if this map contains any of the headers that are set by default by this WebView, such as those controlling caching, accept types or the User-Agent, their values may be overriden by this WebView's defaults.
I'm not sure, if the documentation was that clear two weeks ago (timestamp is 13 Sep 2012). As a workaround I'm trying setting a custom HTTP header like *MYAPP_ACCEPT* to "text/html" which I can evaluate on server side. To add this additional HTTP header I need to use my self-compiled cordova lib, though.
I take the response from an HTTP connection in the form of string and show that to webview like this:
WebView engine = (WebView)findViewById(R.id.webview);
engine.loadData(endResult, "text/html", "UTF-8"); /*endresult is string*/
I actually get a response that contains the google page (google search result direct from google.com).
The loadData method works well i.e it shows the web page but when I click on one of the links on that page it shows "page not available" and said that "xyz link might be temporarily down or it may have moved to permanently to a new web address".
this happens for all links accept the first present link on that page. i.e it shows correct page from first link on that page but fails for others..
I noticed that OSes prior to 2.3 failed to follow links if setHorizontalScrollBarEnabled and setVerticalScrollBarEnabled are set to false.
try to use loadDataWithBaseURL of the WebView class
I would avoid using engine.loadData - it seems to cause all sorts of crazy problems.
Use engine.loadDataWithBaseURL instead, and pass the base URL of where the content exists. I would think that the content you are loading is using relative paths in it's HTML so it's looking inside your app resources. By specifying the base URL you get around this problem.