Android Chat with Google GCM XMPP

This Android tutorial is to walk you through create an Android chat application using Google Cloud Messaging (GCM) using its Google Cloud Connection Server (CSS) via XMPP. Using Google CCS we can send upstream messages from an Android device to another and we will be using that feature primarily to do this chat application.

If you are new to Google Cloud Messaging (GCM), then you need to check a previous introductory tutorial ā€œGoogle Cloud Messaging GCM for Android and Push Notificationsā€. This tutorial will help to understand the basics, setup the prerequisite and start this wonderful GCM journey.

Making of GCM Chat Application

When we put together all the above, we get a nice Android chat application. I have already written detailed tutorials on all the above topics. In this tutorial, we will see how to wire them together as a nice little chat application.

Android Splash Screen

Google-GCM-Chat-Splash-Screen

Above shown screen shot is the splash screen of the chat app we are going to develop. If you want to know how to create this splash screen refer the tutorial Android splash screen.

Simple Android ListView

To list the logged in and available users to chat, we can use a simple list view using a layout already available as part of sdk. Recently I wrote a tutorial for Android ListView and it is a basic introductory tutorial.

Chat Conversation with ListView and Custom Layout

Chat activity is the interesting page. Most of the chat applications follow the same design. Conversations alternately displayed opposite to each other on a bubble background. Have a look at the Android ListView Custom Layout Tutorial and it will help create the layout and date model using the custom adapter.

Android Chat Bubble

We need to display the conversation on a bubble background. The bubble background image should scale according to the foreground text. We should design a ninepatch image and use it as a background for the view. Refer the Android Chat Bubble tutorial which gives detailed explanation on this.

Google-GCM-Chat

GCM upstream communication with CCS via XMPP

As part of the Google Cloud Messaging service, we have support for XMPP. Using this we can have persistent asynchronous and bidirectional communication. Google CCS server relays the messages between our XMPP server and Android device back and forth. In this tutorial example application, I have not completely leveraged the XMPP features. This is just a starting point and should build on top of this. Refer the previous tutorial on Google Cloud Messaging GCM-CCS with XMPP to understand the fundamentals, communication flow and sequence of events.

GCM-Chat-Login-Activity

XMPP Chat Server Application

We need to have a XMPP chat server application as our backend. We will use the Java SmackClient API as XMMP wrapper framework. Our server application is a command line based Java program, this is heavily borrowed from the Google example and customized to suit our need.

/*
 * Most part of this class is copyright Google.
 * It is from https://developer.android.com/google/gcm/ccs.html
 */

import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.PacketInterceptor;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.DefaultPacketExtension;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.provider.PacketExtensionProvider;
import org.jivesoftware.smack.provider.ProviderManager;
import org.jivesoftware.smack.util.StringUtils;
import org.json.simple.JSONValue;
import org.json.simple.parser.ParseException;
import org.xmlpull.v1.XmlPullParser;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.io.*;
import javax.net.ssl.SSLSocketFactory;

/**
 * Sample Smack implementation of a client for GCM Cloud Connection Server.
 *
 * For illustration purposes only.
 */
public class SmackCcsClient {

	static final String REG_ID_STORE = "gcmchat.txt";	

	static final String MESSAGE_KEY = "SM";
	Logger logger = Logger.getLogger("SmackCcsClient");

	public static final String GCM_SERVER = "gcm.googleapis.com";
	public static final int GCM_PORT = 5235;

	public static final String GCM_ELEMENT_NAME = "gcm";
	public static final String GCM_NAMESPACE = "google:mobile:data";

	static Random random = new Random();
	XMPPConnection connection;
	ConnectionConfiguration config;

	/**
	 * XMPP Packet Extension for GCM Cloud Connection Server.
	 */
	class GcmPacketExtension extends DefaultPacketExtension {
		String json;

		public GcmPacketExtension(String json) {
			super(GCM_ELEMENT_NAME, GCM_NAMESPACE);
			this.json = json;
		}

		public String getJson() {
			return json;
		}

		@Override
		public String toXML() {
			return String.format("<%s xmlns=\"%s\">%s</%s>", GCM_ELEMENT_NAME,
					GCM_NAMESPACE, json, GCM_ELEMENT_NAME);
		}

		@SuppressWarnings("unused")
		public Packet toPacket() {
			return new Message() {
				// Must override toXML() because it includes a <body>
				@Override
				public String toXML() {

					StringBuilder buf = new StringBuilder();
					buf.append("<message");
					if (getXmlns() != null) {
						buf.append(" xmlns=\"").append(getXmlns()).append("\"");
					}
					if (getLanguage() != null) {
						buf.append(" xml:lang=\"").append(getLanguage())
								.append("\"");
					}
					if (getPacketID() != null) {
						buf.append(" id=\"").append(getPacketID()).append("\"");
					}
					if (getTo() != null) {
						buf.append(" to=\"")
								.append(StringUtils.escapeForXML(getTo()))
								.append("\"");
					}
					if (getFrom() != null) {
						buf.append(" from=\"")
								.append(StringUtils.escapeForXML(getFrom()))
								.append("\"");
					}
					buf.append(">");
					buf.append(GcmPacketExtension.this.toXML());
					buf.append("</message>");
					return buf.toString();
				}
			};
		}
	}

	public SmackCcsClient() {
		// Add GcmPacketExtension
		ProviderManager.getInstance().addExtensionProvider(GCM_ELEMENT_NAME,
				GCM_NAMESPACE, new PacketExtensionProvider() {

					@Override
					public PacketExtension parseExtension(XmlPullParser parser)
							throws Exception {
						String json = parser.nextText();
						GcmPacketExtension packet = new GcmPacketExtension(json);
						return packet;
					}
				});
	}

	/**
	 * Returns a random message id to uniquely identify a message.
	 *
	 * <p>
	 * Note: This is generated by a pseudo random number generator for
	 * illustration purpose, and is not guaranteed to be unique.
	 *
	 */
	public String getRandomMessageId() {
		return "m-" + Long.toString(random.nextLong());
	}

	/**
	 * Sends a downstream GCM message.
	 */
	public void send(String jsonRequest) {
		Packet request = new GcmPacketExtension(jsonRequest).toPacket();
		connection.sendPacket(request);
	}

	/**
	 * Handles an upstream data message from a device application.
	 *
	 * <p>
	 * This sample echo server sends an echo message back to the device.
	 * Subclasses should override this method to process an upstream message.
	 */
	public void handleIncomingDataMessage(Map<String, Object> jsonObject) {

		String from = jsonObject.get("from").toString();

		// PackageName of the application that sent this message.
		String category = jsonObject.get("category").toString();

		// Use the packageName as the collapseKey in the echo packet
		String collapseKey = "echo:CollapseKey";
		@SuppressWarnings("unchecked")
		Map<String, String> payload = (Map<String, String>) jsonObject
				.get("data");

		String action = payload.get("ACTION");

		if ("ECHO".equals(action)) {

			String clientMessage = payload.get("CLIENT_MESSAGE");
			payload.put(MESSAGE_KEY, "ECHO: " + clientMessage);

			// Send an ECHO response back
			String echo = createJsonMessage(from, getRandomMessageId(),
					payload, collapseKey, null, false);
			send(echo);
		} else if ("SIGNUP".equals(action)) {
			try {
				String userName = payload.get("USER_NAME");
				writeToFile(userName, from);
			} catch (IOException e) {
				e.printStackTrace();
			}
		} else if ("USERLIST".equals(action)) {
		
			Map<String, String> regIdMap = readFromFile();
			String users = "";
			for (Map.Entry<String, String> entry : regIdMap.entrySet()) {
				users = users + entry.getKey() + ":";
			}
			payload.put(MESSAGE_KEY, "USERLIST");		
			payload.put("USERLIST",users);

			String message = createJsonMessage(from, getRandomMessageId(),
					payload, collapseKey, null, false);
			send(message);
		} else if ("CHAT".equals(action)) {
		
			Map<String, String> regIdMap = readFromFile();
			payload.put(MESSAGE_KEY, "CHAT");			
			String toUser = payload.get("TOUSER");
			String toUserRegid = regIdMap.get(toUser);
			
			String message = createJsonMessage(toUserRegid, getRandomMessageId(),
					payload, collapseKey, null, false);
			send(message);
		} 

	}

	/**
	 * Handles an ACK.
	 *
	 * <p>
	 * By default, it only logs a INFO message, but subclasses could override it
	 * to properly handle ACKS.
	 */
	public void handleAckReceipt(Map<String, Object> jsonObject) {
		String messageId = jsonObject.get("message_id").toString();
		String from = jsonObject.get("from").toString();
		logger.log(Level.INFO, "handleAckReceipt() from: " + from
				+ ", messageId: " + messageId);
	}

	/**
	 * Handles a NACK.
	 *
	 * <p>
	 * By default, it only logs a INFO message, but subclasses could override it
	 * to properly handle NACKS.
	 */
	public void handleNackReceipt(Map<String, Object> jsonObject) {
		String messageId = jsonObject.get("message_id").toString();
		String from = jsonObject.get("from").toString();
		logger.log(Level.INFO, "handleNackReceipt() from: " + from
				+ ", messageId: " + messageId);
	}

	/**
	 * Creates a JSON encoded GCM message.
	 *
	 * @param to
	 *            RegistrationId of the target device (Required).
	 * @param messageId
	 *            Unique messageId for which CCS will send an "ack/nack"
	 *            (Required).
	 * @param payload
	 *            Message content intended for the application. (Optional).
	 * @param collapseKey
	 *            GCM collapse_key parameter (Optional).
	 * @param timeToLive
	 *            GCM time_to_live parameter (Optional).
	 * @param delayWhileIdle
	 *            GCM delay_while_idle parameter (Optional).
	 * @return JSON encoded GCM message.
	 */
	public static String createJsonMessage(String to, String messageId,
			Map<String, String> payload, String collapseKey, Long timeToLive,
			Boolean delayWhileIdle) {
		Map<String, Object> message = new HashMap<String, Object>();
		message.put("to", to);
		if (collapseKey != null) {
			message.put("collapse_key", collapseKey);
		}
		if (timeToLive != null) {
			message.put("time_to_live", timeToLive);
		}
		if (delayWhileIdle != null && delayWhileIdle) {
			message.put("delay_while_idle", true);
		}
		message.put("message_id", messageId);
		message.put("data", payload);
		return JSONValue.toJSONString(message);
	}

	/**
	 * Creates a JSON encoded ACK message for an upstream message received from
	 * an application.
	 *
	 * @param to
	 *            RegistrationId of the device who sent the upstream message.
	 * @param messageId
	 *            messageId of the upstream message to be acknowledged to CCS.
	 * @return JSON encoded ack.
	 */
	public static String createJsonAck(String to, String messageId) {
		Map<String, Object> message = new HashMap<String, Object>();
		message.put("message_type", "ack");
		message.put("to", to);
		message.put("message_id", messageId);
		return JSONValue.toJSONString(message);
	}

	/**
	 * Connects to GCM Cloud Connection Server using the supplied credentials.
	 *
	 * @param username
	 *            GCM_SENDER_ID@gcm.googleapis.com
	 * @param password
	 *            API Key
	 * @throws XMPPException
	 */
	public void connect(String username, String password) throws XMPPException {
		config = new ConnectionConfiguration(GCM_SERVER, GCM_PORT);
		config.setSecurityMode(SecurityMode.enabled);
		config.setReconnectionAllowed(true);
		config.setRosterLoadedAtLogin(false);
		config.setSendPresence(false);
		config.setSocketFactory(SSLSocketFactory.getDefault());

		// NOTE: Set to true to launch a window with information about packets
		// sent and received
		config.setDebuggerEnabled(true);

		// -Dsmack.debugEnabled=true
		XMPPConnection.DEBUG_ENABLED = true;

		connection = new XMPPConnection(config);
		connection.connect();

		connection.addConnectionListener(new ConnectionListener() {

			@Override
			public void reconnectionSuccessful() {
				logger.info("Reconnecting..");
			}

			@Override
			public void reconnectionFailed(Exception e) {
				logger.log(Level.INFO, "Reconnection failed.. ", e);
			}

			@Override
			public void reconnectingIn(int seconds) {
				logger.log(Level.INFO, "Reconnecting in %d secs", seconds);
			}

			@Override
			public void connectionClosedOnError(Exception e) {
				logger.log(Level.INFO, "Connection closed on error.");
			}

			@Override
			public void connectionClosed() {
				logger.info("Connection closed.");
			}
		});

		// Handle incoming packets
		connection.addPacketListener(new PacketListener() {

			@Override
			public void processPacket(Packet packet) {
				logger.log(Level.INFO, "Received: " + packet.toXML());
				Message incomingMessage = (Message) packet;
				GcmPacketExtension gcmPacket = (GcmPacketExtension) incomingMessage
						.getExtension(GCM_NAMESPACE);
				String json = gcmPacket.getJson();
				try {
					@SuppressWarnings("unchecked")
					Map<String, Object> jsonObject = (Map<String, Object>) JSONValue
							.parseWithException(json);

					// present for "ack"/"nack", null otherwise
					Object messageType = jsonObject.get("message_type");

					if (messageType == null) {
						// Normal upstream data message
						handleIncomingDataMessage(jsonObject);

						// Send ACK to CCS
						String messageId = jsonObject.get("message_id")
								.toString();
						String from = jsonObject.get("from").toString();
						String ack = createJsonAck(from, messageId);
						send(ack);
					} else if ("ack".equals(messageType.toString())) {
						// Process Ack
						handleAckReceipt(jsonObject);
					} else if ("nack".equals(messageType.toString())) {
						// Process Nack
						handleNackReceipt(jsonObject);
					} else {
						logger.log(Level.WARNING,
								"Unrecognized message type (%s)",
								messageType.toString());
					}
				} catch (ParseException e) {
					logger.log(Level.SEVERE, "Error parsing JSON " + json, e);
				} catch (Exception e) {
					logger.log(Level.SEVERE, "Couldn't send echo.", e);
				}
			}
		}, new PacketTypeFilter(Message.class));

		// Log all outgoing packets
		connection.addPacketInterceptor(new PacketInterceptor() {
			@Override
			public void interceptPacket(Packet packet) {
				logger.log(Level.INFO, "Sent: {0}", packet.toXML());
			}
		}, new PacketTypeFilter(Message.class));

		connection.login(username, password);
	}

	public void writeToFile(String name, String regId) throws IOException {
		Map<String, String> regIdMap = readFromFile();
		regIdMap.put(name, regId);
		PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(
				REG_ID_STORE, false)));
		for (Map.Entry<String, String> entry : regIdMap.entrySet()) {
			out.println(entry.getKey() + "," + entry.getValue());
		}
		out.println(name + "," + regId);
		out.close();

	}

	public Map<String, String> readFromFile() {
	Map<String, String> regIdMap = null;
	try {
		BufferedReader br = new BufferedReader(new FileReader(REG_ID_STORE));
		String regIdLine = "";
		regIdMap = new HashMap<String, String>();
		while ((regIdLine = br.readLine()) != null) {
			String[] regArr = regIdLine.split(",");
			regIdMap.put(regArr[0], regArr[1]);
		}
		br.close();
	} catch(IOException ioe) {
	}
		return regIdMap;
	}
	
 public static void main(String [] args) {
    final String userName = "512218038480" + "@gcm.googleapis.com";
    final String password = "AIzaSyA9DQTcggUtfqOG9lnV_Xb5VEQ8iKBEaP4";

    SmackCcsClient ccsClient = new SmackCcsClient();

    try {
      ccsClient.connect(userName, password);
    } catch (XMPPException e) {
      e.printStackTrace();
    }
}
}
COMPILE: javac -cp *;.; SmackCcsClient.java
RUN: java -cp *;.; SmackCcsClient

Google-GCM-XMPP-Chat-Server

Download the XMMP Server application XMPP Chat Server

Android Chat Application based on Google GCM

I am going to show only the key classes below in the source code listing. For the complete source, download the whole project source using the link give below.

ChatActivity.java

package com.javapapers.android.gcm.chat;

import java.util.Random;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.DataSetObserver;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnKeyListener;
import android.widget.AbsListView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;

import com.google.android.gms.gcm.GoogleCloudMessaging;

public class ChatActivity extends Activity {
    private static final String TAG = "ChatActivity";

    private ChatArrayAdapter chatArrayAdapter;
	private ListView listView;
	private EditText chatText;
    private Button buttonSend;

    GoogleCloudMessaging gcm;
    Intent intent;

	private static Random random;
    private String toUserName;
    MessageSender messageSender;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
        Intent i = getIntent();
        toUserName = i.getStringExtra("TOUSER");
		setContentView(R.layout.activity_chat);

        buttonSend = (Button) findViewById(R.id.buttonSend);
        intent = new Intent(this, GCMNotificationIntentService.class);
        registerReceiver(broadcastReceiver, new IntentFilter("com.javapapers.android.gcm.chat.chatmessage"));

		random = new Random();
        messageSender = new MessageSender();
		listView = (ListView) findViewById(R.id.listView1);
        gcm = GoogleCloudMessaging.getInstance(getApplicationContext());

		chatArrayAdapter = new ChatArrayAdapter(getApplicationContext(), R.layout.activity_chat_singlemessage);
		listView.setAdapter(chatArrayAdapter);

        chatText = (EditText) findViewById(R.id.chatText);
        chatText.setOnKeyListener(new OnKeyListener() {
			public boolean onKey(View v, int keyCode, KeyEvent event) {
                if ((event.getAction() == KeyEvent.ACTION_DOWN) && (keyCode == KeyEvent.KEYCODE_ENTER)) {
                  return sendChatMessage();
                }
                return false;
			}
		});
        buttonSend.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View arg0) {
                sendChatMessage();
            }
        });

        listView.setTranscriptMode(AbsListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);
        listView.setAdapter(chatArrayAdapter);

        chatArrayAdapter.registerDataSetObserver(new DataSetObserver() {
            @Override
            public void onChanged() {
                super.onChanged();
                listView.setSelection(chatArrayAdapter.getCount() - 1);
            }
        });
	}

    private boolean sendChatMessage(){
        //sending gcm message to the paired device
        Bundle dataBundle = new Bundle();
        dataBundle.putString("ACTION", "CHAT");
        dataBundle.putString("TOUSER", toUserName);
        dataBundle.putString("CHATMESSAGE", chatText.getText().toString());
        messageSender.sendMessage(dataBundle,gcm);

        //updating the current device
        chatArrayAdapter.add(new ChatMessage(false, chatText.getText().toString()));
        chatText.setText("");
        return true;
    }


    private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d(TAG, "onReceive: " + intent.getStringExtra("CHATMESSAGE"));
            chatArrayAdapter.add(new ChatMessage(true, intent.getStringExtra("CHATMESSAGE")));
        }
    };
}

ChatArrayAdapter.java

package com.javapapers.android.gcm.chat;

import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.TextView;

public class ChatArrayAdapter extends ArrayAdapter {

	private TextView chatText;
	private List chatMessageList = new ArrayList();
	private LinearLayout singleMessageContainer;

	@Override
	public void add(ChatMessage object) {
		chatMessageList.add(object);
		super.add(object);
	}

	public ChatArrayAdapter(Context context, int textViewResourceId) {
		super(context, textViewResourceId);
	}

	public int getCount() {
		return this.chatMessageList.size();
	}

	public ChatMessage getItem(int index) {
		return this.chatMessageList.get(index);
	}

	public View getView(int position, View convertView, ViewGroup parent) {
		View row = convertView;
		if (row == null) {
			LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
			row = inflater.inflate(R.layout.activity_chat_singlemessage, parent, false);
		}
		singleMessageContainer = (LinearLayout) row.findViewById(R.id.singleMessageContainer);
		ChatMessage chatMessageObj = getItem(position);
		chatText = (TextView) row.findViewById(R.id.singleMessage);
		chatText.setText(chatMessageObj.message);
		chatText.setBackgroundResource(chatMessageObj.left ? R.drawable.bubble_a : R.drawable.bubble_b);
		singleMessageContainer.setGravity(chatMessageObj.left ? Gravity.LEFT : Gravity.RIGHT);
		return row;
	}

	public Bitmap decodeToBitmap(byte[] decodedByte) {
		return BitmapFactory.decodeByteArray(decodedByte, 0, decodedByte.length);
	}

}

ChatMessage.java

package com.javapapers.android.gcm.chat;

public class ChatMessage {
	public boolean left;
	public String message;

	public ChatMessage(boolean left, String message) {
		super();
		this.left = left;
		this.message = message;
	}
}

GcmBroadcastReceiver.java

package com.javapapers.android.gcm.chat;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.support.v4.content.WakefulBroadcastReceiver;
import android.util.Log;

/**
 * Created by Joe on 5/28/2014.
 */
public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d("GcmBroadcastReceiver",
                "onReceive: notification received.");
        ComponentName comp = new ComponentName(context.getPackageName(),
                GCMNotificationIntentService.class.getName());
        startWakefulService(context, (intent.setComponent(comp)));
        setResultCode(Activity.RESULT_OK);
    }
}

GCMNotificationIntentService.java

package com.javapapers.android.gcm.chat;

import android.app.IntentService;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import com.google.android.gms.gcm.GoogleCloudMessaging;

/**
 * Created by Joe on 5/28/2014.
 */
public class GCMNotificationIntentService extends IntentService {
    public static final int NOTIFICATION_ID = 1;
    private NotificationManager mNotificationManager;
    NotificationCompat.Builder builder;

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

    public static final String TAG = "GCMNotificationIntentService";

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.d(TAG, "onHandleIntent "+intent.getDataString());
        Bundle extras = intent.getExtras();
        GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(this);

        String messageType = gcm.getMessageType(intent);

        if (extras != null) {
            if (!extras.isEmpty()) {
                if (GoogleCloudMessaging.MESSAGE_TYPE_SEND_ERROR
                        .equals(messageType)) {
                    sendNotification("Send error: " + extras.toString());
                } else if (GoogleCloudMessaging.MESSAGE_TYPE_DELETED
                        .equals(messageType)) {
                    sendNotification("Deleted messages on server: "
                            + extras.toString());
                } else if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE
                        .equals(messageType)) {

                    if("USERLIST".equals(extras.get("SM"))){
                        Log.d(TAG, "onHandleIntent - USERLIST ");
                        //update the userlist view
                        Intent userListIntent = new Intent("com.javapapers.android.gcm.chat.userlist");
                        String userList = extras.get("USERLIST").toString();
                        userListIntent.putExtra("USERLIST",userList);
                        sendBroadcast(userListIntent);
                    } else if("CHAT".equals(extras.get("SM"))){
                        Log.d(TAG, "onHandleIntent - CHAT ");
                        Intent chatIntent = new Intent("com.javapapers.android.gcm.chat.chatmessage");
                        chatIntent.putExtra("CHATMESSAGE",extras.get("CHATMESSAGE").toString());
                        sendBroadcast(chatIntent);
                    }
                    Log.i(TAG, "SERVER_MESSAGE: " + extras.toString());

                }
            }
        }
        GcmBroadcastReceiver.completeWakefulIntent(intent);
    }

    private void sendNotification(String msg) {
        Log.d(TAG, "Preparing to send notification...: " + msg);
        mNotificationManager = (NotificationManager) this
                .getSystemService(Context.NOTIFICATION_SERVICE);

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

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

        mBuilder.setContentIntent(contentIntent);
        mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
        Log.d(TAG, "Notification sent successfully.");
    }
}

MessageSender.java

package com.javapapers.android.gcm.chat;

import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;

import com.google.android.gms.gcm.GoogleCloudMessaging;

import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by Joe on 6/1/2014.
 */
public class MessageSender {
    private static final String TAG = "MessageSender";
    AsyncTask sendTask;
    AtomicInteger ccsMsgId = new AtomicInteger();

    public void sendMessage(final Bundle data, final GoogleCloudMessaging gcm ) {

        sendTask = new AsyncTask() {
            @Override
            protected String doInBackground(Void... params) {

                String id = Integer.toString(ccsMsgId.incrementAndGet());

                try {
                    Log.d(TAG, "messageid: " + id);
                    gcm.send(Config.GOOGLE_PROJECT_ID + "@gcm.googleapis.com", id,
                            data);
                    Log.d(TAG, "After gcm.send successful.");
                } catch (IOException e) {
                    Log.d(TAG, "Exception: " + e);
                    e.printStackTrace();
                }
                return "Message ID: "+id+ " Sent.";
            }

            @Override
            protected void onPostExecute(String result) {
                sendTask = null;
                Log.d(TAG, "onPostExecute: result: " + result);
            }

        };
        sendTask.execute(null, null, null);
    }

}

UserListActivity.java

GCM-Chat-User-List

package com.javapapers.android.gcm.chat;

import android.app.ListActivity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.TextView;

import com.google.android.gms.gcm.GoogleCloudMessaging;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


public class UserListActivity extends ListActivity {
    private static final String TAG = "UserListActivity";
    TextView content;
    Button refreshButton;
    private Intent intent;
    MessageSender messageSender;
    GoogleCloudMessaging gcm;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user_list);
        content = (TextView)findViewById(R.id.output);
        content.setText("Select user to chat:");
        refreshButton = (Button)findViewById(R.id.refreshButton);
        intent = new Intent(this, GCMNotificationIntentService.class);
        registerReceiver(broadcastReceiver, new IntentFilter("com.javapapers.android.gcm.chat.userlist"));
        messageSender = new MessageSender();
        gcm = GoogleCloudMessaging.getInstance(getApplicationContext());
        refreshButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View arg0) {
                // get user list
                Bundle dataBundle = new Bundle();
                dataBundle.putString("ACTION", "USERLIST");
                messageSender.sendMessage(dataBundle, gcm);
            }
        });

    }

    private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d(TAG, "onReceive: " + intent.getStringExtra("USERLIST"));
            updateUI(intent.getStringExtra("USERLIST"));
        }
    };

    private void updateUI(String userList) {
        //get userlist from the intents and update the list

        String[] userListArr = userList.split(":");

        Log.d(TAG,"userListArr: "+userListArr.length+" tostr "+userListArr.toString());

        //remove empty strings :-)
        List list = new ArrayList();
        for(String s : userListArr) {
            if(s != null && s.length() > 0) {
                list.add(s);
            }
        }
        userListArr = list.toArray(new String[list.size()]);

        ArrayAdapter adapter = new ArrayAdapter(this,
                android.R.layout.simple_list_item_1, userListArr);
        setListAdapter(adapter);

    }

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {

        super.onListItemClick(l, v, position, id);

        // ListView Clicked item index
        int itemPosition     = position;

        // ListView Clicked item value
        String  itemValue    = (String) l.getItemAtPosition(position);

        content.setText("User selected: " +itemValue);


        Intent i = new Intent(getApplicationContext(),
                ChatActivity.class);
        i.putExtra("TOUSER",itemValue);
        startActivity(i);
        finish();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.user_list, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

}

SignUpActivity.java

package com.javapapers.android.gcm.chat;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.google.android.gms.gcm.GoogleCloudMessaging;

import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;


public class SignUpActivity extends ActionBarActivity {

    private static final String TAG = "SignUpActivity";
    public static final String REG_ID = "regId";
    private static final String APP_VERSION = "appVersion";
    Button buttonSignUp;
    Button buttonLogin;
    String regId;
    String signUpUser;
    AsyncTask sendTask;
    AtomicInteger ccsMsgId = new AtomicInteger();
    GoogleCloudMessaging gcm;
    Context context;
    private boolean signupFlag = false;
    MessageSender messageSender;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_sign_up);
        context = getApplicationContext();
        buttonSignUp = (Button) findViewById(R.id.ButtonSignUp);
        messageSender = new MessageSender();
        buttonSignUp.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View arg0) {

                //step 1: register with Google GCM server
                if (TextUtils.isEmpty(regId)) {
                    regId = registerGCM();
                    Log.d(TAG, "GCM RegId: " + regId);
                }

                //step 2: register with XMPP App Server
                if(!regId.isEmpty()) {
                    EditText mUserName = (EditText) findViewById(R.id.userName);
                    signUpUser = mUserName.getText().toString();
                    Bundle dataBundle = new Bundle();
                    dataBundle.putString("ACTION", "SIGNUP");
                    dataBundle.putString("USER_NAME", signUpUser);
                    messageSender.sendMessage(dataBundle,gcm);
                    signupFlag = true;
                    Toast.makeText(context,
                            "Sign Up Complete!",
                            Toast.LENGTH_LONG).show();
                } else {
                    Toast.makeText(context,
                            "Google GCM RegId Not Available!",
                            Toast.LENGTH_LONG).show();
                }
            }
        });

        buttonLogin = (Button) findViewById(R.id.ButtonLogin);
        buttonLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View arg0) {



                    //step 0: register with Google GCM server
                    if (TextUtils.isEmpty(regId)) {
                        regId = registerGCM();
                        Log.d(TAG, "GCM RegId: " + regId);
                    }

                    //step 1: user authentication

                    //step 2: get user list
                    Bundle dataBundle = new Bundle();
                    dataBundle.putString("ACTION", "USERLIST");
                    dataBundle.putString("USER_NAME", signUpUser);
                    messageSender.sendMessage(dataBundle,gcm);

                    Intent i = new Intent(context,
                            UserListActivity.class);
                                Log.d(TAG,
                            "onClick of login: Before starting userlist activity.");
                    startActivity(i);
                    finish();
                    Log.d(TAG, "onClick of Login: After finish.");

                }

        });

    }

    public String registerGCM() {

        gcm = GoogleCloudMessaging.getInstance(this);
        regId = getRegistrationId();

        if (TextUtils.isEmpty(regId)) {

            registerInBackground();

            Log.d(TAG,
                    "registerGCM - successfully registered with GCM server - regId: "
                            + regId);
        } else {
            Log.d(TAG,
                    "Regid already available: "
                            + regId
            );
        }
        return regId;
    }

    private String getRegistrationId() {
        final SharedPreferences prefs = getSharedPreferences(
                SignUpActivity.class.getSimpleName(), Context.MODE_PRIVATE);
        String registrationId = prefs.getString(REG_ID, "");
        if (registrationId.isEmpty()) {
            Log.i(TAG, "Registration not found.");
            return "";
        }
        int registeredVersion = prefs.getInt(APP_VERSION, Integer.MIN_VALUE);
        int currentVersion = getAppVersion();
        if (registeredVersion != currentVersion) {
            Log.i(TAG, "App version changed.");
            return "";
        }
        return registrationId;
    }

    private int getAppVersion() {
        try {
            PackageInfo packageInfo;
            packageInfo = context.getPackageManager()
                    .getPackageInfo(context.getPackageName(), 0);
            return packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            Log.d("RegisterActivity",
                    "I never expected this! Going down, going down!" + e);
            throw new RuntimeException(e);
        }
    }

    private void registerInBackground() {
        new AsyncTask() {
            @Override
            protected String doInBackground(Void... params) {
                String msg = "";
                try {
                    if (gcm == null) {
                        gcm = GoogleCloudMessaging.getInstance(context);
                    }
                    regId = gcm.register(Config.GOOGLE_PROJECT_ID);
                    Log.d("RegisterActivity", "registerInBackground - regId: "
                            + regId);
                    msg = "Device registered, registration ID=" + regId;
                    storeRegistrationId(regId);
                } catch (IOException ex) {
                    msg = "Error :" + ex.getMessage();
                    Log.d(TAG, "Error: " + msg);
                }
                Log.d(TAG, "AsyncTask completed: " + msg);
                return msg;
            }

            @Override
            protected void onPostExecute(String msg) {
                Log.d(TAG, "Registered with GCM Server." + msg);
            }
        }.execute(null, null, null);
    }

    private void storeRegistrationId(String regId) {
        final SharedPreferences prefs = getSharedPreferences(
                SignUpActivity.class.getSimpleName(), Context.MODE_PRIVATE);
        int appVersion = getAppVersion();
        Log.i(TAG, "Saving regId on app version " + appVersion);
        SharedPreferences.Editor editor = prefs.edit();
        editor.putString(REG_ID, regId);
        editor.putInt(APP_VERSION, appVersion);
        editor.commit();
    }



    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.sign_up, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

}

activity_chat.xml

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

    <ListView
        android:id="@+id/listView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="80dp">
    </ListView>

    <RelativeLayout
        android:id="@+id/form"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:orientation="vertical" >

        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:inputType="textMultiLine"
            android:ems="10"
            android:id="@+id/chatText"
            android:layout_alignParentBottom="true"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_toLeftOf="@+id/buttonSend" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Send"
            android:id="@+id/buttonSend"
            android:layout_alignBottom="@+id/chatText"
            android:layout_alignParentRight="true"
            android:layout_alignParentEnd="true" />
    </RelativeLayout>

</RelativeLayout>

activity_chat_singlemessage.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="wrap_content" >

    <LinearLayout
        android:id="@+id/singleMessageContainer"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" >

        <TextView
            android:id="@+id/singleMessage"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_margin="5dip"
            android:background="@drawable/bubble_b"
            android:paddingLeft="10dip"
            android:text="Hello bubbles!"
            android:textColor="@android:color/primary_text_light" />
    </LinearLayout>

</LinearLayout>

activity_user_list.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="com.javapapers.android.gcm.chat.UserListActivity">

    <TextView android:id="@+id/output"
        android:background="@color/BrightBlue"
        android:layout_height="wrap_content"
        android:padding="@dimen/abc_action_bar_icon_vertical_padding"
        android:textColor="#ffffff"
        android:text="Click : " android:layout_width="fill_parent" />
    <ListView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@android:id/list"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_marginTop="103dp" />

    <Button
        style="?android:attr/buttonStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Refresh"
        android:id="@+id/refreshButton"
        android:layout_below="@+id/output"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true" />
</RelativeLayout>

activity_chat_singlemessage.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="com.javapapers.android.gcm.chat.SignUpActivity">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_above="@+id/userName"
        android:layout_centerHorizontal="true"
        android:layout_margin="60dp"
        android:layout_marginBottom="74dp"
        android:contentDescription="Application Logo"
        android:src="@drawable/gcmchat" />

    <EditText
        android:id="@+id/userName"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:layout_marginBottom="20dp"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:singleLine="true"
        android:hint="@string/enter_username" />

    <Button
        android:id="@+id/ButtonLogin"
        style="?android:attr/borderlessButtonStyle"
        android:layout_width="120dp"
        android:layout_height="45dp"
        android:background="@color/BrightBlue"
        android:text="@string/login"
        android:layout_marginTop="37dp"
        android:layout_below="@+id/editText"
        android:layout_alignLeft="@+id/editText"
        android:layout_alignStart="@+id/editText" />

    <Button
        android:id="@+id/ButtonSignUp"
        style="?android:attr/borderlessButtonStyle"
        android:layout_width="120dp"
        android:layout_height="45dp"
        android:background="@color/BrightBlue"
        android:text="@string/signup"
        android:textColorHint="@color/White"
        android:layout_alignTop="@+id/ButtonLogin"
        android:layout_alignRight="@+id/userName"
        android:layout_alignEnd="@+id/userName" />

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:inputType="textPassword"
        android:ems="10"
        android:id="@+id/editText"
        android:layout_below="@+id/userName"
        android:layout_alignLeft="@+id/userName"
        android:layout_alignStart="@+id/userName"
        android:layout_alignRight="@+id/userName"
        android:layout_alignEnd="@+id/userName"
        android:hint="@string/enter_password" />

</RelativeLayout>

Download Android Chat Application Source

For the complete application source download Android GCM Chat Application Project Source

This Android tutorial was added on 17/06/2014.

Comments on "Android Chat with Google GCM XMPP" Tutorial:

  1. Firoz Shaikh says:

    Supperrb sir,
    Above heights…
    Super going…
    first you posted android GCM Tutorial, then Android Chat Bubble Tutorial, then List View Tutorial, and now all Concepts are together. i Like this series so much….
    Your Ideas of posting are always on best way, i am not learning only android or java from your blog, but also learning the way of posting, how you make all tutorials on nice series….
    Hats Off to you sir…

  2. Sundeep Y says:

    Sir .. Can you please start a Android tutorial series . I am really intrested in your articles .

  3. IMRAN says:

    nice work…where is server side code

  4. Christian Loayza says:

    That configuration should have the server side to install the SmackCcsClient
    It can be used in some hosting? which recommend use

  5. Unknown says:

    I am not able to Sign_up..while clicking Sign_up button..getting exception

  6. VarunMad says:

    Cool App,was looking for some ideas regarding the same topic and found the whole project.Thanx A Lot.Appreciate the website.

  7. Jack says:

    Is android apk based only on Google GCM or any other platform? I just need clarifiction. Thanks.

  8. Anirudh Rachit says:

    Sir,
    I am unable to get userlist and unable to refresh the list….
    i downloaded the source which you had given…
    But the tutorial was simply superb sir…

  9. Santhanam gunasekaran says:

    Hi joe,

    Thanks for the tutorial,

    This blog is only contains latest tutorial in internet for android chat app, thank for this additionally.

    i can understand the coding flow in client and XMPP server part, but i dont have any idea to Make XMPP server.

    whether i want to run SmackClass in comment prompt as you mentioned (if yes iā€™m getting no class exception for configuration) or can i keep any apache tomcat server. for future i want to customize server code for user online status options.

    Pls give your reply ASAP.

  10. khyati says:

    I am downloaded project source code but when i am import this project into eclipse there is shown only splash activity and in zip folder there is in bin folder apk file is not available why????

  11. YM says:

    Hey, In your ChatActivity.java. you have create a call to method: registerReceiver() . But nowhere in the source code, the defined method is to be found. Please explain.

  12. Jalal Zia says:

    there is a problem which i am facing in this project i have downloaded from your site that it gives an error in the style.xml
    error: Error retrieving parent for item: No resource found that matches the given name ‘Theme.AppCompat’.

  13. Santhanam gunasekaran says:

    hi, Jalal Zia

    you pls add appcompt7 library then try, it will support

    below library you must to add
    1.google play store library
    2.appcompt7 library

  14. Santhanam gunasekaran says:

    yes i understood the server flow.
    it’s working for me with apache tomcat server.
    thanks

  15. MonsterDeveloper says:

    Excellent tutorial.. But i have a small doubt in this.How to integrate server side to android.. There is no server url specified anywhere in the project..

  16. Kay says:

    Hi Joe, i have tried to follow your instruction to make a chat app.
    But , i wonder that where the device find out the user list.
    And why there is no server url specified .
    I hope you can reply me, because i am doing an assignment of school
    Thank you so much

Comments are closed for this "Android Chat with Google GCM XMPP" tutorial.