Whether for safety or for convenience, location-based features on cell phones are mostly standard these days. As such, incorporating location information, navigation, and mapping features into your project can make your application much more robust.
In this chapter, you learn how to leverage location-based services available within the Android SDK. You learn how to determine the location of the handset using a particular device hardware provider, such as a built-in Global Positioning Systems (GPS) unit. You also learn how to translate raw location coordinates into descriptive location names—and how to do the reverse. Finally, we explore a couple of different methods for mapping and utilities that work with the maps.
The Android Software Development Kit (SDK) provides means for accessing location via a built-in GPS device, when such hardware is available. If a GPS device isn’t available, the Application Programming Interfaces (API) also provides for alternate location providers. These other providers might have advantages and disadvantages in terms of power use, speed, and accuracy of reporting.
To determine device location, you need to perform a few steps and make some choices. The following list summarizes this process:
LocationManager
using a call to the getSystemService()
method using the LOCATION_SERVICE
.AndroidManifest.xml
file, depending on what type of location information the application needs.getAllProviders()
method or the getBestProvider()
method.LocationListener
class.requestLocationUpdates()
method with the chosen provider and the LocationListener
object to start receiving location information.Specific permissions are not needed to retrieve an instance of the LocationManager
object. Instead, the permissions determine the available providers. The following code retrieves an instance of the LocationManager
object:
The following block of XML provides the application with both coarse and fine location permissions when added within the AndroidManifest.xml
permissions file:
Now that the application has permissions to use location information and the LocationManager
object is valid, we must determine what provider to use for location information. The following code configures a Criteria
object and requests the provider based on this information:
The setAccuracy()
method can take values for ACCURACY_COARSE
and ACCURACY_FINE
that can be used (along with the appropriate permissions) to request a provider that the application has permissions to use. The setPowerRequirement()
method can be used to find a provider that fits certain power use requirements, such as POWER_HIGH
or POWER_LOW
. The Criteria
object also enables us to specify if the provider can incur a monetary cost to the user, whether altitude is needed, and some other details. If the application has specific requirements, this is where they can be set. However, setting these criteria doesn’t imply that the provider will be available to the user. Some flexibility might be required to allow use on a broad range of devices. A Boolean
parameter of the getBestProvider()
method enables the application to ask for only enabled providers.
Using the provider returned by the getBestProvider()
method, the application can request the location. Before doing so, however, the application needs to provide an implementation of LocationListener
. The LocationListener
object consists of four methods: two tell the application if the provider has been disabled or enabled; one gives the status about the provider (such as the number of satellites the GPS receiver can see); and the last tells the application location information. The following is a sample implementation for the last method, the onLocationChanged()
method:
The onLocationChanged()
method receives a Location
object with the most recent location information from the chosen provider. In this example, the application merely prints out the location, including the altitude, which might or might not be returned by the provider. Then, it uses a utility method of the Location
object, distanceTo()
, to calculate how far the handset has moved since the last time onLocationChanged()
was called.
It is up to the application to determine how to use this location information. The application might want to turn the location information into an address, display the location on an embedded map, or launch the built-in map application centered at the location.
The Android emulator can simulate location-based services, but as you would expect, it does not have any “underlying hardware” to get a real satellite fix. The Android SDK provides a means to simulate location data with the use of a single location point, GPX file, or KML file. This works only with the emulator, not the physical handset, but it can be useful for testing your location-based application.
For more information on this, see Appendix A, “The Android Emulator Quick-Start Guide.”
Determining the latitude and longitude is useful for precise location, tracking, and measurements; however, it’s not usually descriptive to users. The Android SDK provides some helper methods to turn raw location data into addresses and descriptive place names. These methods can also work in reverse, turning place names or addresses into raw location coordinates.
The Geocoder
object can be used without any special permissions. The following block of code demonstrates using the Geocoder
object to get the location names of a Location
object passed in to the onLocationChanged()
method of a LocationListener
:
You can extract information from the results of the call to the getFromLocation()
method in two ways, both of which are demonstrated. Note that a particular location might have multiple Address
results in the form of a List<Address>
object. Typically, the first Address
is the most detailed, and the subsequent Address
objects have less detail and describe a broader region.
The first method is to query for specific information, such as by using the getFeatureName()
method or the getLocality()
method. These methods are not guaranteed to return useful information for all locations. They are useful, though, when you know you need only a specific piece of general information, such as the country.
The second method for querying information is by “address lines.” This is generally used for displaying the “address” of a location to the user. It might also be useful to use the location in directions and in other cases where a street address is desired. That said, the addresses returned might not be complete. Simply call the getAddressLine()
method and iterate through the values returned by getMaxAddressLineIndex()
. Figure 11.1 shows a sample location with three resulting addresses.
The Geocoder
object also supports using named locations or address lines to generate latitude and longitude information. The input is forgiving and returns reasonable results in most cases. For instance, all the following returns valid and correct results, “Eiffel Tower,” “London, UK,” “Iceland,” “BOS,” “Yellowstone,” and “1600 Pennsylvania Ave, DC.”
The following code demonstrates a button handler for computing location data based on user input of this kind:
The result of the call to the getFromLocationName()
method is a List
of Address
objects, much like the previous example. Figure 11.2 shows the results for entering “Eiffel Tower.”
We have never actually seen more than one result, even when entering ambiguous place names (such as “Dublin”). However, this behavior is not guaranteed. Providing a picker for the user to choose the best location would certainly enhance the application. Another good way to confirm with the user that they entered the correct location is to map it. We now discuss a couple of different methods for mapping locations using Google Maps.
The Android SDK provides two different methods to show a location with Google Maps. The first method is to use a location Uri
to launch the built-in Google Maps application with the specified location. The second method is to use a MapView
embedded within your application to display the map location.
In the previous section, we demonstrated how to determine the latitude and longitude for a place name. Now we map the location using the built-in maps application. The following block of code demonstrates how to perform this:
The first task is to create a String
that conforms to the URI handled by the mapping application. In this case, it’s “geo:
” followed by the latitude and longitude. This URI is then used to create a new Uri
object for creating a new ACTION_VIEW Intent
. Finally, we call the startActivity()
method. If the latitude and longitude are valid, such as the location for the Parthenon entered in the previous example, the screen would look like Figure 11.3.
Using this method of mapping launches the user into a built-in mapping application, in this case Google Maps. If the application does not want to bother with the details of a full mapping application or does not need to provide any further control over the map, this is a fast-and-easy method to use. Users will typically be accustomed to the controls of the mapping application on their handset, too.
Sometimes, though, we want to have the map integrated into our application for a more seamless user experience. Let’s add a small map to our geocoding example to show the location immediately to the users when they enter a place name.
The following block of XML shows the change needed within the layout file to include a widget called the MapView
:
As you might have already noticed, the MapView
XML is a little different. First, the tag name is the fully qualified name. And second, an apiKey
attribute is needed. We get to the key in a moment.
The AndroidManifest.xml
file also needs to be modified to allow for using the MapView
with Google Maps. Here are the two changes needed:
Both of these permission lines are required. The MapView
object specifically requires the INTERNET
permission and its library must be reference explicitly. Otherwise, an error will occur.
Finally, a MapView
can be used only within a MapActivity
. Accessing a MapView
from outside a MapActivity
results in an error. The MapActivity
is similar to a normal Activity
, but it requires implementing the isRouteDisplayed()
method. This method must return true if a route will be displayed. Otherwise, false must be returned. Here is the default implementation for when no route is displayed:
Now the application can use the MapView
to display locations to the user. The following block of code demonstrates retrieval of a MapController
object, which is used to control the location that the MapView
displays:
These lines of code set the display to show the satellite view, which is visually interesting. The MapController
object then sets the zoom level of the map. Larger values are zoomed in farther, with 1 zoomed all the way out. The given value, 17, usually shows a few city blocks, but there are some areas where even this is too close for the data available. In a moment we talk about how to easily give control of this to the user.
Building on the previous example, the following lines of code are added to the button handler for geocoding a place name:
In this case, we create a new GeoPoint
to use with the animateTo()
method. A GeoPoint
object uses microdegrees, so we must multiply the result of the geocoding by 1E6
(1,000,000 or one million). The animateTo()
method smoothly animates the MapView
to the new location. How much of the interim mapping data displays depends on the speed of the Internet connection and what mode the MapView
is in. The setCenter()
method can set the center of the map.
Finally, this is almost enough to test the results. However, there is one last thing that needs to be taken care of. You need to get a Google Maps API Key from Google to use its API and mapping services.
To use a MapView
within your applications, you must obtain a Google Maps API Key from Google. The key is generated from an MD5 fingerprint of a certificate that you use to sign your applications.
For production distribution, you need to follow these steps, substituting your release distribution signing certificate. You can read more about this in Chapter 20, “Selling Your Android Application.” For testing purposes, you can use the debug certificate that is created by the Android SDK for your use.
The following steps need to be performed to generate the appropriate API key:
The first step is performed on your development machine. Locate the debug certificate used by the Android SDK. On all platforms, the filename is debug.keystore
by default. If you use Eclipse, the location of the file is listed under the Android Build preferences. Using this file, you then need to execute the following command (make sure the Java tools are in your path):
keytool -list -keystore /path/to/debug.keystore
The result is the fingerprint that you must paste into the form on step 4. Read the terms of service carefully before proceeding. Although the terms allow many types of applications, you need to make sure your application will be allowed and that your anticipated usage will be acceptable to Google.
When you have successfully completed the steps to get your key, you can then reference your map key in the Layout
file definition for the MapView
you use. Now, when you execute the code, you should be presented with a screen that looks like Figure 11.4.
If you work on multiple development machines or work as part of a team, you need to have an API key for everyone’s debug certificate. Alternating, the debug certificate from one machine can be copied to other machines so that the signing and check against the Android Maps API key is successful. This can save time by not having to modify the code or layout files for each developer on the team.
Sometimes the locations returned either do not show the exact location that the user wanted or the user might want to determine where in the world they are by exploring the map a bit. One way to do this is through panning the map. Luckily, this is as easy as enabling clicking from within the layout file:
Now, if users were to search for “The Great Pyramid,” they could then pan south a tad and see the results, as shown in Figure 11.5.
Other times, panning around won’t help the users. They might want to zoom in or out from the same location. Our application does not have to reimplement the zoom controls, though. They are provided in the Android SDK 1.5 through a single call.
map.setBuiltInZoomControls(true);
When the user clicks on the map, the zoom controls fade in to view and are functional, as shown in Figure 11.6.
Now that users can pan and zoom around, they might lose their position. Sure, they can just search again. Wouldn’t it be more interesting, though, to mark the spot for them? The Android SDK provides a few different ways to do this. One way is to use the MapView
as a container for an arbitrary View
object that can be assigned using a GeoPoint
instead of typical screen or View
coordinates. Another way is to use ItemizedOverlay
, which is especially useful if you have more than one place to mark. Finally, you can manually draw items over the map using the Overlay
and implement the onDraw()
method.
For the place name finder example, we use the first method. Assuming you have a suitable map marker as a drawable resource, the following code demonstrates how to do this:
The MapView
layout parameters enable you to set a GeoPoint
. Doing this allows the added View
to stay put at a geographic location and pan with the map, as shown in Figure 11.7.
Keep in mind that the added View
sticks around as long as the MapView
does. If the application needs to present multiple locations to the user, though, there is a simpler way. Just use the ItemizedOverlay
object.
In this example, a static ItemizedOverlay
will be created to represent the chain of backpacker huts in the White Mountains along the Appalachian Trail:
To do this, we provide implementations for each of the required methods of ItemizedOverlay<OverlayItem>
. First, we define the constructor:
The Drawable
passed in is one that we define later in the onCreate()
method of MapActivity
. The system does not provide a default marker. The call to the boundCenterBottom()
method is made so that the shadow will be cast from the bottom of the marker, which is a more natural look. The default shadow is from the top. If, however, we’d rather turn off the shadow completely, the draw()
method could be overridden, as follows:
Finally, within the constructor we call the populate()
method. This should be done as soon as the location data is available. Because we have it statically compiled into the application, we call it before returning. The populate()
method calls our implementation of the createItem()
method for as many items as we defined in our implementation of the size()
method. Here is the implementation of our createItem()
method, along with a small array of hut locations, in no particular order:
In the array, we’ve multiplied all the location values by one million so that they are in microdegrees, as required by the GeoPoint
object. Within the createItem()
method, the location array is indexed with the passed in value. Neither of the two text fields, Title
and Snippet
, are used at this time, so they are set to null. The maximum index value is determined by the size()
method, which, in this case, merely has to return the length of the array:
The necessary ItemizedOverlay<OverlayItem>
class is now implemented. Next, the application needs to tell the MapView
about it. The following code demonstrates how to do this in the onCreate()
method of our MapActivity
:
First, the Drawable
is retrieved from the resources. We call setBounds()
on it to force it to draw correctly as part of an ItemizedOverlay
. Without this, the markers will not draw. Next, we instantiate the HutsItemizedOverlay
object. The OverlayItems
in it need to be added to the ones that might already exist within the MapView
. The getOverlays()
method of MapView
returns a list of the current Overlay
objects. Calling the add()
method on this list inserts our new ones for each hut. Finally, the zoom controls are added to the MapView
so that the user can zoom in and out. After launching this application and zooming in on New Hampshire, the user should see a screen like Figure 11.8.
Forcing the user to pan and zoom to the location of the huts is not user-friendly. Two of the utility methods that the ItemizedOverlay<OverlayItem>
class provides returns values for the span of the location of the items. Combining this functionality with an override to the default behavior of the getCenter()
method, which normally returns the location of the first item, enables the map to start to draw at a convenient zoom level covering all the huts. This block of code can be added to the onCreate()
method to do just that:
The getCenter()
method computes the average latitude and the average longitude across all the given hut locations. A central point could be provided, or the first item could be placed near the center of all the points requiring no override of the getCenter()
method.
You have been introduced to a number of different location tools provided on Android; however, you should be aware of several more.
The LocationManager
supports Proximity Alerts, which are alerts that trigger a PendingIntent
when the handset comes within some distance of a location. This can be useful for warning the user of an upcoming turn in directions, for scavenger hunts, or help in geocaching.
You saw how to do ItemizedOverlays
. In general, you can assign your own Overlays
to draw custom objects and Views
on the given Canvas
. This is useful for drawing pop-up information for locations, putting logos over the map that don’t move with the map, or putting hints for scavenger hunts over the map. This functionality is similar to displaying photos at a given location, which are often provided on Google Maps at famous locations.
The Android SDK, with Google Maps support available to applications that register for a key, can be used to enhance Android applications with location-rich information. Some applications want to build in seamless map support, whereas others might just launch the built-in map application for the user to leverage. Developers can add to the information provided on the map by using various types of overlays to include even more information to the user. The opportunities for using location-based services to improve Android applications are only just beginning to be explored.
Google Maps API Key: http://code.google.com/android/add-ons/google-apis/mapkey.html
3.147.103.227