jueves, 25 de abril de 2013

Android: Creando nuestra aplicación push I: montando el servidor


En el post anterior aprendimos a montar el ejemplo de Google sobre las notificaciones push y su Google Cloud Messsaging. Ahora será nuestro turno el de crear nuestra propia aplicación que envíe y reciba notificaciones. Como va a ser una tarea ardua, dividiremos el capítulo en al menos dos partes: cliente (Android) y servidor (servicios web Java).

Objetivo de la aplicación

Crearemos una aplicación simple que reciba y envíe mensajes push a un usuario registrado. Cuando se abre la aplicación por primera vez, esta registrará su número de teléfono, nombre e id de dispositivo en el servidor. Cuando esté registrado, mostrará la lista de usuarios disponibles y permitirá escribirles un mensaje a los usuarios registrados. En resumen: un cutre-güasap (así será como la llamaremos).

Para los que entendáis BPM, esto será lo que haremos:


Requisitos para este capítulo



Manos al teclado

Abrimos nuestro fantástico y maravilloso NetBeans y creamos nuestro proyecto web al que llamaramos cutreguasap-server, indicando que nuestro servidor será Tomcat.


Para que sea más cuco, cambiaremos los mensajes de la pantalla de inicio con algo más descriptivo:

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Cutre-Güasap Server</title>
    </head>
    <body>
        <h1>Cutre-Güasap Server is running</h1>
    </body>
</html>

A continuación crearemos un servicio web llamado CutreGuasapWS en el paquete com.example.cutreguasap.


A este WebService le añadiremos dos métodos:
  • Método registro: Recibirá el id de servicio, número de teléfono y el nombre de usuario. Como resultado devolverá un texto.




  • Método enviarMensaje: Recibirá el usuario y el texto del mensaje. Como resultado devolverá un texto.



Tenemos por tanto el siguiente código:

@WebService(serviceName = "CutreGuasapWS")
public class CutreGuasapWS {

    /**
     * Web service operation
     */
    @WebMethod(operationName = "registro")
    public String registro(@WebParam(name = "id") String id, @WebParam(name = "telefono") String telefono, @WebParam(name = "usuario") String usuario) {
        //TODO write your implementation code here:
        return null;
    }

    /**
     * Web service operation
     */
    @WebMethod(operationName = "enviarMensaje")
    public String enviarMensaje(@WebParam(name = "usuario") String usuario, @WebParam(name = "mensaje") String mensaje) {
        //TODO write your implementation code here:
        return null;
    }
}

Ahora necesitaremos importar las librerías necesarias para usar GCM. Para esto, copiamos el directorio lib que encontraremos en el directorio del sdk extras\google\gcm\samples\gcm-demo-server\WebContent\WEB-INF. Dicho directorio lib lo colocaremos en WEB-INF.


Y en las propiedades del proyecto, añadimos estas dos librerías: gcm-server.jar y json_simple-1.1.jar.


Creando el almacenamiento y los objetos

Antes de implementar el servicio, definiremos un objeto que será el que identifique a los usuarios y a sus números de teléfono. Algo muy simple: usuario, teléfono e id de dispositivo. Con su constructor y sus getters y setters.

public class Usuario {
    private String usuario;
    private String telefono;
    private String dispositivo;

    public Usuario(String usuario, String telefono, String dispositivo) {
        this.usuario = usuario;
        this.telefono = telefono;
        this.dispositivo = dispositivo;
    }

    public String getUsuario() {
        return usuario;
    }

    public void setUsuario(String usuario) {
        this.usuario = usuario;
    }

    public String getTelefono() {
        return telefono;
    }

    public void setTelefono(String telefono) {
        this.telefono = telefono;
    }

    public String getDispositivo() {
        return dispositivo;
    }

    public void setDispositivo(String dispositivo) {
        this.dispositivo = dispositivo;
    }

    @Override
    public int hashCode() {
        return usuario.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        Usuario usu2 = (Usuario)obj;
        boolean res = usuario.equalsIgnoreCase(usu2.getUsuario());
        return res;
    }

}


Si os fijais, hemos sobreescrito la función hashCode para que el identificador hash de los usuarios sea el mismo que el de la propiedad usuario, y con la función equals hemos hecho lo mismo.

Como almacenamiento de usuarios, crearemos una clase final con métodos y propiedades estáticas objetivo que solo haya una única instancia de ella (realmente no es instancia, pero para que me entendáis). La llamaremos Datastore y contendrá el listando de usuarios y los métodos necesarios para registrar, borrar, y demás operaciones sobre el listado.

public final class Datastore {
   
    private static final List<Usuario> usuarios = new ArrayList<Usuario>();

  private Datastore() {
    throw new UnsupportedOperationException();
  }

  public static boolean registrar(Usuario usr) {
    boolean res = false;
    if (!usuarios.contains(usr))
        res = usuarios.add(usr);
     return res;
  }
 
  public static boolean borrar(Usuario usr) {
    return usuarios.remove(usr);
  }

  public static boolean actualizar(Usuario usr) {
      boolean res = usuarios.remove(usr);
      if (res)
        usuarios.add(usr);
      return res;
  }

  public static List<String> getUsuariosRegistrados() {
      List<String> lista = new ArrayList<String>();
      for (Usuario u : usuarios){
          lista.add(u.getUsuario());
      }
      return lista;
  }
 
  public static int size(){
        return usuarios.size();
  }
 
  public static String getIdDispositivo (String usuario){
      String res = "";
      Integer idx = usuarios.indexOf(new Usuario(usuario, usuario, usuario));
      if (idx>=0)
          res = usuarios.get(idx).getDispositivo();
     
      return res;
  }
}

Veamos las funciones y propiedades que tenemos en Datasource en detalle:
  • static final List<Usuario> usuarios: declaramos una propiedad estática que será propia de la clase (compartida por todas las instancias y accesible sin instanciar). Esta lista contendrá los usuarios registrados según la implementación de la clase Usuario.
  • static boolean registrar(Usuario): función que nos devuelve un booleano indicando si ha podido registrar el usuario que recibe como parámetro.
  • static boolean borrar(Usuario): función que nos devuelve un booleano indicando si ha podido borrar al usuario que recibe como parámetro.
  • static boolean actualizar(Usuario): función que nos devuelve un booleano indicando si ha podido actualizar los datos de registro del usuario que recibe como parámetro.
  • static int size(): función que nos devuelve el número de usuarios registrados en el servidor.
  • static List<String> getUsuariosRegistrados(): función que nos devuelve un listado con los nombres de los usuarios registrados.
  • static String getIdDispositivo (String): función que nos devuelve (si lo encuentra) el ID de dispositivo asociado al usuario que recibe como parámetro.



Codificando los servicios

Una vez tenemos las librerías, clases y el WebService creado, vamos a implementarlo. Lo primero a hacer es definir dos propiedades que usaremos durante la codifificación: el sender (el encargado de enviar el mensaje) y el apiKey (el api key server que definimos en la web de GCM).

En el constructor inicializaremos dichas propiedades y un usuario de prueba con datos de mi dispositivo para poder testear el servicio con la aplicación del ejemplo que vimos en el post anterior (más adelante explico cómo probarlo).

@WebService(serviceName = "CutreGuasapWS")
public class CutreGuasapWS {

    private Sender sender;
    private String key;
   
    public CutreGuasapWS() {
        super();
        key = "AIzaSyDJ_NY9CC_hy8e-QxgcFCu6A5XbqBj2Eu0";
        sender = new Sender(key);
       
        //si hay cero registros creamos uno de pruebas
        if (Datastore.size()==0){
            Usuario demo = new Usuario("josec", "600111222", "APA91bGiNpwbVc6s31f5kLFEPhkTYGjseMFD8t719vaBO8E9AIqqZt0Ic_lgAhqut6yetz6nACVFa0aJvPhceLA9BuWTmDw-5ilXWsb6qlcrOvME_6_Yn225wN0dzGHuDHvPPqaYdV2zPt2XLdbJPAUH2CW_ZUCBJQ");
            Datastore.registrar(demo);
        }
    }

(...)

}

Ahora queda lo duro: las dos operaciones de los servicios web.

Codificando la operación registro

Esta función será bastante simple: recibiremos los datos de registro y crearemos una instancia de la clase Usuario. Con esto usaremos la función registrar del Datasourse e informaremos de si ha podido o no dar de alta al servicio.

@WebMethod(operationName = "registro")
public String registro(@WebParam(name = "id") String id, @WebParam(name = "telefono") String telefono, @WebParam(name = "usuario") String usuario) {
    String res = "";
    //creamos el usuario
    Usuario user = new Usuario(usuario, telefono, id);
        if (Datastore.registrar(user)) {
            res = "usuario registrado: " + Datastore.size();
        } else {
            res = " Fallo en el registro: " + Datastore.size();
        }
    return res;
}

NOTA: Si miramos el BPM, cuando nos registramos el servidor nos devuelve el listado de usuarios. Tened presente esto ya que esta función tendremos que cambiarla en un futuro para que devuelva el listado. Por ahora la dejaremos así para simplificar las pruebas.


Codificando la operación enviarMensaje

En esta función comprobaremos si nuestro Datastore tiene registros y si no tiene, simplemente mostraremos un mensaje indicando que no hay usuarios registrados.
Si hay dispositivos registrados, comprobaremos si nuestro usuario está en ese listado. Si existe, obtendremos su id de dispositivo, generaremos el mensaje (adjuntando el texto con addExtra) y lo enviaremos. Al final, la operación nos devolverá un texto informando de cómo ha ido la ejecución.

@WebMethod(operationName = "enviarMensaje")
public String enviarMensaje(@WebParam(name = "usuario") String usuario, @WebParam(name = "mensaje") String mensaje) throws IOException {
    String res;
    if (Datastore.size()==0) {
        res = "No hay usuarios registrados";
    } else {
        //obtenemos el dispositivo
        String dispositivoId = Datastore.getIdDispositivo(usuario);
        if (dispositivoId=="") {
            res = "El usuario " + usuario + " no está registrado";
        } else {
            Message.Builder builder = new Message.Builder();
            builder.addData("datos", mensaje);
            Message message = builder.build();
            Result result = sender.send(message, dispositivoId, 5);
            res = "Enviado mensaje '" + mensaje + "' al usuario '" + usuario + "': " + result;
                                                                }
        }
    return res;
}

Ya estamos en condiciones de desplegar nuestro servicio y ver en el explorador nuestro fantástico texto:


Para probar nuestro servicio, tan simple como abrir SoapUI e invocar a nuestro servicio web http://localhost:8080/cutreguasap-server/CutreGuasapWS?WSDL

Probando el servicio de registro

Ejemplo de invocación al método registro:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cut="http://cutreguasap.example.com/">
   <soapenv:Header/>
   <soapenv:Body>
      <cut:registro>
         <!--Optional:-->
         <id>123id</id>
         <!--Optional:-->
         <telefono>123tf</telefono>
         <!--Optional:-->
         <usuario>123us</usuario>
      </cut:registro>
   </soapenv:Body>
</soapenv:Envelope>

Ejemplo de resultado del método registro:

<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
   <S:Body>
      <ns2:registroResponse xmlns:ns2="http://cutreguasap.example.com/">
         <return>usuario registrado: 2</return>
      </ns2:registroResponse>
   </S:Body>
</S:Envelope>

Probando el servicio de enviar mensaje

Ejemplo de invocación al método enviarMensaje:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cut="http://cutreguasap.example.com/">
   <soapenv:Header/>
   <soapenv:Body>
      <cut:enviarMensaje>
         <usuario>josec</usuario>
         <mensaje>prueba</mensaje>
      </cut:enviarMensaje>
   </soapenv:Body>
</soapenv:Envelope>

Como podéis ver, he usado el usuario josec que había creado por defecto en el constructor.

Ejemplo de resultado de la invocación al método enviarRegistro:

<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
   <S:Body>
      <ns2:enviarMensajeResponse xmlns:ns2="http://cutreguasap.example.com/">
         <return>Enviado mensaje 'prueba' al usuario 'josec': [ messageId=0:1366885310279456%921c249af9fd7ecd ]</return>
      </ns2:enviarMensajeResponse>
   </S:Body>
</S:Envelope>



El código lo puedes descargar aquí.

En el siguiente post le meteremos mano a la aplicación Android.