Adventures in Android Geocoding

Experiments with Google Play Services

Screen Shot 2015-05-11 at 12.15.18 PM


“I can’t register!” “Your app isn’t available in New York City??!!”


Comments like these (some more vocal than others) began pouring in immediately after we ramped up our staged rollout to Google Play. Hinge is, in fact, available in New York City (our hometown!) Clearly something was amiss.

We had just pushed out an update to our app which included a new map-based geocoding registration. Instead of using a zipcode text field, the device would automatically determine a user’s neighborhood, city and zipcode. For each location the user picked on the map, we reverse-geocoded the latitude and longitude to obtain a general neighborhood, city, or state. This information was then passed to our servers to determine eligibility.

And it worked great! With our internal group of about 5 testers, with a range of devices, the map worked. At least, we thought.

But when we deployed the new version it appeared  that some new users simply could not register at all, which, of course, is a very serious problem. In the case of a few users, the fetching of address information failed, rejecting the user and instructing them to email support. What could be going on?

Discovery

We immediately attempted to reproduce the experience these users where having, with little success. More and more people wrote in, explaining that, yes, repeated attempts to register in a valid location (e.g. New York City) resulted in failure.

Our first attempt involved testing on our devices and emulators. With a group of 4 people with 2-4 devices each, we continually moved around our map, forcing the geocoder to recalculate the position. Ultimately, we failed to reproduce the problem. A group code review of the key points produced no obvious errors or questions. We were lost. With the app live and being pushed to new markets, a fix was needed—and fast.

After several hours of searching online, finally, a break! A report on Google’s bug tracker was discovered. Was it a problem with our code? Nope. Was it a device issue? Negative. It would seem Hinge wasn’t alone in its problematic geocoding situation.

Who is at fault?

The bug exists in Google Play Services, the framework Google distributes to all Android devices. To put it simply, sometimes the Geocoder just doesn’t work. The only valid solution is a device reboot, which obviously isn’t a tip we can provide to our users. It appears that a bug with Google Play Services causes certain devices to completely fail in loading the module responsible for geocoding addresses and locations. This bug still exists in the latest iteration of the geocoding package. (More information here) It throws the following exception, then fails to produce any valuable results to the device:

Geocoder.getFromLocationName : "IOException: Service is not available"


Our existing calls to to the Geocoders getFromLocation method looks like this:
@Override

protected LocationBundle doInBackground(LatLng... params) {
Geocoder geocoder = new Geocoder(mContext);
double latitude = params[0].latitude;
double longitude = params[0].longitude;

List<Address> addresses;

try {
addresses = geocoder.getFromLocation(latitude, longitude,1);
} catch (IOException e) {
Timber.d("Geocoding failed");
addresses = getManualLocation(latitude, longitude);
e.printStackTrace();
}
if(!addresses.isEmpty()){
return parseLocation(addresses, params);
}

return new LocationBundle();
}


A try/catch block was used in case of network failure. After the geocoding returns, we check to ensure the results are valid. So some users were failing, producing 0 results and stopping the registration process.


Problem solving

Because we had already written a (mostly) working geocoding solution, completely replacing the existing logic was not a popular idea. A backup solution was proposed: because we can detect when the system fails (when the exception is thrown), a secondary geocoding solution can be used instead. We found the Google Maps Geocoding API to be relatively stable. It involves passing a lat/long to public, free endpoint. The results are provided in JSON.

Here’s what the code looks like:

private List<Address> getManualLocation(double lat, double lng) {

SharedPreferences settings;
settings = PreferenceManager.getDefaultSharedPreferences(mContext);
settings.edit().putBoolean("SecondaryGeoCode", true).apply();

String address = String.format(Locale.ENGLISH,"http://maps.googleapis.com/maps/api/geocode/json?latlng=%1$f,%2$f&sensor=true&language="+Locale.getDefault().getCountry(), lat, lng);
Timber.d(address);
HttpGet httpGet = new HttpGet(address);
HttpClient client = new DefaultHttpClient();
HttpResponse response;
StringBuilder stringBuilder = new StringBuilder();

List<Address> retList = new ArrayList<>();

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(stringBuilder.toString());
retList = new ArrayList<>();

if("OK".equalsIgnoreCase(jsonObject.getString(STATUS))){
JSONArray results = jsonObject.getJSONArray(RESULTS);
boolean hasGoodPartTwo=false;

for (int i=0;i<results.length();i++ ) {

//parse addresses
}


A relatively simple solution with a basic usage of HTTPClient and JSON parsing. Because the Android geocoder handles the parsing of data into Address objects, we also had to parse and build a list of these objects ourselves so we could still use our existing registration logic. Because Google returns a large list of varying specificity, we have to find the best data available. Looping through the results and checking what attributes exist will give us that ability.
if(hasJson(types, NEIGHBORHOOD)) {

partOne = addressObject.getString(LONG_NAME);
break;
}
else if(hasJson(types, LOCALITY)) {
partOne = addressObject.getString(LONG_NAME);
break;
}
else if(hasJson(types, SUBLOCALITY_LEVEL_1)) {
partOne = addressObject.getString(LONG_NAME);
break;
}
else if(hasJson(types, SUBLOCALITY_LEVEL_1)) {
partOne = addressObject.getString(LONG_NAME);
break;
}

We then set each object with the proper values, giving us a list of Address objects that, to our registration logic, works just fine.
addr.setSubLocality(partOne);

addr.setAdminArea(partTwo);

Keeping track

Because this bug exists out of our standard crash and analytics scope, keeping track of how often this happens proved difficult.

We provided a pretty painless solution. If we encountered a failure on registration a bool value in SharedPreferences named “failedGeoCode” was stored.

SharedPreferences settings;

SharedPreferences settings;

settings = PreferenceManager.getDefaultSharedPreferences(mContext);
settings.edit().putBoolean("SecondaryGeoCode", true).apply();

After the user installs, registers, and opens the app, we send off this value to our metrics tracking services. As of April 2015, it appears that almost 4% of users are experiencing this bug across all devices, regions, and Android versions. Which means, for most purposes, the Android geocoder simply isn’t reliable enough to use for any purpose in apps.

Moving down the line, we most likely will remove the Geocoder class altogether in favor of the secondary, more reliable, solution.

Sound off

Experiencing this bug in your own apps? Concocted a novel solution different from this? Shoot an email to matt@hinge.co and we can chat.