Monday, August 3, 2015

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

  • 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

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 tutorialAndroid 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 theAndroid 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>

No comments:

Post a Comment