Android Programming: Pushing the Limits (2014)
Part III. Pushing the Limits
Chapter 13. Maps, Location, and Activity APIs
In May 2013, at the annual Google IO conference, Google presented something slightly different for Android
developers. Google used to focus on new platform features and Android versions at this event, but at this
conference, they presented a new service API for location-based apps that works on older Android versions as
well. This new API allows developers to use the new and advanced Location and Activity API for Android, which
is part of the Google Play Services, on most Android devices deployed into the market today.
In this chapter, I cover the features of the new Location API and how you can integrate it into your applications.
Fused Location Manager
Google managed to provide a new unified Location API that works for older Android versions by fusing input
from all the sensors and other inputs into one library. Figures 13-1 and 13-2 show the differences between
the use of the old location-based APIs and the new fused Location Manager. The new library checks for the
existence of all the possible inputs and sensors and manages all the specifics for each Android version.
Apps
Platform
Location Manager
SensorManager
GpsLocationProvider
NetworkLocationProvider
Sensors
GPS
WiFi
Cellular
Hardware
Figure 13-1 The old location-based APIs in the Android platform . Using these APIs requires a great deal of effort
from developers who also have to consider the different Android platforms .
Location
GeofencingActivity Recognition
Apps
Fused Location Manager
Platform
Platform Components
Hardware
Sensors
GPS
WiFi
Cellular
Figure 13-2 The fused Location Manager combines sensors, GPS, Wi-Fi, and cellular into a single API for
location-based applications .
There are several advantages to using the new approach, but the most obvious one is that Google can provide
updates and fixes for bugs that otherwise would have to be rolled out by each handset manufacturer. Also, it
allows you to build advanced maps and location-enabled applications on older devices, as long as they have
Google Play Services installed. For details on how to verify that Google Play Services is installed and working
correctly, refer to Chapter 19.
Google Maps v2 Integration
To integrate the Google Maps API on Android, you need to create a new project in the Google API Console at
https://code.google.com/apis/console. You should create a new project here for each Android
application that will use the Google Play Services APIs (see Figure 13-3). (I go into more detail about how to
work with the API console and Google Play Services in Chapter 19.) The first thing you need to do after you
create the project is to enable the Google Maps Android API v2 under Services and create a new Android key
under API Access.
You generate the key by entering the SHA1 value for the key used for signing your APK, as shown in Figure
13-4. Usually you will have two keys. One is used during development (debug key) that is usually shared among
multiple applications and considered insecure (usually found under $HOME/.android/debug.keystore).
The second one is your release key, which you should always keep secure. I recommend that you create two API
keys for each application, one for the development version and one for the release version.
You get the SHA1 by using the keytool in the terminal, as shown here with the relevant text shown in bold:
$ keytool -list -v -keystore ~/.android/debug.keystore -alias
androiddebugkey
-storepass android -keypass android
Alias name: androiddebugkey
Creation date: May 15, 2013
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=Android Debug, O=Android, C=US
Issuer: CN=Android Debug, O=Android, C=US
Serial number: 5193e7a1
Valid from: Wed May 15 21:53:05 CEST 2013 until: Fri May 08 21:53:05 CEST
2043
Certificate fingerprints:
MD5: 09:AF:DA:16:0B:94:71:67:61:5B:C3:7D:9E:12:53:A0
SHA1: A0:D6:9F:F7:6E:F0:AD:B3:70:0B:74:91:13:E8:57:C3:C9:80:6D:EA
Signature algorithm name: SHA1withRSA
Version: 3
Figure 13-3 The Services section in Google APIs Console where you enable Google Maps Android API v2
Figure 13-4 Creating a new Android key for the Google Maps API access
Copy the text and paste it into the dialog for configuring a new Android key on the API Console (as shown in
Figure 13-4). After the SHA1 value, you append a semicolon followed by the package name for your application.
When you click Create, the key will be generated and you can copy it into your manifest. This process is covered
in more detail in Chapter 19.
You need to add the API key as a metadata element in the manifest, as shown in the following example, with the
API key shown in bold.
<application
android:allowBackup=”true”
android:icon=”@drawable/ic_launcher”
android:label=”@string/app_name”
android:theme=”@style/AppTheme” >
...
<meta-data
android:name=”com.google.android.maps.v2.API_KEY”
android:value=” AIzaSyCXNWDY7nx-_0vnwMW-6mXryYD5BTblyVM”/>
</application>
Working with Google Maps
Working with the new (version 2) Google Maps API is simple and straightforward. In the following example, you
see how Java code adds Google Maps to your application:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mMapFragment = (MapFragment) getFragmentManager().
findFragmentById(R.id.map);
GoogleMap map = mMapFragment.getMap();
map.setTrafficEnabled(true);
CameraPosition cameraPosition = new CameraPosition.Builder().
target(MY_HOME).
zoom(17).
bearing(90).
tilt(30).build();
map.animateCamera(CameraUpdateFactory.
newCameraPosition(cameraPosition));
map.setMapType(GoogleMap.MAP_TYPE_NORMAL);
map.setMyLocationEnabled(true);
map.setIndoorEnabled(true);
}
The GoogleMap object is the central piece when working with the maps. In this example, you also move the
camera for the map to MY_HOME (a predefined LatLng constant) and tell the GoogleMap object to enable
“My location” on the map as well as indoor positioning and maps.
The following XML layout is an example of how you can define the initial map in an XML layout:
<fragment xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:map=”http://schemas.android.com/apk/res-auto”
android:id=”@+id/map”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:name=”com.google.android.gms.maps.MapFragment”
map:cameraBearing=”112.5”
map:cameraTargetLat=”55.59612590”
map:cameraTargetLng=”12.98140870”
map:cameraTilt=”30”
map:cameraZoom=”13”
map:mapType=”normal”
map:uiCompass=”true”
map:uiRotateGestures=”true”
map:uiScrollGestures=”true”
map:uiTiltGestures=”true”
map:uiZoomControls=”true”
map:uiZoomGestures=”true”/>
You’ll find this using the XML layout useful if your application needs to display a default location on the maps
because the values here can be localized as any other resource can.
To interact with a GoogleMap object, you can register a number of listeners for certain events.
Markers on Maps
A Marker is the simplest thing you can add to a GoogleMap object (see Figure 13-5). You simply need a
LatLng object with the coordinates of its position.
The following code shows how to add a new Marker to a GoogleMap object.
mMap.addMarker(new MarkerOptions().
position(latLng).
title(mMarkerTitle).
snippet(mMarkerSnippet).
icon(mMarkerDrawable));
The only required attribute for a marker is the position. It is also possible to define a draggable Marker
that the user can move around. You do so by calling draggable(true) on the MarkerOptions
object and ensuring that you’ve set the Marker drag listener on the map with GoogleMap.
setOnMarkerClickListener().
Figure 13-5 Placing markers on a map . Note the marker
with the custom icon .
Drawing Circles
Drawing circles on a GoogleMap is done in the same way as placing Markers.
Following is an example of how to use the onMapClick() callback on a GoogleMap to place a red circle with
the radius of 40 meters on the map where the user taps (see the result in Figure 13-6).
public void onMapClick(LatLng latLng) {
mMap.addCircle(new CircleOptions().
radius(40).
center(latLng).
fillColor(Color.RED).
strokeColor(Color.BLACK).
strokeWidth(6));
}
Figure 13-6 A Google map with a number of circles
Drawing Polygons
Polygons are complex geometrical objects formed by a number of points. You can use polygons to cover an
area of a map (as shown in Figure 13-7).
Figure 13-7 A Google map with a number of different
polygons drawn on top
The following is a simple example where a quick tap on the maps adds a new point for the polygon and where a
long tap completes the polygon:
@Override
public void onMapClick(LatLng latLng) {
if (mNewPolygon == null) {
mNewPolygon = new PolygonOptions().add(latLng);
} else {
mNewPolygon.add(latLng);
}
}
@Override
public void onMapLongClick(LatLng latLng) {
Log.d(TAG, “Closing polygon at “ + latLng.toString());
mNewPolygon.add(latLng).
fillColor(getColor()).
strokeColor(Color.BLACK).
strokeWidth(6);
mMap.addPolygon(mNewPolygon);
mNewPolygon = null;
mDrawState = null;
}
Useful Location API Utilities
A common operation in location-based applications is to find out whether a certain location (latitude and
longitude) is close to another location.
The following method takes two LatLng parameters and returns the approximate distance between them in
meters.
public float distanceBetween(LatLng latLng1, LatLng latLng2) {
float[] results = new float[3];
Location.distanceBetween(latLng1.latitude,
latLng1.longitude,
latLng2.latitude,
latLng2.longitude,
results);
return results[0];
}
Another operation common for location-based applications is to determine if you are inside a certain area or
not. The most common case is to define an area as a rectangle formed by two coordinates. However, sometimes
you have a number of coordinates around which you first want to form the smallest bounding rectangle.
public LatLngBounds getBoundsForPoints(List<LatLng> coordinates) {
LatLngBounds.Builder builder = LatLngBounds.builder();
for (LatLng coordinate : coordinates) {
builder.include(coordinate);
}
return builder.build();
}
public boolean isWithinBound(LatLng latLng, LatLngBounds bounds) {
return bounds.contains(latLng);
}
The two preceding methods demonstrate how you can use the class LatLngBounds to create a rectangle
around a number of points. You can then use the LatLngBounds.contains() method to determine
whether another LatLng object is contained within that rectangle.
Geocoding
At this point you should be familiar with the GoogleMaps class and how to work with locations in Android
using the new Location API. However, locations are defined as latitude and longitude, which is rarely useful for
users of your application. Most users are more familiar with streets, city names, and names of places. To find a
useful name for a certain latitude and longitude, Android provides the Geocoder class:
public static Address getAddressForLocation(Context context, Location
location) {
try {
Geocoder geocoder = new Geocoder(context);
List<Address> addresses = geocoder.
getFromLocation(location.getLatitude(),
location.getLongitude(),
1);
if(addresses != null && addresses.size() > 0) {
return addresses.get(0);
} else {
return null;
}
} catch (IOException e) {
return null;
}
}
public String getStreetNameForAddress(Address address) {
String streetName = address.getAddressLine(0);
if(streetName == null) {
streetName = address.getThoroughfare();
}
return streetName;
}
The first method shows how to retrieve the first matching Address for a certain Location. There can be
multiple matches, so one alternative could be to provide the user with a list from which they can select the
address that matches.
The Address object is a bit vague on what the different fields represent because they can vary depending on
your country and location.
The second method usually gives you the current street name. However, always assume that the fields returned
from an Address object can be null.
Using the LocationClient
The new fused Location Manager is accessed through the LocationClient class. This class is used to listen
for location updates and to perform geofencing operations. I recommend that you create this instance in
onCreate() and release the API in onDestroy().
Initialize the Location API as shown in the following code:
@Override
protected void onCreate() {
super.onCreate();
setContentView(R.layout.activity_main);
MapFragment mapFragment = (MapFragment) getFragmentManager().
findFragmentById(R.id.map);
mMap = mapFragment.getMap();
mLocationCallbacks = new MyLocationCallbacks();
mLocationClient = new LocationClient(this,
mLocationCallbacks, mLocationCallbacks);
mLocationClient.connect();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mLocationClient != null && mLocationClient.isConnected()) {
mLocationClient.disconnect();
mLocationClient = null;
}
}
This is an asynchronous API, so you need to register the relevant callbacks as well. Remember to disconnect the
LocationClient in the correct callback as well:
private class MyLocationCallbacks
implements GooglePlayServicesClient.ConnectionCallbacks,
GooglePlayServicesClient.OnConnectionFailedListener,
LocationListener {
@Override
public void onConnected(Bundle bundle) {
LocationRequest locationRequest = new LocationRequest();
locationRequest.setSmallestDisplacement(TWENTYFIVE_METERS);
locationRequest.setExpirationDuration(FIVE_MIUTES);
mLocationClient.requestLocationUpdates(locationRequest, this);
}
@Override
public void onDisconnected() {
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
// TODO Error handling...
}
@Override
public void onLocationChanged(Location location) {
LatLng latLng = new LatLng(location.getLatitude(),
location.getLongitude());
CameraPosition cameraPosition = new CameraPosition.Builder()
.target(latLng)
.zoom(17)
.bearing(90)
.tilt(30)
.build();
mMap.animateCamera(CameraUpdateFactory.
newCameraPosition(cameraPosition));
}
}
The callback just shown requests location updates every 25 meters for the next 5 minutes. After you receive
the call to onConnected(), you cannot perform any operations on the LocationClient. Also, it’s good
practice to set the expiration duration for your LocationRequest to a low number and renew the request if
needed. Doing so reduces the risk of unnecessary battery consumption.
When a callback to onLocationChanged() is triggered, you update the camera position for the
GoogleMap object. In this example, you perform an animation of the camera movement to the new location,
which is an efficient way of displaying the map as soon as possible and moving the camera as soon as the
asynchronous location request is complete.
Geofencing
One of the best features of the new Location API for Android is geofencing. Basically, geofencing allows you
to define a circle that represents a virtual fence around a specific location (longitude and latitude). Whenever
the device comes within the area of that circle, it will notify your application. A Geofence is defined by the
latitude, longitude, and a radius in meters, which is then registered using the new Location API with expiration
time, transition type (“enter” or “exit”) and an ID.
The following is the callback method you can set for long taps on a GoogleMap object:
public void onMapLongClick(LatLng latLng) {
Geofence.Builder builder = new Geofence.Builder();
builder.setCircularRegion(latLng.latitude,
latLng.longitude,
TWENTYFIVE_METERS);
builder.setExpirationDuration(ONE_WEEK);
// For now, use the lat/long as ID.
String geofenceRequestId = latLng.latitude + “,”
+ latLng.longitude;
builder.setRequestId(geofenceRequestId);
// Only interested in entering the geofence for now...
builder.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER);
List<Geofence> geofences = new ArrayList<Geofence>();
geofences.add(builder.build());
Intent intent = new Intent(MyIntentService.
ACTION_NOTIFY_ENTERED_GEOFENCE);
PendingIntent pendingIntent = PendingIntent.getService(this,
1001,
intent,
0);
mLocationClient.addGeofences(geofences, pendingIntent,
new LocationClient.OnAddGeofencesResultListener() {
@Override
public void onAddGeofencesResult(int status,
String[] strings) {
if (status == LocationStatusCodes.SUCCESS) {
double latitude = Double.parseDouble(strings[0])
double longitude = Double.parseDouble(strings[0])
LatLng latLng = new LatLng(latitude, longitude);
Circle circle = mMap.addCircle(new
CircleOptions().
fillColor(Color.GREEN).
strokeWidth(5).
strokeColor(Color.BLACK).
center(latLng).
visible(true).
radius(TWENTYFIVE_METERS));
mGeoReminders.add(circle);
} else {
// TODO: Error handling...
}
}
});
}
Here, you add a new Geofence at the position where the user performs a long tap. As with the other
operations on the LocationClient, this is an asynchronous call. After the callback is triggered, you check
the result and if the addition of the new Geofence was a success, you create a circle on the GoogleMap
object indicating where the Geofence is registered.
This example shows how to combine drawing objects on top of a GoogleMap with the Geofence API. The
PendingIntent in the preceding example contains information about the Geofences that was triggered.
The following IntentService will, in this case, show a notification to the user that a Geofence has been
triggered:
public class MyIntentService extends IntentService {
public static final String TAG = “MyIntentService”;
public static final String ACTION_NOTIFY_ENTERED_GEOFENCE =
“com.aptl.locationandmapsdemo.NOTIFY_ENTER_
GEOFENCE”;
private int mNextNotificationId = 1;
public MyIntentService() {
super(TAG);
}
@Override
protected void onHandleIntent(Intent intent) {
String action = intent.getAction();
if (LocationClient.hasError(intent)) {
// TODO: Error handling...
} else {
List<Geofence> geofences =
LocationClient.
getTriggeringGeofences(intent);
for (Geofence geofence : geofences) {
showNotification(geofence);
}
}
}
private void showNotificat(Geofence geofence) {
// TODO: Show notification to user
}
}
Activity Recognition
The final part of the new Location API is Activity Recognition. This feature is controlled by the class
ActivityRecognitionClient that works much as the LocationClient does.
In the following example, you can see a simple Activity that initiates and connects to the
ActivityRecognitionClient:
public class ActivityRecognition extends Activity implements
GooglePlayServicesClient.ConnectionCallbacks,
GooglePlayServicesClient.OnConnectionFailedListener {
private static final long THIRTY_SECONDS = 1000 * 30;
private static final long FIVE_SECONDS = 1000 * 5;
private boolean mActivityRecognitionReady = false;
private ActivityRecognitionClient mActivityRecognitionClient;
private PendingIntent mPendingIntent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mActivityRecognitionReady = false;
setContentView(R.layout.activity_recognition);
mActivityRecognitionClient =
new ActivityRecognitionClient(this, this, this);
mActivityRecognitionClient.connect();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mActivityRecognitionClient != null
&& mActivityRecognitionClient.isConnected()) {
mActivityRecognitionClient.disconnect();
mActivityRecognitionClient = null;
}
}
public void doStartActivityRecognition(View view) {
if (mActivityRecognitionReady) {
Intent intent = new Intent(MyIntentService.
ACTION_NOTIFY_ACTIVITY_DETECTED);
mPendingIntent =
PendingIntent.getService(this, 2001, intent, 0);
mActivityRecognitionClient.
requestActivityUpdates(FIVE_SECONDS,
mPendingIntent);
}
}
@Override
public void onConnected(Bundle bundle) {
mActivityRecognitionReady = true;
findViewById(R.id.start_activity_recognition_btn).
setEnabled(false);
}
@Override
public void onDisconnected() {
mActivityRecognitionReady = false;
findViewById(R.id.start_activity_recognition_btn).
setEnabled(true);
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
mActivityRecognitionReady = false;
findViewById(R.id.start_activity_recognition_btn).
setEnabled(true);
// Error handling...
}
}
The click listener (doStartActivityRecognition()) starts the request for activity (not to be confused
with the Activity component) changes and provides a PendingIntent that will be sent to the same
IntentService as shown earlier, but with modifications for handling activity changes as well. When
requesting activity changes, a frequency of 5 seconds is selected. You adapt this frequency depending on the
needs of your application. The higher the frequency, the more battery your application will consume.
protected void onHandleIntent(Intent intent) {
String action = intent.getAction();
if (ACTION_NOTIFY_ACTIVITY_DETECTED.equals(action)){
if (ActivityRecognitionResult.hasResult(intent)) {
ActivityRecognitionResult result = ActivityRecognitionResult.
extractResult(intent);
DetectedActivity detectedActivity =
result.getMostProbableActivity();
Log.d(TAG, “Detected activity: “ + detectedActivity);
if(detectedActivity.getType() != mLastDetectedActivity) {
mLastDetectedActivity = detectedActivity.getType();
showNotification(detectedActivity);
}
}
} else if (ACTION_NOTIFY_ENTERED_GEOFENCE.equals(action)) {
... Geofencing detection showed in previous section...
}
}
private void showNotification(DetectedActivity detectedActivity) {
Notification.Builder builder = new Notification.Builder(this);
builder.setContentTitle(“Activity change!”);
builder.setContentText(“Activity changed to: “
+ getActivityName(detectedActivity.getType()));
builder.setSmallIcon(R.drawable.ic_launcher);
NotificationManager manager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.notify(2001, builder.build());
}
private String getActivityName(int type) {
switch (type) {
case DetectedActivity.IN_VEHICLE:
return “In vehicle”;
case DetectedActivity.ON_BICYCLE:
return “On bicycle”;
case DetectedActivity.ON_FOOT:
return “On foot”;
case DetectedActivity.STILL:
return “Still”;
}
return “Unknown”;
}
The three preceding methods show how the IntentService listens to both geofencing and activity
changes. When an activity change occurs, the result is retrieved through ActivityRecognitionResult.
extractResult(). From that point, you can inspect the type and the confidence of the detection result.
This API can provide you with very accurate information about the users’ behavior and the context they are in
at the moment. If a user is on a bicycle or in a vehicle, you could adapt the interface of the application and rely
more on speech recognition and text-to-speech. If the device reports that it is still for a while, you can make
further assumptions and adapt the behavior of your application accordingly.
Summary
The new Location API provided by Google doesn’t enable anything that Android developers didn’t have
access to before. There has always been a Location API in the Android platform, and there have been third-
party libraries that estimate the users’ activity by reading the sensors. However, with the new API, you have an
easy-to-use solution that works across all Android devices and is guaranteed to work as long as the device has
Google Play Services installed. The new API is very easy to use and can easily be integrated into any existing
application with little effort.
By combining the new Maps and Location API with Geofencing and Activity Recognition, you have a set of
really powerful tools for building context- and location-aware applications that otherwise could take months to
develop. You can now build new types of location-based social games much easier than ever possible before.
When your application is aware of the current context for the users, you can create a much richer user
experience and provide a user interface that varies depending on the situation.
However, beware of overusing these APIs because they do have an impact on battery consumption. Requesting
too frequent updates will consume a lot of power. Experiment and see what rate suits your application best.
Further Resources Documentation
Google Maps Android API v2: https://developers.google.com/maps/documentation/android
Websites
The Google APIs Console for setting up API access and new keys for your project: https://code.google.com/apis/console
Developer guidelines for location-aware applications: http://developer.android.com/training/location/index.html