Last active
December 29, 2015 00:09
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
For API lvl 23 the following must be added in build.gradle
https://developer.android.com/preview/behavior-changes.html#behavior-apache-http-client