Android

7 fev, 2012

Utilizando Android Cloud to Device Messaging com PayPal Instant Payment Notification

Publicidade

Olá, pessoal! No artigo “Manipulador de notificação de pagamento instantâneo“, ilustramos como construir um manipulador para receber e validar as notificações instantâneas de pagamento do PayPal e como construir as regras de negócio para utilizar essa informação.

Já sabemos que as notificações de pagamento são assíncronas, ou seja, elas podem ocorrer a qualquer momento. Isso significa que, se precisarmos da confirmação da mudança de status de um determinado pagamento para tomar alguma decisão relacionada ao negócio, precisaremos monitor a aplicação para ver se uma notificação chegou, ou então fazer uma consulta manual, utilizando a operação GetTransactionDetails para saber se houve tal mudança de status de pagamento.

A ideia desse artigo é utilizar o manipulador descrito no artigo supracitado em conjunto com o serviço Android Cloud to Device Messaging (C2DM) para notificar dispositivos móveis sobre qualquer mudança de status de pagamento. Dessa forma, assim que uma mensagem IPN chegar, receberemos a informação em nossos dispositivos móveis.

O Android Cloud to Device Messaging está disponível para todos os desenvolvedores, mas para utilizá-lo é preciso ir até Sign Up for Android Cloud to Device Messaging e solicitar acesso.

Bom, o processo para utilizar IPN com C2DM é bem simples:

  1. O usuário acessa a aplicação em seu dispositivo;
  2. A aplicação faz a chamada ao método de registro do C2DM;
  3. Após o registro do usuário, o Google fornece um ID único para ele, que a aplicação deverá enviar para a aplicação no lado do servidor;
  4. A aplicação do lado do servidor recebe o identificador e armazena. Esse ID será utilizado nas comunicações entre servidor e dispositivo;
  5. Quando o PayPal enviar uma mensagem IPN, utilizamos o ID de registro do usuário e fazemos um POST para o serviço C2DM com o ID da transação;
  6. O usuário recebe a notificação em seu dispositivo Android;
  7. A aplicação Android exibe as informações do pagamento.

Como podemos ver, precisaremos de uma aplicação no servidor para receber o identificador único do usuário, as notificações IPN do PayPal e despachá-las para o serviço C2DM.

Google ClientLogin

A aplicação que fará o POST ao serviço C2DM precisa estar autorizada e, para isso, utilizaremos ClientLogin. Como a implementação é feita em três linguagens diferentes, escolhi um padrão de nomenclatura de classes e pacotes para facilitar e simplificar as coisas.

A implementação do ClientLogin é bem simples e o código abaixo dará conta do recado:

Em PHP
com/paypal/ipn/google/auth/ClientLogin.php

<?php
namespace com\paypal\ipn\google\auth;

/**
 * Faz a requisição POST ao ClientLogin do Google para obter a autorização
 * de acesso para contas Google.
 * @author João Batista Neto
 */
class ClientLogin {
    /**
     * URL do serviço de autorização ClientLogin do Google.
     * @var string
     */
    const URL = 'https://www.google.com/accounts/ClientLogin';

    /**
     * Token de autorização.
     * @var    string
     */
    private $auth;

    /**
     * Obtém o token de autorização do Google utilizando ClientLogin
     * 
     * @param    string $accountType Tipo da conta que está solicitando a
     *             autorização, os valores possíveis são:
     *             <ul>
     *             <li>GOOGLE</li>
     *             <li>HOSTED</li>
     *             <li>HOSTED_OR_GOOGLE</li>
     *             </ul>
     * @param    string $Email Email completo do usuário, incluindo o domínio.
     * @param    string $Passwd Senha do usuário.
     * @param    string $source Uma string identificando a aplicação.
     * @param    string $service Nome do serviço que será solicitada a
     *             autorização.
     * @return    string O Token de autorização.
     */
    public function getAuth(
                            $accountType,
                            $Email,
                            $Passwd,
                            $source,
                            $service ) {

        if ( $this->auth === null ) {
            $curl = curl_init();

            curl_setopt( $curl , CURLOPT_URL , ClientLogin::URL );
            curl_setopt( $curl , CURLOPT_RETURNTRANSFER , 1 );
            curl_setopt( $curl , CURLOPT_POST , 1 );
            curl_setopt( $curl , CURLOPT_POSTFIELDS , http_build_query(
                array(
                    'accountType' => $accountType,
                    'Email' => $Email,
                    'Passwd' => $Passwd,
                    'source' => $source,
                    'service' => $service
                )
            ) );

            $responseStr = curl_exec( $curl );
            $responseArr = array();
            $matches = array();
            
            curl_close( $curl );

            if ( preg_match_all(
                                "/\n?(?<field>\\w+)\\=(?<value>[^\n]+)/",
                                $responseStr,
                                $matches ) ) {

                foreach ( $matches[ 'field' ] as $offset => $field ) {
                    $responseArr[ $field ] = $matches[ 'value' ][ $offset ];
                }

                if ( isset( $responseArr[ 'Auth' ] ) ) {
                    $this->auth = $responseArr[ 'Auth' ];
                }
            }
        }

        return $this->auth;
    }
}

Em Java
com/paypal/ipn/google/auth/ClientLogin.java

package com.paypal.ipn.google.auth;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Faz a requisição POST ao ClientLogin do Google para obter a autorização de
 * acesso para contas Google.
 * 
 * @author João Batista Neto
 */
public class ClientLogin {
    /**
     * URL do serviço de autorização ClientLogin do Google.
     */
    public static final String URL = "https://www.google.com/accounts/ClientLogin";

    /**
     * Token de autorização.
     */
    private String auth;

    /**
     * Obtém o token de autorização do Google utilizando ClientLogin
     * 
     * @param accountType
     *            {@link String} Tipo da conta que está solicitando a
     *            autorização, os valores possíveis são:
     *            <ul>
     *            <li>GOOGLE</li>
     *            <li>HOSTED</li>
     *            <li>HOSTED_OR_GOOGLE</li>
     *            </ul>
     * @param Email
     *            {@link String} Email completo do usuário, incluindo o domínio.
     * @param Passwd
     *            {@link String} Senha do usuário.
     * @param source
     *            {@link String} Uma string identificando a aplicação.
     * @param service
     *            {@link String} Nome do serviço que será solicitada a
     *            autorização.
     * @return {@link String} O Token de autorização.
     */
    public String getAuth(String accountType, String Email, String Passwd,
            String source, String service) {

        if (auth == null) {
            try {
                URL url = new URL(URL);
                URLConnection conn = url.openConnection();
                StringBuilder sb = new StringBuilder();

                sb.append("accountType=" + accountType);
                sb.append("&Email=" + Email);
                sb.append("&Passwd=" + Passwd);
                sb.append("&source=" + source);
                sb.append("&service=" + service);

                conn.setDoOutput(true);

                OutputStreamWriter writer = new OutputStreamWriter(
                        conn.getOutputStream());

                writer.write(sb.toString());
                writer.flush();
                writer.close();

                InputStreamReader in = new InputStreamReader(
                        conn.getInputStream());

                BufferedReader reader = new BufferedReader(in);
                sb = new StringBuilder();

                String data = null;

                while ((data = reader.readLine()) != null) {
                    String nv[] = data.split("=");

                    if (nv.length == 2 && nv[0].equals("Auth")) {
                        auth = nv[0];
                        break;
                    }
                }

                reader.close();
            } catch (IOException e) {
                Logger.getLogger(ClientLogin.class.getName()).log(Level.SEVERE,
                        null, e);
            }
        }

        return auth;
    }
}

Em C#
com/paypal/ipn/google/auth/ClientLogin.cs

namespace com.paypal.ipn.google.auth {
    using System;
    using System.IO;
    using System.Text;
    using System.Net;

    /// <summary>
    /// Faz a requisição POST ao ClientLogin do Google para obter a autorização de
    /// acesso para contas Google.
    /// </summary>
    public class ClientLogin {
        /// <summary>
        /// URL do serviço de autorização ClientLogin do Google .
        /// </summary>
        const string URL = "https://www.google.com/accounts/ClientLogin";
        
        /// <summary>
        /// Token de autorização.
        /// </summary>
        private string Auth;
        
        /// <summary>
        /// Obtém o token de autorização do Google utilizando ClientLogin.
        /// </summary>
        /// <param name="accountType">
        /// <see cref="System.String"/> Tipo da conta que está solicitando a
        /// autorização, os valores possíveis são:
        /// <ul>
        /// <li>GOOGLE</li>
        /// <li>HOSTED</li>
        /// <li>HOSTED_OR_GOOGLE</li>
        /// </ul>
        /// </param>
        /// <param name="Email">
        /// <see cref="System.String"/> Email completo do usuário, incluindo o domínio.
        /// </param>
        /// <param name="Passwd">
        /// <see cref="System.String"/> Senha do usuário.
        /// </param>
        /// <param name="source">
        /// Uma <see cref="System.String"/> identificando a aplicação.
        /// </param>
        /// <param name="service">
        /// <see cref="System.String"/> Nome do serviço que será solicitada a autorização.
        /// </param>
        /// <returns>
        /// <see cref="System.String"/> O Token de autorização.
        /// </returns>
        public string getAuth(string accountType,
                              string Email,
                              string Passwd,
                              string source,
                              string service) {

            if ( Auth == null ) {
                HttpWebRequest request = (HttpWebRequest) WebRequest.Create( URL );
                StringBuilder sb = new StringBuilder();

                sb.Append( "accountType=" + accountType );
                sb.Append( "&Email=" + Email );
                sb.Append( "&Passwd=" + Passwd );
                sb.Append( "&source=" + source );
                sb.Append( "&service=" + service );

                request.Method = "POST";
                request.ContentType = "application/x-www-form-urlencoded";                

                using ( Stream stream = request.GetRequestStream() ) {
                    UTF8Encoding encoding = new UTF8Encoding();
                    byte[] bytes = encoding.GetBytes( sb.ToString() );
                    
                    stream.Write( bytes , 0 , bytes.Length );
                }

                HttpWebResponse response = (HttpWebResponse) request.GetResponse();

                using ( Stream stream = response.GetResponseStream() ) {
                    string data;

                    using ( StreamReader reader = new StreamReader( stream , Encoding.UTF8 ) ) {
                        while ( (data = reader.ReadLine()) != null ) {
                            string[] nv = data.Split( '=' );
                            
                            if ( nv.Length == 2 && nv[0].Equals( "Auth" ) ) {
                                Auth = nv[1];
                                break;
                            }
                        }
                        
                        reader.Close();
                    }
                }
            }
            
            return Auth;
        }
    }
}

A requisição ao ClientLogin para obter o token não precisa ser feita todas as vezes. A recomendação é que o token seja armazenado pela aplicação server side, mas é importante ter uma política de atualização constante. Como disse anteriormente, o cliente abrirá a aplicação no dispositivo Android, fará o registro no C2DM e receberá um ID único do Google. Esse ID deverá ser enviado para a aplicação server side, que o utilizará para enviar mensagens para o dispositivo.

Android Cloud to Device Messaging

Para a aplicação no dispositivo Android receber uma mensagem do servidor, primeiro precisamos que aplicação envie essa mensagem para o serviço C2DM. Para isso, é preciso que:

  1. A aplicação envie a mensagem para o serviço C2DM contendo o ID de registro, mensagem e Token de autorização;
  2. Google receba a mensagem e coloque em uma fila. Se o dispositivo estiver offline, o Google armazena a mensagem;
  3. Quando o dispositivo esteja online, o Google envie a mensagem para o dispositivo;
  4. O sistema transmita a mensagem via Intent Broadcast para a aplicação. A aplicação não precisa estar em execução para receber a mensagem.
  5. A aplicação receba a mensagem e a processe adequadamente.

Vamos precisar de dois participantes, o primeiro é o responsável por enviar as mensagens para o serviço C2DM:

Em PHP
com/paypal/ipn/google/ac2dm/AndroidCloud2DeviceMessaging.php

<?php
namespace com\paypal\ipn\google\ac2dm;

/**
 * A classe AndroidCloud2DeviceMessaging faz integração com o serviço
 * Android Cloud to Device Messaging (C2DM) para enviar dados para dispositivos
 * Android.
 * 
 * @author    João Batista Neto
 */
class AndroidCloud2DeviceMessaging {
    /**
     * URL do serviço C2DM.
     * @var    string
     */
    const URL = 'https://android.apis.google.com/c2dm/send';
    
    /**
     * Conjunto de pares key=value que serão enviados para o dispositivo.
     * @var    array
     */
    private $data = array();

    /**
     * Adiciona um par key=value que será enviado para o dispositivo android.
     * 
     * @param    string $key A chave que será enviada para o dispositivo.
     * @param    string $value O valor da chave.
     * @param    string $collapseKey Chave de agrupamento que será utilizado pelo
     *             Google para evitar que várias mensagens do mesmo tipo sejam
     *             enviadas para o usuário de uma vez quando o dispositivo fique
     *             online.
     */
    public function addData( $key , $value , $collapseKey ) {
        if ( !isset( $this->data[ $collapseKey ] ) ) {
            $this->data[ $collapseKey ] = array();
        }
        
        $this->data[ $collapseKey ][ 'data.' . $key ] = $value;
    }
    
    /**
     * Remove todas os pares key=value.
     */
    public function clear() {
        $this->data = array();
    }
    
    /**
     * Envia a mensagem para o servidor C2DM.
     * 
     * @param    string $registrationId ID de registro do dispositivo android.
     * @param    string $auth Token de autorização
     * @see        com\google\auth\ClientLogin::getAuth()
     */
    public function send( $registrationId , $auth ) {
        $curl = curl_init();

        curl_setopt( $curl , CURLOPT_URL , AndroidCloud2DeviceMessaging::URL );
        curl_setopt( $curl , CURLOPT_SSL_VERIFYPEER , false );
        curl_setopt( $curl , CURLOPT_RETURNTRANSFER , 1 );
        curl_setopt( $curl , CURLOPT_POST , 1 );
        curl_setopt( $curl , CURLOPT_HTTPHEADER , array(
            'Authorization: GoogleLogin auth=' . $auth
        ) );
        
        foreach ( $this->data as $collapseKey => $data ) {
            $data[ 'registration_id' ] = $registrationId;
            $data[ 'collapse_key' ] = $collapseKey;
            
            curl_setopt( $curl , CURLOPT_POSTFIELDS , http_build_query($data));
            
            var_dump( curl_exec( $curl ) );
        }
        
        curl_close( $curl );
    }
}

Em Java
com/paypal/ipn/google/ac2dm/AndroidCloud2DeviceMessaging.java

package com.paypal.ipn.google.ac2dm;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;

/**
 * A classe AndroidCloud2DeviceMessaging faz integração com o serviço Android
 * Cloud to Device Messaging (C2DM) para enviar dados para dispositivos Android.
 * 
 * @author João Batista Neto
 */
public class AndroidCloud2DeviceMessaging {
    /**
     * O certificado do Google não cobre android.apis.google.com, então será
     * preciso verificar e fazer a validação do certificado manualmente.
     */
    private static final String HOSTNAME = "android.apis.google.com";

    /**
     * URL do serviço C2DM.
     */
    public static final String URL = "https://android.apis.google.com/c2dm/send";

    /**
     * Conjunto de pares key=value que serão enviados para o dispositivo.
     */
    private Map<String, Map<String, String>> data;

    public AndroidCloud2DeviceMessaging() {
        data = new HashMap<String, Map<String, String>>();
    }

    /**
     * Adiciona um par key=value que será enviado para o dispositivo android.
     * 
     * @param key {@link String}
     *            A chave que será enviada para o dispositivo.
     * @param value {@link String}
     *            O valor da chave.
     * @param collapseKey {@link String}
     *            Chave de agrupamento que será utilizado pelo
     *            Google para evitar que várias mensagens do mesmo tipo sejam
     *            enviadas para o usuário de uma vez quando o dispositivo fique
     *            online.
     */
    public void addData(String key, String value, String collapseKey) {
        Map<String, String> map;

        if ((map = data.get(collapseKey)) == null) {
            map = new HashMap<String, String>();

            data.put(collapseKey, map);
        }

        map.put(key, value);
    }

    /**
     * Remove todas os pares key=value.
     */
    public void clear() {
        data.clear();
    }

    /**
     * Envia a mensagem para o servidor C2DM.
     * 
     * @param registrationId {@link String} ID de registro do dispositivo
     *        android.
     * @param auth {@link String} Token de autorização.
     * @see {@link com.paypal.ipn.google.auth.ClientLogin#getAuth}
     */
    public void send(String registrationId, String auth) {
        try {
            URL url = new URL(URL);
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();

            conn.setDoOutput(true);
            conn.setHostnameVerifier(new HostnameVerifier() {
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    return hostname.equals(HOSTNAME);
                }
            });
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Content-Type",
                    "application/x-www-form-urlencoded");
            conn.setRequestProperty("Authorization", "GoogleLogin auth=" + auth);

            for (String collapseKey : data.keySet()) {
                StringBuilder sb = new StringBuilder();
                Map<String, String> nv = data.get(collapseKey);

                sb.append("registration_id=" + registrationId);
                sb.append("&collapse_key=" + collapseKey);

                for (Entry<String, String> entry : nv.entrySet()) {
                    sb.append("&data." + entry.getKey());
                    sb.append("=");
                    sb.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
                }

                OutputStreamWriter writer = new OutputStreamWriter(
                        conn.getOutputStream());

                writer.write(sb.toString());
                writer.flush();
                writer.close();

                if (conn.getResponseCode() != 200) {
                    Logger.getLogger(
                            AndroidCloud2DeviceMessaging.class.getName()).log(
                            Level.SEVERE, conn.getResponseMessage());
                }
            }
        } catch (IOException e) {
            Logger.getLogger(AndroidCloud2DeviceMessaging.class.getName()).log(
                    Level.SEVERE, null, e);
        }
    }
}

Em C#
com/google/c2dm/AndroidCloud2DeviceMessaging.cs

namespace com.paypal.ipn.google.ac2dm {
    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.IO;
    using System.Net;
    using System.Net.Security;
    using System.Security.Cryptography.X509Certificates;
    using System.Text;
    using System.Web;

    /// <summary>
    /// A classe AndroidCloud2DeviceMessaging faz integração com o serviço Android
    /// Cloud to Device Messaging (C2DM) para enviar dados para dispositivos Android.
    /// 
    /// Author: João Batista Neto
    /// </summary>
    public class AndroidCloud2DeviceMessaging {
        /// <summary>
        /// URL do serviço C2DM.
        /// </summary>
        public const String URL = "https://android.apis.google.com/c2dm/send";
        
        /// <summary>
        /// Conjunto de pares key=value que serão enviados para o dispositivo.
        /// </summary>
        private Dictionary<string,NameValueCollection> data;
        
        public AndroidCloud2DeviceMessaging () {
            data = new Dictionary<string, NameValueCollection>();
        }
        
        /// <summary>
        /// Adiciona um par key=value que será enviado para o dispositivo android.
        /// </summary>
        /// <param name="key">
        /// <see cref="System.String"/> A chave que será enviada para o dispositivo.
        /// </param>
        /// <param name="value">
        /// <see cref="System.String"/> O valor da chave.
        /// </param>
        /// <param name="collapseKey">
        /// <see cref="System.String"/> Chave de agrupamento que será utilizado pelo
        /// Google para evitar que várias mensagens do mesmo tipo sejam
        /// enviadas para o usuário de uma vez quando o dispositivo fique
        /// online.
        /// </param>
        public void addData(string key,string value,string collapseKey) {
            NameValueCollection nv;
            
            if ( !data.ContainsKey(collapseKey) ) {
                nv = new NameValueCollection();
                
                data.Add( collapseKey , nv );
            } else {
                nv = data[collapseKey];
            }
            
            nv.Set(key,value);
        }
        
        /// <summary>
        /// Remove todas os pares key=value.
        /// </summary>
        public void clear() {
            data.Clear();
        }
        
        /// <summary>
        /// Envia a mensagem para o servidor C2DM.
        /// </summary>
        /// <param name="registrationId">
        /// <see cref="System.String"/> ID de registro do dispositivo android.
        /// </param>
        /// <param name="auth">
        /// <see cref="System.String"/> Token de autorização.
        /// </param>
        /// <seealso cref="com.paypal.ipn.google.auth.ClientLogin#getAuth"/>
        public void send( string registrationId,string auth ) {
            ServicePointManager.ServerCertificateValidationCallback = Validator;
            HttpWebRequest request = (HttpWebRequest) WebRequest.Create( URL );
            
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";
            request.Headers.Add( "Authorization" , "GoogleLogin auth=" + auth );
            
            foreach ( string collapseKey in data.Keys ) {
                StringBuilder sb = new StringBuilder();
                NameValueCollection nv = data[collapseKey];
                
                sb.Append("registration_id=" + registrationId);
                sb.Append("&collapse_key=" + collapseKey);
                
                foreach ( string key in nv.AllKeys ) {
                    sb.Append("&data." + key );
                    sb.Append("=");
                    sb.Append(HttpUtility.UrlEncode(nv[key]));
                }
                
                using ( Stream stream = request.GetRequestStream() ) {
                    UTF8Encoding encoding = new UTF8Encoding();
                    byte[] bytes = encoding.GetBytes( sb.ToString() );
                    
                    stream.Write( bytes , 0 , bytes.Length );
                }
            }
        }
        
        /// <summary>
        /// O certificado do Google não cobre android.apis.google.com, então será
        /// preciso verificar e fazer a validação do certificado manualmente.
        /// </summary>
        /// <param name="sender">
        /// <see cref="System.Object"/>
        /// </param>
        /// <param name="certificate">
        /// <see cref="X509Certificate"/>
        /// </param>
        /// <param name="chain">
        /// <see cref="X509Chain"/>
        /// </param>
        /// <param name="sslPolicyErrors">
        /// <see cref="SslPolicyErrors"/>
        /// </param>
        /// <returns>
        /// <see cref="System.Boolean"/> Retornamos
        /// </returns>
        bool Validator(
            object sender,
            X509Certificate certificate,
            X509Chain chain,
            SslPolicyErrors sslPolicyErrors ) {
            return true;
        }
    }
}

Com isso, definimos o manipulador de notificação instantânea de pagamento para receber as mensagens do PayPal, verificá-las e só então despachá-las para o dispositivo Android:

Instant payment notification

Como a validação da mensagem é recorrente para qualquer manipulador, podemos ter um participante que faça essa validação e deixe a parte de regras específicas de negócio para um manipulador abstrato:

Em PHP
com/paypal/ipn/InstantPaymentNotification.php

<?php
namespace com\paypal\ipn;

use \BadMethodCallException;

/**
 * Observador de Notificações de Pagamento Instantâneo.
 * 
 * @author    João Batista Neto
 */
class InstantPaymentNotification {
    /**
     * Endpoint de produção.
     * @var    string
     */
    const HOST = 'https://www.paypal.com';
    
    /**
     * Endpoint de produção.
     * @var    string
     */
    const SANDBOX_HOST = 'https://www.sandbox.paypal.com';
    
    /**
     * @var string
     */
    private $endpoint = InstantPaymentNotification::HOST;
    
    /**
     * @var com\paypal\ipn\IPNHandler
     */
    private $ipnHandler;
    
    /**
     * Constroi o observador no notificação instantânea de pagamento informando
     * o ambiente que será utilizado para validação.
     * 
     * @param    boolean $sandbox Define se será utilizado o Sandbox
     * @throws    InvalidArgumentException
     */
    public function __construct( $sandbox = false ) {
        if ( !!$sandbox ) {
            $this->endpoint = InstantPaymentNotification::SANDBOX_HOST;
        }
        
        $this->endpoint .= '/cgi-bin/webscr?cmd=_notify-validate';
    }
    
    /**
     * Aguarda por notificações de pagamento instantânea; Caso uma nova
     * notificação seja recebida, faz a verificação e notifica um manipulador
     * com o status (verificada ou não) e a mensagem recebida.
     * 
     * @param    array $post Dados postatos pelo PayPal.
     * @see     InstantPaymentNotification::setIPNHandler()
     * @throws  BadMethodCallException Caso o método seja chamado antes de um
     *             manipulador ter sido definido ou nenhum email de recebedor
     *             tenha sido informado.
     */
    public function listen( array $post ) {
        if ( $this->ipnHandler !== null && isset( $post[ 'receiver_email' ] ) ) {
            $curl = curl_init ();
            
            curl_setopt( $curl, CURLOPT_URL, $this->endpoint );
            curl_setopt( $curl, CURLOPT_SSL_VERIFYPEER, false );
            curl_setopt( $curl, CURLOPT_RETURNTRANSFER, 1 );
            curl_setopt( $curl, CURLOPT_POST, 1 );
            curl_setopt( $curl, CURLOPT_POSTFIELDS, http_build_query(
                $post
            ) );
            
            $response = curl_exec( $curl );
            $error = curl_error( $curl );
            $errno = curl_errno( $curl );
            
            curl_close ( $curl );
            
            if ( empty( $error ) && $errno == 0 ) {
                $this->ipnHandler->handle(
                    $response == 'VERIFIED', $post
                );
            }
        }
    }
    
    /**
     * Define o objeto que irá manipular as notificações de pagamento
     * instantâneas enviadas pelo PayPal.
     * 
     * @param    com\paypal\ipn\IPNHandler $ipnHandler
     */
    public function setIPNHandler( IPNHandler $ipnHandler ) {
        $this->ipnHandler = $ipnHandler;
    }
}

com/paypal/ipn/IPNHandler.php

<?php
namespace com\paypal\ipn;

/**
 * Interface para definição de um manipulador de notificação
 * de pagamento instantânea.
 * 
 * @author João Batista Neto
 */
interface IPNHandler {
    /**
     * Manipula uma notificação de pagamento instantânea recebida pelo PayPal.
     * 
     * @param    boolean $isVerified Identifica que a mensagem foi verificada
     *             como tendo sido enviada pelo PayPal.
     * @param    array $message Mensagem completa enviada pelo PayPal.
     */
    public function handle( $isVerified, array $message );
}

E a implementação que fará o trabalho:

com/paypal/ipn/sample/IPNToAC2DMHandler.php

<?php
namespace com\paypal\ipn\sample;

use com\paypal\ipn\google\ac2dm\AndroidCloud2DeviceMessaging;
use com\paypal\ipn\IPNHandler;

/**
 * Manipulador de notificação instantânea de pagamento que envia a
 * mensagem para dispositivos Android.
 * 
 * @author    João Batista Neto
 */
class IPNToAC2DMHandler implements IPNHandler {
    /**
     * @var    com\paypal\ipn\sample\SampleModel
     */
    private $model;
    
    public function __construct( SampleModel $model ) {
        $this->model = $model;
    }
    
    /* (non-PHPdoc)
     * @see com\paypal\ipn\IPNHandler#handle()
     */
    public function handle( $isVerified, array $message ) {
        if ( $isVerified && isset( $message[ 'receiver_email' ] ) ) {
            $ac2dm = new AndroidCloud2DeviceMessaging();
            
            foreach ( $message as $field => $value ) {
                $ac2dm->addData( $field , $value, 'ipn' );
            }
            
            $ac2dm->send(
                $this->model->getRegistrationId( $message['receiver_email' ] ),
                $this->model->getAuth() );
        }
    }
}

Em Java
com/paypal/ipn/InstantPaymentNotification.java

package com.paypal.ipn;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;

/**
 * Observador de Notificações de Pagamento Instantâneo.
 * 
 * @author João Batista Neto
 */
public class InstantPaymentNotification {
    /**
     * HOST de produção.
     */
    private static final String HOST = "https://www.paypal.com";

    /**
     * HOST do Sandbox
     */
    private static final String SANDBOX_HOST = "https://www.sandbox.paypal.com";

    /**
     * Endpoint que será utilizado na validação
     */
    private String endpoint = InstantPaymentNotification.HOST;

    private IPNHandler ipnHandler;

    public InstantPaymentNotification() {
        this(false);
    }

    /**
     * Constroi o observador no notificação instantânea de pagamento informando
     * o ambiente que será utilizado para validação.
     * 
     * @param sandbox
     *            {@link Boolean} Define se será utilizado o ambiente de
     *            produção ou o Sandbox.
     */
    public InstantPaymentNotification(Boolean sandbox) {
        if (sandbox) {
            endpoint = InstantPaymentNotification.SANDBOX_HOST;
        }

        endpoint += "/cgi-bin/webscr?cmd=_notify-validate";
    }

    /**
     * Aguarda por notificações de pagamento instantânea; Caso uma nova
     * notificação seja recebida, faz a verificação e notifica um manipulador
     * com o status (verificada ou não) e a mensagem recebida.
     * 
     * @param post
     *            {@link HttpServletRequest} Dados postatos pelo PayPal.
     * @see {@link InstantPaymentNotification#setIPNHandler()}
     */
    public void listen(HttpServletRequest post) {
        if (ipnHandler != null && post.getParameter("receiver_email") != null) {
            try {
                BufferedReader reader = post.getReader();
                StringBuffer sb = new StringBuffer();
                String data;

                while ((data = reader.readLine()) != null) {
                    sb.append(data);
                }

                URL url = new URL(endpoint);
                URLConnection conn = url.openConnection();

                conn.setDoOutput(true);

                OutputStreamWriter writer = new OutputStreamWriter(
                        conn.getOutputStream());

                writer.write(sb.toString());
                writer.flush();
                writer.close();

                InputStreamReader in = new InputStreamReader(
                        conn.getInputStream());

                reader = new BufferedReader(in);
                sb = new StringBuffer();

                data = null;

                while ((data = reader.readLine()) != null) {
                    sb.append(data);
                }

                ipnHandler.handle(sb.toString().equals("VERIFIED"), post);
            } catch (IOException e) {
                Logger.getLogger(InstantPaymentNotification.class.getName())
                        .log(Level.SEVERE, null, e);
            }
        }
    }

    /**
     * Define o objeto que irá manipular as notificações de pagamento
     * instantâneas enviadas pelo PayPal.
     * 
     * @param ipnHandler
     *            {@link IPNHandler}
     */
    public void setIPNHandler(IPNHandler ipnHandler) {
        this.ipnHandler = ipnHandler;
    }
}

com/paypal/ipn/IPNHandler.java

package com.paypal.ipn;

import javax.servlet.http.HttpServletRequest;

/**
 * Interface para definição de um manipulador de notificação de pagamento
 * instantânea.
 * 
 * @author João Batista Neto
 */
public interface IPNHandler {
    /**
     * Manipula uma notificação de pagamento instantânea recebida pelo PayPal.
     * 
     * @param isVerified
     *            {@link Boolean} Identifica que a mensagem foi verificada como
     *            tendo sido enviada pelo PayPal.
     * @param message
     *            {@link HttpServletRequest} Mensagem completa enviada pelo
     *            PayPal.
     */
    public void handle(Boolean isVerified, HttpServletRequest message);
}

com/paypal/ipn/sample/IPNToAC2DMHandler.java

package com.paypal.ipn.sample;

import java.util.Enumeration;

import javax.servlet.http.HttpServletRequest;

import com.paypal.ipn.IPNHandler;
import com.paypal.ipn.google.ac2dm.AndroidCloud2DeviceMessaging;

/**
 * Manipulador de notificação instantânea de pagamento que envia a mensagem para
 * dispositivos Android.
 * 
 * @author João Batista Neto
 */
public class IPNToAC2DMHandler implements IPNHandler {
    private SampleModel model;

    public IPNToAC2DMHandler(SampleModel model) {
        this.model = model;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.paypal.ipn.IPNHandler#handle()
     */
    @SuppressWarnings("rawtypes")
    @Override
    public void handle(Boolean isVerified, HttpServletRequest message) {
        if (isVerified) {
            AndroidCloud2DeviceMessaging ac2dm = new AndroidCloud2DeviceMessaging();
            Enumeration fields = message.getParameterNames();

            while (fields.hasMoreElements()) {
                String field = (String) fields.nextElement();

                ac2dm.addData(field, message.getParameter(field), "ipn");
            }

            ac2dm.send(model.getRegistrationId(message
                    .getParameter("receiver_email")), model.getAuth());
        }
    }
}

Em C#
com/paypal/ipn/InstantPaymentNotification.cs

namespace com.paypal.ipn {
    using System;
    using System.IO;
    using System.Net;
    using System.Text;
    using System.Web;
    using System.Web.Mvc;

    /// <summary>
    /// Observador de Notificações de Pagamento Instantâneo.
    /// 
    /// Author: João Batista Neto
    /// </summary>
    public class InstantPaymentNotification {
        /// <summary>
        /// Endpoint de produção.
        /// </summary>
        public const string HOST = "https://www.paypal.com";

        /// <summary>
        /// Endpoint do Sandbox
        /// </summary>
        public const string SANDBOX_HOST = "https://www.sandbox.paypal.com";

        /// <summary>
        /// Endpoint que será utilizado na verificação
        /// </summary>
        private string endpoint = InstantPaymentNotification.HOST;

        /// <summary>
        /// Manipulador de notificação instantânea de pagamento.
        /// </summary>
        private IPNHandler handler;

        public InstantPaymentNotification() :this(false) {}

        /// <summary>
        /// Constroi o observador no notificação instantânea de pagamento informando
        /// o ambiente que será utilizado para validação.
        /// </summary>
        /// <param name="sandbox">
        /// <see cref="System.Boolean"/> Define se será utilizado o ambiente de
        /// produção ou o Sandbox.
        /// </param>
        public InstantPaymentNotification ( bool sandbox ) {
            if ( sandbox ) {
                endpoint = InstantPaymentNotification.SANDBOX_HOST;
            }
            
            endpoint += "/cgi-bin/webscr?cmd=_notify-validate";
        }

        /// <summary>
        /// Aguarda por notificações de pagamento instantânea; Caso uma nova
        /// notificação seja recebida, faz a verificação e notifica um manipulador
        /// com o status (verificada ou não) e a mensagem recebida.
        /// </summary>
        /// <param name="post">
        /// A <see cref="FormCollection"/> com os dados postados pelo PayPal
        /// </param>
        /// <seealso cref="InstantPaymentNotification#setIPNHandler()"/>
        public void listem( FormCollection post ) {
            if ( handler != null && post[ "receiver_email" ] != null ) {
                HttpWebRequest request = (HttpWebRequest) WebRequest.Create( endpoint );
            
                request.Method = "POST";
                request.ContentType = "application/x-www-form-urlencoded";
                
                StringBuilder sb = new StringBuilder();
            
                foreach ( string field in post ) {
                    if ( sb.Length != 0 ) {
                        sb.Append( "&" );
                    }
                    
                    sb.Append( field );
                    sb.Append( "=" );
                    sb.Append( post[ field ] );
                }
                
                using ( Stream stream = request.GetRequestStream() ) {
                    UTF8Encoding encoding = new UTF8Encoding();
                    byte[] bytes = encoding.GetBytes( sb.ToString() );
                    
                    stream.Write( bytes , 0 , bytes.Length );
                }
                
                HttpWebResponse response = (HttpWebResponse) request.GetResponse();

                using ( Stream stream = response.GetResponseStream() ) {
                    using ( StreamReader reader = new StreamReader( stream , Encoding.UTF8 ) ) {
                        string data = reader.ReadToEnd();
                        
                        reader.Close();
                        
                        handler.handle( data.Equals( "VERIFIED" ) , post );
                    }
                }
            }
        }

        /// <summary>
        /// Define o objeto que irá manipular as notificações de pagamento
        /// instantâneas enviadas pelo PayPal.
        /// </summary>
        /// <param name="handler">
        /// <see cref="IPNHandler"/>
        /// </param>
        public void setIPNHandler( IPNHandler handler ) {
            this.handler = handler;
        }
    }
}

com/paypal/ipn/IPNHandler.cs

namespace com.paypal.ipn {
    using System;
    using System.Web.Mvc;
    
    /// <summary>
    /// Interface para definição de um manipulador de notificação
    /// de pagamento instantânea.
    /// 
    /// Author: João Batista Neto
    /// </summary>
    public interface IPNHandler {
        /// <summary>
        /// Manipula uma notificação de pagamento instantânea recebida pelo PayPal.
        /// </summary>
        /// <param name="isVerified">
        /// <see cref="System.Boolean"/> isVerified Identifica que a mensagem foi
        /// verificada como tendo sido enviada pelo PayPal.
        /// </param>
        /// <param name="form">
        /// <see cref="FormCollection"/> Mensagem completa enviada pelo PayPal.
        /// </param>
        void handle( bool isVerified , FormCollection message );
    }
}

com/paypal/ipn/sample/IPNToAC2DMHandler.cs

namespace com.paypal.ipn.sample {
    using System;
    using System.Web.Mvc;
    using com.paypal.ipn;
    using com.paypal.ipn.google.ac2dm;

    /// <summary>
    /// Manipulador de notificação instantânea de pagamento que envia a
    /// mensagem para dispositivos Android.
    /// </summary>
    public class IPNToAC2DMHandler :IPNHandler {
        private SampleModel model;
        
        public IPNToAC2DMHandler ( SampleModel model ) {
            this.model = model;
        }
        
        /// <summary>
        /// Envia as notificações instantânea de pagamento para dispositivos
        /// Android caso a mensagem tenha sido verificada pelo PayPal.
        /// </summary>
        /// <param name="isVerified">
        /// <see cref="System.Boolean"/> TRUE caso o PayPal tenha verificado
        /// a mensagem.
        /// </param>
        /// <param name="message">
        /// <see cref="FormCollection"/> A mensagem enviada pelo PayPal
        /// </param>
        public void handle( bool isVerified , FormCollection message ) {
            if ( isVerified ) {
                AndroidCloud2DeviceMessaging ac2dm = new AndroidCloud2DeviceMessaging();
                
                foreach ( string field in message ) {
                    ac2dm.addData( field , message[field] , "ipn" );
                }
                
                ac2dm.send(model.getRegistrationId(message["receiver_email"]),
                           model.getAuth() );
            }
        }
    }
}

Aplicação Android

No Eclipse nós criamos um novo “Projeto Android”:

new project

O Android Cloud to Device Messaging está disponível desde a versão 2.2, então utilizem a que for mais adequada para o projeto.

target

Em seguida, algumas configurações sobre o projeto:

application

E pronto, temos o esqueleto do projeto Android:

project

Para poder utilizar o serviço C2DM, precisaremos adicionar algumas permissões no AndroidManifest.xml. Ele ficará assim:

AndroidManifest.xml

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

    <uses-sdk android:minSdkVersion="11" />

    <permission
        android:name="com.paypal.ipn.permission.C2D_MESSAGE"
        android:protectionLevel="signature" />

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

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".RegisterActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

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

        <!--
             Apenas servidores C2DM podem enviar mensagens para a aplicação.
             Se a permissão não estiver definida, qualquer outra aplicação pode
             gerar a mensagem.
        -->
        <receiver
            android:name="com.google.android.c2dm.C2DMBroadcastReceiver"
            android:permission="com.google.android.c2dm.permission.SEND" >

            <!--
                 Recebe a mensagem
            -->
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />

                <category android:name="com.paypal.ipn" />
            </intent-filter>

            <!--
                 Recebe o ID de registro
            -->
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.REGISTRATION" />

                <category android:name="com.paypal.ipn" />
            </intent-filter>
        </receiver>
    </application>
</manifest>

Nossa interface de usuário é bastante simples:

layout/register.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:gravity="center"
    android:orientation="vertical" >

    <ImageView
        android:id="@+id/imageViewBrand"
        android:contentDescription="@string/app_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingBottom="30dp"
        android:src="@drawable/ic_brand" />

    <EditText
        android:id="@+id/textEmailAddress"
        android:layout_width="676dp"
        android:layout_height="wrap_content"
        android:hint="@string/register_email"
        android:inputType="textEmailAddress" >

        <requestFocus />
    </EditText>

    <Button
        android:id="@+id/registerButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="register"
        android:text="@string/register_button" />
</LinearLayout>

Assim como o processo de registro:

// Usamos a API Intent para para obter o ID de registro
Intent regIntent = new Intent(  "com.google.android.c2dm.intent.REGISTER" );

// Identificamos a aplicação
registrationIntent.putExtra("app",
                PendingIntent.getBroadcast(context, 0, new Intent(), 0));

// Identificamos a conta que o servidor usará para enviar as notificações
registrationIntent.putExtra("sender", senderId);
context.startService(registrationIntent);

Como a Google já providenciou um framework para esse trabalho, precisamos apenas utilizar o código disponibilizado:

com/paypal/ipn/RegisterActivity.java

package com.paypal.ipn;

import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnKeyListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import com.google.android.c2dm.C2DMessaging;

public class RegisterActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.register);

        ((EditText) findViewById(R.id.textEmailAddress))
                .setOnKeyListener(new OnKeyListener() {
                    @Override
                    public boolean onKey(View v, int keyCode, KeyEvent e) {
                        if (e.getAction() == KeyEvent.ACTION_DOWN
                                && keyCode == KeyEvent.KEYCODE_ENTER) {
                            register(v);
                        }
                        return false;
                    }
                });
    }

    public void register(View view) {
        Toast.makeText(this, "Starting", Toast.LENGTH_LONG).show();
        
        ((EditText) findViewById(R.id.textEmailAddress)).setEnabled(false);
        ((Button) findViewById(R.id.registerButton)).setEnabled(false);

        String email = ((EditText) findViewById(R.id.textEmailAddress))
                .getText().toString();

        SharedPreferences prefs = getSharedPreferences(getResources()
                .getString(R.string.app_id), Context.MODE_PRIVATE);

        Editor editor = prefs.edit();
        editor.putString("receiverEmail", email);
        editor.commit();

        C2DMessaging.register(this, email);
    }
}

E o responsável por receber as notificações:

com/paypal/ipn/C2DMReceiver.java

package com.paypal.ipn;

import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;

import com.google.android.c2dm.C2DMBaseReceiver;

public class C2DMReceiver extends C2DMBaseReceiver {
    public C2DMReceiver() {
        super("brasil@x.com");
    }

    /**
     * Recebe o id de registro e envia para a nossa aplicação no servidor, que
     * manipulará as notificações instantânea de pagamento.
     */
    @Override
    public void onRegistered(Context context, String registrationId)
            throws java.io.IOException {

        SharedPreferences prefs = context.getSharedPreferences(
                context.getString(R.string.app_id), Context.MODE_PRIVATE);

        URL url = new URL(context.getString(R.string.register_server));

        URLConnection conn = url.openConnection();

        StringBuilder sb = new StringBuilder();

        sb.append("registrationId=" + registrationId);
        sb.append("&receiverEmail=" + prefs.getString("receiverEmail", ""));

        conn.setDoOutput(true);

        OutputStreamWriter writer = new OutputStreamWriter(
                conn.getOutputStream());

        writer.write(sb.toString());
        writer.flush();
        writer.close();

        Editor editor = prefs.edit();

        editor.putString("registrationId", registrationId);
        editor.commit();
    };

    /**
     * Mensagem recebida!
     */
    @Override
    protected void onMessage(Context context, Intent intent) {
        String ns = Context.NOTIFICATION_SERVICE;

        // Pegamos o gerenciados de notificação
        NotificationManager notificationManager = (NotificationManager) getSystemService(ns);

        // Ícone que aparecerá na notificação
        int icon = R.drawable.ic_launcher;
        
        // Mensagem que aparecerá quando a notificação aparecer.
        CharSequence tickerText = context
                .getString(R.string.ipn_notification_ticker);
        long when = System.currentTimeMillis();

        // Criamos a notificação, definindo o título, texto, som, etc
        Notification notification = new Notification(icon, tickerText, when);

        CharSequence contentTitle = context
                .getString(R.string.ipn_notification_title);
        CharSequence contentText = context
                .getString(R.string.ipn_notification_text);
        Intent notificationIntent = new Intent(this, RegisterActivity.class);
        PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
                notificationIntent, 0);

        notification.defaults |= Notification.DEFAULT_SOUND;
        notification.setLatestEventInfo(context, contentTitle, contentText,
                contentIntent);

        // Exibimos a notificação
        notificationManager.notify(1, notification);

    }

    @Override
    public void onError(Context context, String errorId) {
        // opz
    }
}

É isso! Para testar o recebimento das notificações no dispositivo Android basta ir até a página Testando Notificações Instantâneas de Pagamento, no PayPal Sandbox

Abaixo algumas telas capturadas do dispositivo rodando o exemplo:

ipn c2dm application 1ipn c2dm application 2ipn c2dm application 3ipn c2dm application 4

O código utilizado no exemplo está disponível no perfil do PayPal X Brasil no github: PayPal X Brasil