Skip to content

Instantly share code, notes, and snippets.

@linakis
Last active December 29, 2015 00:09
Show Gist options
  • Save linakis/7583632 to your computer and use it in GitHub Desktop.
Save linakis/7583632 to your computer and use it in GitHub Desktop.
This is a helper class to compensate a known Android bug that forces Geocoder to throw an IOException For more info check: https://code.google.com/p/android/issues/detail?id=38009
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.content.Context;
import android.location.Address;
import android.location.Geocoder;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.text.TextUtils;
import android.util.Log;
/**
* This is a helper class to compensate a known Android bug that forces {@link Geocoder} to throw an {@link IOException}
* For more info check: https://code.google.com/p/android/issues/detail?id=38009
* @author hastoukopsaro
*
*/
public class GeocoderHelper {
private final static String TAG = GeocoderHelper.class.getName();
/**
* HTTP API response statuses
*/
private final static String STATUS_OK = "OK";
private final static String STATUS_ZERO_RESULTS = "ZERO_RESULTS";
private final static String STATUS_OVER_QUERY_LIMIT = "OVER_QUERY_LIMIT";
private final static String STATUS_REQUEST_DENIED = "REQUEST_DENIED";
private final static String STATUS_INVALID_REQUEST = "INVALID_REQUEST";
private final static String STATUS_UNKNOWN_ERROR = "UNKNOWN_ERROR";
/**
* HTTP API response statuses descriptions
*/
private final static String LOG_STATUS_OK = "No errors occurred; the address was successfully parsed and at least one geocode was returned.";
private final static String LOG_STATUS_ZERO_RESULTS = "The geocode was successful but returned no results. This may occur if the geocode was passed a non-existent address or a latlng in a remote location.";
private final static String LOG_STATUS_OVER_QUERY_LIMIT = "You are over your quota";
private final static String LOG_STATUS_REQUEST_DENIED = "Your request was denied, generally because of lack of a sensor parameter.";
private final static String LOG_STATUS_INVALID_REQUEST = "The query (address or latlng) is missing";
private final static String LOG_STATUS_UNKNOWN_ERROR = "The request could not be processed due to a server error. The request may succeed if you try again.";
private Context mContext;
private Locale mLocale;
private Geocoder mGeocoder;
/**
* Constructs a GeocoderHelper whose responses will be localized for the
* default system Locale.
*
* @param context the Context of the calling Activity
*/
public GeocoderHelper(Context context) {
this(context, Locale.getDefault());
}
/**
* Constructs a GeocoderHelper whose responses will be localized for the
* given Locale.
*
* @param context the Context of the calling Activity
* @param locale the desired Locale for the query results
*
* @throws NullPointerException if Locale is null
*/
public GeocoderHelper(Context context, Locale locale) throws NullPointerException{
if (locale == null) {
throw new NullPointerException("locale cannot be null");
}
this.mLocale = locale;
this.mGeocoder = new Geocoder(context, locale);
this.mContext = context;
}
/**
* Returns an array of Addresses that are known to describe the
* area immediately surrounding the given latitude and longitude.
* The returned addresses will be localized for the locale
* provided to this class's constructor.
*
* <p> The returned values may be obtained by means of a network lookup.
* The results are a best guess and are not guaranteed to be meaningful or
* correct. It may be useful to call this method from a thread separate from your
* primary UI thread.
*
* @param lat the latitude a point for the search
* @param lng the longitude a point for the search
* @param maxResults max number of addresses to return. Smaller numbers (1 to 5) are recommended
* @return a list of Address objects. Returns null or empty list if no matches were found
* @throws IOException if any than network related I/O problem occurs
*
* @throws IllegalArgumentException if latitude is
* less than -90 or greater than 90
* @throws IllegalArgumentException if longitude is
* less than -180 or greater than 180
*/
public List<Address> getFromLocation(double lat, double lng, int maxResults) throws IOException {
if (lat < -90.0 || lat > 90.0) {
throw new IllegalArgumentException("latitude == " + lat);
}
if (lng < -180.0 || lng > 180.0) {
throw new IllegalArgumentException("longitude == " + lng);
}
try {
return mGeocoder.getFromLocation(lat, lng, maxResults);
} catch (IOException ioe) {
// this is where the bug appears. Gecocoder throws an IOException.
// ensure that it's not because of network availability and move on.
if(isConnected()) {
return getFromLocationHttpApi(lat, lng, maxResults);
} else {
Log.e(TAG, ioe.getMessage());
throw ioe;
}
}
}
private List<Address> getFromLocationHttpApi(double lat, double lng, int maxResults) {
String URL_PATTERN = "http://maps.googleapis.com/maps/api/geocode/json?latlng=%f,%f&sensor=true&language=%s";
// Use an English locale formatted string to properly get
// the float symbol as a dot (.) instead of a comma (,)
String address = String.format(Locale.ENGLISH, URL_PATTERN, lat, lng, mLocale.getCountry());
HttpGet httpGet = new HttpGet(address);
HttpClient client = new DefaultHttpClient();
HttpResponse response;
StringBuilder stringBuilder = new StringBuilder();
List<Address> retList = null;
try {
response = client.execute(httpGet);
HttpEntity entity = response.getEntity();
InputStream stream = entity.getContent();
int b;
while ((b = stream.read()) != -1) {
stringBuilder.append((char) b);
}
JSONObject jsonObject = new JSONObject();
jsonObject = new JSONObject(stringBuilder.toString());
retList = new ArrayList<Address>();
String status = jsonObject.getString("status");
if (STATUS_OK.equalsIgnoreCase(status)) {
Log.d(TAG, LOG_STATUS_OK);
JSONArray results = jsonObject.getJSONArray("results");
for (int i = 0; i < results.length(); i++) {
JSONObject result = results.getJSONObject(i);
String countryCode = "";
String countryName = "";
String postalCode = "";
String administrativeAreaLevel1 = "";
String administrativeAreaLevel2 = "";
String administrativeAreaLevel3 = "";
String locality = "";
String streetNumber = "";
String streetName = "";
String formattedAddress = result.getString("formatted_address");
JSONArray addressComponents = result.getJSONArray("address_components");
for (int j = 0; j < addressComponents.length(); j++) {
JSONObject component = addressComponents.getJSONObject(j);
JSONArray types = component.getJSONArray("types");
for(int z = 0; z < types.length(); z++) {
String typeItem = types.getString(z);
if("street_number".equals(typeItem)) {
streetNumber = component.getString("long_name");
} else if("route".equals(typeItem)) {
streetName = component.getString("long_name");
} else if("locality".equals(typeItem)) {
locality = component.getString("long_name");
} else if("administrative_area_level_1".equals(typeItem)) {
administrativeAreaLevel1 = component.getString("long_name");
} else if("administrative_area_level_2".equals(typeItem)) {
administrativeAreaLevel2 = component.getString("long_name");
} else if("administrative_area_level_3".equals(typeItem)) {
administrativeAreaLevel3 = component.getString("long_name");
} else if("postal_code".equals(typeItem)) {
postalCode = component.getString("long_name");
} else if("country".equals(typeItem)) {
countryCode = component.getString("short_name");
countryName = component.getString("long_name");
}
}
}
Address addr = new Address(Locale.getDefault());
// we want to have at least one address line so we use the formatted address for default
// the following lines try to fill the address line to match Geocoder functionality
// 0 : StreetName StreetNumber
// 1 : Locality Postal Code
// 2 : Country
String addressLine1 = formattedAddress;
if(!TextUtils.isEmpty(streetName)) {
addressLine1 = TextUtils.isEmpty(streetNumber) ? streetName : streetName + " " + streetNumber;
addr.setThoroughfare(streetName);
}
addr.setAddressLine(0, addressLine1);
if(!TextUtils.isEmpty(locality)) {
String addressLine2 = TextUtils.isEmpty(postalCode) ? locality : locality + " " + postalCode;
addr.setAddressLine(1, addressLine2);
}
if (!TextUtils.isEmpty(countryName)) {
addr.setAddressLine(2, countryName);
}
addr.setAdminArea(administrativeAreaLevel1);
addr.setCountryCode(countryCode);
addr.setCountryName(countryName);
addr.setLocality(locality);
addr.setPostalCode(postalCode);
addr.setSubAdminArea(administrativeAreaLevel2);
retList.add(addr);
}
} else if (STATUS_ZERO_RESULTS.equalsIgnoreCase(status)) {
Log.w(TAG, LOG_STATUS_ZERO_RESULTS);
} else if (STATUS_INVALID_REQUEST.equalsIgnoreCase(status)) {
Log.w(TAG, LOG_STATUS_INVALID_REQUEST);
} else if (STATUS_OVER_QUERY_LIMIT.equalsIgnoreCase(status)) {
Log.w(TAG, LOG_STATUS_OVER_QUERY_LIMIT);
} else if (STATUS_REQUEST_DENIED.equalsIgnoreCase(status)) {
Log.w(TAG, LOG_STATUS_REQUEST_DENIED);
} else if (STATUS_UNKNOWN_ERROR.equalsIgnoreCase(status)) {
Log.w(TAG, LOG_STATUS_UNKNOWN_ERROR);
}
} catch (ClientProtocolException e) {
Log.e(TAG, "Error calling Google geocode webservice.", e);
} catch (IOException e) {
Log.e(TAG, "Error calling Google geocode webservice.", e);
} catch (JSONException e) {
Log.e(TAG, "Error parsing Google geocode webservice response.", e);
} catch (Exception e) {
e.printStackTrace();
}
return retList;
}
/**
* Determine if network connectivity is possible
* @return true if possible or false otherwise
*/
private boolean isConnected() {
ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo netInfo = cm.getActiveNetworkInfo();
if (netInfo != null && netInfo.isConnectedOrConnecting()) {
return true;
}
return false;
}
}
@linakis
Copy link
Author

linakis commented Sep 4, 2015

For API lvl 23 the following must be added in build.gradle

android {
    useLibrary 'org.apache.http.legacy'
}

https://developer.android.com/preview/behavior-changes.html#behavior-apache-http-client

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment