Exibir o endereço de um local

As lições Ver o último local conhecido e Receber atualizações de local descrevem como ver o local do usuário na forma de um objeto Location que contém coordenadas de latitude e longitude. Embora a latitude e a longitude sejam usadas para calcular a distância ou exibir uma posição no mapa, em muitos casos o endereço da localização é mais útil. Por exemplo, se você quer que os usuários saibam onde estão ou o que há por perto, um endereço é mais significativo do que as coordenadas geográficas (latitude/longitude) do local.

Usando a classe Geocoder nas APIs Location do framework do Android, você pode converter um endereço nas coordenadas geográficas correspondentes. Esse processo é denominado geocodificação. Como alternativa, você pode converter uma localização geográfica em um endereço. O recurso de pesquisa de endereço também é conhecido como geocodificação reversa.

Esta lição mostra como usar o método getFromLocation() para converter uma localização geográfica em um endereço. O método retorna um endereço estimado, correspondente a uma determinada latitude e longitude.

Ver uma localização geográfica

O último local conhecido do dispositivo é um ponto de partida útil para o recurso de pesquisa de endereço. A lição sobre como Ver o último local conhecido mostra como usar o método getLastLocation() oferecido pelo provedor de localização combinada para encontrar o local mais recente do dispositivo.

Para acessar o provedor de localização combinada, crie uma instância de FusedLocationProviderClient. Para saber como criar seu cliente, consulte Criar cliente de serviços de localização.

Para permitir que o provedor de localização combinada recupere um endereço preciso, defina a permissão de localização no manifesto do app como ACCESS_FINE_LOCATION, conforme mostrado no exemplo a seguir:

    <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>
    

Definir um serviço de intent para buscar o endereço

O método getFromLocation() disponibilizado pela classe Geocoder aceita uma latitude e uma longitude e retorna uma lista de endereços. O método é síncrono e pode ser muito demorado. Portanto, não é recomendado chamá-lo na linha de execução principal da interface de usuário (IU) do app.

A classe IntentService oferece uma estrutura para executar uma tarefa em uma linha de execução em segundo plano. Usando essa classe, você pode processar uma operação longa sem afetar a capacidade de resposta da IU.

Defina uma classe FetchAddressIntentService que estenda IntentService. Essa classe é seu serviço de pesquisa de endereço. O serviço processa um intent de maneira assíncrona na linha de execução de um worker e para automaticamente quando fica sem trabalho. Os extras de intent disponibilizam os dados necessários para o serviço, incluindo um objeto Location para conversão em um endereço e um objeto ResultReceiver para processar os resultados da pesquisa de endereço. O serviço usa um Geocoder para buscar o endereço do local e envia os resultados para o ResultReceiver.

Definir o serviço de intent no manifesto do app

Adicione ao manifesto do app uma entrada que defina o serviço de intent, conforme mostrado aqui:

    <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>
    

Observação: o elemento <service> no manifesto não precisa incluir um filtro de intent, porque sua atividade principal cria um intent explícito especificando o nome da classe a ser usada.

Criar geocodificador

O processo de conversão de uma localização geográfica em um endereço é denominado geocodificação reversa. Para realizar o trabalho principal do serviço de intent (sua solicitação de geocodificação reversa), implemente onHandleIntent() na classe FetchAddressIntentService. Crie um objeto Geocoder para processar a geocodificação reversa.

Uma localidade representa uma região geográfica ou linguística específica. Os objetos de localidade ajustam a apresentação de informações, como números ou datas, de acordo com as convenções na região representada pela localidade. Passe um objeto Locale para o objeto Geocoder para garantir que o endereço resultante esteja localizado na região geográfica do usuário. Veja um exemplo:

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());
        // ...
    }
    

Recuperar o endereço

Agora você pode recuperar o endereço do geocodificador, corrigir possíveis erros e enviar os resultados de volta à atividade que solicitou o endereço. Para relatar os resultados do processo de geocodificação, você precisa de duas constantes numéricas que indiquem êxito ou falha. Defina constantes que possam conter valores, conforme mostrado neste snippet 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 ver um endereço correspondente a uma localização geográfica, chame getFromLocation(), passando a ele a latitude e a longitude do objeto de localização e o número máximo de endereços a serem retornados. Nesse caso, você quer apenas um endereço. O geocodificador retorna uma matriz de endereços. Se não houver endereços correspondentes ao local especificado, ele retornará uma lista vazia. Se não houver serviço de geocodificação de back-end disponível, o geocodificador retornará nulo.

Verifique os seguintes erros, conforme a amostra de código abaixo:

  • No location data provided: os extras de intent não incluem o objeto Location necessário para a geocodificação reversa.
  • Invalid latitude or longitude used: os valores de latitude e/ou longitude informados no objeto Location são inválidos.
  • No geocoder available: o serviço de geocodificação em segundo plano não está disponível devido a um erro de rede ou exceção de E/S.
  • Sorry, no address found: o geocodificador não consegue encontrar um endereço para a latitude/longitude especificada.

Se ocorrer um erro, coloque a mensagem correspondente na variável errorMessage para enviá-la de volta à atividade solicitante.

Para ver as linhas individuais de um objeto de endereço, use o método getAddressLine() disponibilizado pela classe Address. Combine as linhas em uma lista de fragmentos do endereço pronta para retornar à atividade que o solicitou.

Para enviar os resultados de volta à atividade solicitante, chame o método deliverResultToReceiver() (definido em Retornar o endereço ao solicitante). Os resultados consistem no código numérico de êxito/falha mencionado anteriormente e em uma string. No caso de uma geocodificação reversa bem-sucedida, a string contém o endereço. No caso de falha, a string contém a mensagem de erro, conforme esta amostra de código:

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));
        }
    }
    

Retornar o endereço ao solicitante

A ação final que o serviço de intent precisa concluir é enviar o endereço de volta a um ResultReceiver na atividade que iniciou o serviço. A classe ResultReceiver permite enviar um código de resultado numérico, bem como uma mensagem contendo os dados do resultado. O código numérico é útil para relatar o êxito ou a falha da solicitação de geocodificação. No caso de uma geocodificação reversa bem-sucedida, a mensagem contém o endereço. No caso de falha, a mensagem contém um texto que descreve o que deu errado.

Você já recuperou o endereço do geocodificador, interceptou os possíveis erros e chamou o método deliverResultToReceiver(). Agora, é preciso definir o método deliverResultToReceiver() que envia um código de resultado e o pacote de mensagens ao destinatário do resultado.

Para o código do resultado, use o valor que você passou para o método deliverResultToReceiver() no parâmetro resultCode. Para criar o pacote de mensagens, concatene a constante RESULT_DATA_KEY da classe Constants (definida em Recuperar o endereço) e o valor no parâmetro message que é passado para o método deliverResultToReceiver(), conforme a amostra a seguir:

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);
        }
    }
    

Iniciar o serviço de intent

O serviço de intent, conforme definido na seção anterior, é executado em segundo plano e busca o endereço correspondente a uma determinada localização geográfica. Quando você inicia o serviço, o framework do Android instancia e inicia o serviço, caso ainda não esteja em execução, e cria um processo, se necessário. Se o serviço já estiver em execução, esse processo não será interrompido. Como o serviço estende IntentService, ele será encerrado automaticamente depois que todos os intents forem processados.

Inicie o serviço na atividade principal do app e crie um Intent para passar dados para o serviço. É necessário usar um intent explícito, porque você quer que apenas seu serviço responda ao intent. Para mais informações, consulte Tipos de intents.

Para criar um intent explícito, especifique o nome da classe a ser usada para o serviço: FetchAddressIntentService.class. Passe estas informações nos extras de intent:

  • Um ResultReceiver para processar os resultados da pesquisa de endereço.
  • Um objeto Location contendo a latitude e a longitude que você quer converter em um endereço.

A amostra de código a seguir demonstra como iniciar o serviço de intent:

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);
        }
    }
    

Cuidado: para garantir que o app esteja protegido, use sempre um intent explícito ao iniciar um Service e não declare filtros de intent para os serviços. O uso de um intent implícito para iniciar um serviço representa um risco de segurança porque não é possível determinar qual serviço responderá ao intent, e o usuário não poderá ver qual serviço é iniciado.

Chame o método startIntentService() acima quando o usuário realizar uma ação que exija uma pesquisa de endereço de geocodificação. Por exemplo, o usuário pode pressionar um botão Buscar endereço na IU do app. O snippet de código a seguir mostra a chamada para o método startIntentService() no gerenciador de botões:

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();
                    }
                });
        }
    

Receber os resultados de geocodificação

Depois que o serviço de intent processa a solicitação de geocodificação, ele usa um ResultReceiver para retornar os resultados à atividade que fez a solicitação. Nessa atividade, defina um AddressResultReceiver que estenda ResultReceiver para processar a resposta de FetchAddressIntentService.

O resultado inclui um código numérico (resultCode), bem como uma mensagem contendo os dados do resultado (resultData). Se o processo de geocodificação reversa for bem-sucedido, o resultData conterá o endereço. No caso de falha, o resultData contém um texto que descreve o que deu errado. Para ver mais detalhes sobre os possíveis erros, consulte Retornar o endereço ao solicitante.

Modifique o método onReceiveResult() para processar os resultados entregues ao destinatário correspondente, conforme a amostra de código a seguir:

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));
                }

            }
        }
    }