This lesson teaches you how to
- Get a Geographic Location
- Define an Intent Service to Fetch the Address
- Start the Intent Service
- Receive the Geocoding Results
You should also read
Try it out
The lessons Getting the Last Known
Location and Receiving Location
Updates describe how to get the user's location in the form of a
Location
object that contains latitude and longitude
coordinates. Although latitude and longitude are useful for calculating
distance or displaying a map position, in many cases the address of the
location is more useful. For example, if you want to let your users know where
they are or what is close by, a street address is more meaningful than the
geographic coordinates (latitude/longitude) of the location.
Using the Geocoder
class in the Android framework
location APIs, you can convert an address to the corresponding geographic
coordinates. This process is called geocoding. Alternatively, you can
convert a geographic location to an address. The address lookup feature is
also known as reverse geocoding.
This lesson shows you how to use the
getFromLocation()
method to
convert a geographic location to an address. The method returns an estimated
street address corresponding to a given latitude and longitude.
Get a Geographic Location
The last known location of the device is a useful starting point for the
address lookup feature. The lesson on
Getting the Last Known Location shows you
how to use the
getLastLocation()
method provided by the
fused
location provider to find the latest location of the device.
To access the fused location provider, you need to create an instance of the Google Play services API client. To learn how to connect your client, see Connect to Google Play Services.
In order for the fused location provider to retrieve a precise street
address, set the location permission in your app manifest to
ACCESS_FINE_LOCATION
, as shown in the following example:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.google.android.gms.location.sample.locationupdates" > <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> </manifest>
Define an Intent Service to Fetch the Address
The getFromLocation()
method provided by the Geocoder
class accepts a
latitude and longitude, and returns a list of addresses. The method is
synchronous, and may take a long time to do its work, so you should not call
it from the main, user interface (UI) thread of your app.
The IntentService
class provides a
structure for running a task on a background thread. Using this class, you can
handle a long-running operation without affecting your UI's responsiveness.
Note that the AsyncTask
class also allows you to
perform background operations, but it's designed for short operations. An
AsyncTask
shouldn't keep a reference to the UI if
the activity is recreated, for example when the device is rotated. In
contrast, an IntentService
doesn't need to
be cancelled when the activity is rebuilt.
Define a FetchAddressIntentService
class that extends
IntentService
. This class is your address lookup service.
The intent service handles an intent asynchronously on a worker thread, and
stops itself when it runs out of work. The intent extras provide the data
needed by the service, including a Location
object
for conversion to an address, and a ResultReceiver
object
to handle the results of the address lookup. The service uses a Geocoder
to fetch the address for the location, and sends
the results to the ResultReceiver
.
Define the Intent Service in your App Manifest
Add an entry to your app manifest defining the intent service:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.google.android.gms.location.sample.locationaddress" > <application ... <service android:name=".FetchAddressIntentService" android:exported="false"/> </application> ... </manifest>
Note: The <service>
element in
the manifest doesn't need to include an intent filter, because your main
activity creates an explicit intent by specifying the name of the class to use
for the intent.
Create a Geocoder
The process of converting a geographic location to an address is called
reverse geocoding. To perform the main work of the intent service,
that is, your reverse geocoding request, implement
onHandleIntent()
within the
FetchAddressIntentService
class. Create a
Geocoder
object to handle the reverse geocoding.
A locale represents a specific geographical or linguistic region. Locale
objects are used to adjust the presentation of information, such as numbers or
dates, to suit the conventions in the region represented by the locale. Pass a
Locale
object
to the Geocoder
object, to ensure that the resulting
address is localized to the user's geographic region.
@Override protected void onHandleIntent(Intent intent) { Geocoder geocoder = new Geocoder(this, Locale.getDefault()); ... }
Retrieve the street address data
The next step is to retrieve the street address from the geocoder, handle
any errors that may occur, and send the results back to the activity that
requested the address. To report the results of the geocoding
process, you need two numeric constants that indicate success or failure.
Define a Constants
class to contain the values, as shown in this code
snippet:
public final class Constants { public static final int SUCCESS_RESULT = 0; public static final int FAILURE_RESULT = 1; public static final String PACKAGE_NAME = "com.google.android.gms.location.sample.locationaddress"; public static final String RECEIVER = PACKAGE_NAME + ".RECEIVER"; public static final String RESULT_DATA_KEY = PACKAGE_NAME + ".RESULT_DATA_KEY"; public static final String LOCATION_DATA_EXTRA = PACKAGE_NAME + ".LOCATION_DATA_EXTRA"; }
To get a street address corresponding to a geographical location, call
getFromLocation()
,
passing it the latitude and longitude from the location object, and the
maximum number of addresses you want returned. In this case, you want just one
address. The geocoder returns an array of addresses. If no addresses were
found to match the given location, it returns an empty list. If there is no
backend geocoding service available, the geocoder returns null.
Check for the following errors as shown in the code sample below. If an error
occurs, place the corresponding error message in the errorMessage
variable, so you can send it back to the requesting activity:
- No location data provided - The intent extras do not
include the
Location
object required for reverse geocoding. - Invalid latitude or longitude used - The latitude
and/or longitude values provided in the
Location
object are invalid. - No geocoder available - The background geocoding service is not available, due to a network error or IO exception.
- Sorry, no address found - The geocoder could not find an address for the given latitude/longitude.
To get the individual lines of an address object, use the
getAddressLine()
method provided by the Address
class. Then join the
lines into a list of address fragments ready to return to the activity that
requested the address.
To send the results back to the requesting activity, call the
deliverResultToReceiver()
method (defined in
Return the address to the requestor). The
results consist of the previously-mentioned numeric success/failure code and
a string. In the case of a successful reverse geocoding, the string contains
the address. In the case of a failure, the string contains the error message,
as shown in the code sample below:
@Override
protected void onHandleIntent(Intent intent) {
String errorMessage = "";
// Get the location passed to this service through an extra.
Location location = intent.getParcelableExtra(
Constants.LOCATION_DATA_EXTRA);
...
List<Address> addresses = null;
try {
addresses = geocoder.getFromLocation(
location.getLatitude(),
location.getLongitude(),
// In this sample, get just a single address.
1);
} catch (IOException ioException) {
// Catch network or other I/O problems.
errorMessage = getString(R.string.service_not_available);
Log.e(TAG, errorMessage, ioException);
} catch (IllegalArgumentException illegalArgumentException) {
// Catch invalid latitude or longitude values.
errorMessage = getString(R.string.invalid_lat_long_used);
Log.e(TAG, errorMessage + ". " +
"Latitude = " + location.getLatitude() +
", Longitude = " +
location.getLongitude(), illegalArgumentException);
}
// Handle case where no address was found.
if (addresses == null || addresses.size() == 0) {
if (errorMessage.isEmpty()) {
errorMessage = getString(R.string.no_address_found);
Log.e(TAG, errorMessage);
}
deliverResultToReceiver(Constants.FAILURE_RESULT, errorMessage);
} else {
Address address = addresses.get(0);
ArrayList<String> addressFragments = new ArrayList<String>();
// Fetch the address lines using getAddressLine
,
// join them, and send them to the thread.
for(int i = 0; i < address.getMaxAddressLineIndex(); i++) {
addressFragments.add(address.getAddressLine(i));
}
Log.i(TAG, getString(R.string.address_found));
deliverResultToReceiver(Constants.SUCCESS_RESULT,
TextUtils.join(System.getProperty("line.separator"),
addressFragments));
}
}
Return the address to the requestor
The final thing the intent service must do is send the address back to a
ResultReceiver
in the activity that started the service.
The ResultReceiver
class allows you to send a
numeric result code as well as a message containing the result data. The
numeric code is useful for reporting the success or failure of the geocoding
request. In the case of a successful reverse geocoding, the message contains
the address. In the case of a failure, the message contains some text
describing the reason for failure.
You have already retrieved the address from the geocoder, trapped any errors
that may occur, and called the deliverResultToReceiver()
method. Now
you need to define the deliverResultToReceiver()
method that sends
a result code and message bundle to the result receiver.
For the result code, use the value that you've passed to the
deliverResultToReceiver()
method in the resultCode
parameter.
To construct the message bundle, concatenate the RESULT_DATA_KEY
constant from your Constants
class (defined in
Retrieve the street address data) and
the value in the message
parameter passed to the
deliverResultToReceiver()
method, as shown in the following sample:
public class FetchAddressIntentService extends IntentService { protected ResultReceiver mReceiver; ... private void deliverResultToReceiver(int resultCode, String message) { Bundle bundle = new Bundle(); bundle.putString(Constants.RESULT_DATA_KEY, message); mReceiver.send(resultCode, bundle); } }
Start the Intent Service
The intent service, as defined in the previous section, runs in the
background and is responsible for fetching the address corresponding to a
given geographic location. When you start the service, the Android framework
instantiates and starts the service if it isn't already running, and creates a
process if needed. If the service is already running then it remains running.
Because the service extends IntentService
,
it shuts down automatically when all intents have been processed.
Start the service from your app's main activity,
and create an Intent
to pass data to the service. You
need an explicit intent, because you want only your service
to respond to the intent. For more information, see
Intent
Types.
To create an explicit intent, specify the name of the
class to use for the service: FetchAddressIntentService.class
.
Pass two pieces of information in the intent extras:
- A
ResultReceiver
to handle the results of the address lookup. - A
Location
object containing the latitude and longitude that you want to convert to an address.
The following code sample shows you how to start the intent service:
public class MainActivity extends ActionBarActivity implements ConnectionCallbacks, OnConnectionFailedListener { protected Location mLastLocation; private AddressResultReceiver mResultReceiver; ... protected void startIntentService() { Intent intent = new Intent(this, FetchAddressIntentService.class); intent.putExtra(Constants.RECEIVER, mResultReceiver); intent.putExtra(Constants.LOCATION_DATA_EXTRA, mLastLocation); startService(intent); } }
Call the above startIntentService()
method when the
user takes an action that requires a geocoding address lookup. For example,
the user may press a Fetch address button on your app's UI. Before
starting the intent service, you need to check that the connection to Google
Play services is present. The following code snippet shows the call to the
startIntentService()
method in the button handler:
public void fetchAddressButtonHandler(View view) { // Only start the service to fetch the address if GoogleApiClient is // connected. if (mGoogleApiClient.isConnected() && mLastLocation != null) { startIntentService(); } // If GoogleApiClient isn't connected, process the user's request by // setting mAddressRequested to true. Later, when GoogleApiClient connects, // launch the service to fetch the address. As far as the user is // concerned, pressing the Fetch Address button // immediately kicks off the process of getting the address. mAddressRequested = true; updateUIWidgets(); }
You must also start the intent service when the connection to Google Play
services is established, if the user has already clicked the button on your
app's UI. The following code snippet shows the call to the
startIntentService()
method in the
onConnected()
callback provided by the Google API Client:
public class MainActivity extends ActionBarActivity implements ConnectionCallbacks, OnConnectionFailedListener { ... @Override public void onConnected(Bundle connectionHint) { // Gets the best and most recent location currently available, // which may be null in rare cases when a location is not available. mLastLocation = LocationServices.FusedLocationApi.getLastLocation( mGoogleApiClient); if (mLastLocation != null) { // Determine whether a Geocoder is available. if (!Geocoder.isPresent()) { Toast.makeText(this, R.string.no_geocoder_available, Toast.LENGTH_LONG).show(); return; } if (mAddressRequested) { startIntentService(); } } } }
Receive the Geocoding Results
The intent service has handled the geocoding request, and uses a
ResultReceiver
to return the results to the activity that
made the request. In the activity that makes the request, define an
AddressResultReceiver
that extends ResultReceiver
to handle the response from FetchAddressIntentService
.
The result includes a numeric result code (resultCode
) as well
as a message containing the result data (resultData
). If the
reverse geocoding process was successful, the resultData
contains
the address. In the case of a failure, the resultData
contains
text describing the reason for failure. For details of the possible errors,
see Return the address to the requestor.
Override the
onReceiveResult()
method
to handle the results delivered to the result receiver, as shown in the
following code sample:
public class MainActivity extends ActionBarActivity implements ConnectionCallbacks, OnConnectionFailedListener { ... class AddressResultReceiver extends ResultReceiver { public AddressResultReceiver(Handler handler) { super(handler); } @Override protected void onReceiveResult(int resultCode, Bundle resultData) { // Display the address string // or an error message sent from the intent service. mAddressOutput = resultData.getString(Constants.RESULT_DATA_KEY); displayAddressOutput(); // Show a toast message if an address was found. if (resultCode == Constants.SUCCESS_RESULT) { showToast(getString(R.string.address_found)); } } } }