Android uses the WebKit browser engine as the foundation for both its
Browser application and the WebView
embeddable browsing widget. The
Browser application, of course, is something Android users can
interact with directly; the WebView
widget is something you can
integrate into your own applications for places where an HTML
interface might be useful.
Earlier in this book, we saw a simple integration of a WebView
into
an Android activity, with the activity dictating what the browsing
widget displayed and how it responded to links.
Here, we will expand on this theme, and show how to more tightly integrate the Java environment of an Android application with the JavaScript environment of WebKit.
Understanding this chapter requires that you have read the core chapters,
particularly the one covering WebView
. Some of the samples
use LocationManager
for obtaining a GPS fix.
When you integrate a WebView
into your activity, you can control
what Web pages are displayed, whether they are from a local provider
or come from over the Internet, what should happen when a link is
clicked, and so forth. And between WebView
, WebViewClient
, and
WebSettings
, you can control a fair bit about how the embedded
browser behaves. Yet, by default, the browser itself is just a
browser, capable of showing Web pages and interacting with Web sites,
but otherwise gaining nothing from being hosted by an Android
application.
Except for one thing: addJavascriptInterface()
.
The addJavascriptInterface()
method on WebView
allows you to
inject a Java object into the WebView
, exposing its methods, so
they can be called by JavaScript loaded by the Web content in the
WebView
itself.
Now you have the power to provide access to a wide range of Android
features and capabilities to your WebView
-hosted content. If you
can access it from your activity, and if you can wrap it in something
convenient for use by JavaScript, your Web pages can access it as
well.
For example, HTML5 offers geolocation, whereby the Web page can find out
where the device resides, by browser-supplied means. We can do much of the
same thing ourselves via addJavascriptInterface()
.
In the
WebKit/GeoWeb1
project, you will find a fairly simple
layout (main.xml
):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<WebView android:id="@+id/webkit"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
All this does is host a full-screen WebView
widget.
Next, take a look at the GeoWebOne
activity class:
package com.commonsware.android.geoweb;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import org.json.JSONException;
import org.json.JSONObject;
public class GeoWebOne extends Activity {
private static String PROVIDER=LocationManager.GPS_PROVIDER;
private WebView browser;
private LocationManager myLocationManager=null;
@SuppressLint("SetJavaScriptEnabled")
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
browser=(WebView)findViewById(R.id.webkit);
myLocationManager=(LocationManager)getSystemService(Context.LOCATION_SERVICE);
browser.getSettings().setJavaScriptEnabled(true);
browser.addJavascriptInterface(new Locater(), "locater");
browser.loadUrl("file:///android_asset/geoweb1.html");
}
@Override
public void onResume() {
super.onResume();
myLocationManager.requestLocationUpdates(PROVIDER, 10000,
100.0f,
onLocation);
}
@Override
public void onPause() {
super.onPause();
myLocationManager.removeUpdates(onLocation);
}
LocationListener onLocation=new LocationListener() {
public void onLocationChanged(Location location) {
// ignore...for now
}
public void onProviderDisabled(String provider) {
// required for interface, not used
}
public void onProviderEnabled(String provider) {
// required for interface, not used
}
public void onStatusChanged(String provider, int status,
Bundle extras) {
// required for interface, not used
}
};
public class Locater {
@JavascriptInterface
public String getLocation() throws JSONException {
Location loc=myLocationManager.getLastKnownLocation(PROVIDER);
if (loc==null) {
return(null);
}
JSONObject json=new JSONObject();
json.put("lat", loc.getLatitude());
json.put("lon", loc.getLongitude());
return(json.toString());
}
}
}
This looks a bit like some of the WebView
examples from earlier
in this book. However, it adds three key bits of code:
LocationManager
to provide updates when the device
position changes, routing those updates to a do-nothing
LocationListener
callback objectLocater
inner class that provides a convenient API for
accessing the current location, in the form of latitude and longitude
values encoded in JSONaddJavascriptInterface()
to expose a Locater
instance
under the name locater
to the Web content loaded in the WebView
The Locater API uses JSON to return both a latitude and a longitude
at the same time. We are limited to using data types that are in
common between JavaScript and Java, so we cannot pass back the
Location
object we get from the LocationManager
. Hence, we
convert the key Location
data into a simple JSON structure that the
JavaScript on the Web page can parse.
Note that the getLocation()
method on Locater
has the
@JavascriptInterface
annotation. This is required of apps with
android:targetSdkVersion
set to 17 or higher, though it is a good idea
to start using it anyway. With such an android:targetSdkVersion
, in an
app running on an Android 4.2 or higher device, only public
methods with the
@JavascriptInterface
annotation will be accessible by JavaScript code.
On earlier devices, or with an earlier android:targetSdkVersion
, all
public
methods on the Locater
object would be accessible by JavaScript,
including those inherited from superclasses like Object
. Note that your
build target (i.e., compileSdkVersion
in Android Studio)
will need to be Android 4.2 or higher in order to reference
the @JavascriptInterface
annotation.
Also note that onCreate()
has the @SuppressLint("SetJavaScriptEnabled")
annotation. This overrides a Lint warning about the use of
setJavaScriptEnabled(true)
, where Lint wants to make sure that you understand
the risks of allowing arbitrary JavaScript to execute inside your app. In this
case, the JavaScript is code that we wrote, and so we can ensure that it is safe
and sane.
Later in this chapter,
we will cover security issues with WebView
and put
those annotations into context.
The Web page itself is referenced in the source code as
file:///android_asset/geoweb1.html
, so the GeoWeb1
project has a
corresponding assets/
directory containing geoweb1.html
:
<html>
<head>
<title>Android GeoWebOne Demo</title>
<script language="javascript">
function whereami() {
var location=JSON.parse(locater.getLocation());
document.getElementById("lat").innerHTML=location.lat;
document.getElementById("lon").innerHTML=location.lon;
}
</script>
</head>
<body>
<p>
You are at: <br/> <span id="lat">(unknown)</span> latitude and <br/>
<span id="lon">(unknown)</span> longitude.
</p>
<p><a onClick="whereami()">Update Location</a></p>
</body>
</html>
When you click the “Update Location” link, the page calls a
whereami()
JavaScript function, which in turn uses the locater
object to update the latitude and longitude, initially shown as
“(unknown)” on the page.
If you run the application, initially, the page is pretty boring:
Figure 559: The GeoWebOne sample application, as initially launched
However, if you wait a bit for a GPS fix, and click the “Update Location” link… the page is still pretty boring, but it at least knows where you are:
Figure 560: The GeoWebOne sample application, after clicking the Update Location link
Now that we have seen how JavaScript can call into Java, it would be nice if Java could somehow call out to JavaScript. In our example, it would be helpful if we could expose automatic location updates to the Web page, so it could proactively update the position as the user moves, rather than wait for a click on the “Update Location” link.
Well, as luck would have it, we can do that too. This is a good thing, otherwise, this would be a really weak section of the book.
What is unusual is how you call out to JavaScript. One might imagine
there would be an evaluateJavaScript()
counterpart to
addJavascriptInterface()
, where you could supply some JavaScript
source and have it executed within the context of the
currently-loaded Web page.
Actually, there is such a method on Android 4.4. However, earlier
versions of Android lacked that method.
Instead, on older versions of Android,
given your snippet of JavaScript source to execute, you call
loadUrl()
on your WebView
, as if you were going to load a Web
page, but you put javascript:
in front of your code and use that as
the “address” to load.
If you have ever created a “bookmarklet” for a desktop Web browser,
you will recognize this technique as being the Android analogue
– the javascript:
prefix tells the browser to treat the rest
of the address as JavaScript source, injected into the
currently-viewed Web page.
So, armed with this capability, let us modify the previous example to continuously update our position on the Web page.
The layout for the
WebKit/GeoWeb2
sample project is the same as
before. The Java source for our activity changes a bit:
package com.commonsware.android.geoweb2;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import org.json.JSONException;
import org.json.JSONObject;
public class GeoWebTwo extends Activity {
private static String PROVIDER="gps";
private WebView browser;
private LocationManager myLocationManager=null;
@SuppressLint("SetJavaScriptEnabled")
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);
browser=(WebView)findViewById(R.id.webkit);
myLocationManager=
(LocationManager)getSystemService(Context.LOCATION_SERVICE);
browser.getSettings().setJavaScriptEnabled(true);
browser.addJavascriptInterface(new Locater(), "locater");
browser.loadUrl("file:///android_asset/geoweb2.html");
}
@Override
public void onResume() {
super.onResume();
myLocationManager.requestLocationUpdates(PROVIDER, 0, 0, onLocation);
}
@Override
public void onPause() {
super.onPause();
myLocationManager.removeUpdates(onLocation);
}
LocationListener onLocation=new LocationListener() {
@TargetApi(Build.VERSION_CODES.KITKAT)
public void onLocationChanged(Location location) {
StringBuilder buf=new StringBuilder("whereami(");
buf.append(String.valueOf(location.getLatitude()));
buf.append(",");
buf.append(String.valueOf(location.getLongitude()));
buf.append(")");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
browser.evaluateJavascript(buf.toString(), null);
}
else {
browser.loadUrl("javascript:" + buf.toString());
}
}
public void onProviderDisabled(String provider) {
// required for interface, not used
}
public void onProviderEnabled(String provider) {
// required for interface, not used
}
public void onStatusChanged(String provider, int status,
Bundle extras) {
// required for interface, not used
}
};
public class Locater {
@JavascriptInterface
public String getLocation() throws JSONException {
Location loc=myLocationManager.getLastKnownLocation(PROVIDER);
if (loc == null) {
return(null);
}
JSONObject json=new JSONObject();
json.put("lat", loc.getLatitude());
json.put("lon", loc.getLongitude());
return(json.toString());
}
}
}
Before, the onLocationChanged()
method of our LocationListener
callback did nothing. Now, it builds up a call to a whereami()
JavaScript function, providing the latitude and longitude as
parameters to that call. So, for example, if our location were 40
degrees latitude and –75 degrees longitude, the call would be
whereami(40,-75)
.
What happens then depends upon the version of Android the device is running.
evaluateJavascript()
.
This takes the JavaScript source code, plus an optional callback, and
executes it in the context of the currently-loaded Web page.javascript:
in front of the Javascript source and
calls loadUrl()
on the WebView
. This is the same syntax used
for “bookmarklets” in desktop Web browsers.The result is that a whereami()
function in the Web page gets called with the new location.
That Web page, of course, also needed a slight revision, to accommodate the option of having the position be passed in:
<html>
<head>
<title>Android GeoWebTwo Demo</title>
<script language="javascript">
function whereami(lat, lon) {
document.getElementById("lat").innerHTML=lat;
document.getElementById("lon").innerHTML=lon;
}
function pull() {
var location=JSON.parse(locater.getLocation());
whereami(location.lat, location.lon);
}
</script>
</head>
<body>
<p>
You are at: <br/> <span id="lat">(unknown)</span> latitude and <br/>
<span id="lon">(unknown)</span> longitude.
</p>
<p><a onClick="pull()">Update Location</a></p>
</body>
</html>
The basics are the same, and we can even keep our “Update Location”
link, albeit with a slightly different onClick
attribute.
If you build, install, and run this revised sample on a GPS-equipped Android device, the page will initially display with “(unknown)” for the current position. After a fix is ready, though, the page will automatically update to reflect your actual position. And, as before, you can always click “Update Location” if you wish.
There is no navigation toolbar with the WebView widget. This allows you to use it in places where such a toolbar would be pointless and a waste of screen real estate. That being said, if you want to offer navigational capabilities, you can, but you have to supply the UI. WebView offers ways to perform garden-variety browser navigation, including:
reload()
to refresh the currently-viewed Web pagegoBack()
to go back one step in the browser history, and canGoBack()
to determine if there is any history to go back togoForward()
to go forward one step in the browser history, and
canGoForward()
to determine if there is any history to go forward togoBackOrForward()
to go backwards or forwards in the browser
history, where negative numbers represent a count of steps to go
backwards, and positive numbers represent how many steps to go
forwardscanGoBackOrForward()
to see if the browser can go backwards or
forwards the stated number of steps (following the same
positive/negative convention as goBackOrForward()
)clearCache()
to clear the browser resource cache and clearHistory()
to clear the browsing historyWith your favorite desktop Web browser, you have some sort of “settings” or “preferences” or “options” window. Between that and the toolbar controls, you can tweak and twiddle the behavior of your browser, from preferred fonts to the behavior of JavaScript.
Similarly, you can adjust the settings of your WebView widget as you see fit,
via the WebSettings
instance returned from calling the widget’s
getSettings()
method.
There are lots of options on WebSettings
to play with. Most appear fairly
esoteric (e.g., setFantasyFontFamily()
). However, here are some that you
may find more useful:
setDefaultFontSize()
(to use a point size)
or setTextSize()
(to use constants indicating relative sizes like
LARGER and SMALLEST)setUserAgent()
, so you can supply
your own user agent string to make the Web server think you are a
desktop browser, another mobile device (e.g., iPhone), or whatever.
The settings you change are not persistent, so you should store them
somewhere (such as via the Android preferences engine) if you are allowing
your users to determine the settings, versus hard-wiring the settings in your
application.More so than normal widgets, WebView
opens up potential security
issues, just as a Web browser could. If all you are doing is displaying
your own content, the risks are minimal. If, on the other hand, you are
displaying content from third parties, it is possible that their content
is malicious in a way that can compromise your app’s security, to your
users’ detriment.
If you call setJavaScriptEnabled(true)
on your WebSettings
, you are
allowing JavaScript code to be loaded and executed by WebView
. In many
cases, this is essential to get your content to render properly (e.g.,
the JavaScript is issuing AJAX calls). However, if you did not write the
scripts, you do not know what they might be doing. If there are flaws
in WebView
— such as those discussed in the next sections — then
your users may be at risk.
Even in the absence of such bugs, JavaScript can always:
javascript:
URLs or evaluateJavaScript()
Another way that rogue JavaScript can attack users is if you use
addJavascriptInterface()
to allow JavaScript code to call out to a
Java object that you supply.
As was noted earlier in this chapter, when
addJavascriptInterface()
was introduced, there is this
@JavascriptInterface
annotation that we should apply to the methods
we want JavaScript to be able to call on the object we supply via
addJavascriptInterface()
. This is because of a bug in the
addJavascriptInterface()
implementation, whereby on 4.1 and below
any method on the Java object could be called by JavaScript. This
includes methods like getClass()
… which in turn would allow
JavaScript to use Class.forName()
to get at arbitrary stuff. This
was used by various bits of malware.
Hence, using addJavascriptInterface()
on Android 4.1 and below
is rather risky, if you are loading arbitrary third-party JavaScript.
If you have the means of examining that JavaScript (e.g., you are
loading the scripts yourself), you might perform some simple scans
of it to see if they appear to be doing anything unfortunate with your
Java object that you injected into JavaScript via addJavascriptInterface()
.
Worse, Android sometimes also injects its own objects, without our requesting them.
In particular, this security bug
points out that, through Android 4.3, if users have enabled an accessibility
service, Android automatically injects objects into WebView
, using
addJavascriptInterface()
, named accessibility
and accessibilityTraversal
.
So, even if you do not inject any objects yourself via addJavascriptInterface()
,
your WebView
may be at risk. The security researchers who uncovered
this attack vector suggest using removeJavascriptInterface()
to specifically
get rid of those objects.
Due to a bizarre bug in the parsing of URLs, it is possible for JavaScript
code to violate the “same-origin policy” of a WebView
on Android 4.3 and
earlier.
Quoting Wikipedia from September 2014:
the same-origin policy is an important concept in the web application security model. The policy permits scripts running on pages originating from the same site — a combination of scheme, hostname, and port number — to access each other’s DOM with no specific restrictions, but prevents access to DOM on different sites… This mechanism bears a particular significance for modern web applications that extensively depend on HTTP cookies to maintain authenticated user sessions, as servers act based on the HTTP cookie information to reveal sensitive information or take state-changing actions. A strict separation between content provided by unrelated sites must be maintained on the client side to prevent the loss of data confidentiality or integrity.
All modern Web browsers implement the same-origin policy (SOP)… but there can
be bugs. A security researcher disclosed that the original AOSP Browser
application failed to implement the SOP properly,
when a javascript:
URL has a null byte before the j
in javascript
.
And while the report was focused on the AOSP Browser app, the problem
really lies with WebView
.
To see this in action, load https://commonsware.com/misc/sop-demo.html
in the AOSP Browser app on Android 4.3 or lower. This Web page consists
of:
<html>
<head>
<title>WebView SOP Test</title>
</head>
<body>
<h1>WebView SOP Test</h1>
<iframe name="test" src="http://developer.android.com"></iframe>
<input type=button value="test" onclick="window.open('\u0000javascript:alert(document.domain)','test')">
</body>
</html>
It is derived from a similar example found in the blog post outlining the security flaw.
In an SOP-compliant browser, clicking the button will have no effect.
In the AOSP Browser app, clicking the button shows the domain name of the
document in the iframe
. And, loading this HTML into a WebView
has
the same effect.
Part of the WebView
overhaul in Android 4.4 — replacing the original
implementation with a new one backed by Chromium — had the effect of fixing
this bug, whether intentionally or inadvertently.
There is no obvious mitigation approach for this bug, insofar as for the
attack shown above, none of the callbacks on WebViewClient
or
WebChromeClient
seem to allow us to intercept this URL before its
JavaScript is executed. If you are loading HTML yourself from a third party,
you might consider scanning that HTML for obvious signs of the attack
(e.g., regular expression check for \u0000javascript
), but that will
be limited at best. Beyond that, try to limit the content in a WebView
to be from only one origin, so that there is nothing for attackers to
obtain via this bug.
Also note that the security researcher who found this bug has also found another SOP violation, suggesting that mitigation strategies may be impractical.
Chrome custom tabs
serve as middle ground between using a WebView
in your own
app and launching a URL into a separate Web browser.
With a WebView
, you have complete control over the overall user
experience within your app. However, your WebView
is decoupled
from any other browser the user may be using on the device.
Conversely, launching URLs into the user’s chosen browser gives the
user their normal browsing experience, but you have no control over
the user experience, as the user is now in the browser app, not your app.
With Chrome custom tabs, while Chrome is handling the URL, it will allow
you limited control over the action bar (color and custom actions). It also
simplifies some things that you might otherwise have had to handle yourself,
such as pre-fetching a Web page to be able to quickly switch to it.
Basic integration is also fairly easy, coming in the form of extras on
the same sort of ACTION_VIEW
Intent
that you might have used for
launching the URL in a standalone browser.
At the same time, there are some concerns:
ACTION_VIEW
Intent
, and so the user can choose
to view the URL in a different browser, you will not get the custom
integration in that case. This may be fine, but you will need to make
sure that from a marketing and documentation standpoint you handle both
the case where the user chooses Chrome (and you get the “custom tab”)
and the case where the user chooses something else.ACTION_VIEW
Intent
, you need to take
into account that any information in the custom extras, like the PendingIntent
to use for an custom action bar item, is stuff that you are willing
for arbitrary apps to get their hands on. Do not assume that your
communications will solely be with Chrome.