miércoles, 8 de mayo de 2013

Android: Creando nuestra aplicación push II: registrando el cliente


Partimos de la base del post anterior donde dejamos listo nuestro servidor cutregüasap-server funcionando y probado con la aplicación de ejemplo GCM de Google.

Ahora vamos a montar nuestra aplicación cliente, que serán 3 simples pantallas:
  • Pantalla Carga (activity_loading): esta pantalla será la principal y se mostrará nada más abrir la aplicación. Su objetivo es registrar el dispositivo, y cuando este ya se haya registrado pasará a la siguiente.
  • Pantalla de Usuarios (activity_users): Pantalla principal que muestra todos los usuarios registrados. Clicando en ellos se accederá a la conversación de cada uno.
  • Pantalla de conversación (activity_conversation): Aquí mostraremos los textos que se envían y reciben de cada usuario.




Creando el proyecto


Crearemos nuestro proyecto como de constumbre y lo llamaremos CutreGuasap. Muy importante quedarnos con el nombre del paquete donde meteremos todas nuestras clases: com.example.cutreguasap.


Nuestra primera actividad será LoadingActivity.



Tras esto, crearemos los dos layouts y sus dos actividades ConversationActivity y UserActivity y lo añadiremos al manifest.


Agregando la librería

Copiaremos del directorio libs del ejemplo de google (Demo Activity) la librería gcm.jar y la añadiremos a nuestro directorio libs.



Tras esto, en eclipse refrescaremos y veremos como la tenemos agregada.




Entendiendo nuestras clases

Por ahora, necesitaremos una serie de clases para manejar todo:
  • CommonUtilities: clase estática que simplemente define una serie de propiedades y métodos globales que necesitaremos en cualquier lugar.

  • Datastore: clase estática que almacena la información para poder usarla en cualquier sitio de nuestra aplicación. Por ejemplo: los datos del usuario registrado, los usuarios con los que podemos hablar o las conversaciones que tenemos abiertas.

  • GCMIntentService: Controlador de eventos de GCM. Implementa la clase GCMIntentService que nos indica los métodos que deberemos implementar para controlar los eventos que se producen desde GCM.

  • AsyncRegistro: Clase asíncrona que usaremos para la invocación al webservice de registro de nuestro cutreguasap-server.

  • AsyncEnviarMensaje: Clase asíncrona que usaremos para la invocación al webservice de envío de mensajes a nuestro cutreguasap-server.

  • Usuario: entidad que define a los usuarios registrados en el servidor (nombre, teléfono y conversaciones que hemos mantenido con ellos) y que básicamente maneja la clase Datastore.


Manos al código: clase CommonUtilities

En esta clase definiremos parámetros y funciones estáticos que nos serán de utilidad en toda la aplicación. Un ejemplo serán las URLs de nuestros servicios web:

public final class CommonUtilities {

    static final String SERVER_URL_REG = "http://10.114.201.59:8080/cutreguasap-server/CutreGuasapWS";
    static final String SERVER_URL_CONV = "http://10.114.201.59:8080/cutreguasap-server/CutreGuasapWS";

El identificador de nuestra aplicación en GCM:

static final String SENDER_ID = "302060114739";

Que lo obtenemos de la web de Google API:



Definiremos también los eventos o Intents que lanzaremos desde diferentes puntos de la aplicación para que sean capturados por las Activities oportunas. En el caso del registro definimos dos:

  • Registro GCM finalizado: GCM_FINISH_REGISTRY_ACTION
  • Registro en el Servidor finalizado: SERVER_FINISH_REGISTRY_ACTION

  • El evento DISPLAY_MESSAGE_ACTION lo usaremos para la recepción de un mensaje de GCM pero eso lo veremos más adelante.
    static final String DISPLAY_MESSAGE_ACTION =
            "com.example.cutreguasap.DISPLAY_MESSAGE";
   
    static final String GCM_FINISH_REGISTRY_ACTION =
            "com.example.cutreguasap.GCM_FINISH_REGISTRY";

    static final String SERVER_FINISH_REGISTRY_ACTION =
            "com.example.cutreguasap.SERVER_FINISH_REGISTRY";

MUY IMPORTANTE: fijaros bien que hemos usado en el string el nombre de nuestro paquete.
  


Definiremos también una propiedad para el envío de información en los intentos, aunque no es realmente necesario, pero haremos caso a las buenas prácticas que el ejemplo de Google nos indica.

static final String EXTRA_MESSAGE = "message";

Y finalmente, definimos los 3 métodos estáticos que lanzan los eventos que hemos definido con anterioridad: dos para el registro y uno para los mensajes recibidos por GCM.

    //evento para mostrar un mensaje
    static void displayMessage(Context context, String message) {
        Intent intent = new Intent(DISPLAY_MESSAGE_ACTION);
        intent.putExtra(EXTRA_MESSAGE, message);
        context.sendBroadcast(intent);
    }
   
    //evento para finalizar registro GCM
    static void finishRegistrationGCM(Context context, String registrationId){
       Intent intent = new Intent(GCM_FINISH_REGISTRY_ACTION);
        intent.putExtra(EXTRA_MESSAGE, registrationId);
        context.sendBroadcast(intent);
    }
   
  //evento para finalizar registro Servidor
    static void finishRegistrationServer(Context context, String registrationId){
       Intent intent = new Intent(SERVER_FINISH_REGISTRY_ACTION);
        intent.putExtra(EXTRA_MESSAGE, registrationId);
        context.sendBroadcast(intent);
    }

Si os fijáis, cuando se lanza el intento se hace haciendo uso de la función sendBroadcast, de forma que lo recibirán todos los objetos y solo aquellos que tengan definido un filtro de eventos lo capturarán y lo manejaran. Esto lo veremos cuando definamos la pantalla Loading.

Entidad Usuario

Definiremos una clase que será la que defina a los objetos usuarios y sus métodos. Es tan simple que solo comentaré que para cada usuario almacenaremos su nombre, teléfono y un listado de los mensajes que nos hemos intercambiado con él (conversación). Desde el punto de vista de los métodos, sólo crearemos aquellos necesarios para trabajar con las conversaciones y los método equals y hashCode, que se basará en el teléfono como identificador único del usuario.

public class Usuario {
      
       private String nombre;
       private String telefono;
       private List<String> conversacion;
      
       public Usuario(String nombre)
       {
             this.nombre = nombre;
             this.conversacion = new ArrayList<String>();
       }
      
       public Usuario(Node nodo)
       {
             String str = nodo.getTextContent();
             int idx = str.indexOf("|");
             nombre = str.substring(0,idx);
             telefono = str.substring(idx+1);
             this.conversacion = new ArrayList<String>();
       }
      
       public String addTexto(String autor, String texto)
       {
             String str = autor + ": " + texto;
             conversacion.add(str);
            
             return str;
       }
      
       public String getNombre()
       {
             return nombre;
       }
      
       public String getTelefono()
       {
             return telefono;
       }
      
       public String getConversacion()
       {
             String res = "";
             for (String str : conversacion)
                    res += "\n" + str;
             return res;
       }
      
       public void deleteConversacion(){
             this.conversacion = new ArrayList<String>();
       }
      
       @Override
       public int hashCode() {
             return telefono.hashCode();
       }
      
       @Override
       public boolean equals(Object o) {
             Usuario u2 = (Usuario)o;
             boolean res = telefono.equalsIgnoreCase(u2.getTelefono());
             return res;
       }
      
       @Override
       public String toString() {
             return nombre;
       }
}

Datastore

Seré aún más breve: usaremos la clase Datastore como almacén de información. En ella definimos la información del usuario de nuestro dispositivo (nombre, teléfono e Id de registro en GCM) y el listado de usuarios con los que podemos interactuar. Como métodos, define los necesarios para trabajar con este almacén de datos.

public final class Datastore {
      
       //propiedades de nuestro usuario (del dispositivo)
       private static String nombre = ".";
       private static String telefono = ".";
       private static String regId = ".";
      
       //listado de usuarios con los que podemos interactuar
       private static final List<Usuario> usuarios = new ArrayList<Usuario>();
      
       //es estática, no queremos que se instancie nunca
       private Datastore() {
           throw new UnsupportedOperationException();
         }

      
       //añadir usuario al listado
       public static boolean add(Usuario usr) {
             boolean res = false;
           if (!usuarios.contains(usr))
               res = usuarios.add(usr);
            return res;
       }
      
       //eliminar usuario de la lista
       public static boolean remove(Usuario usr) {
             return usuarios.remove(usr);
       }
      
       //limpiar la lista
       public static void removeAll() {
             for (Usuario usr : usuarios)
                    usuarios.remove(usr);
             //queda mejor esto
             //usuarios.removeAll(usuarios);
       }

       //actualizar usuario (realmente no lo usaremos)
       public static boolean update(Usuario usr) {
             boolean res = usuarios.remove(usr);
             if (res)
                    usuarios.add(usr);
             return res;
       }

       //Obtener el listado de nombres de usuarios.
       //Esto lo usaremos para las pantallas
       public static List<String> getUsuarios() {
             List<String> lista = new ArrayList<String>();
             for (Usuario u : usuarios){
                 lista.add(u.getNombre());
             }
             return lista;
       }
        
       //tamaño de la lista
       public static int size(){
             return usuarios.size();
       }
      
       //obtener la conversación de un usuario
       public static String getConversacion (String usuario){
             String res = "";
           Integer idx = usuarios.indexOf(new Usuario(usuario));
           if (idx>=0)
              res = usuarios.get(idx).getConversacion();
          
           return res;
       }
      
       //añadir texto a una conversación (inicialmente no lo usaremos)
       public static void addConversacion (String usuario, String autor, String texto){
             Integer idx = usuarios.indexOf(new Usuario(usuario));
           if (idx>=0)
              ((Usuario)usuarios.get(idx)).addTexto(autor, texto);
       }
      
       //Carga los datos de nuestro usuario y limpia el listado.
       //Esto se usará al inicializar la aplicación.
       public static void loadDatos(String id, String usuario, String tlf){
             removeAll();
             nombre = usuario;
             regId = id;
             telefono = tlf;
       }
      
       //getters
       public static String getNombre(){
             return nombre;
       }
        
       public static String getRegId(){
             return regId;
       }
        
       public static String getTelefono(){
             return telefono;
       }

       public static Usuario getUsuario(int pos){
             Usuario usr = null;
             if (pos < usuarios.size())
                    usr = usuarios.get(pos);
             return usr;
       }
}


GCMIntentServices

Nuestra clase controladora de eventos GCM es la que gestionará los eventos de Google Cloud Messaging. Al extender de la clase GCMBaseIntentService deberemos implementar una serie de métodos que serán:

  • onMessage: lo veremos más adelante

  • onDeletedMessages: no lo veremos

  • onError: ocurre un error

  • onRecoverableError: tampoco lo veremos

  • onRegistered: ocurre cuando nos registramos en GCM. Para ello, marcaremos que nos hemos registrado en el servidor, almacenaremos nuestros datos (usuario, teléfono e id de registro) en el Datastore y lanzaremos el evento de GCM finalizado para que se lance el registro en el servidor.


//Evento para que ocurre cuando se registra un dispositivo en GCM
@Override
protected void onRegistered(Context context, String registrationId) {
       //nos marcamos como registrados
       GCMRegistrar.setRegisteredOnServer(context, true);
       //registramos los datos en el datastore
       Datastore.loadDatos(registrationId, Datastore.getNombre(), Datastore.getTelefono());
       //registramos en el servidor
       CommonUtilities.finishRegistrationGCM(context, registrationId);
}

  • onUnregistered: cuando ocurre el evento de borrado del registro en GCM. Tampoco lo veremos


Es decir, básicamente implementaremos el onRegistered y onMessage.

public class GCMIntentService extends GCMBaseIntentService {

    @SuppressWarnings("hiding")
    private static final String TAG = "GCMIntentService";
   
    //constructor
    public GCMIntentService() {
        super(SENDER_ID);
    }
   
    //recibe un mensaje
    @Override
    protected void onMessage(Context context, Intent intent) {
        Log.i(TAG, "Received message");
        String datos = intent.getExtras().getString("datos");
        generateNotification(context, datos);
    }

    @Override
    protected void onDeletedMessages(Context context, int total) {
        Log.i(TAG, "Received deleted messages notification");
        generateNotification(context, "mensaje borrado");
    }

    @Override
    public void onError(Context context, String errorId) {
        Log.i(TAG, "Received error: " + errorId);
    }

    @Override
    protected boolean onRecoverableError(Context context, String errorId) {
        // log message
        Log.i(TAG, "Received recoverable error: " + errorId);
        return super.onRecoverableError(context, errorId);
    }

    /**
     * Issues a notification to inform the user that server has sent a message.
     */
    private static void generateNotification(Context context, String message) {
        int icon = R.drawable.ic_stat_gcm;
        long when = System.currentTimeMillis();
        NotificationManager notificationManager = (NotificationManager)
                context.getSystemService(Context.NOTIFICATION_SERVICE);
        Notification notification = new Notification(icon, message, when);
        String title = context.getString(R.string.app_name);
        Intent notificationIntent = new Intent(context, LoadingActivity.class);
        // set intent so it does not start a new activity
        notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
                Intent.FLAG_ACTIVITY_SINGLE_TOP);
        PendingIntent intent =
                PendingIntent.getActivity(context, 0, notificationIntent, 0);
        notification.setLatestEventInfo(context, title, message, intent);
        notification.flags |= Notification.FLAG_AUTO_CANCEL;
        notificationManager.notify(0, notification);
    }

    //Evento para que ocurre cuando se registra un dispositivo en GCM
       @Override
       protected void onRegistered(Context context, String registrationId) {
             //nos marcamos como registrados
             GCMRegistrar.setRegisteredOnServer(context, true);
             //registramos los datos en el datastore
             Datastore.loadDatos(registrationId, Datastore.getNombre(), Datastore.getTelefono());
             //registramos en el servidor
             CommonUtilities.finishRegistrationGCM(context, registrationId);
       }
      
       //borramos nuestro registro: no se usa
       @Override
       protected void onUnregistered(Context context, String registrationId)     {
             Log.i(TAG, "Received unregistered: " + registrationId);
       }

}

OJO! Si veis, usamos un método, generateNotification, que es simple C&P del ejemplo de Google y que modificaremos en el siguiente capítulo.

Modificando nuestro servidor.

 He modificado la clase Usuario para evitar confusiones. El cambio es simple: el índice será el teléfono (hash, equals…) y la propiedad usuario pasará a ser nombre.

Por otro lado, la función registro del webservice ha cambiado para devolver un listado de pares nombre|teléfono que identifican a los usuarios registrados en el servidor con los que podremos entablar conversación.

La petición de registro quedará así:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cut="http://cutreguasap.example.com/">
   <soapenv:Header/>
   <soapenv:Body>
      <cut:registro>
         <id>1231id</id>
         <telefono>1231tf</telefono>
         <nombre>1231us</nombre>
      </cut:registro>
   </soapenv:Body>
</soapenv:Envelope>

Y la respuesta así:

<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
   <S:Body>
      <ns2:registroResponse xmlns:ns2="http://cutreguasap.example.com/">
         <return>josec|josec</return>
         <return>1231us|1231tf</return>
      </ns2:registroResponse>
   </S:Body>
</S:Envelope>

Los cambios los podéis descargar de aquí.


Pantalla Loading

Esta pantalla realizará la llamada al registro tanto de GCM como del servidor. Simplemente mostrará un progress bar y un texto indicando que se está cargando.



<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#000000"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".LoadingActivity" >

    <TextView
        android:id="@+id/txtLoading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="190dp"
        android:text="Loading"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:textColor="#ffffff"/>

    <ProgressBar
        android:id="@+id/pbLoading"
        style="?android:attr/progressBarStyleLarge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@+id/txtLoading"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="42dp" />

</RelativeLayout>

En el código de la pantalla, como viene siendo costumbre, tendremos la función onCreate que invocará  a la función cargarObjetosPantalla, que será la encargada de cargar los objetos de la pantalla y después invocar a registrarCliente.

public class LoadingActivity extends Activity {

       private ProgressBar pbLoading;
       private TextView txtLoading;

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

       @Override
       public boolean onCreateOptionsMenu(Menu menu) {
             // Inflate the menu; this adds items to the action bar if it is present.
             getMenuInflater().inflate(R.menu.loading, menu);
             return true;
       }
      
       private void cargarObjetosPantalla()
    {
             //objetos de la pantalla
             pbLoading = (ProgressBar)findViewById(R.id.pbLoading);
             txtLoading = (TextView)findViewById(R.id.txtLoading);
            
             //registrar app
             registrarCliente();
    }

       (...)

La función registrarCliente creará asociará las funciones a los manejadores de los intentos que hemos definido: recepción de mensaje, registro finalizado de gcm y del servidor. Tras esto, invocará al registro de GCM.

private void registrarCliente(){
       // comprobamos el dispositivo
    GCMRegistrar.checkDevice(this);
    // comprobamos el manifest
    GCMRegistrar.checkManifest(this);
   
    //creamos el evento de recepcion de mensajes
    registerReceiver(mHandleMessageReceiver,
            new IntentFilter(DISPLAY_MESSAGE_ACTION));
    //y los eventos de recepcion de registros
    registerReceiver(mHandleFinishRegistryGCM,
            new IntentFilter(GCM_FINISH_REGISTRY_ACTION));
    registerReceiver(mHandleFinishRegistryServer,
            new IntentFilter(SERVER_FINISH_REGISTRY_ACTION));
   
    //lanzamos el evento de registro
    GCMRegistrar.register(this, SENDER_ID);
}

Como hemos visto, se crean tres manejadores:
  • Manejador de recepción de mensajes de GCM (lo veremos más adelante)

  • Manejador para recepción del registro finalizado en GCM, el cual invocará a la tarea asíncrona para registrarnos en el servidor.


// Registro GCM finalizado
private final BroadcastReceiver mHandleFinishRegistryGCM = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        //aqui hacemos lo necesario para manejar el evento GCM_FINISH_REGISTRY_ACTION
       //obtenemos el regId
        String regId = intent.getExtras().getString(EXTRA_MESSAGE);
        //guardamos la info de nombre, telefono y regId en Datastore
        Datastore.loadDatos(regId, android.os.Build.MODEL, getPhoneNumber());
        //nos registramos en en el servidor
        AsyncRegistro asyncRegistro = new AsyncRegistro(regId, getPhoneNumber(), android.os.Build.MODEL, context);
        URL url = null;
       try {
             url = new URL(SERVER_URL_REG);
       } catch (MalformedURLException e) {
             e.printStackTrace();
       }
       asyncRegistro.execute(url);
    }
};

  • Y finalmente el manejador para la finalización del registro en el servidor, el cual simplemente llama a la función encargada de pasar a la pantalla siguiente.


// Registro Servidor finalizado
private final BroadcastReceiver mHandleFinishRegistryServer = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String registrationId = intent.getExtras().getString(EXTRA_MESSAGE);
        goToUsers();
    }
};


Como funciones extras tenemos la que nos devuelve el número de teléfono del dispositivo:

private String getPhoneNumber(){
  TelephonyManager mTelephonyManager;
  mTelephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
  return mTelephonyManager.getLine1Number();
}

Y la que nos carga la pantalla de usuarios:

protected void goToUsers()
{
       Intent intento = new Intent(LoadingActivity.this, UsersActivity.class);
       startActivity(intento);
}


Modificando el Manifest 

El manifest se torna complejo de trabajar. En la pestaña Application deberemos tener nuestras 3 pantallas y añadiremos lo que aparece en la imagen:


En la pestaña de permisos debemos añadir mucho:



Al final, el manifest deberá quedar así:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.cutreguasap"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="14" />
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
    <uses-permission android:name="android.permission.WAKE_LOCK"/>
    <permission android:name="com.example.cutreguasap.permission.C2D_MESSAGE" android:protectionLevel="signature"></permission>
    <uses-permission android:name="com.example.cutreguasap.permission.C2D_MESSAGE"/>
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.cutreguasap.LoadingActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:label="@string/app_name" android:name="ConversationActivity"></activity>
        <activity android:label="@string/app_name" android:name="UsersActivity"></activity>
        <receiver android:name="com.google.android.gcm.GCMBroadcastReceiver" android:permission="com.google.android.c2dm.permission.SEND">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE"/>
                <action android:name="com.google.android.c2dm.intent.REGISTRATION"/>
                <category android:name="com.example.cutreguasap"/>
            </intent-filter>
        </receiver>
        <service android:name="GCMIntentService"></service>
    </application>

</manifest>

¿Cómo funciona esto?

Como son muchos saltos de un lado a otro, veremos cómo sería la ejecución con un simple diagrama



Ya tenemos hecha la tarea de registro. Ahora nos queda lo más difícil: enviar mensajería desde el servidor. Lo veremos en el próximo post.

El ejemplo, incompleto, lo podéis descargar de aquí.