package io.nlopez.smartlocation.geocoding.providers; import android.app.IntentService; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.location.Address; import android.location.Geocoder; import android.location.Location; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import io.nlopez.smartlocation.OnGeocodingListener; import io.nlopez.smartlocation.OnReverseGeocodingListener; import io.nlopez.smartlocation.geocoding.GeocodingProvider; import io.nlopez.smartlocation.geocoding.utils.LocationAddress; import io.nlopez.smartlocation.utils.Logger; /** * Geocoding provider based on Android's Geocoder class. */ public class AndroidGeocodingProvider implements GeocodingProvider { private static final String BROADCAST_DIRECT_GEOCODING_ACTION = AndroidGeocodingProvider.class.getCanonicalName() + ".DIRECT_GEOCODE_ACTION"; private static final String BROADCAST_REVERSE_GEOCODING_ACTION = AndroidGeocodingProvider.class.getCanonicalName() + ".REVERSE_GEOCODE_ACTION"; private static final String DIRECT_GEOCODING_ID = "direct"; private static final String REVERSE_GEOCODING_ID = "reverse"; private static final String LOCALE_ID = "locale"; private static final String NAME_ID = "name"; private static final String LOCATION_ID = "location"; private static final String RESULT_ID = "result"; private Locale locale; private OnGeocodingListener geocodingListener; private OnReverseGeocodingListener reverseGeocodingListener; private HashMap<String, Integer> fromNameList; private HashMap<Location, Integer> fromLocationList; private Context context; private Logger logger; public AndroidGeocodingProvider() { this(Locale.getDefault()); } public AndroidGeocodingProvider(Locale locale) { if (locale == null) { // This should be super weird throw new RuntimeException("Locale is null"); } this.locale = locale; fromNameList = new HashMap<>(); fromLocationList = new HashMap<>(); if (!Geocoder.isPresent()) { throw new RuntimeException("Android Geocoder not present. Please check if Geocoder.isPresent() before invoking the search"); } } @Override public void init(Context context, Logger logger) { this.logger = logger; this.context = context; } @Override public void addName(String name, int maxResults) { fromNameList.put(name, maxResults); } @Override public void addLocation(Location location, int maxResults) { fromLocationList.put(location, maxResults); } @Override public void start(OnGeocodingListener geocodingListener, OnReverseGeocodingListener reverseGeocodingListener) { this.geocodingListener = geocodingListener; this.reverseGeocodingListener = reverseGeocodingListener; if (fromNameList.isEmpty() && fromLocationList.isEmpty()) { logger.w("No direct geocoding or reverse geocoding points added"); } else { // Registering receivers for both possibilities final IntentFilter directFilter = new IntentFilter(BROADCAST_DIRECT_GEOCODING_ACTION); final IntentFilter reverseFilter = new IntentFilter(BROADCAST_REVERSE_GEOCODING_ACTION); // Launch service for processing the geocoder stuff in a background thread final Intent serviceIntent = new Intent(context, AndroidGeocodingService.class); serviceIntent.putExtra(LOCALE_ID, locale); if (!fromNameList.isEmpty()) { context.registerReceiver(directReceiver, directFilter); serviceIntent.putExtra(DIRECT_GEOCODING_ID, fromNameList); } if (!fromLocationList.isEmpty()) { context.registerReceiver(reverseReceiver, reverseFilter); serviceIntent.putExtra(REVERSE_GEOCODING_ID, fromLocationList); } context.startService(serviceIntent); // Clear hashmaps so they don't stay added for next invocations fromNameList.clear(); fromLocationList.clear(); } } @Override public void stop() { try { context.unregisterReceiver(directReceiver); } catch (IllegalArgumentException e) { logger.d("Silenced 'receiver not registered' stuff (calling stop more times than necessary did this)"); } try { context.unregisterReceiver(reverseReceiver); } catch (IllegalArgumentException e) { logger.d("Silenced 'receiver not registered' stuff (calling stop more times than necessary did this)"); } } private BroadcastReceiver directReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (BROADCAST_DIRECT_GEOCODING_ACTION.equals(intent.getAction())) { logger.d("sending new direct geocoding response"); if (geocodingListener != null) { final String name = intent.getStringExtra(NAME_ID); final ArrayList<LocationAddress> results = intent.getParcelableArrayListExtra(RESULT_ID); geocodingListener.onLocationResolved(name, results); } } } }; private BroadcastReceiver reverseReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (BROADCAST_REVERSE_GEOCODING_ACTION.equals(intent.getAction())) { logger.d("sending new reverse geocoding response"); if (reverseGeocodingListener != null) { final Location location = intent.getParcelableExtra(LOCATION_ID); final ArrayList<Address> results = (ArrayList<Address>) intent.getSerializableExtra(RESULT_ID); reverseGeocodingListener.onAddressResolved(location, results); } } } }; public static class AndroidGeocodingService extends IntentService { private Geocoder geocoder; public AndroidGeocodingService() { super(AndroidGeocodingService.class.getSimpleName()); } @Override protected void onHandleIntent(Intent intent) { final Locale locale = (Locale) intent.getSerializableExtra(LOCALE_ID); if (locale == null) { geocoder = new Geocoder(this); } else { geocoder = new Geocoder(this, locale); } if (intent.hasExtra(DIRECT_GEOCODING_ID)) { final HashMap<String, Integer> nameList = (HashMap<String, Integer>) intent.getSerializableExtra(DIRECT_GEOCODING_ID); for (String name : nameList.keySet()) { int maxResults = nameList.get(name); final ArrayList<LocationAddress> response = addressFromName(name, maxResults); sendDirectGeocodingBroadcast(name, response); } } if (intent.hasExtra(REVERSE_GEOCODING_ID)) { final HashMap<Location, Integer> locationList = (HashMap<Location, Integer>) intent.getSerializableExtra(REVERSE_GEOCODING_ID); for (Location location : locationList.keySet()) { int maxResults = locationList.get(location); final ArrayList<Address> response = addressFromLocation(location, maxResults); sendReverseGeocodingBroadcast(location, response); } } } private void sendDirectGeocodingBroadcast(String name, ArrayList<LocationAddress> results) { final Intent directIntent = new Intent(BROADCAST_DIRECT_GEOCODING_ACTION); directIntent.putExtra(NAME_ID, name); directIntent.putExtra(RESULT_ID, results); sendBroadcast(directIntent); } private void sendReverseGeocodingBroadcast(Location location, ArrayList<Address> results) { final Intent reverseIntent = new Intent(BROADCAST_REVERSE_GEOCODING_ACTION); reverseIntent.putExtra(LOCATION_ID, location); reverseIntent.putExtra(RESULT_ID, results); sendBroadcast(reverseIntent); } private ArrayList<Address> addressFromLocation(Location location, int maxResults) { try { return new ArrayList<>(geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), maxResults)); } catch (IOException ignored) { } return new ArrayList<>(); } private ArrayList<LocationAddress> addressFromName(String name, int maxResults) { try { final List<Address> addresses = geocoder.getFromLocationName(name, maxResults); final ArrayList<LocationAddress> result = new ArrayList<>(); for (Address address : addresses) { result.add(new LocationAddress(address)); } return result; } catch (IOException ignored) { } return new ArrayList<>(); } } }