Proyecto final de curso Android: Programación de aplicaciones (edición online, febrero 2011) Nombre aplicación: AvisoSMS - “detección de cambio brusco avisando con mensajes de un posible accidente” Autor: Jose Parra Salort Licencia: Autorizo la difusión del código fuente con fines educativos siempre que se haga referencia al autor bajo los términos generales de la licencia “Academic Free License v.3.0”. Motivación Este proyecto pretende servir de utilidad a aquellas personas que por la realización de ciertas actividades como ciclismo, senderismo, escaladores, albañiles que trabajen en fachadas, etcétera, puedan estar expuestas a caídas, así como también a personas ancianas que necesiten un cuidado más atento. El funcionamiento de la aplicación desarrollada se basa en el uso del sensor “acelerómetro”, en el de mecanismos localización (por red Wi-fi, telefónica móvil, o por GPS), en el de servicios y notificaciones Android, y en el envío de SMS y de e-mails usando el protocolo SMTP. Funcionalidad La aplicación presenta las siguientes funcionalidades: La aplicación se ejecutará como un servicio en segundo plano, aunque tendrá una simple ventana desde donde se arrancará o detendrá el servicio comentado, y desde la que se accederá a la configuración. Ver gráfico de ventanas en el siguiente apartado (nº 2). Además al encender el teléfono, se arrancará el servicio, apareciendo la notificación correspondiente en la barra de notificaciones. Una vez en marcha el servicio, cuando el sensor “acelerómetro” detecte un cambio brusco, el cual representará una caída repentina del dispositivo móvil, se localizará la posición del dispositivo con el proveedor más adecuado en función de donde se encuentre aquel (dentro de un edificio mediante wi-fi, o en el exterior mediante GPS, o con la red telefónica móvil). Una vez establecidas las coordenadas de la posición, se enviará un mensaje de alerta bien por SMS o bien por e-mail, pero no ambos, al número de teléfono o dirección electrónica respectivamente, indicadas en la configuración. En el caso de envío por e-mail, la aplicación cargará un mapa de Google ubicando la posición del dispositivo, hará una captura de pantalla y la enviará en el e-mail. Cuando el mensaje sea enviado por cualquiera de los dos modos, se actualizará la notificación en la barra de notificaciones y se reproducirá un sonido: el de una campana (fichero campana.mp3 que debe de existir en la carpeta /sdcard/avisos). A partir de este momento no se volverá a considerar cualquier otro cambio brusco, pues el servicio está implementado de forma que solo se detecte un cambio brusco. Si se desea que la aplicación continúe monitorizando, hay que detener el servicio y volverlo a arrancar. La aplicación en lo que se refiere a los textos de la interfaz gráfica como a los distintos mensajes de información, está preparada tanto para el idioma español como para inglés. Parámetros que se pueden configurar en las preferencias de la aplicación. Mostrar el mensaje de bienvenida: pues al principio es recomendable que la primera vez que se ejecute se informe al usuario de que es importante que configure los parámetros. Tipo de mensaje: SMS o e-mail (SMTP). Dirección origen (para el caso de e-mail): indica la dirección desde la que se manda el e-mail de alerta. NOTA: debe ser una cuenta de GMail. Password de la dirección origen (para el caso de e-mail): es el password de la dirección anterior. Es necesario incluirlo pues de lo contrario, no se podrá enviar el e-mail de alerta. Dirección destino (para el caso de e-mail): indica la dirección a la que se manda el e-mail, en este caso no es necesario que sea de GMail. Asunto (para el caso de e-mail): permite especificar el asunto del e-mail de alerta. Número de teléfono (para el caso de SMS): permite especificar el nº de teléfono de la persona a la que se envía el SMS de alerta. Texto del mensaje (tanto para SMS como e-mail): permite especificar el texto que se enviará ya sea mediante SMS o mediante e-mail. A dicho texto se le añadirán a continuación las coordenadas de la ubicación del dispositivo en el momento del cambio brusco. o Es recomendable que si se piensa utilizar en el modo SMS, el texto sea más corto de 65 caracteres ya que la aplicación le concatena las coordenadas, con lo cual se evitaría que sobrepase los 160 caracteres, en cuyo caso se envía más de un SMS. A destacar Estilos aplicados tanto al layout principal como al de preferencias. Servicios y notificaciones. Receptores de anuncios: en particular arranque desde la carga del sistema operativo. Uso de sensores: acelerómetro. Envío de SMS y comprobación del envío. Proveedores de localización. Google Maps. Además se ha utilizado la clase OverLay para pintar un marcador la posición del dispositivo. Animaciones Tween: se ha usado una animación como truco para que no aparezca la pantalla en negro mientras se carga el mapa. Envío de e-mail con SMTP. En primer lugar se probó a implementar el mecanismo de envío proporcionado por Android mediante las intenciones predeterminadas de envío. Pero como dicho mecanismo requería la interacción del usuario (lo cual resulta incompatible con el objetivo de la aplicación, pues el envío debe ser totalmente automático), se optó por el envío del correo electrónico utilizando unas librerías encontradas en Internet (http://mobiledevtuts.com/android/androi d-sdk-smtp-email-tutorial/). Dichas librerías permiten el envío de correos electrónicos usando SMTP. Además en el proyecto Android se ha incluido una clase envoltorio (Mail.java) de dichas librerías para facilitar su uso desde el servicio de monitorización (ServicioAvisos). Imagen ejemplo enviada en el e-mail. Esquema de pantallas A continuación se explica en un gráfico, la interacción con la aplicación y los posibles estados de ésta: 2 1 3 4 ¿Cómo se ha implementado? Lo más básico. Existe una actividad principal (AvisosSMS.java) que es la que maneja el layout principal (Main.xml) de acceso a la configuración y a los botones de arrancar y detener. Pero las clases java más importantes son: 1. La clase ServicioAvisos.java Representa un servicio que ejecutándose en segundo plano se encarga de detectar el cambio brusco. Por lo tanto hereda de Service e implementa la interfaz SensorEventListener para el manejo de eventos de sensores, en este caso para el sensor acelerómetro. Es por tanto el manejador onSensorChanged(SensorEvent event), el centro del servicio, pues es donde se detecta si ha habido un cambio brusco. A continuación se muestra su código, en el que destaca la utilización de un filtro paso-alto para eliminar el efecto de la gravedad en el sensor. Para detectar cambios bruscos, se han tenido en cuenta dos parámetros que se han ajustado haciendo pruebas con el dispositivo móvil. El primero de ellos es factorDeAjuste, que se usa en el filtro paso-alto y que se ha fijado en 0.3, y el segundo es topeCambioBrusco, que se usa en el filtro paso-alto y que se ha fijado en 0.8. Estos valores se han obtenido probando con un teléfono móvil Samsung Galaxy S 2. @Override public void onSensorChanged(SensorEvent event) { synchronized (this) { if (EJECUTADO) return; // El acelarometro servirá para detectar el movimiento brusco. if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { // Recoger posiciones x, y, z. float x = event.values[0]; float y = event.values[1]; float z = event.values[2]; // Implementación de un filtro Paso-Alto para eliminar // el efecto de la gravedad, que está presente aun cuando // el dispositivo está quieto sobre la mesa. float lastX lastY lastZ xPA = yPA = zPA = xPA, yPA, zPA; // x, = x * factorDeAjuste = y * factorDeAjuste = z * factorDeAjuste Math.abs(x - lastX); Math.abs(y - lastY); Math.abs(z - lastZ); y, z después del filtro. + lastX * (1.0f - factorDeAjuste); + lastY * (1.0f - factorDeAjuste); + lastZ * (1.0f - factorDeAjuste); if (xPA > topeCambioBrusco || yPA > topeCambioBrusco || zPA > topeCambioBrusco) { EJECUTADO = true; // Busca la ubicación del Dispositivo. AvisosSMS.UBICACION = DevolverPosicion(); if (AvisosSMS.UBICACION == null) { Toast.makeText(this, getText(R.string.infoPosicionNoEncontrada), Toast.LENGTH_SHORT).show(); } Integer tipoDeEnvio = Integer.parseInt(PREF_desdeServicio .getString("tipoMensaje", "0")); // Abrir un layout u otro según el tipo de envio. switch (tipoDeEnvio) { case 0: Intent intencionSMS = new Intent(this, SMS.class); intencionSMS.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); this.startActivity(intencionSMS); break; case 1: Intent intencionEMailMapa = new Intent(this, EMailMapa.class); intencionEMailMapa .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); this.startActivity(intencionEMailMapa); break; } } } } } El resto de métodos de ServicioAvisos son: para obtener la posición del dispositivo y para el manejo de las notificaciones de la barra de notificaciones. 2. La clase SMS.java Es una actividad que gestiona el envío de SMS, se lanza desde el evento ‘ onSensorChanged’ del servicio anterior. Implementa la interfaz EnvioDeAyuda la cual define los métodos enviar y notificar. En el envío del SMS utiliza la clase SmsManager y también usa un receptor broadcast para la acción “ACTION_SMS_SENT”, en la que se comprueba si el mensaje ha sido enviado o ha ocurrido algún error. 3. La clase EMailMapa.java Es una actividad que gestiona el envío de e-mail, se lanza desde el evento ‘onSensorChanged’ del servicio ServicioAvisos. Implementa la interfaz ‘EnvioDeAyuda’ la cual define los métodos enviar y notificar. En este caso para el envío del e-mail, antes se debe de obtener una imagen con la posición en un mapa Google Maps. Para ello abre un layout con un mapa, se captura la pantalla, se guarda la captura en un fichero jpeg y finalmente construye un e-mail donde adjunta dicho fichero. A continuación se muestra el código que realiza dicha captura de pantalla y el almacenamiento en un fichero jpeg. public void CapturaPantallaMapa() { Bitmap imagenCapturada = null; View vistaCapturada = null; try { vistaCapturada = findViewById(R.id.mapa); vistaCapturada.setDrawingCacheEnabled(true); imagenCapturada = Bitmap.createBitmap(vistaCapturada.getWidth(), vistaCapturada.getHeight(), Bitmap.Config.ARGB_8888); imagenCapturada = vistaCapturada.getDrawingCache(); } catch (Exception e) { // Log.i("PANTALLA", "Exc_msg: " + e.getMessage()); // Log.i("PANTALLA", "Exc: " + e.toString()); } try { // Comprobando que el almacenamiento // externo esté montado y disponible. String estadoSD = Environment.getExternalStorageState(); File sd = Environment.getExternalStorageDirectory(); if (!estadoSD.equals(Environment.MEDIA_MOUNTED) && !estadoSD.equals(Environment.MEDIA_MOUNTED_READ_ONLY)) { Toast.makeText(this, "It's not possible to access \' " + FICHERO + "\'", Toast.LENGTH_LONG).show(); } // El archivo que contendrá la captura. File f = new File(sd, FICHERO); if (sd.canWrite()) { f.createNewFile(); OutputStream os = new FileOutputStream(f); imagenCapturada.compress(Bitmap.CompressFormat.JPEG, 80, os); os.close(); } } catch (FileNotFoundException e) { // Log.i("INFOAVISOS", "traza FileNotFoundException"); } catch (IOException e) { // Log.i("INFOAVISOS", "traza IOException"); } vistaCapturada.setDrawingCacheEnabled(false); } Además para asegurarse de que el mapa está completamente cargado (lo cual depende de la conexión a Internet) se ha hecho uso del evento ‘onWindowFocusChanged’ para detectar un cambio de foco en la pantalla y también de una animación Tween (hacer aparecer el mapa en 5 segundos de forma progresiva). @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); // Se utiliza este evento, porque en este momento // el mapa ya está pintado y se puede capturar la pantalla. if (!CapturaREALIZADA) { CapturaPantallaMapa(); // Enviar el mensaje. enviar(); // Cierra la actividad EMailMapa.this.finish(); CapturaREALIZADA = true; } } El siguiente código, en el método onCreate de la actividad, ejecuta la animación y cuando termina oculta el reloj. // Animar el mapa. Animación 'Alpha' -> se hace visible en 5 segundos. Animation animacion = AnimationUtils.loadAnimation(this, R.anim.tweenanimation); mapView.startAnimation(animacion); animacion.setAnimationListener(new AnimationListener() { @Override public void onAnimationEnd(Animation animation) { // Al terminar la animación oculto el reloj de espera. progressDialog.dismiss(); textViewAux.setVisibility(View.VISIBLE); } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationStart(Animation animation) { } }); También se ha hecho uso de un ProgressDialog para mostrar un reloj de sistema ocupado, mientras se carga el mapa. Una vez cargado el mapa se esconde el reloj y se hace la captura. 4. La clase Mail.java Es una clase envoltorio de las librerías de envío de SMTP. Destacan los métodos send, addAttachment y los constructores. Por simplificar, se ha configurado en esta clase que el servidor de envío sea el de GMail (smtp.gmail.com). Las otras clases del proyecto son meramente auxiliares, una para las preferencias de configuración y otra (ReceptorArranque.java) para que el servicio sea arrancado cuando se termina la carga del sistema operativo. Permisos solicitados por la aplicación (en el manifiesto): - Para permitir la localización del dispositivo: <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> - Para permitir el envío de SMSs: <uses-permission android:name="android.permission.SEND_SMS" /> - Para permitir el acceso a Internet: <uses-permission android:name="android.permission.INTERNET" /> - Para permitir guardar ficheros en el almacenamiento externo: guarda el jpeg que tiene que adjuntar. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> - Para permitir la notificación de que se ha cargado el Sistema Operativo. <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> Observaciones Para probar la aplicación se puede lanzar el móvil sobre una superficie banda (ejemplo un sofá o la cama) o bien agitarlo fuertemente con el brazo, es decir, dando una buena sacudida con el brazo o golpe seco.