Anda di halaman 1dari 39

CURSO DE DESARROLLO DE APLICACIONES ANDROID

Tema 16

Mapas y Localización
TEMA 16. MAPAS Y LOCALIZACIÓN

Introducción

Android permite la implementación de aplicaciones basadas en localización y que utilicen


mapas gracias a las APIs contenidas en la API de Google Play Services.

La API Google Maps para Android ha sido recientemente actualizada a su versión 2 y se incluye
en el paquete com.google.android.gms.maps que es parte de la API Google Location
Services. Las clases contenidas en esta librería gestionan automáticamente descargas,
visualizaciones y cacheado de mapas, además de proporcionar diversos controles y opciones
de visualización, por lo que será el medio más óptimo para integrar mapas y servicios de
localización en una aplicación.

La clase principal de la API de Google Maps para Android (v2) es MapFragment, que muestra
mapas gracias a los datos obtenidos del servicio de Google Maps. Cuando dicha clase obtiene
el foco, capturará pulsaciones y gestos sobre la pantalla (como, por ejemplo, pinch-to-zoom) y
gestionará las peticiones de nuevos mapas cuando sea necesario. Además, proporciona todos
los elementos gráficos necesarios para que el usuario pueda controlar el mapa de forma visual
así como la posibilidad de realizar dichas acciones de forma programática.

Esta librería no está incluida por defecto en la plataforma Android. Para integrar Google Maps
en una aplicación será necesario instalar las librerías de Google Play Services, las cuales están a
disposición de los desarrolladores como add-on de la SDK 1.

Para poder utilizar los mapas de Google, el desarrollador deberá registrarse en el servicio de
Google Maps para obtener una clave para la API Maps (v2), ya que el acceso a datos de mapas
Google está controlado.

La obtención de esta clave implica la instalación previa de la SDK de Google Play Services para
desarrolladores, a través del SDK Manager. También se deberá instalar una versión compatible
de las APIs de Google (para Android 4.2.2 o superior) así como crear un AVD cuyo target sea
una versión de Android 4.2.2 o superior, que incluya dichas APIs de Google.

En el registro (en Google APIs Console) se deberá proveer la huella digital del certificado SHA-1
que se use para firmar la aplicación 2. Este servicio de registro de Google Maps devolverá una
referencia de la Clave para la API Maps (v2) que deberá incluirse en todas las solicitudes de
mapas que realice la aplicación.

Una vez obtenida la clave para la API, se deberán especificar unos ajustes concretos en el
manifiesto, para poder usar la API Maps en la aplicación.

1
Estas librerías se podrán descargar a través del SDK Manager de Android.
2
Para poder publicar una aplicación en Google Play será necesario firmarla digitalmente.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 2


TEMA 16. MAPAS Y LOCALIZACIÓN

Requisitos de uso

El uso de la API de Google Maps en una aplicación implica la inclusión del texto de atribución
de Google Play Services en la sección “Avisos Legales” que deberá incluir la aplicación. Esta
sección deberá incluirse como un ítem independiente del menú o como parte de la sección
“Sobre” (“About”).

El texto está disponible invocando al método:

GooglePlayServicesUtil.getOpenSourceSoftwareLicenseInfo(Context);

Preparación de Eclipse. Instalación de Google Play Services

Se deberá copiar, en el Workspace de Eclipse, el proyecto-librería del cliente de Google Play


Services, que luego será referenciado desde las aplicaciones como librería. Dicho proyecto se
encuentra en:

<android-sdk>/extras/google/google_play_services/libproject/google-play-services_lib/

Una vez copiada la carpeta google-play-services_lib en el Workspace de Eclipse, se importará al


IDE a través del menú File > Import > Android > Existing Android Code into Workspace.

Para referenciar a este proyecto-librería desde una aplicación Android en Eclipse, se


seleccionará el proyecto de la aplicación, y se accederá al menú Project > Properties > Android.
En el panel inferior derecho, Library, se pulsará el botón Add... para elegir el proyectos-librería
google-play-services_lib.

En el manifiesto de la aplicación que utilice esta librería, deberán declararse todos y cada uno
de los elementos referenciados directamente de esta librería (actividades, servicios,
receptores de broadcast y proveedores de contenido) así como todos los permisos adicionales
que sean necesarios, tal y como se verá más adelante.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 3


TEMA 16. MAPAS Y LOCALIZACIÓN

La actualización de Google Play Services se ha lanzado a todos los dispositivos con Android 2.2
o superior, a través de Google Play. No obstante, esto no garantiza que todos estos
dispositivos tengan instalada una versión de la APK de Google Play Services compatible, por lo
que será necesario comprobar si está disponible. El proyecto-librería del cliente de Google Play
Services proporciona métodos para comprobar si la APK Google Play Services es
suficientemente moderna como para soportar dicha librería cliente y, en caso de no serlo,
enviará al usuario a Google Play Store para actualizar dicha APK de Google Play Services.

Generalmente, esta comprobación se realizará en el método onResume() de la actividad


adecuada, invocando al método estático isGooglePlayServicesAvailable(), de la clase
GooglePlayServicesUtil, que devolverá distintos códigos (int) entre los que se encuentran:

• ConnectionResult.SUCCESS: Google Play Services está disponible y actualizado.


• ConnectionResult.SERVICE_MISSING.
• ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED.
• ConnectionResult.SERVICE_DISABLED.

En caso de obtener un código de error (distinto de SUCCESS), será necesario invocar a


getErrorDialog() para mostrar el mensaje de error al usuario y ofrecerle automáticamente
las opciones para descargar, actualizar o habilitar Google Play Services.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 4


TEMA 16. MAPAS Y LOCALIZACIÓN

En principio, las aplicaciones que usan la API de Google Maps deberán ser probadas en
dispositivos reales ya que los AVDs no incluyen el APK de Google Play Services, ni otros APKs
necesarios (como Google Play Store). No obstante, se pueden instalar en el AVD, a través del
comando “adb install <nombre APK>”, la última versión de los APKs de Google Play Services y
de Google Play Store. Estos APK (com.google.android.gms.apk, com.android.vending.apk)
pueden ser obtenidos en Internet o pueden ser extraídos de un dispositivo real, con acceso
root, por ejemplo usando la aplicación Titanium Backup Pro 3.

3
El AVD deberá configurarse para la plataforma Android 2.3 o superior (se evitarán AVDs con las APIs de Google
preinstaladas) Se puede obtener más información de este procedimiento en estos enlaces:

http://stackoverflow.com/questions/14040185/running-google-maps-v2-on-android-
emulator/14271933#14271933

http://stackoverflow.com/questions/13691943/this-app-wont-run-unless-you-update-google-play-
services-via-bazaar/13869332#13869332

CURSO DE DESARROLLO DE APLICACIONES ANDROID 5


TEMA 16. MAPAS Y LOCALIZACIÓN

Obtención y uso de la clave para la API Maps v2

Para poder obtener una clave de uso de la API de Google Maps v2, es necesario registrarse
previamente en Google APIs Console 4 y conseguir un certificado para firmar la aplicación.

La obtención de la clave es gratuita y se permite su uso simultáneo en todas las aplicaciones de


un desarrollador que utilicen la API Maps. Soporta, además, un número ilimitado de usuarios.
La clave para la API Maps será obtenida a través de Google APIS Console al proporcionar la
firma digital de la aplicación que utilizará la API Maps, así como su nombre de paquete. Esta
clave deberá ser añadida como elemento al manifiesto de la aplicación.

Todas las aplicaciones Android deben ser firmadas con un certificado digital del cual el
desarrollador posee le clave privada. Como todos los certificados digitales son únicos,
proporcionan una forma de identificar la aplicación en los sistemas de Google, así como
identificar el uso que hace de los recursos de Google Maps.

La clave de uso de la API de Google Maps v2 se genera para cada certificado y nombre de
paquete 5 realizando los siguientes cuatro pasos:

1. Se obtendrá de información del certificado de la aplicación.


2. Se creará un proyecto en Google APIs Console y se añadirá la API Maps como un
servicio del proyecto.
3. Se solicitarán una o más claves una vez configurado el proyecto.
4. Se añadirá una de las claves obtenidas al manifiesto de la aplicación.

Obtención de información del certificado

La clave de la API Maps se basa en la huella digital SHA-1 del certificado digital de la aplicación.
Dicha huella es una cadena de texto, única, generada por un algoritmo hash, y dependerá del
tipo de certificado que se posea (certificado debug y certificado release).

Durante el desarrollo de una aplicación, el desarrollador podrá obtener una clave provisional
para el uso de la API de Maps, generada a través del certificado debug. Este certificado es
generado por las tools de la SDK de Android y permite el uso temporal de la API Maps. No
obstante, cuando la aplicación sea publicada en Google Play, deberá estar firmada con un
certificado real (release), lo cual obligará a sustituir la clave debug por la clave asociada a dicho
certificado.

Cuando se está desarrollando y depurando una aplicación, las herramientas de compilación de


la SDK de Android firman automáticamente la aplicación utilizando el certificado debug. Para

4
https://code.google.com/apis/console/
5
Es decir, el par “certificado” + “nombre de paquete de la aplicación” genera una clave de Google Maps única.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 6


TEMA 16. MAPAS Y LOCALIZACIÓN

poder mostrar mapas durante este periodo, serán necesario obtener la clave temporal para el
uso de la API Maps, registrando la aplicación a través del certificado de debug. Por lo tanto, es
necesario obtener inicialmente la huella digital del certificado de debug. Para generar dicha
huella, primero es necesario localizar el almacén de claves debug (archivo llamado
debug.keystore). Por defecto, las herramientas de compilación crean dicho almacén de claves
en el directorio donde se almacena la configuración de los AVDs. La localización por defecto de
dicho directorio varía en función del sistema operativo utilizado:

• Windows 7 y Vista: C:\Users\<user>\.android\debug.keystore


• Windows XP: C:\Documents and Settings\<user>\.android\debug.keystore
• OS X y Linux: ~/.android/debug.keystore

Si se utiliza Eclipse con su plugin ADT, la localización del almacén de claves debug podrá ser
consultada accediendo al menú Window > Preferences > Android > Build.

Una vez localizado el almacén de claves debug se utilizará la herramienta Keytool incluida en la
instalación de la JDK para obtener la huella SHA-1 del certificado debug.

El uso de esta herramienta se realiza a través de la consola de comandos del sistema operativo
(accesible en Windows si se escribe “cmd” en Menú Inicio > Ejecutar… y se pulsa el botón
Aceptar). En Windows, por lo tanto, será muy útil hacer accesible dicho comando desde
cualquier ubicación para lo cual se deberá añadir el directorio “bin” de la JDK que se haya
instalado, en la variable de entorno Path. Para modificar dicha variable de entorno, se
accederá a Menú Inicio > Ejecutar (accesible también a través de la combinación de teclas
“Win+E”) y se escribirá sysdm.cpl.

Este comando abrirá el cuadro de diálogo “Propiedades del sistema”. En la pestaña “Opciones
avanzadas”, se pulsará el botón “Variables de entorno”, que abrirá el cuadro de diálogo donde
se podrá editar la variable de entorno (de sistema) Path.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 7


TEMA 16. MAPAS Y LOCALIZACIÓN

Al pulsar el botón Editar se abrirá un último cuadro de diálogo en el cual habrá que añadir, al
final de la cadena de texto Valor de la variable, la ubicación del directorio “bin“ de la
instalación de la JDK. Para JDK 7u7 la ubicación por defecto es C:\Progamr
Files\jdk1.7.0_07\bin.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 8


TEMA 16. MAPAS Y LOCALIZACIÓN

Una vez añadida la ubicación de Keytool a la variable de entorno Path, se podrá obtener la
huella digital SHA-1 del certificado debug escribiendo, en la consola de comandos, lo siguiente
(sin el símbolo $):

$ keytool -v -list -keystore C:\Users\<user>\.android\debug.keystore


-alias androiddebugkey
-storepass android
-keypass android 6
Al pulsar Enter, aparecerá la siguiente información:

Nombre de Alias: androiddebugkey


Fecha de Creación: 29-ago-2012
Tipo de Entrada: PrivateKeyEntry
Longitud de la Cadena de Certificado: 1
Certificado[1]:
Propietario: CN=Android Debug, O=Android, C=US
Emisor: CN=Android Debug, O=Android, C=US
Número de serie: 24aa732d
Válido desde: Wed Aug 29 19:11:04 CEST 2012 hasta: Fri Aug 22 19:11:04
CEST 2042

Huellas digitales del Certificado:


MD5: DE:EF:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:C7:87:B5
SHA1: F0:34:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:C1:27:73
SHA256: E4:21:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:
XX:XX:XX:XX:XX:XX:XX:XX:XX:67:05:84:2E:F8:5A
Nombre del Algoritmo de Firma: SHA256withRSA (etc.)

El procedimiento para la obtención de una clave para la API Maps en modo release es similar,
debiéndose sustituir la referencia al almacén de claves debug por el almacén de claves release.
La generación de paquetes APK firmados en modo release, desde Eclipse, se explica en el tema
19.

6
El símbolo “$” no será escrito. El comando se escribirá sin saltos de línea. La ruta C:\Users\<user>\ variará en
función del nombre de usuario Windows.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 9


TEMA 16. MAPAS Y LOCALIZACIÓN

Creación de proyecto en Google APIs Console y obtención de la clave para la


API Maps (v2)

Una vez obtenida la huella digital SHA-1, se deberá crear un proyecto para la aplicación en
Google APIs Console y se activará el servicio de Google Maps para Android.

Al pulsar Create Project, Google APIs Console creará un nuevo proyecto llamado API Project
(cuyo nombre podrá ser posteriormente modificado). Una vez creado el proyecto, se podrá
acceder a Services desde el panel izquierdo de navegación, para activar el servicio Google
Maps Android API v2. Una vez aceptados los términos y condiciones, el servicio quedará
activado:

Una vez creado el proyecto y activado el servicio Maps para Android, se podrá solicitar la clave
para el uso de dicha API accediendo, desde el panel izquierdo de navegación, a API Access >
Create New Android Key... Se mostrará un cuadro de diálogo donde se deberá copiar la huella
digital SHA-1 del certificado (debug o release) y, a continuación, un punto y coma (“;”) y el
nombre del paquete de la aplicación.

Por ejemplo:

F0:34: XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:C1:27:73;com.cursoandroid.mymaps

CURSO DE DESARROLLO DE APLICACIONES ANDROID 10


TEMA 16. MAPAS Y LOCALIZACIÓN

Al pulsar Create, se creará la clave para el uso de la API Maps en dispositivos Android, en la
sección Key for Android apps (with certificates):

CURSO DE DESARROLLO DE APLICACIONES ANDROID 11


TEMA 16. MAPAS Y LOCALIZACIÓN

Configuración de la aplicación con la clave para la API Maps

Una vez se haya finalizado el registro en el servicio de Google Maps, habiendo obtenido la
clave de la API Maps (sea tipo debug o tipo release), se deberá añadir dicha clave en el
manifiesto de la aplicación. Esta clave será enviada a través de la API Maps a los servidores de
Google Maps, cuando la aplicación solicite la descarga de mapas.

Para añadir la clave a la aplicación, se incluirá el siguiente elemento, justo antes de


</application>:

<meta-data
android:name="com.google.android.maps.v2.API_KEY"
android:value="AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXhRaVlk" />

Este elemento hace que la clave para la API Maps sea visible para cualquier MapFragment que
sea utilizado en la aplicación.

También se deberán solicitar los siguientes permisos en el manifiesto:

<permission
android:name="com.cursoandroid.mymaps.permission.MAPS_RECEIVE"
android:protectionLevel="signature" />

<uses-permission
android:name="com.cursoandroid.mymaps.permission.MAPS_RECEIVE" />

El uso de la API Maps requiere además especificar otros permisos en el manifiesto, para que la
aplicación pueda acceder a funcionalidades del sistema Android, así como para que pueda
acceder a los servidores de Google Maps.

• android.permission.INTERNET: utilizado para descargar mapas desde los servidores


de Google Maps.
• android.permission.ACCESS_NETWORK_STATE: utilizado para determinar cuándo se
pueden descargar los mapas.
• android.permission.WRITE_EXTERNAL_STORAGE: utilizado para almacenar los mapas
descargados en la almacén externo.
• com.google.android.providers.gsf.permission.READ_GSERVICES: utilizado que
la API Maps pueda acceder a servicios de Google basados en web.
• android.permission.ACCESS_COARSE_LOCATION: permite a la API Maps usar las
conexiones WiFi y GPRS para determinar la ubicación del dispositivo.
• android.permission.ACCESS_FINE_LOCATION: permite a la API Maps usar el GPS
para determinar la ubicación del dispositivo.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 12


TEMA 16. MAPAS Y LOCALIZACIÓN

Los dos últimos permisos son recomendables, aunque podrán ser ignorados en caso de que la
aplicación no necesite acceder a la ubicación actual del usuario. Además, en caso de no añadir
estos permisos, la aplicación aún podrá ubicar al usuario si activa programáticamente la capa
My Location.

La última versión de la API Maps (v2) requiere OpenGL ES versión 2, por lo que se deberá
añadir el elemento <uses-feature> como elemento hijo de <manifest>:

<uses-feature
android:glEsVersion="0x00020000"
android:required="true"/>

Este requisito permite a Google Play no mostrar la aplicación a dispositivos que no soporten
OpenGL ES 2.0 (aquellos con versiones de Android inferiores a la 2.2 – Nivel de API 8).

CURSO DE DESARROLLO DE APLICACIONES ANDROID 13


TEMA 16. MAPAS Y LOCALIZACIÓN

Uso de Google Maps Android API v2

Ya se ha comentado en la introducción del tema que la librería Maps proporciona un conjunto


de clases que permiten mostrar y manipular los datos de Google Maps, y que se agrupan en el
paquete com.google.android.gms.maps.

La clase principal de la API de Google Maps para Android (v2) es MapFragment, que muestra
mapas gracias a los datos obtenidos del servicio de Google Maps. Cuando dicha clase obtiene
el foco, capturará pulsaciones y gestos sobre la pantalla (como, por ejemplo, pinch-to-zoom) y
gestionará las peticiones de nuevos mapas cuando sea necesario. Además, proporciona todos
los elementos gráficos necesarios para que el usuario pueda controlar el mapa de forma visual
así como la posibilidad de realizar dichas acciones de forma programática.

MapFragment, SupportMapFragment y GoogleMap

El objeto mapa es una instancia de la clase GoogleMap, la cual gestiona, automáticamente, la


conexión con el servicio de Google Maps, la descarga y visualización de imágenes de mapas, la
visualización de controles (zoom, panorámica, etc.) así como la interacción con los mismos
(listeners). Esta clase contiene además otros métodos callback para responder a pulsaciones
de teclas o gestos, y permite la superposición de marcadores y polígonos.

MapFragment (o SupportMapFragment) y MapView son objetos contenedores que


representan mapas en la interfaz de usuario y que exponen la funcionalidad del mapa a través
del objeto GoogleMap, que devuelven a través del método getMap(). Mientras que el uso de
MapFragment (o SupportMapFragment) es trivial, el uso de MapView implica invocar, en cada
uno de los métodos del ciclo de vida de la actividad, al correspondiente método homónimo del
ciclo de vida de MapView.

El modo más sencillo de incluir mapas en una aplicación es crear un layout que incluya un
fragmento MapFragment:

<fragment
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="com.google.android.gms.maps.MapFragment" />

CURSO DE DESARROLLO DE APLICACIONES ANDROID 14


TEMA 16. MAPAS Y LOCALIZACIÓN

El código anterior obliga a establecer android:minSdkVersion="12" ya que implica el uso de


fragmentos. No obstante, se puede utilizar la librería de Google Maps a partir del nivel de API 8
(Android 2.3.3), gracias a la clase SupportMapFragment 7:

<fragment
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="com.google.android.gms.maps.SupportMapFragment" />

La actividad que utilice dicha clase, deberá extender de FragmentActiviy:

public class MapasMainActivity extends FragmentActivity { ... }

Como todos los fragmentos, también se puede añadir en tiempo de ejecución un MapFragment
(o un SupportMapFragment) obteniendo una instancia del mismo a través de su método
newInstance(). Esta instancia será añadida a un contenedor a través de una transacción. Por
ejemplo, en el método onCreate() de la actividad que extienda de FragmentActivity, se
podría añadir el fragmento así:

SupportMapFragment myMapContainer = SupportMapFragment.newInstance();


FragmentTransaction transaction =
getSupportFragmentManager().beginTransaction();
transaction.add(R.id.myMapContainer, myMapContainer);
transaction.commit();

Inicialización y configuración del mapa

El uso de la librería de Google Maps (v2) implica, como ya se ha mencionado, que el dispositivo
tenga instalado el APK de Google Play Services. En caso de que no se encuentre su APK en el
dispositivo, o esté desactualizado, o deshabilitado, MapFragment, SupportMapFragment o
MapView mostrarán automáticamente un mensaje al usuario ofreciendo la posibilidad de
instalar, actualizar o activar Google Play Services a través de la Google Play Store.

El objeto mapa es una instancia de la clase GoogleMaps que deberá ser inicializada para que
incluya listeners, marcadores, polígonos, o incluso una configuración inicial de la perspectiva o
del punto de vista. Esta inicialización deberá realizarse una sola vez en el ciclo de vida del

7
Para usar esta clase se deberá incluir la última versión de la librería Android Support v4 en build path del proyecto
Eclipse, lo cual podrá hacerse automáticamente con la opción Source > Organize Imports (Ctrl+Mayúsculas+O), una
vez se haya referenciado SupportMapFragment en el código.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 15


TEMA 16. MAPAS Y LOCALIZACIÓN

fragmento. Debido a que MapFragment, SupportMapFragment o MapView dará la opción al


usuario de interrumpir la inicialización del fragmento (o vista), pausándolo (pausándola), en
caso de tener que actualizar Google Play Services, el método que inicialice el mapa deberá ser
invocado tanto desde onCreate() como desde onResume(), y deberá inicializar el mapa solo
en caso de que no sea nulo:

public class MapasMainActivity extends FragmentActivity {

/**
* El mapa puede ser null si el APK de Google Play Services no está
* disponible.
*/
private GoogleMap mMyMap;

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

@Override
protected void onResume() {
super.onResume();
inicializarMapaSoloSiEsNecesario();
}

private void inicializarMapaSoloSiEsNecesario() {


// Se confirma que no se ha instanciado (e inicializado) ya el mapa,
// comprobando si es nulo.
if (mMyMap == null) {
// Se intenta obtener el mapa a través de SupportMapFragment. Es
// en este punto donde se comprueba la disponibilidad de Google
// Play Services
mMyMap = ((SupportMapFragment) getSupportFragmentManager()
.findFragmentById(R.id.map)).getMap();
// Google Play Services está disponible. Si se ha obtenido el
// objeto mapa, se inicializa.
if (mMyMap != null) {
inicializarMapa();
}
}
}

/**
* Se añadirán los listeners, marcadores, polígonos, etc.
*
* Este método solo debe ser invocado una vez en el ciclo de vida, y
* solo en caso de que mMyMap NO sea nulo.
*/
private void inicializarMapa() { ... }

CURSO DE DESARROLLO DE APLICACIONES ANDROID 16


TEMA 16. MAPAS Y LOCALIZACIÓN

El estado inicial del mapa podrá ser inicializado en código, dentro del método
inicializarMapa(), y también a través de atributos XML personalizados.

Por ejemplo, se puede


establecer el tipo de mapa a través del método
GoogleMap.setMapType(int), que recibe como parámetro una de estas constantes:

• GoogleMap.MAP_TYPE_NORMAL
• GoogleMap.MAP_TYPE_HYBRID
• GoogleMap.MAP_TYPE_SATELLITE
• GoogleMap.MAP_TYPE_TERRAIN
• GoogleMap.MAP_TYPE_NONE (mostrará una cuadrícula vacía sin imágenes de mapas)

La API Maps define una serie de atributos XML personalizados que pueden ser utilizados en los
contenedores de mapas (MapFragment, SupportMapFragment y MapView). Para poder usar
estos atributos se deberá declarar un nuevo espacio de nombres en el archivo de layout:

xmlns:map="http://schemas.android.com/apk/res-auto"

Debido a un bug aún sin resolver, Eclipse no permite la definición de un espacio de nombres
custom, si dicho espacio de nombres se declara en un contenedor del elemento que utiliza
estos atributos. Por ejemplo, el siguiente layout no será considerado válido:

<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:map="http://schemas.android.com/apk/res-auto"
.../>

<fragment
android:id="@+id/map"
...
map:mapType="normal" />

<RelativeLayout />

Eclipse mostrará el error “Unexpected namespace prefix ‘map’ found for tag fragment” , en la
línea map:mapType="normal". Mientras este bug es resuelto, la única forma de utilizar los
atributos custom es declarar el fragmento de forma independiente en un archivo XML de
layout, sin ningún contenedor:

CURSO DE DESARROLLO DE APLICACIONES ANDROID 17


TEMA 16. MAPAS Y LOCALIZACIÓN

<fragment
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:map="http://schemas.android.com/apk/res-auto"
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="com.google.android.gms.maps.SupportMapFragment"
map:mapType="normal" />

Los atributos disponibles son los siguientes:

• cameraTargetLat, cameraTargetLng, cameraZoom, cameraBearing, cameraTilt:


establecen la posición inicial de la cámara (latitud, longitud, altura, orientación –
ángulo, en sentido horario, respecto al Norte–, e inclinación, respectivamente).
• mapType: establece el tipo de mapa (normal, hybrid, satellite, terrain, none).
• uiZoomControls, uiCompass: establecen si aparecen o no los controles de zoom y la
brújula (valor por defecto: true).
• uiZoomGestures, uiScrollGestures, uiRotateGestures, uiTiltGestures:
establecen qué gestos se habilitan o deshabilitan (valor por defecto: true).
• useViewLifecycle: (solo válido para MapFragment) permite especificar si el ciclo de
vida del mapa estará ligado al ciclo de vida de la actividad (a la vista) que contiene el
fragmento o al propio ciclo de vida del fragmento. Salvo excepciones, no se modificará
este atributo, dejando su valor por defecto (false), lo cual hace que el renderizado
sea más ágil cuando el fragmento se acopla de nuevo después de haberse desacoplado
de la actividad. Esto es debido a que los recursos subyacentes se mantienen, lo cual
implica un mayor consumo de memoria. Para liberar los recursos, no bastará con
desacoplar el fragmento: se deberá destruir completamente.
Por el contrario, si se usa el ciclo de vida de la vista del fragmento (true), se liberarán
más recursos, ya que el mapa no será reutilizado cuando se desacople y se acople de
nuevo el fragmento. Esto hace que se más lento el dibujado del mapa, ya que se ha de
inicializar de nuevo. Además, si el fragmento no es visible, los métodos del objeto
GoogleMap previamente inicializado devolverán NullPointerException.
• zOrderOnTop: controla si la superficie de la vista del mapa (la capa del mapa) se sitúa
como elemento superior de la ventana. Estableciendo este atributo a true se
ocultarán los controles y vistas que puedan aparecer en el mapa.

Si se va a añadir en código un MapFragment o un SupportMapFragment (o una MapView), se


podrá pasar un objeto de tipo GoogleMapOptions al método newInstance() (o al
constructor de MapView). Este objeto contendrá las opciones con las que se configurará el
objeto GoogleMap.

Por ejemplo:

CURSO DE DESARROLLO DE APLICACIONES ANDROID 18


TEMA 16. MAPAS Y LOCALIZACIÓN

GoogleMapOptions opcionesIniciales = new GoogleMapOptions();

opcionesIniciales.compassEnabled(false)
.rotateGesturesEnabled(false)
.mapType(GoogleMap.MAP_TYPE_TERRAIN);

SupportMapFragment myMapContainer =
SupportMapFragment.newInstance(opcionesIniciales);

FragmentTransaction transaction =
getSupportFragmentManager().beginTransaction();
transaction.add(R.id.myMapContainer, myMapContainer);
transaction.commit();

El punto de vista inicial también puede ser establecido vía código, a través de una instancia de
la clase CameraPosition obtenida a través de su Builder. Este objeto será pasado al método
GoogleMapOptions.camera(CameraPosition):

private static final LatLng COSTA_CAPARICA =


new LatLng(38.630958, -9.229691);
...

CameraPosition puntoDeVistaInicial = new CameraPosition.Builder()


.zoom(10) // Cercanía al suelo
.target(COSTA_CAPARICA) // Centro del mapa
.tilt(45) // Inclinación (grados hexagesimales)
.bearing(180) // Rotación del punto de vista, en
// sentido horario, desde el Norte
.build() // Método del Builder que construye
// el objeto CameraPosition;

GoogleMapOptions opcionesIniciales = new GoogleMapOptions();

opcionesIniciales.compassEnabled(false)
.rotateGesturesEnabled(false)
.mapType(GoogleMap.MAP_TYPE_TERRAIN)
.camera(puntoDeVistaInicial);

...

Tanto los controles que se muestran en el GoogleMap así como los gestos permitidos y los
eventos que lanzará el mapa, podrán ser establecidos a través de los métodos directos de
GoogleMapOptions.

Además, también se podrá usar el objeto de tipo UiSettings que devuelve el método
GoogleMap.getUiSettings(). Cualquier cambio que se realice en este objeto será
inmediatamente reflejado en el mapa:

CURSO DE DESARROLLO DE APLICACIONES ANDROID 19


TEMA 16. MAPAS Y LOCALIZACIÓN

• UiSettings.setZoomControlsEnabled(boolean): muestra u oculta los controles de


zoom.
• UiSettings.setCompassEnabled(boolean): muestra u oculta el control brújula. Al
ser pulsado, reinicia la inclinación y la rotación.
• UiSettings.setMyLocationButtonEnabled(boolean): muestra u oculta el botón
My Location solo en el caso de que esta capa esté activada (se verá a continuación).
• UiSettings.setScrollGesturesEnabled(boolean)
• UiSettings.setTiltGesturesEnabled(boolean)
• UiSettings.setRotateGesturesEnabled(boolean)
• UiSettings.setZoomGesturesEnabled(boolean)

Se ha de tener en cuenta que los gestos se habilitan o deshabilitan para el usuario. Se podrán
realizar animaciones y movimientos de cámara vía código aunque los gestos estén
deshabilitados.

La API Maps también permite que la aplicación escuche eventos que ocurren sobre el mapa:

• GoogleMaps.setOnMapClickListener(OnMapClickListener): establece el listener


para el método callback onMapClick(LatLng), que recibe las coordenadas del punto
pulsado. Estas coordenadas podrán ser convertidas a coordenadas de pantalla en
píxeles a través de la clase Projection.
• GoogleMaps.setOnMapLongClickListener(OnMapClickLongListener): establece el
listener para el método callback onMapLongClick(LatLng).
• GoogleMaps.setOnCameraChangeListener(OnCameraChangeListener) ): establece
el listener para el método callback onCameraChange(CameraPosition). El objeto
CameraPosition recibido corresponde al nuevo punto de vista de la cámara.

El punto de vista de la cámara (CameraPosition) podrá ser modificado como respuesta a la


interacción del usuario en otras partes de la aplicación (como por ejemplo, al seleccionar una
ubicación de una lista de ubicaciones disponibles). El cambio de punto de vista puede
realizarse instantáneamente a través del método GoogleMap.moveCamera(), o con una suave
animación, a través del método GoogleMap.animateCamera(). Este método requiere al
menos un objeto de tipo CameraUpdate 8, el cual será generado a través de los métodos de
CameraUpdateFactory. La animación se realizará interpolando los valores del punto de vista
actual y el nuevo punto de vista (objetos CameraPosition).

8
También permite establecer la duración de la animación así como el listener CancellableCallback.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 20


TEMA 16. MAPAS Y LOCALIZACIÓN

Como se ha mencionado, cada método de CameraUpdateFactory devuelve un objeto


CameraUpdate que contendrá los datos para una animación concreta. Entre sus métodos,
destacan:

• CameraUpdateFactory.zoomIn(), CameraUpdateFactory.zoomOut(): devuelve un


objeto de tipo CameraUpdate que cambia el zoom en ± 1.0.
• CameraUpdateFactory.zoomTo(float), CameraUpdateFactory.zoomBy(float):
cambia el zoom al valor proporcionado o incrementa/decrementa dicho valor.
• CameraUpdateFactory.newLatLng(LatLng),
CameraUpdateFactory.newLatLngZoom(LatLng, float): cambia el punto central
del mapa y la altitud del punto de vista.
• CameraUpdateFactoy.scrollBy(float, float): desplaza el punto central la
cantidad de píxeles indicados (x e y). Una valor de x positivo desplazará el punto
central a la derecha, por lo que el mapa aparentará moverse hacia la izquierda.
Igualmente, una valor de y negativo desplazará el punto central hacia abajo, por lo que
el mapa aparentará moverse hacia arriba.
• CameraUpdateFactory.newCameraPosition(CameraPosition): cambia el punto de
vista a través de un objeto CameraPosition. Se consigue así más flexibilidad en las
animaciones.

En ciertos casos es útil mover el punto de vista de la cámara de forma que el área de interés se
visualice con el mayor nivel de zoom posible. Para ello, se puede utilizar
CameraUpdateFactory.newLatLngBounds(LatLngBounds bounds, int padding) que
devolverá un objeto CameraUpdate para cambiar el punto de vista de la cámara de forma que
el objeto LatLngBounds proporcionado encaje completamente en el mapa. La clase
LatLngBounds incluye un constructor que requiere las coordenadas 9 (objetos LatLng) de los
puntos suroeste y noreste, respectivamente, del área que se desea visualizar. Además
contiene otro método, including(LatLng) que devolverá otro objeto LatLngBounds que
modificará las dimensiones del área de interés al incluir el nuevo punto. También se podrá
utilizar un builder para incluir más fácilmente conjuntos de coordenadas, que será obtenido a
través del método LatLngBounds.build(). Este método devuelve un objeto de tipo
LatLngBounds.Builder que incluye el método include(LatLng):

9
El formato por defecto de las coordenadas es grados decimales. Este es el formato que usa por defecto Google en
sus mapas. Para obtener las coordenadas de una localización concreta a través Google Maps bastará con pulsar con
el botón derecho del ratón sobre la localización y elegir la opción “¿Qué hay aquí?” Esta acción hará que aparezcan
las coordenadas (grados decimales) de dicha localización en la barra de búsqueda (search bar) de Google Maps.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 21


TEMA 16. MAPAS Y LOCALIZACIÓN

LatLngBounds.Builder boundsBuilder = LatLngBounds.builder();


boundsBuilder.include(COSTA_CAPARICA);
boundsBuilder.include(SAGRES);
boundsBuilder.include(ESSAOUIRA);

// El área de interés tendrá un paddding de 100 px.


CameraUpdate zoomAreaInteres =
CameraUpdateFactory.newLatLngBounds(boundsBuilder.build(), 100);

// Se realiza la animación
mMyMap.animateCamera(zoomAreaInteres);

Inclusión de marcadores y formas sobre el mapa

Marcadores

La API Maps proporciona marcadores que representan puntos concretos en el mapa y que
pueden ser personalizados. Los marcadores son instancias de la clase Marker y serán añadidos
sobre el mapa a través del método GoogleMap.addMarker(MarkerOptions).

Los marcadores responden a eventos on-click, mostrando, por ejemplo, ventanas con
información. Por defecto, los marcadores son visibles y no pueden ser arrastrados (al realizar
sobre ellos una pulsación larga), aunque se puede establecer a false la propiedad visible y a
true la propiedad draggable para marcadores concretos.

La adición de un marcador a una instancia de GoogleMap es sencilla:

private static final LatLng ESSAOUIRA = new LatLng(31.513417,-9.769733);


private GoogleMap mMyMap;

...

mMyMap.addMarker(new MarkerOptions()
.position(ESSAOUIRA)
.title(getString(R.string.essaouira))
.snippet(getString(R.string.ciudad_laberinto))
.icon(BitmapDescriptorFactory
.defaultMarker(BitmapDescriptorFactory.HUE_RED)));

Además de la posición y el título, cada marcador puede contener un snippet (texto extra
debajo del título) así como un icono (que no podrá ser cambiado una vez se haya creado el
marcador). El punto del icono que coincidirá con las coordenadas de la localización es, por
defecto, el punto medio de la línea base del icono. Este punto podrá ser modificado a través de
la propiedad anchor.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 22


TEMA 16. MAPAS Y LOCALIZACIÓN

El icono por defecto de un marcador puede ser modificado a través de su método icon(),
usando BitmapDescriptorFactory. Invocando al método defaultMarker(float hue) se
podrá cambiar el color 10 del icono. Para sustituir la imagen del icono, se deberá pasar una
instancia de BitmapDescriptor al método icon(), la cual habrá sido obtenida a través de uno
de los métodos de BitmapDescriptorFactory:

• fromAsset(String name): utiliza una imagen del directorio assets.


• fromBitmap(Bitmap image): utiliza el Bitmap para crear el icono.
• fromFile(String path): la imagen se ubicará en un directorio concreto.
• fromResource(int resourceId): se utilizará un recurso ya existente (clase R).

Cuando el usuario toca un marcador que tiene un título asociado, se mostrará una ventana con
la información proporcionada (título en negrita y snippet). Este comportamiento puede ser
programado invocando a los métodos del marcador showInfoWindow() y hideInfoWindow().

El layout de esta ventana puede ser


modificado invocando al método
GoogleMap.setInfoWindowAdapter(InfoWindowAdapter). La interfaz InfoWindowAdapter
deberá implementar los métodos getInfoWindow(Marker) y getInfoContents(Marker). La
API invocará al primero de estos métodos y, si este devuelve null, invocará al segundo. Si este
segundo método también devuelve null, mostrará la ventana por defecto. El primer método
permite proporcionar una View para la ventana de información del marcador recibido como
parámetro mientras que el segundo permite modificar los componentes de la ventana,
manteniendo su marco y su fondo por defecto.

Es muy importante tener en cuenta que esta ventana de información es dibujada en el mapa
como una imagen, a través del método View.draw(Canvas). Los listeners asociados a dicha
View (toques y gestos) serán ignorados y modificaciones posteriores de la misma no serán
reflejadas en el marcador hasta que se invoque al método showInfoWindow().

La API Maps permite escuchar y responder a eventos click (sobre marcadores o sobre sus
ventanas de información) y drag, asociando los listeners OnMarkerClickListener,
OnMarkerDragListener u OnInfoWindowsClickListener al objeto GoogleMap. Los métodos
que implementarán estas interfaces recibirán como parámetro el marcador sobre el que se
produce el evento.

• OnMarkerClickListener.onMarkerClick(Marker): método que devolverá un


booleano que indica si se consume el evento. En caso de devolver false, el
comportamiento programado será sumado al comportamiento por defecto (mostrar
ventana de información, en caso de que exista).

10
HUE puede adoptar un valor entre 0 y 360, representando puntos en una rueda de colores.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 23


TEMA 16. MAPAS Y LOCALIZACIÓN

• OnMarkerDragListener.onMarkerDragStart(Marker): método invocado


inicialmente cuando se pulsa un marcador con el objeto de arrastrarlo.
• OnMarkerDragListener.onMarkerDrag(Marker): método invocado continuamente
mientras se arrastra el marcador. Se puede obtener la posición del marcador en
cualquier instante a través del método Marker.getPosition().
• OnMarkerDragListener.onMarkerDragEnd(Marker): método invocado cuando se
suelta el marcador.
• OnInfoWindowClickListener.onInfoWindowClick(Marker): método invocado al
pulsar dentro de la ventana de información del marcador y que permitirá sustituir o
ampliar el comportamiento por defecto (cambio de color del fondo de la ventana),
devolviendo false o true, respectivamente. Como ya se ha mencionado, estas
ventanas no son View reales, por lo que cualquier evento que pudiese añadir la View,
será ignorado. Estas View, por lo tanto, no deberán contener botones, EditText
checkboxes u otros campos de entrada.

Formas

La API Maps proporciona la posibilidad de añadir diversas formas sobre el mapa. Estas formas
serán instancias de:

• Polilyne: serie de segmentos lineales conectados que proporcionan una ruta.


• Polygon: forma cerrada que delimita un área.
• Circle: proyección geográficamente exacta (Mercator) de un círculo sobre la
superficie terrestre.

La clase Polyline será definida a través de un conjunto ordenado de objetos LatLng que
serán proporcionados invocando consecutivamente al método PolilyneOptions.add():

private Polyline mMyPolyline;

...

PolylineOptions puntosItinerario = new PolylineOptions()


.add(ESSAOUIRA)
.add(SAGRES)
.add(COSTA_CAPARICA);

mMyPolyline = mMyMap.addPolyline(puntosItinerario);

También se podrá usar el método PolylineOptions.addAll(Iterable<LatLng>) si los


puntos ya se encuentran contenidos en una lista. Una vez obtenido el objeto Polyline, se
podrá proporcionar una nueva lista de puntos a través de su método setPoints().

CURSO DE DESARROLLO DE APLICACIONES ANDROID 24


TEMA 16. MAPAS Y LOCALIZACIÓN

El uso de la clase Polygon es similar al uso de la clase Polyline. Un polígono es una polilínea
cuyo puntos inicial y final son el mismo. La zona delimitada por un polígono tendrá el fondo de
un color específico.

private Polygon mMyPolygon;


private static final LatLng ANNECY = new LatLng(45.919751 , 6.143562);
...

PolygonOptions puntosPoligono = new PolygonOptions()


.add(ANNECY, ESSAOUIRA, SAGRES, COSTA_CAPARICA) 11;

mMyPolygon = mMyMap.addPolygon(puntosPoligono);

Gracias a la clase Polygon se pueden crear figuras complejas que incluyan agujeros, los cuales
podrán ser definidos a través de una secuencia de puntos, pasada al método
PolygonOptions.addHole(LatLng...).

Para dibujar un círculo, se deberá especificar el punto central y el radio, en metros:

private Circle mMyCircle;


private static final LatLng ANNECY = new LatLng(45.919751 , 6.143562);

...

// Círculo verde de radio 200 km en torno a ANNECY, con borde amarillo.


CircleOptions datosCirculo = new CircleOptions()
.center(ANNECY)
.radius(200000)
.fillColor(Color.GREEN)
.strokeColor(Color.YELLOW)
.strokeWidth(20)
.zIndex(1);

mMyCircle = mMyMap.addCircle(datosCirculo);

Como puede observarse en el código anterior, también se puede modificar el aspecto de las
diversas formas, así como su orden de superposición (z-index).

Una vez añadido el círculo, se podrá modificar el punto central, el radio y el resto de
propiedades, a través de los métodos set: Circle.setCenter(LatLng),
Circle.setRadius(int), etc. También se podrán modificar las propiedades de los polígonos
y polilíneas accediendo a sus correspondientes métodos set.

11
Los métodos add(), de PolylineOptions y PolygonOptions, polimórficos, también admiten una secuencia de n
puntos LatLng como parámetros de entrada.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 25


TEMA 16. MAPAS Y LOCALIZACIÓN

Todas las formas son visibles por defecto. Sin embargo, pueden ser ocultadas temporalmente
invocando a su método setVisible(false). Para eliminar definitivamente una forma, se
deberá invocar a su método remove().

Por último, ya se ha mencionado que Circle es una proyección tipo Mercator de un círculo
sobre la superficie terrestre. Esto implica que el aspecto de estas formas no será exactamente
un círculo, sino que, en general, serán elípticas. De forma análoga, las polilíneas y los polígonos
pueden ser dibujadas de forma geodésica, como proyecciones tipo Mercator de líneas sobre la
superficie terrestre, de forma que tendrán un aspecto algo curvo. Se deberá usar el método
PolylineOptions.geodesic(true) o PolygonOptions.geodesic(true) ya que, por
defecto, las polilíneas y polígonos se dibujan de forma no geodésica.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 26


TEMA 16. MAPAS Y LOCALIZACIÓN

Localización

Gracias a los servicios incluidos en la API de Google Maps, se pueden desarrollar aplicaciones
sensibles a la ubicación, dirección de movimiento y método de movimiento del dispositivo,
gracias a los datos obtenidos utilizando una combinación de tecnologías disponibles en el
dispositivo (GPS, triangulaciones entre antenas 3G, WiFi, sensores de movimiento, etc.).
Además, también se puede determinar si el dispositivo sale de una zona geográfica delimitada,
o geofence.

Para usar los datos de localización se podrá usar una capa llamada My Location, que muestra la
ubicación del dispositivo en el mapa sin proveer ningún dato de localización a la aplicación, o
bien se podrán usar los servicios de localización que provee la API Google Play Services
Location para solicitar programáticamente datos de localización, o la interfaz
LocationSource, que permite la creación de un proveedor de localización específico.

Capa My Location

La forma más sencilla de mostrar la ubicación del dispositivo es usar la capa y el botón My
Location. Cuando se pulse este botón, que aparecerá en el ángulo superior derecho del mapa,
la cámara centrará el mapa en la ubicación actual del dispositivo, en caso de que esté
disponible, mostrando un icono u otro en función de si el dispositivo está o no en movimiento.

Para activar esta capa, simplemente se incluirá la siguiente línea, al inicializar el objeto
GoogleMap:

mMyMap.setMyLocationEnabled(true);

Además, será necesario solicitar los siguientes permisos en el manifiesto:

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

</manifest>

Solicitar el primero de los permisos (acceso a ubicación exacta) obliga a solicitar el segundo
permiso (acceso a ubicación aproximada, cuya precisión equivale a un edificio promedio de
una ciudad).

CURSO DE DESARROLLO DE APLICACIONES ANDROID 27


TEMA 16. MAPAS Y LOCALIZACIÓN

Uso de la API Google Location Services

La API Google Location Services proporciona funcionalidades 12 que permiten determinar la


ubicación del dispositivo, escuchar cambios de ubicación, averiguar el modo de transporte (en
caso de que el dispositivo se esté desplazando) y crear y monitorizar zonas delimitadas
geográficamente (geofences) 13.

Los Servicios de Localización se encargan de mantener el dispositivo ubicado en todo


momento, y proporcionarán la información de la ubicación (con precisión exacta o
aproximada) en función de los permisos que solicite la aplicación. La aplicación deberá
implementar un cliente de localización, encargado de recibir la ubicación desde los Servicios de
Localización, que será una instancia de LocationClient.

Inicialización y conexión del cliente

Antes de inicializar el cliente de los Servicios de Localización, se deberá comprobar que están
disponibles dispositivo los servicios de Google Play. En el método onCreate() de la actividad
principal de la aplicación, se podrá implementar un código similar al siguiente:

private LocationClient mMyLocationClient;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mapas_main);
if (isGoogleServicesAvailable()) {
// Se inicializa el LocationClient (aún no está conectado)
mMyLocationClient = new LocationClient(this, this, this);
}
}

El contenido del método isGoogleServicesAvailable() podrá ser algo así:

private boolean isGoogleServicesAvailable() {


int resultCode =
GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
if (resultCode != ConnectionResult.SUCCESS) {
// Los servicios de Google Play NO están disponibles. Se ofrece
// actualizar el dispositivo a través del siguiente diálogo
GooglePlayServicesUtil.getErrorDialog(resultCode, this, 0).show();
return false;
} else {// Los servicios de Google Play están disponibles.
return true;
}

12
Esta API ha sido diseñada para minimizar el consumo energético del dispositivo al usar servicios de localización.
13
Esta compleja y exclusiva funcionalidad puede ser consultada en este artículo:
http://developer.android.com/training/location/geofencing.html

CURSO DE DESARROLLO DE APLICACIONES ANDROID 28


TEMA 16. MAPAS Y LOCALIZACIÓN

Como puede observarse, la inicialización de LocationClient requiere tres parámetros. Estos


parámetros son, respectivamente, el contexto de la aplicación, y los listeners
ConnectionCallbacks y OnConnectionFailedListener. En este caso, será la actividad quien
implemente los métodos callback de dichas interfaces, los cuales serán invocados por los
Servicios de Localización:

• ConnectionCallbacks.onConnected(): el cliente (LocationClient) ha sido


conectado a los Servicios de Localización y se puede comenzar a usar.
• ConnectionCallbacks.onDisconnected(): el cliente ha sido desconectado.
• OnConnectionFailedListener.onConnectionFailed(ConnectionResult): ha
fallado la conexión con los Servicios de Localización. Este fallo de conexión podría
tener solución, en caso de que ConnectionResult.hasResolution() sea true.

Una vez inicializado el LocationClient, se deberá conectar a los Servicios de Localización


invocando a su método connect(). Una vez se haya conectado el cliente, los Servicios de
Localización invocarán al método callback onConnected(), momento a partir del cual se podrá
obtener información de la ubicación del dispositivo a través del método getLastLocation()
de la instancia de LocationClient.

Para desconectar el cliente, se invocará a su método disconnect(). Esto invalidará dicho


cliente, por lo que será necesario inicializar uno nuevo la próxima vez que se necesite la
ubicación del dispositivo.

La conexión y desconexión del cliente se deberá realizar en los métodos onStart() y


onStop(), respectivamente, para optimizar el uso de la batería:

// Se invoca cuando la actividad se hace visible


@Override
protected void onStart() {
super.onStart();
// Se conecta el cliente de los servicios de localización
if (mMyLocationClient != null && !mMyLocationClient.isConnected())
mMyLocationClient.connect();
}

// Se invoca cuando la actividad deja de ser visible. Por ejemplo, al


// cambiar de orientación el dispositivo (para forzar el repintado
// de su layout)
@Override
protected void onStop() {
// La desconexión del cliente lo invalida, de forma que habrá que
// hacer new LocationClient() de nuevo
if (mMyLocationClient != null && mMyLocationClient.isConnected())
mMyLocationClient.disconnect();
super.onStop();
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 29


TEMA 16. MAPAS Y LOCALIZACIÓN

Gestión de la no disponibilidad de los Servicios de Localización

En caso de que los servicios de Google Play no estén disponibles, o en caso de que el cliente,
por causas desconocidas, no pueda conectar con los Servicios de Localización (o estos no
puedan obtener información de los servidores de Google), se podrá ofrecer al usuario la
posibilidad de descargar los APK necesarios, o bien se podrán intentar resolver los problemas
de conexión que pudiesen existir, de forma automática.

Ya se ha mencionado que, cuando se comprueba la disponibilidad de los servicios de Google


Play, a través de:

int resultCode =
GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);

se puede obtener un código de error contenido en resultCode. Con este código, se podrá
mostrar un diálogo al usuario, para que descargue, actualice o active los servicios de Google
Play:

Dialog errorDialog = GooglePlayServicesUtil.getErrorDialog(resultCode,


this,
CONNECTION_FAILURE_RESOLUTION_REQUEST);

Una vez realizada la acción que ofrezca el diálogo, se devolverá el control a la actividad
principal de la aplicación, invocándose al método onActivityResult(int requestCode,
int resultCode, Intent data). El requestCode que recibirá este método será
CONNECTION_FAILURE_RESOLUTION_REQUEST.

Por otro lado, también se podrán dar fallos de conexión que fuercen a los Servicios de
Localización a invocar al método callback onConnectionFailed(ConnectionResult). El
objeto ConnectionResult recibido como parámetro contiene el método hasResolution(). Si
este método devuelve true, se podrá intentar resolver el fallo invocando a:

connectionResult.startResolutionForResult(
this,
CONNECTION_FAILURE_RESOLUTION_REQUEST);

Este procedimientohará que, finalmente, también se invoque al método


onActivityResult(), recibiendo, de nuevo, el mismo requestCode.

En ambos procedimientos, si se consigue disponibilidad de los servicios de Google Play o se


reconecta el cliente, onActivityResult() recibirá el resultCode Activity.RESULT_OK.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 30


TEMA 16. MAPAS Y LOCALIZACIÓN

@Override
public void onConnectionFailed(ConnectionResult connectionResult) {

showMessage(getString(R.string.ha_fallado_la_conexion_) +
connectionResult.getErrorCode());

if (connectionResult.hasResolution()) {
showMessage(
getString(R.string.intentando_resolver_el_fallo_de_conexion_) +
connectionResult.getErrorCode());
try {
// Se lanza una actividad que intentará resolver el fallo de
// conexión
connectionResult.startResolutionForResult(
this,
CONNECTION_FAILURE_RESOLUTION_REQUEST);
} catch (IntentSender.SendIntentException e) {
// Esta excepción se lanza si los servicios de Google Play han
// cancelado el PendingIntent original
Log.e(TAG_ERROR, e.getMessage());
}
} else {
showMessage(
getString(R.string.fallo_de_conexion_sin_posible_resolucion_) +
connectionResult.getErrorCode());
}
}

// Método sobrescrito para recibir la resolución del intento de


// reconexión del cliente
@Override
protected void onActivityResult(
int requestCode, int resultCode, Intent data) {

// Se decide qué hacer basándose en el requestCode original


switch (requestCode) {

// Respuesta de la actividad invocada en caso de recibir un fallo de


// conexión del cliente con los Servicios de Localización.
case CONNECTION_FAILURE_RESOLUTION_REQUEST:

// Si se obtiene resultCode = Activity.RESULT_OK, se intenta


// reconectar el cliente
switch (resultCode) {
case Activity.RESULT_OK:
inicializarClienteLocalizacion();
break;
case Activity.RESULT_CANCELED:
default:
showMessage(
getString(R.string.google_play_services_no_disponible));
finish();
break;
}
}
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 31


TEMA 16. MAPAS Y LOCALIZACIÓN

Obtención y actualización de la ubicación actual

Una vez se haya conectado el cliente de localización, se podrá obtener la ubicación del
dispositivo a través del método LocationClient.getLastLocation(). Este método
devolverá un objeto de tipo Location que contiene las coordenadas (latitud y longitud).

Si el cliente es desconectado, este método devolverá la última ubicación que hubiese


obtenido, por lo que podría estar desactualizada.

Por otro lado, si el dispositivo está en movimiento, se podrán obtener actualizaciones


periódicas de la ubicación desde los Servicios de Localización si se solicitan desde el cliente.
Dependiendo del formato de la petición, los Servicios de Localización invocarán a un método
callback al cual pasarán un objeto Location, o bien lanzarán un Intent que contendrá
información extra sobre la ubicación. Tanto la precisión con la frecuencia de las actualizaciones
dependerán de los parámetros que reciban los Servicios de Localización en la petición.

Para que la actividad pueda escuchar las actualizaciones de la ubicación, deberá implementar
el listener com.google.android.gms.location.LocationListener. Esta interfaz
proporciona el método callback onLocationChanged(Location), que recibirá la nueva
ubicación cada vez que esta cambie, contenida en el parámetro Location.

Además de implementar el método callback, la actividad deberá solicitar la actualización de la


ubicación a los Servicios de Localización. Para esta solicitud, se deberá crear un objeto
LocationRequest, a través de su método LocationRequest.create(). Este objeto
contendrá información sobre la frecuencia de actualización y sobre la precisión deseada:

• LocationRequest.setInterval() establecerá el tiempo, en milisegundos, entre


actualizaciones 14.
• LocationRequest.setFastestInterval() establecerá la tasa más rápida, en
milisegundos, a la cual la aplicación puede recibir actualizaciones de la ubicación.
Evitará posibles parpadeos de la interfaz de usuario y desbordamientos y permite
ahorrar energía.
• LocationRequest.setPriority(int): establece el nivel de precisión. Los valores posibles
del parámetro son:
o LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY: precisión que
equivale a un edificio promedio de una ciudad (precisión de 100 metros).
o LocationRequest.PRIORITY_HIGH_ACCURACY: máxima precisión posible.
o LocationRequest.PRIORITY_NO_POWER: mejor ubicación posible sin consumir
nada de energía adicional. No se obtendrán nuevas ubicaciones a no ser que
se solicite la actualización de la ubicación a través de un nuevo cliente.

14
Este tiempo será respetado siempre y cuando no haya más aplicaciones solicitando actualizaciones de la
ubicación simultáneamente.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 32


TEMA 16. MAPAS Y LOCALIZACIÓN

El cliente LocationClient realizará la solicitud de actualización de la ubicación, una vez haya


sido conectado a los Servicios de Localización, invocando a su método
LocationClient.requestLocationUpdates(LocationRequest, LocationListener). Para
detener la actualización de la ubicación, se invocará a
LocationClient.removeLocationUpdates(LocationListener) (antes de que el cliente sea
desconectado):

@Override
public void onConnected(Bundle arg0) {
// Ya se puede usar el LocationClient
showMessage(getString(R.string.cliente_conectado));

// Se solicita la actualización de la ubicación


LocationRequest locationRequest = LocationRequest.create();
locationRequest.setInterval(10000);
locationRequest.setFastestInterval(5000);
locationRequest.setPriority(
LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);

mMyLocationClient.requestLocationUpdates(locationRequest, this);
}

...

@Override
protected void onStop() {
// La desconexión del cliente lo invalida, de forma que habrá que
// hacer new LocationClient() de nuevo
if (mMyLocationClient != null && mMyLocationClient.isConnected()) {
mMyLocationClient.removeLocationUpdates(this);
mMyLocationClient.disconnect();
}
super.onStop();
}

Reconocimiento de la actividad del usuario

El proceso para obtener actualizaciones sobre el reconocimiento del tipo de actividad que está
realizando el usuario (desplazarse en coche, andando, en bicicleta, parado, inclinando) es
similar al proceso de actualización de la ubicación actual, aunque requiere otro permiso
distinto 15:

<uses-permission
android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION" />

15
El reconocimiento de la actividad del usuario no necesita los permisos ACCESS_COARSE_LOCATION ni
ACCESS_FINE_LOCATION.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 33


TEMA 16. MAPAS Y LOCALIZACIÓN

Cuando el cliente de localización se haya conectado correctamente, se podrá enviar una


solicitud a los Servicios de Localización, los cuales enviarán la información solicitada a la
aplicación través de un PendingIntent, en vez de invocar a métodos callback de algún
listener. La solicitud podrá ser enviada después de que los Servicios de Localización invoquen al
método callback onConnected(). Una vez enviada la solicitud, el cliente podrá ser
desconectado, si no se necesita más información de los Servicios de Localización.

Para poder recibir las actualizaciones de reconocimiento de actividad, se deberá declarar, por
lo tanto, un PendingIntent, así como un cliente de reconocimiento de actividad, que será una
instancia de la clase ActivityRecognitionClient. El PendingIntent será utilizado por los
Servicios de Localización para enviar actualizaciones de la actividad del usuario a la aplicación.
Además, se deberá indicar la frecuencia con la que se desea recibir las actualizaciones.

En el método onCreate() de la actividad, se deberán instanciar tanto el PendingIntent como


la ActivityRecognitionClient. La inicialización de este cliente requiere tres parámetros, al
igual que el cliente de localización. Estos parámetros son, respectivamente, el contexto de la
aplicación, y los listeners ConnectionCallbacks y OnConnectionFailedListener. De nuevo,
será la actividad quien implemente los métodos callback de dichas interfaces, los cuales serán
invocados por los Servicios de Localización:

private ActivityRecognitionClient mMyActivityRecognitionClient;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mapas_main);
if (isGoogleServicesAvailable()) {
// Se inicializa el cliente de la actividad del usuario
// (aún no está conectado)
mMyActivityRecognitionClient =
new ActivityRecognitionClient(this, this, this);

...
// (inicialización de PendingIntent)
}
}

Una vez inicializado el ActivityRecognitionClient, se deberá conectar a los Servicios de


Localización invocando a su método connect(). Una vez se haya conectado el cliente, los
Servicios de Localización invocarán al método callback onConnected(), momento a partir del
cual se podrán solicitar actualizaciones de la actividad del usuario a través del método
requestActivityUpdates(int interval, PendingIntent pi) de la instancia de
ActivityRecognitionClient.

Si el cliente se desconecta por alguna razón, los Servicios de Localización invocarán al método
callback onDisconnected(). Esta desconexión invalidará dicho cliente (y habrá que asignar a
null la instancia del cliente), por lo que será necesario inicializar uno nuevo si se necesita

CURSO DE DESARROLLO DE APLICACIONES ANDROID 34


TEMA 16. MAPAS Y LOCALIZACIÓN

seguir recibiendo actualizaciones de la actividad del usuario. Para desconectar manualmente el


cliente, se invocará a su método disconnect().

La conexión y desconexión del cliente se deberá realizar en los métodos onStart() y


onStop(), respectivamente, para optimizar el uso de la batería:

// Se invoca cuando la actividad se hace visible


@Override
protected void onStart() {
super.onStart();
// Se conecta el cliente del reconocimiento de actividad del usuario
if (mMyActivityRecognitionClient != null &&
!mMyActivityRecognitionClient.isConnected()) {
mMyActivityRecognitionClient.connect();
}
}

// Se invoca cuando la actividad deja de ser visible. Por ejemplo, al


// cambiar de orientación el dispositivo (para forzar el repintado
// de su layout)
@Override
protected void onStop() {
// La desconexión del cliente lo invalida, de forma que habrá que
// hacer new ActivityRecognitionClient() de nuevo
if(mMyActivityRecognitionClient != null &&
mMyActivityRecognitionClient.isConnected()) {
mMyActivityRecognitionClient
.removeActivityUpdates(mMyActivityRecognitionPendingIntent);
mMyActivityRecognitionClient.disconnect();
}
super.onStop();
}

El proceso para inicializar el PendingIntent es algo más complejo. Los Servicios de


Localización envían Intents periódicamente con la información de la actividad del usuario, por
lo que se tendrá que construir un servicio de tipo IntentService e implementar su método
onHandleIntent(). Los Intents son enviados utilizando el PendingIntent que se pasa como
parámetro al invocar al método requestActivityUpdates() del cliente. Al inicializar este
PendingIntent se deberá proporcionar un Intent explícito que apunte al IntentService,
de forma que los Intents que envíen los Servicios de Localización solo serán recibidos por el
IntentService que haya sido definido.

Cuando el cliente haya conectado con los Servicios de Localización, estos invocarán al método
callback onConnected(). Será en este método donde se inicialice el PendingIntent y se
soliciten las actualizaciones de la actividad del usuario:

CURSO DE DESARROLLO DE APLICACIONES ANDROID 35


TEMA 16. MAPAS Y LOCALIZACIÓN

@Override
public void onConnected(Bundle arg0) {
// Ya se puede usar el ActivityRecognitionClient
showMessage(getString(R.string.cliente_conectado));

Intent intentActivityRecognition = new Intent(this,


ActivityRecognitionIntentService.class);

// Se inicializa el PendingIntent que iniciará el IntentService


PendingIntent mMyActivityRecognitionPendingIntent =
PendingIntent.getService(this, 0,
intentActivityRecognition,
PendingIntent.FLAG_UPDATE_CURRENT);

// Se activa el reconocimiento de la actividad del usuario


mMyActivityRecognitionClient
.requestActivityUpdates(ACTIVITY_UPDATE_INTERVAL,
mMyActivityRecognitionPendingIntent);

El IntentService será el responsable de extraer, en el método onHandleIntent(), la


información de la actividad del usuario de los Intents que envíen los Servicios de Localización.
Para extraer esta información se utilizarán las clases ActivityRecognitionResult y
DetectedActivity. La primera clase proporciona el método estático hasResult(Intent) que
devolverá true en caso de que el Intent que recibe como parámetro contenga información
sobre la actividad del usuario. Esta información se extraerá a través del método estático
extractResult(Intent), que devolverá un objeto también de tipo
ActivityRecognitionResult.

Los Servicios de Localización utilizan la información obtenida de las antenas y sensores del
dispositivo para deducir cuál es la actividad que está realizando el usuario. Por lo tanto,
proporcionan la probabilidad de que el usuario esté realizando cierta actividad en ese instante.
El objeto de tipo ActivityRecognitionResult que contiene el resultado devolverá, a través
de su método getMostProbableActivity(), un objeto de tipo DetectedActivity, que
devolverá, a su vez, la probabilidad de dicha actividad a través del método getConfidence() y
el tipo de actividad a través del método getType(). Los posibles tipos de actividad son:

• DetectedActivity.IN_VEHICLE
• DetectedActivity.ON_BICYCLE
• DetectedActivity.ON_FOOT
• DetectedActivity.STILL (parado)
• DetectedActivity.TILTING (inclinación)
• DetectedActivity.UNKNOWN

CURSO DE DESARROLLO DE APLICACIONES ANDROID 36


TEMA 16. MAPAS Y LOCALIZACIÓN

La información que se extraiga en el IntentService, podrá ser enviada a la actividad principal


vía broadcast, encapsulada en otro Intent, o bien se podrá mostrar directamente en la
interfaz de usuario, por ejemplo, a través de una Toast.

Los IntentService son la clase base para los servicios que manejan peticiones asíncronas, en
forma de Intent. Para ello, utilizan, internamente, AsyncTask, que encapsula un Handler. En
caso de querer acceder a la interfaz de usuario desde el IntentService, se deberá tener en
cuenta que existe un bug 16 en AsyncTask que hace dicho Handler no se inicialice siempre en el
hilo main, sino que será inicializado en aquel hilo donde la clase ejecute sus inicializadores
estáticos. Por lo tanto, en IntentService.onHandleIntent() se deberá utilizar un Handler
propio para poder acceder al hilo main:

public class MyActivityRecognitionIntentService extends IntentService {

Handler mMainThreadHandler = null;

public MyActivityRecognitionIntentService() {
super(MyActivityRecognitionIntentService.class.getName());
mMainThreadHandler = new Handler();
}

@Override
protected void onHandleIntent(Intent intent) {

// Se comprueba si el Intent recibido contiene información de la


// actividad del usuario.
if (ActivityRecognitionResult.hasResult(intent)) {
// Extracción del resultado
ActivityRecognitionResult result =
ActivityRecognitionResult.extractResult(intent);
// Se obtiene la actividad más probable
DetectedActivity mostProbableActivity =
result.getMostProbableActivity();
// Se obtiene la probabilidad
final int confidence = mostProbableActivity.getConfidence();
// Tipo de actividad
int activityType = mostProbableActivity.getType();
final String activityName = getNameFromType(activityType);
// Ya se ha obtenido la información de la actividad.
// Ahora, se muestra al usuario a través de una Toast
// Debido al BUG de AsyncTask (que se ejecuta en cualquier hilo,
// no en el main), se ha de encapsular la Toast así.
mMainThreadHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(),
getString(R.string.actividad_del_usuario_) +
activityName +

16
Resolución bug AsyncTask:
http://stackoverflow.com/questions/4443278/toast-sending-message-to-a-handler-on-a-dead-thread
También se podría hacer Class.forName() en Application:
https://code.google.com/p/android/issues/detail?id=20915

CURSO DE DESARROLLO DE APLICACIONES ANDROID 37


TEMA 16. MAPAS Y LOCALIZACIÓN

getString(R.string._probabilidad_) +
confidence +
getString(R.string._simbolo_porcentaje),
Toast.LENGTH_SHORT).show();
}
});

// También se podría enviar esta información a la actividad


// principal vía broadcast
} else {
// Se ignoran los Intents que no contienen información de la
// actividad del usuario.
}
}

// Extracción del nombre de la actividad en función de su tipo


private String getNameFromType(int activityType) {
switch (activityType) {
case DetectedActivity.IN_VEHICLE:
return getString(R.string.en_coche);
case DetectedActivity.ON_BICYCLE:
return getString(R.string.en_bicicleta);
case DetectedActivity.ON_FOOT:
return getString(R.string.a_pie);
case DetectedActivity.STILL:
return getString(R.string.parado);
case DetectedActivity.UNKNOWN:
return getString(R.string.desconocido);
case DetectedActivity.TILTING:
return getString(R.string.inclinando);
}
return getString(R.string.desconocido);
}
}

Por último, se ha de recordar que el IntentService ha de ser declarado en el manifiesto de la


aplicación, al igual que el resto de actividades o servicios que pudiesen existir:

<service
android:name="com.cursoandroid.mymaps.MyActivityRecognitionIntentService"
android:exported="false">
</service>

CURSO DE DESARROLLO DE APLICACIONES ANDROID 38


TEMA 16. MAPAS Y LOCALIZACIÓN

Simulación de ubicaciones

Durante el desarrollo de la aplicación se tendrán que hacer diversas pruebas para verificar el
modelo implementado para obtener la ubicación. Estas pruebas podrán realizarse en un AVD
al cual se envíen datos de ubicación GPS simulados desde Eclipse o vía Telnet, aunque para ello
será necesario instalar un APK de Google Play Services actualizado en el AVD, así como otros
paquetes.

En Eclipse, se abrirá la perspectiva DDMS accediendo al menú Window > Open Perspective >
Other… > DDMS. Dicha perspectiva contiene el panel Emulator Control el cual, en la zona
inferior, presenta los controles de ubicación (Location Controls).

Existen tres formas de introducir datos de ubicación simulados:

• Manual: se pueden enviar datos de ubicación (longitud y latitud) en formato


sexagesimal o en formato decimal.
• GPX: se podrá utilizar un archivo GPX para simular una ruta a través de un conjunto
ordenado de ítems.
• KML: se podrá utilizar un archivo KML para proporcionar información de ubicación de
múltiples puntos en el mapa.

Para poder enviar los datos simulados al AVD, será necesario seleccionarlo previamente en la
vista Devices.

Si la aplicación está escuchando las actualizaciones de ubicación, al pulsar el botón Send de la


vista Emulator Control, la aplicación recibirá los datos de ubicación introducidos en dicha vista.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 39

Anda mungkin juga menyukai