domingo, 23 de fevereiro de 2014

Google Cloud Messaging

Olá povo,

Depois de um bom tempo sem postar, resolvi voltar com um tópico muito bacana: Google Cloud Messaging, ou simplesmente GCM. Esse serviço gratuito permite que sua aplicação web envie mensagens de notificação para sua aplicação Android. Nesse tutorial vou tentar mostrar como aplicar esse recurso na sua aplicação.

Cada aplicação deve ter um ID associado. Com esse ID, a aplicação solicita um token para o servidor do GCM, esse token é único por dispositivo+aplicação. De posse desse token, a aplicação Android deve enviar esse valor para a aplicação servidora. Esse token deve ser armazenado de alguma forma no servidor, pois quando ele precisar/desejar enviar uma mensagem, o fará através de uma requisição via POST para o servidor do GCM que se encarregará de despachá-las para os dispositivos Android.
Nada muito complexo não é? :) Agora vamos para a prática!

Gerando as chaves de acesso

Acesse o Google Developers Console e crie um novo projeto. Precisaremos do número do projeto para podermos registrar a app+device no servidor do GCM.
Uma vez criado o projeto, selecione a opção APIs dentro do menu APIs & auth. Agora, habilite o Google Cloud Messaging.

Agora vamos gerar o SHA1 que será usado para gerar a chave para acessarmos o serviço do GCM. Digite o comando abaixo na pasta bin do JDK (Java Development Kit) (ou em qualquer lugar se esse caminho estiver no PATH do seu sistema operacional).

keytool -list -v -keystore ~/.android/debug.keystore

Aqui, estou usando o debug.keystore que por padrão fica no diretório usuário/.android (no Windows seria C:\Users\usuario). O próximo passo é criar duas chaves, uma para ser usada pela app Android e outra para app Web. Para isso, selecione a opção Credentials no menu APIs & auth. Clique na opção Create new key e selecione Android key. Na janela que for exibida, digite o SHA1 gerado pelo comando keytool, seguido de ";" e o pacote do projeto.
Exemplo:
41:F7:05:0F:72:29:99:28:55:BA:4B:79:AB:27:BC:7A:62:03:85:2C;ngvl.android.exemplogcm

Clique novamente em Create new key e selecione Server key. Aqui podemos filtrar o uso dessa chave por IP. Na prática utilizaríamos o IP real do servidor, mas para esse exemplo, não usaremos. É só confirmar para criar a nova chave.

Aplicação Cliente

Para a aplicação cliente primeiro temos que adicionar a referência ao projeto do Google Play Services, disponível na pasta ANDROID_SDK/extras/google/google_play_services/libproject. Depois adicione a dependência ao seu projeto.
No AndroidManifest.xml do seu projeto você deve adicionar as seguintes informações:
<manifest
  package="ngvl.android.exemplogcm"
  ...>
  <uses-permission android:name=
    "com.google.android.c2dm.permission.RECEIVE"/>
  <uses-permission android:name=
    "android.permission.INTERNET"/>
  <uses-permission android:name=
    "android.permission.GET_ACCOUNTS"/>
  <uses-permission android:name=
    "android.permission.WAKE_LOCK"/>

  <permission android:name=
    "ngvl.android.exemplogcm.permission.C2D_MESSAGE"
    android:protectionLevel="signature"/>

  <uses-permission android:name=
    "ngvl.android.exemplogcm.permission.C2D_MESSAGE"/>

  <application
    ...>

    ...

    <receiver 
      android:name=
        "ngvl.android.exemplogcm.GcmBroadcastReceiver"
      android:permission=
        "com.google.android.c2dm.permission.SEND">
      <intent-filter>
        <action 
          android:name=
            "com.google.android.c2dm.intent.RECEIVE"/>
        <category 
          android:name="ngvl.android.exemplogcm"/>
      </intent-filter>
    </receiver>

    <service 
      android:name=
        "ngvl.android.exemplogcm.GcmIntentService"/>
        
    <meta-data 
      android:name="com.google.android.gms.version"      
      android:value="@integer/google_play_services_version"/>
  </application>
</manifest>
Basicamente, adicionamos algumas permissões:
- Para receber mensagens do GCM;
- Acessar internet;
- Ter acesso às contas cadastradas do aparelho (para usar o GCM, temos que ter uma conta Google criada no aparelho);
- Wake Lock para ligar a tela quando chegar uma notificação;
Além disso, declaramos um Broadcast Receiver que recebe as mensagens e delega o tratamento para o Intent Service que também adicionamos.

Abaixo temos o código do BroadcastReceiver. Ele é um WakefulBroadcastReceiver para evitar que o aparelho apague a tela enquanto ele está executando, que nesse caso é receber a notificação. Quando uma mensagem GCM é recebida esse componente é disparado, e então delega o tratamento para o GcmIntentService.
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);
  }
}
O IntentService é um serviço especial do Android onde cada chamada ao mesmo é empilhada e executada sequencialmente em uma Thread separada. A requisição é tratada no método onHandleIntent e nele checamos se a mensagem que estamos recebendo é uma mensagem de dados (o GCM pode enviar mensagens de erro). Se for uma mensagem de dados, disparamos uma notificação.
public class GcmIntentService extends IntentService {
  public static final int NOTIFICATION_ID = 1;
  private NotificationManager mNotificationManager;
  private NotificationCompat.Builder builder;

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

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

    String messageType = gcm.getMessageType(intent);

    if (!extras.isEmpty()) {

      if (GoogleCloudMessaging.
          MESSAGE_TYPE_MESSAGE.equals(messageType)) {

          sendNotification("Mensagem: " + 
            extras.toString());
      }
    }
    GcmBroadcastReceiver.completeWakefulIntent(intent);
  }

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

    PendingIntent contentIntent = 
      PendingIntent.getActivity(
        this, 0, new Intent(), 0);

    NotificationCompat.Builder mBuilder =
        new NotificationCompat.Builder(this)
      .setSmallIcon(R.drawable.ic_launcher)
      .setContentTitle("GCM Notification")
      .setStyle(new NotificationCompat.BigTextStyle()
      .bigText(msg))
      .setContentText(msg);

    mBuilder.setContentIntent(contentIntent);
    mNotificationManager.notify(
      NOTIFICATION_ID, mBuilder.build());
  }
}
Logo abaixo, criei uma classe utilitária para realizar as operações com o GCM. O método checkPlayService verifica se o google play está instalado, atualizado e se existe uma conta google criada no aparelho. Já o método getRegistrationId obtém o chave de acesso que o GCM retornou e que no nosso exemplo, estamos armazenando em uma SharedPreference. Isso está sendo feito no método storeRegistrationId.
O método registerInBackground tenta obter a chave de acesso do GCM usando uma AsyncTask. Ou seja, em uma Thread separada. Quando obtermos essa chave, a enviamos para nossa aplicação web e salvamos na SharedPreference.
Nesse exemplo, estamos enviando essa chave via requisição GET para o servidor. Obviamente, isso deve ser melhorado em uma aplicação de produção.
public class GcmHelper {

  public static final String 
    EXTRA_MESSAGE = "message";
  public static final String 
    PROPERTY_REG_ID = "registration_id";
    
  private static final String 
    PROPERTY_APP_VERSION = "appVersion";
  private static final String 
    SENDER_ID = "ID_DA_SUA_APP";
  private static final String 
    IP = "IP_DO_SERVIDOR";
 
  public static boolean checkPlayServices(
    Activity activity, int requestCode) {

    int resultCode = GooglePlayServicesUtil
       .isGooglePlayServicesAvailable(activity);

    if (resultCode != ConnectionResult.SUCCESS) {
      if (GooglePlayServicesUtil
          .isUserRecoverableError(resultCode)) {

        GooglePlayServicesUtil.getErrorDialog(
          resultCode, activity, requestCode).show();

      } else {
        Toast.makeText(activity, 
            "Dispositivo não suportado.", 
            Toast.LENGTH_SHORT).show();
        activity.finish();
      }
      return false;
    }
    return true;
  }
 
  public static String getRegistrationId(
    Context context) {

    final SharedPreferences prefs = 
      getGCMPreferences(context);

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

    if (registrationId.isEmpty()) {
      return "";
    }
    int registeredVersion = 
      prefs.getInt(PROPERTY_APP_VERSION, 
        Integer.MIN_VALUE);

    int currentVersion = getAppVersion(context);
    if (registeredVersion != currentVersion) {
      return "";
    }
    return registrationId;
  }
 
  private static void storeRegistrationId(
    Context context, String regId) {

    final SharedPreferences prefs = 
      getGCMPreferences(context);

    int appVersion = getAppVersion(context);

    SharedPreferences.Editor editor = prefs.edit();
    editor.putString(PROPERTY_REG_ID, regId);
    editor.putInt(PROPERTY_APP_VERSION, appVersion);
    editor.commit();
  }

  public static void registerInBackground(
    final Context ctx, 
    final GoogleCloudMessaging gcm,
    final RegisterListener listener) {
  
    new AsyncTask<Void, Void, String>() {
      @Override
      protected String doInBackground(
        Void... params) {

        String regid = null;
        try {
          regid = gcm.register(SENDER_ID);

          sendRegistrationIdToBackend(regid);

          storeRegistrationId(ctx, regid);
                 
        } catch (IOException ex) {
          ex.printStackTrace();
        }
        return regid;
      }

      @Override
      protected void onPostExecute(String msg) {
        listener.onRegisterComplete(msg);
      }
    }.execute();
  }
 
  private static void sendRegistrationIdToBackend(
    String key) throws IOException {

    URL url = new URL(
      "http://"+ IP +"/SeuServer/registrar?token="+ key);

    HttpURLConnection conexao = 
      (HttpURLConnection)url.openConnection();
    conexao.connect();
  }
 
  private static SharedPreferences 
    getGCMPreferences(Context context) {

    return context.getSharedPreferences(
      "GDE", Context.MODE_PRIVATE);
  }
 
  private static int getAppVersion(Context context) {
    try {
      PackageInfo packageInfo = 
        context.getPackageManager()
          .getPackageInfo(context.getPackageName(), 0);
      return packageInfo.versionCode;
         
    } catch (NameNotFoundException e) {
      throw new RuntimeException(e);
    }
  }

  public interface RegisterListener {
    void onRegisterComplete(String s);
  }
}
A Activity né bem simples. Ela checa se o device suporta o Google Play (e se está atualizado), tenta obter a chave de acesso do GCM e depois a envia para o servidor.
public class ClientActivity extends Activity 
  implements RegisterListener {

  private static final int REQUEST_CODE_GOOGLEPLAY = 0;
 
  GoogleCloudMessaging gcm;
 
  String regid;
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_client);
  
    if (GcmHelper.checkPlayServices(
        this, REQUEST_CODE_GOOGLEPLAY)) {

      gcm = GoogleCloudMessaging.getInstance(this);
      regid = GcmHelper.getRegistrationId(this);

      if (regid.isEmpty()) {
        GcmHelper.registerInBackground(this, gcm, this);
      }
    } else {
      Log.i("NGVL", "Google Play não instalado.");
    }
  }
  
  @Override
  public void onRegisterComplete(String s) {
    Toast.makeText(this, "Registered: "+ s, 
      Toast.LENGTH_LONG).show();
  }
}

Aplicação Servidora

Você pode usar qualquer tecnologia do lado do servidor para usar o GCM. Aqui vou usar um Servlet Java para receber o token do cliente Android e enviar mensagens para o mesmo. Aqui só estou salvando um device. Você terá que fazer a sua lógica para salvar vários.

@WebServlet("/gcm")
public class GcmServlet extends HttpServlet {

  private static final long serialVersionUID = 1L;

  protected void doGet(
    HttpServletRequest request,
    HttpServletResponse response) 
      throws ServletException, IOException {

    PrintWriter writer = response.getWriter();
    writer.print("Current key:"+ retrieveToken());
  
    String key = request.getParameter("token");
    if (key != null && !key.isEmpty()){
      saveToken(key);
      response.getWriter().print("\nRegistrado:"+ key);
    }
  }

  protected void doPost(
    HttpServletRequest request,
    HttpServletResponse response) 
      throws ServletException, IOException {
  
    String msgJson = 
      "{ \"data\": { \"mensagem\":\""+ 
      request.getParameter("msg") +"\"}, "+
      "\"registration_ids\": [ \""+ retrieveToken() +
      "\" ] }";

    byte[] data = msgJson.getBytes("UTF-8");
  
    URL url = new URL(
      "https://android.googleapis.com/gcm/send");

    HttpsURLConnection conexao = 
      (HttpsURLConnection)url.openConnection();

    conexao.addRequestProperty("Authorization", 
      "key="+"sua_chave_web_aqui");
    conexao.addRequestProperty(
      "Content-Type", "application/json");
    conexao.setRequestMethod("POST");
    conexao.setDoOutput(true);
    conexao.setUseCaches(false);
    conexao.connect();
  
    OutputStream os = conexao.getOutputStream();
    os.write(data);
    os.close();
  
    PrintWriter writer = response.getWriter();
    writer.print("Trying to send: "+ msgJson);
  
    if (conexao.getResponseCode() == 
      HttpURLConnection.HTTP_OK){

      writer.print("\nDONE");
    } else {
      writer.print(
        "\nERRO:"+ conexao.getResponseCode() +" - "+ 
      conexao.getResponseMessage());
    }
  }

  private void saveToken(String key)
    throws IOException {

    String serverDir = 
      getServletContext().getRealPath("/");

    File f = new File(serverDir, "devices");
    if (!f.exists())
      f.createNewFile();

    FileOutputStream fos = new FileOutputStream(f);
    BufferedWriter out = new BufferedWriter(
      new FileWriter(f, true));
    out.write(key);

    out.close();
    fos.close();
  }
 
  private String retrieveToken() throws IOException{
    String serverDir = 
      getServletContext().getRealPath("/");

    File f = new File(serverDir, "devices");
    if (f.exists()){
      InputStream is = new FileInputStream(f);
      BufferedReader rd = new BufferedReader(
        new InputStreamReader(is));
   
      String line = rd.readLine();
      rd.close();
      is.close();
      return line;
    }
    return null;
  }
}
Para fins de exemplo, o método GET desse servidor está recebendo o token do cliente. Aqui só estou armazenando 1 única chave em um arquivo TXT, mas na prática, devemos armazenar isso em um banco de dados. Para enviar uma mensagem para o Android, basta abrir a página abaixo, digitar a mensagem e clicar em enviar.
<html>
<head>
<title>Exemplo GCM</title>
</head>
<body>
<form method="post" action="gcm">
  Mensagem:<input type="text" name="msg"><br>
  <input type="submit" value="OK">
</form>
</body>
</html>

Qualquer dúvida, deixem seus comentários.

4br4ç05,
nglauber

10 comentários:

Álvaro Lucas disse...

Tudo bem Nelson?

Seu post me tirou muitas dúvidas e me trouxeram outras: no caso de um aplicativo, um chat para Android em que haveria troca de mensagens entre os dispositivos e não entre um dispositivo e uma aplicação web, o GCM suportaria? Como ficaria a questão das chaves?

Desde já obrigado por compartilhar o conhecimento e parabéns pelo post.

Nelson Glauber de Vasconcelos Leal disse...

Oi Álvaro,

A arquitetura do GCM é dessa forma, então se você quisesse fazer uma troca de mensagens entre dispositivos, teria que ter um intermediário, ou seja, uma aplicação web intermediando.
Mas se você quiser troca direta entre os dispositivos, você pode pensar em bluetooth ou wi-fi direct. Mas ambos são limitados pela distância.
Uma outra opção (que mais uma vez precisaria de um servidor) seria usar WebSocket.

4br4ç05,
nglauber

Unknown disse...

Excelente tutorial. Ajudou muito e friso que funciona.

Alexsandro GC. disse...

Fiquei com uma dúvida, me corrija se eu estiver errado. Eu vi o código do lado do servidor, minha dúvida é, o json que você criou e colocou na variável 'msgJson', não está servindo para nada, pois você não adiciona ela na conexão.

Nelson Glauber de Vasconcelos Leal disse...

Oi Alexsandro,

Estou usando sim... Eu pego os bytes dele.
byte[] data = msgJson.getBytes("UTF-8");

E envio pelo outputStream...
os.write(data);

4br4ç05,
nglauber

Lucas Dolci disse...

Estou em dúvida com os dados abaixo:

EXTRA_MESSAGE = "message";
PROPERTY_REG_ID = "registration_id";
PROPERTY_APP_VERSION = "appVersion";
SENDER_ID = "ID_DA_SUA_APP";

Pode me explicar cada uma delas? Preciso substituir por quais informações. Fico grato.

Nelson Glauber disse...

Oi Lucas,

Você precisa alterar apenas o SENDER_ID com a chave gerada no Google Developers Console.

4br4ç05,
nglauber

Mychelle disse...

Olá Glauber, muito bom o seu post.

Eu tenho uma dúvida e gostaria de saber se a tag google_play_services_version no seu xml, recebe como valor a chave que eu gerei para o android?

Nelson Glauber disse...
Este comentário foi removido pelo autor.
Nelson Glauber disse...

Oi Mychelle,

É para ele ficar assim:

<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>

4br4ç05,
nglauber