Cómo mostrar la dirección de una ubicación

En las lecciones Cómo obtener la última ubicación conocida y Cómo recibir actualizaciones de ubicación, se describe cómo obtener la ubicación del usuario en formato de objeto Location que contiene las coordenadas de latitud y longitud. Si bien la latitud y longitud son útiles para calcular distancias o mostrar la posición en el mapa, en muchos casos resulta más útil la dirección de la ubicación. Por ejemplo, si quieres permitir que los usuarios sepan dónde están o qué lugares tienen cerca, resulta más útil una dirección que las coordenadas geográficas (latitud y longitud) de la ubicación.

El uso de la clase Geocoder en las API de ubicación del marco de trabajo de Android te permite convertir una dirección en las coordenadas geográficas correspondientes. Este proceso se llama codificación geográfica. De manera alternativa, puedes convertir una ubicación geográfica en una dirección. La función de búsqueda de direcciones también se conoce como codificación geográfica inversa.

En esta lección, se muestra cómo usar el método getFromLocation() para convertir una ubicación geográfica en una dirección. Este método muestra una dirección estimada que corresponde a una latitud y longitud determinadas.

Cómo obtener una ubicación geográfica

La última ubicación conocida del dispositivo es un punto de partida útil para la función de búsqueda de direcciones. En la lección sobre Cómo obtener la última ubicación conocida, se muestra cómo usar el método getLastLocation() que proporciona el proveedor de ubicación combinada.

Si quieres acceder al proveedor de ubicación combinada, crea una instancia de FusedLocationProviderClient. Para obtener información sobre cómo crear tu cliente, consulta Cómo crear un cliente de servicios de ubicación.

Para habilitar el proveedor de ubicación combinada a fin de recuperar una dirección precisa, configura el permiso de ubicación en el manifiesto de tu app en ACCESS_FINE_LOCATION, como se muestra en el siguiente ejemplo:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.google.android.gms.location.sample.locationupdates" >

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

Cómo definir un servicio de intents para buscar una dirección

La clase Geocoder proporciona el método getFromLocation() que acepta las coordenadas de latitud y longitud, y muestra una lista de direcciones. Es un método síncrono y es posible que tarde en realizar su trabajo, por lo que no debes llamarlo desde el subproceso de la interfaz de usuario (IU) principal de tu app.

La clase IntentService proporciona una estructura para ejecutar una tarea en un subproceso en segundo plano. Con esta clase puedes administrar operaciones prolongadas sin afectar la capacidad de respuesta de la IU.

Define una clase FetchAddressIntentService que extienda el IntentService. Esta clase es tu servicio de búsqueda de direcciones. El servicio de intents administra un intent de manera asíncrona en un subproceso de trabajo y se detiene cuando no tiene más tareas para realizar. Los extras de intent proporcionan los datos que necesita el servicio, incluido un objeto Location, a fin de realizar la conversión a una dirección y un objeto ResultReceiver para administrar los resultados de la búsqueda de direcciones. El servicio utiliza un elemento Geocoder a fin de buscar la dirección de la ubicación y envía los resultados al ResultReceiver.

Cómo definir un servicio de intents en el manifiesto de tu app

Agrega una entrada en el manifiesto de tu app que defina el servicio de intents de la siguiente manera:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.google.android.gms.location.sample.locationaddress" >
        <application
            ...
            <service
                android:name=".FetchAddressIntentService"
                android:exported="false"/>
        </application>
        ...
    </manifest>
    

Nota: El elemento <service> del manifiesto no necesita incluir un filtro de intent, ya que tu actividad principal crea un intent explícito mediante la especificación del nombre de la clase que se usará para el intent.

Cómo crear un Geocoder

El proceso de conversión de una ubicación geográfica en una dirección se llama codificación geográfica inversa. Para realizar el trabajo principal del servicio de intents (tu solicitud de codificación geográfica inversa), implementa onHandleIntent() con la clase FetchAddressIntentService. Crea un objeto Geocoder a fin de administrar la codificación geográfica inversa.

Una configuración regional representa una región lingüística o geográfica. Sus objetos ajustan la presentación de la información, como los números o las fechas, de manera que se adapten a las convenciones de la región que representa la configuración regional. Pasa un objeto Locale al objeto Geocoder a fin de garantizar que la dirección de resultado se localice en la región geográfica del usuario. A continuación, se muestra un ejemplo:

Kotlin

    override fun onHandleIntent(intent: Intent?) {
        val geocoder = Geocoder(this, Locale.getDefault())
        // ...
    }
    

Java

    @Override
    protected void onHandleIntent(Intent intent) {
        Geocoder geocoder = new Geocoder(this, Locale.getDefault());
        // ...
    }
    

Cómo recuperar la dirección

Ahora puedes recuperar la dirección del Geocoder, administrar cualquier error que se produzca y enviar los resultados a la actividad que la solicitó. Si quieres informar los resultados del proceso de codificación geográfica, necesitarás dos constantes numéricas que indiquen el éxito o fracaso. Define las constantes para que contengan los valores, como se muestra en el siguiente fragmento de código:

Kotlin

    object Constants {
        const val SUCCESS_RESULT = 0
        const val FAILURE_RESULT = 1
        const val PACKAGE_NAME = "com.google.android.gms.location.sample.locationaddress"
        const val RECEIVER = "$PACKAGE_NAME.RECEIVER"
        const val RESULT_DATA_KEY = "${PACKAGE_NAME}.RESULT_DATA_KEY"
        const val LOCATION_DATA_EXTRA = "${PACKAGE_NAME}.LOCATION_DATA_EXTRA"
    }
    

Java

    public final class Constants {
        public static final int SUCCESS_RESULT = 0;
        public static final int FAILURE_RESULT = 1;
        public static final String PACKAGE_NAME =
            "com.google.android.gms.location.sample.locationaddress";
        public static final String RECEIVER = PACKAGE_NAME + ".RECEIVER";
        public static final String RESULT_DATA_KEY = PACKAGE_NAME +
            ".RESULT_DATA_KEY";
        public static final String LOCATION_DATA_EXTRA = PACKAGE_NAME +
            ".LOCATION_DATA_EXTRA";
    }
    

Para obtener la dirección correspondiente a una ubicación geográfica, llama a getFromLocation(), y pasa la latitud y longitud del objeto de ubicación y la cantidad máxima de direcciones que quieres mostrar. En este caso, quieres una sola. El Geocoder muestra un conjunto de direcciones. Si no hay coincidencias con la ubicación especificada, se muestra una lista vacía. Si no hay ningún servicio de codificación geográfica de backend disponible, el Geocoder muestra un valor nulo.

Busca los siguientes errores que aparecen en la muestra de código debajo:

  • No hay datos de ubicación: Los extras de intent no incluyen el objeto Location que se requiere para la codificación geográfica inversa.
  • Latitud y longitud no válidas: Los valores de latitud y longitud que se proporcionan en el objeto Location no son válidos.
  • No hay un Geocoder disponible: El servicio de codificación geográfica en segundo plano no está disponible debido a un error de red o a una excepción de IO.
  • Lo siento. No se encontró ninguna dirección: El Geocoder no puede encontrar una dirección para la latitud y longitud especificadas.

Si se produce un error, incluye el mensaje de error correspondiente en la variable errorMessage de manera que puedas enviarlo a la actividad que realiza la solicitud.

Para obtener líneas individuales de un objeto de dirección, usa el método getAddressLine() que proporciona la clase Address. Une las líneas en una lista de fragmentos de direcciones a fin de enviárselas a la actividad que solicitó la dirección.

Para enviar los resultados a la actividad que los solicita, llama al método deliverResultToReceiver() (que se define en Cómo enviar la dirección a la actividad que la solicita). Los resultados incluyen una string y un código numérico de éxito o fracaso que se mencionó anteriormente. En el caso de una codificación geográfica inversa exitosa, la string contiene una dirección. Para fracaso, la string contiene el mensaje de error, como se muestra en el siguiente código de ejemplo:

Kotlin

    protected fun onHandleIntent(intent: Intent?) {
        intent ?: return

        var errorMessage = ""

        // Get the location passed to this service through an extra.
        val location = intent.getParcelableExtra(
                Constants.LOCATION_DATA_EXTRA)

        // ...

        var addresses: List<Address> = emptyList()

        try {
            addresses = geocoder.getFromLocation(
                    location.latitude,
                    location.longitude,
                    // In this sample, we get just a single address.
                    1)
        } catch (ioException: IOException) {
            // Catch network or other I/O problems.
            errorMessage = getString(R.string.service_not_available)
            Log.e(TAG, errorMessage, ioException)
        } catch (illegalArgumentException: IllegalArgumentException) {
            // Catch invalid latitude or longitude values.
            errorMessage = getString(R.string.invalid_lat_long_used)
            Log.e(TAG, "$errorMessage. Latitude = $location.latitude , " +
                    "Longitude =  $location.longitude", illegalArgumentException)
        }

        // Handle case where no address was found.
        if (addresses.isEmpty()) {
            if (errorMessage.isEmpty()) {
                errorMessage = getString(R.string.no_address_found)
                Log.e(TAG, errorMessage)
            }
            deliverResultToReceiver(Constants.FAILURE_RESULT, errorMessage)
        } else {
            val address = addresses[0]
            // Fetch the address lines using getAddressLine,
            // join them, and send them to the thread.
            val addressFragments = with(address) {
                (0..maxAddressLineIndex).map { getAddressLine(it) }
            }
            Log.i(TAG, getString(R.string.address_found))
            deliverResultToReceiver(Constants.SUCCESS_RESULT,
                    addressFragments.joinToString(separator = "\n"))
        }
    }
    

Java

    @Override
    protected void onHandleIntent(Intent intent) {
        if (intent == null) {
            return;
        }
        String errorMessage = "";

        // Get the location passed to this service through an extra.
        Location location = intent.getParcelableExtra(
                Constants.LOCATION_DATA_EXTRA);

        // ...

        List<Address> addresses = null;

        try {
            addresses = geocoder.getFromLocation(
                    location.getLatitude(),
                    location.getLongitude(),
                    // In this sample, get just a single address.
                    1);
        } catch (IOException ioException) {
            // Catch network or other I/O problems.
            errorMessage = getString(R.string.service_not_available);
            Log.e(TAG, errorMessage, ioException);
        } catch (IllegalArgumentException illegalArgumentException) {
            // Catch invalid latitude or longitude values.
            errorMessage = getString(R.string.invalid_lat_long_used);
            Log.e(TAG, errorMessage + ". " +
                    "Latitude = " + location.getLatitude() +
                    ", Longitude = " +
                    location.getLongitude(), illegalArgumentException);
        }

        // Handle case where no address was found.
        if (addresses == null || addresses.size()  == 0) {
            if (errorMessage.isEmpty()) {
                errorMessage = getString(R.string.no_address_found);
                Log.e(TAG, errorMessage);
            }
            deliverResultToReceiver(Constants.FAILURE_RESULT, errorMessage);
        } else {
            Address address = addresses.get(0);
            ArrayList<String> addressFragments = new ArrayList<String>();

            // Fetch the address lines using getAddressLine,
            // join them, and send them to the thread.
            for(int i = 0; i <= address.getMaxAddressLineIndex(); i++) {
                addressFragments.add(address.getAddressLine(i));
            }
            Log.i(TAG, getString(R.string.address_found));
            deliverResultToReceiver(Constants.SUCCESS_RESULT,
                    TextUtils.join(System.getProperty("line.separator"),
                            addressFragments));
        }
    }
    

Cómo enviar la dirección a la actividad que la solicita

La acción final que el servicio de intents debe completar es enviar la dirección de vuelta al ResultReceiver en la actividad que inició el servicio. La clase ResultReceiver te permite enviar un código numérico de resultado, además de un mensaje que contiene los datos de resultado. El código numérico es útil para indicar el éxito o fracaso de la solicitud de codificación geográfica. En el caso de una solicitud exitosa, el mensaje contiene la dirección. En el caso de que falle, el mensaje contiene el texto que describe el motivo.

Una vez que obtengas la dirección del Geocoder, revises cualquier error que haya podido ocurrir y llames al método deliverResultToReceiver(), debes definir el método deliverResultToReceiver() que envíe un código de resultado y un conjunto de mensajes al receptor código de resultados.

En el caso del código de resultado, usa el valor que pasaste al método deliverResultToReceiver() en el parámetro resultCode. Si quieres crear un conjunto de mensajes, concatena la constante RESULT_DATA_KEY de tu clase Constants (definida en Cómo recuperar los datos de dirección) y el valor en el parámetro message que se pasa al método deliverResultToReceiver(), como se muestra en el siguiente ejemplo:

Kotlin

    class FetchAddressIntentService : IntentService() {
        private var receiver: ResultReceiver? = null

        // ...

        private fun deliverResultToReceiver(resultCode: Int, message: String) {
            val bundle = Bundle().apply { putString(Constants.RESULT_DATA_KEY, message) }
            receiver?.send(resultCode, bundle)
        }

    }
    

Java

    public class FetchAddressIntentService extends IntentService {
        protected ResultReceiver receiver;
        // ...
        private void deliverResultToReceiver(int resultCode, String message) {
            Bundle bundle = new Bundle();
            bundle.putString(Constants.RESULT_DATA_KEY, message);
            receiver.send(resultCode, bundle);
        }
    }
    

Cómo iniciar el servicio de intents

El servicio de intents, como se definió en la sección anterior, se ejecuta en segundo plano y busca la dirección correspondiente a una ubicación geográfica específica. Cuando inicias el servicio, el marco de trabajo de Android crea instancias del servicio y lo inicia si aún no está en ejecución. Luego crea un proceso si es necesario. Si el servicio ya está ejecutándose, continuará haciéndolo. Como este extiende el IntentService, se cierra automáticamente después de que se procesen todos los intents.

Inicia el servicio desde la actividad principal de tu app y crea un Intent para pasar datos a este. Necesitarás un intent explícito porque quieres que únicamente tu servicio responda al intent. Para obtener más información, consulta los Tipos de intent.

Si quieres crear un intent explícito, especifica el nombre de la clase que se usará para el servicio FetchAddressIntentService.class. Luego, pasa esta información a los extras del intent:

  • Un ResultReceiver para administrar los resultados de la búsqueda de direcciones
  • Un objeto Location que contenga la latitud y longitud para convertir en una dirección

En el siguiente código de ejemplo, puedes ver cómo iniciar el servicio de intents:

Kotlin

    class MainActivity : AppCompatActivity(), ConnectionCallbacks, OnConnectionFailedListener {

        private var lastLocation: Location? = null
        private lateinit var resultReceiver: AddressResultReceiver

        // ...

        private fun startIntentService() {

            val intent = Intent(this, FetchAddressIntentService::class.java).apply {
                putExtra(Constants.RECEIVER, resultReceiver)
                putExtra(Constants.LOCATION_DATA_EXTRA, lastLocation)
            }
            startService(intent)
        }
    }
    

Java

    public class MainActivity extends AppCompatActivity implements
            ConnectionCallbacks, OnConnectionFailedListener {

        protected Location lastLocation;
        private AddressResultReceiver resultReceiver;

        // ...

        protected void startIntentService() {
            Intent intent = new Intent(this, FetchAddressIntentService.class);
            intent.putExtra(Constants.RECEIVER, resultReceiver);
            intent.putExtra(Constants.LOCATION_DATA_EXTRA, lastLocation);
            startService(intent);
        }
    }
    

Precaución: Para garantizar que tu app sea segura, siempre usa un intent explícito cuando inicies un Service y no declares filtros de intent para tus servicios. El uso de un intent implícito para iniciar un servicio representa un riesgo de seguridad porque no puedes saber qué servicio responderá al intent y el usuario no puede ver qué servicio se inicia.

Llama al método startIntentService() cuando el usuario realice una acción que requiera la búsqueda de direcciones (codificación geográfica). Por ejemplo, el usuario podría presionar el botón Buscar dirección en la IU de tu app. En el siguiente fragmento de código, se muestra la llamada al método startIntentService() en el controlador del botón:

Kotlin

    fun fetchAddressButtonHander(view: View) {
        fusedLocationClient?.lastLocation?.addOnSuccessListener { location: Location? ->
            lastKnownLocation = location

            if (lastKnownLocation == null) return@addOnSuccessListener

            if (!Geocoder.isPresent()) {
                Toast.makeText(this@MainActivity,
                        R.string.no_geocoder_available,
                        Toast.LENGTH_LONG).show()
                return@addOnSuccessListener
            }

            // Start service and update UI to reflect new location
            startIntentService()
            updateUI()
        }
    }
    

Java

    private void fetchAddressButtonHander(View view) {
        fusedLocationClient.getLastLocation()
                .addOnSuccessListener(this, new OnSuccessListener<Location>() {
                    @Override
                    public void onSuccess(Location location) {
                        lastKnownLocation = location;

                        // In some rare cases the location returned can be null
                        if (lastKnownLocation == null) {
                            return;
                        }

                        if (!Geocoder.isPresent()) {
                            Toast.makeText(MainActivity.this,
                                    R.string.no_geocoder_available,
                                    Toast.LENGTH_LONG).show();
                            return;
                        }

                        // Start service and update UI to reflect new location
                        startIntentService();
                        updateUI();
                    }
                });
        }
    

Cómo recibir los resultados de la codificación geográfica

Luego del que el servicio de intents administre la solicitud de codificación geográfica, usa un ResultReceiver a fin de mostrar los resultados a la actividad que realizó la solicitud. En esta actividad, define un AddressResultReceiver que extienda el ResultReceiver para administrar la respuesta de FetchAddressIntentService.

El resultado incluye un código de resultado numérico (resultCode), además de un mensaje que contiene los datos de resultado (resultData). Si el proceso de codificación geográfica inversa es exitoso, los resultData incluyen la dirección. Para fracasos, los resultData incluyen texto que describe el motivo. Para obtener detalles de los errores posibles, consulta Cómo enviar la dirección a la actividad que la solicita.

Anula el método onReceiveResult() a fin de administrar los resultados que se entregan al receptor de resultados, como se observa en el siguiente ejemplo de código:

Kotlin

    class MainActivity : AppCompatActivity() {
        // ...
        internal inner class AddressResultReceiver(handler: Handler) : ResultReceiver(handler) {

            override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {

                // Display the address string
                // or an error message sent from the intent service.
                addressOutput = resultData?.getString(Constants.RESULT_DATA_KEY) ?: ""
                displayAddressOutput()

                // Show a toast message if an address was found.
                if (resultCode == Constants.SUCCESS_RESULT) {
                    showToast(getString(R.string.address_found))
                }

            }
        }
    }
    

Java

    public class MainActivity extends AppCompatActivity {

        // ...

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

            @Override
            protected void onReceiveResult(int resultCode, Bundle resultData) {

                if (resultData == null) {
                    return;
                }

                // Display the address string
                // or an error message sent from the intent service.
                addressOutput = resultData.getString(Constants.RESULT_DATA_KEY);
                if (addressOutput == null) {
                    addressOutput = "";
                }
                displayAddressOutput();

                // Show a toast message if an address was found.
                if (resultCode == Constants.SUCCESS_RESULT) {
                    showToast(getString(R.string.address_found));
                }

            }
        }
    }