Localización geográfica en Android (2)

Curso Programación Android

Este artículo forma parte del Curso de Programación Android que tienes disponible de forma completamente gratuita en sgoliver.net

En el artículo anterior del curso vimos cómo configurar todo lo necesario para acceder a los servicios de localización o ubicación geográfica de Google Play Services, y como primera opción describimos cómo obtener la última localización conocida del dispositivo. Esta opción es una buena forma de conseguir rápidamente una primera ubicación, que en un dispositivo real suele ser bastante aproximada a menos que llevemos siempre desactivadas todas las opciones de ubicación, pero como ya advertimos no siempre puede corresponderse con la ubicación real actual.

En esta entrega vamos a describir cómo solicitar al sistema datos actualizados, esta vez sí, de la posición actual del dispositivo, por supuesto siempre cumpliendo con el nivel de permisos y precisión que hayamos solicitado.

Como ya dijimos, en Android no tenemos ningún método que nos devuelva directamente la posición actual, entre otras cosas porque es impredecible el tiempo que podemos tardar en obtenerla. Seguiremos por tanto otra estrategia, que consistirá en primer lugar en indicar al sistema nuestros requerimientos, entre ellos la precisión y periodicidad con que nos gustaría recibir actualizaciones de la posición actual, y en segundo lugar definiremos un método encargado de procesar los nuevos datos a medida que se vayan recibiendo.

Pero antes de esto otro tema importante. En la precisión de los datos obtenidos no solo interviene lo que nuestra aplicación solicite, sino también la configuración del dispositivo que el usuario tenga establecida. Por ejemplo, nuestra aplicación no podría obtener, aunque así lo solicite, una ubicación con máxima precisión si el usuario lleva deshabilitada la Ubicación en el dispositivo, o si el modo que tiene seleccionado en las opciones de ubicación de Android no es el de “Alta precisión“. Por tanto, un primer paso importante será chequear de alguna forma si las necesidades de nuestra aplicación son coherentes con la configuración actual establecida en el dispositivo, y en caso contrario solicitar al usuario que la modifique siempre que sea posible.

Vayamos paso a paso. La forma en que nuestra aplicación puede definir sus requerimientos en cuanto a opciones de ubicación será a través de un objeto de tipo LocationRequest. Este objeto almacenará las opciones de ubicación que nuestra aplicación necesita, entre las que destacan:

  • Periodicidad de actualizaciones. Se establece mediante el método setInterval() y define cada cuanto tiempo (en milisegundos) nos gustaría recibir datos actualizados de la posición. De esta forma, si queremos recibir la nueva posición cada 2 segundos utilizaremos setInterval(2000). ¿Y por qué digo “nos gustaría”? Con este método lo único que damos es nuestra preferencia, pero la periodicidad real podría ser mayor o menor dependiendo de muchas circunstancias (conectividad GPS limitada o intermitente, otras aplicaciones han solicitado periodicidades más altas, …).
  • Periodicidad máxima de actualizaciones. El proveedor de localización de Android (Fused Location Provider) proporciona actualizaciones de la ubicación con la periodicidad más alta que haya solicitado cualquier aplicación ejecutándose en el dispositivo (éste es uno de los motivos por los que en el apartado anterior indicábamos que es posible recibir actualizaciones a mayor velocidad de la solicitada). Por este motivo, es importante indicar al sistema a qué periodicidad máxima (también en milisegundos) nuestra aplicación es capaz de procesar nuevos datos de ubicación de forma que no nos provoque problemas de rendimiento o sobrecarga. Este dato lo proporcionaremos mediante el método setFastestInterval().
  • Precisión. La precisión de los datos que queremos recibir se establecerá mediante el método setPriority(). Existen varios valores posibles para definir esta información:
    • PRIORITY_BALANCED_POWER_ACCURACY. Los datos recibidos tendrán una precisión de unos 100 metros. En este modo el dispositivo tendrá un consumo de energía comedido al utilizar normalmente la señal WIFI y de datos móviles para determinar la ubicación.
    • PRIORITY_HIGH_ACCURACY. Es el modo más preciso para obtener la ubicación, por lo que utilizará normalmente la señal GPS.
    • PRIORITY_LOW_POWER. Los datos recibidos tendrán una precisión de unos 10 kilómetros, pero se utilizará muy poca energía para obtener la ubicación.
    • PRIORITY_NO_POWER. En este modo nuestra aplicación solo recibirá datos si éstos están disponibles porque alguna otra aplicación los haya solicitado. Es decir, nuestra aplicación no tendrá un impacto directo en el consumo de energía solicitando nuevas ubicaciones, pero si éstas están disponibles las utilizará.

Con esta información vamos a definir ya el LocationRequest para nuestro ejemplo. Crearemos un método auxiliar enableLocationUpdates() al que llamaremos desde nuestro botón de iniciar/detener las actualizaciones. Para el ejemplo utilizaremos una periodicidad de 2 segundos, una periodicidad máxima de 1 segundo, y una precisión alta (PRIORITY_HIGH_ACCURACY).

private LocationRequest locRequest;

//...

protected void onCreate(Bundle savedInstanceState) {

    //...

    btnActualizar = (ToggleButton) findViewById(R.id.btnActualizar);
    btnActualizar.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            toggleLocationUpdates(btnActualizar.isChecked());
        }
    });

    //...
}

private void toggleLocationUpdates(boolean enable) {
    if (enable) {
        enableLocationUpdates();
    } else {
        disableLocationUpdates();
    }
}

private void enableLocationUpdates() {

    locRequest = new LocationRequest();
    locRequest.setInterval(2000);
    locRequest.setFastestInterval(1000);
    locRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

    //...
}

Definidos nuestros requisitos vamos ahora a comprobar si la configuración actual del dispositivo es coherente con ellos. Para ello construiremos, a continuación dentro de enableLocationUpdates(), un objeto LocationSettingsRequest mediante su builder, al que pasaremos el LocationRequest definido en el paso anterior.

LocationSettingsRequest locSettingsRequest =
    new LocationSettingsRequest.Builder()
        .addLocationRequest(locRequest)
        .build();

Con esto queremos que de alguna forma el sistema compare los requisitos de nuestra aplicación con la configuración actual. ¿Pero cómo ejecutamos y conocemos el resultado de dicha comparación? Para esto llamaremos al método checkLocationSettings() de la API de localización, al que pasaremos la instancia de nuestro cliente API y del LocationSettingsRequest que acabamos de construir. El resultado vendrá dado en forma de objeto PendingResult, del que tendremos de definir su evento onResult() para conocer el resultado de la comparación una vez esté disponible. Este evento recibe como parámetro un objeto LocationSettingsResult, cuyo método getStatus() contiene el resultado de la comparación.

Contemplaremos tres posibles resultados:

  • SUCCESS. Significará que la configuración del dispositivo es válida para nuestros requisitos de información.
  • RESOLUTION_REQUIRED. Indica que la configuración actual del dispositivo no es suficiente para nuestra aplicación, pero existe una posible solución por parte del usuario (por ejemplo: solicitarle que active la ubicación en el dispositivo o que cambie su modalidad).
  • SETTINGS_CHANGE_UNAVAILABLE. Indica que la configuración del dispositivo no es suficiente y además no existe ninguna acción del usuario que pueda solucionarlo.

En el primer caso ya podríamos solicitar el inicio de las actualizaciones de localización, ya que sabemos que la configuración del dispositivo es correcta. En el tercer caso, no nos quedaría más opción que mostrar algún mensaje al usuario indicando que no es posible obtener la ubicación, o bien deshabilitar la funcionalidad relacionada.

Y el caso más interesante, el segundo, necesitamos solicitar al usuario que cambie la configuración del sistema. Por suerte, esta solicitud está ya implementada en la api, por lo que tan sólo tendremos que llamar al método startResolutionForResult() sobre el estado recibido en el evento onResult(). Este método recibe una referencia a la actividad principal y una constante arbitraria (que podemos definir con cualquier valor único) que después nos servirá para obtener el resultado de la operación.

Veamos todo lo anterior sobre el código.

private void enableLocationUpdates() {

    locRequest = new LocationRequest();
    locRequest.setInterval(2000);
    locRequest.setFastestInterval(1000);
    locRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

    LocationSettingsRequest locSettingsRequest =
        new LocationSettingsRequest.Builder()
            .addLocationRequest(locRequest)
            .build();

    PendingResult<LocationSettingsResult> result =
        LocationServices.SettingsApi.checkLocationSettings(
                apiClient, locSettingsRequest);

    result.setResultCallback(new ResultCallback<LocationSettingsResult>() {
        @Override
        public void onResult(LocationSettingsResult locationSettingsResult) {
            final Status status = locationSettingsResult.getStatus();
            switch (status.getStatusCode()) {
                case LocationSettingsStatusCodes.SUCCESS:

                    Log.i(LOGTAG, "Configuración correcta");
                    startLocationUpdates();
                    break;

                case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
                    try {
                        Log.i(LOGTAG, "Se requiere actuación del usuario");
                        status.startResolutionForResult(MainActivity.this, PETICION_CONFIG_UBICACION);
                    } catch (IntentSender.SendIntentException e) {
                        btnActualizar.setChecked(false);
                        Log.i(LOGTAG, "Error al intentar solucionar configuración de ubicación");
                    }
                    break;
    
                case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
                    Log.i(LOGTAG, "No se puede cumplir la configuración de ubicación necesaria");
                    btnActualizar.setChecked(false);
                    break;
            }
        }
    });
}

Nos faltaría saber el resultado de la solicitud realizada al usuario para cambiar la configuración en el caso de RESOLUTION_REQUIRED. Para ello sobrescribiremos el método onActivityResult() de la actividad principal, y atenderemos el caso en el que el requestCode recibido sea igual a la constante que utilizamos en el método startResolutionForResult(). Existen dos posibles resultados:

  • RESULT_OK. Indica que el usuario ha realizado el cambio solicitado. En este caso ya podremos solicitar el inicio de las actualizaciones de ubicación.
  • RESULT_CANCELED. Indica que el usuario no ha realizado ningún cambio. En nuestro caso de ejemplo mostraremos un error en el log y desactivaremos el botón de inicio de las actualizaciones.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
        case PETICION_CONFIG_UBICACION:
            switch (resultCode) {
                case Activity.RESULT_OK:
                    startLocationUpdates();
                    break;
                case Activity.RESULT_CANCELED:
                    Log.i(LOGTAG, "El usuario no ha realizado los cambios de configuración necesarios");
                    btnActualizar.setChecked(false);
                    break;
            }
            break;
    }
}

Pues bien, después de todo esto, ya nos quedaría únicamente saber como solicitar el inicio de las actualizaciones de localización del dispositivo. Esto lo haremos en un método auxiliar startLocationUpdates(). Esta acción es muy sencilla, basta con llamar al método requestLocationUpdates() de la API de localización, pasándole como parámetros nuestro cliente API, el objeto LocationRequest construido al inicio y una referencia al objeto que implementará la interfaz LocationListener, cuyo método onLocationChanged() recibirá los datos de ubicación actualizados. En nuestro caso, haremos que sea nuestra actividad principal la que implemente esta interfaz. Definiremos por tanto el evento indicado en nuestra actividad, que se limitará a llamar a nuestro método auxiliar updateUI() con los nuevos datos recibidos.

public class MainActivity extends AppCompatActivity
        implements GoogleApiClient.OnConnectionFailedListener,
        GoogleApiClient.ConnectionCallbacks,
        LocationListener {

    //...

    private void startLocationUpdates() {
        if (ActivityCompat.checkSelfPermission(MainActivity.this,
                Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {

            //Ojo: estamos suponiendo que ya tenemos concedido el permiso.
            //Sería recomendable implementar la posible petición en caso de no tenerlo.

            Log.i(LOGTAG, "Inicio de recepción de ubicaciones");

            LocationServices.FusedLocationApi.requestLocationUpdates(
                    apiClient, locRequest, MainActivity.this);
        }
    }

    @Override
    public void onLocationChanged(Location location) {

        Log.i(LOGTAG, "Recibida nueva ubicación!");

        //Mostramos la nueva ubicación recibida
        updateUI(location);
    }

    //...
}

Por último, para detener la actualización de ubicaciones, tan sólo tendremos que llamar al método removeLocationUpdates() de la API de localización, lo que haremos en un método auxiliar disableLocationUpdates() que a su vez llamaremos cuando corresponda al pulsar el botón de iniciar/detener actualizaciones.

private void disableLocationUpdates() {

    LocationServices.FusedLocationApi.removeLocationUpdates(
            apiClient, this);

}

Y con esto habríamos terminado. Ya estaríamos listos para ejecutar la aplicación de ejemplo en el emulador o en un dispositivo real. Para ello, configuraremos primero el dispositivo para desactivar las opciones de ubicación y poder comprobar si nuestra aplicación detecta correctamente esta situación.

ubicacion_off

Ejecutamos ahora la aplicación y aceptamos los permisos de localización si se nos solicita (en Android 6 o superior). Pulsamos el botón de “INICIAR ACTUALIZACIONES” y deberíamos ver un diálogo como el siguiente, donde se nos indica que debemos habilitar las opciones de Ubicación para usar la señal móvil, Wi-Fi y GPS (recordemos que hemos solicitado ubicaciones con la máxima precisión).

activar_gps

Si pulsamos “SÍ” se habilitarán automáticamente estas opciones (Ubicación activada y modo de “Alta precisión“) y nuestra aplicación debería comenzar a recibir actualizaciones de ubicación tal y como habíamos previsto.

Sin embargo, si estamos ejecutando la aplicación en un emulador no veremos ningún cambio en la ubicación. Latitud y Longitud quedarán con valor fijo (última posición conocida) o bien con valor “(desconocido)”. ¿Por qué ocurre esto? Muy sencillo, el emulador, al no ser un dispositivo real, no recibe señal móvil ni GPS, por lo que es incapaz de obtener la ubicación actual a partir de dicha información.

Por suerte, el emulador ofrece un método alternativo para simular que el dispositivo recibe actualizaciones de ubicación. Estas opciones se pueden encontrar accediendo a los controles extendidos del emulador, en la sección “Location“.

controles extendidos emulador android

Accediendo a estos controles tendremos dos alternativas para enviar ubicaciones a nuestro emulador, una manual y otra automática. La manual, situada en la parte superior, nos permite introducir un valor de latitud-longitud y enviarlo al emulador mediante el botón “SEND”, de uno en uno. Si lo hacemos mientras nuestra aplicación se está ejecutando y nuestro botón de actualizaciones está activado, veremos cómo el dato de latitud-longitud introducido en las opciones del emulador aparece en nuestra aplicación (es posible que tarde un poco en aparecer, en general los controles extendidos del emulador van relativamente lentos dependiendo de los recursos del equipo de trabajo).

ubicaciones-emulador-manual

Este método, aunque efectivo, es algo laborioso si queremos probar que nuestra aplicación recibe actualizaciones de la ubicación con cierta frecuencia. Para solucionar esto podemos utilizar la segunda de las opciones, que nos permite automatizar el envío al emulador de un listado de ubicaciones a una cierta velocidad.

El listado de ubicaciones debe estar en formato GPX o KML. En mi caso particular, he elegido KML por su simplicidad. Desde este enlace podéis descargar un fichero KML de ejemplo, que podéis abrir/editar con cualquier editor de texto para adaptarlo a vuestras necesidades. Como podéis comprobar no es más que un listado de pares de latitud-longitud con una estructura muy sencilla de etiquetas tipo XML.

Para cargar este fichero pulsaremos sobre el botón inferior “LOAD GPX/KML” y seleccionaremos nuestro fichero de prueba. Inmediatamente (o como digo, no tan inmediato) aparecerán en la lista superior nuestro listado de ubicaciones. A continuación seleccionaremos la velocidad a la que queremos enviar estos valores al emulador con el desplegable de la parte inferior izquierda (Speed 1X – 5X) y por último pulsamos el botón de “Play” de la izquierda.

ubicaciones-emulador-automatico

Hecho esto, ya deberíamos empezar a ver en nuestra aplicación cómo se reciben periódicamente los valores de ubicación de nuestro listado de prueba.

demo-localizacion-final

Y con esto finalizaríamos con los servicios básicos de ubicación de Google Play Services. Hemos aprendido cómo hacer que nuestras aplicaciones obtengan la localización actual del dispositivo, pasando para ello por conocer cómo conectarnos a los servicios correspondientes de Google Play, y cómo lidiar con los permisos y configuraciones requeridas para el acceso a este tipo de información.

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.