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