is 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.
Making of GCM Chat Application
- A cool Splash screen (just for fun)
- A Simple Android List View (available chat users list screen)
- A List View wit Row Layout (chat conversation display)
- Chat Bubble
- GCM upstream communication with XMPP
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
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.
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.
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
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
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>