martes, 25 de noviembre de 2014

Recepción de notificaciones

El cliente de un servicio GCM podrá recibir notificaciones Push y procesarlas mediante algún mecanismo. Cabe destacar que una notificación Push despierta a un servicio de la app pasándole información proveniente del servidor, dicha información puede ser directamente un dato necesario en la app, un identificador de algún registro de la base de datos, o simplemente la llamada que le indique a la app que los datos deben ser actualizados con información del servidor. Una vez obtenida esta alerta, la app puede indicar mediante una notificación del sistema operativo algún mensaje al usuario.
El proceso completo para poder recibir notificaciones es el siguiente:

  • La app se registra en GCM, empleando el id de la aplicación correspondiente
  • La app envía a un servidor propio el id de registro, necesario para poder enviarle notificaciones
  • El servidor ya podrá enviar notificaciones a la app, indicando su id de registro y la información necesaria para la comunicación

Lo mínimo necesario

Configurar el manifest del proyecto de aplicación. En este caso hay dos Activity definidas, la principal que realiza el registro, y una Activity para mostrar cuando se recibe una alerta. El resto de elementos son requeridos para poder recibir las notificaciones push.
La aplicación debe referenciar la librería Google Play Services para poder funcionar correctamente:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="es.hubiqus.gcmclient"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="11"
        android:targetSdkVersion="17" />
   
    <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="es.hubiqus.gcmclient.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
    <uses-permission
android:name="es.hubiqus.gcmclient.permission.C2D_MESSAGE" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
       
        <meta-data
                 android:name="com.google.android.gms.version"
                 android:value="@integer/google_play_services_version" />
       
        <activity
            android:name="es.hubiqus.gcmclient.MainActivity"
            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:name="es.hubiqus.gcmclient.AlertaActivity"
            android:label="@string/app_name"/>
       
        <receiver
            android:name="es.hubiqus.gcmclient.GcmBroadcastReceiver"
            android:permission="com.google.android.c2dm.permission.SEND" >
            <intent-filter>
                <action
android:name="com.google.android.c2dm.intent.RECEIVE" />
                <category android:name="es.hubiqus.gcmclient" />
            </intent-filter>
        </receiver>
        <service android:name="es.hubiqus.gcmclient.GcmIntentService" />
       
    </application>

</manifest>

Cabe destacar del Manifest anterior que en los permisos de tipo C2D_MESSAGE debe incluirse el nombre de paquete de la app, así como en la etiqueta category del receiver.

Una Activity principal que realiza el registro con el servidor GCM. Una vez registrado envía el id recibido al servidor propio.
Es necesario que esta clase chequee si se encuentra habilitado el APK de Google Play Services en el dispositivo, en caso contrario lanza una ventana de descarga del mismo, y cierra la app:
public class MainActivity extends Activity {

       private static final String PROP_REGID = "regId";
       private static final String PROP_VERSION = "appVersion";
       private static final int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;
      
       private GoogleCloudMessaging gcm;
      
       //Id del proyecto, se obtiene en la Google API Console
       private String SENDER_ID = "ID";

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

             //Comprobar que existen los servicios de Google Play,
//en tal caso registrar el dispositivo
             if (checkPlayServices()) {
                    gcm = GoogleCloudMessaging.getInstance(this);
                    String regid = getRegId();

                    //Registrar si no estaba registrado
                    if (regid.length() == 0) {
                           registrar();
                    }
             }
       }
      
       @Override
       protected void onResume() {
              super.onResume();
              //Chequear Play Services
             checkPlayServices();
       }
      
   /**
     * Chequear la APK Google Play Services APK
     * Si no, lanza diálogo para la descarga
     */
    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 {
                finish();
            }
            return false;
        }
        return true;
    }
   
    /**
     * Obtener el ID de registro si ya existía
     * Si devuelve cadena vacía es necesario registro con el servidor
     *
     * @return regID, o cadena vacía si no está registrado
     */
    private String getRegId() {
        SharedPreferences prefs =
getSharedPreferences(getString(R.string.app_name), Context.MODE_PRIVATE);
        String registrationId = prefs.getString(PROP_REGID, "");
        if (registrationId.length() == 0) {
            return "";
        }
        //Si la app fue actualizada se obtiene nuevo ID
        int registeredVersion = prefs.getInt(PROP_VERSION,
Integer.MIN_VALUE);
        int currentVersion = getAppVersion();
        if (registeredVersion != currentVersion) {
            return "";
        }
        return registrationId;
    }
   
    /**
     * Guardar regId y versión
     * @param regId registration ID
     */
    private void setRegId(String regId) {
       SharedPreferences prefs = getSharedPreferences(getString(R.string.app_name), Context.MODE_PRIVATE);
        int appVersion = getAppVersion();
        SharedPreferences.Editor editor = prefs.edit();
        editor.putString(PROP_REGID, regId);
        editor.putInt(PROP_VERSION, appVersion);
        editor.commit();
    }
   
    /**
     * Obtener la versión de la app
     * @return número de versión
     */
    private int getAppVersion() {
        try {
            PackageInfo packageInfo = getPackageManager().
getPackageInfo(getPackageName(), 0);
            return packageInfo.versionCode;
        } catch (NameNotFoundException e) {
            return 0;
        }
    }
   
    /**
     * Registrar con el servidor
     */
    private void registrar(){
       TareaRegistro tarea = new TareaRegistro();
       tarea.execute();
    }
   
    /**
     * Enviar el regId al servidor para que lo incluya en los push
     */
    private void enviar(String regId){
       TareaEnvio tarea = new TareaEnvio();
       tarea.execute(regId);
    }
   
    private class TareaRegistro extends AsyncTask<Void, Void, String> {

       @Override
       protected String doInBackground(Void... args) {
     String regid = "";
            try {
                if (gcm == null) {
                    gcm = GoogleCloudMessaging.
getInstance(MainActivity.this);
                }
                regid = gcm.register(SENDER_ID);

                //Enviar al servidor
                enviar(regid);
            } catch (IOException ex) {
            regid = "Error :" + ex.getMessage();               
            }
            return regid;
       }

    }
   
    private class TareaEnvio extends AsyncTask<String, Void, String> {

             @Override
             protected String doInBackground(String... args) {
                    String res = args[0];
                    try {
                           EnviarHttp client =
new EnviarHttp(MainActivity.this);
                           res = client.callService(res);
                    } catch (Exception ex) {
                           ex.printStackTrace();
                           res = null;
                    }
                    return res;
             }

             @Override
             protected void onPostExecute(String result) {
                    if (result != null){
                           //Guardar el regId
                           setRegId(result);
                    }
             }
       }

}

Una clase para el envío de id de registro al servidor:
public class EnviarHttp {
      
       //Codificación empleada
       public static final String ENCODING = "UTF-8";
      
       private static final String PARAM_OP = "op";
       private static final String PARAM_ID = "id";
       private static final String OP_SAVE = "save";
      
       private static final int TIMEOUT_CONN = 5000;
       private static final int TIMEOUT_SOCK = 15000;
      
       private Context context;
      
       public EnviarHttp(Context context){
             this.context = context;
       }

       /**
        * Envío de solicitud HTTP
        * @return
        * @throws Exception
        */
       public String callService(String id) throws Exception {
             String res = null;
            
             InputStream is = null;
            
             HttpParams httpParameters = new BasicHttpParams();
             //Timeout para conectar, por defecto 0 (no hay timeout)
             HttpConnectionParams.setConnectionTimeout(httpParameters,
TIMEOUT_CONN);
             //Timeout para obtener datos
             HttpConnectionParams.setSoTimeout(httpParameters,
TIMEOUT_SOCK);
             HttpClient client = new DefaultHttpClient(httpParameters);     
            
             try {
                    //La solicitud será a la URL que determine el parámetro
                    HttpPost post =
new HttpPost(context.getString(R.string.ws_url));
                   
                    //Parámetros de envío
                    List<NameValuePair> params =
new ArrayList<NameValuePair>();
                    params.add(new BasicNameValuePair(PARAM_OP, OP_SAVE));
                    params.add(new BasicNameValuePair(PARAM_ID, id));
                    post.setEntity(new UrlEncodedFormEntity(params));
              
                    //Lanzar petición
                    HttpResponse response = client.execute(post);
              
                    // Procesar la información
                    is = response.getEntity().getContent();
                    res = this.procesar(is);
             } finally {
                    // cerrar las conexiones
                    if (is != null) {
                           is.close();
                    }
             }
            
             return res;
       }
      
       /**
        * Método para obtener la respuesta
        * @param is stream de entrada con los datos a procesar
        * @return objeto procesado
        * @throws IOException error en la conexión
        */
       public String procesar(InputStream is) throws IOException{
             String res = null;
            
             BufferedReader br =
new BufferedReader(new InputStreamReader(is, ENCODING));
             String linea;
             if ((linea = br.readLine()) != null){
                    res = linea;
             }
            
             return res;
       }
}

Un receiver que se encarga de obtener la notificación Push y ceder el control al servicio de notificaciones:
public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        //Servicio que gestionará el push
        ComponentName comp = new ComponentName(context.getPackageName(),
                GcmIntentService.class.getName());
        //Lanzar el servicio
        startWakefulService(context, (intent.setComponent(comp)));
        setResultCode(Activity.RESULT_OK);
    }
}

Un servicio de notificaciones que se encargará de realizar el procesamiento en función del mensaje recibido, y de notificar en la barra del sistema:
public class GcmIntentService extends IntentService {
   
    public GcmIntentService() {
        super("GcmIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
       try {
   int id = Integer.parseInt(intent.getExtras().getString("data"));

   GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(this);
                   
   String messageType = gcm.getMessageType(intent);
                   
   if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.
equals(messageType)) {
             //Realizar el procesamiento
             this.notificarBarra(getApplicationContext(), id);
   }
       } catch (Exception e) {
   e.printStackTrace();
} finally {
   //Liberar el bloqueo lanzado por el WakefulBroadcastReceiver
   GcmBroadcastReceiver.completeWakefulIntent(intent);
       }
    }

   
    /**
     * Notificar alerta, pasando datos a la activity correspondiente
     * @param context
     * @param id
     */
    private void notificarBarra(Context context, int id) {
   //Gestor de notificaciones
   NotificationManager mNotificationManager =
(NotificationManager) context.getSystemService(
Context.NOTIFICATION_SERVICE);
      
   int icon = R.drawable.ic_launcher;
   long when = System.currentTimeMillis();
      
   //Información de la notificación    
   CharSequence contentTitle = context.getString(R.string.app_name);
   CharSequence contentText = context.getString(R.string.msg_alerta);
      
   //Construir alerta
   NotificationCompat.Builder builder =
new NotificationCompat.Builder(context)
                      .setContentTitle(contentTitle)
                      .setContentText(contentText)
                      .setWhen(when)
                      .setSmallIcon(icon)
                      .setAutoCancel(true);
   builder.setDefaults(Notification.DEFAULT_ALL);
      
   //Intent que se lanzará cuando el usuario pulse en la alerta
   Intent notificationIntent =
new Intent(context, AlertaActivity.class);
   Bundle args = new Bundle();
   args.putInt(AlertaActivity.PARAM_ID, id);
   notificationIntent.putExtras(args);
   notificationIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
      
   PendingIntent contentIntent =
PendingIntent.getActivity(context, id, notificationIntent, 0);
      
   builder.setContentIntent(contentIntent);
      
   //Notificar
   mNotificationManager.notify(id, builder.build());
   }
}

Referencias

https://developer.android.com/google/gcm/client.html