martes, 25 de noviembre de 2014

Servidor Push con GCM

Para poder realizar el envío de notificaciones a los dispositivos es necesario enviar una solicitud al servidor de Google Cloud Messaging.
Para ello se pueden emplear distintas alternativas de comunicación con el mismo, en este caso se va a utilizar una comunicación vía HTTP empleando formato JSON. Se creará un servicio web que cumple una doble función, por un lado el mencionado envío de notificaciones, y por otro la recepción de los ids de los dispositivos que se registran en el sistema, los cuales hay que especificar al enviar una notificación obligatoriamente para que el servicio GCM sepa cuáles son los destinatarios del envío (mínimo un dispositivo, máximo 1000).

Lo mínimo necesario

Una clase para el envío de peticiones al servidor GCM:
public class GcmClient {

    private static final String PROP_URL =
"https://android.googleapis.com/gcm/send";
   
    //Clave obtenida con Google Developers API
    private static final String PROP_KEY = "key";

    /**
     * Enviar un mensaje simple
     * @param ids ids separados por comas
     * @param data datos
     * @return respuesta
     * @throws IOException error al enviar
     */
    public String send(String ids, String data) throws IOException{
        //Datos
        String input = "{\"registration_ids\": [" + ids + "]" +
                        ",\"data\": {" +
                            ",\"data\": \"" + data + "\"}}";
       
        return send(input);
    }
   
    /**
     * Enviar un mensaje
     * @param input mensaje
     * @return respuesta
     * @throws IOException error al enviar
     */
    private String send(String input) throws IOException {
        String res = null;
        HttpURLConnection conn = null;
        BufferedReader br = null;
        try {
            URL url = new URL(PROP_URL);
            conn = (HttpURLConnection) url.openConnection();
           
            //Establecer propiedades
            conn.setDoOutput(true);
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Content-Type", "application/json");
            conn.setRequestProperty("Authorization", "key=" + PROP_KEY);

            //Enviar
            OutputStream os = conn.getOutputStream();
            os.write(input.getBytes("UTF-8"));
            os.flush();
           
            if (conn.getResponseCode() == HttpURLConnection.HTTP_OK){
                br = new BufferedReader(
new InputStreamReader((conn.getInputStream())));

                res = "";
                String linea;
                while ((linea = br.readLine()) != null) {
                    res += linea;
                }
            }
        } finally {
            // cerramos las conexiones
            if (br != null) {
          br.close();
            }
            if (conn != null) {
                conn.disconnect();
            }
        }
       
        return res;
    }
}

Un Servlet para gestionar tanto el envío como la recepción de ids de dispositivo:
@WebServlet(name = "GcmWS", urlPatterns = {"/GcmWS"})
public class GcmWS extends HttpServlet {

    private static final String PARAM_OP = "op";
    private static final String PARAM_ID = "id";
    private static final String OP_SEND = "send";
    private static final String OP_SAVE = "save";
   
    private List<String> ids;

    @Override
    public void init() throws ServletException {
        super.init();
        //Inicializar la lista de dispòsitivos
        //El método init solamente se llama una vez al desplegar el Servlet
        ids = new ArrayList<String>();
    }   
   
    @Override
    protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
        //Habilitado en desarrollo para poder probar con el navegador
        this.doPost(request, response);
    }

    /**
     * Handles the HTTP
     * <code>POST</code> method.
     *
     * @param request servlet request
     * @param response servlet response
     * @throws ServletException if a servlet-specific error occurs
     * @throws IOException if an I/O error occurs
     */
    @Override
    protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
        response.setContentType("application/json;charset=UTF-8");
        PrintWriter out = response.getWriter();
       
        try{
            //Obtener la operación que requiere el usuario
            String op = request.getParameter(PARAM_OP);
               
            //Responder en función de la operación solicitada
            if (op.equals(OP_SEND)) {
                send(request, out);
            }else if (op.equals(OP_SAVE)) {
                save(request, out);
            }
        } catch (Exception ex) {
            out.println(ex);
        } finally {
            out.close();
        }

    }
   
    /**
     * Enviar un push a todos los recipientes
     * @param request
     * @param out
     * @throws Exception
     */
    private void send(HttpServletRequest request, PrintWriter out)
throws Exception {
        //Datos para la prueba
        //Se puede pasar al cliente cualquier dato serializado o en JSON
        //Lo recibirá como parámetros del Intent
        String data = "12";
       
        //Enviar y devolver resultado
        GcmClient client = new GcmClient();               
        out.println(client.send(getIds(), data));       
    }
   
    /**
     * Obtener los ids destino, separados por comas
     * @return
     */
    private String getIds(){
        String res = "";
        for (String s: ids){
            res += "\"" + s + "\",";
        }
        //Quitar la última coma
        if (res.length() > 0){
            res = res.substring(0, res.length() - 1);
        }
        return res;
    }
   
    /**
     * Almacenar un id
     * @param request
     * @param out
     * @throws Exception
     */
    private void save(HttpServletRequest request, PrintWriter out)
throws Exception {
        String id = request.getParameter(PARAM_ID);
       
        if (id == null || id.length() == 0){
            out.println("ID incorrecto");
        }else{
            ids.add(id);
            out.println("OK");
        }
    }

}

Referencias

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

Configuración de Google Cloud Messaging

Para poder realizar el envío de notificaciones Push a dispositivos se emplea Google Cloud Messaging, que es la plataforma de Google Play destinada a notificaciones.
En primer lugar, se necesita configurar un proyecto a través del Google API Console, que proporcionará un ID de proyecto, así como una clave necesaria para emplear desde el cliente.

Lo mínimo necesario

Crear el proyecto:
  • Abrir Google Developers Console
  • Crear un proyecto proporcionando un nombre
  • Una vez creado el proyecto aparecerá una página con la Descripción General del proyecto, donde aparece el número identificador del mismo

Habilitar la API para el proyecto:
  • Seleccionar APIs en el menú de la izquierda
  • Activar Google Cloud Messaging for Android

Obtener una clave para el proyecto:
  • Seleccionar Credenciales en el menú de la izquierda
  • Pulsar Crear clave nueva en el Acceso a API pública
  • Elegir Clave del servidor
  • Es necesario introducir la IP (o IPs) que va a tener el servidor, ya sea externo o la propia máquina de desarrollo
  • Se generará una clave para el servidor

Referencias

domingo, 26 de octubre de 2014

Comunicación Soap

Para la comunicación cliente/servidor, en el caso de las arquitecturas móviles se recomiendan protocolos ligeros como el caso de JSON que sobrecarga mínimamente la información transmitida, resulta independiente de la plataforma del servidor, y presenta muchas facilidades a la hora de parsear la información recibida como el empleo de la API Gson o de las propias clases Android de manejo de JSON como JSONObject.
De cualquier forma, en ocasiones resulta necesario acudir a web services clásicos que emplean el protocolo Soap para la comunicación, el cual transmite la información en formato XML que deberá ser procesado en el cliente al recibir la respuesta.

Compatibilidad

v1

Lo mínimo necesario

Es necesario permiso de Internet en el Manifest:
<uses-permission android:name="android.permission.INTERNET"/>

Una clase que represente el objeto del servidor:
public class Weather {
      
       private String location;
       private String time;
       private String wind;
       private String visibility;
       private String temperature;
       private String dewPoint;
       private String relativeHumidity;
       private String pressure;
       private String status;
      
       public String getLocation() {
             return location;
       }
       public void setLocation(String location) {
             this.location = location;
       }
       public String getTime() {
             return time;
       }
       public void setTime(String time) {
             this.time = time;
       }
       public String getWind() {
             return wind;
       }
       public void setWind(String wind) {
             this.wind = wind;
       }
       public String getVisibility() {
             return visibility;
       }
       public void setVisibility(String visibility) {
             this.visibility = visibility;
       }
       public String getTemperature() {
             return temperature;
       }
       public void setTemperature(String temperature) {
             this.temperature = temperature;
       }
       public String getDewPoint() {
             return dewPoint;
       }
       public void setDewPoint(String dewPoint) {
             this.dewPoint = dewPoint;
       }
       public String getRelativeHumidity() {
             return relativeHumidity;
       }
       public void setRelativeHumidity(String relativeHumidity) {
             this.relativeHumidity = relativeHumidity;
       }
       public String getPressure() {
             return pressure;
       }
       public void setPressure(String pressure) {
             this.pressure = pressure;
       }
       public String getStatus() {
             return status;
       }
       public void setStatus(String status) {
             this.status = status;
       }
      
}

Una clase para conectar con el servicio web a través de la conexión Soap empleando la API ksoap, que deberá estar configurada en el proyecto Android. Cuando los datos se reciben se procesan con las clases Android para el parseo XML:
public class EnviarSoap {
    //Parámetros de conexión
    private static final String NAMESPACE = "http://www.webserviceX.NET";
    private static final String URL =
"http://www.webservicex.net/globalweather.asmx";
    private static final String METHOD_NAME = "GetWeather";
    private static final String SOAP_ACTION =
"http://www.webserviceX.NET/GetWeather";
   
    //Parámetros del método
    private static final String PARAM_CITY = "CityName";
    private static final String PARAM_COUNTRY = "CountryName";
   
    //Etiquetas XML
    private static final String TAG_ROOT = "CurrentWeather";
    private static final String TAG_LOCATION = "Location";
    private static final String TAG_TIME = "Time";
    private static final String TAG_WIND = "Wind";
    private static final String TAG_VISIBILITY = "Visibility";
    private static final String TAG_TEMPERATURE = "Temperature";
    private static final String TAG_DEW = "DewPoint";
    private static final String TAG_HUMIDITY = "RelativeHumidity";
    private static final String TAG_PRESSURE = "Pressure";
    private static final String TAG_STATUS = "Status";
   
    private Context context;
    private Weather item;
      
    public EnviarSoap(Context context){
       this.context = context;
    }

    /**
     * Envío de solicitud HTTP
     * @return
     * @throws Exception
     */
    public Weather callService(String city, String country)
                                            throws Exception {
       Weather res = null;
       try {
             //Crear la petición con los parámetros
             SoapObject request = new SoapObject(NAMESPACE, METHOD_NAME);             
             request.addProperty(PARAM_CITY, city);
             request.addProperty(PARAM_COUNTRY, country);
                   
             //Envelope de tipo .NET
             SoapSerializationEnvelope envelope =
                    new SoapSerializationEnvelope(SoapEnvelope.VER11);                       
             envelope.dotNet = true;
             envelope.setOutputSoapObject(request);
                   
             //Llamada al servicio
             HttpTransportSE transporte = new HttpTransportSE(URL);                  
             transporte.call(SOAP_ACTION, envelope);
                   
             //Obtener respuesta y procesar
             SoapPrimitive response =
(SoapPrimitive) envelope.getResponse();
             res = this.procesar(response.toString());     
       }catch (Exception ex){
             ex.printStackTrace();
       }
            
       return res;
    }
      
    /**
     * Método para obtener la respuesta
     * @param is string de entrada con los datos a procesar
     * @return objeto procesado
     * @throws IOException error en la conexión
     * @throws SAXException
     */
    public Weather procesar(String in) throws IOException, SAXException{
       RootElement root = new RootElement(TAG_ROOT);
Element eLocation = root.getChild(TAG_LOCATION);
Element eTime = root.getChild(TAG_TIME);
Element eWind = root.getChild(TAG_WIND);
Element eVisibility = root.getChild(TAG_VISIBILITY);
Element eTemperature = root.getChild(TAG_TEMPERATURE);
Element eDew = root.getChild(TAG_DEW);
Element eHumidity = root.getChild(TAG_HUMIDITY);
Element ePressure = root.getChild(TAG_PRESSURE);
Element eStatus = root.getChild(TAG_STATUS);
       
//Al iniciar el elemento raíz se crea nueva instancia
root.setStartElementListener(new StartElementListener() {
             public void start(Attributes attrs) {
                    item = new Weather();
             }
});
       
//Leer cadenas al finalizar la etiqueta
eLocation.setEndTextElementListener(new EndTextElementListener() {
             public void end(String str) {
                    item.setLocation(str);
             }
});
eTime.setEndTextElementListener(new EndTextElementListener() {
             public void end(String str) {
                    item.setTime(str);
             }
});
eWind.setEndTextElementListener(new EndTextElementListener() {
             public void end(String str) {
                    item.setWind(str);
             }
});
eVisibility.setEndTextElementListener(new EndTextElementListener() {
             public void end(String str) {
                    item.setVisibility(str);
             }
});
eTemperature.setEndTextElementListener(new EndTextElementListener() {
             public void end(String str) {
                    item.setTemperature(str);
             }
});
eDew.setEndTextElementListener(new EndTextElementListener() {
             public void end(String str) {
                    item.setDewPoint(str);
             }
});
eHumidity.setEndTextElementListener(new EndTextElementListener() {
             public void end(String str) {
                    item.setRelativeHumidity(str);
             }
});
ePressure.setEndTextElementListener(new EndTextElementListener() {
             public void end(String str) {
                    item.setPressure(str);
             }
});
eStatus.setEndTextElementListener(new EndTextElementListener() {
             public void end(String str) {
                    item.setStatus(str);
             }
});
            
       //Parsear o
       Xml.parse(in, root.getContentHandler());
       return item;
    }
}

Invocar a la clase anterior desde una tarea asíncrona:
private class TareaEnvio extends AsyncTask<String, Integer, Weather> {

       private ProgressDialog dialog;
                   
       @Override
       protected void onPreExecute() {
             dialog = ProgressDialog.show(MainActivity.this,
                                  getString(R.string.app_name),
getString(R.string.buscando), true);
       }

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

       @Override
       protected void onPostExecute(Weather result) {
             dialog.dismiss();
                          
             TextView tv = (TextView) findViewById(R.id.tvResultado);
             tv.setText("Temperature: " + result.getTemperature() +
                           "\nHumidity:" + result.getRelativeHumidity());
       }
}


Referencias

http://developer.android.com/reference/android/sax/Element.html