Maps, Location, and Activity APIs - Pushing the Limits - Android Programming: Pushing the Limits (2014)

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

image

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

image

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.

image

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 .

image

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

image

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