Inicio Android Localización geográfica en Android (1)

Localización geográfica en Android (1)

por sgoliver
localizacion-1

[mensaje-curso]

En este capítulo vamos a aprender a acceder a los datos de localización o ubicación actual del dispositivo en el que se está ejecutando una aplicación Android. El contar con datos de ubicación va a multiplicar las posibilidades a la hora de crear nuestras aplicaciones, permitiendo ofrecer al usuario información relativa al contexto en el que se encuentra.

Desde el punto de vista del desarrollador, hay que decir que la API de ubicación es una de esas características de Android que quizá no sean tan intuitivas como deberían ser, por lo que es importante estar atento a los detalles, aunque bien es cierto que ha ido mejorando con los años y a día de hoy contamos con muchas «facilidades» que antes no teníamos. Como ejemplo más llamativo, actualmente no tenemos que preocuparnos demasiado, al menos no de forma explícita, de qué proveedor de localización queremos utilizar en cada momento (señal móvil, Wi-Fi, o GPS), ya que tenemos disponible con un nuevo proveedor (Fused Location Provider) que se encargará automáticamente de gestionar todas las fuentes de datos disponibles para obtener la información que nuestra aplicación necesita.

La funcionalidad de localización geográfica fue movida hace ya algún tiempo desde el SDK general de Android a las librerías de los Google Play Services. Por lo tanto, lo primero que tendremos que aprender será a conectarnos a dichos servicios.

El primer paso siempre que queramos hacer uso de los Google Play Services será añadir a la sección de dependencias de nuestro fichero build.gradle la referencia a las librerías necesarias. En el caso de estos servicios existen dos alternativas, o bien añadimos la referencia a la librería completa de Google Play Services, que nos daría acceso a todos los servicios disponibles, o bien utilizamos las librerías independientes de cada servicio que vayamos a utilizar.

Para la primera opción la referencia a añadir sería la siguiente (en el momento de escribir este artículo la versión más reciente de los servicios es la 9.4.0):

dependencies {
    ...
    compile 'com.google.android.gms:play-services:9.4.0'
}

Y si sólo queremos añadir la librería de localización (que también incluye las funciones de Reconocimiento de Actividad y Google Places), utilizaríamos la siguiente:

dependencies {
    ...
    compile 'com.google.android.gms:play-services-location:9.4.0'
}

Por norma general utilizaremos esta segunda alternativa, añadiendo únicamente las librerías de los servicios que vamos a utilizar. Podéis encontrar el listado completo de librerías para los servicios disponibles en el siguiente enlace.

Hecho esto vamos a empezar a crear nuestra aplicación de ejemplo. No nos complicaremos mucho, incluiremos tan sólo un par de etiquetas de texto para mostrar la latitud y longitud recibidas, y un botón para iniciar o parar las actualizaciones de posición (esto último lo veremos en el próximo artículo).

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context="net.sgoliver.android.localizacion.MainActivity">

    <TextView android:id="@+id/lblLatitud"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/latitud" />

    <TextView android:id="@+id/lblLongitud"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/longitud" />

    <ToggleButton android:id="@+id/btnActualizar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textOn="@string/parar_actualizaciones"
        android:textOff="@string/iniciar_actualizaciones" />

</LinearLayout>

Obtenemos como siempre las referencias a estos controles en el método onCreate() de la actividad principal, y crearemos un método auxiliar updateUI() que actualice los campos de latitud y longitud a partir de un objeto Location, que como veremos más adelante será lo que obtengamos del servicio de localización.

import android.location.Location;

//...

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    lblLatitud = (TextView) findViewById(R.id.lblLatitud);
    lblLongitud = (TextView) findViewById(R.id.lblLongitud);
    btnActualizar = (ToggleButton) findViewById(R.id.btnActualizar);

    //...
}

private void updateUI(Location loc) {
    if (loc != null) {
        lblLatitud.setText("Latitud: " + String.valueOf(loc.getLatitude()));
        lblLongitud.setText("Longitud: " + String.valueOf(loc.getLongitude()));
    } else {
        lblLatitud.setText("Latitud: (desconocida)");
        lblLongitud.setText("Longitud: (desconocida)");
    }
}

El objeto Location simplemente encapsula los datos principales de una dirección geográfica, entre otros la latitud y longitud, a los que podemos acceder mediante los métodos getLatitude() y getLongitude() respectivamente. En caso de recibirse un objeto nulo mostraremos un literal de dirección «(desconocida)«, más adelante veremos por qué puede ocurrir esto.

Hechos los preparativos iniciales, empecemos ya a añadir funcionalidad más específica del tema que nos ocupa. Comenzaremos por crear un objeto de tipo GoogleApiClient, que generalmente será el punto de acceso común a los servicios de Google Play. Para la creación de este objeto deberemos indicar la API a la que queremos acceder y cómo queremos gestionar la conexión con los servicios (de forma manual o automática, utilizaremos la segunda opción siempre que sea posible). Habrá que proporcionar también los listeners necesarios para responder a los eventos de conexión/desconexión a los servicios y a posibles errores que pudieran producirse durante la conexión. Veamos en primer lugar el código de creación de este objeto y posteriormente pasaremos a comentar los detalles.

GoogleApiClient apiClient = new GoogleApiClient.Builder(this)
    .enableAutoManage(this, this)
    .addConnectionCallbacks(this)
    .addApi(Location.API)
    .build();

Como puede comprobarse, la configuración del cliente se realiza utilizando un patrón builder a través de la clase GoogleApiClient.Builder.

En primer lugar solicitamos con enableAutoManage() que la gestión de la conexión a los servicios se realice automáticamente, esto nos evitará entre otras cosas tener que conectarnos y desconectarnos manualmente a los servicios en los eventos onStart() y onStop() de la actividad, y además también se gestionarán de forma automática muchos de los posibles errores que puedan producirse durante la conexión (por ejemplo, solicitando al usuario una actualización de los play services en el dispositivo, si fuera necesario). Este método recibe dos parámetros, el primero de ellos es una referencia a la actividad en cuyo ciclo de vida debe integrarse la conexión/desconexión a los servicios, en nuestro caso será la propia actividad principal (this). El segundo parámetro es la referencia al listener (de tipo OnConnectionFailedListener) que se llamará en caso de producirse algún error que el sistema no pueda gestionar automáticamente. En nuestro caso haremos que la propia actividad principal implemente la interfaz OnConnectionFailedListener, por lo que también pasaremos this en este segundo parámetro. Para implementar esta interfaz tendremos que definir el método onConnectionFailed(ConnectionResult), que en nuestro caso de ejemplo se limitará a mostrar un mensaje de error en el log:

@Override
public void onConnectionFailed(ConnectionResult result) {
    //Se ha producido un error que no se puede resolver automáticamente
    //y la conexión con los Google Play Services no se ha establecido.

    Log.e(LOGTAG, "Error grave al conectar con Google Play Services");
}

Lo siguiente que hacemos es indicar el listener que se ocupará de responder a los eventos de conexión y desconexión de los servicios, mediante una llamada a addConnectionCallbacks(). Esto no es obligatorio, aunque en la práctica casi siempre nos interesará conocer cuándo se realiza la conexión y cuándo se pierde, de forma que podamos adaptarnos convenientemente a cada situación (es importante entender que no podemos hacer llamadas a los servicios mientras no estemos conectados a ellos). Una vez más haremos que nuestra actividad implemente la interfaz necesaria, en este caso ConnectionCallbacks, lo que nos obligará a implementar los métodos onConnected(), que se ejecutará cuándo se realice la conexión con los servicios, y onConnectionSuspended(), que se lanzará cuando la conexión se pierda temporalmente (cuando la conexión se recupera volverá a lanzarse el evento onConnected()). En breve veremos el código de estos dos eventos.

Posteriormente indicamos mediante addApi() la API de los servicios a los que vamos a acceder, en este caso Location.API. En caso de querer utilizar diferentes servicios podríamos realizar varias llamadas al método addApi() añadiendo los servicios necesarios.

Por último, construimos el objeto final llamando al método build(). Esto iniciará la conexión con los servicios solicitados, lo que desembocará como hemos indicado en una llamada al evento onConnectionFailed() en caso de error, o al evento onConnected() en el caso de funcionar todo correctamente.

El evento onConnected() se aprovecha frecuentemente para realizar la interacción deseada con el servicio, una vez que estamos seguros que la conexión se ha realizado correctamente. En nuestro caso utilizaremos este evento para obtener una primera posición inicial para inicializar los datos de latitud y longitud de la interfaz. Para ello obtendremos la última posición geográfica conocida, lo que conseguimos llamando al método getLastLocation() del proveedor de datos de localización LocationServices.FusedLocationApi, pasándole como parámetro la referencia al cliente API que hemos creado anteriormente. Por último llamamos a nuestro método auxiliar updateUI() con el valor recibido para mostrar los datos en la actividad principal. Muestro también a continuación el evento onConnectionSuspended(), que para nuestro ejemplo se limitará a mostrar un mensaje en el log.

@Override
public void onConnected(@Nullable Bundle bundle) {
    //Conectado correctamente a Google Play Services

    //...

    Location lastLocation =
        LocationServices.FusedLocationApi.getLastLocation(apiClient);

    updateUI(lastLocation);

    //...
}

@Override
public void onConnectionSuspended(int i) {
    //Se ha interrumpido la conexión con Google Play Services

    Log.e(LOGTAG, "Se ha interrumpido la conexión con Google Play Services");
}

¿Pero por qué hablo de «última posición conocida» y no de «posición actual«?  En Android no existe ningún método del tipo “obtenerPosiciónActual()“. Obtener la posición a través de un dispositivo de localización como por ejemplo el GPS no es una tarea inmediata, sino que puede requerir de un cierto tiempo de espera y procesamiento, por lo que no tendría demasiado sentido proporcionar un método de ese tipo. Lo más parecido que encontramos es el método indicado getLastLocation() que nos devuelve la posición más reciente obtenida por el dispositivo (no necesariamente solicitada por nuestra aplicación). Y es importante entender esto: este método NO devuelve la posición actual, este método NO solicita una nueva posición al proveedor de localización, este método se limita a devolver la última posición que se obtuvo a través del proveedor de localización. Y esta posición se pudo obtener hace unos pocos segundos, hace días, hace meses, o incluso nunca (si el dispositivo ha estado apagado, si nunca se ha activado el GPS, …). Por tanto, cuidado cuando se haga uso de la posición devuelta por el método getLastLocation(). En los casos raros en que no existe ninguna posición conocida en el dispositivo este método devuelve null, algo para lo que ya hemos preparado nuestro método updateUI().

En el código anterior del evento onConnected() he omitido inicialmente, por claridad, un fragmento de código relacionado con la obtención de los permisos necesarios para que la aplicación pueda acceder a datos de localización. A continuación, comentaré un poco este tema de permisos y completaremos el código anterior.

Para empezar, indicar que los permisos relacionados con la ubicación geográfica en Android son básicamente dos:

  • ACCESS_FINE_LOCATION. Permiso para acceder a datos de localización con una precisión alta.
  • ACCESS_COARSE_LOCATION. Permiso para acceder a datos de localización con una precisión baja.

Dependiendo de qué permiso/s solicita nuestra aplicación, los datos de localización obtenidos posteriormente podrán tener una mayor o menor precisión.

Para versiones de Android hasta la 5.0 los permisos necesarios para la aplicación se deben declarar en el fichero AndroidManifest.xml, y deben ser aceptados por el usuario (todos o ninguno) en el momento de instalar la aplicación. En nuestro caso solicitaremos permisos para obtener ubicaciones con la máxima precisión disponible, añadiendo la cláusula correspondiente <uses-permission> a nuestro fichero AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.sgoliver.android.localizacion">

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

    ....

</manifest>

Sin embargo, a partir de Android 6.0 algunos permisos se deben consultar en tiempo de ejecución, con lo que el usuario decidirá si conceder o no, cada uno de ellos, en el momento en que se haga uso de cada funcionalidad que los necesite (y no en la instalación como ocurría en versiones anteriores). Nuestra aplicación debería estar preparada por tanto para todas las situaciones posibles, es decir, debería funcionar sin errores tanto si se le conceden los permisos solicitados como si no (deshabilitando por supuesto en este caso la funcionalidad asociada).

No entraré en mucho detalle dado que no es el tema principal de este artículo, para más información sobre permisos puede consultarse la documentación oficial de Android, pero veamos rápidamente cómo chequear y solicitar permisos en tiempo de ejecución para versiones de Android >= 6.0. Para chequear si nuestra aplicación tiene concedido un determinado permiso utilizamos el método checkSelfPermission(). En el caso de no tenerlo aún concedido, lo solicitaremos al usuario llamado a requestPermissions(), pasándole como parámetro el identificador del permiso deseado y una constante arbitraria definida por nuestra aplicación (PETICION_PERMISO_LOCALIZACION en mi ejemplo) que nos permita identificar posteriormente dicha petición. Para conocer el resultado de la petición sobrescribiremos el evento onRequestPermissionResult(), utilizando la constante indicada para reconocer a qué petición corresponde el resultado recibido.

A continuación completo el código del evento onConnected() con el chequeo/petición de permisos y añado la implementación de onRequestPermissionResult() para actuar según la respuesta del usuario a la petición de permisos en caso de haberse realizado.

@Override
public void onConnected(@Nullable Bundle bundle) {
    //Conectado correctamente a Google Play Services

    if (ActivityCompat.checkSelfPermission(this,
        Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {

        ActivityCompat.requestPermissions(this,
            new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
                PETICION_PERMISO_LOCALIZACION);
    } else {

        Location lastLocation =
            LocationServices.FusedLocationApi.getLastLocation(apiClient);

        updateUI(lastLocation);
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    if (requestCode == PETICION_PERMISO_LOCALIZACION) {
        if (grantResults.length == 1
            && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

            //Permiso concedido

            @SuppressWarnings("MissingPermission")
            Location lastLocation =
                LocationServices.FusedLocationApi.getLastLocation(apiClient);

            updateUI(lastLocation);

        } else {
            //Permiso denegado:
            //Deberíamos deshabilitar toda la funcionalidad relativa a la localización.

            Log.e(LOGTAG, "Permiso denegado");
        }
    }
}

Para nuestro ejemplo, en el evento onRequestPermissionResult(), en el caso de obtener los permisos solicitados hacemos exactamente lo mismo que en el evento onConnected(), es decir, obtener la última posición conocida y actualizar la interfaz. En caso de no recibir los permisos por parte del usuario mostramos simplemente un mensaje de error en el log.

Con esto, ya tendríamos una versión inicial, muy básica, de la aplicación de ejemplo, que al iniciarse obtendría y mostraría la última posición recibida en el dispositivo.

Si ejecutamos en este momento la aplicación en el emulador, lo primero que deberíamos ver es la petición del permiso para acceder a la ubicación:

localizacion-permiso

Si pulsamos en el botón PERMITIR del diálogo para aceptar el permiso solicitado por la aplicación pueden pasar dos cosas. Si la aplicación se está ejecutando en un emulador recién creado, o donde nunca se ha ejecutado ninguna aplicación que acceda a ubicaciones es muy posible que obtengamos una localización nula, por lo que se mostraría el literal «(desconocida)» en los valores de latitud y longitud.

localizacion-desconocida

Si por el contrario ya se había obtenido alguna ubicación en el emulador, o bien si estamos ejecutando la aplicación sobre un dispositivo físico, deberían mostrarse sin problemas los valores correspondientes de latitud y longitud de la ubicación más reciente recibida.

localizacion-lastlocation

Si estás en el primer caso, no te preocupes porque pronto lo vamos a arreglar. En el siguiente artículo veremos como activar la recepción periódica de ubicaciones (mediante el botón «Iniciar Actualizaciones» que hemos añadido a la interfaz) de forma que nuestra aplicación esté periódicamente recibiendo, esta vez sí, la posición real y exacta del dispositivo.

Puedes consultar y/o descargar el código completo de los ejemplos desarrollados en este artículo accediendo a la página del curso en GitHub.

También te puede interesar

12 comentarios

Localización Geográfica en Android [Serie]  | sgoliver.net 06/09/2016 - 10:35

[…] Localización geográfica en Android (Parte 1) [Actualizado Septiembre 2016] […]

Responder
Localización geográfica en Android (2) | sgoliver.net 08/09/2016 - 8:52

[…] el artículo anterior del curso vimos cómo configurar todo lo necesario para acceder a los servicios de localización o […]

Responder
Joel Colmenares 16/09/2016 - 22:28

Me parece excelente este tema! Al llegar a casa voy a implementarlo a ver si me funciona al terminar de estudiarlo bien con uds!

Responder
Paco 17/11/2016 - 23:21

Hice todo tal cual y al ver que no funcionaba, lo baje directamente de github y tambien lo corri tal cual…..Pero sigue dando igual, nunca pide permisos!!!Si esta prendido el wifi entra directamente y da los datos y si esta desactivado, nunca entra a los datos. :/ alguna idea??

Responder
Eduardo Yurén Cruz Baltazar 05/01/2017 - 18:27

Gracias por tu aporte.

Responder
Johan Cruz 06/03/2017 - 23:52

GoogleApiClient apiClient = new GoogleApiClient.Builder(this)
.enableAutoManage(this, this)
.addConnectionCallbacks(this)
.addApi(LocationServices.API) // -> LocationServices
.build();

Responder
Enrique Pérez 11/04/2017 - 0:31

No funciona, le he dado los permisos manualmente, en los ajustes, y nada nunca entra a “onConnected”, he activado el GPS y nada tampoco

Responder
Juan 02/05/2017 - 22:11

Me funciono perfecto!! gracias

Responder
Andres 03/12/2017 - 21:27

muy buen curso, estoy aprendiendo mucho, aunque en esta parte estoy haciendo mas pruebas como por ejemplo intentar guardar mi latitud y longitud con shared preferences para poder utilizarlo en otra activity pero no me lo guarda y no se a que es debido.

Responder
El Vergone 05/01/2018 - 23:15

Para los que dicen que no funciona, primero deben de darse cuenta que la función del botón no ha sido llamado nunca, si revisan git hay dos versiones, en la segunda esta completo ya que es parte de un curso. saludos y MPLVT! xD

Responder
Polsy 03/09/2018 - 19:36

Hay algun video donde se desarrollen todo estos codigos ???

Responder
NomeGusta 23/01/2019 - 10:41

Hola buenas,
¿tienes algún post donde profundizas con el tema de Google Places y autotext?
Muchas gracias, un saludo

Responder

Responder a Johan Cruz Cancelar respuesta

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información. Aceptar Más Información

Política de Privacidad y Cookies