[mensaje-curso]
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.
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).
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«.
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).
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.
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.
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.
14 comentarios
[…] Localización geográfica en Android (Parte 2) [Act. Septiembre 2016] […]
[…] 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 […]
Soy un fan tuyo,. Muchas gracias por tu dedicación y entrega. Me has ayudado mucho. Explicas las cosas de una forma sencilla sin perder contenido e importancia.
No tengo mucho mas que ofrecerte,,,, que mi agradecimiento.
Cuando empiece este nuevo emprendimiento a despegar ,,, te lo agradecere ,,, pero ya con dinerito.
Lo dicho….. Saludos y muchas gracias.
Chutxi,,, Un Castellano en Bolivia.
Me funciona super bien en el emulador, pero cuando quiero convertirlo a una APK me saltan errores :(
Muchas gracias por tu dedicación, todo lo que explicas se entiende perfectamente y me ha servido y funcionado muy bien.
Quería saber si esto mismo se puede aplicar a un servicio ya que quiero que siga actualizando (y utilizando) la localización incluso si cerré mi MainActivity con la idea de llamarlo (volver a abrir el Main) en algún momento.
Hola! Primero de todo quiero decir que me encanta esta página, me está ayudando muchísimo. Mis felicitaciones al autor.
Por otro lado, tengo un problema del que espero que alguien pudiese tener alguna solución. El caso es que he creado esta app y en mi smartphone funciona perfectamente, pero cuando intento lanzarla al emulador éste me dice que la app no funcionará hasta que no actualice los servicios de google play. Además de ese mensaje tiene un botón donde dice actualizar, así que lo pulso, pro no sucede nada. He intentado solucionarlo actualizando en el sdk manager y algunas cosas más, ninguna funciona. Cada vez que intento emular me salta el mismo mensaje y en ningún caso el botón de actualizar parece funcionar.
¿Alguna idea de qué pude ocurrir?
Muchas gracias.
la verdad de esto no se ni pio pero me parece tan interesante en especial lo de el desarrollo de GPS, muchas gracias por su tiempo y dedicación como le digo no se nada pero con sus explicaciones en menos de20 horas ya estoy entendiendo lo q se hace gracias .invito a los nuevos que con un poco de dedicacion se puede aprender
Joder funciona excelente gracias enserio que buen post.
Si funciona Genial, gracias por compartirlo.
hola que tal!, excelente tutorial, lo explicas muy bien y no hay dudas en lo que vas desarrollando, pero si tengo una duda, cómo puedo guardar esas coordenadas en una base de datos?, espero me puedas ayudar, saludos
Roberto Rivera
Excelentes aportes… tengo una duda, que debo de hacer para que se actualicen las coordenadas estando en segundo plano?
Lo mejor que he podido encontrar, feliciades.
Hola. En primer lugar agradecerte el tutorial porque está realmente bien.
Por otro lado me gustaría saber cual sería la mejor opción para obtener la posición (longitud y latitud) en un instante determinado, sin importar si tarda demasiado en realizar el obtener este dato, pero sí que la posición sea lo más exacta posible en un momento determinado.
Gracias.
Buenas tardes,
Antes de nada muchas gracias por el articulo es genial, llevo buscando esto bastante tiempo.
Por otro lado me gustaría implementar esto como una clase de un programa mio, pero no se como implementarlo. Me podrías ayudar?