Notificaciones Push Android: Google Cloud Messaging (GCM). Implementación Cliente (Nueva Versión)

En los apartados anteriores del curso hemos hablado sobre el servicio Google Cloud Messaging y hemos visto cómo implementar una aplicación web que haga uso de dicho servicio para enviar mensajes a dispositivos Android. Para cerrar el círculo, en este nuevo apartado nos centraremos en la aplicación Android cliente.

Esta aplicación cliente, como ya hemos comentado en alguna ocasión será responsable de:

  1. Registrarse contra los servidores de GCM como cliente capaz de recibir mensajes.
  2. Almacenar el “Registration ID” recibido como resultado del registro anterior.
  3. Comunicar a la aplicación web el “Registration ID” de forma que ésta pueda enviarle mensajes.
  4. Recibir y procesar los mensajes desde el servidor de GCM.

En la versión anterior de GCM, las tareas 1 y 4 se realizaban normalmente utilizando como ayuda una librería adicional (gcm.jar) proporcionada por Google. Sin embargo, en la nueva versión de GCM incluida como parte de los Google Play Services cambian un poco la filosofía de trabajo y esta librería ya no es necesaria.

Por su parte, el punto 2 lo resolveremos fácilmente mediante el uso de SharedPreferences. Y por último el punto 3 lo implementaremos mediante la conexión al servicio web SOAP que creamos en el apartado anterior, sirviéndonos para ello de la librería ksoap2, tal como ya describimos en el capítulo sobre servicios web SOAP en Android.

Durante el capítulo construiremos una aplicación de ejemplo muy sencilla, en la que el usuario podrá introducir un nombre de usuario identificativo y pulsar un botón para que quede guardado en las preferencias de la aplicación. Tras esto podrá registrarse como cliente capaz de recibir mensajes desde GCM pulsando un botón llamado “Registrar”. En caso de realizarse de forma correcta este registro la aplicación enviará automáticamente el Registration ID recibido y el nombre de usuario almacenado a la aplicación servidor a través del servicio web. Obviamente todo este proceso de registro debería hacerse de forma transparente para el usuario de una aplicación real, en esta ocasión he colocado un botón para ello sólo por motivos didácticos y para poder hacer una prueba más controlada.

demo gmc

Como en el caso de cualquier otro servicio incluido en los Google Play Services el primer paso para crear nuestra aplicación Android será importar el proyecto de librería de los servicios, crear nuestro propio proyecto y finalmente hacer referencia a la librería desde nuestro proyecto. Todo este proceso está explicado en el artículo de introducción a los Google Play Services.

El siguiente paso será configurar nuestro AndroidManifest. Lo primero que revisaremos será la cláusula <usessdk>, donde como versión mínima del SDK debemos indicar la 8 (Android 2.2) o superior. Con esto nos aseguraremos de que la aplicación no se instala en dispositivos con versión de Android anterior, no soportadas por los Google Play Services.

A continuación añadiremos los permisos necesarios para ejecutar la aplicación y utilizar GCM:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

<permission android:name="net.sgoliver.android.newgcm.permission.C2D_MESSAGE"
     android:protectionLevel="signature" />
<uses-permission android:name="net.sgoliver.android.newgcm.permission.C2D_MESSAGE" />

El primero (INTERNET) nos dará acceso a internet en la aplicación, el segundo (GET_ACCOUNTS) es necesario porque GCM requiere una cuenta de Google configurada en el dispositivo, el tercero (WAKE_LOCK) será necesario para utilizar un determinado tipo de broadcast receiver que comentaremos más adelante, el cuarto (RECEIVE) es el que permitirá que la aplicación se registre y reciba mensajes de GCM. Los dos últimos aseguran que sólo nosotros podremos recibir los mensajes de nuestra aplicación (sustituir mi paquete java “net.sgoliver.android.newgcm” por el vuestro propio en estas dos lineas).

Por último, como componentes de la aplicación, además de la actividad principal ya añadida por defecto, deberemos declarar un broadcast receiver, que llamaremos GCMBroadcastReceiver (tenéis que modificar el elemento <category> con vuestro paquete java), y un servicio que llamaremos GCMIntentService. Más adelante veremos cuál será el cometido de cada uno de estos componentes.

<application
     android:allowBackup="true"
     android:icon="@drawable/ic_launcher"
     android:label="@string/app_name"
     android:theme="@style/AppTheme" >

     ...

     <receiver
         android:name=".GCMBroadcastReceiver"
         android:permission="com.google.android.c2dm.permission.SEND" >
         <intent-filter>
              <action android:name="com.google.android.c2dm.intent.RECEIVE" />
              <category android:name="net.sgoliver.android.newgcm" />
         </intent-filter>
     </receiver>

     <service android:name=".GCMIntentService" />

</application>

Una vez definido nuestro AndroidManifest con todos los elementos necesarios vamos a empezar a implementar la funcionalidad de nuestra aplicación de ejemplo. Empezaremos por el proceso de registro que se desencadena al pulsar el botón “Registrar” de la aplicación tras introducir un nombre de usuario.

Nuestro botón de registro tendrá que realizar las siguientes acciones:

  1. Verificar que el dispositivo tiene instalado Google Play Services.
  2. Revisar si ya tenemos almacenado el código de registro de GCM (registration id) de una ejecución anterior.
  3. Si no disponemos ya del código de registro realizamos un nuevo registro de la aplicación y guardamos los datos.

El código del botón con estos tres pasos, que iremos comentando por partes, sería el siguiente:

btnRegistrar.setOnClickListener(new OnClickListener() {

	@Override
	public void onClick(View v)
	{
		context = getApplicationContext();

		//Chequemos si está instalado Google Play Services
		//if(checkPlayServices())
		//{
		        gcm = GoogleCloudMessaging.getInstance(MainActivity.this);

		        //Obtenemos el Registration ID guardado
		        regid = getRegistrationId(context);

		        //Si no disponemos de Registration ID comenzamos el registro
		        if (regid.equals("")) {
		    		TareaRegistroGCM tarea = new TareaRegistroGCM();
		    		tarea.execute(txtUsuario.getText().toString());
		        }
		//}
		//else
		//{
	        //    Log.i(TAG, "No se ha encontrado Google Play Services.");
	        //}
	}
});

El chequeo de si están instalados los Google Play Services en el dispositivo no se comporta demasiado bien al ejecutar la aplicación sobre el emulador (dependiendo de la versión de Android utilizada) por lo que he decidido mantenerlo comentado para este ejemplo, pero en una aplicación real sí debería realizarse. Además, también debería incluirse en el evento onResume() de la actividad:

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

//    checkPlayServices();
}

En cuanto a la lógica para hacer el chequeo podremos ayudarnos de la clase GooglePlayServicesUtil, que dispone del método isGooglePlayServicesAvailable() para hacer la verificación. En caso de no estar disponibles (si el método devuelve un valor distinto a SUCCESS) aún podemos mostrar un diálogo de advertencia al usuario dando la posibilidad de instalarlos. Esto lo haremos llamando al método getErrorDialog() de la misma clase GooglePlayServicesUtil. Quedaría algo así:

private boolean checkPlayServices() {
    int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
    if (resultCode != ConnectionResult.SUCCESS)
    {
        if (GooglePlayServicesUtil.isUserRecoverableError(resultCode))
        {
            GooglePlayServicesUtil.getErrorDialog(resultCode, this,
                    PLAY_SERVICES_RESOLUTION_REQUEST).show();
        }
        else
        {
            Log.i(TAG, "Dispositivo no soportado.");
            finish();
        }
        return false;
    }
    return true;
}

Si Google Play Services está instalado en el dispositivo el siguiente paso será comprobar si ya tenemos guardado los datos de registro de una ejecución anterior, en cuyo caso no habrá que volver a hacer el registro (salvo en contadas ocasiones que comentaremos ahora). Esta comprobación la heremos dentro de un método llamado getRegistrationId(), que entre otras cosas hará uso de preferencias compartidas (Shared Preferences) para recuperar los datos guardados. Nuestra aplicación guardará 4 preferencias, que definiremos en nuestra actividad como constantes:

private static final String PROPERTY_REG_ID = "registration_id";
private static final String PROPERTY_APP_VERSION = "appVersion";
private static final String PROPERTY_EXPIRATION_TIME = "onServerExpirationTimeMs";
private static final String PROPERTY_USER = "user";

La primera de ellas es el código de registro de GCM, la segunda guardará la versión de la aplicación para la que se ha obtenido dicho código, la tercera indicará la fecha de caducidad del código de registro guardado, y por último guardaremos el nombre de usuario.

En el método getRegistrationId() lo primero que haremos será recuperar la preferencia PROPERTY_REG_ID. Si ésta no está informada saldremos inmediatamente del método para proceder a un nuevo registro.

Si por el contrario ya teníamos un registration_id guardado podríamos seguir utilizándolo sin tener que registrarnos de nuevo (lo devolveremos como resultado), pero habrá tres situaciones en las que queremos volver a realizar el registro para asegurarnos de que nuestra aplicación pueda seguir recibiendo mensajes sin ningún problema:

  • Si el nombre de usuario ha cambiado.
  • Si la versión de la aplicación ha cambiado.
  • Si se ha sobrepasado la fecha de caducidad del código de registro.

Para verificar esto nuestro método recuperará cada una de las preferencias compartidas, realizará las verificaciones indicadas y en caso de cumplirse alguna de ellas saldrá del método sin devolver el antiguo registration_id para que se vuelva a realizar el registro.

private String getRegistrationId(Context context)
{
    SharedPreferences prefs = getSharedPreferences(
	MainActivity.class.getSimpleName(),
        Context.MODE_PRIVATE);

    String registrationId = prefs.getString(PROPERTY_REG_ID, "");

    if (registrationId.length() == 0)
    {
        Log.d(TAG, "Registro GCM no encontrado.");
        return "";
    }

    String registeredUser =
	prefs.getString(PROPERTY_USER, "user");

    int registeredVersion =
	prefs.getInt(PROPERTY_APP_VERSION, Integer.MIN_VALUE);

    long expirationTime =
        prefs.getLong(PROPERTY_EXPIRATION_TIME, -1);

    SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm", Locale.getDefault());
    String expirationDate = sdf.format(new Date(expirationTime));

    Log.d(TAG, "Registro GCM encontrado (usuario=" + registeredUser +
	", version=" + registeredVersion +
	", expira=" + expirationDate + ")");

    int currentVersion = getAppVersion(context);

    if (registeredVersion != currentVersion)
    {
        Log.d(TAG, "Nueva versión de la aplicación.");
        return "";
    }
    else if (System.currentTimeMillis() > expirationTime)
    {
    	Log.d(TAG, "Registro GCM expirado.");
        return "";
    }
    else if (!txtUsuario.getText().toString().equals(registeredUser))
    {
    	Log.d(TAG, "Nuevo nombre de usuario.");
        return "";
    }

    return registrationId;
}

private static int getAppVersion(Context context)
{
    try
    {
        PackageInfo packageInfo = context.getPackageManager()
                .getPackageInfo(context.getPackageName(), 0);

        return packageInfo.versionCode;
    }
    catch (NameNotFoundException e)
    {
        throw new RuntimeException("Error al obtener versión: " + e);
    }
}

Como podéis observar, para consultar la versión actual de la aplicación utilizamos un método auxiliar getAppVersion() que obtiene la versión mediante el Package Manager y su método getPackageInfo().

Bien, pues llegados aquí si el método anterior nos ha devuelto un código de registro (es decir, que ya teníamos uno guardado) no tendríamos que hacer nada más, significaría que ya estamos registrados en GCM y tan sólo tenemos que esperar a recibir mensajes. En caso contrario, tendremos que realizar un nuevo registro, de lo que nos ocuparemos mediante la tarea asíncrona TareaRegistroGCM.

Esta tarea asíncrona tendrá que realizar tres acciones principales: registrar la aplicación contra los servidores de GCM, registrarnos contra nuestro propio servidor al que tendrá que enviar entre otras cosas el registration_id obtenido de GCM, y por último guardar como preferencias compartidas los nuevos datos de registro.

private class TareaRegistroGCM extends AsyncTask<String,Integer,String>
{
	@Override
        protected String doInBackground(String... params)
	{
            String msg = "";

            try
            {
                if (gcm == null)
                {
                    gcm = GoogleCloudMessaging.getInstance(context);
                }

                //Nos registramos en los servidores de GCM
                regid = gcm.register(SENDER_ID);

                Log.d(TAG, "Registrado en GCM: registration_id=" + regid);

                //Nos registramos en nuestro servidor
                boolean registrado = registroServidor(params[0], regid);

                //Guardamos los datos del registro
                if(registrado)
                {
                	setRegistrationId(context, params[0], regid);
                }
            }
            catch (IOException ex)
            {
            	Log.d(TAG, "Error registro en GCM:" + ex.getMessage());
            }

            return msg;
        }
}

Lo primero que haremos será obtener una instancia del servicio de Google Cloud Messaging mediante el método GoogleCloudMessaging.getInstance(). Obtenido este objeto, el registro en GCM será tan sencillo como llamar a su método register() pasándole como parámetro el Sender ID que obtuvimos al crear el proyecto en la Consola de APIs de Google. Esta llamada nos devolverá el registration_id asignado a nuestra aplicación.

Tras el registro en GCM debemos también registrarnos en nuestro servidor, al que al menos debemos enviarle nuestro registration_id para que nos pueda enviar mensajes posteriormente. En nuestro caso de ejemplo, además del código de registro vamos a enviarle también nuestro nombre de usuario. Como ya dijimos este registro lo vamos a realizar utilizando el servicio web que creamos en el artículo sobre la parte servidor. La llamada al servicio web es análoga a las que ya explicamos en el artículo sobre servicios web SOAP por lo que no entraré en más detalles, tan sólo veamos el código.

private boolean registroServidor(String usuario, String regId)
{
	boolean reg = false;

	final String NAMESPACE = "http://sgoliver.net/";
	final String URL="http://10.0.2.2:1634/ServicioRegistroGCM.asmx";
	final String METHOD_NAME = "RegistroCliente";
	final String SOAP_ACTION = "http://sgoliver.net/RegistroCliente";

	SoapObject request = new SoapObject(NAMESPACE, METHOD_NAME);

	request.addProperty("usuario", usuario);
	request.addProperty("regGCM", regId);

	SoapSerializationEnvelope envelope =
		new SoapSerializationEnvelope(SoapEnvelope.VER11);

	envelope.dotNet = true;

	envelope.setOutputSoapObject(request);

	HttpTransportSE transporte = new HttpTransportSE(URL);

	try
	{
		transporte.call(SOAP_ACTION, envelope);
		SoapPrimitive resultado_xml =(SoapPrimitive)envelope.getResponse();
		String res = resultado_xml.toString();

		if(res.equals("1"))
		{
			Log.d(TAG, "Registrado en mi servidor.");
			reg = true;
		}
	}
	catch (Exception e)
	{
		Log.d(TAG, "Error registro en mi servidor: " + e.getCause() + " || " + e.getMessage());
	}

	return reg;
}

Por último, si todo ha ido bien guardaremos los nuevos datos de registro (usuario, registration_id, version de la aplicación y fecha de caducidad) como preferencias compartidas. Lo haremos todo dentro del método setRegistrationId().

private void setRegistrationId(Context context, String user, String regId)
{
    SharedPreferences prefs = getSharedPreferences(
	MainActivity.class.getSimpleName(),
        Context.MODE_PRIVATE);

    int appVersion = getAppVersion(context);

    SharedPreferences.Editor editor = prefs.edit();
    editor.putString(PROPERTY_USER, user);
    editor.putString(PROPERTY_REG_ID, regId);
    editor.putInt(PROPERTY_APP_VERSION, appVersion);
    editor.putLong(PROPERTY_EXPIRATION_TIME,
	System.currentTimeMillis() + EXPIRATION_TIME_MS);

    editor.commit();
}

La forma de guardar los datos mediante preferencias compartidas ya la comentamos en detalle en el artículo dedicado a las Shared Preferences. Lo único a comentar es la forma de calcular la fecha de caducidad del código de registro. Vamos a calcular esa fecha por ejemplo como la actual más una semana. Para ello obtenemos la fecha actual en milisegundos con currentTimeMillis() y le sumamos una constante EXPIRATION_TIME_MS que hemos definido con el valor 1000 * 3600 * 24 * 7, es decir, los milisegundos de una semana completa.

Y con esto habríamos terminado la fase de registro de la aplicación. Pero para recibir mensajes aún nos faltan dos elementos importantes. Por un lado tendremos que implementar un Broadcast Receiver que se encargue de recibir los mensajes, y por otro lado crearemos un nuevo servicio (concretamente un Intent Service) que se encargue de procesar dichos mensajes. Esto lo hacemos así porque no es recomendable realizar tareas complejas dentro del propio broadcast receiver, por lo que normalmente utilizaremos este patrón en el que delegamos todo el trabajo a un servicio,  y el broadcast receiver se limitará a llamar a éste.

En esta ocasión vamos a utilizar un nuevo tipo específico de broadcast receiver, WakefulBroadcastReceiver, que nos asegura que el dispositivo estará “despierto” el tiempo que sea necesario para que termine la ejecución del servicio que lancemos para procesar los mensajes. Esto es importante, dado que si utilizáramos un broadcast receiver tradicional el dispositivo podría entrar en modo de suspensión (sleep mode) antes de que termináramos de procesar el mensaje.

Crearemos por tanto una nueva clase que extienda de WakefulBroadcastReceiver, la llamamos GCMBroadcastReceiver, e implementaremos el evento onReceive() para llamar a nuestro servicio de procesamiento de mensajes, que recordemos lo llamamos GCMIntentService. La llamada al servicio la realizaremos mediante el método startWakefulService() que recibirá como parámetros el contexto actual, y el mismo intent recibido sobre el que indicamos el servicio a ejecutar mediante su método setComponent().

public class GCMBroadcastReceiver extends WakefulBroadcastReceiver
{
    @Override
    public void onReceive(Context context, Intent intent)
    {
        ComponentName comp =
        	new ComponentName(context.getPackageName(),
                GCMIntentService.class.getName());

        startWakefulService(context, (intent.setComponent(comp)));

        setResultCode(Activity.RESULT_OK);
    }
}

Para el servicio crearemos una nueva clase GCMIntentService que extienda de IntentService (para más información sobre los Intent Service puedes consultar el artículo dedicado a ellos) y como siempre implementaremos su evento onHandleIntent(). Aquí lo primero que haremos será nuevamente obtener una instancia a los Servicios de Google Play, y posteriormente obtener el tipo de mensaje recibido (mediante getMessageType()) y sus parámetros (mediante getExtras()). Dependiendo del tipo de mensaje obtenido podremos realizar unas acciones u otras. Existen algunos tipos especiales de mensaje (MESSAGE_TYPE_SEND_ERROR, MESSAGE_TYPE_DELETED, …) para ser notificado de determinados eventos, pero el que nos interesa más será el tipo MESSAGE_TYPE_MESSAGE que identifica a los mensajes “normales” o genéricos de GCM. Para nuestro ejemplo, en caso de recibirse uno de estos mensajes simplemente mostraremos una notificación en la barra de estado llamando a un método auxiliar mostrarNotificacion(). La implementación de este último método tampoco la comentaremos en detalle puesto que tenéis disponible un artículo del curso especialmente dedicado a este tema.

public class GCMIntentService extends IntentService
{
	private static final int NOTIF_ALERTA_ID = 1;

	public GCMIntentService() {
	        super("GCMIntentService");
    	}

	@Override
    	protected void onHandleIntent(Intent intent)
	{
        	GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(this);

        	String messageType = gcm.getMessageType(intent);
        	Bundle extras = intent.getExtras();

        	if (!extras.isEmpty())
        	{
            		if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType))
            		{
            			mostrarNotification(extras.getString("msg"));
            		}
        	}

        	GCMBroadcastReceiver.completeWakefulIntent(intent);
    	}

	private void mostrarNotification(String msg)
	{
		NotificationManager mNotificationManager =
				(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

		NotificationCompat.Builder mBuilder =
			new NotificationCompat.Builder(this)
				.setSmallIcon(android.R.drawable.stat_sys_warning)
				.setContentTitle("Notificación GCM")
				.setContentText(msg);

		Intent notIntent =  new Intent(this, MainActivity.class);
		PendingIntent contIntent = PendingIntent.getActivity(
				this, 0, notIntent, 0);

		mBuilder.setContentIntent(contIntent);

		mNotificationManager.notify(NOTIF_ALERTA_ID, mBuilder.build());
    	}
}

Sí es importante fijarse en que al final del método onHandleIntent(), tras realizar todas las acciones necesarias para procesar el mensaje recibido, debemos llamar al método completeWakefulIntent() de nuestro GCMBroadcastReceiver. Esto hará que el dispositivo pueda volver a entrar en modo sleep cuando sea necesario. Olvidar esta llamada podría implicar consumir rápidamente la batería del dispositivo, y no es lo que queremos, verdad?

Pues bien, hemos terminado. Ya tenemos nuestro servidor y nuestro cliente GCM preparados.  Si ejecutamos ambas y todo ha ido bien, introducimos un nombre de usuario en la aplicación Android, pulsamos “Registrar” para guardarlo y registrarnos, seguidamente desde la aplicación web introducimos el mismo nombre de usuario del cliente y pulsamos el botón “Enviar GCM”, en pocos segundos nos debería aparecer la notificación en la barra de estado de nuestro emulador como se observa en la imagen siguiente:

notificacion-gcm

Es conveniente utilizar un emulador en el que se ejecute una versión de Android 4.2.2 o superior, dado que en versiones anteriores Google Play Services podría no funcionar. Aún así, en ocasiones los mensajes tardar varios minutos en recibirse, por lo que tened algo de paciencia.

Puedes consultar y/o descargar el código completo de los ejemplos desarrollados en este artículo accediendo a la pagina del curso en GitHub.