Understanding geocoding in Android

Understanding geocoding in Android

For this tutorial, we are going to use the Geocoder class to translate a given address into its latitude and longitude values (geocoding), and also translate a latitude and longitude value into an address (reverse geocoding).

Geocoding is the process of converting the addresses (postal address) into geo coordinates as latitude and longitude. Reverse geocoding is converting a geo coordinate latitude and longitude to an address.

The Android API contains a Geocoder class that can use either a location name or a location’s latitude and longitude values to get further details about an address (it can perform both forward and reverse geocoding). The returned address details include address name, country name, country code, postal code and more. You can read about limits here.

We need location access permission to find the latitude and longitude of the Android device. Following two lines are the key to give permission to access the location.

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />

Geocoder has two methods for fetching an Address, getFromLocation(), which uses latitude and longitude, and getFromLocationName(), which uses the location’s name. Both methods return a list of Address objects. An Address contains information like address name, country, latitude and longitude, whereas Location contains latitude, longitude, bearing and altitude among others.

So, to achieve forward Geocode, use the below code

import android.location.Address;
import android.location.Geocoder;
...
Geocoder gc = new Geocoder(this);
List<Address> list = null;

if(gc.isPresent()) {
    try {
        list = gc.getFromLocationName("Karmelyuka St, 9532, Vinnytsia", 1);
    } catch (IOException e) {
        e.printStackTrace();
    }

    if (!list.isEmpty()) {
        Address address = list.get(0);
        double lat = address.getLatitude();
        double lng = address.getLongitude();
        Log.d(TAG, "Lat: " + String.valueOf(lat) + ", Lng: " + String.valueOf(lng));
    }
}

To achieve reverse Geocode, use the below code

Geocoder gc = new Geocoder(context);
List<Address> list = null;

if(gc.isPresent()) {
    try {
        list = gc.getFromLocation(49.2327, 28.4856, 1);
    } catch (IOException e) {
        e.printStackTrace();
    }

    if (!list.isEmpty()) {
        Address address = list.get(0);
        StringBuffer str = new StringBuffer();
        str.append("Name: " + address.getLocality() + "\n");
        str.append("Sub-Admin Ares: " + address.getSubAdminArea() + "\n");
        str.append("Admin Area: " + address.getAdminArea() + "\n");
        str.append("Country: " + address.getCountryName() + "\n");
        str.append("Country Code: " + address.getCountryCode() + "\n");
        String strAddress = str.toString();
        Log.d(TAG, strAddress);
    }
}

Let's develop simple app that will do forward or reverse geocoding in background using IntentService.

Our app is going to use both forward and reverse geocoding to get location address, and the app layout reflects this. The layout contains two EditTexts for latitude and longitude respectively, and an EditText for address name input. Beneath these, we have two RadioButtons, to select if we are fetching an address using the location’s latitude/longitude values, or using the address name. There is a Button, that begins the geocoding lookup when clicked, a ProgressBar, to show the user that the lookup task is running in the background, and a TextView to show the result received.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:orientation="vertical">

    <EditText
        android:id="@+id/etLatitude"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Latitude"
        android:inputType="numberDecimal|numberSigned"/>

    <EditText
        android:id="@+id/etLongitude"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/etLatitude"
        android:hint="Longitude"
        android:inputType="numberDecimal|numberSigned"/>

    <EditText
        android:id="@+id/etAddress"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/etLongitude"
        android:minLines="4"
        android:hint="Address"
        android:scrollHorizontally="false"
        android:scrollbars="vertical"
        android:enabled="false"/>

    <RadioGroup
        android:id="@+id/rgSwitchers"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/etAddress"
        android:layout_centerHorizontal="true"
        android:orientation="horizontal">

        <RadioButton
            android:id="@+id/rbLocation"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Use location"
            android:checked="true"
            android:onClick="onRadioButtonClicked"/>

        <RadioButton
            android:id="@+id/rbAddress"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Use address"
            android:onClick="onRadioButtonClicked"/>
    </RadioGroup>

    <Button
        android:id="@+id/btnGet"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/rgSwitchers"
        android:layout_centerHorizontal="true"
        android:text="Get"
        android:onClick="onButtonClicked"/>

    <TextView
        android:id="@+id/tvInfo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/btnGet"/>

    <ProgressBar
        android:id="@+id/pbResult"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/tvInfo"
        android:visibility="invisible"/>
</RelativeLayout>

To perform a long running task in the background, we can use an AsyncTask. However, the AsyncTask is not recommended for operations like the Geocoder lookup, because it can take a potentially long time to return. AsyncTask's should be used for comparatively shorter operations. We would use an IntentService. An IntentService extends Service, and operations run in it can take as long as necessary. An IntentService holds no reference to the Activity it was started from, and so, the activity can be rebuilt (like when the device is rotated), without affecting the IntentService’s tasks, unlike the AsyncTask.

We extend IntentService and define GeocodeIntentService. An IntentService is started much like an Activity. We build an Intent, and start the service by calling Context.startService() method.

Before defining the class, we include our GeocodeIntentService in the AppManifest.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="me.proft.geocoding">

    <uses-permission android:name="android.permission.INTERNET"/>

    <application
        ...
        <service
            android:name=".GeocodeIntentService"
            android:exported="false"/>
    </application>
</manifest>

Next step is to define Constants class with constants.

public final class Constants {
    public static final String FETCH_TYPE_EXTRA = "FETCH_TYPE_EXTRA";
    public static final String LOCATION_NAME_DATA_EXTRA = "LOCATION_NAME_DATA_EXTRA";
    public static final String RECEIVER = "RECEIVER";
    public static final String RESULT_DATA_KEY = "RESULT_DATA_KEY";
    public static final String RESULT_ADDRESS = "RESULT_ADDRESS";
    public static final String LOCATION_LATITUDE_DATA_EXTRA = "LOCATION_LATITUDE_DATA_EXTRA";
    public static final String LOCATION_LONGITUDE_DATA_EXTRA = "LOCATION_LONGITUDE_DATA_EXTRA";

    public static final int SUCCESS_RESULT = 0;
    public static final int FAILURE_RESULT = 1;
    public static final int USE_ADDRESS_NAME = 1;
    public static final int USE_ADDRESS_LOCATION = 2;
}

The code snippet below contains the actual forward or reverse geocoding lookup calls. We determine if the search uses location name, or location latitude/longitude values, and call the appropriate method. If using location name, we call the Geocoder.getFromLocationName() method, and if using latitude/longitude, we call Geocoder.getFromLocation() method. You can specify a maximum number of addresses to be returned. In our sample, we request for a maximum of one (1) address. Note that an address name can refer to more than one location, spread across multiple countries. In a production app, you might want to fetch more than one, and have an algorithm determine which is the most likely required address.

To use our IntentService, we must implement the onHandleIntent(Intent) method. This is the entry point for IntentService’s, much like the onCreate() is the entry point for Activity’s. In the code snippet below, take notice of the ResultReceiver object. When your IntentService has completed it’s task, it should have a way to send the results back to the invoking Activity. That is where the ResultReceiver comes in.

deliverResultToReceiver is a simple method, that handles returning the results of the operation to the invoking Activity, through the ResultReceiver.

import android.location.Address;
import android.location.Geocoder;
...

public class GeocodeIntentService extends IntentService {
    protected ResultReceiver resultReceiver;
    private static final String TAG = "GEO";

    public GeocodeIntentService() {
        super("GeocodeIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Geocoder geocoder = new Geocoder(this, Locale.getDefault());
        String errorMessage = "";
        List<Address> addresses = null;

        int fetchType = intent.getIntExtra(Constants.FETCH_TYPE_EXTRA, 0);

        if(fetchType == Constants.USE_ADDRESS_NAME) {
            String name = intent.getStringExtra(Constants.LOCATION_NAME_DATA_EXTRA);
            try {
                addresses = geocoder.getFromLocationName(name, 1);
            } catch (IOException e) {
                errorMessage = "Service not available";
                Log.e(TAG, errorMessage, e);
            }
        }
        else if(fetchType == Constants.USE_ADDRESS_LOCATION) {
            double latitude = intent.getDoubleExtra(Constants.LOCATION_LATITUDE_DATA_EXTRA, 0);
            double longitude = intent.getDoubleExtra(Constants.LOCATION_LONGITUDE_DATA_EXTRA, 0);

            try {
                addresses = geocoder.getFromLocation(latitude, longitude, 1);
            } catch (IOException ioException) {
                errorMessage = "Service Not Available";
                Log.e(TAG, errorMessage, ioException);
            } catch (IllegalArgumentException illegalArgumentException) {
                errorMessage = "Invalid Latitude or Longitude Used";
                Log.e(TAG, errorMessage + ". " +
                        "Latitude = " + latitude + ", Longitude = " +
                        longitude, illegalArgumentException);
            }
        }
        else {
            errorMessage = "Unknown Type";
            Log.e(TAG, errorMessage);
        }

        resultReceiver = intent.getParcelableExtra(Constants.RECEIVER);
        if (addresses == null || addresses.size()  == 0) {
            if (errorMessage.isEmpty()) {
                errorMessage = "Not Found";
                Log.e(TAG, errorMessage);
            }
            deliverResultToReceiver(Constants.FAILURE_RESULT, errorMessage, null);
        } else {
            for(Address address : addresses) {
                String outputAddress = "";
                for(int i = 0; i < address.getMaxAddressLineIndex(); i++) {
                    outputAddress += " --- " + address.getAddressLine(i);
                }
                Log.e(TAG, outputAddress);
            }

            Address address = addresses.get(0);
            ArrayList<String> addressFragments = new ArrayList<>();

            for(int i = 0; i < address.getMaxAddressLineIndex(); i++) {
                addressFragments.add(address.getAddressLine(i));
            }

            String addressResult = TextUtils.join(System.getProperty("line.separator"), addressFragments);
            deliverResultToReceiver(Constants.SUCCESS_RESULT, addressResult, address);
        }
    }

    private void deliverResultToReceiver(int resultCode, String message, Address address) {
        Bundle bundle = new Bundle();
        bundle.putParcelable(Constants.RESULT_ADDRESS, address);
        bundle.putString(Constants.RESULT_DATA_KEY, message);
        resultReceiver.send(resultCode, bundle);
    }

}

And MainActivity.java file.

public class MainActivity extends AppCompatActivity {
    AddressResultReceiver resultReceiver;

    EditText etLatitude, etLongitude, etAddress;
    ProgressBar pbResult;
    TextView tvInfo;

    boolean fetchAddress;
    int fetchType = Constants.USE_ADDRESS_LOCATION;

    private static final String TAG = "GEO";

    public class AddressResultReceiver extends ResultReceiver {
        public AddressResultReceiver(Handler handler) {
            super(handler);
        }

        @Override
        protected void onReceiveResult(int resultCode, final Bundle resultData) {
            if (resultCode == Constants.SUCCESS_RESULT) {
                final Address address = resultData.getParcelable(Constants.RESULT_ADDRESS);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        pbResult.setVisibility(View.INVISIBLE);
                        tvInfo.setVisibility(View.VISIBLE);
                        tvInfo.setText("Latitude: " + address.getLatitude() + "\n" +
                                "Longitude: " + address.getLongitude() + "\n" +
                                "Address: " + resultData.getString(Constants.RESULT_DATA_KEY));
                    }
                });
            } else {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        pbResult.setVisibility(View.INVISIBLE);
                        tvInfo.setVisibility(View.VISIBLE);
                        tvInfo.setText(resultData.getString(Constants.RESULT_DATA_KEY));
                    }
                });
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        etLongitude = (EditText) findViewById(R.id.etLongitude);
        etLatitude = (EditText) findViewById(R.id.etLatitude);
        etAddress = (EditText) findViewById(R.id.etAddress);
        pbResult = (ProgressBar) findViewById(R.id.pbResult);
        tvInfo = (TextView) findViewById(R.id.tvInfo);

        resultReceiver = new AddressResultReceiver(null);
    }

    public void onRadioButtonClicked(View view) {
        boolean checked = ((RadioButton) view).isChecked();

        switch(view.getId()) {
            case R.id.rbAddress:
                if (checked) {
                    fetchAddress = false;
                    fetchType = Constants.USE_ADDRESS_NAME;
                    etLongitude.setEnabled(false);
                    etLatitude.setEnabled(false);
                    etAddress.setEnabled(true);
                    etAddress.requestFocus();
                }
                break;
            case R.id.rbLocation:
                if (checked) {
                    fetchAddress = true;
                    fetchType = Constants.USE_ADDRESS_LOCATION;
                    etLatitude.setEnabled(true);
                    etLatitude.requestFocus();
                    etLongitude.setEnabled(true);
                    etAddress.setEnabled(false);
                }
                break;
        }
    }

    public void onButtonClicked(View view) {
        Intent intent = new Intent(this, GeocodeIntentService.class);
        intent.putExtra(Constants.RECEIVER, resultReceiver);
        intent.putExtra(Constants.FETCH_TYPE_EXTRA, fetchType);
        if(fetchType == Constants.USE_ADDRESS_NAME) {
            if(etAddress.getText().length() == 0) {
                Toast.makeText(this, "Please enter address", Toast.LENGTH_LONG).show();
                return;
            }
            intent.putExtra(Constants.LOCATION_NAME_DATA_EXTRA, etAddress.getText().toString());
        }
        else {
            if(etLatitude.getText().length() == 0 || etLongitude.getText().length() == 0) {
                Toast.makeText(this,
                        "Please enter latitude and longitude",
                        Toast.LENGTH_LONG).show();
                return;
            }
            intent.putExtra(Constants.LOCATION_LATITUDE_DATA_EXTRA,
                    Double.parseDouble(etLatitude.getText().toString()));
            intent.putExtra(Constants.LOCATION_LONGITUDE_DATA_EXTRA,
                    Double.parseDouble(etLongitude.getText().toString()));
        }
        tvInfo.setVisibility(View.INVISIBLE);
        pbResult.setVisibility(View.VISIBLE);
        Log.e(TAG, "Starting Service");
        startService(intent);
    }
}

Starting the IntentService is pretty similar to starting a new Activity. We build an Intent, put in the necessary Extras, and call Context.startService(Intent). The Extras we bundle in the Intent is dependent on if we are performing a forward or reverse lookup.

Result

android_geocoding.png
comments powered by Disqus