Páginas

20 sept. 2015

Migrar una aplicación de Android a Xamarin

Tras las últimas batallas con arquitecturas Android he decidido darle una oportunidad a Xamarin ya que los problemas que estaba teniendo con Android Studio estoy acostumbrado a resolverlos a diario con Visual Studio, el entorno con el que habitualmente trabajo. Estoy muy contento pues he estado migrando la aplicación en la que más usuarios activos tengo y el proceso no ha sido demasiado costoso.

1º Crear una solución en Blanco
2º Añadir un Proyecto Android vacío
3º Copiar layouts de su carpeta en el proyecto antiguo a la carpeta del proyecto nuevo.
4º Renombrar de xml a axml esos ficheros
5º Lo mismo pero con los strings (en éste proyecto tengo muchísimas traducciones y esto fue un rollo)
6º Generar las carpetas para los drawables e ir copiando del proyecto viejo al nuevo.
7º Hacer los ajustes necesarios en el manifest.
8º Para poder usar los anuncios, tal y como los usaba con Admob, tuve que añadir el componente de Google Play Services (botón derecho sobre la carpeta componentes desde la solución, siguiente siguiente, etcétera...)
9º Copiar la actividad de inicio, el .java, al nuevo proyecto, renombrar el fichero a .cs y modificar el código para que sea C# en vez de java (son cuatro cosas, de imports a usings, the super a base, los getters y los setters pasan a ser propiedades y los namespaces se mantienen casi iguales)
10º Establecer la actividad de inicio en el código añadiendo el atributo así:

[Activity(Label = "YourProyect.Droid", MainLauncher = true, Icon = "@drawable/your_icon_resource")]

Ojo: todas las Activities tienen que tener el atributo [Activity]

Además, de esto añadí varios servicios, un bradcast reciever y ahora estoy adaptando los anuncios pero he tenido algún problema para que compilara y lo arreglé fijándome bien en las versiones de Android contra las que compilo (21 en mi caso) y añadiendo en las opciones del proyecto, en opciones de Android, Opciones avanzadas "Java Max Heap Size:" 1G.


Y de momento eso es todo por ahora, voy a seguir más actividades y dejando el código bonito, como a mi me gusta. ¡Que viva C#!

16 sept. 2015

Como sobrevivir a CryptoWall 3.0

El otro día me llamó mi madre mientras yo estaba escalando en la montaña y me dijo que el ordenador no arrancaba, que llevaba un rato y no había ni aparecido el escritorio así que le dije que lo apagara con un botonazo si hacía falta y le prometí que me pasaría en cuanto pudiera para echarle un vistazo. El lunes, al salir del trabajo, me pasé por su casa y encendí el ordenador y me encontré con que tras hacer login el equipo no respondía y si iniciaba el administrador de tareas éste se cerraba a los pocos segundos. 


Pensé que lo mejor era reiniciar y arrancar el equipo en el modo a prueba de fallos pero al intentarlo éste se reiniciaba automáticamente, no lo conseguí ni en modo consola, ni en modo a prueba de fallos ni en modo a prueba de fallos con funciones de red, se reiniciaba siempre, no obstante se me ocurrió probar el "Modo de restauración de servicios de directorio" (información sobre los modos de arranque) y para mi sorpresa el sistema, por fin, arrancó en modo a prueba de fallos. 

Según arrancó el sistema pulsé Ctrl+Sift+Esc para abrir el administrador de tareas y me puse a matar procesos como un loco hasta dejar lo imprescindible para que no se reiniciara el sistema. Me dí cuenta de que había una serie de procesos que pese a matarlos se relanzaban ellos solos y en concreto el que me llamó la atención era dllhost.exe. Éste proceso es el que estaba ralentizando el sistema y si lo mataba volvía a ejecutarse tras unos cinco segundos. 



Desde el propio administrador de tareas ejecuté cmd para abrir una consola de windows y mientras cada cinco segundos mataba el proceso dllhost.exe escribí lo siguiente en la consola:

for /l %x in (1,1,100) do ( taskkill /f /im dllhost.exe)


Ese comando ejecuta cien veces la instrucción de matar el proceso dllhost.exe, luego lo edité para que se ejecutara cien mil veces y entonces ya puede empezar a trabajar. El ordenador seguía yendo muy lento pero una vez muerto el proceso dllhost.exe por fin pude ver el escritorio y un montón de ficheros con extensión .aaa y busqué desde mi móvil al respecto mientras pasaba el Malwarebytes Antimalware. Vi que se trataba del virus Crypto Wall porque se abrieron varias ventanas de internet explorer con el mensaje de chantaje y también en formato txt. Mientras trabajaba para intentar quitar el virus vi en el administrador de tareas (no lo cerré en ningún momento) que se abrían procesos con nombrees conocidos tipo msiexec.exe y cosas así y esos los fui matando según me percataba. 

En ésta página vi varias rutas donde se suele alojar el virus y con rmdir /R NombreDelDirectorio, desde la consola, fui eliminando carpetas "raras" que iba encontrando en:
  • C:\<random>\<random>.exe
  • C:\Users\<User>\AppData\Local\<random>.exe (Vista/7/8)
  • C:\Users\<User>\AppData\Local\<random>.exe (Vista/7/8)
  • C:\Documents and Settings\<User>\Application Data\<random>.exe (XP)
  • C:\Documents and Settings\<User>\Local Application Data\<random>.exe (XP)
  • %Temp%
También me percaté de que en el registro había entradas del tipo svc+ 5 caracteres aleatorios así que busqué svc y me recorrí todo el registro borrando entradas sospechosas (ojo que aquí si no sabes lo que borras te la juegas)...

Y todo esto que os cuento lo hice mientras pasaba mbam.exe y me bajaba un antivirus y lo instalaba. Tras varias horas de lucha reinicié el equipo para que el antivirus hiciera un análisis profundo y éste quitó todo lo que yo no había sido capaz de encontrar y borrar a mano y mi madre vuelve a tener ordenador pero muchos de sus documentos fueron cifrados y tras probar varias recetas he preferido darlos por perdidos y directamente los he eliminado. Hubo mucha suerte porque el virus se cebó principalmente con varias copias de seguridad que yo tenía en ese ordenador con documentos de la universidad, de repositorios de código y cosas que, en el fondo, ya no eran útiles, eso sí, todo lo que había en el escritorio, fotos y documentos de MS Office, fueron cifrados y los hemos dado por perdidos y eliminado. En total la cantidad de ficheros cifrados han sido algo mas de once mil, unos 8 GB de ficheros de texto, código y documentos de MS Office... Pero lo cogí a tiempo y le corté los huevos.

Un detalle chulo al caso de todo esto, recordé varias pruebas que hice hace tiempo con rootkits caseros pues mbam y el antivirus encontraron entradas ADS (Alternate Data System) con el virus que es la delicia para los rootkits y el spyware. A grandes rasgos ADS puede servir como sistema de ficheros alternativo y oculto para el sistema operativo y el usuario, es el lugar ideal para meter mierda sin que nadie se entere...

Y lo dejo por hoy. Espero que esto le valga a alguien.

31 ago. 2015

Sobre arquitecturas en Android

Ya hablé algo sobre este tema hace mucho tiempo pero las cosas han cambiado mucho desde entonces y escribo este artículo puesto que me ha costado mucho encontrar información útil sobre la materia. Parece que a quienes documentan para los desarrolladores Android no les importan mucho las buenas prácticas en cuanto a arquitectura o dan por hecho que los desarrolladores son suficientemente maduros como para solucionarse ellos solos estas cuestiones. La verdad es que ideas no me faltan, ni me faltaban, pero al no encontrar ni una sola referencia a este tema uno acaba metiendo todo en el mismo proyecto Android y acaba cayendo en errores típicos de otra década como son alto acoplamiento, poca reutilización de código y poca cohesión, en definitiva código sucio y difícilmente escalable... Pero por suerte, tras mucho buscar y varias preguntas en Stack Overflow, entre ellas éste, he encontrado con algo que me gusta y sobre lo que quiero basar mi actual desarrollo, Clean Architecture.

Una de las últimas tendencias en cuanto a arquitecturas, no sólo para Android, se conoce como Clean Architecture, no voy a entrar en detalles pues el artículo que la presenta lo hace mucho mejor de lo que yo lo podría hacer, se merece al menos una lectura en diagonal si vas a seguir leyendo mi artículo:
Buscando por la red he encontrado una implementación de este patrón que es lo que quiero replicar hoy y es el objetivo de éste artículo. Estoy hablando del artículo de Fernando Cejas - Architecting Android…The evolution Él propone lo siguiente:


Fernando nos dice que la cantidad de círculos, de capas, no importa, que lo verdaderamente importante es que las dependencias vayan en una única dirección, hacia dentro. Si nos fijamos este modelo viene a ser el diseño guiado por domino (Domain Driven Design) pero representado en forma de círculos concéntricos. ¿Qué significa cada cosa? (Me voy a limitar a hacer una traducción casi literal)
  • Entities: Entidades, objetos de negocio de la aplicación.
  • Use Cases: dirigen el flujo de datos de y hacia las entidades, también se les conoce como Interactors
  • Interface Adapters: son un conjunto de adaptadores que convierten datos entre el formato mas conveniente para los Interactors y las entidades...
  • Frameworks and Drivers: aquí es donde van todos los detalles, la interfaz de usuario, herramientas y ese tipo de cosas...
Yo no voy a seguir el ejemplo de Fernando, yo ya tengo una aplicación con una funcionalidad muy básica que quiero migrar hacia éste modelo arquitectónico por capas tal y como estoy acostumbrado a hacerlo en otros entornos partiendo de mi actual proyecto y lo que voy a hacer va a ser crear una librería de Android para el dominio y otra para los datos y ahora os enseño como queda la estructura de mi proyecto.
Varias horas después...
 Hey! ¡Sigo aquí! Me he peleado con gradle y con temas de dependencias, con los nombres de los paquetes, con Android Studio y con un montón de cosas (de hecho empecé ayer con todo esto y hoy he seguido según he vuelto del trabajo...) Y bueno, ya tengo un proyecto que compila, aún no lo he ejecutado porque sé que el funcionamiento de lo que haya ahora será trivial, absurdo, he quitado mucho de lo que tenía para ceñirme a la estructura del proyecto de Fernando porque viendo su código y su estructura he visto muchas cosas que me gustan y muchas técnicas que quiero utilizar en mi proyecto... Y, lo dicho, por fin tengo un proyecto que compila y con la siguiente estructura:


Una de las cosas que me han gustado de lo que he visto y copiado del proyecto de Fernando es la Inyección de Dependencias ya que yo vengo de .Net y no había visto nada parecido aplicado a Android como Dagger 2. Lo nombro pero no voy a explicar como se hace ya que podéis verlo en el código del proyecto y podéis leer un manual bastante potente que el mismo Fernando escribió hace no mucho: Tasting Dagger 2 on Android

De igual forma me ha gustado mucho el uso de RxJava, ya me lo habían recomendado pero hasta ahora no me había atrevido a adentrarme mucho, pero ya que el ejemplo lo traía le he dedicado un buen tiempo a leer sobre él, tanto en sus orígenes como Rx (Reactive Extensions) (puede que lo use para solucionar un requisito con el que estamos de lleno en el curro), hasta varios ejemplos de RxJava como éste del propio Fernando y o este otro muy interesante de Matthias Käppler, además de la Wiki de GitHub ... Pero quizá lo que más aclare su aplicación y uso sea ésta página sobre el The RxJava Android Module y éste diagrama que explica como se utiliza, con qué intenciones:


Al final el resultado es, a grandes rasgos, el módulo de presentación que contiene lo relativo a presentación pura, con un patrón MVP además de varios paquetes de utilidades (constantes, excepciones, la inyección de dependencias, etcétera); un modulo de Datos cuya responsabilidad es la gestión de los datos de la aplicación, esto es, cachés, ficheros locales, llamadas al API Rest, etcétera; y un módulo de Dominio donde se definen las clases del dominio y los casos de uso, los interactors, que son inyectados en la capa de presentación mediante ActivityComponentes y Módulos (ésto está en presentation/internal.di/).  

Me ha costado bastante, la verdad, sacar conclusiones de lo que he ido viendo, entender usos de algunas cosas haciendo ingeniería inversa, adaptar su proyecto y la inyección de dependencias en la capa de presentación para tirar de AppCompatActivity y poder seguir usando Material tal y como lo estaba haciendo antes... pero todo o casi todo lo que he visto me ha gustado y definitivamente voy a usar esta estructura para mi proyecto actual. Aún tengo que refactorizar cosas, por ejemplo me gustaría sacar clases genéricas en varias cosas pues en su ejemplo trabaja con users pero yo voy a trabajar con otras entidades y ya he visto mientras estudiaba el código varios puntos donde tendría que generalizar... Quizá dentro de poco vuelva a escribir para contar qué tal me va con ésta aventura en la que ando liado últimamente, espero que todo esto que he escrito, que no dejan de ser divagaciones, le sirva a alguien para mejorar un poco la estructura y la calidad de sus proyectos Android.

A Fernando, si alguna vez me lees, gracias!

24 feb. 2013

Sobre requisitos e iteraciones en metodologías ágiles

Cuando se trata de la creación de aplicaciones a medida, muchos de nosotros creemos que es posible predecir con exactitud el tiempo que un grupo de desarrolladores necesita para crear el software que cumpla con nuestros requisitos. También queremos creer que sabemos cuáles son esos requisitos desde el principio, antes incluso de haber hecho ningún análisis o haber oído las necesidades del cliente en profundidad pensando que los requisitos no van a cambiar durante el desarrollo. Ayer mismo oí que "el problema de éste proyecto es que los requisitos están cambiando constantemente" y eso es lo que hoy me lleva a escribir ésta pequeña entrada.

En general nada de lo anterior es cierto en la mayoría de los proyectos, no es posible la predicción en un desarrollo a largo plazo, digamos que de más de un año, en gran parte debido a que no se pueden obtener los requisitos totalmente detallados por adelantado y según va creciendo el proyecto tendremos que ir dejando que los requisitos se adapten a los deseos del cliente. Tenemos que dejar evolucionar los requisitos porque aunque suene como el típico tópico el cliente siempre tiene razón, él es quien nos paga, quien nos da de comer, y quien durante y después del desarrollo nos criticará o nos alabará y de quien dependerá nuestra reputación en el futuro y no podemos negar estas realidades. Muchas empresas siguen utilizando procesos de desarrollo de software que no funcionan bien y que alimentan pensamientos inadecuados en arquitectos, analistas, desarrolladores etcétera y que influyen negativamente en la moral indivudual y colectiva.

Por suerte parece que esto está cambiando, hay metodologías cada día más populares como SCRUM que pregonan que "el cambio es bienvenido" y buenas prácticas de desarrollo ya comentadas en éste mismo blog que preparan nuestro código para ello. Nos adaptamos y adaptamos nuestras arquitecturas y nuestros diseños a la realidad o fracasamos, esa es la cuestión y la realidad es que hay que adaptarse a los cambios. 

Haciendo el desarrollo de software de esta manera, de una manera ágil al fin y al cabo, puede dar miedo al principio, los procesos ágiles son por lo general mejores tanto para los equipos de desarrollo como para los comerciales o empresarios que pagan las facturas y para entender por qué esto es así, tenemos que empezar por entender lo que es un proceso ágil en realidad.

¿Qué significa el desarrollo ágil?

Es una gran pregunta y muy a la orden del día. El reto en cualquier proyecto de desarrollo de software siempre es el mismo: crear software en un contexto de incertidumbre. Al inicio de un proyecto de desarrollo no sabemos si hemos definido bien los requisitos y tampoco si éstos requisitos cambiarán mientras estamos desarrollando e implantando el software.

Un proceso de desarrollo tradicional hace lo posible para fingir que esta incertidumbre no existe. En el enfoque de cascada clásico, por ejemplo, una organización crea planes detallados y calendarios precisos antes de escribir ningún código. Lo puedo decir alto y claro porque me avalan miles de experiencias fallidas a lo largo de la historia de la ingeniería del software: ¡esto está mal! ¡Es un error! Los proyectos reales rara vez cumplen con estos planes y programas porque a pesar de que se utiliza el término "ingeniería del software", escribir código no es como otros tipos de ingeniería, yo siempre digo que se trata de un proceso de ingeniería y un proceso artístico-creativo a partes iguales. Para construir un puente, un edificio, un barco o un satélite de comunicaciones es posible definir unos requisitos por adelantado y darle una forma concreta que quedará plasmada en unos planos que no cambiarán a lo largo de la fabricación... Sin embargo en proyectos de software ésto nunca ha sido posible aunque se ha intentado innumerables veces porque la creación de requisitos estables por adelantado es generalmente imposible, en parte porque la gente no sabe realmente lo que quiere hasta que no lo ven. Además es difícil basar un proyecto en experiencias previas ya que cada proyecto de desarrollo implica cierto grado de innovación porque si no fuera así sería más fácil comprar algún producto comercial que desarrollar uno a medida.

Los procesos tradicionales van de espaldas a la realidad y sin embargo los procesos ágiles están pensados para estas situaciones. Un proceso ágil proporciona una manera de agregar, eliminar y modificar  requisitos durante el desarrollo y, como tan claramente propone SCRUM, en vez de oponerse al cambio, éste tiene que ser bienvenido. Algo también importante en procesos ágiles es que reconocen que a corto plazo los planes son más estables que a largo plazo lo que hace posible que aunque no sepamos exactamente lo que se quiere en un futuro a medio plazo, digamos tres meses, probablemente si sabemos lo que queremos hacer a corto plazo, digamos durante las próximas tres semanas, la duración típica de un Sprint SCRUM.

Para lograr esto, un proceso de desarrollo ágil se basa en la iteración como unidad de trabajo, una unidad de tiempo que será analizable y cuantificable sobre la que se pueden aplicar métodos científicos en busca de optimizaciones. Un gran avance en muchos aspectos.

¿Qué se espera de una iteración?

Cada iteración crea más producto acabado, es decir, aporta valor añadido a nuestro software sobre unos  requisitos actualizados al comienzo de cada una de ellas. Un proceso de desarrollo ágil es iterativo si ofrece la posibilidad de añadir, eliminar o modificar los requisitos del proyecto al comienzo de cada iteración, es decir, de cada pequeño ciclo y en esto se fundamenta uno de los procesos ágiles mas utilizados en la actualidad: SCRUM.

Como ya he hablado y ofrecido en éste mismo blog mi visión de SCRUM(*1, 2, 3) hoy quiero volver a hablar sobre sobre Scrum, pocas palabras, ésta vez sobre una parte importante de éste proceso ágil: el jefe de producto o Product Owner, que representa al cliente o patrocinador del proyecto. El Product Owner no tiene por qué entender de código, no es un papel técnico, tan sólo tiene que tener claro lo que quiere y cómo lo quiere y, mediante un contacto y un feedback continuo con el equipo de desarrollo conseguir que el equipo haga lo que tiene que hacer y desarrolle exactamente lo que tiene que desarrollar.

Desde la perspectiva de un patrocinador comercial, la iteración (que en Scrum se conoce como SPRINT) consta de cuatro pasos:
  1. En primer lugar, el Product Owner crea a una lista de elementos a desarrollar y les asigna una prioridad, esta lista se denomina Product Backlog y contiene los requisitos  del producto. Ésta lista es dinámica y cambia con el tiempo, con cada iteración. 
  2. A continuación, atendiendo a las prioridades definidas por el Product Owner, el equipo selecciona qué elementos van a desarrollar y crean lo que se conoce como Sprint Backlog. 
  3. El equipo de desarrollo implementa los elementos seleccionados, y lo hacen "a su manera" autogestionándose y sin intromisiones externas, y el resultado del Sprint es un software potencialmente entregable, en otras palabras, es una aplicación útil y probada que incluye los elementos en el sprint backlog junto con los de anteriores Sprints.
  4. Una vez que el Sprint ha terminado el equipo de desarrollo muestra su trabajo al Product Owner para que lo valide. El Product Owner puede entender, de esta manera, lo que realmente se está creando y cómo se está haciendo y durante las demostraciones se podrán incluir sugerencias sobre nuevas características o cambios que, si el Product Owner ve convenientes, podrían convertirse en parte del Product Backlog.
Al acabar la iteración o Sprint los requisitos podrían haber cambiado, de hecho, no sería raro que lo hicieran, o podría haber requisitos nuevos o se podrían haber eliminado requisitos aún no desarrollados pero esto, en contraposición a procesos clásicos de desarrollo de software, no será un problema en absoluto ya que el Product Owner actualizará el Product Backlog, volverá a asignar prioridades y el proceso comenzará con la selección por parte del equipo de desarrollo de un nuevo conjunto de requisitos, Sprint Backlog, a desarrollar durante el siguiente Sprint.

Éste proceso continúa hasta que una de éstas tres cosas ocurre:
  1. El proyecto se queda sin dinero 
  2. Se acaba el tiempo total asignado al proyecto 
  3. Todos los elementos del Product Backlog son implementados y se entrega el producto acabado.
Creo que están claros muchos de los beneficios del desarrollo ágil pero voy a intentar enumerarlos:

  1. El equipo tiene más posibilidades de crear lo que la empresa realmente necesita.
  2. Hay un responsable directode crear y dar prioridad a los elementos del product backlog y en definitiva de que el producto satisfaga las necesidades reales de la empresa, el Product Owner.
  3. El equipo desarrolla los requisitos más importantes primero, algo que es bueno para la empresa ya que es mucho más probable conseguir lo que quiere, o acercarse mucho al resultado deseado en caso de que acaben faltando recursos monetarios o del tipo que sean.
  4. Y debido a que el proceso es iterativo, hay muchas posibilidades de hacer cambios.
  5. El Product Owner obtiene una gran comprensión, un gran conocimiento, de lo que se creó durante el proyecto. Y un punto de vista objetivo y directo del proyecto en cada iteración, pudiendo cambiar lo que ocurra o se aborde en la siguiente.
  6. No hay nada como ver, usar y ver usar a otros las primeras versiones de software, es la mejor manera de entender lo que los usuarios realmente quieren, son pasos hacia el éxito.
Pero ojo, el riesgo de un proyecto no se reduce sólo por aplicar metodologías ágiles, siempre hay riesgos. La metodologías ágiles iterativas permiten medir la idoneidad, es decir, si se está desarrollando lo que hay que desarrollar, pero siempre habrá riesgos en cuanto a plazos de entrega, riesgos arquitectónicos y, principalmente, riesgos organizativos. Imaginemos una gran compañía con un gran departamento dedicado al desarrollo de software para uso interno o para incluirlo en sus productos. Puede suceder, sería lo lógico, que este gran departamento incluyera a gente gestora de proyectos, gente de análisis de requisitos y gente de desarrollo y arquitecturas de software. Y puede suceder y sucede que el exceso de burocracia haga que los requisitos cambien en una dirección y varios sprints después cambien en la contraria para luego volver al planteamiento incial dos o tres sprints después con el consiguiente gasto de tiempo y dinero y la consecuente frustración del personal humano involucrado en las decisiones, el análisis, la gestión y el desarrollo. Estas cosas pasan, yo lo he visto con mis propios ojos, el exceso de burocracia es un riesgo no asumido y desgraciadamente difícilmente asumible pues en las grandes compañías siempre hay largas cadenas de mando y responsabilidades y al final hay muchas cabezas pensantes las cuales, durante pequeños momentos de gloria, ejercen momentáneamente de jefes de producto imponiendo sus opiniones sin conocer cuál ha sido la evolución real del proyecto. Esto desencadena requisitos que quizá  ya fueron desarollados y después eliminados o modificados por el motivo que fuera, funcionalidades que volverán a ser analizadas por los grupos de análisis, probablemente de mala gana, y re-desarrolladas en un ambiente poco deseable por los desarrolladores.

Ésto es a lo que se refería aquel compañero del que hablaba al principio y lo que me ha llevado a escribir éste artículo y son estos cambios erráticos de requisitos provocados por los excesos de burocracia, por las largas cadenas de mando y por los múltiples egos involucrados en grandes proyectos con los que quiero cerrarlo.

Hasta la próxima entrada,

Juan García Carmona

28 ene. 2013

Google Maps API key para apk firmados

Es la tercera vez que me tengo que poner a buscar cómo sacar la key de una aplicación para que ésta pueda acceder al API de Google Maps y he decidido hacer una mini receta para tenerla a mano.

Hay que localizar el keystore que en mi caso está en C:\Users\Juan\.android y ejecutar desde el directorio /bin de nuestro jdk (en mi caso C:\Program Files\Java\jdk1.7.0_03\bin>) lo siguiente:

keytool.exe -V -list -alias androiddebugkey -keystore "C:\Users\Juan\.android\debug.keystore" -storepass android -keypass android

Y obtendremos una salida como la siguiente:

Nombre de Alias: androiddebugkey
Fecha de Creaci¾n: 20-abr-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: 4f912680
Vßlido desde: Fri Apr 20 11:04:00 CEST 2012 hasta: Sun Apr 13 11:04:00 CEST 2042

Huellas digitales del Certificado:
         MD5: B5:E3:29:E3:9A:18:69:20:06:E0:23:1C:XX:XX:XX:XX
         SHA1: E6:F4:3B:8C:32:F3:D4:D5:A8:8C:91:C3:5C:D8:4F:24:XX:XX:XX:XX
         SHA256: 84:4B:8C:89:DB:89:1E:D1:91:7E:9A:FC:AE:DB:27:BC:43:E0:E5:6E:FB:
26:44:68:CF:CB:7C:3A:XX:XX:XX:XX
         Nombre del Algoritmo de Firma: SHA1withRSA
         Versi¾n: 3

Pero ojo, ésto sólo nos vale para depurar, si queremos publicar la aplicación tendremos que ir a la consola del desarrollador de Google, crear un proyecto, activar el API de Google maps y crear una nueva Android Key. Mi keystore para firmar aplicaciones lo tengo en D:\WORK\eclipsee\Keystore\keystore  y la documentación del API v2 dice exáctamente:

This key can be deployed in your Android applications.
API requests are sent directly to Google from your clients' Android devices. Google verifies that each request originates from an Android application that matches one of the certificate SHA1 fingerprints and package names listed below. You can discover the SHA1 fingerprint of your developer certificate using the following command:
keytool -list -v -keystore mystore.keystore Learn moreAccept requests from an Android application with one of the certificate fingerprints and package names listed below:
One SHA1 certificate fingerprint and package name (separated by a semicolon) per line. Example:45:B5:E4:6F:36:AD:0A:98:94:B4:02:66:2B:12:17:F2:56:26:A0:E0;com.example

Por lo tanto mi comando desde el bin del jdk será:

keytool -list -v -keystore "D:\WORK\eclipsee\Keystore\keystore"

Y efectivamente he obtenido el SHA1 que estaba buscando, tan sólo me queda pegarlo seguido del nombre del paquete que utilizará la API para obtener la key que tendré que pegar en el AndroidManifest.xml para que mi aplicación firmada y subida a Google Play pueda acceder a los mapas de Google.

Posteriormente la experiencia me dice que lo mejor es crear el apk firmado, subirlo al teléfono localmente para instalarlo y probarlo y si todo va como debe entonces subirlo a Google Play. (ya perdí unos 300 usuarios una vez por subir con prisas un apk que tenía en el manifest la clave en modo debug :s)

Hasta las próxima entrada.

Juan García Carmona

26 dic. 2012

Crear aplicaciones Multiidioma para Android

En los dos últimos artículos intenté mostrar todo el proceso de creación de una aplicación y, sobretodo, cómo dar los primeros pasos desarrollando aplicaciones de mapas utilizando Google Maps Android API v2. En ésta ocasión y a raiz de que creo que la aplicación que desarrollé a modo de ejemplo podría tener algo de tirón dentro de toda la comunidad de musulmanes a nivel internacional y no sólo entre los musulmanes hispanohablantes como pensé en un principio he decidido hacer multi-idioma dicha aplicación para que cualquier musulman del planeta la pueda utilizar.

Lo primero que voy a hacer va a ser cambiarla de nombre para que tenga un nombre más internacional. Me he preguntado cuál es el idioma que más musulmanes hablan y buscando un poco he encontrado ésta pregunta y una buena respuesta dice:
El mayor número de musulmanes del mundo hablan Filipino, Indú, Arabe y Francés, ya que en cuanto a números la mayor cantidad de Musulmanes están en el sudeste de Asia, en la India, en la llamada "Media Luna" de Oriente Medio y en los países excolonias belgo/francesas de Africa.
Atendiendo a esa respuesta y haciendo varias búsquedas en Google barajo varios posibles nombres,  Mekah y Makkah, siendo esta segunda la palabra más internacional de todas para referirse a La Meca. Creo que la voy a cambiar de nombre a Makkah Compass.

Hay una manera fácil y recomendada para hacer aplicaciones multi-idioma para Android y consiste en crear una carpeta llamada "values-sufijo" que contenga el strings.xml que corresponda dentro, por ejemplo values-en tendrá en su interior el fichero con los textos en inglés. (podéis ver la documentación oficial aquí: http://developer.android.com/guide/topics/resources/localization.html )

He encontrado el listado de los ISO Country codes aquí:


Voy a empezar con:

AE UNITED ARAB EMIRATES
FR FRANCE
ID INDONESIA
IQ IRAQ officially assigned
IR IRAN, ISLAMIC REPUBLIC OF
PH PHILIPPINES
PK PAKISTAN
PS PALESTINIAN TERRITORY, OCCUPIED
IN INDIA

Éstos son los distintos ficheros strings.xml que acabo de generar:

values-ae/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="intro_arabe">وأعرب عن وحدانية الله في الصلاة عقائديا المجتمع، يجب على جميع المسلمين مراقبة، والصلاة في نفس الاتجاه: نحو الكعبة، أول بيت الله.</string>
    <string name="intro"></string>
    <string name="lanzar_mapa">الى اين هو الكعبة؟</string>
    <string name="lanzar_configuracion_gps">GPS إعدادات</string>
    <string name="app_name">مكة البوصلة</string>

</resources>
values-es/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="intro_arabe">وأعرب عن وحدانية الله في الصلاة عقائديا المجتمع، يجب على جميع المسلمين مراقبة، والصلاة في نفس الاتجاه: نحو الكعبة، أول بيت الله.</string>
    <string name="intro">La unicidad de Dios se expresa ritualmente en la oración comunitaria, que todos los musulmanes deben observar, rezando en la misma dirección: hacia la Kaaba, la primera casa de Dios.</string>
    <string name="lanzar_mapa">¿Hacia dónde está la Kabaa?</string>
    <string name="lanzar_configuracion_gps">Configuración GPS</string>
    <string name="app_name">Makkah Compass</string>

</resources>
values-fr/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="intro_arabe">وأعرب عن وحدانية الله في الصلاة عقائديا المجتمع، يجب على جميع المسلمين مراقبة، والصلاة في نفس الاتجاه: نحو الكعبة، أول بيت الله.</string>
    <string name="intro">L\'unicité de Dieu est exprimé rituellement dans la prière communautaire, tous les musulmans doivent observer, en priant dans la même direction: vers la Kaaba, la première maison de Dieu.</string>
    <string name="lanzar_mapa">Où se trouve la Kaaba?</string>
    <string name="lanzar_configuracion_gps">Paramètres GPS</string>
    <string name="app_name">La Mecque boussole</string>

</resources>
values-id/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="intro_arabe">وأعرب عن وحدانية الله في الصلاة عقائديا المجتمع، يجب على جميع المسلمين مراقبة، والصلاة في نفس الاتجاه: نحو الكعبة، أول بيت الله.</string>
    <string name="intro">Keesaan Tuhan diungkapkan dalam doa ritual komunitas, semua muslim harus mengamati, berdoa dalam arah yang sama: menuju Ka'bah, rumah Allah yang pertama.</string>
    <string name="lanzar_mapa">Kemanakah adalah Ka'bah?</string>
    <string name="lanzar_configuracion_gps">GPS Pengaturan</string>
    <string name="app_name">Mekkah kompas</string>

</resources>
values-in/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="intro_arabe">وأعرب عن وحدانية الله في الصلاة عقائديا المجتمع، يجب على جميع المسلمين مراقبة، والصلاة في نفس الاتجاه: نحو الكعبة، أول بيت الله.</string>
    <string name="intro">परमेश्वर की एकता धार्मिक समुदाय प्रार्थना में व्यक्त किया है, सभी मुसलमानों का निरीक्षण, एक ही दिशा में प्रार्थना: Kaaba, भगवान के पहले घर की ओर.</string>
    <string name="lanzar_mapa">मक्का कहाँ है?</string>
    <string name="lanzar_configuracion_gps">जीपीएस सेटिंग्स</string>
    <string name="app_name">मक्का कम्पास</string>

</resources>
values-iq/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="intro_arabe">وأعرب عن وحدانية الله في الصلاة عقائديا المجتمع، يجب على جميع المسلمين مراقبة، والصلاة في نفس الاتجاه: نحو الكعبة، أول بيت الله.</string>
    <string name="intro"></string>
    <string name="lanzar_mapa">الى اين هو الكعبة؟</string>
    <string name="lanzar_configuracion_gps">GPS إعدادات</string>
    <string name="app_name">مكة البوصلة</string>

</resources>
values-ir/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="intro_arabe">وأعرب عن وحدانية الله في الصلاة عقائديا المجتمع، يجب على جميع المسلمين مراقبة، والصلاة في نفس الاتجاه: نحو الكعبة، أول بيت الله.</string>
    <string name="intro">یکتایی و یگانگی خداوند رسوم در نماز جامعه بیان می شود، همه مسلمانان باید رعایت، دعا در همان جهت به سمت کعبه، اولین خانه خدا.</string>
    <string name="lanzar_mapa">کجا کعبه است؟</string>
    <string name="lanzar_configuracion_gps">تنظیمات GPS</string>
    <string name="app_name">مکه قطب نما</string>

</resources>
values-ph/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="intro_arabe">وأعرب عن وحدانية الله في الصلاة عقائديا المجتمع، يجب على جميع المسلمين مراقبة، والصلاة في نفس الاتجاه: نحو الكعبة، أول بيت الله.</string>
    <string name="intro">Ang kaisahan ng Diyos ay ipinahayag ritually sa komunidad panalangin, ang lahat ng Muslim ay dapat obserbahan, nagdarasal sa parehong direksyon: patungo sa Kaaba, ang unang bahay ng Diyos.</string>
    <string name="lanzar_mapa">Pasaan ang Kaaba?</string>
    <string name="lanzar_configuracion_gps">GPS Setting</string>
    <string name="app_name">Mecca compass</string>

</resources>
values-pk/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="intro_arabe">وأعرب عن وحدانية الله في الصلاة عقائديا المجتمع، يجب على جميع المسلمين مراقبة، والصلاة في نفس الاتجاه: نحو الكعبة، أول بيت الله.</string>
    <string name="intro">کمیونٹی کی نماز میں خدا کی وحدانیت کا اظہار کیا ہے، تمام مسلمانوں کا مشاہدہ، ایک ہی سمت میں نماز بھی ضروری ہے: کعبہ، خدا کے پہلے گھر کی طرف.</string>
    <string name="lanzar_mapa">کعبہ کہاں ہے؟</string>
    <string name="lanzar_configuracion_gps">GPS ترتیبات</string>
    <string name="app_name">مکہ کمپاس</string>

</resources>
values-ps/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="intro_arabe">وأعرب عن وحدانية الله في الصلاة عقائديا المجتمع، يجب على جميع المسلمين مراقبة، والصلاة في نفس الاتجاه: نحو الكعبة، أول بيت الله.</string>
    <string name="intro"></string>
    <string name="lanzar_mapa">الى اين هو الكعبة؟</string>
    <string name="lanzar_configuracion_gps">GPS إعدادات</string>
    <string name="app_name">مكة البوصلة</string>

</resources>


Ha sido fácil, he copiado los cinco textos y los he ido traduciendo a los distintos idiomas oficiales de cada país, quizá en una próxima versión suba más traducciones a más idiomas pero creo que con estos será suficiente por ahora. Si alguien encuentra alguna traducción mal hecha que por favor me corrija.

إذا وجدت أي عدم تطابق الترجمة يرجى تصحيح لي.
यदि आप पाते हैं किसी भी बेमेल अनुवाद मुझे सही कृपया.
Jika Anda menemukan terjemahan mismatch tolong benar saya.
Kung nalaman mo ang anumang Maling pagtutugma ng pagsasalin paki-tama sa akin.

اگر آپ کو تلاش کسی بھی بیمیل ترجمہ براہ مہربانی آپ کے وزٹرز کا ریکارڈ رکھا درست کریں.


 La aplicación la podréis descargar en muy poco tiempo, actualizaré ésta misma entrada con el enlace a la descarga. 

24 dic. 2012

Aplicaciones de Google Maps v2 desde cero (II)

Antes de ayer le dediqué la mañana a aprender lo básico de programación con el API de Google Maps para Android y de paso a escribir un breve artículo exponiendo sin tapujos mis cábalas, lo que me iba pasando por la cabeza... Estuvo entretenido pero, como digo, me llevó toda la mañana incrustar un mapa en mi aplicación. Es cierto que tuve que leer bastante y también es cierto que tuve distracciones ya que estuve leyendo las noticias, mirando el facebook y haciendo alguna "tarea del hogar" pero no fue sencillo del todo. En pocas palabras, el anterior artículo se resume en:
  1. Crear el proyecto y el repositorio para la gestión y el control de cambios.
  2. Darle forma al código para que sea fácil de mantener, robusto y versátil.
  3. Conseguir la key del Google Maps Android API.
  4. Incluir referencias y librerías (tuve que actualizar eclipse y varios plugins y eso también me llevó un rato largo, google-play-services_lib.jar, android-support-v4.jar) 
  5. Insertar el mapa como fragment (lo que me llevó a pasar de utilizar Activity a usar FragmentActivity)
Todas estas cábalas las podéis leer con mucho mas detalle en la parte primera de ésta serie de artículos sobre Google Maps Android API.

Hoy quiero continuar con ese proyecto, el objetivo sigue siendo el mismo, construir una aplicación robusta, versátil, mantenible y ampliable enfocada a la comunidad musulmana cuya principal característica tiene que ser que nos diga siempre en qué dirección se encuentra exactamente la ciudad santa de La Meca. La planificación inicial era:

1º Requisitos
2º Análisis
3º Creación del proyecto y repositorio
4º Inserción de mapa
5º Localizar y apuntar a La Meca
6º Detalles de usabilidad
7º Crear y firmar la applicación y subirla a +Google Play

En la primera parte llegué hasta el punto 4 y hoy me gustaría terminar y publicar una versión BETA de ésta aplicación. así que empecemos.

Localización

En la actividad que maneja el mapa necesitamos un objeto tipo GoogleMap para manejarlo par alo cual hay que hacer lo siguiente:
GoogleMap mapa = ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map)).getMap();
Tendremos que importar además com.google.android.gms.maps.GoogleMap y com.google.android.gms.maps.SupportMapFragment, cosa que no debería darnos problemas si tenemos bien enlazadas las librerías del API y con ésto obtenemos una instancia del mapa que hemos declarado en la vista así:
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/map"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        class="com.google.android.gms.maps.SupportMapFragment"/>
Se pueden hacer muchas cosas con ese mapa y recomiendo leer éste segundo artículo de sgoliver sobre mapas para ver como jugar con los estilos del mapa y con la cámara, habrá cosas de ese artículo que utilizaré más adelante... Pero lo que ahora quiero es centrar el mapa en mi posición.

Haciendo mapa.setMyLocationEnabled(true); nos aparecerá el famoso círculo azul que indica nuestra posición y el radio aproximado de precisión pero el mapa seguirá centrado en el punto 0,0 y con el zoom por defecto...

Después de darle muchas vueltas a todo el tema de la localización y haber evolucionado mucho el código de mi MapActivity creo que es mejor que lo exponga y lo desgrane aquí que ir explicando los pasos que he ido siguiendo porque he dado muchas vueltas sin resultados. Es cierto que el código es bastante sucio pero esto es porque he intentado con multitud de comentarios que el código sea auto-explicativo:
package com.jgc.lameca;

import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.LocationSource;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.PolylineOptions;

import android.graphics.Color;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;

public class MapActivity extends BaseActivity implements LocationListener, LocationSource, SensorEventListener {
 // El mapa
 private GoogleMap mMap;
 // Para escuchar los cambios de localización del usuario, 
 // si se mueve o si la localización es más precisa
 private OnLocationChangedListener mListener;
 // Para gestionar nosotros la localización y no el propio mapa y poder 
 // manejar eventos como onLocationChanged que de otra manera no podríamos gestionar
 private LocationManager locationManager;

 private boolean haveLocation = false;

 // Sensores para orientar el mapa de acuerdo a la brújula
 private SensorManager mSensorManager;
 private float[] mGravity;
 private float[] mGeomagnetic;

 // para controlar el intervalo de ejecución del evento de los sensores
 private long lastTimeCompassUpdated = 0;

 /** Called when the activity is first created. */
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.map_layout);       

  // Instanciamos locationManager
  locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
  // Establecemos la precisión
  Criteria locationCriteria = new Criteria();
  locationCriteria.setAccuracy(Criteria.ACCURACY_FINE);
  // Y establecemos que se escuche al mejor proveedor de localización 
  // (o redes móviles o GPS o WiFi, dependerá del estado del dispositivo
  // y así nos despreocupamos nosotros)
  locationManager.requestLocationUpdates(locationManager.getBestProvider(locationCriteria, true), 1L, 2F, (LocationListener) this);

  initializeMap();       
 }

 private void initializeMap() {
  // Confirmamos que no se ha inicializado el mapa todavía
  if (mMap == null) 
  {
   // obtenemos el mapa de la vista
   mMap = ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map)).getMap();

   // Registramos o establecemos ésta clase, MapActivity, como LocationSource 
   // del mapa para utilizar nuestro locationManager y el Listener ;)
   mMap.setLocationSource(this);

   // Inicializamos el mapa...
   if (mMap != null) 
   {
    setUpMap();     
   }
  }
 }

 private void setUpMap() 
 {
  // Nos localiza...
  mMap.setMyLocationEnabled(true);
  // Quitamos el botón de "mi posición"
  mMap.getUiSettings().setMyLocationButtonEnabled(false);
  // Pinta una marca en La Meca
  addLaMecaMarkOnMap();
  centerMapOnLaMeca();
 }

 private void addLaMecaMarkOnMap(){
  mMap.addMarker(new MarkerOptions()
  .position(new LatLng(Constants.laMecaLatitude, Constants.laMecaLongitude))
  .title("La Meca")
  .icon(BitmapDescriptorFactory.fromResource(R.drawable.icono_meca)));
 }

 public void centerMapOnLaMeca(){
  CameraPosition newCameraPosition = new CameraPosition.Builder()
  .target(new LatLng(Constants.laMecaLatitude, Constants.laMecaLatitude))      
  .zoom(1)                   
  .bearing(0)        
  .tilt(0)                  
  .build();  

  mMap.animateCamera(CameraUpdateFactory.newCameraPosition(newCameraPosition));
 }

 private void initialiceSensors() {
  mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
  // Vamos a usar el acelerómetro y el sensor magnético
  Sensor gsensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
  Sensor msensor = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);

  mSensorManager.registerListener(this, gsensor, SensorManager.SENSOR_DELAY_NORMAL); 
  mSensorManager.registerListener(this, msensor, SensorManager.SENSOR_DELAY_NORMAL);
 } 

 private void drawLineBetweenOurLocationAndLaMeca(){
  // Debe ejecutarse cuando sepamos nuestra localización
  // es decir, onLocationChanged
  Location myLocation = mMap.getMyLocation();
  LatLng myLatLng = new LatLng(myLocation.getLatitude(), myLocation.getLongitude());
  LatLng laMecaLatLng = new LatLng(Constants.laMecaLatitude, Constants.laMecaLongitude);

  // creamos un polyline (linea poligonal) con dos puntos
  PolylineOptions rectOptions = new PolylineOptions()
  .add(myLatLng) // nuestra posición
  .add(laMecaLatLng); // la posición de la meca

  // pintamos esa línea de color verde
  rectOptions.color(Color.GREEN);

  // Y la añadimos al mapa
  mMap.addPolyline(rectOptions); 
 }

 @Override
 public void onPause()
 {
  if(locationManager != null)
  {
   locationManager.removeUpdates((LocationListener) this);
  }

  super.onPause();
 }

 @Override
 public void onResume()
 {
  super.onResume();

  initializeMap();

  if(locationManager != null)
  {
   mMap.setMyLocationEnabled(true);
  }
 }


 public void activate(OnLocationChangedListener listener) 
 {
  mListener = listener;
 }

 public void deactivate() 
 {
  mListener = null;
 }

 public void onLocationChanged(Location location) 
 {
  if (!haveLocation){
   initialiceSensors();
   haveLocation = true;
  }
  mMap.clear();
  if( mListener != null )
  {
   addLaMecaMarkOnMap();

   mListener.onLocationChanged( location );
   CameraPosition newCameraPosition = new CameraPosition.Builder()
   .target(new LatLng(location.getLatitude(), location.getLongitude()))      // centra el mapa en nuestra posición
   .zoom(19)   // establece el zoom
   .bearing(0)        
   .tilt(0)                  
   .build();  

   //Move the camera to the user's location once it's available!
   mMap.animateCamera(CameraUpdateFactory.newCameraPosition(newCameraPosition)); 
   drawLineBetweenOurLocationAndLaMeca();

  }
 }

 public void onProviderDisabled(String provider) 
 {
  // cuando se deshabilita un probeedor de localización... prefiero no hacer nada
  // Toast.makeText(this, "provider disabled", Toast.LENGTH_SHORT).show();
 }

 public void onProviderEnabled(String provider) 
 {
  // cuando se habilita un probeedor de localización... prefiero no hacer nada
  // Toast.makeText(this, "provider enabled", Toast.LENGTH_SHORT).show();
 }

 public void onStatusChanged(String provider, int status, Bundle extras) 
 {
  // Cuando cambia el estado de la localización... prefiero no hacer nada
  // Toast.makeText(this, "status changed", Toast.LENGTH_SHORT).show();
 }

 public void onAccuracyChanged(Sensor sensor, int accuracy) {
  // cuando cambia la precisión...
 }


 public void onSensorChanged(SensorEvent event) {
  // quiero dejar un margen de 3 segundos al principio
  // para la animación hasta el punto donde se encuentra el usuario...
  if (lastTimeCompassUpdated == 0){
   lastTimeCompassUpdated = System.currentTimeMillis()+3000;
   return;
  }
  else if (System.currentTimeMillis()>lastTimeCompassUpdated ){
   if (mMap!=null){
    int matrix_size = 16;
    float[] R = new float[matrix_size];
    float[] outR = new float[matrix_size];

    if (event.accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE)
     return;

    switch (event.sensor.getType()) {
    case Sensor.TYPE_MAGNETIC_FIELD:
     mGeomagnetic = event.values.clone();
     break;
    case Sensor.TYPE_ACCELEROMETER:
     mGravity = event.values.clone();
     break;
    }

    if (mGeomagnetic != null && mGravity != null) {
     if (SensorManager.getRotationMatrix(R, null, mGravity, mGeomagnetic)) {
      SensorManager.getOrientation(R, outR);
     }

     CameraPosition oldPosition = mMap.getCameraPosition();

     // Quiero que sólo se actualice si hay una variación de mas de un grado
     //if (Math.abs(Math.abs(oldPosition.bearing) - Math.abs(Math.toDegrees(outR[0])))>1){

     CameraPosition newCameraPosition = new CameraPosition.Builder()
     .target(oldPosition.target)      // Sets the center of the map to Mountain View
     .zoom(oldPosition.zoom)                   // Sets the zoom
     .bearing((float) Math.toDegrees(outR[0]))        
     .tilt(oldPosition.tilt)                 
     .build();                   // Creates a CameraPosition from the builder

     mMap.moveCamera(CameraUpdateFactory.newCameraPosition(newCameraPosition));
     
    }
   }
  }
 }
}
Si te fijas el código es muy lineal, hay una secuencia de acontecimientos. Se pinta el icono sobre la meca y se centra la cámara en ese punto y cuando el dispositivo da una localización (onLocationChanged) si es la primera localización se inicializan los sensores y se lanza una animación de la cámara hasta nuestra posición.

Durante mis pruebas y errores me he encontrado con que el giro (la animación o el movimiento) de la cámara de acuerdo a los sensores interrumpía las animaciones del mapa, es decir, el movimiento de la cámara de un punto a otro, si éste movimiento se hacía por código, es decir, si hacia un movimiento desde La Meca hasta mi localización y empezaban a funcionar los sensores (onSensorChanged) el ajuste de la orientación de la cámara en ese método paraba la animación así que decidí meter un pequeño retardo de tres segundos en el evento la primera vez e inicializar los sensores una vez tuviera una localización. Así he conseguido que, primero el mapa nos muestre La Meca y una vez sepamos donde estamos movernos "elegantemente" hasta nuestra posición y, tres segundos después, orientar nuestro mapa de acuerdo a la brújula para saber en qué dirección se encuentra exáctamente La Meca. 

Hay que tener en cuenta también que si no hay ningún proveedor de localización éste código fallará y se cerrará la aplicación, por tanto lo que debemos hacer es obligar a que haya un proveedor de localización activo antes de poder lanzar ésta actividad. Para ello, en EntryPointActivity, tengo el siguiente código:
import android.location.LocationManager;
import android.os.Bundle;
import android.widget.Button;

public class EntryPointActivity extends BaseActivity {
 /** Called when the activity is first created. */
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
 }
 
 @Override
 protected void onResume() {
  setMapButtonVisibility();
  super.onResume();
 }

 private void setMapButtonVisibility() {
  LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);

  boolean network_enabled=locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
  boolean gps_enabled=locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
  Button mapButton = ((Button)this.findViewById(R.id.ep_map_button)); 
  if (!(network_enabled || gps_enabled)){
   
   mapButton.setEnabled(false);
  }
  else {
   mapButton.setEnabled(true);
  }
 }
}
Lo último que nos queda por hacer antes de poder firmar y subir a Google Play es pulir detalles de interfaz de usuario y de usabilidad... vamos con ello:

Interfaz de Usuario

Que no se crea nadie que me he complicado demasiado, he buscado un mensaje que invita a la oración, lo he traducido al árabe y he incluido los dos textos, he buscado alguna imagen bonita de la Kaaba y la he centrado y colocado como background del layout principal, también he centrado los botones y buscado una imagen pequeña de la Kaaba para utilizarla como icono de la aplicación. Así ha quedado el xml (main.xml) de EntryPoint:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@drawable/centered"
    android:orientation="vertical" >

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        android:text="@string/intro_arabe" />

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_margin="5dp"
        android:text="@string/intro" />

    <RelativeLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_gravity="bottom|center_horizontal"
        android:layout_marginBottom="1dip"
        android:orientation="vertical" >

        <Button
            android:id="@+id/ep_gps_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            android:layout_marginBottom="25dp"
            android:onClick="onClickFeature"
            android:text="@string/lanzar_configuracion_gps" />

        <Button
            android:id="@+id/ep_map_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_above="@+id/ep_gps_button"
            android:layout_centerHorizontal="true"
            android:layout_marginBottom="14dp"
            android:onClick="onClickFeature"
            android:text="@string/lanzar_mapa" />

     </RelativeLayout>

</LinearLayout>
Respecto al centrado de la imagen, he creado un fichero en /res/drawable llamado centered.xml así:
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
    android:gravity="center"
    android:src="@drawable/Kaaba_mirror_edit_jj" />

Esto hace que la imagen elegida quede centrada y no se deforme, ha quedado así a la primera y me ha gustado aunque quizá en móviles con la pantalla un poco más pequeña no quede del todo bien, ya me plantearé ese ajuste más adelante, de momento doy por terminado "el maquillaje" y voy a firmar la aplicación y a subirla a Google Play.

Para firmar la aplicación: Botón derecho sobre el proyecto -> Android Tools -> Export Signed Application Package... Pones tus credenciales, creas el apk, bas a https://play.google.com/apps/publish  y subes tu aplicación...

(siento haber resumido ésto tanto pero da para varias entradas y ya se ha escrito mucho sobre firmar aplicaciones y gestionar tus aplicaciones en Google Play, en cuanto esté activa pondré un enlace para que veáis el resultado)


Y como hoy es Noche Buena voy a ducharme y a vestirme para ir a cenar con mi familia, espero que éste artículo sirva como introducción a la programación de aplicaciones con Google Maps. En el próximo artículo me gustaría hacer una nueva versión de ésta aplicación donde aparezcan laas mezquitas más cercanas, tendré que buscar si existe esa información en algún sitio y hacer un poco de I+D, y eso será dentro de unos días...

¡Hasta la próxima entrada!

Juan García Carmona
d.jgc.it@gmail.com